-
Notifications
You must be signed in to change notification settings - Fork 7
refactor(sender): refactor sender components for 0.4.0 version #283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
WalkthroughRefactors Sender into a composable-driven TipTap editor, adds Mention/Suggestion/Template extensions, adds sender-action components (action button, submit, clear, upload, voice, word counter), provides SenderCompat shim, many new composables/types/styles, and removes legacy editor components and hooks. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as Sender UI (Layouts + Actions)
participant Core as useSenderCore / Context
participant Editor as TipTap Editor
participant Ext as Extensions (Mention/Template)
participant Speech as SpeechHandler
participant Files as FileDialog
User->>UI: Type or interact
UI->>Core: setContent / invoke action
Core->>Editor: update document
Editor->>Ext: plugin state updates
Ext-->>Core: structured data / suggestions
Core-->>UI: expose state (hasContent, canSubmit)
User->>UI: Click Submit
UI->>Core: submit()
Core->>Editor: getTextWithTemplates / getMentions
Editor-->>Core: text + structuredData
Core-->>UI: emit submit(text, structuredData)
User->>UI: Click Voice Button
UI->>Speech: start()
Speech->>UI: interim/final transcripts
UI->>Editor: insert transcript
Editor->>Ext: update
sequenceDiagram
participant User
participant UI as UploadButton
participant Files as FileDialog
participant Core as useSenderCore
User->>UI: Click Upload
UI->>Files: open file dialog
Files-->>UI: selected files
UI->>UI: validate maxCount / maxSize
alt valid
UI->>Core: emit select(files)
else invalid
UI->>Core: emit error(Error, files?)
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📦 Package Previewpnpm add https://pkg.pr.new/opentiny/tiny-robot/@opentiny/tiny-robot@a38859f pnpm add https://pkg.pr.new/opentiny/tiny-robot/@opentiny/tiny-robot-kit@a38859f pnpm add https://pkg.pr.new/opentiny/tiny-robot/@opentiny/tiny-robot-svgs@a38859f commit: a38859f |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/components/src/sender-actions/voice-button/useSpeechHandler.ts (1)
74-82: Potential issues in restart logic.Two concerns with the restart flow:
The
setTimeoutcallback callshandler.start(callbacks)without the try-catch wrapper used at line 84-89, so startup errors during restart won't be caught.If the component unmounts during the 200ms delay, the
setTimeoutwill still fire, potentially causing errors or unexpected behavior since the handler was already cleaned up inonUnmounted.🔎 Proposed fix
+ let restartTimer: ReturnType<typeof setTimeout> | null = null + // 开始录音 const start = () => { // ...existing checks... // 如果正在录音,先停止再重新开始 if (speechState.isRecording) { handler.stop() speechState.isRecording = false // 短暂延迟后重新开始 - setTimeout(() => { - handler.start(callbacks) + restartTimer = setTimeout(() => { + restartTimer = null + try { + handler.start(callbacks) + } catch (error) { + speechState.error = error instanceof Error ? error : new Error('启动失败') + optionsRef.value.onError?.(speechState.error) + } }, 200) return } // ...rest of function... } // 组件卸载时清理资源 onUnmounted(() => { + if (restartTimer) { + clearTimeout(restartTimer) + restartTimer = null + } if (speechState.isRecording && handler) { handler.stop() speechState.isRecording = false } })
🤖 Fix all issues with AI agents
In @packages/components/src/sender-compat/index.vue:
- Around line 63-80: The computed defaultActions erroneously maps
props.buttonGroup.file to actions.clear; change the mapping so
props.buttonGroup.file populates a proper file/upload action (e.g., actions.file
or actions.upload) with its properties (disabled and tooltip) and keep
actions.clear reserved for clear/submit logic; update the computed
(defaultActions) to assign actions.file = { disabled:
props.buttonGroup.file.disabled, tooltip: props.buttonGroup.file.disabled ?
'无法上传' : '上传文件' } (or similar localized tooltip) instead of assigning to
actions.clear, and ensure any consumers of defaultActions use the new
actions.file/upload key.
In @packages/components/src/sender/extensions/template/select/plugins.ts:
- Around line 99-104: The deletion loop uses original positions so earlier
deletes shift later indices and cause incorrect removals; update the logic that
iterates over todoPositions (where tr.delete(pos, pos+1) is called) to process
positions in reverse order or adjust by a running offset—e.g., iterate from
todoPositions.length-1 down to 0, extract [, pos] for each entry and call tr =
tr.delete(pos, pos + 1), then return tr; this ensures each zero-width character
is removed using correct, non-shifted indices.
In @packages/components/src/sender/extensions/template/types.ts:
- Line 102: The type annotation for the items prop is wrong: `Ref` is declared
with two type parameters in `items?: TemplateItem[] | Ref<TemplateItem[],
TemplateItem[]>`; change it to a single-parameter Ref such as
`Ref<TemplateItem[]>` so the union becomes `items?: TemplateItem[] |
Ref<TemplateItem[]>`; update the import/use sites of `TemplateItem` and `Ref` if
needed to satisfy the corrected type.
- Around line 15-30: The SelectOption interface is duplicated; remove the local
interface declaration in this file and instead import the canonical SelectOption
type from the base types module (the existing interface named SelectOption in
the base types). Replace the local definition with an import (and re-export if
this module should expose it) and update any local usages to reference the
imported SelectOption to avoid type drift.
- Around line 1-10: The compat layer only maps the old 'template' variant to
'block' but ignores the sender TemplateItem variants 'text' and 'select'; update
the sender-compat conversion logic (the mapping function in the sender-compat
component that currently converts 'template' → 'block') to either map 'text' and
'select' to appropriate compat types (e.g., 'text' → 'text' or 'block' as
chosen, 'select' → 'select' or a handled fallback) or explicitly
throw/log/annotate unsupported types, and add a brief comment documenting the
intentional type differences between TemplateItem in sender and sender-compat
and which variants are supported or intentionally dropped.
🟡 Minor comments (16)
packages/components/src/sender/extensions/template/commands.ts-122-156 (1)
122-156:focusFirstTemplatereturns before async operation completes.The command returns
trueat line 155 before thesetTimeoutcallback executes. If an error occurs inside the callback (line 150-152), the caller already received a success indication. Additionally,blocksis captured before the timeout but the document may have changed by the time the callback runs.Consider either:
- Making this an async command if Tiptap supports it
- Documenting that success means "scheduled" not "completed"
- Re-fetching blocks inside the callback to ensure fresh data
🔎 Suggested improvement
focusFirstTemplate: () => ({ editor }: { editor: Editor }) => { - const blocks = getAllTemplates(editor) - // 使用 setTimeout 确保在文档更新后执行 setTimeout(() => { + const blocks = getAllTemplates(editor) const { state, view } = editor const tr = state.trpackages/components/src/sender/extensions/mention/components/mention-list.vue-84-86 (1)
84-86::keyuses optionalitem.idwhich may be undefined.Per
MentionItemtype,idis optional. Usingundefinedas a key can cause Vue warnings or unexpected re-renders. Consider a fallback.🔎 Proposed fix
- :key="item.id" + :key="item.id ?? `mention-${index}`"packages/components/src/sender/extensions/mention/components/mention-list.vue-31-32 (1)
31-32: Potential index of -1 when items array is empty.
Math.min(props.items.length - 1, ...)evaluates to-1whenitems.lengthis0, which could cause issues if ArrowDown is pressed on an empty list.🔎 Proposed fix
if (event.key === 'ArrowDown') { - selectedIndex.value = Math.min(props.items.length - 1, selectedIndex.value + 1) + if (props.items.length > 0) { + selectedIndex.value = Math.min(props.items.length - 1, selectedIndex.value + 1) + } scrollToSelected() return true }packages/components/src/sender/extensions/mention/components/mention-list.vue-46-56 (1)
46-56: Globaldocument.querySelectoris fragile with multiple instances.If multiple
mention-listcomponents exist on the page,document.querySelector('.mention-item.is-selected')will find the first match globally, not necessarily within this component. Use a template ref instead.🔎 Proposed fix using template ref
+const listRef = ref<HTMLElement | null>(null) + function scrollToSelected() { nextTick(() => { - const selectedElement = document.querySelector('.mention-item.is-selected') + const selectedElement = listRef.value?.querySelector('.mention-item.is-selected') if (selectedElement) { selectedElement.scrollIntoView({ block: 'nearest', behavior: 'smooth', }) } }) }And in template:
- <div class="mention-list"> + <div ref="listRef" class="mention-list">Committable suggestion skipped: line range outside the PR's diff.
packages/components/src/sender/composables/useKeyboardShortcuts.ts-16-17 (1)
16-17: OnlysubmitTypeis destructured from params, but interface expects more.Per
UseKeyboardShortcutsParamsintypes/composables.ts, the params includecanSubmit,mode,submit, andsetModewhich are not referenced anywhere in the function implementation. The caller inuseSenderCore.tspasses all five parameters, but the function only usessubmitType. Consider narrowing the interface usingPick<UseKeyboardShortcutsParams, 'submitType'>to clarify intent and reduce unnecessary parameter passing.packages/components/src/sender/extensions/template/select/extension.ts-44-47 (1)
44-47: Add error handling for JSON.parse in options attribute.
JSON.parsecan throw ifdata-optionscontains malformed JSON (e.g., from copy-paste or corrupted HTML). This would crash the editor during content loading.🔎 Proposed fix
options: { default: [], parseHTML: (element) => { const optionsStr = element.getAttribute('data-options') - return optionsStr ? JSON.parse(optionsStr) : [] + if (!optionsStr) return [] + try { + return JSON.parse(optionsStr) + } catch { + return [] + } },packages/components/src/sender/extensions/template/block/extension.ts-26-42 (1)
26-42: Watch cleanup and edge cases in onCreate.A few concerns with the watch setup:
Redundant isRef check: Line 33 re-checks
isRef(items)but line 29 already confirmeditemsis a ref.Missing watch cleanup: The
watchreturn value (stop handle) is not captured. In Tiptap,onCreatedoesn't automatically clean up Vue watchers when the extension is destroyed, which could cause memory leaks or stale callbacks if the editor is recreated.UX concern:
focusFirstTemplate()is called on every items change (due toimmediate: true+ watch), which may unexpectedly steal focus when template data is updated programmatically.🔎 Proposed fix
+ // Store the stop function for cleanup + private stopWatch?: () => void + onCreate() { const { items } = this.options if (items && isRef(items)) { - watch( + this.stopWatch = watch( items, () => { - const currentItems = isRef(items) ? items.value : items + const currentItems = items.value if (currentItems !== null && currentItems !== undefined) { this.editor.commands.setTemplateData(currentItems) - this.editor.commands.focusFirstTemplate() + // Consider only focusing on initial load, not on every update } }, { deep: true, immediate: true }, ) } }, + + onDestroy() { + this.stopWatch?.() + },Committable suggestion skipped: line range outside the PR's diff.
packages/components/src/sender/extensions/template/types.ts-84-88 (1)
84-88: Align example with actual TemplateItem union types.The JSDoc example on line 87 uses
type: 'template', but the actualTemplateItemunion (frompackages/components/src/sender/index.type.tslines 52-118) defines types as'text' | 'block' | 'select', not'template'.This mismatch could confuse API consumers and lead to runtime errors.
🔎 Proposed fix
* @example 响应式配置(推荐) * ```typescript * const items = ref<TemplateItem[]>([ * { type: 'text', content: '帮我分析' }, - * { type: 'template', content: '' } + * { type: 'block', content: '' } * ]) * Template.configure({ items }) // 传入 ref,自动双向绑定 * ```packages/components/src/sender-actions/action-button/index.vue-34-34 (1)
34-34: Add a comment explaining the focus event suppression or verify it doesn't break accessibility features.The
@focus.capture="(event: FocusEvent) => event.stopPropagation()"pattern is unique to this component and undocumented. While it prevents focus events from bubbling, this is only applied to the button inside the tooltip (line 34), not the fallback button without a tooltip. Other similar button components (submit-button, upload-button, voice-button) that also use TinyTooltip do not suppress focus events.If this suppression is intentional to prevent tooltip-specific focus side effects, document the rationale. Otherwise, consider removing it or verify that it doesn't interfere with keyboard navigation, focus indicators, or screen reader announcements.
packages/components/src/sender/extensions/mention/extension.ts-100-117 (1)
100-117: Watcher created inonCreateis never cleaned up.The
watchcreated whenitemsis a ref will persist for the lifetime of the extension. If the extension is destroyed and recreated, old watchers may accumulate. Consider usingonDestroyto clean up:🔎 Proposed fix
+import { watch, isRef, type WatchStopHandle } from 'vue' export const Mention = Node.create<MentionOptions>({ // ... existing code ... + addStorage() { + return { + unwatch: null as WatchStopHandle | null, + } + }, onCreate() { const { items } = this.options if (isRef(items)) { - watch( + this.storage.unwatch = watch( items, () => { const tr = this.editor.state.tr tr.setMeta('mention-update', true) this.editor.view.dispatch(tr) }, { deep: true }, ) } }, + onDestroy() { + this.storage.unwatch?.() + },packages/components/src/sender/composables/useModeSwitch.ts-59-74 (1)
59-74: Potential memory leak:setTimeoutcallback may fire after unmount.If the component unmounts while
isAutoSwitchingis true, the 300ms timeout continues running. Consider clearing the timeout on cleanup or usinguseTimeoutFnfrom@vueuse/core.🔎 Proposed fix
+import { useTimeoutFn } from '@vueuse/core' + export function useModeSwitch( props: SenderProps, editor: Ref<Editor | undefined>, editorRef: Ref<HTMLElement | null>, ): UseModeSwitchReturn { + const { start: startAutoSwitchTimeout, stop: stopAutoSwitchTimeout } = useTimeoutFn(() => { + isAutoSwitching.value = false + }, 300, { immediate: false }) + // ... existing code ... const setMode = (mode: InputMode) => { if (currentMode.value === mode) return isAutoSwitching.value = true currentMode.value = mode nextTick(() => { if (editor.value) { editor.value.commands.focus('end') } - setTimeout(() => { - isAutoSwitching.value = false - }, 300) + startAutoSwitchTimeout() }) }Committable suggestion skipped: line range outside the PR's diff.
packages/components/src/sender/extensions/suggestion/types.ts-260-269 (1)
260-269: Replace the localEditorStateinterface with an import from@tiptap/pm/state.Other extensions in the codebase (mention, template/block, template/select) already import the actual
EditorStatetype from@tiptap/pm/state. The local interface duplicates the real type and creates a type mismatch whengetStatereceives real editor state instances. Change line 263 fromexport interface EditorStateto:import type { EditorState } from '@tiptap/pm/state'Then remove the local interface definition (lines 263-269).
packages/components/src/sender/types/composables.ts-12-16 (1)
12-16: Remove unusedKeyboardHandlersinterface.The
KeyboardHandlersinterface (lines 12-16) is never used anywhere in the codebase. Additionally, it incorrectly includes asubmitmethod which is not returned byuseKeyboardShortcuts(thesubmitparameter is passed in, not returned). Either remove this unused interface or clarify its intended purpose if it should be kept.packages/components/src/index.ts-23-31 (1)
23-31: ActionButton naming conflict shadows attachments interface in root barrel export.The root index.ts re-exports
ActionButtoninterface fromattachments/index.type.ts(line 39), but then explicitly exports theActionButtoncomponent fromsender-actions(line 138-139). This causes the explicit component export to shadow the wildcard interface export, making the attachmentsActionButtoninterface inaccessible to consumers importing from the root barrel.Consider renaming one of these exports or managing them explicitly to avoid shadowing (e.g., export the attachments interface under a different name like
AttachmentActionButton).packages/components/src/sender-compat/index.vue-273-291 (1)
273-291: Addref="voiceRef"to VoiceButton in #footer-right slot for multi-line mode compatibility.The
startSpeechandstopSpeechmethods rely onvoiceRefto control the VoiceButton. While the VoiceButton in#actions-inline(single-line mode) hasref="voiceRef", the VoiceButton in#footer-right(multi-line mode) is missing this ref attribute. This causes speech control methods to fail when the component is in multi-line mode.Add
ref="voiceRef"to the VoiceButton at line 282-291 to ensure consistent behavior across layout modes.packages/components/src/sender/index.vue-7-14 (1)
7-14: Inconsistent default value forautoSize.The default value here is
{ minRows: 1, maxRows: 5 }, but the JSDoc inindex.type.ts(line 102) documents@default { minRows: 1, maxRows: 3 }. Please align the actual default with the documentation.🔎 Proposed fix
Either update the code:
- autoSize: () => ({ minRows: 1, maxRows: 5 }), + autoSize: () => ({ minRows: 1, maxRows: 3 }),Or update the documentation in
index.type.ts:- * @default { minRows: 1, maxRows: 3 } + * @default { minRows: 1, maxRows: 5 }
🧹 Nitpick comments (35)
packages/components/src/sender/extensions/template/block/plugins.ts (2)
416-427: Redundantevent.key === 'Delete'check.The static analysis tool correctly identified that the condition
event.key === 'Delete'on line 421 is always true since we're already inside theif (event.key === 'Delete' && selection.empty)block (line 408). This redundant check can be removed for clarity.🔎 Proposed fix
if ( $from.pos === $from.start() && content.length === 1 && - content !== ZERO_WIDTH_CHAR && - event.key === 'Delete' + content !== ZERO_WIDTH_CHAR ) {
226-236: Similar redundant check forevent.key === 'Backspace'.Same pattern as the Delete handler - line 230 redundantly checks
event.key === 'Backspace'when we're already inside the Backspace handling block (line 217).🔎 Proposed fix
if ( $from.pos === $from.end() && content.length === 1 && - content !== ZERO_WIDTH_CHAR && - event.key === 'Backspace' + content !== ZERO_WIDTH_CHAR ) {packages/components/src/sender/extensions/template/commands.ts (1)
10-11: Consider a more explicit type definition for PMNode.The current type
ReturnType<Editor['state']['doc']['nodeAt']> & { nodeSize: number }is clever but fragile.nodeAtreturnsNode | null, and intersecting with{ nodeSize: number }doesn't actually narrow away the null case.🔎 Proposed fix
-// ProseMirror Node 类型 -type PMNode = ReturnType<Editor['state']['doc']['nodeAt']> & { nodeSize: number } +import type { Node as PMNode } from '@tiptap/pm/model'Then update the
getAllTemplatesreturn type toArray<{ node: PMNode; pos: number }>.packages/components/src/sender-compat/index.type.ts (1)
17-41: Type duplication for compatibility layer.These types (
SpeechCallbacks,SpeechHandler,SpeechConfig) are duplicated frompackages/components/src/sender-actions/voice-button/speech.types.ts. While this isolation is intentional for the compatibility layer, consider re-exporting from the canonical source to reduce maintenance burden:export type { SpeechCallbacks, SpeechHandler, SpeechConfig } from '../sender-actions/voice-button/speech.types'If the compat layer needs to remain fully isolated (e.g., for potential version divergence), the current approach is acceptable.
packages/components/src/sender-actions/types/tooltip.ts (1)
1-1: Use type-only import for better tree-shaking.Since this file only uses
VNodeas a type, use a type-only import to help bundlers optimize unused imports.🔎 Proposed fix
-import type { VNode } from 'vue' +import type { VNode } from 'vue'Wait, the import already uses
import type. Let me re-check. Actually looking at line 1, it showsimport type { VNode } from 'vue'- this is already correct. Let me revise.packages/components/src/sender-actions/default-actions/index.vue (1)
11-20: Consider the nested transition pattern for clarity and consistency.The template uses nested
Transitioncomponents with the same name (tr-slide-right):
- Line 11: Outer transition wrapping the button group
- Line 13: Inner transition wrapping only the ClearButton
This nested pattern with identical transition names could lead to conflicting animations when
hasContentchanges. Additionally, the SubmitButton (line 17) lacks a transition wrapper, creating inconsistency.Consider one of the following approaches:
- Use different transition names for nested transitions if staggered animation is intended
- Remove the inner transition and apply the same transition behavior to all buttons uniformly
- Document the intended animation behavior if this pattern is deliberate
packages/components/src/sender/extensions/utils/id-generator.ts (1)
17-19: Consider crypto.randomUUID() for better uniqueness guarantees.The current implementation using
Math.random()is suitable for UI element IDs but has two potential weaknesses:
- Collision risk when multiple IDs are generated in the same millisecond
- The random suffix may be shorter than 7 characters for very small random values
If the runtime supports it, consider using
crypto.randomUUID()or ensure the random suffix is padded to a consistent length.🔎 Proposed enhancement with crypto.randomUUID()
export function generateId(prefix: string): string { - return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}` + // Use crypto.randomUUID if available, fallback to current approach + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return `${prefix}_${crypto.randomUUID()}` + } + const random = Math.random().toString(36).slice(2, 9).padEnd(7, '0') + return `${prefix}_${Date.now()}_${random}` }packages/components/src/sender/components/footer/index.vue (1)
16-16: Consider explicit comparison for maxLength.The condition
showWordLimit && maxLengthrelies on JavaScript's truthiness, which treats0as falsy. While this is likely intentional (no limit = no counter), an explicit comparisonmaxLength > 0would make the intent clearer.🔎 Suggested clarification
- <WordCounter v-if="showWordLimit && maxLength" /> + <WordCounter v-if="showWordLimit && maxLength > 0" />packages/components/src/sender/components/editor-content/index.vue (2)
32-47: Document browser-specific scrollbar styling.The
::-webkit-scrollbarpseudo-elements only work in WebKit-based browsers (Chrome, Edge, Safari). Firefox users will see the default scrollbar. Consider adding a fallback using the standardscrollbar-widthandscrollbar-colorproperties for broader compatibility.🔎 Optional Firefox fallback
.tr-sender-editor-scroll { flex: 1; min-width: 0; overflow-y: hidden; // 默认隐藏,由 JS 控制 + + // Firefox fallback + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.15) transparent; // 滚动条样式 &::-webkit-scrollbar {
40-46: Extract scrollbar colors to CSS variables for theme support.The hardcoded
rgba(0, 0, 0, ...)values won't adapt to dark themes. Consider extracting these to CSS variables (e.g.,--tr-sender-scrollbar-thumb,--tr-sender-scrollbar-thumb-hover) defined in the theme system.🔎 Proposed refactor
First, add variables to
sender.less:scrollbar-thumb: rgba(0, 0, 0, 0.15); scrollbar-thumb-hover: rgba(0, 0, 0, 0.25);Then update this component:
&::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.15); + background: var(--tr-sender-scrollbar-thumb); border-radius: 3px; &:hover { - background: rgba(0, 0, 0, 0.25); + background: var(--tr-sender-scrollbar-thumb-hover); } }packages/components/src/sender/components/layouts/SingleLineLayout.vue (1)
37-40: DefaultActionButtons will render alongside custom actions, but visibility is internally controlled.While
DefaultActionButtonsis rendered unconditionally, it has internal conditional logic (v-if="hasContent || loading") that controls whether its buttons (ClearButton and SubmitButton) are actually visible. This means custom slot content viaactions-inlineand default action buttons can coexist in the same flex container, showing both when appropriate.If the intent is to show only one or the other (custom actions OR defaults, not both), consider using a slot fallback:
Suggested pattern for mutually exclusive actions
- <slot name="actions-inline" v-bind="slotScope" /> - <DefaultActionButtons /> + <slot name="actions-inline" v-bind="slotScope"> + <DefaultActionButtons /> + </slot>Otherwise, if both are intended to coexist, document this behavior to avoid future confusion.
packages/components/src/sender/composables/useEditor.ts (1)
46-48: Consider wiringmaxLengthto CharacterCount extension.The
CharacterCountextension is configured with onlymode: 'textSize', butprops.maxLengthis available and could be used to enforce character limits at the editor level.🔎 Proposed enhancement
CharacterCount.configure({ mode: 'textSize', + limit: props.maxLength, }),packages/components/src/sender-actions/clear-button/index.vue (2)
32-32: RedundantisDisabledinshowcomputed and:disabledprop.When
isDisabledis true,showevaluates to false and the button won't render. This means when the button does render,isDisabledis always false, making the:disabled="isDisabled"binding always false. If the intent is to hide disabled buttons entirely, remove the:disabledprop; if the intent is to show but disable the button, remove!isDisabledfromshow.🔎 Option A: Remove redundant disabled prop (current behavior preserved)
<ActionButton v-if="show" :icon="IconClear" - :disabled="isDisabled" :tooltip="tooltipRenderFn" :tooltip-placement="tooltipPlacement" @click="handleClick" />🔎 Option B: Show disabled button instead of hiding
-const show = computed(() => clearable.value && hasContent.value && !loading.value && !isDisabled.value) +const show = computed(() => clearable.value && hasContent.value && !loading.value)Also applies to: 44-52
37-41: TheisDisabledguard inhandleClickis unreachable.Since
showis false whenisDisabledis true, the button won't be rendered, sohandleClickcan never be called whenisDisabledis true. This guard is dead code.🔎 Simplified handler
const handleClick = () => { - if (!isDisabled.value) { - clear() - } + clear() }packages/components/src/sender/components/layouts/MultiLineLayout.vue (1)
7-8: Consider destructuring only what's needed from context.
contextis only used forcontext.editorin the template. Destructuring improves clarity:🔎 Proposed simplification
-const context = useSenderContext() +const { editor } = useSenderContext() const slotScope = useSlotScope()And in template:
-<slot name="content" :editor="context.editor"> +<slot name="content" :editor="editor">packages/components/src/sender/extensions/mention/commands.ts (1)
32-44: Position corruption risk when deleting nodes during traversal.The
tr.delete()call modifies the document structure whiledescendants()is iterating. Althoughreturn falsestops iteration after the first match, if this pattern is ever extended to delete multiple nodes, the positions would become invalid after the first deletion.Consider using
dispatchpattern or collecting positions first, then deleting in reverse order:🔎 Safer deletion pattern (for future-proofing)
deleteMention: (id: string) => ({ tr, state }: { tr: Editor['state']['tr']; state: Editor['state'] }) => { let deleted = false + let nodePos: number | null = null + let nodeSize: number = 0 state.doc.descendants((node, pos) => { if (node.type.name === 'mention' && node.attrs.id === id) { - tr.delete(pos, pos + node.nodeSize) - deleted = true + nodePos = pos + nodeSize = node.nodeSize return false } }) + if (nodePos !== null) { + tr.delete(nodePos, nodePos + nodeSize) + deleted = true + } return deleted },packages/components/src/sender/extensions/template/select/template-select-view.vue (2)
226-228: Global keydown listener may interfere with other components.The
handleKeyDownlistener is attached todocumentand will capture keyboard events even when other dropdowns or components are focused. Consider scoping this to only handle events when this specific dropdown is open and focused.🔎 Suggested improvement
onMounted(() => { - document.addEventListener('keydown', handleKeyDown) + // Only add listener when dropdown is open (in openDropdown) })Move the listener registration into
openDropdown()and removal intocloseDropdown():const openDropdown = async () => { // ... existing code ... document.addEventListener('keydown', handleKeyDown) } const closeDropdown = async () => { document.removeEventListener('keydown', handleKeyDown) // ... existing code ... }
329-390: Consider scoping global styles more specifically.The unscoped
<style lang="less">block defines.template-select__dropdownand.template-select__optionglobally. While necessary for Teleport content, these class names could potentially conflict with other components.Consider adding a more unique prefix or using CSS custom properties for theming consistency.
packages/components/src/sender/composables/useAutoSize.ts (2)
28-41:autoSize: trueis silently ignored.Based on the type
AutoSize = boolean | { minRows: number; maxRows: number }, whenautoSizeistrue, the computed returnsnull, effectively disabling auto-sizing. This may be unexpected behavior for users who passautoSize={true}expecting default row limits.Consider providing default values when
autoSize === true:🔎 Proposed fix
const autoSizeConfig = computed(() => { if (autoSize === false || autoSize === undefined) { return null } + // autoSize: true 使用默认值 + if (autoSize === true) { + return { + minRows: 1, + maxRows: 6, + } + } + if (typeof autoSize === 'object') { return { minRows: autoSize.minRows, maxRows: autoSize.maxRows, } } return null })
74-92: DuplicateupdateHeightcalls on initialization.Both watchers have
{ immediate: true }, causingupdateHeight()to be called twice on mount. Consider consolidating into a single watcher or removingimmediatefrom one:🔎 Consolidate watchers
- watch( - currentMode, - () => { - nextTick(() => { - updateHeight() - }) - }, - { immediate: true }, - ) - - watch( - autoSizeConfig, - () => { - nextTick(() => { - updateHeight() - }) - }, - { immediate: true }, - ) + watch( + [currentMode, autoSizeConfig], + () => { + nextTick(() => { + updateHeight() + }) + }, + { immediate: true }, + )packages/components/src/styles/variables.css (1)
127-188: Well-structured theme variables for new Sender components.The new CSS variables for Sender, Suggestion, Mention, and Template components follow the established
--tr-*naming convention and are well-organized by feature area.Consider moving theme-invariant variables (e.g.,
--tr-suggestion-item-font-size,--tr-suggestion-item-icon-size,--tr-sender-template-border-radius,--tr-sender-template-padding,--tr-sender-template-margin,--tr-sender-template-min-width) to the:rootblock since they have identical values in both light and dark themes. This would reduce duplication and make maintenance easier.packages/components/src/sender-actions/action-button/index.vue (1)
23-48: Reduce duplication between tooltip and non-tooltip branches.The button element is duplicated across lines 31-40 and 44-48, differing only in the tooltip wrapper. This makes maintenance harder if button props or slots need to change.
🔎 Suggested refactor to extract button into a reusable template ref
<script setup lang="ts"> import { computed } from 'vue' import { TinyTooltip } from '@opentiny/vue' import type { ActionButtonProps } from '../types/common' import { normalizeTooltipContent } from '../utils/tooltip' const props = withDefaults(defineProps<ActionButtonProps>(), { disabled: false, active: false, size: 32, tooltipPlacement: 'top', }) const tooltipRenderFn = computed(() => normalizeTooltipContent(props.tooltip)) const sizeStyle = computed(() => { const size = typeof props.size === 'number' ? `${props.size}px` : props.size return { fontSize: size } }) </script> <template> + <component + :is="props.tooltip ? TinyTooltip : 'div'" + v-bind="props.tooltip ? { + renderContent: tooltipRenderFn, + placement: props.tooltipPlacement, + effect: 'light', + visibleArrow: false, + popperClass: 'tr-action-button-tooltip-popper' + } : {}" + > - <tiny-tooltip - v-if="props.tooltip" - :render-content="tooltipRenderFn" - :placement="props.tooltipPlacement" - effect="light" - :visible-arrow="false" - popper-class="tr-action-button-tooltip-popper" - > <button :class="['tr-action-button', { active: props.active }]" :disabled="props.disabled" @focus.capture="(event: FocusEvent) => event.stopPropagation()" > - <!-- 优先使用插槽,如果没有插槽则使用 icon prop --> <slot name="icon"> <component :is="props.icon" :style="sizeStyle" /> </slot> </button> - </tiny-tooltip> - - <!-- 无 tooltip 时直接渲染按钮 --> - <button v-else :class="['tr-action-button', { active: props.active }]" :disabled="props.disabled"> - <slot name="icon"> - <component :is="props.icon" :style="sizeStyle" /> - </slot> - </button> + </component> </template>packages/components/src/sender/extensions/suggestion/extension.ts (1)
28-42: Consider performance implications of deep watching suggestion items.The
watchwith{ deep: true }on line 39 will recursively track all nested properties of theitemsarray. For large suggestion lists or frequently updated items, this could cause performance issues.If only the array reference or top-level properties need to be tracked, consider using a shallow watch or restructuring to avoid deep reactivity.
💡 Alternative approach if deep watching isn't necessary
onCreate() { if (isRef(this.options.items)) { watch( this.options.items, () => { // 触发更新 const tr = this.editor.state.tr // 使用一个特殊的 meta 来触发插件更新,虽然实际上只要有 dispatch 就会触发 apply tr.setMeta(SuggestionPluginKey, { type: 'update' }) this.editor.view.dispatch(tr) }, - { deep: true }, + // Remove deep: true if only array reference changes need to be tracked ) } },Consider this change only if the suggestion items don't have nested properties that need reactivity, or if the array reference is replaced on updates.
packages/components/src/sender-actions/submit-button/index.vue (1)
37-80: Consider extracting the button element to reduce duplication.Similar to the action-button component, the submit button element is duplicated across the tooltip and non-tooltip branches (lines 45-57 vs 61-80). The duplication is more complex here due to loading state rendering.
While extraction might be less straightforward given the loading state differences, consider whether a computed component or render function could reduce the duplication without sacrificing readability.
packages/components/src/sender/extensions/template/utils.ts (1)
13-13: Consider using a literal regex instead ofnew RegExp().Since
ZERO_WIDTH_CHARis a compile-time constant (not user input), the static analysis ReDoS warning is a false positive. However, you can simplify and clarify the intent by using a literal regex:🔎 Proposed refactor
-export const ZERO_WIDTH_CHAR = '\u200B' +export const ZERO_WIDTH_CHAR = '\u200B' +export const ZERO_WIDTH_CHAR_REGEX = /\u200B/gThen use
ZERO_WIDTH_CHAR_REGEXin the replace calls:-const content = (node.textContent || '').replace(new RegExp(ZERO_WIDTH_CHAR, 'g'), '') +const content = (node.textContent || '').replace(ZERO_WIDTH_CHAR_REGEX, '')-const text = (node.text || '').replace(new RegExp(ZERO_WIDTH_CHAR, 'g'), '') +const text = (node.text || '').replace(ZERO_WIDTH_CHAR_REGEX, '')Also applies to: 38-38, 53-53
packages/components/src/sender/extensions/mention/plugin.ts (1)
339-346: Magic number+2for cursor position is fragile.The cursor calculation assumes mention node size (1) + space (1) = 2. Consider computing from actual node sizes for maintainability:
🔎 Proposed refactor
// 插入 mention 节点和空格 tr.insert(range.from, [mentionNode, spaceNode]) - // 设置光标到空格之后(mention 节点 + 空格 = +2) - const cursorPos = range.from + 2 + // 设置光标到空格之后 + const cursorPos = range.from + mentionNode.nodeSize + spaceNode.nodeSize tr.setSelection(TextSelection.create(tr.doc, cursorPos))packages/components/src/sender/extensions/suggestion/types.ts (1)
130-130: Consider moving import to the top of the file.The
import type { Ref } from 'vue'statement is placed mid-file. While TypeScript hoists imports, placing them at the top improves readability and follows standard conventions.packages/components/src/sender-actions/upload-button/index.vue (1)
38-54: Hardcoded Chinese error messages limit i18n support.Error messages like
最多只能选择 ${props.maxCount} 个文件and以下文件超过 ${props.maxSize}MB 限制should be externalized for internationalization. Consider using an i18n solution or allowing custom error messages via props.packages/components/src/sender/extensions/template/select/dropdown-manager.ts (1)
59-67: Potential race condition between cleanup and delayed listener attachment.If the returned cleanup function is called before the
setTimeoutcallback executes,removeEventListenerwill be called on a listener that was never added. While this is harmless, consider using a flag to track whether the listener was actually attached.🔎 Proposed fix
export function setupClickOutside( selectElement: HTMLElement, dropdownElement: HTMLElement, onClose: () => void, ): () => void { + let listenerAdded = false + const handleClickOutside = (event: MouseEvent) => { const target = event.target as Node if (!selectElement.contains(target) && !dropdownElement.contains(target)) { onClose() document.removeEventListener('click', handleClickOutside) + listenerAdded = false } } // 延迟添加监听器,避免立即触发 setTimeout(() => { document.addEventListener('click', handleClickOutside) + listenerAdded = true }, 0) // 返回清理函数 return () => { - document.removeEventListener('click', handleClickOutside) + if (listenerAdded) { + document.removeEventListener('click', handleClickOutside) + } } }packages/components/src/sender/composables/useSenderCore.ts (2)
69-78: Consider caching text extraction to avoid redundant computation.
getTextWithTemplates(editor.value)is called twice—once inhasContent(line 71) and again incharacterCount(line 77). Since both computed properties depend on the same editor state, the extraction happens twice per reactivity cycle.🔎 Proposed optimization
+ // Cache the extracted text to avoid redundant computation + const textContent = computed(() => { + if (!editor.value) return '' + return getTextWithTemplates(editor.value) + }) + const hasContent = computed(() => { - if (!editor.value) return false - const text = getTextWithTemplates(editor.value) - return text.trim().length > 0 + return textContent.value.trim().length > 0 }) const characterCount = computed(() => { - if (!editor.value) return 0 - const text = getTextWithTemplates(editor.value) - return text.length + return textContent.value.length })
215-223: Consider usingnextTickinstead ofsetTimeout(0)for Vue-idiomatic deferral.While
setTimeout(0)works, Vue'snextTickis more semantically appropriate for waiting until DOM updates complete and ensures consistency with Vue's update cycle.🔎 Proposed change
+import { computed, provide, toRef, watch, nextTick } from 'vue' ... watch( () => editor.value?.state.doc.content, () => { - setTimeout(() => { + nextTick(() => { checkOverflow() - }, 0) + }) }, { deep: true }, )packages/components/src/sender/extensions/suggestion/plugin.ts (1)
259-262: ThejustClosedguard withsetTimeout(0)may be timing-sensitive.The zero-delay timeout relies on the event loop ordering, which can vary across browsers. If this proves flaky, consider a small delay (e.g., 10-50ms) or using a frame-based approach with
requestAnimationFrame.packages/components/src/sender/index.vue (1)
128-135: Consider using CSS variables for scrollbar colors.The scrollbar thumb colors are hardcoded with
rgba(0, 0, 0, ...)which may not work well with dark themes. Other components in this PR (e.g.,suggestion-list.vue) use CSS variables like--tr-suggestion-scrollbar-thumb-colorfor consistency.🔎 Proposed fix
&::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.2); + background: var(--tr-sender-scrollbar-thumb-color, rgba(0, 0, 0, 0.2)); border-radius: 4px; &:hover { - background: rgba(0, 0, 0, 0.3); + background: var(--tr-sender-scrollbar-thumb-hover-color, rgba(0, 0, 0, 0.3)); } }packages/components/src/sender/index.type.ts (2)
160-161: Consider a more specific extension type.The
any[]fallback reduces type safety. If the intent is to support configured extensions (e.g., from.configure()), consider usingExtension<any, any>[]or creating a custom union type that captures known extension shapes.🔎 Proposed fix
- // eslint-disable-next-line @typescript-eslint/no-explicit-any - extensions?: Extension[] | any[] + extensions?: Extension<any, any>[]Or if AnyExtension is exported from @tiptap/core:
import type { Extension, AnyExtension } from '@tiptap/core' // ... extensions?: AnyExtension[]
331-337: Consider using a more specific type foreditor.Using
unknownfor the editor instance loses type safety and IDE autocompletion. Consider importing and using the Tiptap Editor type:import type { Editor } from '@tiptap/core' content?: (props: { editor: Editor | null }) => unknownThis provides better developer experience when accessing editor methods in the slot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @packages/components/src/sender-compat/index.vue:
- Around line 228-252: The UploadButton and VoiceButton are being rendered twice
because they’re unconditionally present in both the #actions-inline and
#footer-right slots; change their v-if checks to render them only in the
appropriate layout mode (e.g., inline for single-line mode, footer for
multi-line mode) by gating on a layout/computed flag (e.g., isInlineActions or
singleLineMode) instead of just allowFiles/allowSpeech, and ensure the
footer-right VoiceButton also receives ref="voiceRef" and the same props/events
as the inline VoiceButton so startSpeech()/stopSpeech() continue to work; update
UploadButton/VoiceButton usages to use allowFiles/allowSpeech && isInlineActions
for the inline slot and allowFiles/allowSpeech && !isInlineActions for the
footer slot.
🧹 Nitpick comments (4)
packages/components/src/sender-compat/index.vue (1)
212-212: Redundant event handler with v-model.The
@update:model-valuehandler is unnecessary sincev-model="modelValue"already handles two-way binding through the computed setter on line 18-20.🔎 Proposed fix
@focus="handleFocus" @blur="handleBlur" - @update:model-value="(val) => (modelValue = val)" >packages/components/src/sender-compat/index.type.ts (3)
33-37: Consider renaming to PascalCase for consistency.The interface
fileUploadConfiguses camelCase, but TypeScript convention prefers PascalCase for type names (e.g.,FileUploadConfig). While this doesn't affect functionality, aligning with the convention improves consistency withVoiceButtonConfigbelow.🔎 Proposed refactor
-interface fileUploadConfig { +interface FileUploadConfig { accept?: string multiple?: boolean reset?: boolean }And update the reference:
export interface ButtonGroupConfig { - file?: ControlState & fileUploadConfig + file?: ControlState & FileUploadConfig submit?: ControlState voice?: VoiceButtonConfig }
54-67: Consider importing types instead of duplicating definitions.
SuggestionTextPartandHighlightFunctionare duplicated frompackages/components/src/sender/extensions/suggestion/types.ts. While the comments indicate these are for compatibility, duplicating types can lead to drift if the source definitions change. Consider importing and re-exporting instead:🔎 Proposed refactor
+import type { SuggestionTextPart, HighlightFunction } from '../sender/extensions/suggestion/types' + // ============================================ // 建议项相关类型(旧版兼容层专用) // 注意:这些类型与新版 sender 的 SuggestionItem 不同,仅用于兼容旧版 API // ============================================ -// 高亮片段类型 -export interface SuggestionTextPart { - text: string - isMatch: boolean -} - -// 高亮函数类型 -type HighlightFunction = (suggestionText: string, inputText: string) => SuggestionTextPart[] +// Re-export for compatibility +export type { SuggestionTextPart, HighlightFunction } // 建议项类型 export interface ISuggestionItem { content: string highlights?: string[] | HighlightFunction }
131-135: Consider simplifying UserTemplateItem for clarity.The
Omit<Pick<CompatTemplateItem, 'type' | 'content'>, 'id'>is redundant sincePickdoesn't include'id'in the first place. The type works correctly but could be simplified for readability.🔎 Proposed simplification
export type UserTextItem = Omit<CompatTextItem, 'id'> & { id?: CompatTextItem['id'] } -export type UserTemplateItem = Omit<Pick<CompatTemplateItem, 'type' | 'content'>, 'id'> & { +export type UserTemplateItem = Pick<CompatTemplateItem, 'type' | 'content'> & { id?: CompatTemplateItem['id'] }
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
packages/components/src/sender-compat/index.type.tspackages/components/src/sender-compat/index.vuepackages/components/src/sender/extensions/template/types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/components/src/sender/extensions/template/types.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/components/src/sender-compat/index.type.ts (6)
packages/components/src/sender/extensions/index.ts (2)
SuggestionTextPart(17-17)HighlightFunction(18-18)packages/components/src/sender/extensions/suggestion/types.ts (2)
SuggestionTextPart(10-13)HighlightFunction(22-22)packages/components/src/sender/index.ts (2)
SuggestionTextPart(60-60)HighlightFunction(61-61)packages/components/src/sender/types/base.ts (3)
AutoSize(264-264)InputMode(13-13)SubmitTrigger(21-21)packages/components/src/sender-actions/index.type.ts (1)
SpeechConfig(12-12)packages/components/src/sender-actions/voice-button/speech.types.ts (1)
SpeechConfig(25-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build / build
🔇 Additional comments (7)
packages/components/src/sender-compat/index.vue (4)
63-73: Previous issue addressed: incorrectbuttonGroup.filemapping removed.The incorrect mapping of
buttonGroup.filetoactions.clearhas been removed. The current implementation correctly maps onlybuttonGroup.submit, while file upload functionality is handled via directUploadButtonrendering in slots.
83-105: Well-implemented template data transformation with proper guards.The
setTemplateDatamethod correctly:
- Guards against unready editor state with a helpful warning
- Handles empty data by clearing content
- Transforms legacy
templatetype to newblocktype
107-186: Event handlers and exposed methods are well-structured.The implementation correctly:
- Forwards all events with proper typing
- Uses optional chaining for defensive method calls
- Exposes a clean public API via
defineExpose
29-61: Dynamic extension access pattern is acceptable for compatibility layer.The use of
anycasts to accessSender.SuggestionandSender.Templateis a pragmatic approach for this compatibility wrapper. The null checks (lines 38, 56) before use provide adequate safety.packages/components/src/sender-compat/index.type.ts (3)
6-8: LGTM!The imports are correct and align with the existing type definitions in the codebase.
10-31: LGTM!The basic type definitions and ControlState interface are well-structured and provide appropriate flexibility for tooltip configuration.
70-112: LGTM!The
SenderPropsandSenderEmitsdefinitions are comprehensive and correctly structured for Vue 3. Event signatures follow the proper emit pattern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
packages/components/src/sender-compat/index.vue (1)
190-212: Redundant@update:model-valuehandler.Line 212 is redundant since
v-model="modelValue"(line 192) already expands to include@update:model-value="val => modelValue = val". The explicit handler duplicates this behavior.♻️ Suggested fix
:default-actions="defaultActions" :stop-text="stopText" @submit="handleSubmit" @clear="handleClear" @cancel="handleCancel" @focus="handleFocus" @blur="handleBlur" - @update:model-value="(val) => (modelValue = val)" >
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/components/src/sender-compat/index.vue
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-25T07:04:18.791Z
Learnt from: gene9831
Repo: opentiny/tiny-robot PR: 123
File: packages/components/src/bubble/message/Message.vue:22-40
Timestamp: 2025-06-25T07:04:18.791Z
Learning: In the Message component (packages/components/src/bubble/message/Message.vue), the renderer resolution is intentionally not reactive to type changes - the component is designed to work with a fixed type that doesn't change after initialization.
Applied to files:
packages/components/src/sender-compat/index.vue
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build / build
🔇 Additional comments (12)
packages/components/src/sender-compat/index.vue (12)
1-13: LGTM!Clean imports and prop definitions with sensible defaults.
15-23: LGTM!Correct pattern for v-model forwarding with proper nullish coalescing fallback.
25-27: LGTM!Properly typed component refs using
InstanceType<typeof Component>.
29-61: LGTM!The dynamic access pattern for Sender extensions is appropriate for a compatibility layer. The unconditional addition of Template extension ensures
setTemplateDataworks regardless of initial configuration.
63-73: Previous issue addressed.The incorrect mapping of
buttonGroup.filetoactions.clearhas been removed. Now onlysubmitaction is mapped correctly.
75-81: LGTM!Clean normalization of speech config - boolean
trueuses defaults while object provides full configuration.
83-105: LGTM!Well-structured with proper null checking and helpful console warning. The type conversion from
'template'to'block'properly handles API migration.
107-186: LGTM!Clean event forwarding with thin handlers and properly guarded method delegation using optional chaining.
214-226: LGTM!Proper conditional slot forwarding with scoped slot props passed through correctly.
227-252: Previous issue addressed.The duplicate button rendering concern has been resolved by adding mode-based conditional rendering (
mode === 'single'). VoiceButton properly receivesref="voiceRef"for speech control methods.
254-259: LGTM!Footer slot composition maps legacy
footer-leftalongsidefootercontent into the new Sender's footer slot.
261-287: Previous issues addressed.Both past review concerns are resolved:
- Mode-based conditional rendering (
mode === 'multiple') prevents duplicate buttons- VoiceButton at line 278 now includes
ref="voiceRef"ensuringstartSpeech()/stopSpeech()work in multi-line mode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 20
🤖 Fix all issues with AI agents
In @.github/workflows/auto-build-main-module-docs.yml:
- Around line 63-72: In the "Update PR status" step fix the shell conditional by
quoting and using POSIX equality: replace the line if [ ${{ steps.build.outcome
}} == 'success' ]; with a quoted comparison such as if [ "${{
steps.build.outcome }}" = "success" ]; or set an env var (e.g.,
BUILD_OUTCOME="${{ steps.build.outcome }}") and then use if [ "$BUILD_OUTCOME" =
"success" ]; ensuring proper quotes and single = for portability.
- Around line 30-46: The script uses an untrusted branch name (github.head_ref)
assigned to BRANCH_NAME and interpolates it into shell commands (git fetch
origin $BRANCH_NAME, git checkout $BRANCH_NAME), creating a command-injection
risk; fix by either switching to actions/checkout@v3 with ref: ${{
github.head_ref }} or, if you need shell use, sanitize and safely pass the
variable: assign BRANCH_NAME="${{ github.head_ref }}", validate it against a
strict regex (e.g., only [A-Za-z0-9._/-]) and exit on mismatch, then use safe
git invocations with explicit separator and quoting (git fetch origin --
"$BRANCH_NAME" && git checkout -- "$BRANCH_NAME") to prevent injection.
In @packages/components/src/sender-actions/upload-button/index.vue:
- Around line 50-53: The error message lists all oversized files but the
component only emits the first one; change the emit call to pass the full
oversized array (emit('error', error, oversized)) so consumers receive all
offending files, and update the event type in index.type.ts (the error event
signature) to accept an array of File objects instead of a single File; ensure
props.maxSize and the oversized variable are left unchanged.
In @packages/components/src/sender/composables/useAutoSize.ts:
- Around line 28-41: autoSizeConfig currently ignores the case when autoSize ===
true and thus disables auto-sizing; update the computed to return a default
config when autoSize is strictly true (e.g., provide sensible defaults for
minRows and maxRows) so that AutoSize = true enables resizing; specifically
modify the autoSizeConfig computed (which reads the autoSize prop/variable) to
check for autoSize === true and return an object with default minRows and
maxRows before falling back to null, keeping existing handling for object values
(autoSize.minRows / autoSize.maxRows).
In @packages/components/src/sender/composables/useModeSwitch.ts:
- Around line 59-74: The setMode function schedules a setTimeout to reset
isAutoSwitching after 300ms but never clears that timer on component unmount,
risking a memory leak; capture the timeout id when calling setTimeout (e.g.,
store in a local variable like autoSwitchTimeoutId), clear any previous timeout
before assigning a new one, and register an onBeforeUnmount hook to clear
autoSwitchTimeoutId so that isAutoSwitching.value = false cannot run after the
component is destroyed; references: setMode, isAutoSwitching, currentMode,
nextTick, editor, onBeforeUnmount.
In @packages/components/src/sender/composables/useSenderCore.ts:
- Around line 186-191: The setTimeout(0) used after calling setMode('multiple')
to defer editorInstance.commands.splitBlock() and focus is a fragile race
workaround; replace it by awaiting Vue's nextTick (and if needed a second
nextTick) after setMode('multiple') to ensure DOM and reactive updates complete
before calling editorInstance.commands.splitBlock() and
editorInstance.commands.focus(); update the flow in the function containing
setMode and editorInstance to await nextTick(s) instead of using setTimeout so
the mode switch is reliably applied before splitBlock/focus.
- Around line 166-172: The plugin-state checks assume the plugin keys are
present and can throw or return undefined; update the handler around the
MentionPluginKey.getState(view.state), SuggestionPluginKey.getState(view.state),
and TemplateSelectDropdownPluginKey.getState(view.state) calls to first confirm
each plugin key exists (or wrap in try/catch) and only access .active or .isOpen
if the returned state is non-null/defined; keep the existing early return
behavior (return false) when any plugin state indicates active/isOpen, but avoid
direct property access on undefined by guarding each call (e.g., check the
result of getState before reading .active/.isOpen or handle exceptions).
- Around line 109-123: The code is using repeated string literals for extension
names (e.g., 'template', 'mention') which is brittle; define and export shared
constants like TEMPLATE_EXTENSION = 'template' and MENTION_EXTENSION = 'mention'
in a central module (e.g., extensions/constants) and replace all literal checks
in useSenderCore (the conditional that calls
getTemplateStructuredData/getTextWithTemplates and
getMentionStructuredData/getTextWithMentions), mention/utils.ts, and
suggestion/plugin.ts to use those constants so the checks are consistent and
compile-time refactoring-safe.
- Around line 156-205: The injected handleKeyDown must be cleaned up: update the
watch callback that calls editorInstance.setOptions to return a cleanup function
which resets the handler (e.g. editorInstance.setOptions({ editorProps: {
...editorInstance.options.editorProps, handleKeyDown: undefined }})) when the
editor instance changes; additionally register an onBeforeUnmount/onUnmounted
inside this composable to clear the handler (or call editor.destroy()) so the
dynamic keyboard logic (handleKeyDown, keyboardHandlers, submit, setMode,
editorInstance.commands) is not leaked after unmount.
In
@packages/components/src/sender/extensions/mention/components/mention-list.vue:
- Around line 46-56: The scrollToSelected function currently uses
document.querySelector('.mention-item.is-selected') which can pick items outside
this component; change it to scope the query to this component's root/list
element via a template ref (e.g., a ref like listRef or rootRef) and query that
ref (ref.value.querySelector('.mention-item.is-selected')) inside the existing
nextTick callback so only items in this MentionList component are targeted;
update the template to add the corresponding ref to the container element and
use that ref name in the scrollToSelected implementation.
In @packages/components/src/sender/extensions/mention/extension.ts:
- Around line 100-117: The watcher created in onCreate (checking isRef(items)
and calling watch) must be cleaned up on extension destroy and must set metadata
with the same key/shape the plugin expects: instead of
tr.setMeta('mention-update', true) use the MentionPluginKey with an object
matching plugin expectations (e.g., tr.setMeta(MentionPluginKey, { type:
'update' })) and save the watch stop handle (const stopWatcher = watch(...)) so
you can implement onDestroy() to call stopWatcher(); update references to items,
isRef, watch, onCreate, onDestroy, tr.setMeta, and MentionPluginKey accordingly.
- Around line 140-142: The mentionCommands object lacks an explicit type
annotation, weakening TipTap type safety; update the export of mentionCommands
in commands.ts to declare a precise type that matches the Commands augmentation
in types.ts (e.g., annotate it so each entry is a curried command taking the
correct arg and context types for Editor), ensuring insertMention and
deleteMention signatures use Partial<MentionAttrs>, Editor['commands'],
Editor['state'], and tr types accordingly (or create a dedicated Commands-like
type and apply it to mentionCommands) so the object is fully type-checked
against the Commands interface.
In @packages/components/src/sender/extensions/template/block/plugins.ts:
- Around line 613-619: Deleting $from.nodeBefore first shifts document positions
so using the original $from.pos for the subsequent $from.nodeAfter deletion is
unsafe; update the position with the transaction mapping or compute fixed
offsets before mutating. Modify the block that checks ZERO_WIDTH_CHAR so you
either (a) compute start/end positions for both deletions up front (e.g.,
beforePos = $from.pos, beforeSize = $from.nodeBefore?.nodeSize, afterSize =
$from.nodeAfter?.nodeSize) and call tr.delete using those computed numeric
ranges, or (b) after performing the first tr.delete, remap the original
$from.pos via tr.mapping.map($from.pos) and use that remapped position for the
second tr.delete; apply this change around the ZERO_WIDTH_CHAR checks to ensure
correct deletion targets for $from.nodeBefore and $from.nodeAfter.
- Around line 417-427: The duplicated check for event.key === 'Delete' is
redundant because this block already sits inside the outer if that ensures
event.key === 'Delete' && selection.empty; remove the inner event.key ===
'Delete' condition and similarly remove duplicate event.key checks in the other
backspace/delete branches (the blocks that reference ZERO_WIDTH_CHAR, $from.pos,
dispatch(state.tr.insertText(...)), and selection.empty) so each inner
conditional only tests the remaining conditions (e.g., $from.pos ===
$from.start(), content length, content !== ZERO_WIDTH_CHAR) before inserting the
ZERO_WIDTH_CHAR and calling event.preventDefault() and return true.
In @packages/components/src/sender/extensions/template/commands.ts:
- Around line 119-156: The code captures blocks via getAllTemplates(editor)
before the setTimeout, causing a stale closure; move the call to
getAllTemplates(editor) (and any use of editor.state/doc) inside the setTimeout
callback in focusFirstTemplate so template nodes/positions are recomputed after
document updates, then compute targetPos and create the TextSelection from the
fresh state before dispatching the transaction and focusing the view.
In @packages/components/src/sender/extensions/template/select/extension.ts:
- Around line 44-47: The parseHTML function currently calls JSON.parse on
element.getAttribute('data-options') which can throw on malformed JSON; wrap the
parse in a try/catch inside parseHTML (referencing parseHTML,
element.getAttribute('data-options'), and optionsStr) and on error return a safe
default (e.g., []), optionally logging or silently swallowing the parse error;
ensure the function still returns the same type in success and failure cases.
In @packages/components/src/sender/extensions/template/types.ts:
- Around line 63-80: The examples in the JSDoc use an invalid TemplateItem type
value 'template'; update both example blocks to use the correct union member
'block' instead. Locate the examples that construct items (the
ref<TemplateItem[]>() example and the static Template.configure({ items: [...]
}) example) and replace type: 'template' with type: 'block' so they match the
TemplateItem type definition.
In @packages/components/src/sender/extensions/template/utils.ts:
- Around line 43-51: The current templateSelect branch only stores the selected
option's value as content and drops other metadata; update the templateSelect
handling (the block checking node.type.name === 'templateSelect') to populate
the TemplateItem['select'] object with optional fields from node.attrs: include
options (node.attrs.options), value (node.attrs.value), and placeholder (if
present), while still preserving content (selectedOption?.value || ''), so the
pushed item contains type: 'select' and the full select payload { content,
options, value, placeholder } with absent fields omitted or undefined.
In @packages/components/src/sender/index.type.ts:
- Around line 93-104: The JSDoc for the autoSize prop in index.type.ts says the
default is { minRows: 1, maxRows: 3 } but the implementation in index.vue sets {
minRows: 1, maxRows: 5 }; pick one and make them consistent: either update the
autoSize default in index.vue to { minRows: 1, maxRows: 3 } (so the component
default matches the documented @default) or update the JSDoc in index.type.ts to
document { minRows: 1, maxRows: 5 } instead; ensure you reference the autoSize
prop and adjust both files so the documented @default and the runtime default
are identical.
🧹 Nitpick comments (20)
packages/components/src/sender/extensions/utils/id-generator.ts (1)
1-6: Duplicate documentation block.The file-level comment (lines 1-5) and the function-level JSDoc (lines 7-16) both describe "生成唯一 ID". Consider removing the file-level block since the function already has comprehensive documentation.
packages/components/src/sender/components/footer/index.vue (1)
37-42: Minor CSS redundancy.The
margin-left: autoon&-rightis redundant since the parent already usesjustify-content: space-between. You can remove it without affecting the layout.♻️ Suggested simplification
&-right { display: flex; align-items: center; gap: var(--tr-sender-gap); - margin-left: auto; }packages/components/src/sender-actions/voice-button/index.type.ts (1)
37-41: Potential API overlap withspeechConfig.onVoiceButtonClick.Both
onButtonClick(here) andspeechConfig.onVoiceButtonClick(inspeech.types.ts) provide button click interception. Consider documenting the precedence or consolidating to avoid consumer confusion.packages/components/src/sender/extensions/suggestion/extension.ts (1)
28-42: Watcher lacks cleanup on extension destroy.The
watchcall inonCreatedoesn't return a stop handle to clean up when the extension/editor is destroyed. If the editor is destroyed while the ref still exists, the watcher continues running.♻️ Suggested fix
+import { watch, isRef, type WatchStopHandle } from 'vue' + export const Suggestion = Extension.create<SuggestionOptions>({ name: 'suggestion', + + addStorage() { + return { + stopWatch: null as WatchStopHandle | null, + } + }, onCreate() { if (isRef(this.options.items)) { - watch( + this.storage.stopWatch = watch( this.options.items, () => { const tr = this.editor.state.tr tr.setMeta(SuggestionPluginKey, { type: 'update' }) this.editor.view.dispatch(tr) }, { deep: true }, ) } }, + + onDestroy() { + this.storage.stopWatch?.() + }, })packages/components/src/sender/extensions/template/utils.ts (1)
37-38: Static analysis false positive; consider pre-compiling regex for efficiency.The ReDoS warnings are false positives since
ZERO_WIDTH_CHARis a constant single character. However, creating the regex on each descendant callback is slightly wasteful.♻️ Pre-compile the regex
export const ZERO_WIDTH_CHAR = '\u200B' +const ZERO_WIDTH_REGEX = new RegExp(ZERO_WIDTH_CHAR, 'g') // Then use ZERO_WIDTH_REGEX.replaceAll or .replace() calls: -const content = (node.textContent || '').replace(new RegExp(ZERO_WIDTH_CHAR, 'g'), '') +const content = (node.textContent || '').replace(ZERO_WIDTH_REGEX, '')Also applies to: 52-53
packages/components/src/sender/extensions/template/select/template-select-view.vue (1)
226-228: Global keydown listener added per component instance.Each
template-select-viewinstance adds its owndocument.addEventListener('keydown', ...). With multiple selects, all handlers run on every keypress. The early return on line 153 prevents action, but consider using event delegation or checkingshowDropdownmore efficiently.For a minor optimization, consider only registering the listener when the dropdown opens and removing it on close, rather than keeping it mounted for the component's lifetime.
packages/components/src/sender/composables/useModeSwitch.ts (2)
70-72: Extract the magic number 300ms to a named constant.The 300ms timeout appears to coordinate with animation/transition timing but is embedded as a magic number. Extract it to a named constant (e.g.,
MODE_SWITCH_DURATION_MS) for maintainability and to clarify its purpose.♻️ Proposed refactor
+const MODE_SWITCH_DURATION_MS = 300 + export function useModeSwitch( props: SenderProps, editor: Ref<Editor | undefined>, editorRef: Ref<HTMLElement | null>, ): UseModeSwitchReturn { // ... const setMode = (mode: InputMode) => { if (currentMode.value === mode) return isAutoSwitching.value = true currentMode.value = mode nextTick(() => { if (editor.value) { editor.value.commands.focus('end') } setTimeout(() => { isAutoSwitching.value = false - }, 300) + }, MODE_SWITCH_DURATION_MS) }) }
34-35: Guard against missing ProseMirror element more defensively.The query
.querySelector('.ProseMirror')assumes a specific DOM structure. While there's an early return ifeditorElementis null, consider logging a warning in development to help debug structural issues.packages/components/src/sender/composables/useSenderCore.ts (1)
215-223: Another setTimeout(0) workaround - consider refactoring timing dependencies.Similar to the keyboard handler, using
setTimeout(0)to trigger overflow checking after content updates is a workaround. The{ deep: true }watch oneditor.value?.state.doc.contentcombined with a timeout suggests the overflow check needs to wait for layout.Consider using
watchPostEffector consolidating these timing-dependent operations.packages/components/src/sender-actions/upload-button/index.vue (1)
46-54: Extract file size validation logic to a utility function.The file size validation logic (converting MB to bytes, filtering oversized files) could be extracted to a utility function for reusability and testability.
packages/components/src/sender/extensions/suggestion/types.ts (2)
130-130: Move import statement to the top of the file.The
Refimport from Vue should be placed at the top of the file with other imports for consistency with TypeScript conventions.♻️ Suggested fix
+import type { Ref } from 'vue' + /** * Suggestion 插件类型定义 * * 包含建议项、高亮、插件配置和状态等类型定义 */ // ... (at line 130, remove) -import type { Ref } from 'vue'
260-269: Consider importingEditorStatefrom@tiptap/pm/stateinstead of redefining it.This minimal
EditorStateinterface duplicates the official ProseMirror type. Using the actual type from@tiptap/pm/statewould provide better type safety and ensure compatibility with future ProseMirror updates.♻️ Suggested approach
+import type { EditorState } from '@tiptap/pm/state' + /** * 插件 Key 类型 * * 用于访问插件状态 */ export interface SuggestionPluginKeyType { getState: (state: EditorState) => SuggestionState | undefined } - -/** - * EditorState 类型(来自 @tiptap/pm/state) - */ -export interface EditorState { - doc: unknown - selection: unknown - storedMarks: unknown - schema: unknown - [key: string]: unknown -}packages/components/src/sender/extensions/template/commands.ts (1)
10-11: Type assertion may be fragile—nodeAtreturnsNode | null.The
PMNodetype alias assumesnodeAtalways returns a node, but it can returnnull. ThegetAllTemplatesfunction pushes nodes fromdescendantswhich provides non-null nodes, so this works, but the type could be cleaner.♻️ Suggested improvement
-// ProseMirror Node 类型 -type PMNode = ReturnType<Editor['state']['doc']['nodeAt']> & { nodeSize: number } +import type { Node as PMNode } from '@tiptap/pm/model'packages/components/src/sender/extensions/mention/plugin.ts (3)
341-346: Hardcoded cursor offset assumes fixed node sizes—use actual node sizes instead.The cursor position calculation
range.from + 2assumes the mention node size is 1 and the space is 1. If the mention node's structure changes or attributes affect its size, this will break. Use the actual node sizes for robustness.♻️ Proposed fix
// 插入 mention 节点和空格 tr.insert(range.from, [mentionNode, spaceNode]) - // 设置光标到空格之后(mention 节点 + 空格 = +2) - const cursorPos = range.from + 2 + // 设置光标到空格之后 + const cursorPos = range.from + mentionNode.nodeSize + spaceNode.nodeSize tr.setSelection(TextSelection.create(tr.doc, cursorPos))
243-266: Popup appended todocument.bodymay cause z-index or scrolling issues in certain layouts.Appending the popup directly to
document.body(line 266) can cause issues with stacking contexts, especially in modals or iframes. Consider using a configurable mount point or Teleport-like pattern.
61-67: Resource cleanup variables should be scoped or managed more explicitly.The
component,popup, andcleanupvariables are declared in the outer function scope and mutated across plugin lifecycle methods. While this works, it can be error-prone. Consider encapsulating these in a state object for clarity.packages/components/src/sender/extensions/suggestion/plugin.ts (2)
54-66: Dynamic extension lookup may fail silently if extension is not registered.
getCurrentSuggestionslooks up the extension by name and accesses its options. If the extension isn't found, it returns an empty array silently. Consider adding a warning in development mode to help debug configuration issues.♻️ Suggested improvement
function getCurrentSuggestions(): SuggestionItem[] { const suggestionExtension = editor.extensionManager.extensions.find((ext) => ext.name === 'suggestion') + + if (!suggestionExtension && process.env.NODE_ENV === 'development') { + console.warn('[Suggestion] Extension not found in editor') + } + const options = suggestionExtension?.options const items = options?.items || options?.suggestions || []
259-262:justClosedflag withsetTimeout(0)is a timing-sensitive workaround.This pattern prevents immediate reopening but relies on JavaScript event loop timing. Consider using a more explicit state machine or debounce mechanism for robustness across different browsers/environments.
packages/components/src/sender-compat/index.vue (1)
30-61: Consider adding type declarations to avoidanycasts for extension access.The
(Sender as any).Suggestionand(Sender as any).Templatecasts are necessary but reduce type safety. Consider augmenting the Sender type to include these static properties, which would eliminate the need for eslint-disable comments.♻️ Suggested approach in a `.d.ts` file
// In sender/index.type.ts or a declaration file declare module './index.vue' { interface SenderComponent { Suggestion: typeof import('./extensions').Suggestion Template: typeof import('./extensions').Template Mention: typeof import('./extensions').Mention } }packages/components/src/sender/index.vue (1)
128-135: Consider using CSS variables for scrollbar colors.The scrollbar styling uses hardcoded
rgba(0, 0, 0, 0.2)andrgba(0, 0, 0, 0.3)values while thevariables.cssfile defines theme-aware scrollbar variables like--tr-suggestion-scrollbar-thumb-color. For consistent theming in dark mode, consider extracting these to CSS variables.♻️ Suggested refactor
&::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.2); + background: var(--tr-sender-scrollbar-thumb-color, rgba(0, 0, 0, 0.2)); border-radius: 4px; &:hover { - background: rgba(0, 0, 0, 0.3); + background: var(--tr-sender-scrollbar-thumb-hover-color, rgba(0, 0, 0, 0.3)); } }Then add corresponding variables to
variables.cssfor both light and dark themes.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (94)
.github/workflows/auto-build-main-module-docs.yml.github/workflows/auto-deploy-docs.ymldocs/src/guide/plugin-badge.mdpackages/components/package.jsonpackages/components/src/index.tspackages/components/src/sender-actions/action-button/index.vuepackages/components/src/sender-actions/clear-button/index.vuepackages/components/src/sender-actions/default-actions/index.vuepackages/components/src/sender-actions/index.tspackages/components/src/sender-actions/index.type.tspackages/components/src/sender-actions/submit-button/index.vuepackages/components/src/sender-actions/types/common.tspackages/components/src/sender-actions/types/index.tspackages/components/src/sender-actions/types/tooltip.tspackages/components/src/sender-actions/upload-button/index.type.tspackages/components/src/sender-actions/upload-button/index.vuepackages/components/src/sender-actions/utils/tooltip.tspackages/components/src/sender-actions/voice-button/index.type.tspackages/components/src/sender-actions/voice-button/index.vuepackages/components/src/sender-actions/voice-button/speech.types.tspackages/components/src/sender-actions/voice-button/useSpeechHandler.tspackages/components/src/sender-actions/voice-button/webSpeechHandler.tspackages/components/src/sender-actions/word-counter/index.vuepackages/components/src/sender-compat/index.tspackages/components/src/sender-compat/index.type.tspackages/components/src/sender-compat/index.vuepackages/components/src/sender/components/ActionButtons.vuepackages/components/src/sender/components/Block.vuepackages/components/src/sender/components/TemplateEditor.vuepackages/components/src/sender/components/editor-content/index.vuepackages/components/src/sender/components/footer/index.vuepackages/components/src/sender/components/global.d.tspackages/components/src/sender/components/layouts/MultiLineLayout.vuepackages/components/src/sender/components/layouts/SingleLineLayout.vuepackages/components/src/sender/composables/index.tspackages/components/src/sender/composables/useAutoSize.tspackages/components/src/sender/composables/useEditor.tspackages/components/src/sender/composables/useInputHandler.tspackages/components/src/sender/composables/useKeyboardHandler.tspackages/components/src/sender/composables/useKeyboardShortcuts.tspackages/components/src/sender/composables/useModeSwitch.tspackages/components/src/sender/composables/useSenderCore.tspackages/components/src/sender/composables/useSlotScope.tspackages/components/src/sender/composables/useSuggestionHandler.tspackages/components/src/sender/composables/useUndoRedo.tspackages/components/src/sender/context/index.tspackages/components/src/sender/context/types.tspackages/components/src/sender/extensions/index.tspackages/components/src/sender/extensions/mention/commands.tspackages/components/src/sender/extensions/mention/components/mention-list.vuepackages/components/src/sender/extensions/mention/components/mention-view.vuepackages/components/src/sender/extensions/mention/extension.tspackages/components/src/sender/extensions/mention/index.lesspackages/components/src/sender/extensions/mention/index.tspackages/components/src/sender/extensions/mention/plugin.tspackages/components/src/sender/extensions/mention/types.tspackages/components/src/sender/extensions/mention/utils.tspackages/components/src/sender/extensions/suggestion/extension.tspackages/components/src/sender/extensions/suggestion/index.lesspackages/components/src/sender/extensions/suggestion/index.tspackages/components/src/sender/extensions/suggestion/plugin.tspackages/components/src/sender/extensions/suggestion/suggestion-list.vuepackages/components/src/sender/extensions/suggestion/types.tspackages/components/src/sender/extensions/suggestion/utils/filter.tspackages/components/src/sender/extensions/suggestion/utils/highlight.tspackages/components/src/sender/extensions/template/block/extension.tspackages/components/src/sender/extensions/template/block/plugins.tspackages/components/src/sender/extensions/template/block/template-block-view.vuepackages/components/src/sender/extensions/template/commands.tspackages/components/src/sender/extensions/template/extension.tspackages/components/src/sender/extensions/template/index.tspackages/components/src/sender/extensions/template/select/dropdown-manager.tspackages/components/src/sender/extensions/template/select/extension.tspackages/components/src/sender/extensions/template/select/plugins.tspackages/components/src/sender/extensions/template/select/template-select-view.vuepackages/components/src/sender/extensions/template/types.tspackages/components/src/sender/extensions/template/utils.tspackages/components/src/sender/extensions/utils/id-generator.tspackages/components/src/sender/extensions/utils/index.tspackages/components/src/sender/extensions/utils/position.tspackages/components/src/sender/index.lesspackages/components/src/sender/index.tspackages/components/src/sender/index.type.tspackages/components/src/sender/index.vuepackages/components/src/sender/types/base.tspackages/components/src/sender/types/components.tspackages/components/src/sender/types/composables.tspackages/components/src/sender/types/context.tspackages/components/src/sender/types/editor.type.tspackages/components/src/sender/types/slots.tspackages/components/src/styles/components/index.csspackages/components/src/styles/components/sender.lesspackages/components/src/styles/variables.csspackages/components/vite.config.ts
💤 Files with no reviewable changes (9)
- packages/components/src/sender/composables/useKeyboardHandler.ts
- packages/components/src/sender/composables/useSuggestionHandler.ts
- packages/components/src/sender/composables/useInputHandler.ts
- packages/components/src/sender/components/Block.vue
- packages/components/src/sender/components/ActionButtons.vue
- packages/components/src/sender/components/global.d.ts
- packages/components/src/sender/components/TemplateEditor.vue
- packages/components/src/sender/types/editor.type.ts
- packages/components/src/sender/composables/useUndoRedo.ts
✅ Files skipped from review due to trivial changes (1)
- docs/src/guide/plugin-badge.md
🚧 Files skipped from review as they are similar to previous changes (41)
- packages/components/src/sender/extensions/template/extension.ts
- packages/components/src/sender-actions/types/tooltip.ts
- packages/components/src/sender/context/types.ts
- packages/components/src/sender/components/editor-content/index.vue
- packages/components/src/sender-compat/index.ts
- packages/components/src/sender-actions/types/common.ts
- packages/components/src/sender/extensions/template/block/extension.ts
- packages/components/src/sender/composables/useEditor.ts
- packages/components/src/sender/extensions/mention/index.ts
- packages/components/src/styles/components/sender.less
- packages/components/src/sender/extensions/suggestion/utils/filter.ts
- packages/components/src/sender/context/index.ts
- packages/components/src/sender/types/slots.ts
- packages/components/src/sender/composables/useSlotScope.ts
- packages/components/src/sender-actions/index.type.ts
- packages/components/src/sender/extensions/mention/commands.ts
- packages/components/src/sender-actions/voice-button/speech.types.ts
- packages/components/src/sender-actions/upload-button/index.type.ts
- packages/components/src/sender/extensions/mention/types.ts
- packages/components/src/sender-actions/clear-button/index.vue
- packages/components/src/sender/extensions/template/select/dropdown-manager.ts
- packages/components/src/sender/extensions/template/select/plugins.ts
- packages/components/src/sender/extensions/mention/components/mention-view.vue
- packages/components/src/sender-actions/types/index.ts
- packages/components/src/sender-actions/action-button/index.vue
- packages/components/package.json
- packages/components/src/sender/extensions/mention/utils.ts
- packages/components/src/sender/extensions/utils/index.ts
- packages/components/src/sender/extensions/suggestion/index.less
- packages/components/src/sender-actions/voice-button/index.vue
- packages/components/src/sender-compat/index.type.ts
- packages/components/src/sender/types/base.ts
- packages/components/src/sender/components/layouts/SingleLineLayout.vue
- packages/components/src/sender/extensions/index.ts
- packages/components/src/sender-actions/word-counter/index.vue
- packages/components/src/sender-actions/voice-button/webSpeechHandler.ts
- packages/components/src/sender/extensions/mention/index.less
- packages/components/src/sender-actions/default-actions/index.vue
- packages/components/src/sender-actions/submit-button/index.vue
- packages/components/src/sender/types/components.ts
- packages/components/src/sender-actions/utils/tooltip.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-06-18T09:29:47.974Z
Learnt from: SonyLeo
Repo: opentiny/tiny-robot PR: 119
File: packages/components/src/attachments/index.less:213-213
Timestamp: 2025-06-18T09:29:47.974Z
Learning: 在 packages/components/src/attachments/index.less 中,.tr-file-card__close 的背景色使用了硬编码的 rgb(194, 194, 194),但这个UI元素(关闭按钮)将会被直接替换为图标,所以不需要抽取为CSS变量。
Applied to files:
packages/components/src/sender/index.lesspackages/components/src/sender/index.vuepackages/components/src/styles/variables.css
📚 Learning: 2025-06-25T07:04:18.791Z
Learnt from: gene9831
Repo: opentiny/tiny-robot PR: 123
File: packages/components/src/bubble/message/Message.vue:22-40
Timestamp: 2025-06-25T07:04:18.791Z
Learning: In the Message component (packages/components/src/bubble/message/Message.vue), the renderer resolution is intentionally not reactive to type changes - the component is designed to work with a fixed type that doesn't change after initialization.
Applied to files:
packages/components/src/sender-compat/index.vuepackages/components/src/sender/index.vue
📚 Learning: 2025-05-27T03:35:11.008Z
Learnt from: gene9831
Repo: opentiny/tiny-robot PR: 59
File: packages/components/src/suggestion-popover/index.vue:131-133
Timestamp: 2025-05-27T03:35:11.008Z
Learning: In the SuggestionPopover component (packages/components/src/suggestion-popover/index.vue), the click handler can be bound unconditionally because the `show` computed property has a custom setter that prevents state mutations when `props.trigger === 'manual'`. This design centralizes trigger mode logic in the computed property rather than requiring conditional checks in event handlers.
Applied to files:
packages/components/src/sender/extensions/suggestion/suggestion-list.vue
🧬 Code graph analysis (17)
packages/components/src/sender/composables/useAutoSize.ts (2)
packages/components/src/sender/composables/index.ts (1)
useAutoSize(6-6)packages/components/src/sender/types/base.ts (2)
InputMode(13-13)AutoSize(264-264)
packages/components/src/sender/extensions/utils/id-generator.ts (1)
packages/components/src/sender/extensions/utils/index.ts (1)
generateId(5-5)
packages/components/src/sender-actions/voice-button/useSpeechHandler.ts (2)
packages/components/src/sender-actions/index.type.ts (2)
SpeechState(14-14)SpeechCallbacks(15-15)packages/components/src/sender-actions/voice-button/speech.types.ts (2)
SpeechState(35-39)SpeechCallbacks(5-11)
packages/components/src/sender/extensions/mention/extension.ts (3)
packages/components/src/sender/extensions/mention/types.ts (1)
MentionOptions(84-104)packages/components/src/sender/extensions/mention/plugin.ts (1)
createSuggestionPlugin(61-318)packages/components/src/sender/extensions/mention/commands.ts (1)
mentionCommands(12-45)
packages/components/src/sender/extensions/suggestion/utils/highlight.ts (2)
packages/components/src/sender/extensions/suggestion/types.ts (2)
SuggestionItem(48-76)SuggestionTextPart(10-13)packages/components/src/sender-compat/index.type.ts (1)
SuggestionTextPart(55-58)
packages/components/src/sender/extensions/mention/plugin.ts (7)
packages/components/src/sender/extensions/index.ts (3)
MentionPluginKey(6-6)MentionItem(8-8)suggestion(11-11)packages/components/src/sender/extensions/mention/index.ts (1)
MentionPluginKey(13-13)packages/components/src/sender/extensions/mention/types.ts (2)
MentionSuggestionState(109-129)MentionItem(15-39)packages/components/src/sender/index.ts (1)
MentionItem(47-47)packages/components/src/sender/extensions/utils/index.ts (2)
findTextRange(6-6)generateId(5-5)packages/components/src/sender/extensions/utils/position.ts (1)
findTextRange(26-65)packages/components/src/sender/extensions/utils/id-generator.ts (1)
generateId(17-19)
packages/components/src/sender/composables/useSenderCore.ts (17)
packages/components/src/sender/context/types.ts (1)
SenderContext(8-8)packages/components/src/sender/index.ts (1)
SenderContext(41-41)packages/components/src/sender/types/context.ts (1)
SenderContext(11-158)docs/.vitepress/theme/color-mode.ts (1)
emit(25-29)packages/components/src/sender/composables/index.ts (3)
useEditor(5-5)useModeSwitch(7-7)useAutoSize(6-6)packages/components/src/sender/composables/useEditor.ts (1)
useEditor(26-131)packages/components/src/sender/extensions/index.ts (4)
getTextWithTemplates(23-23)getTemplateStructuredData(23-23)MentionPluginKey(6-6)SuggestionPluginKey(11-11)packages/components/src/sender/extensions/template/index.ts (2)
getTextWithTemplates(16-16)getTemplateStructuredData(16-16)packages/components/src/sender/extensions/template/utils.ts (2)
getTextWithTemplates(20-23)getTemplateStructuredData(30-71)packages/components/src/sender/types/base.ts (1)
StructuredData(192-192)packages/components/src/sender/composables/useModeSwitch.ts (1)
useModeSwitch(11-99)packages/components/src/sender/composables/useKeyboardShortcuts.ts (1)
useKeyboardShortcuts(16-78)packages/components/src/sender/extensions/mention/plugin.ts (2)
view(238-316)MentionPluginKey(24-24)packages/components/src/sender/extensions/suggestion/plugin.ts (2)
view(424-504)SuggestionPluginKey(21-21)packages/components/src/sender/extensions/mention/index.ts (1)
MentionPluginKey(13-13)packages/components/src/sender/extensions/suggestion/index.ts (1)
SuggestionPluginKey(13-13)packages/components/src/sender/composables/useAutoSize.ts (1)
useAutoSize(16-97)
packages/components/src/sender-actions/voice-button/index.type.ts (3)
packages/components/src/sender-actions/types/common.ts (2)
TooltipContent(5-5)TooltipPlacement(12-24)packages/components/src/sender-actions/types/tooltip.ts (1)
TooltipContent(8-8)packages/components/src/sender-actions/voice-button/speech.types.ts (1)
SpeechConfig(25-32)
packages/components/src/sender/extensions/suggestion/index.ts (2)
packages/components/src/sender/extensions/suggestion/types.ts (2)
SuggestionItem(48-76)SuggestionOptions(135-249)packages/components/src/sender/extensions/suggestion/extension.ts (1)
Suggestion(16-52)
packages/components/src/index.ts (2)
packages/components/src/suggestion-pills/index.ts (1)
SuggestionPillButton(25-25)packages/components/src/attachments/index.type.ts (1)
ActionButton(45-49)
packages/components/src/sender/composables/useKeyboardShortcuts.ts (1)
packages/components/src/sender/types/composables.ts (2)
UseKeyboardShortcutsParams(21-27)UseKeyboardShortcutsReturn(32-35)
packages/components/src/sender/extensions/template/commands.ts (3)
packages/components/src/sender/extensions/template/types.ts (3)
TemplateItem(10-10)TemplateAttrs(40-50)TemplateSelectAttrs(15-35)packages/components/src/sender/types/base.ts (1)
TemplateItem(53-119)packages/components/src/sender/extensions/utils/id-generator.ts (1)
generateId(17-19)
packages/components/src/sender/composables/useModeSwitch.ts (4)
packages/components/src/sender-compat/index.type.ts (1)
SenderProps(70-93)packages/components/src/sender/index.type.ts (1)
SenderProps(32-227)packages/components/src/sender/types/composables.ts (1)
UseModeSwitchReturn(56-80)packages/components/src/sender/types/base.ts (1)
InputMode(13-13)
packages/components/src/sender/extensions/utils/position.ts (1)
packages/components/src/sender/extensions/utils/index.ts (1)
findTextRange(6-6)
packages/components/src/sender/extensions/template/index.ts (3)
packages/components/src/sender/extensions/template/types.ts (2)
TemplateItem(10-10)TemplateOptions(55-88)packages/components/src/sender/types/base.ts (1)
TemplateItem(53-119)packages/components/src/sender/extensions/template/extension.ts (1)
Template(14-25)
packages/components/src/sender/extensions/template/block/plugins.ts (1)
packages/components/src/sender/extensions/template/utils.ts (1)
ZERO_WIDTH_CHAR(13-13)
packages/components/src/sender/extensions/template/utils.ts (1)
packages/components/src/sender/types/base.ts (1)
TemplateItem(53-119)
🪛 actionlint (1.7.10)
.github/workflows/auto-build-main-module-docs.yml
31-31: "github.head_ref" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/reference/security/secure-use#good-practices-for-mitigating-script-injection-attacks for more details
(expression)
🪛 ast-grep (0.40.4)
packages/components/src/sender/extensions/template/utils.ts
[warning] 37-37: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(ZERO_WIDTH_CHAR, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
[warning] 52-52: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(ZERO_WIDTH_CHAR, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🪛 Biome (2.1.2)
packages/components/src/sender/extensions/template/block/plugins.ts
[error] 423-424: This branch can never execute. Its condition is a duplicate or covered by previous conditions in the if-else-if chain.
(lint/suspicious/noDuplicateElseIf)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build / build
packages/components/src/sender/extensions/template/block/plugins.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 14
🤖 Fix all issues with AI agents
In @packages/components/src/sender-actions/submit-button/index.vue:
- Around line 8-16: The computed isDisabled value is only visual but click
handlers still invoke actions; update the handleClick function (and any other
click handlers in this component that call submit/cancel) to early-return when
isDisabled.value is true: e.g. at start of handleClick check if
(isDisabled.value) return; then keep existing logic that checks loading.value to
call cancel() or submit(); ensure you reference the existing symbols isDisabled,
handleClick, submit, cancel, and loading so the guard is applied wherever clicks
currently trigger those actions; optionally add pointer-events: none to the
.is-disabled style for extra safety.
In @packages/components/src/sender-actions/upload-button/index.vue:
- Around line 50-52: The error message lists all oversized files but the emit
only passes oversized[0]; update the emit call in the upload-button component to
provide the full oversized array (e.g., emit('error', error, oversized) or
attach the array to the Error object) so consumers receive all oversized files;
keep the constructed Error (using props.maxSize and the mapped names) and
replace oversized[0] with the full oversized array (or error.files) when calling
emit('error').
In @packages/components/src/sender-compat/index.vue:
- Around line 190-213: The SenderCompat template duplicates Vue's v-model
handling by using v-model="modelValue" on the <Sender> component and also a
manual @update:model-value="(val) => (modelValue = val)" listener; remove the
explicit @update:model-value listener (the arrow handler) so only the v-model
expansion handles updates, leaving the rest of the props/handlers
(ref="senderRef", @submit="handleSubmit", @clear="handleClear", etc.) intact,
and verify no other code expects the manual listener to exist.
In @packages/components/src/sender/components/layouts/SingleLineLayout.vue:
- Around line 7-9: The v-if guard in the template is checking the Ref objects
themselves (context.showWordLimit and context.maxLength) instead of their
values, so change the condition to unwrap the refs (e.g.,
context.showWordLimit.value && context.maxLength.value) or use unref/context
helpers to evaluate their primitives; locate the useSenderContext() call and the
template v-if that references context.showWordLimit/context.maxLength (and keep
consistency with other checks like context.hasContent.value and
context.loading.value) and update the guard to use .value (or unref) so the
outer conditional reflects the actual runtime booleans/numbers.
In @packages/components/src/sender/composables/useEditor.ts:
- Around line 26-61: The editor initialization uses logical OR which treats
empty string as falsy; update the useTiptapEditor call inside useEditor to use
nullish coalescing for content: content: props.modelValue ?? props.defaultValue
?? '' so explicit empty strings are preserved; also update the watch on
props.modelValue (the watcher referenced later in useEditor) to assign using ??
when syncing into the editor and add { immediate: true } to that watch so the
editor is initialized consistently with the modelValue.
In @packages/components/src/sender/composables/useSenderCore.ts:
- Around line 186-191: The setTimeout(0) is masking a race between
setMode('multiple') and the editor commands; replace this fragile delay by
awaiting the actual DOM/state update instead: call Vue's nextTick (or make
setMode return a Promise) and then invoke editorInstance.commands.splitBlock()
and editorInstance.commands.focus(), ensuring you await setMode/nextTick so mode
switching is complete before running splitBlock/focus instead of using
setTimeout.
- Around line 109-123: Replace hard-coded extension name strings in
useSenderCore.ts with shared constants: define e.g. EXTENSION_TEMPLATE and
EXTENSION_MENTION in a central constants module (or the existing extensions
registry) and import them into useSenderCore.ts; update the checks that call
editor.value.extensionManager.extensions.some((ext) => ext.name === 'template')
and the mention check to compare ext.name against these constants, and update
any other files using the same string literals to use the shared constants to
ensure compile-time consistency.
In @packages/components/src/sender/extensions/suggestion/extension.ts:
- Around line 28-42: The watcher created in onCreate (watch(this.options.items,
...)) is never stopped, causing a memory leak; capture the stop handle returned
by watch (e.g., const stopWatcher = watch(...)) and store it on the extension
instance, and implement onDestroy to call that stop handle (stopWatcher()) and
clear the reference so the Vue watcher is torn down when the extension is
destroyed or recreated; ensure you reference the same watcher created for
this.options.items and keep the existing dispatch logic with SuggestionPluginKey
unchanged.
In @packages/components/src/sender/extensions/template/block/plugins.ts:
- Around line 613-619: Deleting $from.nodeBefore first shifts document positions
so the subsequent tr.delete for $from.nodeAfter uses stale positions; to fix,
compute the deletion ranges up front (capture start and end positions for both
$from.nodeBefore and $from.nodeAfter before mutating the transaction) or delete
in reverse document order, or after the first tr.delete remap the second
delete's positions via tr.mapping.map(...). Update the block that checks
ZERO_WIDTH_CHAR and calls tr.delete to either gather both ranges and apply
deletes descending, or map the second delete using tr.mapping.map to ensure
correct positions are used.
- Around line 226-231: The condition event.key === 'Backspace' inside the inner
if is redundant because this code already lives within the outer if (event.key
=== 'Backspace' && selection.empty); remove the duplicate event.key ===
'Backspace' checks from the inner blocks that test ($from.pos === $from.end() &&
content.length === 1 && content !== ZERO_WIDTH_CHAR && event.key ===
'Backspace') and the similar occurrences around the handlers at the other two
spots (the checks near where content.length === 1 at the later branches),
leaving the other boolean checks intact (e.g., $from.pos === $from.end(),
content.length === 1, content !== ZERO_WIDTH_CHAR, selection.empty) so behavior
is unchanged.
In @packages/components/src/sender/extensions/template/commands.ts:
- Around line 122-156: The closure captures `blocks` before the setTimeout, so
when document changes (e.g., setTemplateData) run the callback reads stale data;
move the call to getAllTemplates(editor) inside the setTimeout callback so
template nodes/positions are recalculated at execution time, then compute
targetPos and create the selection with TextSelection.create(state.doc,
targetPos), set the transaction selection and dispatch/view.focus as before in
focusFirstTemplate to ensure accurate cursor placement.
In @packages/components/src/sender/extensions/template/select/extension.ts:
- Around line 44-47: The parseHTML handler currently calls JSON.parse on
element.getAttribute('data-options') which can throw on malformed JSON; update
parseHTML to read the attribute, wrap JSON.parse in a try-catch, return the
parsed array on success and fallback to an empty array on failure, and
optionally emit a warning (e.g., via console.warn or the module logger)
including the raw attribute and parse error to aid debugging; target the
parseHTML function and the element.getAttribute('data-options') usage when
making this change.
In @packages/components/src/sender/types/base.ts:
- Around line 152-167: The doc example in handleSubmit referencing
StructuredData incorrectly checks for and filters a non-existent 'template'
variant; update the example to use the actual variant name 'block' everywhere
(change the sample data element type, the guard from data[0]?.type ===
'template' to data[0]?.type === 'block', the filter to item.type === 'block',
and the example variable/log names from templates to blocks) so it matches the
real TemplateItem/StructuredData types.
- Around line 169-190: The example in handleSubmit misuses a type guard by
checking data[0]?.type === 'mention' on a union StructuredData (TemplateItem[] |
MentionStructuredItem[]), implying mixed element types; change the logic in
handleSubmit to perform a type-safe filter that narrows items to mentions (i.e.,
filter with a user-defined type predicate or Extract to assert item is
MentionStructuredItem when item.type === 'mention'), avoid assuming mixed
arrays, and only access mention-only properties like value after that narrowing
(use a conditional check like mentions.length > 0 before mapping value).
🧹 Nitpick comments (12)
packages/components/src/sender/extensions/mention/plugin.ts (2)
207-225: Unreachablereturn trueafter fallback selection.Line 224 returns
trueeven when no item was selected (e.g.,filteredItems.length === 0). This prevents the default Enter/Tab behavior. If the panel is active but empty, consider allowing the default behavior or closing the panel instead.♻️ Suggested improvement
// 如果组件方法不可用,直接选择第一个提及项(fallback) if (pluginState.filteredItems.length > 0 && pluginState.range) { const firstItem = pluginState.filteredItems[0] insertMention(view, pluginState.range, firstItem) return true } - return true + // No items to select - allow default behavior + return false }
339-346: Cursor position assumes fixed node sizes.The calculation
range.from + 2assumes the mention node has size 1 and the space has size 1. While this is typically true for atomic inline nodes in ProseMirror, consider using the actual node sizes for robustness:♻️ More robust cursor positioning
// 删除触发文本(包括触发字符) tr.delete(range.from, range.to) // 插入 mention 节点和空格 tr.insert(range.from, [mentionNode, spaceNode]) - // 设置光标到空格之后(mention 节点 + 空格 = +2) - const cursorPos = range.from + 2 + // 设置光标到空格之后 + const cursorPos = range.from + mentionNode.nodeSize + spaceNode.nodeSize tr.setSelection(TextSelection.create(tr.doc, cursorPos))packages/components/src/sender/extensions/mention/utils.ts (1)
76-110: Consider handling mentions in all block types that support inline content.The filter
parent.type.name === 'paragraph'only collects mentions from paragraphs. Since the mention node is configured asgroup: 'inline', it can theoretically appear in any block type that accepts inline content (headings, list items, etc.). If the editor schema is extended with additional block types supporting mentions, this function would skip them. Consider checking the block type's ability to contain inline content rather than hardcoding paragraph filtering.packages/components/src/sender/extensions/template/utils.ts (1)
37-38: Consider using string literal orreplaceAllto simplify regex pattern.Static analysis flags the dynamic
RegExpconstruction as a potential ReDoS risk. While this is a false positive (sinceZERO_WIDTH_CHARis a constant literal), using a static pattern orreplaceAllimproves clarity and silences the warning.♻️ Cleaner alternatives
Option 1: Use literal regex pattern
- const content = (node.textContent || '').replace(new RegExp(ZERO_WIDTH_CHAR, 'g'), '') + const content = (node.textContent || '').replace(/\u200B/g, '')Option 2: Use
replaceAll(ES2021+)- const content = (node.textContent || '').replace(new RegExp(ZERO_WIDTH_CHAR, 'g'), '') + const content = (node.textContent || '').replaceAll(ZERO_WIDTH_CHAR, '')Apply the same fix at line 53 for text nodes.
Also applies to: 52-53
packages/components/src/sender-actions/action-button/index.vue (1)
22-48: Refactor to eliminate code duplication.The button element is rendered twice with nearly identical markup (lines 31-41 and lines 44-48), differing only in the tooltip wrapper. This violates the DRY principle and makes maintenance harder.
♻️ Proposed refactor to extract button into a reusable template
<script setup lang="ts"> import { computed } from 'vue' import { TinyTooltip } from '@opentiny/vue' import type { ActionButtonProps } from '../types/common' import { normalizeTooltipContent } from '../utils/tooltip' const props = withDefaults(defineProps<ActionButtonProps>(), { disabled: false, active: false, size: 32, tooltipPlacement: 'top', }) const tooltipRenderFn = computed(() => normalizeTooltipContent(props.tooltip)) const sizeStyle = computed(() => { const size = typeof props.size === 'number' ? `${props.size}px` : props.size return { fontSize: size } }) </script> <template> + <!-- 提取共享的按钮模板 --> + <template #button-content> + <button + :class="['tr-action-button', { active: props.active }]" + :disabled="props.disabled" + @focus.capture="(event: FocusEvent) => event.stopPropagation()" + > + <slot name="icon"> + <component :is="props.icon" :style="sizeStyle" /> + </slot> + </button> + </template> + <tiny-tooltip v-if="props.tooltip" :render-content="tooltipRenderFn" :placement="props.tooltipPlacement" effect="light" :visible-arrow="false" popper-class="tr-action-button-tooltip-popper" > - <button - :class="['tr-action-button', { active: props.active }]" - :disabled="props.disabled" - @focus.capture="(event: FocusEvent) => event.stopPropagation()" - > - <!-- 优先使用插槽,如果没有插槽则使用 icon prop --> - <slot name="icon"> - <component :is="props.icon" :style="sizeStyle" /> - </slot> - </button> + <template v-slot:default> + <slot name="button-content" /> + </template> </tiny-tooltip> - <!-- 无 tooltip 时直接渲染按钮 --> - <button v-else :class="['tr-action-button', { active: props.active }]" :disabled="props.disabled"> - <slot name="icon"> - <component :is="props.icon" :style="sizeStyle" /> - </slot> - </button> + <slot v-else name="button-content" /> </template>Note: Vue 3 doesn't support named template definitions in the same component. A better approach would be to extract the button to a separate render function or use a v-bind approach:
<script setup lang="ts"> import { computed } from 'vue' import { TinyTooltip } from '@opentiny/vue' import type { ActionButtonProps } from '../types/common' import { normalizeTooltipContent } from '../utils/tooltip' const props = withDefaults(defineProps<ActionButtonProps>(), { disabled: false, active: false, size: 32, tooltipPlacement: 'top', }) const tooltipRenderFn = computed(() => normalizeTooltipContent(props.tooltip)) const sizeStyle = computed(() => { const size = typeof props.size === 'number' ? `${props.size}px` : props.size return { fontSize: size } }) const buttonAttrs = computed(() => ({ class: ['tr-action-button', { active: props.active }], disabled: props.disabled, onFocusCapture: (event: FocusEvent) => event.stopPropagation() })) </script> <template> <component :is="props.tooltip ? TinyTooltip : 'div'" v-bind="props.tooltip ? { renderContent: tooltipRenderFn, placement: props.tooltipPlacement, effect: 'light', visibleArrow: false, popperClass: 'tr-action-button-tooltip-popper' } : {}" > <button v-bind="buttonAttrs"> <slot name="icon"> <component :is="props.icon" :style="sizeStyle" /> </slot> </button> </component> </template>packages/components/src/sender/types/base.ts (1)
125-133: Deprecated re-export may cause confusion.Re-exporting
MentionItemfrom this base types file with a deprecation notice creates an unnecessary import path. Since it's deprecated, consider whether this re-export serves a genuine backward compatibility need or if it should be removed to encourage direct imports from the extensions module.packages/components/src/sender-actions/voice-button/useSpeechHandler.ts (1)
1-8: Align error handling between initial start and restart pathThe new try/catch around
handler.start(callbacks)in the normal start path is a good improvement, but the restart path still callshandler.start(callbacks)without protection:if (speechState.isRecording) { handler.stop() speechState.isRecording = false setTimeout(() => { handler.start(callbacks) // no try/catch here }, 200) return }If a custom
SpeechHandlerthrows synchronously onstart, this branch will still surface an uncaught error.Consider mirroring the same error handling in the restart path:
Suggested fix
- // 短暂延迟后重新开始 - setTimeout(() => { - handler.start(callbacks) - }, 200) + // 短暂延迟后重新开始 + setTimeout(() => { + try { + handler.start(callbacks) + } catch (error) { + speechState.error = error instanceof Error ? error : new Error('启动失败') + optionsRef.value.onError?.(speechState.error) + } + }, 200)This keeps error reporting consistent regardless of whether recording is started fresh or restarted.
Also applies to: 20-22, 30-55, 65-90
packages/components/src/sender/composables/useSenderCore.ts (1)
215-223: Another setTimeout(0) workaround for overflow checking.Similar to the mode-switching issue, using
setTimeout(0)to defercheckOverflowafter content changes indicates a race condition with DOM updates. While less critical than the keyboard handler case, consider usingnextTickfor more predictable timing.💡 Suggested alternative
watch( () => editor.value?.state.doc.content, () => { - setTimeout(() => { + nextTick(() => { checkOverflow() - }, 0) + }) }, { deep: true }, )packages/components/src/sender/types/context.ts (1)
145-157: ClarifysetContent/getContentvalue shape in docs vs typesDocs say
setContentaccepts “HTML 或 JSON”,but the signature only allowsstring, andgetContentis documented as returning HTML. If you intend to support TipTap’s JSON document objects, consider widening the type (e.g.string | JSONContent) or narrowing the comments so they consistently describe string-only usage.packages/components/src/sender/extensions/suggestion/plugin.ts (1)
102-116:insertSuggestioncurrently replaces the entire document, ignoringrangeRight now
insertSuggestionignores therangeargument and always doeseditor.commands.setContent(item.content), so selecting a suggestion replaces the whole doc. That matches the current “global mode” whererangeis{ from: 0, to: doc.size }, but it will be wrong if you later support character-triggered or partial-range suggestions (as hinted by theSuggestionState.rangedocs).Consider switching to something like
editor.commands.insertContentAt(range, item.content)(or an equivalent command) so the implementation already respectsrangeand won’t silently misbehave once non-global modes are introduced.packages/components/src/sender/index.type.ts (2)
160-161: Consider tightening the extensions type for better type safety.The
Extension[] | any[]union allows arbitrary arrays to bypass type checking. While this provides flexibility, it sacrifices type safety and could lead to runtime errors if invalid extensions are passed.Consider either:
- Removing
any[]to enforce type safety (preferred)- Or documenting why
any[]is necessary if there are specific compatibility requirements♻️ Proposed refactor for stricter typing
- // eslint-disable-next-line @typescript-eslint/no-explicit-any - extensions?: Extension[] | any[] + extensions?: Extension[]
337-337: Improve type safety: specify the editor type instead ofunknown.Typing the editor as
unknownforces consumers to cast before accessing editor methods, reducing both type safety and developer experience. Consider typing it asEditorfrom@tiptap/coreor a more specific interface that exposes the public API.♻️ Suggested improvement
Add the Editor import at the top:
import type { Extension, Editor } from '@tiptap/core'Then update the slot definition:
- content?: (props: { editor: unknown }) => unknown + content?: (props: { editor: Editor }) => unknown
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (91)
packages/components/package.jsonpackages/components/src/index.tspackages/components/src/sender-actions/action-button/index.vuepackages/components/src/sender-actions/clear-button/index.vuepackages/components/src/sender-actions/default-actions/index.vuepackages/components/src/sender-actions/index.tspackages/components/src/sender-actions/index.type.tspackages/components/src/sender-actions/submit-button/index.vuepackages/components/src/sender-actions/types/common.tspackages/components/src/sender-actions/types/index.tspackages/components/src/sender-actions/types/tooltip.tspackages/components/src/sender-actions/upload-button/index.type.tspackages/components/src/sender-actions/upload-button/index.vuepackages/components/src/sender-actions/utils/tooltip.tspackages/components/src/sender-actions/voice-button/index.type.tspackages/components/src/sender-actions/voice-button/index.vuepackages/components/src/sender-actions/voice-button/speech.types.tspackages/components/src/sender-actions/voice-button/useSpeechHandler.tspackages/components/src/sender-actions/voice-button/webSpeechHandler.tspackages/components/src/sender-actions/word-counter/index.vuepackages/components/src/sender-compat/index.tspackages/components/src/sender-compat/index.type.tspackages/components/src/sender-compat/index.vuepackages/components/src/sender/components/ActionButtons.vuepackages/components/src/sender/components/Block.vuepackages/components/src/sender/components/TemplateEditor.vuepackages/components/src/sender/components/editor-content/index.vuepackages/components/src/sender/components/footer/index.vuepackages/components/src/sender/components/global.d.tspackages/components/src/sender/components/layouts/MultiLineLayout.vuepackages/components/src/sender/components/layouts/SingleLineLayout.vuepackages/components/src/sender/composables/index.tspackages/components/src/sender/composables/useAutoSize.tspackages/components/src/sender/composables/useEditor.tspackages/components/src/sender/composables/useInputHandler.tspackages/components/src/sender/composables/useKeyboardHandler.tspackages/components/src/sender/composables/useKeyboardShortcuts.tspackages/components/src/sender/composables/useModeSwitch.tspackages/components/src/sender/composables/useSenderCore.tspackages/components/src/sender/composables/useSlotScope.tspackages/components/src/sender/composables/useSuggestionHandler.tspackages/components/src/sender/composables/useUndoRedo.tspackages/components/src/sender/context/index.tspackages/components/src/sender/context/types.tspackages/components/src/sender/extensions/index.tspackages/components/src/sender/extensions/mention/commands.tspackages/components/src/sender/extensions/mention/components/mention-list.vuepackages/components/src/sender/extensions/mention/components/mention-view.vuepackages/components/src/sender/extensions/mention/extension.tspackages/components/src/sender/extensions/mention/index.lesspackages/components/src/sender/extensions/mention/index.tspackages/components/src/sender/extensions/mention/plugin.tspackages/components/src/sender/extensions/mention/types.tspackages/components/src/sender/extensions/mention/utils.tspackages/components/src/sender/extensions/suggestion/extension.tspackages/components/src/sender/extensions/suggestion/index.lesspackages/components/src/sender/extensions/suggestion/index.tspackages/components/src/sender/extensions/suggestion/plugin.tspackages/components/src/sender/extensions/suggestion/suggestion-list.vuepackages/components/src/sender/extensions/suggestion/types.tspackages/components/src/sender/extensions/suggestion/utils/filter.tspackages/components/src/sender/extensions/suggestion/utils/highlight.tspackages/components/src/sender/extensions/template/block/extension.tspackages/components/src/sender/extensions/template/block/plugins.tspackages/components/src/sender/extensions/template/block/template-block-view.vuepackages/components/src/sender/extensions/template/commands.tspackages/components/src/sender/extensions/template/extension.tspackages/components/src/sender/extensions/template/index.tspackages/components/src/sender/extensions/template/select/dropdown-manager.tspackages/components/src/sender/extensions/template/select/extension.tspackages/components/src/sender/extensions/template/select/plugins.tspackages/components/src/sender/extensions/template/select/template-select-view.vuepackages/components/src/sender/extensions/template/types.tspackages/components/src/sender/extensions/template/utils.tspackages/components/src/sender/extensions/utils/id-generator.tspackages/components/src/sender/extensions/utils/index.tspackages/components/src/sender/extensions/utils/position.tspackages/components/src/sender/index.lesspackages/components/src/sender/index.tspackages/components/src/sender/index.type.tspackages/components/src/sender/index.vuepackages/components/src/sender/types/base.tspackages/components/src/sender/types/components.tspackages/components/src/sender/types/composables.tspackages/components/src/sender/types/context.tspackages/components/src/sender/types/editor.type.tspackages/components/src/sender/types/slots.tspackages/components/src/styles/components/index.csspackages/components/src/styles/components/sender.lesspackages/components/src/styles/variables.csspackages/components/vite.config.ts
💤 Files with no reviewable changes (9)
- packages/components/src/sender/components/ActionButtons.vue
- packages/components/src/sender/composables/useSuggestionHandler.ts
- packages/components/src/sender/components/global.d.ts
- packages/components/src/sender/components/TemplateEditor.vue
- packages/components/src/sender/composables/useUndoRedo.ts
- packages/components/src/sender/types/editor.type.ts
- packages/components/src/sender/components/Block.vue
- packages/components/src/sender/composables/useInputHandler.ts
- packages/components/src/sender/composables/useKeyboardHandler.ts
🚧 Files skipped from review as they are similar to previous changes (43)
- packages/components/src/sender/extensions/suggestion/index.less
- packages/components/src/sender/extensions/utils/index.ts
- packages/components/src/sender/composables/useSlotScope.ts
- packages/components/src/sender-actions/voice-button/webSpeechHandler.ts
- packages/components/src/sender/composables/useKeyboardShortcuts.ts
- packages/components/src/sender/extensions/template/index.ts
- packages/components/src/sender-actions/upload-button/index.type.ts
- packages/components/src/sender-actions/voice-button/index.vue
- packages/components/src/sender/extensions/mention/index.less
- packages/components/src/sender-actions/index.type.ts
- packages/components/src/sender/types/components.ts
- packages/components/src/sender-compat/index.ts
- packages/components/src/sender/extensions/suggestion/index.ts
- packages/components/src/sender/extensions/mention/components/mention-view.vue
- packages/components/src/sender/extensions/mention/commands.ts
- packages/components/src/sender/context/index.ts
- packages/components/src/sender/components/editor-content/index.vue
- packages/components/src/sender/components/footer/index.vue
- packages/components/src/sender-compat/index.type.ts
- packages/components/src/sender/extensions/template/select/plugins.ts
- packages/components/src/sender-actions/default-actions/index.vue
- packages/components/src/sender/extensions/mention/index.ts
- packages/components/src/sender-actions/clear-button/index.vue
- packages/components/src/sender/extensions/suggestion/types.ts
- packages/components/src/sender/context/types.ts
- packages/components/src/sender-actions/types/tooltip.ts
- packages/components/src/sender/extensions/index.ts
- packages/components/src/sender/extensions/suggestion/utils/filter.ts
- packages/components/src/sender-actions/utils/tooltip.ts
- packages/components/src/sender-actions/voice-button/speech.types.ts
- packages/components/src/sender-actions/types/common.ts
- packages/components/src/sender/extensions/mention/components/mention-list.vue
- packages/components/src/sender/extensions/template/select/dropdown-manager.ts
- packages/components/src/sender/extensions/template/types.ts
- packages/components/src/sender/extensions/template/block/extension.ts
- packages/components/src/sender/composables/useAutoSize.ts
- packages/components/src/sender/extensions/mention/types.ts
- packages/components/src/sender-actions/types/index.ts
- packages/components/src/sender/types/composables.ts
- packages/components/src/styles/components/index.css
- packages/components/src/sender/composables/useModeSwitch.ts
- packages/components/src/sender-actions/voice-button/index.type.ts
- packages/components/src/styles/variables.css
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-06-18T09:29:47.974Z
Learnt from: SonyLeo
Repo: opentiny/tiny-robot PR: 119
File: packages/components/src/attachments/index.less:213-213
Timestamp: 2025-06-18T09:29:47.974Z
Learning: 在 packages/components/src/attachments/index.less 中,.tr-file-card__close 的背景色使用了硬编码的 rgb(194, 194, 194),但这个UI元素(关闭按钮)将会被直接替换为图标,所以不需要抽取为CSS变量。
Applied to files:
packages/components/src/styles/components/sender.lesspackages/components/src/sender/index.lesspackages/components/src/sender/index.vue
📚 Learning: 2025-06-25T07:04:18.791Z
Learnt from: gene9831
Repo: opentiny/tiny-robot PR: 123
File: packages/components/src/bubble/message/Message.vue:22-40
Timestamp: 2025-06-25T07:04:18.791Z
Learning: In the Message component (packages/components/src/bubble/message/Message.vue), the renderer resolution is intentionally not reactive to type changes - the component is designed to work with a fixed type that doesn't change after initialization.
Applied to files:
packages/components/src/sender/index.vue
📚 Learning: 2025-05-27T03:35:11.008Z
Learnt from: gene9831
Repo: opentiny/tiny-robot PR: 59
File: packages/components/src/suggestion-popover/index.vue:131-133
Timestamp: 2025-05-27T03:35:11.008Z
Learning: In the SuggestionPopover component (packages/components/src/suggestion-popover/index.vue), the click handler can be bound unconditionally because the `show` computed property has a custom setter that prevents state mutations when `props.trigger === 'manual'`. This design centralizes trigger mode logic in the computed property rather than requiring conditional checks in event handlers.
Applied to files:
packages/components/src/sender/extensions/suggestion/suggestion-list.vue
🧬 Code graph analysis (9)
packages/components/src/index.ts (2)
packages/components/src/suggestion-pills/index.ts (1)
SuggestionPillButton(25-25)packages/components/src/attachments/index.type.ts (1)
ActionButton(45-49)
packages/components/src/sender/extensions/utils/position.ts (1)
packages/components/src/sender/extensions/utils/index.ts (1)
findTextRange(6-6)
packages/components/src/sender/composables/useSenderCore.ts (10)
packages/components/src/sender/context/types.ts (1)
SenderContext(8-8)packages/components/src/sender/index.type.ts (1)
SenderProps(32-227)packages/components/src/sender/composables/index.ts (2)
useEditor(5-5)useModeSwitch(7-7)packages/components/src/sender/composables/useEditor.ts (1)
useEditor(26-131)packages/components/src/sender/extensions/template/utils.ts (2)
getTextWithTemplates(20-23)getTemplateStructuredData(30-71)packages/components/src/sender/extensions/mention/utils.ts (2)
getMentionStructuredData(76-110)getTextWithMentions(41-61)packages/components/src/sender/composables/useModeSwitch.ts (1)
useModeSwitch(11-99)packages/components/src/sender/composables/useKeyboardShortcuts.ts (1)
useKeyboardShortcuts(16-78)packages/components/src/sender/extensions/mention/plugin.ts (1)
MentionPluginKey(24-24)packages/components/src/sender/extensions/suggestion/plugin.ts (1)
SuggestionPluginKey(21-21)
packages/components/src/sender/extensions/mention/plugin.ts (5)
packages/components/src/sender/extensions/mention/types.ts (2)
MentionSuggestionState(109-129)MentionItem(15-39)packages/components/src/sender/extensions/suggestion/index.ts (1)
suggestion(32-40)packages/components/src/sender/extensions/utils/position.ts (1)
findTextRange(26-65)packages/components/src/sender/extensions/suggestion/types.ts (1)
EditorState(263-269)packages/components/src/sender/extensions/utils/id-generator.ts (1)
generateId(17-19)
packages/components/src/sender/index.ts (1)
packages/components/src/index.ts (3)
install(101-106)Sender(136-136)Sender(137-137)
packages/components/src/sender/extensions/suggestion/plugin.ts (2)
packages/components/src/sender/extensions/suggestion/types.ts (3)
SuggestionState(83-128)SuggestionOptions(135-249)SuggestionItem(48-76)packages/components/src/sender/extensions/suggestion/utils/filter.ts (1)
syncAutoComplete(25-57)
packages/components/src/sender/types/base.ts (3)
packages/components/src/sender/extensions/mention/types.ts (1)
MentionStructuredItem(46-55)packages/components/src/sender-actions/types/common.ts (2)
TooltipContent(5-5)TooltipPlacement(12-24)packages/components/src/sender-actions/types/tooltip.ts (1)
TooltipContent(8-8)
packages/components/src/sender/extensions/utils/id-generator.ts (1)
packages/components/src/sender/extensions/utils/index.ts (1)
generateId(5-5)
packages/components/src/sender/extensions/suggestion/extension.ts (4)
packages/components/src/sender/extensions/suggestion/index.ts (2)
Suggestion(12-12)SuggestionPluginKey(13-13)packages/components/src/sender/extensions/suggestion/types.ts (1)
SuggestionOptions(135-249)packages/components/src/sender/index.ts (1)
SuggestionOptions(58-58)packages/components/src/sender/extensions/suggestion/plugin.ts (2)
SuggestionPluginKey(21-21)createSuggestionPlugin(36-506)
🪛 ast-grep (0.40.4)
packages/components/src/sender/extensions/template/utils.ts
[warning] 37-37: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(ZERO_WIDTH_CHAR, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
[warning] 52-52: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(ZERO_WIDTH_CHAR, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🪛 Biome (2.1.2)
packages/components/src/sender/extensions/template/block/plugins.ts
[error] 423-424: This branch can never execute. Its condition is a duplicate or covered by previous conditions in the if-else-if chain.
(lint/suspicious/noDuplicateElseIf)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: e2e-test / e2e-test
🔇 Additional comments (38)
packages/components/vite.config.ts (1)
46-63: Approved: TipTap externals configuration is complete and correct.All nine @tiptap peerDependencies are properly externalized in the configuration. The regex pattern
/^@tiptap\/pm(\/.*)?$/correctly matches the imported ProseMirror submodules (@tiptap/pm/view, @tiptap/pm/state). Verification confirms all actual imports in the source files (sender composables and extensions) are covered by the externals declaration.packages/components/package.json (3)
22-22: LGTM! Trailing comma improves diff hygiene.The added trailing comma is a good practice that reduces diff noise when adding new dependencies in the future.
23-31: The TipTap peer dependencies at version ^3.11.0 are valid, compatible with Vue ^3.3.11, and have no known vulnerabilities. The semantic version constraint will permit updates to the latest stable 3.x releases (currently 3.13.0 as of December 2025) when dependencies are refreshed.
34-34: No issues found with @floating-ui/dom ^1.6.0.Version 1.6.0 exists and has no known security vulnerabilities. The dependency is appropriately versioned for production use.
packages/components/src/sender/extensions/utils/position.ts (2)
1-25: LGTM! Documentation is clear and comprehensive.The file-level and function-level documentation clearly explains the purpose and usage of this utility. The JSDoc includes parameter descriptions, return type, and a practical example.
26-65: Well-implemented utility function with sound logic.The position calculation and query extraction logic are correct:
- Uses
textBetweenwith the standard ProseMirror object replacement character (\ufffc)- Correctly finds the last trigger occurrence using
lastIndexOf- Properly calculates absolute positions:
from = $from.start() + lastCharIndex,to = $from.pos- The range includes the trigger character, which is correct for mention/suggestion replacement behavior
The edge cases are properly handled (empty selection, non-textblock, no trigger found, space validation).
packages/components/src/sender/extensions/mention/utils.ts (2)
13-28: LGTM!The
getMentionsfunction correctly traverses the document and extracts mention node attributes. The use ofanytype with the eslint-disable comment is acceptable for ProseMirror node traversal where precise typing is difficult.
41-61: LGTM!The function correctly retrieves the configured trigger character from the mention extension options with a sensible default, and properly handles both mention and text nodes during traversal.
packages/components/src/sender/extensions/utils/id-generator.ts (1)
17-19: LGTM!The ID generator is appropriate for UI element identification. The combination of timestamp and 7 random base-36 characters provides sufficient uniqueness for mention/template IDs without requiring cryptographic randomness.
packages/components/src/sender/extensions/mention/extension.ts (2)
30-69: LGTM!The attribute definitions correctly handle parsing from and rendering to HTML data attributes. The conditional returns for empty attributes prevent unnecessary attribute pollution in the rendered HTML.
71-92: LGTM!The HTML parsing and rendering configuration is well-structured. Using
data-mentionas the parsing selector and merging attributes withmergeAttributesfollows TipTap conventions correctly.packages/components/src/sender/extensions/mention/plugin.ts (5)
36-56: LGTM!The filter function correctly handles both
labelandvaluematching with case-insensitive comparison. Returning all items when query is empty is the expected behavior for initial display.
81-124: LGTM!The state management correctly handles meta-driven close events and document/selection changes. The early return when neither
docChangednorselectionSetis true prevents unnecessary recomputation.
144-175: Backspace handling converts mention to trigger character — verify this is intended UX.When the user presses Backspace immediately after a mention, the mention is deleted and replaced with the trigger character (e.g.,
@). This is a two-step delete pattern. Ensure this UX is intentional and documented, as some users might expect a single Backspace to fully remove the mention.
275-294: Potential race condition if decoration element is not yet rendered.The
querySelector('.mention-trigger')relies on the decoration being rendered synchronously before thisupdatecall. If the DOM update is deferred,referenceElementcould be null, and the popup won't be positioned correctly. Consider adding a fallback or usingrequestAnimationFrameif positioning issues arise.
310-316: LGTM!The
destroymethod correctly cleans up all resources: the autoUpdate subscription, the Vue component, and the popup element. This prevents memory leaks when the plugin is destroyed.packages/components/src/sender/extensions/template/extension.ts (1)
1-25: LGTM! Clean extension aggregator.The Template extension properly aggregates TemplateBlock and TemplateSelect, passes configuration options, and exposes the unified command set. The implementation follows TipTap patterns correctly.
packages/components/src/sender/extensions/template/block/template-block-view.vue (1)
1-77: LGTM! Well-implemented NodeView component.The template block view is cleanly implemented with proper TypeScript types, correct NodeView integration, and thoughtful styling that handles both normal and empty states.
packages/components/src/sender/extensions/template/select/template-select-view.vue (1)
1-390: LGTM! Comprehensive template select implementation.The template select component is well-architected with:
- Proper state management and ProseMirror plugin synchronization
- Robust keyboard navigation with wrapping and scrolling
- Correct Floating UI integration with cleanup
- Appropriate lifecycle management and event handling
- Clean separation of scoped and global styles
packages/components/src/sender/extensions/template/utils.ts (1)
43-51: Incomplete data extraction fortemplateSelectitems.As noted in a previous review, the current implementation only extracts the selected option's
valuebut discards available metadata. To preserve the full template state, consider storing the complete select configuration.♻️ Suggested enhancement
} else if (node.type.name === 'templateSelect') { - // 获取选中的值 const selectedOption = node.attrs.options.find((opt: { value: string }) => opt.value === node.attrs.value) - const content = selectedOption?.value || '' items.push({ type: 'select', - content, + content: selectedOption?.value || '', + options: node.attrs.options, + value: node.attrs.value, + placeholder: node.attrs.placeholder, })Ensure the
TemplateItem['select']type inindex.type.tsaccommodates these fields.Likely an incorrect or invalid review comment.
packages/components/src/sender-actions/action-button/index.vue (1)
34-34: Review focus event handling strategy.The focus event is stopped from propagating to parent handlers using
@focus.capturewithstopPropagation(). This might prevent legitimate parent focus handlers from executing, potentially breaking focus management in complex layouts.Consider whether this is truly necessary or if there's a more targeted solution.
packages/components/src/sender/composables/index.ts (1)
1-9: LGTM! Clean barrel export.The file provides a centralized entry point for sender composables with proper TypeScript type exports. No issues detected.
packages/components/src/styles/components/sender.less (1)
1-69: LGTM! Well-structured theming system.The LESS mixin correctly defines CSS custom properties for sender theming with both normal and compact size variants. All referenced global variables (
--tr-container-bg-default,--tr-text-primary,--tr-text-tertiary,--tr-container-bg-hover) are properly defined in the project's global variables file with support for both light and dark themes. The use of maps and theeach()function to generate variables is clean and maintainable.packages/components/src/sender/types/slots.ts (1)
1-86: LGTM! Clean and well-documented slot scope interface.The
SenderSlotScopeinterface provides a clear contract for slot-based components with appropriate typing and comprehensive JSDoc documentation.@tiptap/coreis properly declared as a peer dependency inpackage.json.packages/components/src/sender-actions/word-counter/index.vue (1)
1-28: Word counter visibility and styling look consistentThe computed
showguard and use of context values (characterCount,maxLength,isOverLimit,showWordLimit) are wired correctly, and the styles follow the sender CSS-variable scheme. No issues from this file alone.packages/components/src/sender/index.less (1)
6-19: Small size token mapping is clean and consistentThe
.tr-sender--smallblock cleanly remaps all relevant layout and button variables to their-smallcounterparts and keeps size concerns centralized. Looks good.packages/components/src/sender/components/layouts/MultiLineLayout.vue (1)
1-90: Multi-line layout wiring looks correctContext, slotScope, and EditorContent are composed cleanly, header/footer slots are flexible, and the multiline ProseMirror styling matches expectations for a textarea-like editor. No issues noticed here.
packages/components/src/sender-actions/index.ts (1)
1-23: LGTM! Clean barrel export structure.The centralized export pattern is well-organized and properly documented. All component and utility exports follow a consistent structure.
packages/components/src/sender/extensions/suggestion/utils/highlight.ts (1)
1-188: LGTM! Excellent documentation improvements.The updated JSDoc with concrete examples for all three highlight modes significantly improves the maintainability of this utility. The type import path consolidation aligns with the broader refactoring goals.
packages/components/src/sender/composables/useSenderCore.ts (2)
166-172: Plugin state checks assume plugins are correctly initialized.Direct calls to
getStateonMentionPluginKey,SuggestionPluginKey, andTemplateSelectDropdownPluginKeyassume these plugins are registered. If a plugin is not present,getStatemay return undefined, leading to runtime errors when accessing.activeor.isOpen.Likely an incorrect or invalid review comment.
156-205: Add cleanup for dynamically injected keyboard handlers.The
watchthat injectseditorProps.handleKeyDowndoes not provide cleanup when the editor instance changes or the component unmounts. Without explicit cleanup, handlers may leak between editor instances.Likely an incorrect or invalid review comment.
packages/components/src/index.ts (1)
16-31: SenderCompat and sender-action components are correctly wired into the public surfaceThe additions cleanly integrate SenderCompat and the action components into both the plugin
componentsarray and the named exports, and theuseSenderContextre-export from./senderkeeps the context API discoverable. No issues from an API-surface or install perspective.Also applies to: 68-98, 139-167
packages/components/src/sender/index.vue (1)
18-71: Core/context + layout split inSender.vuelooks cleanThe component now focuses solely on wiring
useSenderCoreinto layout selection and slot passthrough, with mode/size/disabled styling driven fromcontext. The separation intoSingleLineLayout/MultiLineLayoutand thedefineExpose(expose)pattern read cleanly and should make future core changes much easier to contain.Also applies to: 75-137
packages/components/src/sender/extensions/suggestion/suggestion-list.vue (1)
41-87: Suggestion list callback API and UI behavior look solidThe move from emits to optional callback props (
onSelect/onMouseEnter/onMouseLeave) makes the list easier to embed viaVueRenderer, and the keyboard index watcher + scroll-into-view logic is straightforward. The click handler passingitem.contentintoonSelectmatches how the plugin resolves the selected item, and the updated styling/transition hooks are consistent with the rest of the design.Also applies to: 105-137, 139-217
packages/components/src/sender/index.ts (1)
14-35: CompositeSenderexport and type surface are well-structuredAugmenting
SenderComponentwithinstall, static extensions (Mention/Suggestion/Template), and helper functions while re-exporting the core and extension types plususeSenderContextgives consumers a single, coherent entrypoint. This aligns nicely with howSenderCompatand root index wire things up.Also applies to: 37-62
packages/components/src/sender/index.type.ts (3)
1-15: LGTM! Clean modular type organization.The import and re-export structure effectively centralizes the type surface while maintaining modularity. The separation of concerns (base, composables, components, context, slots) will improve maintainability.
18-23: LGTM!The DefaultConfig interface provides a clean contract for default configuration values.
236-311: LGTM! Well-designed event interface.The emit signatures are well-structured, especially the enhanced
submitevent that provides both plain text and optional structured data. The JSDoc examples clearly demonstrate the intended usage patterns.
packages/components/src/sender/components/layouts/SingleLineLayout.vue
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In @packages/components/src/sender/extensions/mention/commands.ts:
- Around line 33-45: The deleteMention command currently modifies the
transaction but never dispatches it and doesn't validate the id; update the
deleteMention function in commands.ts to accept the optional dispatch parameter
(per TipTap v3 command signature), validate that id is provided and non-empty
before scanning, and if a mention node is found and tr.delete was called, invoke
dispatch(tr) to apply the transaction (otherwise return false). Ensure the
command returns a boolean indicating success/failure and uses the existing
NODE_TYPE_NAMES.MENTION reference and the tr/state parameters as before.
In @packages/components/src/sender/extensions/mention/extension.ts:
- Around line 82-93: The renderHTML method accesses node.attrs.id,
node.attrs.label, node.attrs.value and concatenates this.options.char with
node.attrs.label without null checks; update renderHTML to defensively handle
null/undefined by coalescing each attribute to an empty string (or a safe
default) before casting/using them, e.g., compute local variables like id =
node.attrs.id ?? '' (label = node.attrs.label ?? '' value = node.attrs.value ??
'' and char = this.options.char ?? ''), use those variables when building
mergeAttributes and when concatenating the displayed text so no "null" or
invalid attributes are emitted.
In @packages/components/src/sender/extensions/template/block/plugins.ts:
- Around line 116-588: The function keyboardNavigationPlugin has very high
cyclomatic complexity; extract the large inline conditionals into focused
handler functions (e.g., handleArrowLeft, handleArrowRight,
handleArrowsInTemplateBlock, handleBackspace, handleBackspaceSelection,
handleDelete, handleDeleteSelection) that accept (view, event, state, selection,
$from, dispatch) and return a boolean, move the corresponding code blocks into
those functions, and then simplify handleKeyDown to call each handler based on
isKey/isAnyKey checks (returning the handler's boolean) so
keyboardNavigationPlugin only composes handlers and remains concise and
testable.
- Around line 419-423: The inner isKey(event, 'DELETE') checks are redundant
because the code is already inside the outer if (isKey(event, 'DELETE') &&
selection.empty) block; remove the isKey(event, 'DELETE') sub-conditions at the
places that check $from.pos === $from.start() && content.length === 1 && content
!== ZERO_WIDTH_CHAR, the later condition around content length and
ZERO_WIDTH_CHAR, and the third similar check (the ones that currently include
isKey(event, 'DELETE')), keeping the other predicates ($from.pos, $from.start(),
content length, ZERO_WIDTH_CHAR, selection.empty) intact so the logic and early
returns remain unchanged.
- Around line 228-232: The inner checks for isKey(event, 'BACKSPACE') are
redundant because the surrounding guard already ensures isKey(event,
'BACKSPACE') && selection.empty; remove the repeated isKey(event, 'BACKSPACE')
conditions from the inner if expressions (the ones that test $from.pos ===
$from.end() && content.length === 1 && content !== ZERO_WIDTH_CHAR, and the
similar checks later) so they only rely on the existing outer isKey(event,
'BACKSPACE') && selection.empty guard; keep the other conditions ($from.pos ===
$from.end(), content.length === 1, content !== ZERO_WIDTH_CHAR, ZERO_WIDTH_CHAR,
selection.empty, etc.) intact to preserve behavior.
- Around line 373-382: The else-if branch using the same check beforeNode.text
=== ZERO_WIDTH_CHAR is unreachable because an earlier if already tests that
exact condition; either remove this block or change its condition to the
intended complementary check so it can ever run. Locate the block with symbols
index, $from.pos, beforeNode.text, ZERO_WIDTH_CHAR, selection, dispatch,
state.tr.delete, and event.preventDefault in plugins.ts and either delete the
unreachable else-if, or replace its condition with the correct predicate (for
example the logical complement or a check against a different node/character
that matches the intended logic) so the deletion and event.preventDefault() code
executes only when appropriate.
In @packages/components/src/sender/extensions/template/select/extension.ts:
- Around line 56-58: The renderHTML arrow functions (renderHTML) call
JSON.stringify on attributes.options without protection; wrap those
JSON.stringify calls in try/catch similar to parseHTML, log or report the caught
error (include error message and context) and fall back to a safe serializable
value (e.g., '{}' or 'null') for the 'data-options' attribute so serialization
failures (circular refs/non-serializable values) don’t throw and break
rendering.
In @packages/components/src/sender/extensions/template/select/plugins.ts:
- Around line 75-113: The deletion loop in
selectZeroWidthPlugin.appendTransaction collects todoPositions then deletes in
forward order, which causes later positions to shift and produce incorrect
deletions; change the deletion application to process todoPositions in reverse
order (or sort by position descending) before calling tr.delete (the entries in
todoPositions and use tr.delete(pos, pos + 1)) so earlier deletions do not
invalidate later offsets.
🧹 Nitpick comments (9)
packages/components/src/sender-actions/upload-button/index.vue (1)
33-59: Internationalize error messages and standardize error event payload.The validation logic is sound, but two improvements should be made:
Hardcoded Chinese error messages (lines 40, 51): These should be internationalized or made configurable for broader usability across language-specific deployments.
Inconsistent error event parameters: The
maxCountvalidation (line 41) emits only the error, while themaxSizevalidation (line 52) emits both the error and the oversized files array. While the type signature(e: 'error', error: Error, files?: File[]): voidsupports the optional second parameter, the implementation should be consistent—either always pass the problematic files or document why they differ.packages/components/src/sender/composables/useModeSwitch.ts (1)
83-88: Consider debouncing resize handling more robustly.The
requestAnimationFrameapproach doesn't fully throttle rapid resize events—multiple callbacks can still queue. While theisAutoSwitchingguard provides protection, consider usinguseDebounceFnor tracking a pending rAF ID to cancel stale frames.♻️ Optional: Cancel stale rAF callbacks
+ let rafId: number | null = null + useResizeObserver(containerRef, () => { - // 使用 requestAnimationFrame 避免频繁触发 - requestAnimationFrame(() => { - checkOverflow() - }) + if (rafId !== null) { + cancelAnimationFrame(rafId) + } + rafId = requestAnimationFrame(() => { + rafId = null + checkOverflow() + }) })packages/components/src/sender/composables/useSenderCore.ts (1)
216-224: Consider using TipTap'sonUpdatecallback instead of deep watchingstate.doc.content.Deep watching
editor.value?.state.doc.contentwithdeep: truewalks the entire document tree on each change, which is expensive by TipTap design. The intended pattern is to use the editor'sonUpdatecallback, which receives transaction metadata and allows you to checktransaction.docChangedto skip unnecessary work. For a sender component with typically short inputs, the current approach is acceptable, but usingonUpdatealigns with TipTap best practices and avoids potential issues if content grows larger.packages/components/src/sender/extensions/template/select/plugins.ts (2)
137-191: Add edge case handling for null nodes in Backspace logic.In Scenario 3 (lines 169-182), when
afterNodeis atemplateSelectandbeforeNodeis aTEMPLATE_BLOCK, the code returnsfalseto delegate handling. However, there's no explicit handling for whenbeforeNodeisnullin this scenario, which could occur at the start of a paragraph.🛡️ Suggested edge case handling
// 场景3:光标后面是 templateSelect,前面是普通文本 // 删除文本的最后一个字符 if (afterNode?.type.name === NODE_TYPE_NAMES.TEMPLATE_SELECT) { + // 如果前面没有节点(段落开头),不处理 + if (!beforeNode) { + return false + } // 如果前面是普通文本(非零宽字符) if (beforeNode?.isText && beforeNode.text !== ZERO_WIDTH_CHAR) { dispatch(state.tr.delete($from.pos - 1, $from.pos)) event.preventDefault() return true } // 如果前面是 template 节点,不处理,让 TemplateBlock 插件处理 if (beforeNode?.type.name === NODE_TYPE_NAMES.TEMPLATE_BLOCK) { return false } }
193-246: Add edge case handling for null nodes in Delete logic.Similar to the Backspace scenarios, the Delete key handling in Scenario 3 (lines 224-237) should explicitly handle the case when
afterNodeisnull(at the end of a paragraph).🛡️ Suggested edge case handling
// 场景3:光标前面是 templateSelect,后面是普通文本 // 删除文本的第一个字符 if (beforeNode?.type.name === NODE_TYPE_NAMES.TEMPLATE_SELECT) { + // 如果后面没有节点(段落末尾),不处理 + if (!afterNode) { + return false + } // 如果后面是普通文本(非零宽字符) if (afterNode?.isText && afterNode.text !== ZERO_WIDTH_CHAR) { dispatch(state.tr.delete($from.pos, $from.pos + 1)) event.preventDefault() return true } // 如果后面是 template 节点,不处理,让 TemplateBlock 插件处理 if (afterNode?.type.name === NODE_TYPE_NAMES.TEMPLATE_BLOCK) { return false } }packages/components/src/sender/extensions/suggestion/extension.ts (1)
36-51: Consider documenting the reactive behavior.The
onCreatehook sets up a Vue watcher only whenitemsis a ref. This reactive pattern is valuable but might not be immediately obvious to developers extending or using this extension.📝 Suggested documentation enhancement
onCreate() { + // Setup reactive watching for items when provided as a Vue ref + // This allows the suggestion list to update automatically when items change if (isRef(this.options.items)) { // 保存到实例 storage this.storage.watchStopHandle = watch( this.options.items, () => { // 触发更新 const tr = this.editor.state.tr // 使用 SuggestionPluginKey 确保插件能正确接收更新 tr.setMeta(SuggestionPluginKey, { type: 'update' }) this.editor.view.dispatch(tr) }, { deep: true }, ) } },packages/components/src/sender/extensions/template/select/extension.ts (1)
100-104: Document the TypeScript suppression reason.The
@ts-expect-errorcomment mentions "Vue SFC type compatibility" but doesn't provide context about why this is necessary or link to relevant issues.📝 Enhanced documentation
// 使用 Vue 组件渲染 addNodeView() { - // @ts-expect-error - Vue SFC type compatibility + // @ts-expect-error - VueNodeViewRenderer expects a specific component type + // but Vue SFC types don't always match perfectly. This is a known limitation + // when using TipTap with Vue 3. See: https://github.com/ueberdosis/tiptap/issues/xxxx return VueNodeViewRenderer(TemplateSelectView) },packages/components/src/sender/extensions/template/commands.ts (1)
123-157: Consider using editor lifecycle hooks instead of setTimeout.The
focusFirstTemplatecommand usessetTimeout(..., 0)(line 129) to defer focus until after document updates. While this pattern works, it's fragile and dependent on the event loop timing.Consider using TipTap's lifecycle hooks for more reliable timing:
♻️ Alternative approach using editor events
focusFirstTemplate: () => ({ editor }: { editor: Editor }) => { const blocks = getAllTemplates(editor) // Queue the focus operation after the next editor update const cleanup = editor.on('update', () => { cleanup() // Remove the listener after first execution const { state, view } = editor const tr = state.tr try { let targetPos: number if (blocks.length === 0) { targetPos = state.doc.content.size - 1 } else { const { node, pos } = blocks[0] const contentLength = node.textContent?.length || 0 targetPos = pos + 1 + contentLength } const selection = TextSelection.create(state.doc, targetPos) tr.setSelection(selection) view.dispatch(tr) view.focus() } catch (error) { console.error('[focusFirstTemplate] 设置光标失败', error) } }) return true },packages/components/src/sender/extensions/template/block/plugins.ts (1)
18-95: Consider refactoring for improved maintainability.The
handleZeroWidthCharLogicfunction is quite complex (~77 lines) and handles multiple distinct concerns (paragraph boundaries, empty blocks, consecutive blocks). Consider extracting each concern into separate helper functions to improve readability and testability.♻️ Suggested refactoring approach
Break down into focused helpers:
function handleParagraphBoundaries(node, pos, parent, todoPositions) { /* lines 26-38 */ } function handleSingleZeroWidth(node, pos, todoPositions) { /* lines 41-43 */ } function handleEmptyTemplateBlock(node, pos, todoPositions) { /* lines 47-49 */ } function handleConsecutiveBlocks(node, pos, parent, todoPositions) { /* lines 52-73 */ }Then call them in sequence from the main function.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (25)
packages/components/src/sender-actions/upload-button/index.type.tspackages/components/src/sender-actions/upload-button/index.vuepackages/components/src/sender/composables/useAutoSize.tspackages/components/src/sender/composables/useKeyboardShortcuts.tspackages/components/src/sender/composables/useModeSwitch.tspackages/components/src/sender/composables/useSenderCore.tspackages/components/src/sender/extensions/constants.tspackages/components/src/sender/extensions/mention/commands.tspackages/components/src/sender/extensions/mention/components/mention-list.vuepackages/components/src/sender/extensions/mention/extension.tspackages/components/src/sender/extensions/mention/plugin.tspackages/components/src/sender/extensions/mention/utils.tspackages/components/src/sender/extensions/suggestion/extension.tspackages/components/src/sender/extensions/suggestion/plugin.tspackages/components/src/sender/extensions/template/block/extension.tspackages/components/src/sender/extensions/template/block/plugins.tspackages/components/src/sender/extensions/template/commands.tspackages/components/src/sender/extensions/template/extension.tspackages/components/src/sender/extensions/template/select/extension.tspackages/components/src/sender/extensions/template/select/plugins.tspackages/components/src/sender/extensions/template/utils.tspackages/components/src/sender/extensions/utils/index.tspackages/components/src/sender/extensions/utils/keyboard.tspackages/components/src/sender/types/base.tspackages/components/vite.config.ts
🚧 Files skipped from review as they are similar to previous changes (10)
- packages/components/src/sender/extensions/template/extension.ts
- packages/components/vite.config.ts
- packages/components/src/sender/extensions/mention/utils.ts
- packages/components/src/sender/extensions/mention/components/mention-list.vue
- packages/components/src/sender/extensions/utils/index.ts
- packages/components/src/sender/types/base.ts
- packages/components/src/sender/extensions/template/block/extension.ts
- packages/components/src/sender/composables/useKeyboardShortcuts.ts
- packages/components/src/sender/composables/useAutoSize.ts
- packages/components/src/sender-actions/upload-button/index.type.ts
🧰 Additional context used
🧬 Code graph analysis (4)
packages/components/src/sender/extensions/suggestion/extension.ts (3)
packages/components/src/sender/extensions/suggestion/types.ts (1)
SuggestionOptions(135-249)packages/components/src/sender/extensions/constants.ts (1)
EXTENSION_NAMES(7-14)packages/components/src/sender/extensions/suggestion/plugin.ts (2)
SuggestionPluginKey(23-23)createSuggestionPlugin(38-510)
packages/components/src/sender/extensions/template/commands.ts (4)
packages/components/src/sender/extensions/constants.ts (2)
NODE_TYPE_NAMES(22-33)USER_API_TYPES(63-72)packages/components/src/sender/types/base.ts (1)
TemplateItem(53-119)packages/components/src/sender/extensions/template/types.ts (3)
TemplateItem(10-10)TemplateAttrs(40-50)TemplateSelectAttrs(15-35)packages/components/src/sender/extensions/utils/id-generator.ts (1)
generateId(17-19)
packages/components/src/sender/extensions/template/block/plugins.ts (3)
packages/components/src/sender/extensions/constants.ts (2)
NODE_TYPE_NAMES(22-33)PLUGIN_KEY_NAMES(38-55)packages/components/src/sender/extensions/template/utils.ts (1)
ZERO_WIDTH_CHAR(14-14)packages/components/src/sender/extensions/utils/index.ts (2)
isKey(7-7)isAnyKey(7-7)
packages/components/src/sender/extensions/mention/plugin.ts (8)
packages/components/src/sender/extensions/mention/index.ts (1)
MentionPluginKey(13-13)packages/components/src/sender/extensions/mention/types.ts (2)
MentionSuggestionState(109-129)MentionItem(15-39)packages/components/src/sender/extensions/constants.ts (2)
PLUGIN_KEY_NAMES(38-55)NODE_TYPE_NAMES(22-33)packages/components/src/sender/extensions/suggestion/plugin.ts (1)
view(428-508)packages/components/src/sender/extensions/utils/index.ts (4)
findTextRange(6-6)isKey(7-7)isAnyKey(7-7)generateId(5-5)packages/components/src/sender/extensions/utils/position.ts (1)
findTextRange(26-65)packages/components/src/sender/extensions/utils/keyboard.ts (2)
isKey(25-27)isAnyKey(40-42)packages/components/src/sender/extensions/utils/id-generator.ts (1)
generateId(17-19)
🪛 ast-grep (0.40.4)
packages/components/src/sender/extensions/template/utils.ts
[warning] 38-38: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(ZERO_WIDTH_CHAR, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
[warning] 53-53: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(ZERO_WIDTH_CHAR, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🪛 Biome (2.1.2)
packages/components/src/sender/extensions/template/block/plugins.ts
[error] 424-426: This branch can never execute. Its condition is a duplicate or covered by previous conditions in the if-else-if chain.
(lint/suspicious/noDuplicateElseIf)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build / build
🔇 Additional comments (40)
packages/components/src/sender-actions/upload-button/index.vue (6)
1-18: LGTM!The component setup is clean. Props are correctly defined with appropriate defaults, and the use of
toRefsfor reactive file dialog options follows Vue 3 best practices.
20-23: LGTM!The disabled state composition correctly combines local and context-level disabled states, allowing both component-level and system-level control.
25-30: LGTM!The file dialog is correctly configured with reactive options, and the
reset: truedefault ensures a clean state after each file selection.
61-65: LGTM!The click handler appropriately guards against disabled state before opening the file dialog.
67-70: LGTM!Exposing the
openmethod provides a clean programmatic API for parent components to trigger file selection.
73-82: LGTM!The template correctly wires all props and events to the underlying
ActionButton, with appropriate fallback for the icon prop.packages/components/src/sender/composables/useModeSwitch.ts (6)
1-9: LGTM!Clean module header with appropriate imports from Vue, VueUse, and Tiptap.
11-31: LGTM!State initialization and container resolution are well-structured. The
useTimeoutFncorrectly manages the auto-switching debounce withimmediate: false.
37-61: LGTM!The overflow detection logic using
scrollWidth > clientWidthis a reliable browser-native approach. The mode switching conditions are well-guarded with appropriate early returns.
67-81: LGTM!The mode setter correctly guards against redundant updates and properly manages the auto-switching flag with timeout reset. Focusing the editor at 'end' after mode switch is a good UX pattern.
90-98: LGTM!The watcher correctly synchronizes external mode prop changes with internal state, updating both
initialModeand triggeringsetMode.
100-106: LGTM!Clean return object matching the expected
UseModeSwitchReturninterface.packages/components/src/sender/composables/useSenderCore.ts (8)
1-29: LGTM!Well-organized imports with clear separation between external dependencies, internal extensions, and composables.
30-52: LGTM!Clear interface definition for the composable's return type with well-documented context and expose objects.
70-95: LGTM!Computed properties are well-structured with appropriate null guards. The
canSubmitlogic correctly aggregates all submission prerequisites.
101-133: LGTM with a note on mutual exclusivity.The submit flow correctly handles Template and Mention as mutually exclusive scenarios. The fallback to
getText()when no extension produces content is appropriate.If both extensions were ever loaded simultaneously (contrary to the documented design), Template would take precedence due to the if/else-if structure—this behavior seems intentional.
157-206: LGTM!The dynamic keyboard handler injection is well-implemented. The plugin state checks ensure popups/dropdowns handle their own keyboard events. The
setTimeout(0)for newline in single mode is an acceptable pattern to ensure mode transition completes before inserting the line break.
230-254: LGTM!Helper methods are concise with appropriate null-safety via optional chaining. The
clear()method correctly combines content clearing with focus restoration.
260-288: LGTM!Context assembly is well-structured with appropriate use of
computed()for props needing default values andtoRef()for optional props. Theprovide()call correctly exposes the context for child components.
294-306: LGTM!Clean return structure providing both the context for dependency injection and the expose object for parent component access.
packages/components/src/sender/extensions/template/select/plugins.ts (3)
13-32: LGTM!The
TemplateSelectDropdownStateinterface andPluginKeyare well-defined and properly typed for managing dropdown state in ProseMirror.
34-73: LGTM!The dropdown state management plugin correctly implements the ProseMirror plugin pattern with proper state initialization and meta-driven state transitions.
115-252: Verify keyboard deletion logic with comprehensive testing.The keyboard plugin implements complex deletion scenarios with multiple cursor positions and node type interactions. Given the intricate edge cases involving zero-width characters, template nodes, and paragraph boundaries, thorough testing is essential to ensure correct behavior.
Consider adding integration tests that cover:
- Deleting templateSelect nodes at various positions (start/middle/end of paragraph)
- Interactions between templateSelect and templateBlock nodes
- Zero-width character handling during deletions
- Cursor positioning after deletions
- Edge cases with empty paragraphs or single-node paragraphs
You can create test scenarios using ProseMirror test-builder utilities to verify the plugin behavior programmatically.
packages/components/src/sender/extensions/suggestion/extension.ts (1)
29-58: LGTM! Proper lifecycle management for Vue reactivity.The extension correctly implements storage for the watch handle and properly cleans it up in the
onDestroyhook. The reactiveitemsref is watched and triggers plugin updates as expected.packages/components/src/sender/extensions/utils/keyboard.ts (1)
1-68: LGTM! Clean and well-documented keyboard utilities.The keyboard utility functions are well-structured with clear documentation and examples. The implementation correctly uses the centralized
KEYBOARD_KEYSmapping to provide cross-platform keyboard event handling.packages/components/src/sender/extensions/template/select/extension.ts (1)
43-59: Good error handling for JSON parsing.The
parseHTMLimplementation correctly uses a try-catch block to handle potential JSON parsing errors and provides a safe fallback to an empty array.packages/components/src/sender/extensions/mention/commands.ts (1)
17-28: Thevalueproperty is intentionally optional inMentionAttrs.The type definition at line 78 in
types.tsshowsvalue?: string, which is documented as "关联值(可选)" (Associated value - optional). The current code correctly allowsvalueto be undefined, which aligns with the type design. No validation is needed.Likely an incorrect or invalid review comment.
packages/components/src/sender/extensions/mention/extension.ts (2)
20-28: LGTM!The node configuration is appropriate for mention nodes: inline, atom, and selectable properties ensure mentions are treated as indivisible units that can be selected but not edited internally.
108-126: LGTM! Proper reactive data handling.The lifecycle hooks correctly handle reactive items with a Vue watcher and properly dispatch transactions to trigger plugin updates. The deep watch and cleanup in
onDestroyprevent memory leaks.packages/components/src/sender/extensions/template/utils.ts (2)
9-24: LGTM!The utility functions provide a clean API for extracting template content. The zero-width character constant is properly documented with Unicode reference.
38-39: Static analysis false positive - regex pattern is safe.The static analysis tool flagged lines 38 and 54 for potential ReDoS vulnerabilities when constructing RegExp from a variable. However, this is a false positive because
ZERO_WIDTH_CHARis a constant single Unicode character ('\u200B'), not user input. The pattern is safe and performs a simple global character replacement.As per static analysis hints, the tool suggested validating regex safety, but in this case the constant nature of the input makes it inherently safe. No remediation needed.
Also applies to: 54-54
packages/components/src/sender/extensions/suggestion/plugin.ts (3)
192-238: LGTM! Well-designed autocomplete UX.The decorator implementation correctly shows autocomplete hints only when the cursor is at the document end (lines 203-206), preventing confusing mid-text insertions. The conditional TAB indicator provides good user guidance.
428-509: LGTM! Proper component lifecycle management.The view implementation correctly handles the creation, updating, and destruction of the Vue-rendered popup. Cleanup handlers are properly invoked to prevent memory leaks.
109-120: The use ofsetContentis intentional and correctly documented.Line 116 using
editor.commands.setContent(item.content)is the intended behavior for this suggestion plugin. The type definition (packages/components/src/sender/extensions/suggestion/types.ts) explicitly documents this with examples showingsetContentas the standard method for filling suggestion content. TheonSelectcallback design allows users to returnfalseto prevent default behavior and customize the fill operation if needed, supporting use cases where content should be modified before insertion (e.g., adding prefixes/suffixes).Likely an incorrect or invalid review comment.
packages/components/src/sender/extensions/template/commands.ts (1)
36-94: LGTM! Robust template data handling.The
setTemplateDatacommand properly handles all template item types with appropriate ID generation and default values. The conditional nested text node creation for template blocks (lines 62-69) correctly handles empty content.packages/components/src/sender/extensions/mention/plugin.ts (3)
72-125: LGTM! Clean state management.The plugin state correctly handles both reactive (Ref) and static item arrays (lines 116-117) and uses the
findTextRangeutility to detect trigger patterns. The state transitions are well-defined.
145-236: LGTM! Well-implemented keyboard handling.The backspace handling (lines 149-176) correctly converts mention nodes back to trigger characters with proper position calculations. The fallback logic for Enter/Tab (lines 219-222) ensures functionality even when the component method is unavailable.
324-353: LGTM! Correct mention insertion logic.The
insertMentionfunction properly creates a mention node with a trailing space and positions the cursor after both (line 346:range.from + 2). This calculation is correct for atom nodes, which have a nodeSize of 1 in ProseMirror.packages/components/src/sender/extensions/template/block/plugins.ts (1)
100-111: LGTM!The
ensureZeroWidthCharsplugin correctly usesappendTransactionto maintain zero-width character consistency, and thepasteHandlerPluginproperly handles both HTML and plain text paste scenarios with multi-line support.Also applies to: 593-653
packages/components/src/sender/extensions/constants.ts (1)
1-98: LGTM!This constants file provides a clean, centralized source of truth for string identifiers across the extension system. The use of
as constassertions ensures type safety, and the comprehensive JSDoc comments clearly document each constant's purpose and usage context.
packages/components/src/sender/extensions/template/block/plugins.ts
Outdated
Show resolved
Hide resolved
packages/components/src/sender/extensions/template/block/plugins.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In @packages/components/src/sender-compat/index.vue:
- Line 23: The computed `defaultValue` currently uses an unsafe type assertion
("const defaultValue = computed(() => props.defaultValue as string") which can
hide non-string values; update the `defaultValue` computed to validate
`props.defaultValue` at runtime (e.g., check typeof props.defaultValue ===
'string') and return the string when valid or a safe fallback (empty string or
props.defaultValue?.toString()) and optionally log or warn when the type is
unexpected; reference the computed `defaultValue` and the prop
`props.defaultValue` when implementing this guard.
- Around line 83-105: The setTemplateData function calls
editor.commands.setTemplateData and editor.commands.focusFirstTemplate without
guarding against runtime exceptions; wrap the calls inside a try-catch around
the block that converts data and invokes editor.commands (in the setTemplateData
function), catch any error thrown, call console.error or the component logger
with a clear message including the caught error, and add a safe fallback (e.g.,
call editor.commands.clearContent() or leave editor unchanged) so an exception
from setTemplateData or focusFirstTemplate does not propagate and break the app.
In @packages/components/src/sender/extensions/template/block/plugins.ts:
- Around line 381-402: The selection-expansion logic around BACKSPACE currently
only inspects $from.nodeBefore/$from.nodeAfter and can miss zero-width chars at
the far end of a non-empty selection; update the block that computes
startPos/endPos (the code using selection, $from, $to, ZERO_WIDTH_CHAR) to also
inspect $to.nodeBefore and $to.nodeAfter and adjust startPos/endPos accordingly
so any zero-width chars adjacent to either boundary are included before calling
state.tr.delete and dispatch.
- Around line 326-378: The else-if branch is unreachable because you test
beforeNode.text === ZERO_WIDTH_CHAR in the outer if and then again in the
else-if; move the index===1 / delete-newline logic into the same beforeNode.text
=== ZERO_WIDTH_CHAR branch (i.e., under the if (beforeNode.text ===
ZERO_WIDTH_CHAR) block) or restructure the outer condition to first check
beforeNode.isText and then switch on index (index > 1 vs index === 1). Update
the block handling ZERO_WIDTH_CHAR (references: beforeNode, ZERO_WIDTH_CHAR,
index, prevPrevNode, $from, selection, dispatch) so the index===1 case executes
when beforeNode.text === ZERO_WIDTH_CHAR instead of being placed in an else that
can never run.
- Around line 551-572: The Delete-key selection handler incorrectly inspects
$from for the end-of-selection zero-width char; update the logic in the DELETE
branch (the isKey(event, 'DELETE') block) to mirror the Backspace fix: derive
and use selection.$to (e.g., $to) to check the end boundary and adjust endPos
when $to.nodeAfter is text and startsWith(ZERO_WIDTH_CHAR), while keeping the
startPos adjustment using $from.nodeBefore; then dispatch the adjusted tr as
before.
- Around line 604-611: The deletion of $from.nodeBefore mutates the document and
invalidates the original $from resolved position, so after performing the first
tr.delete you must map and re-resolve the original position before
checking/removing nodeAfter; do this by mapping the original $from.pos through
tr.mapping (e.g. mappedPos = tr.mapping.map($from.pos)) and then resolving it
against the updated transaction document (e.g. newFrom =
tr.doc.resolve(mappedPos)) and use newFrom.nodeAfter for the second
ZERO_WIDTH_CHAR check and potential tr.delete to avoid referencing stale nodes.
🧹 Nitpick comments (5)
packages/components/src/sender/composables/useEditor.ts (2)
87-90: Consider renaming the callback parameter for clarity.The parameter name
propsshadows the component props in scope, which could cause confusion when reading or maintaining this code.♻️ Rename callback parameter
- onUpdate: (props) => { - const text = props.editor.getText() + onUpdate: ({ editor: editorInstance }) => { + const text = editorInstance.getText() emit('update:modelValue', text) emit('input', text) },
110-120: The manual transaction dispatch is necessary due to a Tiptap Placeholder extension limitation.Tiptap's Placeholder extension does not automatically react to changes in Vue refs, even when passed as a function. Using
() => placeholderRef.valueonly evaluates at initialization; subsequent ref changes don't trigger the placeholder to update. The manual empty transaction dispatch (editor.view.dispatch(tr)) forces Tiptap to re-evaluate its decorations, which is a documented workaround for this limitation.The current implementation is correct. Consider adding an inline comment explaining this Tiptap limitation so future maintainers understand why the dispatch is necessary.
packages/components/src/sender/composables/useSenderCore.ts (1)
218-229: Document or eliminate the overflow check delay.Line 224 uses
setTimeout(..., 0)to defercheckOverflow()after content changes. This appears to wait for DOM updates, but the timing assumption might be fragile.Consider alternatives:
- Use
nextTick()for more explicit Vue lifecycle synchronization- If waiting for editor layout, consider using
editor.value.view.updateState()or checking editor state stability- Add a comment explaining why the delay is needed
♻️ Alternative using nextTick
watch( () => editor.value?.state.doc.content, () => { - setTimeout(() => { + nextTick(() => { checkOverflow() - }, 0) + }) }, { deep: true }, )Note: Import
nextTickfromvueif using this approach.packages/components/src/sender-compat/index.vue (1)
29-61: Consider adding diagnostic logging when extensions aren't available.The code accesses
Sender.SuggestionandSender.Templateas static properties (properly exported viaObject.assignin the Sender module). The type assertions toanyare necessary since these runtime-added properties aren't visible to TypeScript, and the existing guards prevent crashes.However, adding console warnings when expected extensions aren't found would improve observability, consistent with the existing logging pattern in
setTemplateDataat line 93:Suggested enhancement
// 转换 suggestions if (props.suggestions?.length) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const Suggestion = (Sender as any).Suggestion if (Suggestion) { exts.push( Suggestion.configure({ items: props.suggestions, popupWidth: props.suggestionPopupWidth, activeSuggestionKeys: props.activeSuggestionKeys, // eslint-disable-next-line @typescript-eslint/no-explicit-any onSelect: (item: any) => { emit('suggestion-select', item.content) }, }), ) + } else { + console.warn('[sender-compat] Sender.Suggestion extension not found') } } // 添加 Template 扩展(空配置,通过 setTemplateData 方法手动设置) // eslint-disable-next-line @typescript-eslint/no-explicit-any const Template = (Sender as any).Template if (Template) { exts.push(Template.configure({})) + } else { + console.warn('[sender-compat] Sender.Template extension not found') }packages/components/src/sender/extensions/template/block/plugins.ts (1)
171-177: Potential node comparison issue.The comparison
state.doc.lastChild !== $from.node()compares Node object references. In ProseMirror, these may be different instances even when representing the same logical node. Consider comparing positions instead, e.g.,$from.after() < state.doc.content.size.♻️ Suggested fix
- } else if (index === parent.childCount - 1 && state.doc.lastChild !== $from.node()) { + } else if (index === parent.childCount - 1 && $from.after() < state.doc.content.size) {
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
packages/components/src/sender-actions/submit-button/index.vuepackages/components/src/sender-compat/index.vuepackages/components/src/sender/components/layouts/SingleLineLayout.vuepackages/components/src/sender/composables/useEditor.tspackages/components/src/sender/composables/useSenderCore.tspackages/components/src/sender/extensions/template/block/plugins.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/components/src/sender-actions/submit-button/index.vue
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-27T03:35:11.008Z
Learnt from: gene9831
Repo: opentiny/tiny-robot PR: 59
File: packages/components/src/suggestion-popover/index.vue:131-133
Timestamp: 2025-05-27T03:35:11.008Z
Learning: In the SuggestionPopover component (packages/components/src/suggestion-popover/index.vue), the click handler can be bound unconditionally because the `show` computed property has a custom setter that prevents state mutations when `props.trigger === 'manual'`. This design centralizes trigger mode logic in the computed property rather than requiring conditional checks in event handlers.
Applied to files:
packages/components/src/sender-compat/index.vue
🧬 Code graph analysis (2)
packages/components/src/sender/composables/useSenderCore.ts (17)
packages/components/src/sender/index.ts (1)
SenderContext(41-41)packages/components/src/sender/types/context.ts (2)
SenderContext(11-158)SENDER_CONTEXT_KEY(165-165)packages/components/src/sender/composables/useEditor.ts (1)
useEditor(26-131)packages/components/src/sender/composables/index.ts (3)
useEditor(5-5)useModeSwitch(7-7)useAutoSize(6-6)packages/components/src/sender/extensions/index.ts (7)
getTextWithTemplates(23-23)getTemplateStructuredData(23-23)getMentionStructuredData(7-7)getTextWithMentions(7-7)MentionPluginKey(6-6)SuggestionPluginKey(11-11)TemplateSelectDropdownPluginKey(23-23)packages/components/src/sender/extensions/template/index.ts (3)
getTextWithTemplates(16-16)getTemplateStructuredData(16-16)TemplateSelectDropdownPluginKey(17-17)packages/components/src/sender/types/base.ts (1)
StructuredData(199-199)packages/components/src/sender/extensions/constants.ts (1)
EXTENSION_NAMES(7-14)packages/components/src/sender/extensions/mention/utils.ts (2)
getMentionStructuredData(77-111)getTextWithMentions(42-62)packages/components/src/sender/composables/useModeSwitch.ts (1)
useModeSwitch(11-106)packages/components/src/sender/composables/useKeyboardShortcuts.ts (1)
useKeyboardShortcuts(17-78)packages/components/src/sender/extensions/mention/plugin.ts (2)
view(239-317)MentionPluginKey(25-25)packages/components/src/sender/extensions/suggestion/plugin.ts (2)
view(428-508)SuggestionPluginKey(23-23)packages/components/src/sender/extensions/mention/index.ts (1)
MentionPluginKey(13-13)packages/components/src/sender/extensions/suggestion/index.ts (1)
SuggestionPluginKey(13-13)packages/components/src/sender/extensions/template/select/plugins.ts (1)
TemplateSelectDropdownPluginKey(30-32)packages/components/src/sender/composables/useAutoSize.ts (1)
useAutoSize(16-104)
packages/components/src/sender/composables/useEditor.ts (2)
packages/components/src/sender/index.type.ts (2)
SenderProps(32-227)SenderEmits(236-311)packages/components/src/sender/types/composables.ts (1)
UseEditorReturn(40-51)
🪛 Biome (2.1.2)
packages/components/src/sender/extensions/template/block/plugins.ts
[error] 416-418: This branch can never execute. Its condition is a duplicate or covered by previous conditions in the if-else-if chain.
(lint/suspicious/noDuplicateElseIf)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build / build
🔇 Additional comments (23)
packages/components/src/sender/components/layouts/SingleLineLayout.vue (4)
1-9: LGTM! Clean script setup.The imports and context usage are well-structured. All destructured properties are utilized in the template.
28-32: LGTM! Good slot design.Providing the
:editorprop to the content slot allows consumers to access the editor instance for custom rendering while maintaining a sensible default with EditorContent.
86-91: LGTM! Appropriate single-line styling.The ProseMirror styling correctly enforces single-line behavior with
nowrap,overflow: hidden, andtext-overflow: ellipsis. This ensures content stays on one line with truncation when it exceeds the available width.
36-40: DefaultActionButtons is conditionally rendered and the current design pattern is intentional.The
DefaultActionButtonscomponent has internal conditional logic (v-if="hasContent || loading") in its template, so buttons only appear when there is content or a loading state. WhileDefaultActionButtonsis placed unconditionally in the layout template, it does not render unconditionally. The pattern of always positioning defaults after the customactions-inlineslot is consistent across layouts (SingleLineLayout and Footer) and appears to be intentional—allowing custom actions to coexist with defaults rather than replace them. No action needed unless the design intent changes to require full replacement capability.packages/components/src/sender/composables/useEditor.ts (1)
1-98: Well-structured editor initialization.The editor setup is solid:
- Base extensions (Document, Paragraph, Text, History, Placeholder, CharacterCount) provide good foundation
- User extensions are properly merged
- Paste handler correctly converts to plain text and respects single/multiple mode
- Event wiring (onUpdate, onFocus, onBlur) is appropriate
- Content initialization with fallback chain is correct
packages/components/src/sender/composables/useSenderCore.ts (4)
1-95: Excellent initialization order and state management.The core composable is well-structured:
- Clear initialization order with editor first (line 64)
- Derived state (hasContent, characterCount, isOverLimit, canSubmit) computed correctly
- Good use of
getTextWithTemplatesfor consistent text extraction- Comments clearly document the initialization phases
101-133: Submit logic correctly handles structured data.The submit method properly:
- Checks canSubmit conditions before proceeding
- Handles mutually exclusive Template and Mention extensions
- Extracts appropriate structured data for each extension type
- Falls back to plain text when no extensions are present
- Emits both textContent and optional structuredData
157-211: Verify timing reliability of mode-switch-then-insert pattern.Lines 194-197 use
setTimeout(..., 0)to defersplitBlockafter callingsetMode('multiple'). This pattern works but relies on timing assumptions about when the mode switch completes.Consider verifying whether:
- The mode switch is truly asynchronous and requires this delay
- A more explicit synchronization (e.g., watching
currentModeor using a callback) would be more reliable- This timing works consistently across different browsers and Vue versions
If the delay is necessary due to DOM updates or internal state transitions, please add a comment explaining why.
Additionally, you might want to verify this pattern with a test case that rapidly triggers mode switches to ensure it doesn't race.
235-311: Context assembly and public API are well-designed.The context object and exposed methods provide a clean, comprehensive API:
- All computed state properly wrapped
- Methods correctly delegate to editor commands
- Context provision via
SENDER_CONTEXT_KEYenables clean dependency injection- Expose object surfaces the right methods for parent components
packages/components/src/sender-compat/index.vue (8)
1-11: LGTM! Props and imports are well-structured.The component imports are appropriate, and the prop defaults (mode, placeholder, submitType) provide sensible fallbacks for common use cases.
13-21: LGTM! Two-way binding correctly implemented.The computed property correctly implements Vue 3's v-model pattern with proper nullish coalescing to ensure a non-null value.
63-73: LGTM! Action mapping is clean.The defaultActions computed properly maps the submit button configuration and returns undefined when empty, which is a good pattern for optional props.
75-81: LGTM! Voice config handling is appropriate.The type discrimination between boolean and object
speechprops provides a clean API that supports both simple enable/disable and advanced configuration.
107-160: LGTM! Event handlers properly forward events.All event handlers correctly forward their respective events to parent components, and the optional chaining on voiceRef methods provides safe access.
162-186: LGTM! Exposed API is well-designed.The exposed methods use optional chaining for safe access to child component methods, providing a robust public API surface.
213-286: LGTM! Slot architecture is comprehensive.The slot passthrough and conditional button injection logic correctly handles both single and multiple modes. The mapping of actions to
actions-inlinefor single mode andfooter-rightfor multiple mode maintains separation of concerns.
229-248: Add safeguards for voiceRef stability when mode changes dynamically.Both VoiceButton instances (lines 241 and 277) share
ref="voiceRef". While only one renders based onmode, the mode prop can change dynamically at runtime (auto-switching in the parent Sender component based on content overflow, or external parent updates). IfstartSpeech()orstopSpeech()are called while mode is transitioning, the ref may point to an unmounting instance or become null, causing the operation to fail silently.Recommended fixes:
- Prevent mode changes while speech is active, or
- Store speech state and re-apply it to the newly mounted VoiceButton instance after mode transition, or
- Document that mode must not change during active speech sessions.
Also applies to: 265-284
packages/components/src/sender/extensions/template/block/plugins.ts (6)
1-13: LGTM!Imports are well-organized with proper type-only imports separated using the
typekeyword.
18-43: LGTM!The logic correctly handles zero-width character management around template blocks. The descending sort on line 78-83 ensures positions remain valid during batch modifications.
100-111: LGTM!The plugin correctly uses
appendTransactionto ensure zero-width characters are maintained after document changes, with an appropriate early exit when no changes occurred.
182-216: LGTM!The template block internal navigation correctly handles both empty blocks and content-filled blocks with proper boundary detection.
218-265: LGTM!The Backspace handling within template blocks covers all edge cases: preserving blocks when deleting the last character, handling zero-width content, empty content, and cursor positioning at boundaries.
614-630: Consider edge case for multi-line paste location.The multi-line paste creates new paragraph nodes and inserts them at the current position. If the cursor is inside an existing paragraph, this inserts paragraphs inline rather than after the current paragraph, which may produce unexpected nesting or structure. Verify this behavior matches the intended UX.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In @packages/components/src/sender/extensions/suggestion/plugin.ts:
- Around line 447-456: The selection logic in onSelect uses
state.filteredSuggestions.find(item => item.content === content) which can pick
the wrong item when multiple suggestions share the same content; change the
selection to use a unique identifier or index instead — update the
SuggestionList to emit an index or an id and then in the onSelect handler lookup
selectedItem via state.filteredSuggestions[index] or
state.filteredSuggestions.find(item => item.id === emittedId), then call
insertSuggestion(view, state.range, selectedItem) and dispatch the
SuggestionPluginKey close meta as before (ensure symbols: onSelect,
state.filteredSuggestions, insertSuggestion, SuggestionPluginKey, view,
state.range are used to locate and fix the code).
In @packages/components/src/sender/extensions/suggestion/suggestion-list.vue:
- Around line 85-87: handleSelect currently calls props.onSelect with
item.content which can ambiguously match duplicate suggestions; change
handleSelect to pass the suggestion index (e.g., idx) instead of content and
update the props.onSelect invocation/typing to accept an index (number) so
selection is unambiguous; also adjust the consumer logic (the plugin's
find-by-content path) to use the received index to directly select the correct
suggestion item rather than searching by content.
In @packages/components/src/sender/extensions/suggestion/types.ts:
- Around line 135-148: The JSDoc says items is required but the type declares it
optional; make the type match the documentation by removing the optional marker
so SuggestionOptions.items is declared as items: SenderSuggestionItem[] |
Ref<SenderSuggestionItem[]>; update any callers if necessary to always provide
items and keep the existing JSDoc comment unchanged.
In @packages/components/src/sender/index.type.ts:
- Around line 25-30: The exported interface DefaultConfig is currently unused
but part of the public API; either document it for consumers or remove it if
unintended. If keeping it, add a JSDoc block above the DefaultConfig declaration
describing its purpose, each property (placeholder, mode, submitType, autoSize
with minRows/maxRows), expected types/values and example usage; if it's not
meant to be public, delete the exported DefaultConfig or change it to a
non-exported/internal type and update any related exports to avoid exposing it.
🧹 Nitpick comments (5)
packages/components/src/sender/index.ts (1)
14-19: Component name may cause inconsistency with main install function.Based on the relevant code snippet from
packages/components/src/index.ts, the main install function stripsTinyorTrprefixes before re-registering components. Settingname = 'TrSender'here means:
- This local install registers as
TrSender- The main install would register it as
TrSender(after strippingTr→Sender, then addingTrback)This works correctly, but consider whether
Sender(without prefix) would be more consistent with other components in the library, letting the main install handle theTrprefix uniformly.packages/components/src/sender/index.type.ts (2)
167-168: Consider improving type safety for extensions array.The
any[]fallback undermines type safety. If the goal is to support custom extensions that may not strictly matchExtension, consider using a more specific union type or documenting whyany[]is necessary.💡 Potential improvement
- // eslint-disable-next-line @typescript-eslint/no-explicit-any - extensions?: Extension[] | any[] + /** + * Using Extension<any, any> to support extensions with various configurations + * while maintaining some type safety + */ + extensions?: Extension<any, any>[]Alternatively, if third-party extensions truly require
any, keep the current approach but ensure the eslint-disable is documented with a reason.
327-370: LGTM! Comprehensive slot definitions with proper typing.The slot interfaces are well-documented with examples. The use of
SenderSlotScopefor action slots provides consistent access toinsert,focus, anddisabledproperties.One minor note: consider whether
contentslot'seditor: unknowncould use a more specific type from Tiptap (e.g.,Editorfrom@tiptap/core) to improve developer experience.💡 Optional: Use specific Editor type
+import type { Editor } from '@tiptap/core' + export interface SenderSlots { // ... - content?: (props: { editor: unknown }) => unknown + content?: (props: { editor: Editor | null }) => unknown // ... }packages/components/src/sender/extensions/suggestion/types.ts (1)
130-130: Move import to the top of the file.The
import type { Ref } from 'vue'statement is placed mid-file. Convention is to group imports at the top for better readability.♻️ Suggested fix
+import type { Ref } from 'vue' + /** * Suggestion 插件类型定义 *And remove line 130.
packages/components/src/sender/extensions/suggestion/plugin.ts (1)
86-88: Consider removing this pass-through function or adding a TODO.
getCurrentQuerysimply returns its input. If it's a placeholder for future character-trigger mode, a brief comment would clarify intent. Otherwise, it could be inlined.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
packages/components/src/sender/extensions/index.tspackages/components/src/sender/extensions/suggestion/index.tspackages/components/src/sender/extensions/suggestion/plugin.tspackages/components/src/sender/extensions/suggestion/suggestion-list.vuepackages/components/src/sender/extensions/suggestion/types.tspackages/components/src/sender/extensions/suggestion/utils/highlight.tspackages/components/src/sender/index.tspackages/components/src/sender/index.type.tspackages/components/src/sender/types/components.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/components/src/sender/extensions/suggestion/index.ts
- packages/components/src/sender/types/components.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-27T03:35:11.008Z
Learnt from: gene9831
Repo: opentiny/tiny-robot PR: 59
File: packages/components/src/suggestion-popover/index.vue:131-133
Timestamp: 2025-05-27T03:35:11.008Z
Learning: In the SuggestionPopover component (packages/components/src/suggestion-popover/index.vue), the click handler can be bound unconditionally because the `show` computed property has a custom setter that prevents state mutations when `props.trigger === 'manual'`. This design centralizes trigger mode logic in the computed property rather than requiring conditional checks in event handlers.
Applied to files:
packages/components/src/sender/extensions/suggestion/suggestion-list.vue
🧬 Code graph analysis (3)
packages/components/src/sender/extensions/suggestion/plugin.ts (8)
packages/components/src/sender/extensions/index.ts (5)
SuggestionPluginKey(11-11)SuggestionState(16-16)SuggestionOptions(15-15)SenderSuggestionItem(14-14)syncAutoComplete(12-12)packages/components/src/sender/extensions/suggestion/index.ts (2)
SuggestionPluginKey(13-13)syncAutoComplete(15-15)packages/components/src/sender/extensions/suggestion/types.ts (3)
SuggestionState(83-128)SuggestionOptions(135-249)SenderSuggestionItem(48-76)packages/components/src/sender/index.ts (3)
SuggestionState(59-59)SuggestionOptions(58-58)SenderSuggestionItem(57-57)packages/components/src/sender/index.type.ts (3)
SuggestionState(19-19)SuggestionOptions(18-18)SenderSuggestionItem(17-17)packages/components/src/sender/extensions/constants.ts (1)
PLUGIN_KEY_NAMES(38-55)packages/components/src/sender/extensions/suggestion/utils/filter.ts (1)
syncAutoComplete(25-57)packages/components/src/sender/extensions/utils/keyboard.ts (2)
isKey(25-27)isAnyKey(40-42)
packages/components/src/sender/index.ts (1)
packages/components/src/index.ts (3)
install(101-106)Sender(136-136)Sender(137-137)
packages/components/src/sender/extensions/suggestion/utils/highlight.ts (4)
packages/components/src/sender/extensions/suggestion/types.ts (2)
SenderSuggestionItem(48-76)SuggestionTextPart(10-13)packages/components/src/sender/index.ts (2)
SenderSuggestionItem(57-57)SuggestionTextPart(60-60)packages/components/src/sender/index.type.ts (2)
SenderSuggestionItem(17-17)SuggestionTextPart(20-20)packages/components/src/sender-compat/index.type.ts (1)
SuggestionTextPart(55-58)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build / build
🔇 Additional comments (27)
packages/components/src/sender/extensions/index.ts (1)
1-24: Well-organized barrel file with clear extension groupings.The export structure is clean and consistent across all three extension modules. Each section appropriately separates the main extension, plugin key, utility functions, and type exports. All exported symbols are correctly defined in their respective source modules.
packages/components/src/sender/index.ts (4)
22-33: LGTM! Clean extension pattern.The
Object.assignapproach to attach static properties (extension classes and convenience functions) is a well-established pattern for Vue components. This provides a clean API:Sender.Mention.configure()for inheritance andSender.mention()for simple scenarios.
53-62: LGTM! Extension type exports are well-organized.The extension types are properly grouped and exported, providing consumers with access to
TemplateAttrs,TemplateOptions,MentionAttrs,MentionOptions, and Suggestion-related types for advanced typing needs.
51-51:useSenderContextis properly exported from./context.The export is confirmed at
packages/components/src/sender/context/index.ts:12. The re-export in the index file is valid.
37-49: All exported types are properly defined and exported from./index.type.Verification confirms that all 11 types (
SenderProps,SenderEmits,SenderSlots,SenderContext,UseEditorReturn,UseModeSwitchReturn,UseSuggestionReturn,UseKeyboardShortcutsReturn,TemplateItem,MentionItem,DefaultActions) are correctly exported. The file uses a combination of wildcard re-exports (export * from './types/base',export * from './types/composables',export * from './types/context',export * from './types/slots') and explicit exports (export type { MentionItem } from './extensions/mention').packages/components/src/sender/index.type.ts (3)
1-9: LGTM! Clean module organization.The barrel re-exports from sub-modules (
types/base,types/composables, etc.) provide a clean public API surface while keeping implementation details modular.
243-276: Well-designed submit event signature with structured data.The enhanced
submitevent that provides both plain text and optionalStructuredDatais a clean API design. It allows simple use cases to just use the text while advanced scenarios can access template/mention metadata.
39-234: Comprehensive and well-documented props interface.The
SenderPropsinterface is thorough with:
- Clear grouping of props by purpose (core data, config, mode, content, extensions, styling, actions, submit)
- Detailed JSDoc with defaults and usage examples
- Good use of imported base types (
InputMode,SubmitTrigger,AutoSize, etc.)The documentation quality is excellent and will help consumers understand the API.
packages/components/src/sender/extensions/suggestion/utils/highlight.ts (3)
29-111: LGTM!The interval merging algorithm is well-implemented, correctly handling overlapping matches and edge cases (empty highlights, no matches). The case-insensitive matching while preserving original text length is a nice touch.
129-135: LGTM!Clean delegation to the core function with proper empty input handling.
173-188: LGTM!The three-mode dispatch logic is clean and correctly matches the
SenderSuggestionItem.highlightstype definition.packages/components/src/sender/extensions/suggestion/types.ts (4)
10-22: LGTM!Type definitions are clean and consistent with the compat layer's
SuggestionTextPartinterface.
48-76: LGTM!The
SenderSuggestionIteminterface is well-designed with clear documentation. The flexiblehighlightsunion type elegantly supports all three highlighting modes.
83-128: LGTM!Comprehensive state interface with clear documentation for both global and character trigger modes.
260-269:or afterpackages/components/src/sender/extensions/suggestion/plugin.ts (8)
20-30: LGTM!Plugin key and options interface are correctly defined, extending the public
SuggestionOptionswith the internaleditordependency.
56-70: LGTM!The Ref detection via duck-typing (
'value' in items) avoids importing Vue'sisRefwhile still handling reactive refs. The fallback tosuggestionsprovides backwards compatibility.
109-120: LGTM!The
onSelectcallback pattern with!== falsecheck allows consumers to prevent default insertion while still receiving the selection event.
140-187: LGTM!Good use of
@floating-ui/domwith proper cleanup ofautoUpdatesubscription. The percentage-based width calculation is a nice feature for responsive layouts.
192-238: LGTM!Smart UX decision to only show autocomplete when cursor is at document end, preventing confusing mid-text insertions. The decoration widget is properly marked as non-editable.
262-276: LGTM!The
justClosedflag withsetTimeout(0)is a clever pattern to prevent the suggestion list from immediately reopening after being closed, handling the race condition between transaction dispatch and state updates.
366-425: LGTM!Comprehensive keyboard handling with circular navigation, configurable selection keys, and proper event prevention. The separation between Tab (autocomplete) and Enter (selection via
activeSuggestionKeys) provides good flexibility.
502-507: LGTM!Thorough cleanup in
destroy()properly releases all resources: floating-ui subscription, Vue component, and DOM element.packages/components/src/sender/extensions/suggestion/suggestion-list.vue (4)
10-55: LGTM!Using callback props instead of emits is the right choice here since the component is instantiated via
VueRendererrather than in a template. The optional callbacks with?.()invocation are safe.
92-102: LGTM!The
scrollIntoView({ block: 'nearest' })is the correct choice for keyboard navigation in lists, ensuring visibility without jarring scroll jumps.
113-134: LGTM!Template logic is clean. Using
@clickfor selection and@mouseenter/@mouseleavefor hover state is appropriate. The inlineprocessHighlightscall in v-for is acceptable given typical suggestion list sizes.
139-216: LGTM!Well-structured styles using CSS custom properties for theming, BEM-like naming, and proper scrollbar customization for both WebKit and Firefox (
scrollbar-color). The slide-up transition provides smooth UX.
packages/components/src/sender/extensions/suggestion/suggestion-list.vue
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In @packages/components/src/sender/extensions/suggestion/plugin.ts:
- Around line 48-51: The module-scoped variables component, popup, cleanup, and
justClosed must be moved into the plugin instance to avoid shared mutable state:
relocate their declarations inside createSuggestionPlugin (or inside the object
returned by its view() method) so each invocation gets its own
component/popup/cleanup/justClosed, update all references in
createSuggestionPlugin and the view() implementation to use the instance-scoped
variables, and ensure the setTimeout callback is tracked via the instance
cleanup and cleared in the view()'s destroy() to avoid memory leaks and
cross-instance interference.
- Around line 109-120: The insertSuggestion function accepts a redundant range
parameter that is only null-checked and never used; remove the range parameter
from insertSuggestion and from all internal calls, update any callers that pass
a range to stop providing it (or adapt them to the new signature), and keep the
existing onSelect callback and editor.commands.setContent flow unchanged so
default whole-document replacement still occurs; if partial insertion is needed
later, handle it via onSelect instead of adding range back.
- Around line 486-503: The update(view: EditorView) method in the suggestion
plugin should be wrapped in a try-finally so the cleanup block that destroys
component, nulls cleanup, and removes popup always runs on errors; modify the
update function (the one using SuggestionPluginKey.getState and
creating/positioning component/popup) to enclose its current body in try { ... }
finally { /* run the existing else cleanup: cleanup?.(); cleanup = null;
component?.destroy(); component = null; popup?.remove(); popup = null; */ } and
keep destroy() as-is for fallback.
🧹 Nitpick comments (4)
packages/components/src/sender/extensions/suggestion/plugin.ts (3)
256-276: Refactor fragile timing hack with setTimeout.Lines 263-266 use
justClosedwithsetTimeout(..., 0)to prevent the suggestion list from immediately reopening after closing. This timing-based approach is fragile and could fail under different execution conditions.♻️ Consider using transaction metadata instead
A more robust approach is to check the transaction that triggered the state change:
- if (meta.type === 'close') { - justClosed = true - setTimeout(() => { - justClosed = false - }, 0) return { active: false, range: null, query: '', filteredSuggestions: [], selectedIndex: -1, autoCompleteText: '', showTabIndicator: false, } } } - // 保持关闭状态,防止立即重新打开 - if (justClosed) { - return state - } - // 如果文档没有变化,保持状态 - if (!tr.docChanged && !tr.selectionSet) { + // Skip reopening if this is the same transaction that closed it or if nothing changed + if ((!tr.docChanged && !tr.selectionSet) || (meta && meta.type === 'close')) { return state }This eliminates the timing dependency and the need for setTimeout.
145-146: Suggest using a configurable selector or data attribute.Line 145 hardcodes the CSS class
.tr-senderfor finding the editor wrapper. This creates a tight coupling between the plugin and the component structure.♻️ Consider making the reference element configurable
Pass the reference element through options or use a data attribute:
interface PluginOptions extends SuggestionOptions { editor: Editor + referenceElement?: HTMLElement }function positionPopup(view: EditorView, popup: HTMLElement) { cleanup?.() - const editorWrapper = view.dom.closest('.tr-sender') - const referenceElement = (editorWrapper as HTMLElement) || view.dom + const referenceElement = options.referenceElement || + (view.dom.closest('[data-sender-wrapper]') as HTMLElement) || + view.domThis makes the plugin more reusable and testable.
63-67: Document the Ref handling workaround.Lines 64-67 handle Vue's
Reftype with a type cast and eslint-disable comment. While this works, the comment "避免引入 vue 依赖导致类型问题" (avoid introducing vue dependency causing type issues) could be more specific.Consider adding a more detailed comment explaining why this is necessary and what the type expectations are:
- // 处理 Ref (简单的 value 检查,避免引入 vue 依赖导致类型问题) + // Handle Vue Ref without importing 'vue' dependency to avoid type conflicts + // The extension options may be wrapped in a Ref for reactivity + // Check for .value property to unwrap the Ref if (items && typeof items === 'object' && 'value' in items) {packages/components/src/sender/index.type.ts (1)
132-160: Consider more specific typing for extensions property.Lines 159-160 use
Extension[] | any[]with an eslint-disable comment. While this provides flexibility for users who may not have@tiptap/coretypes available, it bypasses type checking.Consider using a generic constraint or conditional type to preserve some type safety:
+/** + * Tiptap extension or extension-like object + */ +export type TiptapExtensionLike = Extension | { name: string; configure?: (options: any) => any } + export interface SenderProps { // ... - // eslint-disable-next-line @typescript-eslint/no-explicit-any - extensions?: Extension[] | any[] + extensions?: TiptapExtensionLike[]This provides better IntelliSense while still allowing non-Extension objects that follow the same pattern.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
packages/components/src/sender/extensions/suggestion/plugin.tspackages/components/src/sender/extensions/suggestion/suggestion-list.vuepackages/components/src/sender/extensions/suggestion/types.tspackages/components/src/sender/index.type.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/components/src/sender/extensions/suggestion/types.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-27T03:35:11.008Z
Learnt from: gene9831
Repo: opentiny/tiny-robot PR: 59
File: packages/components/src/suggestion-popover/index.vue:131-133
Timestamp: 2025-05-27T03:35:11.008Z
Learning: In the SuggestionPopover component (packages/components/src/suggestion-popover/index.vue), the click handler can be bound unconditionally because the `show` computed property has a custom setter that prevents state mutations when `props.trigger === 'manual'`. This design centralizes trigger mode logic in the computed property rather than requiring conditional checks in event handlers.
Applied to files:
packages/components/src/sender/extensions/suggestion/suggestion-list.vue
🧬 Code graph analysis (1)
packages/components/src/sender/extensions/suggestion/plugin.ts (7)
packages/components/src/sender/extensions/suggestion/index.ts (2)
SuggestionPluginKey(13-13)syncAutoComplete(15-15)packages/components/src/sender/extensions/suggestion/types.ts (3)
SuggestionState(83-128)SuggestionOptions(135-253)SenderSuggestionItem(48-76)packages/components/src/sender/index.type.ts (3)
SuggestionState(19-19)SuggestionOptions(18-18)SenderSuggestionItem(17-17)packages/components/src/sender/index.ts (3)
SuggestionState(59-59)SuggestionOptions(58-58)SenderSuggestionItem(57-57)packages/components/src/sender/extensions/constants.ts (2)
PLUGIN_KEY_NAMES(38-55)EXTENSION_NAMES(7-14)packages/components/src/sender/extensions/suggestion/utils/filter.ts (1)
syncAutoComplete(25-57)packages/components/src/sender/extensions/utils/keyboard.ts (2)
isKey(25-27)isAnyKey(40-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build / build
🔇 Additional comments (8)
packages/components/src/sender/extensions/suggestion/plugin.ts (1)
199-206: Good defensive coding for autocomplete placement.The guard at lines 202-205 ensures autocomplete text only appears when the cursor is at the document end. This prevents the confusing UX of autocomplete text appearing in the middle of existing content when the user moves the cursor.
packages/components/src/sender/extensions/suggestion/suggestion-list.vue (4)
41-55: Good API design: Using callback props for better composition.The component uses optional callback props (
onSelect,onMouseEnter,onMouseLeave) instead of Vue emits. This is a valid pattern that provides better TypeScript inference and allows the parent (plugin.ts) to handle events without Vue's event system.Based on learnings, this pattern is intentional and appropriate for this use case.
85-90: Correct usage of toRaw for external callbacks.Line 88 uses
toRaw(item)before passing the item to the callback. This is correct because the item comes from reactive props, and external consumers (the plugin) shouldn't receive Vue reactive proxies.
95-105: Auto-scroll implementation ensures selected item visibility.The watch on
activeKeyboardIndexautomatically scrolls the selected item into view usingscrollIntoView({ block: 'nearest' }). This provides good UX for keyboard navigation.
142-208: No action needed. All CSS custom properties referenced in the styles (--tr-suggestion-bg-color,--tr-suggestion-box-shadow-color,--tr-suggestion-text-color, etc.) are properly defined inpackages/components/src/styles/variables.cssfor both light and dark themes.packages/components/src/sender/index.type.ts (3)
1-23: Well-organized type exports with clear structure.The file properly separates concerns by:
- Importing core types from
@tiptap/core- Re-exporting internal type modules
- Exporting extension-specific types
This makes the public API clear and maintainable.
138-157: Clear and helpful JSDoc examples.The JSDoc comments for the
extensionsproperty include practical examples showing both basic usage and reactive configuration. This significantly improves developer experience.
244-268: Well-documented submit event with structured data.The submit event documentation (lines 244-268) clearly explains the dual parameters (
textContentandstructuredData) with a helpful example showing how to handle both template and mention scenarios. This is excellent documentation for a complex API.

Sender 组件重构
📋 概述
本次 PR 对
packages/components/src/sender目录进行了大规模重构,从基于自定义编辑器的组件架构重构为基于 Tiptap 编辑器的现代化架构。重构涉及核心组件重写、扩展系统构建、类型系统优化、操作按钮模块化以及新增多项功能特性。🎯 主要变更
1. 核心组件重构
1.1 Sender.vue - 架构重构
变更前:
contenteditable实现的编辑器变更后:
useSenderCore统一管理所有逻辑关键代码变更:
1.2 新增布局组件
SingleLineLayout.vue
MultiLineLayout.vue
EditorContent/index.vue
Footer/index.vue
2. Composables 系统重构
2.1 useSenderCore.ts - 核心逻辑聚合器
职责:
提供的能力:
2.2 新增 Composables
useEditor.ts
useKeyboardShortcuts.ts
useModeSwitch.ts
useAutoSize.ts
useSlotScope.ts
2.3 删除的 Composables
useInputHandler.ts- 输入处理逻辑(由 Tiptap 接管)useKeyboardHandler.ts- 键盘处理逻辑(重构为 useKeyboardShortcuts)useSuggestionHandler.ts- 建议处理逻辑(重构为 Suggestion 扩展)useUndoRedo.ts- 撤销重做逻辑(由 Tiptap History 扩展接管)useSpeechHandler.ts- 语音处理逻辑(移至 sender-actions)webSpeechHandler.ts- Web Speech API 处理(移至 sender-actions)3. Tiptap 扩展系统
新增三大扩展模块,基于 Tiptap 的 ProseMirror 插件系统实现:
主要特性:
4. 操作按钮模块化 - sender-actions
4.1 结构说明
设计理念:
目录结构:
4.2 组件说明
5. 兼容层 - sender-compat
5.1 新增 sender-compat 组件
目的:
文件结构:
功能:
6. Context 系统
6.1 新增 Context 管理
通过
provide/inject机制统一管理组件状态和方法,避免 props drilling。核心能力:
优势:
7. 类型系统重构
7.1 新增类型文件
将类型定义集中管理,提供完善的 TypeScript 支持(750+ 行类型代码):
base.ts(264 行) - 基础类型定义components.ts(84 行) - 组件 Props/Emits 类型composables.ts(146 行) - Composables 返回类型context.ts(170 行) - Context 接口定义slots.ts(86 行) - 插槽类型定义7.2 主要变更
新增 Props:
extensions- Tiptap 扩展配置mode/autoSwitch- 模式配置autoSize- 自动高度配置submitType- 提交方式配置defaultActions- 操作按钮配置删除:
editor.type.ts- 旧的编辑器类型(由 Tiptap 类型替代)8. 删除的组件文件
8.1 旧版组件清理
ActionButtons.vue (419 行) - 已删除
sender-actions/目录下的独立组件Block.vue (58 行) - 已删除
TemplateEditor.vue (1129 行) - 已删除
SuggestionList.vue - 已移动
sender/components/移动到sender/extensions/suggestion/suggestion-list.vue(R059 - 59% 相似度)global.d.ts (11 行) - 已删除
types/目录9. 样式文件变更
9.1 Sender 样式重构
sender/index.less - 大幅精简
新增:styles/components/sender.less (69 行)
9.2 CSS 变量系统
styles/variables.css - 新增 120 行 CSS 变量
示例变量:
9.3 样式导入
styles/components/index.css - 更新
@import './sender.less';10. 依赖变更
10.1 新增 Tiptap 依赖(peerDependencies)
新增 9 个 Tiptap 相关依赖(版本 ^3.11.0):
@tiptap/core- 核心库@tiptap/vue-3- Vue 3 集成@tiptap/pm- ProseMirror 核心@tiptap/extension-*- 6 个基础扩展(document、paragraph、text、history、placeholder、character-count)10.2 新增其他依赖
@floating-ui/dom(^1.6.0) - 用于浮动定位(下拉菜单、提示等)为什么使用 peerDependencies?
避免重复安装、让使用者控制版本、减小包体积、避免版本冲突
11. 导出 API 变更
11.1 新增导出
TrSender核心组件及便捷方法SubmitButton、ClearButton、VoiceButton、UploadButton、WordCounter等)TrSenderCompat兼容层组件TrSender.Mention、TrSender.Suggestion、TrSender.Template访问使用示例:
12. 构建配置变更
vite.config.ts - 更新外部依赖配置
🔄 破坏性变更
本次重构通过 sender-compat 兼容层确保了向后兼容,
现有用户修改少量代码即可升级至新架构。
可以参考文档了解具体变化👉 sender_v0.4.0
📖 迁移指南
sender-compat 是为 v0.3.0 用户提供的过渡期兼容组件。
它保留了 v0.3.0 的大部分 API,让你:
✅ 快速升级到 v0.4.0 的底层实现
✅ 保持现有代码最小改动
✅ 为将来完全升级到 v0.4.0 Sender 做准备
推荐迁移路径: v0.3.0 Sender → SenderCompat (快速) → v0.4.0 Sender (最终)
3. API 对比表
Props 对比
v-modelv-modelplaceholderplaceholderdisabled/loadingdisabled/loadingmaxLength/showWordLimitmaxLength/showWordLimitmodemodesubmitTypesubmitTypesizeautoSizedefaultActionsextensionsallowSpeech/speechallowFilesbuttonGroupthemesuggestions/suggestionPopupWidthtemplateDataSlots 对比
#header#header#prefix#prefix#footer-left#footer#footer-right#footer-right#actions#actions-inline#decorativeContent#content(需配合 disabled)Events 对比
@update:modelValue@update:modelValue@submit@submit@clear@clear@focus/@blur@focus/@blur@cancel@change@files-selected@speech-*@suggestion-selectMethods 对比
focus()/blur()focus()/blur()clear()clear()submit()submit()setContent()/getContent()cancel()startSpeech()/stopSpeech()activateTemplateFirstField()📁 完整目录结构
重构后的目录结构
文件变更统计
新增文件(60+)
删除文件(10+)
修改文件(7个)
移动/重命名文件(4个)
代码行数统计
主要新增代码分布:
主要删除代码:
构建体积对比
重构前

重构后

✅ 总结
本次重构是 Sender 组件的一次全面升级,主要成果包括:
重构规模:
感谢 Reviewers 的审查! 🙏
Summary by CodeRabbit
New Features
Refactor
Style
✏️ Tip: You can customize this high-level summary in your review settings.