feat: update custom mode draft

This commit is contained in:
duanfuxiang 2025-04-28 16:58:29 +08:00
parent 5558c96aa1
commit 497a9739d7
12 changed files with 2539 additions and 124 deletions

View File

@ -97,6 +97,7 @@
"p-limit": "^6.1.0", "p-limit": "^6.1.0",
"parse5": "^7.1.2", "parse5": "^7.1.2",
"path": "^0.12.7", "path": "^0.12.7",
"radix-ui": "^1.3.4",
"react": "^18.3.1", "react": "^18.3.1",
"react-contenteditable": "^3.3.7", "react-contenteditable": "^3.3.7",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",

1703
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@ import * as path from 'path'
import { BaseSerializedNode } from '@lexical/clipboard/clipboard' import { BaseSerializedNode } from '@lexical/clipboard/clipboard'
import { useMutation } from '@tanstack/react-query' import { useMutation } from '@tanstack/react-query'
import { CircleStop, History, Plus, SquareSlash } from 'lucide-react' import { CircleStop, History, NotebookPen, Plus, SquareSlash } from 'lucide-react'
import { App, Notice } from 'obsidian' import { App, Notice } from 'obsidian'
import { import {
forwardRef, forwardRef,
@ -54,6 +54,7 @@ import PromptInputWithActions, { ChatUserInputRef } from './chat-input/PromptInp
import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text' import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text'
import { ChatHistory } from './ChatHistoryView' import { ChatHistory } from './ChatHistoryView'
import CommandsView from './CommandsView' import CommandsView from './CommandsView'
import CustomModeView from './CustomModeView'
import MarkdownReasoningBlock from './Markdown/MarkdownReasoningBlock' import MarkdownReasoningBlock from './Markdown/MarkdownReasoningBlock'
import QueryProgress, { QueryProgressState } from './QueryProgress' import QueryProgress, { QueryProgressState } from './QueryProgress'
import ReactMarkdown from './ReactMarkdown' import ReactMarkdown from './ReactMarkdown'
@ -161,7 +162,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
} }
} }
const [tab, setTab] = useState<'chat' | 'commands'>('chat') const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode'>('custom-mode')
const [selectedSerializedNodes, setSelectedSerializedNodes] = useState<BaseSerializedNode[]>([]) const [selectedSerializedNodes, setSelectedSerializedNodes] = useState<BaseSerializedNode[]>([])
useEffect(() => { useEffect(() => {
@ -935,6 +936,19 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
> >
<SquareSlash size={18} color={tab === 'commands' ? 'var(--text-accent)' : 'var(--text-color)'} /> <SquareSlash size={18} color={tab === 'commands' ? 'var(--text-accent)' : 'var(--text-color)'} />
</button> </button>
<button
onClick={() => {
// switch between chat and prompts
if (tab === 'custom-mode') {
setTab('chat')
} else {
setTab('custom-mode')
}
}}
className="infio-chat-list-dropdown"
>
<NotebookPen size={18} color={tab === 'custom-mode' ? 'var(--text-accent)' : 'var(--text-color)'} />
</button>
</div> </div>
</div> </div>
{/* main view */} {/* main view */}
@ -1047,12 +1061,16 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
addedBlockKey={addedBlockKey} addedBlockKey={addedBlockKey}
/> />
</> </>
) : ( ) : tab === 'commands' ? (
<div className="infio-chat-commands"> <div className="infio-chat-commands">
<CommandsView <CommandsView
selectedSerializedNodes={selectedSerializedNodes} selectedSerializedNodes={selectedSerializedNodes}
/> />
</div> </div>
) : (
<div className="infio-chat-commands">
<CustomModeView />
</div>
)} )}
</div> </div>
) )

View File

@ -0,0 +1,555 @@
import { Plus, Undo2, Settings, Circle, Trash2 } from 'lucide-react';
import React, { useState, useEffect } from 'react';
import { useCustomModes } from '../../hooks/use-custom-mode';
import { CustomMode, ToolGroup, toolGroups, GroupEntry } from '../../database/json/custom-mode/types';
import { modes as buildinModes } from '../../utils/modes';
const CustomModeView = () => {
const {
createCustomMode,
deleteCustomMode,
updateCustomMode,
customModeList,
} = useCustomModes()
// 当前选择的模式
const [selectedMode, setSelectedMode] = useState<string>('ask')
const [isBuiltinMode, setIsBuiltinMode] = useState<boolean>(true)
const isNewMode = React.useMemo(() => selectedMode === "add_new_mode", [selectedMode])
// new mode config
const [newMode, setNewMode] = useState<CustomMode>({
id: '',
slug: '',
name: '',
roleDefinition: '',
customInstructions: '',
groups: [],
source: 'global',
updatedAt: 0,
})
// custom mode id
const [customModeId, setCustomModeId] = useState<string>('')
// 模型名称
const [modeName, setModeName] = useState<string>('')
// 角色定义
const [roleDefinition, setRoleDefinition] = useState<string>('')
// 选中的工具组
const [selectedTools, setSelectedTools] = useState<GroupEntry[]>([])
// 自定义指令
const [customInstructions, setCustomInstructions] = useState<string>('')
// 当模式变更时更新表单数据
useEffect(() => {
// new mode
if (isNewMode) {
setIsBuiltinMode(false);
setModeName(newMode.name);
setRoleDefinition(newMode.roleDefinition);
setCustomInstructions(newMode.customInstructions || '');
setSelectedTools(newMode.groups as GroupEntry[]);
setCustomModeId('');
return;
}
const builtinMode = buildinModes.find(m => m.slug === selectedMode);
if (builtinMode) {
setIsBuiltinMode(true);
setModeName(builtinMode.name);
setRoleDefinition(builtinMode.roleDefinition);
setCustomInstructions(builtinMode.customInstructions || '');
setSelectedTools(builtinMode.groups as GroupEntry[]);
setCustomModeId(''); // 内置模式没有自定义 ID
} else {
setIsBuiltinMode(false);
const customMode = customModeList.find(m => m.slug === selectedMode);
if (customMode) {
setCustomModeId(customMode.id || '');
setModeName(customMode.name);
setRoleDefinition(customMode.roleDefinition);
setCustomInstructions(customMode.customInstructions || '');
setSelectedTools(customMode.groups);
} else {
console.log("error, custom mode not found")
}
}
}, [selectedMode, customModeList]);
// 处理工具组选择变更
const handleToolChange = React.useCallback((tool: ToolGroup) => {
if (isNewMode) {
setNewMode((prev) => ({
...prev,
groups: prev.groups.includes(tool) ? prev.groups.filter(t => t !== tool) : [...prev.groups, tool]
}))
}
setSelectedTools(prev => {
if (prev.includes(tool)) {
return prev.filter(t => t !== tool);
} else {
return [...prev, tool];
}
});
}, [isNewMode])
// 更新模式配置
const handleUpdateMode = React.useCallback(async () => {
if (!isBuiltinMode) {
await updateCustomMode(
customModeId,
modeName,
roleDefinition,
customInstructions,
selectedTools
);
}
}, [isBuiltinMode, customModeId, modeName, roleDefinition, customInstructions, selectedTools])
// 创建新模式
const createNewMode = React.useCallback(async () => {
if (!isNewMode) return;
await createCustomMode(
modeName,
roleDefinition,
customInstructions,
selectedTools
);
// reset
setNewMode({
id: '',
slug: '',
name: '',
roleDefinition: '',
customInstructions: '',
groups: [],
source: 'global',
updatedAt: 0,
})
setSelectedMode("add_new_mode")
}, [isNewMode, modeName, roleDefinition, customInstructions, selectedTools])
// 删除模式
const deleteMode = React.useCallback(async () => {
if (isNewMode || isBuiltinMode) return;
await deleteCustomMode(customModeId);
setModeName('')
setRoleDefinition('')
setCustomInstructions('')
setSelectedTools([])
setSelectedMode('add_new_mode')
}, [isNewMode, isBuiltinMode, customModeId])
return (
<div className="infio-custom-modes-container">
{/* 模式配置标题和按钮 */}
<div className="infio-custom-modes-header">
<div className="infio-custom-modes-title">
<h2></h2>
</div>
{/* <div className="infio-custom-modes-actions">
<button className="infio-custom-modes-btn">
<PlusCircle size={18} />
</button>
<button className="infio-custom-modes-btn">
<Settings size={18} />
</button>
</div> */}
</div>
{/* 创建模式提示 */}
<div className="infio-custom-modes-tip">
+
</div>
{/* 模式选择区 */}
<div className="infio-custom-modes-builtin">
{[...buildinModes, ...customModeList].map(mode => (
<button
key={mode.slug}
className={`infio-mode-btn ${selectedMode === mode.slug ? 'active' : ''}`}
onClick={() => { setSelectedMode(mode.slug) }}
>
{mode.name}
</button>
))}
<button
key={"add_new_mode"}
className={`infio-mode-btn ${selectedMode === "add_new_mode" ? 'active' : ''}`}
onClick={() => setSelectedMode("add_new_mode")}
>
<Plus size={18} />
</button>
</div>
{/* 模式名称 */}
<div className="infio-custom-modes-section">
<div className="infio-section-header">
<h3></h3>
{!isBuiltinMode && !isNewMode && (
<button className="infio-section-btn" onClick={deleteMode}>
<Trash2 size={16} />
</button>
)}
</div>
<input
type="text"
value={modeName}
onChange={(e) => {
if (isNewMode) {
setNewMode((prev) => ({ ...prev, name: e.target.value }))
}
setModeName(e.target.value)
}}
className="infio-custom-modes-input"
placeholder="输入模式名称..."
disabled={isBuiltinMode}
/>
</div>
{/* 角色定义 */}
<div className="infio-custom-modes-section">
<div className="infio-section-header">
<h3></h3>
<button className="infio-section-btn">
<Undo2 size={16} />
</button>
</div>
<p className="infio-section-subtitle"></p>
<textarea
className="infio-custom-textarea"
value={roleDefinition}
onChange={(e) => {
if (isNewMode) {
setNewMode((prev) => ({ ...prev, roleDefinition: e.target.value }))
}
setRoleDefinition(e.target.value)
}}
placeholder="输入角色定义..."
/>
</div>
{/* 可用功能 */}
<div className="infio-custom-modes-section">
<div className="infio-section-header">
<h3></h3>
<button className="infio-section-btn">
<Undo2 size={16} />
</button>
</div>
<p className="infio-section-subtitle"></p>
<div className="infio-tools-list">
<div className="infio-tool-item">
<label>
<input
type="checkbox"
checked={selectedTools.includes('read')}
onChange={() => handleToolChange('read')}
/>
</label>
</div>
<div className="infio-tool-item">
<label>
<input
type="checkbox"
checked={selectedTools.includes('edit')}
onChange={() => handleToolChange('edit')}
/>
</label>
</div>
<div className="infio-tool-item">
<label>
<input
type="checkbox"
checked={selectedTools.includes('research')}
onChange={() => handleToolChange('research')}
/>
</label>
</div>
</div>
</div>
{/* 模式专属规则 */}
<div className="infio-custom-modes-section">
<div className="infio-section-header">
<h3> </h3>
</div>
<p className="infio-section-subtitle"></p>
<textarea
className="infio-custom-textarea"
value={customInstructions}
onChange={(e) => {
if (isNewMode) {
setNewMode((prev) => ({ ...prev, customInstructions: e.target.value }))
}
setCustomInstructions(e.target.value)
}}
placeholder="输入模式自定义指令..."
/>
<p className="infio-section-footer">
<a href="#" className="infio-link">_infio_prompts/code-rules/</a>
</p>
</div>
<div className="infio-custom-modes-actions">
<button className="infio-preview-btn">
</button>
<button
className="infio-preview-btn"
onClick={() => {
if (isNewMode) {
createNewMode()
} else {
handleUpdateMode()
}
}}
>
</button>
</div>
{/* 样式 */}
<style>
{`
.infio-custom-modes-container {
display: flex;
flex-direction: column;
padding: 16px;
gap: 16px;
color: var(--text-normal);
height: 100%;
overflow-y: auto;
}
.infio-custom-modes-input {
background-color: var(--background-primary) !important;
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
color: var(--text-normal);
padding: var(--size-4-2);
font-size: var(--font-ui-small);
width: 100%;
box-sizing: border-box;
margin-bottom: var(--size-4-2);
}
.infio-custom-modes-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.infio-custom-modes-title h2 {
margin: 0;
font-size: 24px;
}
.infio-custom-modes-actions {
display: flex;
gap: 8px;
}
.infio-custom-modes-btn {
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid #444;
color: var(--text-normal)
border-radius: 4px;
padding: 6px;
cursor: pointer;
}
.infio-custom-modes-tip {
color: #888;
font-size: 14px;
margin-bottom: 8px;
}
.infio-custom-modes-builtin {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 10px;
}
.infio-mode-btn {
display: flex;
align-items: center;
justify-content: center;
gap: var(--size-2-2);
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
border-radius: var(--radius-s);
padding: var(--size-2-3) var(--size-4-3);
cursor: pointer;
font-size: var(--font-ui-small);
align-self: flex-start;
margin-top: var(--size-4-2);
}
.infio-mode-btn.active {
background-color: var(--text-accent);
}
.infio-custom-modes-custom {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 16px;
}
.infio-mode-btn-custom {
background-color: transparent;
border: 1px solid #444;
border-radius: 4px;
padding: 6px 12px;
color: #888;
cursor: pointer;
font-size: 14px;
}
.infio-mode-btn-custom.active {
background-color: var(--text-accent);
border-color: var(--text-accent);
color: var(--text-normal);
}
.infio-custom-modes-section {
margin-bottom: 16px;
}
.infio-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.infio-section-header h3 {
margin: 0;
font-size: 16px;
}
.infio-section-btn {
display: flex;
align-items: center;
justify-content: center;
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
color: var(--text-muted);
padding: 0 !important;
margin: 0 !important;
width: 24px !important;
height: 24px !important;
&:hover {
background-color: var(--background-modifier-hover) !important;
}
}
.infio-section-subtitle {
color: #888;
font-size: 14px;
margin: 4px 0 12px;
}
.infio-custom-textarea {
background-color: var(--background-primary) !important;
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
color: var(--text-normal);
padding: var(--size-4-2);
font-size: var(--font-ui-small);
width: 100%;
min-height: 160px;
resize: vertical;
box-sizing: border-box;
}
.infio-select {
width: 100%;
border: 1px solid #444;
border-radius: 4px;
color: var(--text-normal);
padding: 8px 12px;
margin-bottom: 8px;
}
.infio-tools-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.infio-tool-item {
display: flex;
align-items: center;
}
.infio-tool-item label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.infio-code-section {
border: 1px solid #444;
border-radius: 4px;
padding: 8px;
margin-bottom: 12px;
}
.infio-code-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
color: #888;
}
.infio-section-footer {
margin-top: 0px;
font-size: 14px;
color: #888;
}
.infio-link {
color: var(--text-accent);
text-decoration: none;
}
.infio-preview-btn {
border: 1px solid #444;
color: var(--text-normal);
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
}
`}
</style>
</div>
)
}
export default CustomModeView

View File

@ -33,7 +33,7 @@ import {
import { getToolDescriptionsForMode } from "./tools" import { getToolDescriptionsForMode } from "./tools"
export class SystemPromptsManager { export class SystemPrompt {
protected dataDir: string protected dataDir: string
protected app: App protected app: App

View File

@ -1,4 +1,5 @@
export const ROOT_DIR = '.infio_json_db' export const ROOT_DIR = '.infio_json_db'
export const COMMAND_DIR = 'commands' export const COMMAND_DIR = 'commands'
export const CHAT_DIR = 'chats' export const CHAT_DIR = 'chats'
export const CUSTOM_MODE_DIR = 'custom_modes'
export const INITIAL_MIGRATION_MARKER = '.initial_migration_completed' export const INITIAL_MIGRATION_MARKER = '.initial_migration_completed'

View File

@ -0,0 +1,153 @@
import fuzzysort from 'fuzzysort'
import { App } from 'obsidian'
import { v4 as uuidv4 } from 'uuid'
import { AbstractJsonRepository } from '../base'
import { CUSTOM_MODE_DIR, ROOT_DIR } from '../constants'
import {
DuplicateCustomModeException,
EmptyCustomModeNameException,
} from '../exception'
import { CUSTOM_MODE_SCHEMA_VERSION, CustomMode, CustomModeMetadata } from './types'
export class CustomModeManager extends AbstractJsonRepository<
CustomMode,
CustomModeMetadata
> {
constructor(app: App) {
super(app, `${ROOT_DIR}/${CUSTOM_MODE_DIR}`)
}
protected generateFileName(mode: CustomMode): string {
// Format: v{schemaVersion}_name_id.json (with name encoded)
const encodedName = encodeURIComponent(mode.name)
return `v${CUSTOM_MODE_SCHEMA_VERSION}_${encodedName}_${mode.id}.json`
}
protected parseFileName(fileName: string): CustomModeMetadata | null {
const match = fileName.match(
new RegExp(`^v${CUSTOM_MODE_SCHEMA_VERSION}_(.+)_([0-9a-f-]+)\\.json$`),
)
if (!match) return null
const encodedName = match[1]
const id = match[2]
const name = decodeURIComponent(encodedName)
return {
id,
name,
updatedAt: Date.now(),
schemaVersion: CUSTOM_MODE_SCHEMA_VERSION,
}
}
public async createCustomMode(
customMode: Omit<
CustomMode,
'id' | 'slug' | 'createdAt' | 'updatedAt' | 'schemaVersion'
>,
): Promise<CustomMode> {
if (customMode.name !== undefined && customMode.name.length === 0) {
throw new EmptyCustomModeNameException()
}
const existingCustomMode = await this.findByName(customMode.name)
if (existingCustomMode) {
throw new DuplicateCustomModeException(customMode.name)
}
const newCustomMode: CustomMode = {
id: uuidv4(),
...customMode,
slug: customMode.name.toLowerCase().replace(/ /g, '-'),
updatedAt: Date.now(),
schemaVersion: CUSTOM_MODE_SCHEMA_VERSION,
}
await this.create(newCustomMode)
return newCustomMode
}
public async ListCustomModes(): Promise<CustomMode[]> {
const allMetadata = await this.listMetadata()
const allCustomModes = await Promise.all(allMetadata.map(async (meta) => this.read(meta.fileName)))
return allCustomModes.sort((a, b) => b.updatedAt - a.updatedAt)
}
public async findById(id: string): Promise<CustomMode | null> {
const allMetadata = await this.listMetadata()
const targetMetadata = allMetadata.find((meta) => meta.id === id)
if (!targetMetadata) return null
return this.read(targetMetadata.fileName)
}
public async findByName(name: string): Promise<CustomMode | null> {
const allMetadata = await this.listMetadata()
const targetMetadata = allMetadata.find((meta) => meta.name === name)
if (!targetMetadata) return null
return this.read(targetMetadata.fileName)
}
public async updateCustomMode(
id: string,
updates: Partial<
Omit<CustomMode, 'id' | 'slug' | 'createdAt' | 'updatedAt' | 'schemaVersion'>
>,
): Promise<CustomMode | null> {
if (updates.name !== undefined && updates.name.length === 0) {
throw new EmptyCustomModeNameException()
}
const customMode = await this.findById(id)
if (!customMode) return null
if (updates.name && updates.name !== customMode.name) {
const existingCustomMode = await this.findByName(updates.name)
if (existingCustomMode) {
throw new DuplicateCustomModeException(updates.name)
}
}
const updatedCustomMode: CustomMode = {
...customMode,
...updates,
updatedAt: Date.now(),
}
await this.update(customMode, updatedCustomMode)
return updatedCustomMode
}
public async deleteCustomMode(id: string): Promise<boolean> {
const customMode = await this.findById(id)
if (!customMode) return false
const fileName = this.generateFileName(customMode)
await this.delete(fileName)
return true
}
public async searchCustomModes(query: string): Promise<CustomMode[]> {
const allMetadata = await this.listMetadata()
const results = fuzzysort.go(query, allMetadata, {
keys: ['name'],
threshold: 0.2,
limit: 20,
all: true,
})
const customModes = (
await Promise.all(
results.map(async (result) => this.read(result.obj.fileName)),
)
).filter((customMode): customMode is CustomMode => customMode !== null)
return customModes
}
}

View File

@ -0,0 +1,84 @@
import { z } from "zod"
export const CUSTOM_MODE_SCHEMA_VERSION = 1
export const toolGroups = [
"read",
"edit",
"research",
// "browser",
// "command",
// "mcp",
"modes",
] as const
export const toolGroupsSchema = z.enum(toolGroups)
export type ToolGroup = z.infer<typeof toolGroupsSchema>
export const groupOptionsSchema = z.object({
fileRegex: z
.string()
.optional()
.refine(
(pattern) => {
if (!pattern) {
return true // Optional, so empty is valid.
}
try {
new RegExp(pattern)
return true
} catch {
return false
}
},
{ message: "Invalid regular expression pattern" },
),
description: z.string().optional(),
})
export const groupEntrySchema = z.union([toolGroupsSchema, z.tuple([toolGroupsSchema, groupOptionsSchema])])
export type GroupEntry = z.infer<typeof groupEntrySchema>
const groupEntryArraySchema = z.array(groupEntrySchema).refine(
(groups) => {
const seen = new Set()
return groups.every((group) => {
// For tuples, check the group name (first element).
const groupName = Array.isArray(group) ? group[0] : group
if (seen.has(groupName)) {
return false
}
seen.add(groupName)
return true
})
},
{ message: "Duplicate groups are not allowed" },
)
export const modeConfigSchema = z.object({
id: z.string().uuid("Invalid ID"),
slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
name: z.string().min(1, "Name is required"),
roleDefinition: z.string().min(1, "Role definition is required"),
customInstructions: z.string().optional(),
groups: groupEntryArraySchema,
source: z.enum(["global", "project"]).optional(),
updatedAt: z.number().int().positive(),
schemaVersion: z.literal(CUSTOM_MODE_SCHEMA_VERSION),
})
export type CustomMode = z.infer<typeof modeConfigSchema>
export type CustomModeMetadata = {
id: string
name: string
updatedAt: number
schemaVersion: number
}

View File

@ -5,6 +5,13 @@ export class DuplicateCommandException extends Error {
} }
} }
export class DuplicateCustomModeException extends Error {
constructor(customModeName: string) {
super(`Custom mode with name "${customModeName}" already exists`)
this.name = 'DuplicateCustomModeException'
}
}
export class EmptyCommandNameException extends Error { export class EmptyCommandNameException extends Error {
constructor() { constructor() {
super('Command name cannot be empty') super('Command name cannot be empty')
@ -18,3 +25,10 @@ export class EmptyChatTitleException extends Error {
this.name = 'EmptyChatTitleException' this.name = 'EmptyChatTitleException'
} }
} }
export class EmptyCustomModeNameException extends Error {
constructor() {
super('Custom mode name cannot be empty')
this.name = 'EmptyCustomModeNameException'
}
}

View File

@ -0,0 +1,95 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useApp } from '../contexts/AppContext'
import { CustomModeManager } from '../database/json/custom-mode/CustomModeManager'
import { CustomMode, GroupEntry } from '../database/json/custom-mode/types'
type UseCustomModes = {
createCustomMode: (
name: string,
roleDefinition: string,
customInstructions: string,
groups: GroupEntry[]
) => Promise<void>
deleteCustomMode: (id: string) => Promise<void>
updateCustomMode: (
id: string,
name: string,
roleDefinition: string,
customInstructions: string,
groups: GroupEntry[]
) => Promise<void>
FindCustomModeByName: (name: string) => Promise<CustomMode | undefined>
customModeList: CustomMode[]
}
export function useCustomModes(): UseCustomModes {
const [customModeList, setCustomModeList] = useState<CustomMode[]>([])
const app = useApp()
const customModeManager = useMemo(() => new CustomModeManager(app), [app])
const fetchCustomModeList = useCallback(async () => {
customModeManager.ListCustomModes().then((rows) => {
setCustomModeList(rows)
})
}, [customModeManager])
useEffect(() => {
void fetchCustomModeList()
}, [fetchCustomModeList])
const createCustomMode = useCallback(
async (
name: string,
roleDefinition: string,
customInstructions: string,
groups: GroupEntry[]
): Promise<void> => {
await customModeManager.createCustomMode({
name,
roleDefinition,
customInstructions,
groups,
})
fetchCustomModeList()
},
[customModeManager, fetchCustomModeList],
)
const deleteCustomMode = useCallback(
async (id: string): Promise<void> => {
await customModeManager.deleteCustomMode(id)
fetchCustomModeList()
},
[customModeManager, fetchCustomModeList],
)
const updateCustomMode = useCallback(
async (id: string, name: string, roleDefinition: string, customInstructions: string, groups: GroupEntry[]): Promise<void> => {
await customModeManager.updateCustomMode(id, {
name,
roleDefinition,
customInstructions,
groups,
})
fetchCustomModeList()
},
[customModeManager, fetchCustomModeList],
)
const FindCustomModeByName = useCallback(
async (name: string): Promise<CustomMode | undefined> => {
return customModeList.find((customMode) => customMode.name === name)
}, [customModeList])
return {
createCustomMode,
deleteCustomMode,
updateCustomMode,
FindCustomModeByName,
customModeList,
}
}

View File

@ -79,13 +79,13 @@ export function getToolsForMode(groups: readonly GroupEntry[]): string[] {
// Main modes configuration as an ordered array // Main modes configuration as an ordered array
export const modes: readonly ModeConfig[] = [ export const modes: readonly ModeConfig[] = [
{ {
slug: "research", slug: "ask",
name: "Research", name: "Ask",
roleDefinition: roleDefinition:
"You are Infio, an advanced research assistant specialized in comprehensive investigation and analytical thinking. You excel at breaking down complex questions, exploring multiple perspectives, and synthesizing information to provide well-reasoned conclusions.", "You are Infio, a versatile assistant dedicated to providing informative responses, thoughtful explanations, and practical guidance on virtually any topic or challenge you face.",
groups: ["research"], groups: ["read"],
customInstructions: customInstructions:
"You can conduct thorough research by analyzing available information, connecting related concepts, and applying structured reasoning methods. Help users explore topics in depth by considering multiple angles, identifying relevant evidence, and evaluating the reliability of sources. Use step-by-step analysis when tackling complex problems, explaining your thought process clearly. Create visual representations like Mermaid diagrams when they help clarify relationships between ideas. Use Markdown tables to present statistical data or comparative information when appropriate. Present balanced viewpoints while highlighting the strength of evidence behind different conclusions.", "You can analyze information, explain concepts across various domains, and access external resources when helpful. Make sure to address the user's questions thoroughly with thoughtful explanations and practical guidance. Use visual aids like Mermaid diagrams when they help make complex topics clearer. Offer solutions to challenges from diverse fields, not just technical ones, and provide context that helps users better understand the subject matter.",
}, },
{ {
slug: "write", slug: "write",
@ -97,13 +97,13 @@ export const modes: readonly ModeConfig[] = [
"You can create and modify any text-based files, with particular expertise in Markdown formatting. Help users organize their thoughts, create documentation, take notes, or draft any written content they need. When appropriate, suggest structural improvements and formatting enhancements that make content more readable and accessible. Consider the purpose and audience of each document to provide the most relevant assistance." "You can create and modify any text-based files, with particular expertise in Markdown formatting. Help users organize their thoughts, create documentation, take notes, or draft any written content they need. When appropriate, suggest structural improvements and formatting enhancements that make content more readable and accessible. Consider the purpose and audience of each document to provide the most relevant assistance."
}, },
{ {
slug: "ask", slug: "research",
name: "Ask", name: "Research",
roleDefinition: roleDefinition:
"You are Infio, a versatile assistant dedicated to providing informative responses, thoughtful explanations, and practical guidance on virtually any topic or challenge you face.", "You are Infio, an advanced research assistant specialized in comprehensive investigation and analytical thinking. You excel at breaking down complex questions, exploring multiple perspectives, and synthesizing information to provide well-reasoned conclusions.",
groups: ["read"], groups: ["research"],
customInstructions: customInstructions:
"You can analyze information, explain concepts across various domains, and access external resources when helpful. Make sure to address the user's questions thoroughly with thoughtful explanations and practical guidance. Use visual aids like Mermaid diagrams when they help make complex topics clearer. Offer solutions to challenges from diverse fields, not just technical ones, and provide context that helps users better understand the subject matter.", "You can conduct thorough research by analyzing available information, connecting related concepts, and applying structured reasoning methods. Help users explore topics in depth by considering multiple angles, identifying relevant evidence, and evaluating the reliability of sources. Use step-by-step analysis when tackling complex problems, explaining your thought process clearly. Create visual representations like Mermaid diagrams when they help clarify relationships between ideas. Use Markdown tables to present statistical data or comparative information when appropriate. Present balanced viewpoints while highlighting the strength of evidence behind different conclusions.",
}, },
] as const ] as const

View File

@ -3,12 +3,11 @@ import { App, MarkdownView, TAbstractFile, TFile, TFolder, Vault, getLanguage, h
import { editorStateToPlainText } from '../components/chat-view/chat-input/utils/editor-state-to-plain-text' import { editorStateToPlainText } from '../components/chat-view/chat-input/utils/editor-state-to-plain-text'
import { QueryProgressState } from '../components/chat-view/QueryProgress' import { QueryProgressState } from '../components/chat-view/QueryProgress'
import { DiffStrategy } from '../core/diff/DiffStrategy' import { DiffStrategy } from '../core/diff/DiffStrategy'
import { SYSTEM_PROMPT } from '../core/prompts/system' import { SystemPrompt } from '../core/prompts/system'
import { RAGEngine } from '../core/rag/rag-engine' import { RAGEngine } from '../core/rag/rag-engine'
import { SelectVector } from '../database/schema' import { SelectVector } from '../database/schema'
import { ChatMessage, ChatUserMessage } from '../types/chat' import { ChatMessage, ChatUserMessage } from '../types/chat'
import { ContentPart, RequestMessage } from '../types/llm/request' import { ContentPart, RequestMessage } from '../types/llm/request'
import { SystemPromptsManager } from '../core/prompts/system-prompts-manager'
import { import {
MentionableBlock, MentionableBlock,
MentionableFile, MentionableFile,
@ -116,7 +115,7 @@ export class PromptGenerator {
private app: App private app: App
private settings: InfioSettings private settings: InfioSettings
private diffStrategy: DiffStrategy private diffStrategy: DiffStrategy
private systemPromptsManager: SystemPromptsManager private systemPrompt: SystemPrompt
private static readonly EMPTY_ASSISTANT_MESSAGE: RequestMessage = { private static readonly EMPTY_ASSISTANT_MESSAGE: RequestMessage = {
role: 'assistant', role: 'assistant',
content: '', content: '',
@ -132,7 +131,7 @@ export class PromptGenerator {
this.app = app this.app = app
this.settings = settings this.settings = settings
this.diffStrategy = diffStrategy this.diffStrategy = diffStrategy
this.systemPromptsManager = new SystemPromptsManager(this.app) this.systemPrompt = new SystemPrompt(this.app)
} }
public async generateRequestMessages({ public async generateRequestMessages({
@ -468,7 +467,7 @@ export class PromptGenerator {
} }
private async getSystemMessageNew(mode: Mode, filesSearchMethod: string, preferredLanguage: string): Promise<RequestMessage> { private async getSystemMessageNew(mode: Mode, filesSearchMethod: string, preferredLanguage: string): Promise<RequestMessage> {
const systemPrompt = await this.systemPromptsManager.getSystemPrompt( const prompt = await this.systemPrompt.getSystemPrompt(
this.app.vault.getRoot().path, this.app.vault.getRoot().path,
false, false,
mode, mode,
@ -479,7 +478,7 @@ export class PromptGenerator {
return { return {
role: 'system', role: 'system',
content: systemPrompt, content: prompt,
} }
} }