diff --git a/src/main/presenter/agentRuntimePresenter/contextBuilder.ts b/src/main/presenter/agentRuntimePresenter/contextBuilder.ts index 579cd08f5..6e48f7d27 100644 --- a/src/main/presenter/agentRuntimePresenter/contextBuilder.ts +++ b/src/main/presenter/agentRuntimePresenter/contextBuilder.ts @@ -177,12 +177,14 @@ export function normalizeUserInput(input: string | SendMessageInput): SendMessag ) ) : [] + const inlineItems = Array.isArray(input.inlineItems) ? input.inlineItems : [] return { text: typeof input.text === 'string' ? input.text : '', files: Array.isArray(input.files) ? (input.files.filter((file): file is MessageFile => Boolean(file)) as MessageFile[]) : [], - ...(activeSkills.length > 0 ? { activeSkills } : {}) + ...(activeSkills.length > 0 ? { activeSkills } : {}), + ...(inlineItems.length > 0 ? { inlineItems } : {}) } } @@ -402,13 +404,45 @@ function buildImageMetadataContext(files: MessageFile[]): string { .join('\n\n') } +function buildInlineDisplayText(input: SendMessageInput): string { + const text = input.text ?? '' + const inlineItems = Array.isArray(input.inlineItems) ? input.inlineItems : [] + if (inlineItems.length === 0) { + return text + } + + const validItems = inlineItems + .map((item, index) => ({ item, index })) + .filter( + ({ item }) => Number.isInteger(item.offset) && item.offset >= 0 && item.offset <= text.length + ) + .sort((left, right) => left.item.offset - right.item.offset || left.index - right.index) + if (validItems.length === 0) { + return text + } + + const parts: string[] = [] + let cursor = 0 + for (const { item } of validItems) { + if (item.offset > cursor) { + parts.push(text.slice(cursor, item.offset)) + } + parts.push(item.type === 'skill' ? `[Skill: ${item.skillName}]` : `[File: ${item.fileName}]`) + cursor = item.offset + } + if (cursor < text.length) { + parts.push(text.slice(cursor)) + } + return parts.join('') +} + export function buildUserMessageContent( input: SendMessageInput, supportsVision: boolean, supportsAudioInput: boolean = false, options: UserMessageContentBuildOptions = {} ): ChatMessage['content'] { - const text = input.text ?? '' + const text = buildInlineDisplayText(input) const files = Array.isArray(input.files) ? input.files : [] const includeImageData = options.includeImageData !== false const includeAudioData = options.includeAudioData !== false diff --git a/src/main/presenter/agentRuntimePresenter/dispatch.ts b/src/main/presenter/agentRuntimePresenter/dispatch.ts index 942e07cd6..988def569 100644 --- a/src/main/presenter/agentRuntimePresenter/dispatch.ts +++ b/src/main/presenter/agentRuntimePresenter/dispatch.ts @@ -1191,7 +1191,9 @@ async function runToolCall(params: { onProgress: applyProgressUpdate, signal: io.abortSignal, permissionMode: toolPermissionMode, - activeSkillNames: hooks?.getActiveSkillNames?.() + activeSkillNames: hooks?.getActiveSkillNames?.(), + enabledSkillNames: hooks?.getEnabledSkillNames?.(), + enabledPluginIds: hooks?.getEnabledPluginIds?.() }) let toolCallResult = await callTool() diff --git a/src/main/presenter/agentRuntimePresenter/index.ts b/src/main/presenter/agentRuntimePresenter/index.ts index 43a33af9b..a6f73e175 100644 --- a/src/main/presenter/agentRuntimePresenter/index.ts +++ b/src/main/presenter/agentRuntimePresenter/index.ts @@ -1330,7 +1330,8 @@ export class AgentRuntimePresenter implements IAgentImplementation { think: false, ...(normalizedInput.activeSkills?.length ? { activeSkills: normalizedInput.activeSkills } - : {}) + : {}), + ...(normalizedInput.inlineItems?.length ? { inlineItems: normalizedInput.inlineItems } : {}) } let compactionIntent: CompactionIntent | null = null @@ -3447,6 +3448,7 @@ export class AgentRuntimePresenter implements IAgentImplementation { const streamSessionActiveSkillNames = await this.resolveActiveSkillNamesForToolProfile(sessionId) + const streamExtensionPolicy = await this.resolveAgentExtensionPolicy(sessionId) const getEffectiveRuntimeSkillNames = (baseSkillNames = streamSessionActiveSkillNames) => this.resolveEffectiveActiveSkillNames(baseSkillNames, sessionId) const tools = @@ -3853,6 +3855,10 @@ export class AgentRuntimePresenter implements IAgentImplementation { Boolean(this.pendingInputCoordinator.getNextSteerInput(sessionId)), hooks: { getActiveSkillNames: () => getEffectiveRuntimeSkillNames(), + getEnabledSkillNames: () => + this.normalizeNullablePolicyList(streamExtensionPolicy.enabledSkillNames), + getEnabledPluginIds: () => + this.normalizeNullablePolicyList(streamExtensionPolicy.enabledPluginIds), activateSkill: async (skillName) => { const policy = await this.resolveAgentExtensionPolicy(sessionId) if (this.filterSkillNamesByPolicy([skillName], policy).length === 0) { @@ -5851,10 +5857,18 @@ export class AgentRuntimePresenter implements IAgentImplementation { ? ((parsed as { activeSkills?: unknown }).activeSkills as string[]) : [] ) + const inlineItems: NonNullable = Array.isArray( + (parsed as { inlineItems?: unknown }).inlineItems + ) + ? ((parsed as { inlineItems?: unknown }).inlineItems as NonNullable< + SendMessageInput['inlineItems'] + >) + : [] return { text, files, - ...(activeSkills.length > 0 ? { activeSkills } : {}) + ...(activeSkills.length > 0 ? { activeSkills } : {}), + ...(inlineItems.length > 0 ? { inlineItems } : {}) } } catch { return { text: content, files: [] } @@ -5875,10 +5889,12 @@ export class AgentRuntimePresenter implements IAgentImplementation { const activeSkills = this.normalizeSkillNames( Array.isArray(input.activeSkills) ? input.activeSkills : [] ) + const inlineItems = Array.isArray(input.inlineItems) ? input.inlineItems : [] return { text, files, - ...(activeSkills.length > 0 ? { activeSkills } : {}) + ...(activeSkills.length > 0 ? { activeSkills } : {}), + ...(inlineItems.length > 0 ? { inlineItems } : {}) } } @@ -5928,6 +5944,7 @@ export class AgentRuntimePresenter implements IAgentImplementation { } const next = { ...parsed, text } as Record + delete next.inlineItems if (!Array.isArray(next.files)) { next.files = [] @@ -5964,6 +5981,10 @@ export class AgentRuntimePresenter implements IAgentImplementation { next.content = mapped } + if (Array.isArray(next.inlineItems)) { + delete next.inlineItems + } + return JSON.stringify(next) } catch { return JSON.stringify(fallback) @@ -6497,6 +6518,7 @@ export class AgentRuntimePresenter implements IAgentImplementation { const extensionPolicy = await this.resolveAgentExtensionPolicy(sessionId) const result = await this.toolPresenter.callTool(request, { agentId: this.getSessionAgentId(sessionId) ?? 'deepchat', + enabledSkillNames: extensionPolicy.enabledSkillNames ?? undefined, enabledPluginIds: extensionPolicy.enabledPluginIds ?? undefined, onProgress: (update) => { if ( diff --git a/src/main/presenter/agentRuntimePresenter/messageStore.ts b/src/main/presenter/agentRuntimePresenter/messageStore.ts index 78cea62b6..b2b6d7756 100644 --- a/src/main/presenter/agentRuntimePresenter/messageStore.ts +++ b/src/main/presenter/agentRuntimePresenter/messageStore.ts @@ -742,13 +742,15 @@ export class DeepChatMessageStore { const rawUserContent = this.parseUserContent(row.content) const activeSkills = rawUserContent?.activeSkills ?? [] + const inlineItems = rawUserContent?.inlineItems ?? [] return JSON.stringify({ text: userRow.text, files: fileRows.map((fileRow) => this.toMessageFile(fileRow)), links: linkRows.map((linkRow) => linkRow.url), search: userRow.search_enabled === 1, think: userRow.think_enabled === 1, - ...(activeSkills.length > 0 ? { activeSkills } : {}) + ...(activeSkills.length > 0 ? { activeSkills } : {}), + ...(inlineItems.length > 0 ? { inlineItems } : {}) } satisfies UserMessageContent) } @@ -786,7 +788,8 @@ export class DeepChatMessageStore { : [], search: parsed.search === true, think: parsed.think === true, - activeSkills: this.normalizeActiveSkills(parsed.activeSkills) + activeSkills: this.normalizeActiveSkills(parsed.activeSkills), + inlineItems: Array.isArray(parsed.inlineItems) ? parsed.inlineItems : [] } } catch { return null diff --git a/src/main/presenter/agentRuntimePresenter/pendingInputCoordinator.ts b/src/main/presenter/agentRuntimePresenter/pendingInputCoordinator.ts index b33575ae3..94bf19923 100644 --- a/src/main/presenter/agentRuntimePresenter/pendingInputCoordinator.ts +++ b/src/main/presenter/agentRuntimePresenter/pendingInputCoordinator.ts @@ -23,10 +23,12 @@ function normalizeInput(input: string | SendMessageInput): SendMessageInput { ) : [] + const inlineItems = Array.isArray(input?.inlineItems) ? input.inlineItems : [] return { text: typeof input?.text === 'string' ? input.text : '', files: Array.isArray(input?.files) ? input.files.filter(Boolean) : [], - ...(activeSkills.length > 0 ? { activeSkills } : {}) + ...(activeSkills.length > 0 ? { activeSkills } : {}), + ...(inlineItems.length > 0 ? { inlineItems } : {}) } } diff --git a/src/main/presenter/agentRuntimePresenter/pendingInputStore.ts b/src/main/presenter/agentRuntimePresenter/pendingInputStore.ts index 893e09392..2744eaddb 100644 --- a/src/main/presenter/agentRuntimePresenter/pendingInputStore.ts +++ b/src/main/presenter/agentRuntimePresenter/pendingInputStore.ts @@ -7,6 +7,8 @@ import type { import type { SQLitePresenter } from '../sqlitePresenter' import type { DeepChatPendingInputRow } from '../sqlitePresenter/tables/deepchatPendingInputs' +type InlineItem = NonNullable[number] + function normalizeInput(input: string | SendMessageInput): SendMessageInput { if (typeof input === 'string') { return { text: input, files: [] } @@ -22,13 +24,29 @@ function normalizeInput(input: string | SendMessageInput): SendMessageInput { ) : [] + const inlineItems = Array.isArray(input?.inlineItems) ? input.inlineItems : [] return { text: typeof input?.text === 'string' ? input.text : '', files: Array.isArray(input?.files) ? input.files.filter(Boolean) : [], - ...(activeSkills.length > 0 ? { activeSkills } : {}) + ...(activeSkills.length > 0 ? { activeSkills } : {}), + ...(inlineItems.length > 0 ? { inlineItems } : {}) } } +function shiftInlineItems( + inlineItems: SendMessageInput['inlineItems'], + offset: number +): InlineItem[] { + if (!Array.isArray(inlineItems) || inlineItems.length === 0) { + return [] + } + + return inlineItems.map((item) => ({ + ...item, + offset: Math.max(0, item.offset + offset) + })) +} + export class DeepChatPendingInputStore { private readonly sqlitePresenter: SQLitePresenter @@ -117,16 +135,25 @@ export class DeepChatPendingInputStore { const existing = this.parsePayload(row.payload_json) const next = normalizeInput(input) - const text = [existing.text.trim(), next.text.trim()].filter(Boolean).join('\n\n') + const existingText = existing.text.trim() + const nextText = next.text.trim() + const separator = existingText && nextText ? '\n\n' : '' + const text = [existingText, nextText].filter(Boolean).join(separator) + const nextOffset = existingText.length + separator.length const files = [...(existing.files ?? []), ...(next.files ?? [])].filter(Boolean) const activeSkills = Array.from( new Set([...(existing.activeSkills ?? []), ...(next.activeSkills ?? [])]) ) + const inlineItems = [ + ...(existing.inlineItems ?? []), + ...shiftInlineItems(next.inlineItems, nextOffset) + ] this.sqlitePresenter.deepchatPendingInputsTable.update(itemId, { payload_json: JSON.stringify({ text, files, - ...(activeSkills.length > 0 ? { activeSkills } : {}) + ...(activeSkills.length > 0 ? { activeSkills } : {}), + ...(inlineItems.length > 0 ? { inlineItems } : {}) }) }) return this.toRecord(this.requireRow(itemId, row.session_id)) diff --git a/src/main/presenter/agentRuntimePresenter/types.ts b/src/main/presenter/agentRuntimePresenter/types.ts index 25825088a..ce52b2d17 100644 --- a/src/main/presenter/agentRuntimePresenter/types.ts +++ b/src/main/presenter/agentRuntimePresenter/types.ts @@ -108,6 +108,8 @@ export interface ProcessHooks { commitDecision: (granted: boolean) => void ) => void getActiveSkillNames?: () => string[] + getEnabledSkillNames?: () => string[] | null | undefined + getEnabledPluginIds?: () => string[] | null | undefined activateSkill?: (skillName: string) => Promise normalizeToolResult?: (tool: { sessionId: string diff --git a/src/main/presenter/agentSessionPresenter/index.ts b/src/main/presenter/agentSessionPresenter/index.ts index 9f27c2331..862ad61bb 100644 --- a/src/main/presenter/agentSessionPresenter/index.ts +++ b/src/main/presenter/agentSessionPresenter/index.ts @@ -4126,10 +4126,12 @@ export class AgentSessionPresenter { ? content.files.filter((file): file is MessageFile => Boolean(file)) : [] const activeSkills = this.normalizeActiveSkills(content.activeSkills) + const inlineItems = Array.isArray(content.inlineItems) ? content.inlineItems : [] return { text, files, - ...(activeSkills.length > 0 ? { activeSkills } : {}) + ...(activeSkills.length > 0 ? { activeSkills } : {}), + ...(inlineItems.length > 0 ? { inlineItems } : {}) } } @@ -4138,7 +4140,15 @@ export class AgentSessionPresenter { const files = Array.isArray(input.files) ? input.files.filter((file): file is MessageFile => Boolean(file)) : [] - return this.withInitialMessageActiveSkills({ text, files }, input.activeSkills) + const inlineItems = Array.isArray(input.inlineItems) ? input.inlineItems : [] + return this.withInitialMessageActiveSkills( + { + text, + files, + ...(inlineItems.length > 0 ? { inlineItems } : {}) + }, + input.activeSkills + ) } private withInitialMessageActiveSkills( diff --git a/src/main/presenter/skillPresenter/skillTools.ts b/src/main/presenter/skillPresenter/skillTools.ts index 95e143e6f..b082818ef 100644 --- a/src/main/presenter/skillPresenter/skillTools.ts +++ b/src/main/presenter/skillPresenter/skillTools.ts @@ -11,7 +11,9 @@ export class SkillTools { async handleSkillList( conversationId?: string, - allowedSkillNames?: string[] + allowedSkillNames?: string[] | null, + activeSkillNames?: string[], + allowedPluginIds?: string[] | null ): Promise<{ skills: SkillListItem[] pinnedCount: number @@ -21,15 +23,27 @@ export class SkillTools { const allowedSkillSet = Array.isArray(allowedSkillNames) ? new Set(allowedSkillNames.map((skillName) => skillName.trim()).filter(Boolean)) : undefined - const allSkills = (await this.skillPresenter.getMetadataList()).filter( - (skill) => !allowedSkillSet || allowedSkillSet.has(skill.name) - ) + const allowedPluginSet = Array.isArray(allowedPluginIds) + ? new Set(allowedPluginIds.map((pluginId) => pluginId.trim()).filter(Boolean)) + : undefined + const allSkills = (await this.skillPresenter.getMetadataList()).filter((skill) => { + if (allowedSkillSet && !allowedSkillSet.has(skill.name)) { + return false + } + const ownerPluginId = skill.ownerPluginId?.trim() + return !ownerPluginId || !allowedPluginSet || allowedPluginSet.has(ownerPluginId) + }) + const listedSkillNames = new Set(allSkills.map((skill) => skill.name)) const pinnedSkills = conversationId - ? (await this.skillPresenter.getActiveSkills(conversationId)).filter( - (skillName) => !allowedSkillSet || allowedSkillSet.has(skillName) + ? (await this.skillPresenter.getActiveSkills(conversationId)).filter((skillName) => + listedSkillNames.has(skillName) ) : [] + const activeSkills = (Array.isArray(activeSkillNames) ? activeSkillNames : pinnedSkills).filter( + (skillName) => listedSkillNames.has(skillName) + ) const pinnedSet = new Set(pinnedSkills) + const activeSet = new Set(activeSkills) const skillList = allSkills.map((skill) => ({ name: skill.name, @@ -38,13 +52,13 @@ export class SkillTools { platforms: skill.platforms, metadata: skill.metadata, isPinned: pinnedSet.has(skill.name), - active: pinnedSet.has(skill.name) + active: activeSet.has(skill.name) })) return { skills: skillList, pinnedCount: pinnedSkills.length, - activeCount: pinnedSkills.length, + activeCount: activeSkills.length, totalCount: allSkills.length } } @@ -52,7 +66,8 @@ export class SkillTools { async handleSkillView( conversationId: string | undefined, input: { name: string; file_path?: string }, - allowedSkillNames?: string[] + allowedSkillNames?: string[] | null, + allowedPluginIds?: string[] | null ): Promise { const requestedSkillName = input.name.trim() const allowedSkillSet = Array.isArray(allowedSkillNames) @@ -66,6 +81,23 @@ export class SkillTools { } } + if (Array.isArray(allowedPluginIds)) { + const allowedPluginSet = new Set( + allowedPluginIds.map((pluginId) => pluginId.trim()).filter(Boolean) + ) + const metadata = (await this.skillPresenter.getMetadataList()).find( + (skill) => skill.name === requestedSkillName + ) + const ownerPluginId = metadata?.ownerPluginId?.trim() + if (ownerPluginId && !allowedPluginSet.has(ownerPluginId)) { + return { + success: false, + name: requestedSkillName, + error: `Skill '${requestedSkillName}' is not enabled for this agent` + } + } + } + return await this.skillPresenter.viewSkill(requestedSkillName, { filePath: input.file_path, conversationId diff --git a/src/main/presenter/toolPresenter/agentTools/agentToolManager.ts b/src/main/presenter/toolPresenter/agentTools/agentToolManager.ts index 0d4c9fa97..30b77d86d 100644 --- a/src/main/presenter/toolPresenter/agentTools/agentToolManager.ts +++ b/src/main/presenter/toolPresenter/agentTools/agentToolManager.ts @@ -105,6 +105,8 @@ interface AgentToolExecutionOptions { signal?: AbortSignal allowExternalFileAccess?: boolean activeSkillNames?: string[] + enabledSkillNames?: string[] | null + enabledPluginIds?: string[] | null } interface AgentToolPermissionCheckOptions { @@ -2135,11 +2137,21 @@ export class AgentToolManager { return undefined } + return this.normalizeSkillNameList(activeSkillNames) + } + + private normalizeNullableSkillOption(skillNames?: string[] | null): string[] | null | undefined { + if (skillNames === null || skillNames === undefined) { + return skillNames + } + + return this.normalizeSkillNameList(skillNames) + } + + private normalizeSkillNameList(skillNames: string[]): string[] { return Array.from( new Set( - activeSkillNames - .map((skillName) => skillName.trim()) - .filter((skillName) => skillName.length > 0) + skillNames.map((skillName) => skillName.trim()).filter((skillName) => skillName.length > 0) ) ) } @@ -2161,9 +2173,16 @@ export class AgentToolManager { const skillTools = this.getSkillTools() const effectiveActiveSkills = this.normalizeActiveSkillOption(options?.activeSkillNames) + const enabledSkillNames = this.normalizeNullableSkillOption(options?.enabledSkillNames) + const enabledPluginIds = this.normalizeNullableSkillOption(options?.enabledPluginIds) if (toolName === 'skill_list') { - const result = await skillTools.handleSkillList(conversationId, effectiveActiveSkills) + const result = await skillTools.handleSkillList( + conversationId, + enabledSkillNames, + effectiveActiveSkills, + enabledPluginIds + ) return { content: JSON.stringify(result) } } @@ -2181,7 +2200,8 @@ export class AgentToolManager { const result = await skillTools.handleSkillView( conversationId, validationResult.data, - effectiveActiveSkills + enabledSkillNames, + enabledPluginIds ) const normalizedViewedSkill = result.name?.trim() || validationResult.data.name.trim() const activeSkillNamesForResult = effectiveActiveSkills ?? [] diff --git a/src/main/presenter/toolPresenter/index.ts b/src/main/presenter/toolPresenter/index.ts index 60ed275d9..5b4c61eb3 100644 --- a/src/main/presenter/toolPresenter/index.ts +++ b/src/main/presenter/toolPresenter/index.ts @@ -79,9 +79,10 @@ export interface IToolPresenter { signal?: AbortSignal permissionMode?: PermissionMode activeSkillNames?: string[] + enabledSkillNames?: string[] | null agentId?: string enabledMcpServerIds?: string[] - enabledPluginIds?: string[] + enabledPluginIds?: string[] | null } ): Promise<{ content: unknown; rawData: MCPToolResponse }> preCheckToolPermission?( @@ -302,9 +303,10 @@ export class ToolPresenter implements IToolPresenter { signal?: AbortSignal permissionMode?: PermissionMode activeSkillNames?: string[] + enabledSkillNames?: string[] | null agentId?: string enabledMcpServerIds?: string[] - enabledPluginIds?: string[] + enabledPluginIds?: string[] | null } ): Promise<{ content: unknown; rawData: MCPToolResponse }> { const toolName = request.function.name @@ -346,7 +348,9 @@ export class ToolPresenter implements IToolPresenter { onProgress: options?.onProgress, signal: options?.signal, allowExternalFileAccess: allowsExternalFileAccess(options?.permissionMode), - activeSkillNames: options?.activeSkillNames + activeSkillNames: options?.activeSkillNames, + enabledSkillNames: options?.enabledSkillNames, + enabledPluginIds: options?.enabledPluginIds } ) const resolvedResponse = this.resolveAgentToolResponse(response) diff --git a/src/renderer/src/components/chat/ChatInputBox.vue b/src/renderer/src/components/chat/ChatInputBox.vue index 026459b03..e3a44e039 100644 --- a/src/renderer/src/components/chat/ChatInputBox.vue +++ b/src/renderer/src/components/chat/ChatInputBox.vue @@ -10,37 +10,6 @@ > -
-
- - {{ skillName }} - -
-
- -
- -
-
- -
diff --git a/src/renderer/src/components/chat/messageListItems.ts b/src/renderer/src/components/chat/messageListItems.ts index c1dd03d43..223263007 100644 --- a/src/renderer/src/components/chat/messageListItems.ts +++ b/src/renderer/src/components/chat/messageListItems.ts @@ -1,4 +1,4 @@ -import type { MessageFile } from '@shared/types/agent-interface' +import type { MessageFile, UserMessageInlineItem } from '@shared/types/agent-interface' import type { AgentPlanDisplayItem, AgentPlanTerminalReason } from '@shared/types/agent-plan' import type { ToolCallImagePreview } from '@shared/types/core/mcp' @@ -32,6 +32,25 @@ export type DisplayUserMessageMentionBlock = { category: string } +export type DisplayUserMessageSkillBlock = { + type: 'skill' + skillName: string +} + +export type DisplayUserMessageFileBlock = { + type: 'file' + fileName: string + filePath: string + mimeType?: string +} + +export type DisplayUserMessageInlineBlock = + | DisplayUserMessageTextBlock + | DisplayUserMessageMentionBlock + | DisplayUserMessageCodeBlock + | DisplayUserMessageSkillBlock + | DisplayUserMessageFileBlock + export type DisplayUserMessageContent = { continue?: boolean files: MessageFile[] @@ -42,6 +61,7 @@ export type DisplayUserMessageContent = { search: boolean activeSkills?: string[] text: string + inlineItems?: UserMessageInlineItem[] content?: ( | DisplayUserMessageTextBlock | DisplayUserMessageMentionBlock diff --git a/src/renderer/src/components/chat/nodes/CommandFormView.vue b/src/renderer/src/components/chat/nodes/CommandFormView.vue new file mode 100644 index 000000000..fc2ca2ff7 --- /dev/null +++ b/src/renderer/src/components/chat/nodes/CommandFormView.vue @@ -0,0 +1,129 @@ + + + diff --git a/src/renderer/src/components/chat/nodes/FileAttachmentView.vue b/src/renderer/src/components/chat/nodes/FileAttachmentView.vue new file mode 100644 index 000000000..97d704986 --- /dev/null +++ b/src/renderer/src/components/chat/nodes/FileAttachmentView.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/renderer/src/components/chat/nodes/SkillChipView.vue b/src/renderer/src/components/chat/nodes/SkillChipView.vue new file mode 100644 index 000000000..3782708fa --- /dev/null +++ b/src/renderer/src/components/chat/nodes/SkillChipView.vue @@ -0,0 +1,39 @@ + + + diff --git a/src/renderer/src/components/chat/nodes/commandForm.ts b/src/renderer/src/components/chat/nodes/commandForm.ts new file mode 100644 index 000000000..fce398cab --- /dev/null +++ b/src/renderer/src/components/chat/nodes/commandForm.ts @@ -0,0 +1,68 @@ +import { Node, mergeAttributes } from '@tiptap/core' +import { VueNodeViewRenderer } from '@tiptap/vue-3' +import CommandFormView from './CommandFormView.vue' + +export const CommandForm = Node.create({ + name: 'commandForm', + + group: 'block', + + atom: true, + + selectable: false, + + draggable: false, + + addAttributes() { + return { + mode: { + default: 'command' + }, + commandName: { + default: '' + }, + description: { + default: '' + }, + confirmText: { + default: '' + }, + fields: { + default: '[]' + }, + pendingCommand: { + default: null + }, + pendingPrompt: { + default: null + } + } + }, + + parseHTML() { + return [ + { + tag: 'div[data-command-form]' + } + ] + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'div', + mergeAttributes(HTMLAttributes, { + 'data-command-form': '', + class: 'my-2 rounded-lg border border-border bg-card p-3 shadow-sm' + }), + ['div', { class: 'text-sm font-medium' }, 'Command Form'] + ] + }, + + renderText() { + return '' + }, + + addNodeView() { + return VueNodeViewRenderer(CommandFormView) + } +}) diff --git a/src/renderer/src/components/chat/nodes/fileAttachment.ts b/src/renderer/src/components/chat/nodes/fileAttachment.ts new file mode 100644 index 000000000..30f9961b6 --- /dev/null +++ b/src/renderer/src/components/chat/nodes/fileAttachment.ts @@ -0,0 +1,70 @@ +import { Node, mergeAttributes } from '@tiptap/core' +import { VueNodeViewRenderer } from '@tiptap/vue-3' +import FileAttachmentView from './FileAttachmentView.vue' + +export const FileAttachment = Node.create({ + name: 'fileAttachment', + + group: 'inline', + + inline: true, + + atom: true, + + selectable: true, + + draggable: false, + + addAttributes() { + return { + fileName: { + default: '', + parseHTML: (el) => el.getAttribute('data-file-name'), + renderHTML: (attrs) => ({ + 'data-file-name': attrs.fileName + }) + }, + filePath: { + default: '', + parseHTML: (el) => el.getAttribute('data-file-path'), + renderHTML: (attrs) => ({ + 'data-file-path': attrs.filePath + }) + }, + mimeType: { + default: '', + parseHTML: (el) => el.getAttribute('data-mime-type'), + renderHTML: (attrs) => ({ + 'data-mime-type': attrs.mimeType + }) + } + } + }, + + parseHTML() { + return [ + { + tag: 'span[data-file-attachment]' + } + ] + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'span', + mergeAttributes(HTMLAttributes, { + 'data-file-attachment': '', + class: + 'inline-flex items-center gap-1 rounded-md border border-muted-foreground/30 bg-muted/20 px-1.5 py-0.5 text-xs text-muted-foreground' + }) + ] + }, + + renderText() { + return '' + }, + + addNodeView() { + return VueNodeViewRenderer(FileAttachmentView) + } +}) diff --git a/src/renderer/src/components/chat/nodes/skillChip.ts b/src/renderer/src/components/chat/nodes/skillChip.ts new file mode 100644 index 000000000..0dd0bec6c --- /dev/null +++ b/src/renderer/src/components/chat/nodes/skillChip.ts @@ -0,0 +1,56 @@ +import { Node, mergeAttributes } from '@tiptap/core' +import { VueNodeViewRenderer } from '@tiptap/vue-3' +import SkillChipView from './SkillChipView.vue' + +export const SkillChip = Node.create({ + name: 'skillChip', + + group: 'inline', + + inline: true, + + atom: true, + + selectable: true, + + draggable: false, + + addAttributes() { + return { + skillName: { + default: null, + parseHTML: (el) => el.getAttribute('data-skill-name'), + renderHTML: (attrs) => ({ + 'data-skill-name': attrs.skillName + }) + } + } + }, + + parseHTML() { + return [ + { + tag: 'span[data-skill-chip]' + } + ] + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'span', + mergeAttributes(HTMLAttributes, { + 'data-skill-chip': '', + class: + 'inline-flex items-center gap-1 rounded-md border border-primary/30 bg-primary/10 px-1.5 py-0.5 text-xs text-primary' + }) + ] + }, + + renderText() { + return '' + }, + + addNodeView() { + return VueNodeViewRenderer(SkillChipView) + } +}) diff --git a/src/renderer/src/components/chat/nodes/symbols.ts b/src/renderer/src/components/chat/nodes/symbols.ts new file mode 100644 index 000000000..e4bf2d9da --- /dev/null +++ b/src/renderer/src/components/chat/nodes/symbols.ts @@ -0,0 +1,11 @@ +import type { InjectionKey } from 'vue' + +export interface InputNodeActions { + prepareCommandFormSubmit: () => void + removeSkill: (skillName: string) => void + removeFile: (filePath: string) => void + submitCommandForm: (values: Record) => void + cancelCommandForm: () => void +} + +export const INPUT_NODE_ACTIONS: InjectionKey = Symbol('input-node-actions') diff --git a/src/renderer/src/components/message/MessageContent.vue b/src/renderer/src/components/message/MessageContent.vue index 0c3014af0..1cf9edd9c 100644 --- a/src/renderer/src/components/message/MessageContent.vue +++ b/src/renderer/src/components/message/MessageContent.vue @@ -22,6 +22,26 @@ > {{ block.content }} + + + + + {{ block.skillName }} + + + + + + {{ block.fileName }} + @@ -31,10 +51,13 @@ import { computed } from 'vue' import { Icon } from '@iconify/vue' import type { DisplayUserMessageCodeBlock, + DisplayUserMessageFileBlock, DisplayUserMessageMentionBlock, + DisplayUserMessageSkillBlock, DisplayUserMessageTextBlock } from '@/components/chat/messageListItems' import { useLanguageStore } from '@/stores/language' +import { getMimeTypeIcon } from '@/lib/utils' const MENTION_ICON_MAP: Record = { context: 'lucide:quote', @@ -54,6 +77,8 @@ type ContentBlock = | DisplayUserMessageTextBlock | DisplayUserMessageMentionBlock | DisplayUserMessageCodeBlock + | DisplayUserMessageSkillBlock + | DisplayUserMessageFileBlock const props = defineProps<{ content: ContentBlock[] @@ -94,4 +119,8 @@ const getMentionTitle = (block: DisplayUserMessageMentionBlock) => { } return block.content } + +const getFileIcon = (block: DisplayUserMessageFileBlock) => { + return getMimeTypeIcon(block.mimeType || 'application/octet-stream') +} diff --git a/src/renderer/src/components/message/MessageItemUser.vue b/src/renderer/src/components/message/MessageItemUser.vue index c24b28446..9db1066b3 100644 --- a/src/renderer/src/components/message/MessageItemUser.vue +++ b/src/renderer/src/components/message/MessageItemUser.vue @@ -19,12 +19,12 @@ :timestamp="message.timestamp" />
-
+
+ @@ -114,10 +119,11 @@