mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-01-16 08:21:55 +00:00
update workspace
This commit is contained in:
parent
0df4e4edd3
commit
772270863c
178
README-dataview.md
Normal file
178
README-dataview.md
Normal file
@ -0,0 +1,178 @@
|
||||
# Dataview 集成使用指南
|
||||
|
||||
本插件已成功集成 Dataview 功能,让你可以在插件中执行 Dataview 查询。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. DataviewManager 类
|
||||
- 检查 Dataview 插件是否可用
|
||||
- 执行各种类型的 Dataview 查询(LIST、TABLE、TASK、CALENDAR)
|
||||
- 获取页面数据和任务信息
|
||||
- 搜索和过滤功能
|
||||
|
||||
### 2. DataviewQueryBuilder 类
|
||||
- 链式查询构建器
|
||||
- 支持复杂查询的构建
|
||||
- 类型安全的查询构建
|
||||
|
||||
### 3. 命令面板集成
|
||||
- 新增"执行 Dataview 查询"命令
|
||||
- 可通过命令面板快速访问
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 通过命令面板使用
|
||||
|
||||
1. 打开命令面板(Ctrl/Cmd + P)
|
||||
2. 输入"执行 Dataview 查询"
|
||||
3. 在弹出的对话框中输入你的查询
|
||||
4. 查询结果将保存到新的笔记中
|
||||
|
||||
### 编程方式使用
|
||||
|
||||
```typescript
|
||||
// 在插件代码中使用
|
||||
import { createDataviewManager } from './utils/dataview';
|
||||
|
||||
// 创建 DataviewManager 实例
|
||||
const dataviewManager = createDataviewManager(this.app);
|
||||
|
||||
// 检查 Dataview 是否可用
|
||||
if (dataviewManager.isDataviewAvailable()) {
|
||||
// 执行查询
|
||||
const result = await dataviewManager.executeQuery('LIST FROM #项目');
|
||||
|
||||
if (result.success) {
|
||||
console.log('查询结果:', result.data);
|
||||
} else {
|
||||
console.error('查询失败:', result.error);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用查询构建器
|
||||
const queryBuilder = dataviewManager.createQueryBuilder();
|
||||
const result = await queryBuilder
|
||||
.type('table')
|
||||
.select('file.name', 'file.mtime')
|
||||
.from('#项目')
|
||||
.where('file.mtime >= date(today) - dur(7 days)')
|
||||
.sort('file.mtime', 'DESC')
|
||||
.limit(10)
|
||||
.execute();
|
||||
```
|
||||
|
||||
## 常用查询示例
|
||||
|
||||
### 1. 列出所有笔记
|
||||
```dataview
|
||||
LIST FROM ""
|
||||
```
|
||||
|
||||
### 2. 今天创建的笔记
|
||||
```dataview
|
||||
LIST WHERE file.cday = date(today)
|
||||
```
|
||||
|
||||
### 3. 最近7天修改的笔记
|
||||
```dataview
|
||||
LIST WHERE file.mtime >= date(today) - dur(7 days) SORT file.mtime DESC
|
||||
```
|
||||
|
||||
### 4. 带有特定标签的笔记
|
||||
```dataview
|
||||
LIST FROM #项目
|
||||
```
|
||||
|
||||
### 5. 未完成的任务
|
||||
```dataview
|
||||
TASK WHERE !completed
|
||||
```
|
||||
|
||||
### 6. 今天到期的任务
|
||||
```dataview
|
||||
TASK WHERE due = date(today)
|
||||
```
|
||||
|
||||
### 7. 文件夹中的笔记表格
|
||||
```dataview
|
||||
TABLE file.name, file.mtime, file.size
|
||||
FROM "项目文件夹"
|
||||
SORT file.mtime DESC
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### DataviewManager
|
||||
|
||||
#### 方法
|
||||
|
||||
- `isDataviewAvailable(): boolean` - 检查 Dataview 插件是否可用
|
||||
- `executeQuery(query: string): Promise<DataviewQueryResult>` - 执行查询
|
||||
- `getPage(path: string): unknown` - 获取页面数据
|
||||
- `getPages(source?: string): unknown[]` - 获取所有页面
|
||||
- `searchPages(query: string): unknown[]` - 搜索页面
|
||||
- `getPagesByTag(tag: string): unknown[]` - 获取带有特定标签的页面
|
||||
- `getPagesByFolder(folder: string): unknown[]` - 获取文件夹中的页面
|
||||
- `getTasks(source?: string): unknown[]` - 获取任务
|
||||
- `getIncompleteTasks(source?: string): unknown[]` - 获取未完成的任务
|
||||
- `getCompletedTasks(source?: string): unknown[]` - 获取已完成的任务
|
||||
|
||||
### DataviewQueryBuilder
|
||||
|
||||
#### 方法
|
||||
|
||||
- `type(type: 'table' | 'list' | 'task' | 'calendar'): this` - 设置查询类型
|
||||
- `select(...fields: string[]): this` - 添加字段选择(用于 table 查询)
|
||||
- `from(source: string): this` - 添加数据源
|
||||
- `where(condition: string): this` - 添加 WHERE 条件
|
||||
- `sort(field: string, direction: 'ASC' | 'DESC'): this` - 添加排序
|
||||
- `limit(count: number): this` - 添加限制
|
||||
- `groupBy(field: string): this` - 添加分组
|
||||
- `build(): string` - 构建查询字符串
|
||||
- `execute(): Promise<DataviewQueryResult>` - 执行查询
|
||||
|
||||
### DataviewQueryResult
|
||||
|
||||
```typescript
|
||||
interface DataviewQueryResult {
|
||||
success: boolean;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保已安装并启用 Dataview 插件
|
||||
2. 查询语法遵循 Dataview 的标准语法
|
||||
3. 查询结果会自动保存到新的笔记中
|
||||
4. 支持所有 Dataview 的查询类型:LIST、TABLE、TASK、CALENDAR
|
||||
|
||||
## 故障排除
|
||||
|
||||
### Dataview 插件未安装或未启用
|
||||
- 确保在 Obsidian 中安装了 Dataview 插件
|
||||
- 确保 Dataview 插件已启用
|
||||
|
||||
### 查询语法错误
|
||||
- 检查查询语法是否符合 Dataview 标准
|
||||
- 参考 Dataview 官方文档:https://blacksmithgu.github.io/obsidian-dataview/
|
||||
|
||||
### 查询结果为空
|
||||
- 检查查询条件是否正确
|
||||
- 确保有符合条件的文件存在
|
||||
|
||||
## 扩展功能
|
||||
|
||||
你可以通过以下方式扩展 Dataview 集成:
|
||||
|
||||
1. 添加自定义查询模板
|
||||
2. 集成到聊天界面中
|
||||
3. 添加查询历史记录
|
||||
4. 实现查询结果的可视化展示
|
||||
|
||||
## 更多资源
|
||||
|
||||
- [Dataview 官方文档](https://blacksmithgu.github.io/obsidian-dataview/)
|
||||
- [Dataview 查询语法](https://blacksmithgu.github.io/obsidian-dataview/queries/structure/)
|
||||
- [Dataview API 文档](https://blacksmithgu.github.io/obsidian-dataview/api/intro/)
|
||||
@ -41,9 +41,12 @@
|
||||
"eslint-plugin-no-inline-styles": "^1.0.5",
|
||||
"eslint-plugin-react": "^7.37.3",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"install": "^0.13.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"npm": "^11.4.2",
|
||||
"obsidian": "^1.8.7",
|
||||
"obsidian-dataview": "^0.5.68",
|
||||
"prettier": "^3.4.2",
|
||||
"stylelint": "^16.12.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
|
||||
1161
pnpm-lock.yaml
generated
1161
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@ import { CHAT_VIEW_TYPE } from './constants'
|
||||
import { AppProvider } from './contexts/AppContext'
|
||||
import { DarkModeProvider } from './contexts/DarkModeContext'
|
||||
import { DatabaseProvider } from './contexts/DatabaseContext'
|
||||
import { DataviewProvider } from './contexts/DataviewContext'
|
||||
import { DialogProvider } from './contexts/DialogContext'
|
||||
import { DiffStrategyProvider } from './contexts/DiffStrategyContext'
|
||||
import { LLMProvider } from './contexts/LLMContext'
|
||||
@ -95,17 +96,19 @@ export class ChatView extends ItemView {
|
||||
>
|
||||
<DiffStrategyProvider diffStrategy={this.plugin.diffStrategy}>
|
||||
<RAGProvider getRAGEngine={() => this.plugin.getRAGEngine()}>
|
||||
<McpHubProvider getMcpHub={() => this.plugin.getMcpHub()}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<React.StrictMode>
|
||||
<DialogProvider
|
||||
container={containerElement}
|
||||
>
|
||||
<Chat ref={this.chatRef} {...this.initialChatProps} />
|
||||
</DialogProvider>
|
||||
</React.StrictMode>
|
||||
</QueryClientProvider>
|
||||
</McpHubProvider>
|
||||
<DataviewProvider dataviewManager={this.plugin.dataviewManager}>
|
||||
<McpHubProvider getMcpHub={() => this.plugin.getMcpHub()}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<React.StrictMode>
|
||||
<DialogProvider
|
||||
container={containerElement}
|
||||
>
|
||||
<Chat ref={this.chatRef} {...this.initialChatProps} />
|
||||
</DialogProvider>
|
||||
</React.StrictMode>
|
||||
</QueryClientProvider>
|
||||
</McpHubProvider>
|
||||
</DataviewProvider>
|
||||
</RAGProvider>
|
||||
</DiffStrategyProvider>
|
||||
</DatabaseProvider>
|
||||
|
||||
@ -2,7 +2,7 @@ import * as path from 'path'
|
||||
|
||||
import { BaseSerializedNode } from '@lexical/clipboard/clipboard'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { CircleStop, History, NotebookPen, Plus, Search, Server, SquareSlash, Undo } from 'lucide-react'
|
||||
import { Box, CircleStop, History, NotebookPen, Plus, Search, Server, SquareSlash, Undo } from 'lucide-react'
|
||||
import { App, Notice, TFile, WorkspaceLeaf } from 'obsidian'
|
||||
import {
|
||||
forwardRef,
|
||||
@ -18,6 +18,7 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { ApplyView, ApplyViewState } from '../../ApplyView'
|
||||
import { APPLY_VIEW_TYPE } from '../../constants'
|
||||
import { useApp } from '../../contexts/AppContext'
|
||||
import { useDataview } from '../../contexts/DataviewContext'
|
||||
import { useDiffStrategy } from '../../contexts/DiffStrategyContext'
|
||||
import { useLLM } from '../../contexts/LLMContext'
|
||||
import { useMcpHub } from '../../contexts/McpHubContext'
|
||||
@ -33,6 +34,7 @@ import {
|
||||
LLMBaseUrlNotSetException,
|
||||
LLMModelNotSetException,
|
||||
} from '../../core/llm/exception'
|
||||
import { TransformationType, runTransformation } from '../../core/transformations/run_trans'
|
||||
import { useChatHistory } from '../../hooks/use-chat-history'
|
||||
import { useCustomModes } from '../../hooks/use-custom-mode'
|
||||
import { t } from '../../lang/helpers'
|
||||
@ -73,6 +75,8 @@ import SearchView from './SearchView'
|
||||
import SimilaritySearchResults from './SimilaritySearchResults'
|
||||
import UserMessageView from './UserMessageView'
|
||||
import WebsiteReadResults from './WebsiteReadResults'
|
||||
import WorkspaceSelect from './WorkspaceSelect'
|
||||
import WorkspaceView from './WorkspaceView'
|
||||
|
||||
// Add an empty line here
|
||||
const getNewInputMessage = (app: App, defaultMention: string): ChatUserMessage => {
|
||||
@ -115,6 +119,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
const { settings, setSettings } = useSettings()
|
||||
const { getRAGEngine } = useRAG()
|
||||
const diffStrategy = useDiffStrategy()
|
||||
const dataviewManager = useDataview()
|
||||
const { getMcpHub } = useMcpHub()
|
||||
const { customModeList, customModePrompts } = useCustomModes()
|
||||
|
||||
@ -179,7 +184,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
}
|
||||
}
|
||||
|
||||
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp' | 'search' | 'history'>('chat')
|
||||
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp' | 'search' | 'history' | 'workspace'>('chat')
|
||||
|
||||
const [selectedSerializedNodes, setSelectedSerializedNodes] = useState<BaseSerializedNode[]>([])
|
||||
|
||||
@ -792,6 +797,124 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
mentionables: [],
|
||||
}
|
||||
}
|
||||
} else if (toolArgs.type === 'dataview_query') {
|
||||
if (!dataviewManager) {
|
||||
throw new Error('DataviewManager 未初始化')
|
||||
}
|
||||
|
||||
if (!dataviewManager.isDataviewAvailable()) {
|
||||
throw new Error('Dataview 插件未安装或未启用,请先安装并启用 Dataview 插件')
|
||||
}
|
||||
|
||||
// 执行 Dataview 查询
|
||||
const result = await dataviewManager.executeQuery(toolArgs.query)
|
||||
|
||||
let formattedContent: string;
|
||||
if (result.success) {
|
||||
formattedContent = `[dataview_query] 查询成功:\n${result.data}`;
|
||||
} else {
|
||||
formattedContent = `[dataview_query] 查询失败:\n${result.error}`;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'dataview_query',
|
||||
applyMsgId,
|
||||
applyStatus: result.success ? ApplyStatus.Applied : ApplyStatus.Failed,
|
||||
returnMsg: {
|
||||
role: 'user',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
content: null,
|
||||
promptContent: formattedContent,
|
||||
id: uuidv4(),
|
||||
mentionables: [],
|
||||
}
|
||||
}
|
||||
} else if (toolArgs.type === 'analyze_paper' ||
|
||||
toolArgs.type === 'key_insights' ||
|
||||
toolArgs.type === 'dense_summary' ||
|
||||
toolArgs.type === 'reflections' ||
|
||||
toolArgs.type === 'table_of_contents' ||
|
||||
toolArgs.type === 'simple_summary') {
|
||||
// 处理文档转换工具
|
||||
console.log('toolArgs', toolArgs)
|
||||
|
||||
try {
|
||||
// 获取文件
|
||||
const targetFile = app.vault.getFileByPath(toolArgs.path)
|
||||
if (!targetFile) {
|
||||
throw new Error(`文件未找到: ${toolArgs.path}`)
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
const fileContent = await readTFileContentPdf(targetFile, app.vault, app)
|
||||
|
||||
// 映射工具类型到转换类型
|
||||
const transformationTypeMap: Record<string, TransformationType> = {
|
||||
'analyze_paper': TransformationType.ANALYZE_PAPER,
|
||||
'key_insights': TransformationType.KEY_INSIGHTS,
|
||||
'dense_summary': TransformationType.DENSE_SUMMARY,
|
||||
'reflections': TransformationType.REFLECTIONS,
|
||||
'table_of_contents': TransformationType.TABLE_OF_CONTENTS,
|
||||
'simple_summary': TransformationType.SIMPLE_SUMMARY
|
||||
}
|
||||
|
||||
const transformationType = transformationTypeMap[toolArgs.type]
|
||||
if (!transformationType) {
|
||||
throw new Error(`不支持的转换类型: ${toolArgs.type}`)
|
||||
}
|
||||
|
||||
// 执行转换
|
||||
const transformationResult = await runTransformation({
|
||||
content: fileContent,
|
||||
transformationType,
|
||||
settings,
|
||||
model: {
|
||||
provider: settings.applyModelProvider,
|
||||
modelId: settings.applyModelId,
|
||||
}
|
||||
})
|
||||
|
||||
if (!transformationResult.success) {
|
||||
throw new Error(transformationResult.error || '转换失败')
|
||||
}
|
||||
|
||||
// 构建结果消息
|
||||
let formattedContent = `[${toolArgs.type}] 转换完成:\n\n${transformationResult.result}`
|
||||
|
||||
// 如果内容被截断,添加提示
|
||||
if (transformationResult.truncated) {
|
||||
formattedContent += `\n\n*注意: 原始内容过长(${transformationResult.originalTokens} tokens),已截断为${transformationResult.processedTokens} tokens进行处理*`
|
||||
}
|
||||
|
||||
return {
|
||||
type: toolArgs.type,
|
||||
applyMsgId,
|
||||
applyStatus: ApplyStatus.Applied,
|
||||
returnMsg: {
|
||||
role: 'user',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
content: null,
|
||||
promptContent: formattedContent,
|
||||
id: uuidv4(),
|
||||
mentionables: [],
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`转换失败 (${toolArgs.type}):`, error)
|
||||
return {
|
||||
type: toolArgs.type,
|
||||
applyMsgId,
|
||||
applyStatus: ApplyStatus.Failed,
|
||||
returnMsg: {
|
||||
role: 'user',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
content: null,
|
||||
promptContent: `[${toolArgs.type}] 转换失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
id: uuidv4(),
|
||||
mentionables: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to apply changes', error)
|
||||
@ -1006,7 +1129,9 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
<div className="infio-chat-container">
|
||||
{/* header view */}
|
||||
<div className="infio-chat-header">
|
||||
INFIO
|
||||
<div className="infio-chat-header-title">
|
||||
{t('workspace.shortTitle')}: <WorkspaceSelect />
|
||||
</div>
|
||||
<div className="infio-chat-header-buttons">
|
||||
<button
|
||||
onClick={() => {
|
||||
@ -1029,6 +1154,18 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
>
|
||||
<History size={18} color={tab === 'history' ? 'var(--text-accent)' : 'var(--text-color)'} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (tab === 'workspace') {
|
||||
setTab('chat')
|
||||
} else {
|
||||
setTab('workspace')
|
||||
}
|
||||
}}
|
||||
className="infio-chat-list-dropdown"
|
||||
>
|
||||
<Box size={18} color={tab === 'workspace' ? 'var(--text-accent)' : 'var(--text-color)'} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (tab === 'search') {
|
||||
@ -1275,6 +1412,10 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : tab === 'workspace' ? (
|
||||
<div className="infio-chat-commands">
|
||||
<WorkspaceView />
|
||||
</div>
|
||||
) : (
|
||||
<div className="infio-chat-commands">
|
||||
<McpHubView />
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
import { Check, ChevronDown, ChevronRight, Database, Loader2, X } from 'lucide-react'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { t } from '../../../lang/helpers'
|
||||
import { ApplyStatus, DataviewQueryToolArgs } from "../../../types/apply"
|
||||
|
||||
export default function MarkdownDataviewQueryBlock({
|
||||
applyStatus,
|
||||
onApply,
|
||||
query,
|
||||
outputFormat,
|
||||
finish
|
||||
}: {
|
||||
applyStatus: ApplyStatus
|
||||
onApply: (args: DataviewQueryToolArgs) => void
|
||||
query: string
|
||||
outputFormat: string
|
||||
finish: boolean
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (finish && applyStatus === ApplyStatus.Idle) {
|
||||
onApply({
|
||||
type: 'dataview_query',
|
||||
query: query,
|
||||
outputFormat: outputFormat,
|
||||
})
|
||||
}
|
||||
}, [finish])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`infio-chat-code-block has-filename`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div className={'infio-chat-code-block-header'}>
|
||||
<div
|
||||
className={'infio-chat-code-block-header-filename'}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
style={{ cursor: isHovered ? 'pointer' : 'default' }}
|
||||
>
|
||||
{isHovered ? (
|
||||
isOpen ? <ChevronDown size={14} className="infio-chat-code-block-header-icon" /> : <ChevronRight size={14} className="infio-chat-code-block-header-icon" />
|
||||
) : (
|
||||
<Database size={14} className="infio-chat-code-block-header-icon" />
|
||||
)}
|
||||
Dataview 查询 ({outputFormat})
|
||||
</div>
|
||||
<div className={'infio-chat-code-block-header-button'}>
|
||||
<button
|
||||
className="infio-dataview-query-button"
|
||||
disabled={true}
|
||||
>
|
||||
{
|
||||
!finish || applyStatus === ApplyStatus.Idle ? (
|
||||
<>
|
||||
<Loader2 className="spinner" size={14} /> 执行中...
|
||||
</>
|
||||
) : applyStatus === ApplyStatus.Applied ? (
|
||||
<>
|
||||
<Check size={14} /> 完成
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X size={14} /> 失败
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className={'infio-chat-code-block-content'}>
|
||||
<pre>
|
||||
<code>{query}</code>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { ChevronDown, ChevronRight, Brain } from 'lucide-react'
|
||||
import { Brain, ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { PropsWithChildren, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { useDarkModeContext } from "../../../contexts/DarkModeContext"
|
||||
|
||||
@ -0,0 +1,132 @@
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
import { useApp } from "../../../contexts/AppContext"
|
||||
import { ApplyStatus, ToolArgs } from "../../../types/apply"
|
||||
import { openMarkdownFile } from "../../../utils/obsidian"
|
||||
|
||||
export type TransformationToolType = 'analyze_paper' | 'key_insights' | 'dense_summary' | 'reflections' | 'table_of_contents' | 'simple_summary'
|
||||
|
||||
interface MarkdownTransformationToolBlockProps {
|
||||
applyStatus: ApplyStatus
|
||||
onApply: (args: ToolArgs) => void
|
||||
toolType: TransformationToolType
|
||||
path: string
|
||||
depth?: number
|
||||
format?: string
|
||||
include_summary?: boolean
|
||||
finish: boolean
|
||||
}
|
||||
|
||||
const getToolConfig = (toolType: TransformationToolType) => {
|
||||
switch (toolType) {
|
||||
case 'analyze_paper':
|
||||
return {
|
||||
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
|
||||
title: 'Analyze Paper',
|
||||
description: 'Deep analysis of academic papers'
|
||||
}
|
||||
case 'key_insights':
|
||||
return {
|
||||
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
|
||||
title: 'Key Insights',
|
||||
description: 'Extract key insights'
|
||||
}
|
||||
case 'dense_summary':
|
||||
return {
|
||||
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
|
||||
title: 'Dense Summary',
|
||||
description: 'Create information-dense summary'
|
||||
}
|
||||
case 'reflections':
|
||||
return {
|
||||
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
|
||||
title: 'Deep Reflections',
|
||||
description: 'Generate deep reflections'
|
||||
}
|
||||
case 'table_of_contents':
|
||||
return {
|
||||
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
|
||||
title: 'Table of Contents',
|
||||
description: 'Generate table of contents structure'
|
||||
}
|
||||
case 'simple_summary':
|
||||
return {
|
||||
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
|
||||
title: 'Simple Summary',
|
||||
description: 'Create readable summary'
|
||||
}
|
||||
default:
|
||||
return {
|
||||
icon: <Sparkles size={14} className="infio-chat-code-block-header-icon" />,
|
||||
title: 'Document Processing',
|
||||
description: 'Process document'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function MarkdownTransformationToolBlock({
|
||||
applyStatus,
|
||||
onApply,
|
||||
toolType,
|
||||
path,
|
||||
depth,
|
||||
format,
|
||||
include_summary,
|
||||
finish
|
||||
}: MarkdownTransformationToolBlockProps) {
|
||||
const app = useApp()
|
||||
const config = getToolConfig(toolType)
|
||||
|
||||
const handleClick = () => {
|
||||
if (path) {
|
||||
openMarkdownFile(app, path)
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (finish && applyStatus === ApplyStatus.Idle) {
|
||||
// 构建符合标准ToolArgs类型的参数
|
||||
if (toolType === 'table_of_contents') {
|
||||
onApply({
|
||||
type: toolType,
|
||||
path: path || '',
|
||||
depth,
|
||||
format,
|
||||
include_summary
|
||||
})
|
||||
} else {
|
||||
onApply({
|
||||
type: toolType,
|
||||
path: path || '',
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [finish])
|
||||
|
||||
const getDisplayText = () => {
|
||||
if (toolType === 'table_of_contents') {
|
||||
let text = `${config.title}: ${path || '未指定路径'}`
|
||||
if (depth) text += ` (深度: ${depth})`
|
||||
if (format) text += ` (格式: ${format})`
|
||||
if (include_summary) text += ` (包含摘要)`
|
||||
return text
|
||||
}
|
||||
return `${config.title}: ${path || '未指定路径'}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`infio-chat-code-block ${path ? 'has-filename' : ''}`}
|
||||
onClick={handleClick}
|
||||
style={{ cursor: path ? 'pointer' : 'default' }}
|
||||
>
|
||||
<div className={'infio-chat-code-block-header'}>
|
||||
<div className={'infio-chat-code-block-header-filename'}>
|
||||
{config.icon}
|
||||
<span>{getDisplayText()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -7,6 +7,7 @@ import {
|
||||
} from '../../utils/parse-infio-block'
|
||||
|
||||
import MarkdownApplyDiffBlock from './Markdown/MarkdownApplyDiffBlock'
|
||||
import MarkdownDataviewQueryBlock from './Markdown/MarkdownDataviewQueryBlock'
|
||||
import MarkdownEditFileBlock from './Markdown/MarkdownEditFileBlock'
|
||||
import MarkdownFetchUrlsContentBlock from './Markdown/MarkdownFetchUrlsContentBlock'
|
||||
import MarkdownListFilesBlock from './Markdown/MarkdownListFilesBlock'
|
||||
@ -19,6 +20,7 @@ import MarkdownSearchWebBlock from './Markdown/MarkdownSearchWebBlock'
|
||||
import MarkdownSemanticSearchFilesBlock from './Markdown/MarkdownSemanticSearchFilesBlock'
|
||||
import MarkdownSwitchModeBlock from './Markdown/MarkdownSwitchModeBlock'
|
||||
import MarkdownToolResult from './Markdown/MarkdownToolResult'
|
||||
import MarkdownTransformationToolBlock from './Markdown/MarkdownTransformationToolBlock'
|
||||
import MarkdownWithIcons from './Markdown/MarkdownWithIcon'
|
||||
import RawMarkdownBlock from './Markdown/RawMarkdownBlock'
|
||||
import UseMcpToolBlock from './Markdown/UseMcpToolBlock'
|
||||
@ -202,6 +204,72 @@ function ReactMarkdown({
|
||||
parameters={block.parameters}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'dataview_query' ? (
|
||||
<MarkdownDataviewQueryBlock
|
||||
key={"dataview-query-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
query={block.query}
|
||||
outputFormat={block.outputFormat}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'analyze_paper' ? (
|
||||
<MarkdownTransformationToolBlock
|
||||
key={"analyze-paper-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
toolType="analyze_paper"
|
||||
path={block.path}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'key_insights' ? (
|
||||
<MarkdownTransformationToolBlock
|
||||
key={"key-insights-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
toolType="key_insights"
|
||||
path={block.path}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'dense_summary' ? (
|
||||
<MarkdownTransformationToolBlock
|
||||
key={"dense-summary-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
toolType="dense_summary"
|
||||
path={block.path}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'reflections' ? (
|
||||
<MarkdownTransformationToolBlock
|
||||
key={"reflections-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
toolType="reflections"
|
||||
path={block.path}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'table_of_contents' ? (
|
||||
<MarkdownTransformationToolBlock
|
||||
key={"table-of-contents-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
toolType="table_of_contents"
|
||||
path={block.path}
|
||||
depth={block.depth}
|
||||
format={block.format}
|
||||
include_summary={block.include_summary}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'simple_summary' ? (
|
||||
<MarkdownTransformationToolBlock
|
||||
key={"simple-summary-" + index}
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
toolType="simple_summary"
|
||||
path={block.path}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'tool_result' ? (
|
||||
<MarkdownToolResult
|
||||
key={"tool-result-" + index}
|
||||
|
||||
893
src/components/chat-view/WorkspaceEditModal.tsx
Normal file
893
src/components/chat-view/WorkspaceEditModal.tsx
Normal file
@ -0,0 +1,893 @@
|
||||
import { ChevronDown, FolderOpen, Plus, Tag, Trash2, X } from 'lucide-react'
|
||||
import { App, TFile, TFolder } from 'obsidian'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { Workspace, WorkspaceContent } from '../../database/json/workspace/types'
|
||||
import { t } from '../../lang/helpers'
|
||||
import { createDataviewManager } from '../../utils/dataview'
|
||||
|
||||
interface WorkspaceEditModalProps {
|
||||
workspace?: Workspace
|
||||
app: App
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSave: (updatedWorkspace: Partial<Workspace>) => Promise<void>
|
||||
}
|
||||
|
||||
const WorkspaceEditModal = ({
|
||||
workspace,
|
||||
app,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave
|
||||
}: WorkspaceEditModalProps) => {
|
||||
// 生成默认工作区名称
|
||||
const getDefaultWorkspaceName = () => {
|
||||
const now = new Date()
|
||||
const date = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`
|
||||
return t('workspace.editModal.defaultName', { date })
|
||||
}
|
||||
|
||||
const [name, setName] = useState(workspace?.name || getDefaultWorkspaceName())
|
||||
const [content, setContent] = useState<WorkspaceContent[]>(workspace?.content ? [...workspace.content] : [])
|
||||
const [availableFolders, setAvailableFolders] = useState<string[]>([])
|
||||
const [availableTags, setAvailableTags] = useState<string[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// 智能添加相关状态
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [showSuggestions, setShowSuggestions] = useState(false)
|
||||
const [filteredSuggestions, setFilteredSuggestions] = useState<{type: 'folder' | 'tag', value: string}[]>([])
|
||||
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const suggestionsRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 获取可用的文件夹和标签
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
|
||||
const loadAvailableOptions = async () => {
|
||||
// 获取所有文件夹
|
||||
const folders: string[] = []
|
||||
const addFolder = (folder: TFolder) => {
|
||||
folders.push(folder.path)
|
||||
// folder.children.forEach(child => {
|
||||
// if (child instanceof TFolder) {
|
||||
// addFolder(child)
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
app.vault.getAllFolders(false).forEach(folder => {
|
||||
addFolder(folder)
|
||||
})
|
||||
|
||||
setAvailableFolders(folders.sort())
|
||||
|
||||
// 使用 dataview 查询获取所有标签
|
||||
const dataviewManager = createDataviewManager(app)
|
||||
|
||||
if (dataviewManager.isDataviewAvailable()) {
|
||||
try {
|
||||
const result = await dataviewManager.executeQuery('TABLE file.tags FROM ""')
|
||||
|
||||
if (result.success && result.data) {
|
||||
const tags = new Set<string>()
|
||||
|
||||
// 解析结果中的标签
|
||||
const lines = result.data.split('\n')
|
||||
lines.forEach(line => {
|
||||
if (line.includes('#')) {
|
||||
const tagMatches = line.match(/#[a-zA-Z0-9\u4e00-\u9fa5_-]+/g)
|
||||
if (tagMatches) {
|
||||
tagMatches.forEach(tag => tags.add(tag))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setAvailableTags(Array.from(tags).sort())
|
||||
} else {
|
||||
// 回退到传统方法
|
||||
fallbackToTraditionalTagQuery()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Dataview 查询失败:', error)
|
||||
// 回退到传统方法
|
||||
fallbackToTraditionalTagQuery()
|
||||
}
|
||||
} else {
|
||||
// 回退到传统方法
|
||||
fallbackToTraditionalTagQuery()
|
||||
}
|
||||
}
|
||||
|
||||
// 传统方法获取标签(作为回退方案)
|
||||
const fallbackToTraditionalTagQuery = () => {
|
||||
const tags = new Set<string>()
|
||||
app.vault.getAllLoadedFiles().forEach(file => {
|
||||
if (file instanceof TFile) {
|
||||
const cache = app.metadataCache.getFileCache(file)
|
||||
if (cache?.tags) {
|
||||
cache.tags.forEach(tag => {
|
||||
tags.add(tag.tag)
|
||||
})
|
||||
}
|
||||
if (cache?.frontmatter?.tags) {
|
||||
const frontmatterTags = cache.frontmatter.tags
|
||||
if (Array.isArray(frontmatterTags)) {
|
||||
frontmatterTags.forEach(tag => tags.add(`#${tag}`))
|
||||
} else if (typeof frontmatterTags === 'string') {
|
||||
tags.add(`#${frontmatterTags}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setAvailableTags(Array.from(tags).sort())
|
||||
}
|
||||
|
||||
loadAvailableOptions()
|
||||
}, [isOpen, app])
|
||||
|
||||
// 重置表单
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setName(workspace?.name || getDefaultWorkspaceName())
|
||||
setContent(workspace?.content ? [...workspace.content] : [])
|
||||
}
|
||||
}, [isOpen, workspace])
|
||||
|
||||
// 更新建议列表
|
||||
useEffect(() => {
|
||||
if (!inputValue.trim()) {
|
||||
setFilteredSuggestions([])
|
||||
setShowSuggestions(false)
|
||||
return
|
||||
}
|
||||
|
||||
const suggestions: {type: 'folder' | 'tag', value: string}[] = []
|
||||
const searchTerm = inputValue.toLowerCase()
|
||||
|
||||
// 搜索匹配的文件夹
|
||||
availableFolders.forEach(folder => {
|
||||
if (folder.toLowerCase().includes(searchTerm)) {
|
||||
// 检查是否已存在
|
||||
const exists = content.some(item =>
|
||||
item.type === 'folder' && item.content === folder
|
||||
)
|
||||
if (!exists) {
|
||||
suggestions.push({ type: 'folder', value: folder })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 搜索匹配的标签
|
||||
availableTags.forEach(tag => {
|
||||
if (tag.toLowerCase().includes(searchTerm)) {
|
||||
// 检查是否已存在
|
||||
const exists = content.some(item =>
|
||||
item.type === 'tag' && item.content === tag
|
||||
)
|
||||
if (!exists) {
|
||||
suggestions.push({ type: 'tag', value: tag })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 如果输入以#开头,优先显示标签建议
|
||||
if (inputValue.startsWith('#')) {
|
||||
suggestions.sort((a, b) => {
|
||||
if (a.type === 'tag' && b.type !== 'tag') return -1
|
||||
if (a.type !== 'tag' && b.type === 'tag') return 1
|
||||
return 0
|
||||
})
|
||||
} else {
|
||||
// 否则优先显示文件夹建议
|
||||
suggestions.sort((a, b) => {
|
||||
if (a.type === 'folder' && b.type !== 'folder') return -1
|
||||
if (a.type !== 'folder' && b.type === 'folder') return 1
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
setFilteredSuggestions(suggestions.slice(0, 10)) // 限制显示数量
|
||||
setShowSuggestions(suggestions.length > 0)
|
||||
setSelectedSuggestionIndex(-1)
|
||||
}, [inputValue, availableFolders, availableTags, content])
|
||||
|
||||
// 添加内容项
|
||||
const addContentItem = (type: 'folder' | 'tag', contentValue: string) => {
|
||||
if (!contentValue.trim()) return
|
||||
|
||||
// 检查是否已存在
|
||||
const exists = content.some(item =>
|
||||
item.type === type && item.content === contentValue
|
||||
)
|
||||
|
||||
if (exists) return
|
||||
|
||||
const newItem: WorkspaceContent = {
|
||||
type,
|
||||
content: contentValue
|
||||
}
|
||||
|
||||
setContent([...content, newItem])
|
||||
|
||||
// 清空输入框和建议
|
||||
setInputValue('')
|
||||
setShowSuggestions(false)
|
||||
setSelectedSuggestionIndex(-1)
|
||||
}
|
||||
|
||||
// 处理建议选择
|
||||
const handleSuggestionSelect = (suggestion: {type: 'folder' | 'tag', value: string}) => {
|
||||
addContentItem(suggestion.type, suggestion.value)
|
||||
}
|
||||
|
||||
// 处理手动添加
|
||||
const handleManualAdd = () => {
|
||||
const value = inputValue.trim()
|
||||
if (!value) return
|
||||
|
||||
// 自动判断类型
|
||||
const type = value.startsWith('#') ? 'tag' : 'folder'
|
||||
addContentItem(type, value)
|
||||
}
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (!showSuggestions) {
|
||||
if (e.key === 'Enter') {
|
||||
handleManualAdd()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
setSelectedSuggestionIndex(prev =>
|
||||
prev < filteredSuggestions.length - 1 ? prev + 1 : prev
|
||||
)
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
setSelectedSuggestionIndex(prev => prev > 0 ? prev - 1 : -1)
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
if (selectedSuggestionIndex >= 0 && selectedSuggestionIndex < filteredSuggestions.length) {
|
||||
handleSuggestionSelect(filteredSuggestions[selectedSuggestionIndex])
|
||||
} else {
|
||||
handleManualAdd()
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
setShowSuggestions(false)
|
||||
setSelectedSuggestionIndex(-1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部关闭建议
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target
|
||||
if (
|
||||
target instanceof Node &&
|
||||
inputRef.current &&
|
||||
!inputRef.current.contains(target) &&
|
||||
suggestionsRef.current &&
|
||||
!suggestionsRef.current.contains(target)
|
||||
) {
|
||||
setShowSuggestions(false)
|
||||
setSelectedSuggestionIndex(-1)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
// 删除内容项
|
||||
const removeContentItem = (index: number) => {
|
||||
setContent(content.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
// 保存更改
|
||||
const handleSave = async () => {
|
||||
if (!name.trim()) {
|
||||
alert(t('workspace.editModal.nameRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
await onSave({
|
||||
name: name.trim(),
|
||||
content
|
||||
})
|
||||
onClose()
|
||||
} catch (error) {
|
||||
console.error('保存工作区失败:', error)
|
||||
alert(t('workspace.editModal.saveFailed'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="workspace-edit-modal-overlay">
|
||||
<div className="workspace-edit-modal">
|
||||
{/* 头部 */}
|
||||
<div className="workspace-edit-modal-header">
|
||||
<h3>{workspace ? t('workspace.editModal.editTitle') : t('workspace.editModal.createTitle')}</h3>
|
||||
<button
|
||||
className="workspace-edit-modal-close"
|
||||
onClick={onClose}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
<div className="workspace-edit-modal-content">
|
||||
{/* 工作区名称 */}
|
||||
<div className="workspace-edit-section">
|
||||
<label className="workspace-edit-label">{t('workspace.editModal.nameLabel')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="workspace-edit-input"
|
||||
placeholder={workspace ? t('workspace.editModal.namePlaceholder') : t('workspace.editModal.newNamePlaceholder')}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 工作区内容 */}
|
||||
<div className="workspace-edit-section">
|
||||
<label className="workspace-edit-label">{t('workspace.editModal.contentLabel')}</label>
|
||||
|
||||
{/* 当前内容列表 */}
|
||||
<div className="workspace-content-list">
|
||||
{content.map((item, index) => (
|
||||
<div key={index} className="workspace-content-item">
|
||||
<div className="workspace-content-item-info">
|
||||
{item.type === 'folder' ? (
|
||||
<FolderOpen size={16} />
|
||||
) : (
|
||||
<Tag size={16} />
|
||||
)}
|
||||
<span className="workspace-content-item-text">
|
||||
{item.content}
|
||||
</span>
|
||||
<span className="workspace-content-item-type">
|
||||
({item.type === 'folder' ? t('workspace.editModal.folder') : t('workspace.editModal.tag')})
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="workspace-content-item-remove"
|
||||
onClick={() => removeContentItem(index)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{content.length === 0 && (
|
||||
<div className="workspace-content-empty">
|
||||
{t('workspace.editModal.noContent')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 智能添加 - 作为内容列表的一部分 */}
|
||||
<div className="workspace-smart-add-item">
|
||||
<div className="workspace-smart-add-container">
|
||||
<div className={`workspace-smart-input-wrapper ${showSuggestions ? 'has-suggestions' : ''}`}>
|
||||
<Plus size={16} className="workspace-smart-add-icon" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => {
|
||||
if (filteredSuggestions.length > 0) {
|
||||
setShowSuggestions(true)
|
||||
}
|
||||
}}
|
||||
placeholder={t('workspace.editModal.addPlaceholder')}
|
||||
className="workspace-smart-input"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{showSuggestions && (
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className="workspace-smart-dropdown-icon workspace-smart-dropdown-icon-up"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 建议下拉列表 */}
|
||||
{showSuggestions && filteredSuggestions.length > 0 && (
|
||||
<div ref={suggestionsRef} className="workspace-suggestions">
|
||||
{filteredSuggestions.map((suggestion, index) => (
|
||||
<div
|
||||
key={`${suggestion.type}-${suggestion.value}`}
|
||||
className={`workspace-suggestion-item ${
|
||||
index === selectedSuggestionIndex ? 'selected' : ''
|
||||
}`}
|
||||
onClick={() => handleSuggestionSelect(suggestion)}
|
||||
onMouseEnter={() => setSelectedSuggestionIndex(index)}
|
||||
>
|
||||
<div className="workspace-suggestion-content">
|
||||
{suggestion.type === 'folder' ? (
|
||||
<FolderOpen size={14} />
|
||||
) : (
|
||||
<Tag size={14} />
|
||||
)}
|
||||
<span className="workspace-suggestion-text">
|
||||
{suggestion.value}
|
||||
</span>
|
||||
<span className="workspace-suggestion-type">
|
||||
{suggestion.type === 'folder' ? t('workspace.editModal.folder') : t('workspace.editModal.tag')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="workspace-smart-add-tip">
|
||||
{t('workspace.editModal.tip')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<div className="workspace-edit-modal-footer">
|
||||
<button
|
||||
className="workspace-edit-btn workspace-edit-btn-cancel"
|
||||
onClick={onClose}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t('workspace.editModal.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="workspace-edit-btn workspace-edit-btn-save"
|
||||
onClick={handleSave}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading
|
||||
? (workspace ? t('workspace.editModal.saving') : t('workspace.editModal.creating'))
|
||||
: (workspace ? t('workspace.editModal.save') : t('workspace.editModal.create'))
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 样式 */}
|
||||
<style>
|
||||
{`
|
||||
.workspace-edit-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.workspace-edit-modal {
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.workspace-edit-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.workspace-edit-modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workspace-edit-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.workspace-edit-modal-close:hover:not(:disabled) {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.workspace-edit-modal-close:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.workspace-edit-modal-content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.workspace-edit-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.workspace-edit-label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.workspace-edit-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.workspace-edit-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.workspace-edit-input:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.workspace-content-list {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
margin-bottom: 2px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.workspace-content-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.workspace-content-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.workspace-content-list::-webkit-scrollbar-thumb {
|
||||
background-color: var(--background-modifier-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.workspace-content-list::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
.workspace-content-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.workspace-content-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.workspace-content-item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.workspace-content-item-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.workspace-content-item-type {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.workspace-content-item-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--text-error);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.workspace-content-item-remove:hover:not(:disabled) {
|
||||
background-color: var(--background-modifier-error);
|
||||
}
|
||||
|
||||
.workspace-content-item-remove:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.workspace-content-empty {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.workspace-smart-add-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.workspace-smart-add-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.workspace-smart-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--background-primary);
|
||||
padding: 8px 12px;
|
||||
gap: 8px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.workspace-smart-input-wrapper:hover {
|
||||
border-color: var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
.workspace-smart-input-wrapper:focus-within {
|
||||
border-color: var(--text-accent);
|
||||
box-shadow: 0 0 0 2px rgba(var(--text-accent-rgb), 0.1);
|
||||
}
|
||||
|
||||
.workspace-smart-input-wrapper.has-suggestions {
|
||||
border-radius: 0 0 var(--radius-s) var(--radius-s);
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
.workspace-smart-input-wrapper.has-suggestions:focus-within {
|
||||
border-radius: 0 0 var(--radius-s) var(--radius-s);
|
||||
border-top-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.workspace-smart-add-container:focus-within .workspace-suggestions {
|
||||
border-color: var(--text-accent);
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(var(--text-accent-rgb), 0.1);
|
||||
}
|
||||
|
||||
.workspace-smart-input {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-normal);
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.workspace-smart-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.workspace-smart-input:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.workspace-smart-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.workspace-smart-add-icon {
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.workspace-smart-dropdown-icon {
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.workspace-smart-dropdown-icon-up {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.workspace-suggestions {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-bottom: none;
|
||||
border-radius: var(--radius-s) var(--radius-s) 0 0;
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.workspace-suggestions::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.workspace-suggestions::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.workspace-suggestions::-webkit-scrollbar-thumb {
|
||||
background-color: var(--background-modifier-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.workspace-suggestions::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
.workspace-suggestion-item {
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.workspace-suggestion-item:first-child {
|
||||
border-radius: var(--radius-s) var(--radius-s) 0 0;
|
||||
}
|
||||
|
||||
.workspace-suggestion-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.workspace-suggestion-item:hover,
|
||||
.workspace-suggestion-item.selected {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.workspace-suggestion-item.selected {
|
||||
background-color: var(--background-modifier-active-hover);
|
||||
}
|
||||
|
||||
.workspace-suggestion-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.workspace-suggestion-text {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
color: var(--text-normal);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workspace-suggestion-type {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
background-color: var(--background-secondary);
|
||||
padding: 2px 6px;
|
||||
border-radius: calc(var(--radius-s) - 1px);
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.workspace-suggestion-item:hover .workspace-suggestion-type,
|
||||
.workspace-suggestion-item.selected .workspace-suggestion-type {
|
||||
background-color: var(--background-modifier-border);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.workspace-smart-add-tip {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
margin-top: 8px;
|
||||
padding: 0 2px;
|
||||
background-color: var(--background-secondary-alt);
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--radius-s);
|
||||
border-left: 2px solid var(--text-accent);
|
||||
}
|
||||
|
||||
.workspace-edit-modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.workspace-edit-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-s);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.workspace-edit-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.workspace-edit-btn-cancel {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.workspace-edit-btn-cancel:hover:not(:disabled) {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.workspace-edit-btn-save {
|
||||
background-color: var(--text-accent);
|
||||
border: 1px solid var(--text-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.workspace-edit-btn-save:hover:not(:disabled) {
|
||||
background-color: var(--text-accent-hover);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkspaceEditModal
|
||||
295
src/components/chat-view/WorkspaceSelect.tsx
Normal file
295
src/components/chat-view/WorkspaceSelect.tsx
Normal file
@ -0,0 +1,295 @@
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { Check, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { Notice } from 'obsidian'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { useApp } from '../../contexts/AppContext'
|
||||
import { useSettings } from '../../contexts/SettingsContext'
|
||||
import { Workspace } from '../../database/json/workspace/types'
|
||||
import { WorkspaceManager } from '../../database/json/workspace/WorkspaceManager'
|
||||
|
||||
interface WorkspaceInfo extends Workspace {
|
||||
isCurrent: boolean
|
||||
}
|
||||
|
||||
const WorkspaceSelect = () => {
|
||||
const app = useApp()
|
||||
const { settings, setSettings } = useSettings()
|
||||
const [workspaces, setWorkspaces] = useState<WorkspaceInfo[]>([])
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [workspaceManager, setWorkspaceManager] = useState<WorkspaceManager | null>(null)
|
||||
|
||||
// 初始化工作区管理器
|
||||
useEffect(() => {
|
||||
const manager = new WorkspaceManager(app)
|
||||
setWorkspaceManager(manager)
|
||||
}, [app])
|
||||
|
||||
// 获取当前工作区名称
|
||||
const getCurrentWorkspaceName = () => {
|
||||
return settings.workspace || 'vault'
|
||||
}
|
||||
|
||||
// 获取工作区列表
|
||||
const getWorkspaces = useCallback(async () => {
|
||||
if (!workspaceManager) return []
|
||||
|
||||
try {
|
||||
// 确保默认 vault 工作区存在
|
||||
await workspaceManager.ensureDefaultVaultWorkspace()
|
||||
|
||||
// 获取所有工作区
|
||||
const workspaceMetadata = await workspaceManager.listWorkspaces()
|
||||
|
||||
const workspaceList: WorkspaceInfo[] = []
|
||||
const currentWorkspaceName = getCurrentWorkspaceName()
|
||||
|
||||
for (const meta of workspaceMetadata) {
|
||||
const workspace = await workspaceManager.findById(meta.id)
|
||||
if (workspace) {
|
||||
workspaceList.push({
|
||||
...workspace,
|
||||
isCurrent: workspace.name === currentWorkspaceName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 按名称排序,vault 排在最前面
|
||||
workspaceList.sort((a, b) => {
|
||||
if (a.name === 'vault') return -1
|
||||
if (b.name === 'vault') return 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
|
||||
return workspaceList
|
||||
} catch (error) {
|
||||
console.error('获取工作区列表失败:', error)
|
||||
return []
|
||||
}
|
||||
}, [workspaceManager, settings.workspace])
|
||||
|
||||
// 刷新工作区列表
|
||||
const refreshWorkspaces = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const workspaceList = await getWorkspaces()
|
||||
setWorkspaces(workspaceList)
|
||||
} catch (error) {
|
||||
console.error('刷新工作区列表失败:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [getWorkspaces])
|
||||
|
||||
// 切换到指定工作区
|
||||
const switchToWorkspace = async (workspace: WorkspaceInfo) => {
|
||||
if (workspace.isCurrent) {
|
||||
setIsOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新设置中的工作区
|
||||
setSettings({
|
||||
...settings,
|
||||
workspace: workspace.name
|
||||
})
|
||||
|
||||
// 关闭下拉菜单
|
||||
setIsOpen(false)
|
||||
|
||||
// 刷新工作区列表以更新状态
|
||||
await refreshWorkspaces()
|
||||
} catch (error) {
|
||||
console.error('切换工作区失败:', error)
|
||||
new Notice('切换工作区失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化和设置变化时刷新
|
||||
useEffect(() => {
|
||||
refreshWorkspaces()
|
||||
}, [refreshWorkspaces])
|
||||
|
||||
// 下拉菜单打开时刷新数据
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (open && !isOpen) {
|
||||
refreshWorkspaces()
|
||||
}
|
||||
setIsOpen(open)
|
||||
}
|
||||
|
||||
const currentWorkspace = workspaces.find(w => w.isCurrent)
|
||||
const displayName = currentWorkspace?.name || getCurrentWorkspaceName()
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu.Root open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenu.Trigger className="infio-workspace-select">
|
||||
<span className="infio-workspace-select__name">
|
||||
{displayName}
|
||||
</span>
|
||||
<div className="infio-workspace-select__icon">
|
||||
{isOpen ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
||||
</div>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content className="infio-popover infio-workspace-select-content">
|
||||
{isLoading ? (
|
||||
<div className="infio-workspace-loading">
|
||||
加载中...
|
||||
</div>
|
||||
) : workspaces.length === 0 ? (
|
||||
<div className="infio-workspace-empty">
|
||||
暂无工作区
|
||||
</div>
|
||||
) : (
|
||||
<ul>
|
||||
{workspaces.map((workspace) => (
|
||||
<DropdownMenu.Item
|
||||
key={workspace.id}
|
||||
onSelect={() => switchToWorkspace(workspace)}
|
||||
asChild
|
||||
>
|
||||
<li className={`infio-workspace-item`}>
|
||||
<span className="infio-workspace-item-name">
|
||||
{workspace.name}
|
||||
</span>
|
||||
{workspace.isCurrent && (
|
||||
<Check size={14} className="infio-workspace-check" />
|
||||
)}
|
||||
</li>
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<style>{`
|
||||
button.infio-workspace-select {
|
||||
background-color: var(--background-modifier-hover);
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
padding: var(--size-4-1) var(--size-4-1);
|
||||
font-size: var(--font-small);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
gap: var(--size-2-2);
|
||||
border-radius: var(--radius-l);
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
button.infio-workspace-select:hover {
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
button.infio-workspace-select:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.infio-workspace-select__name {
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.infio-workspace-select__icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.infio-workspace-select-content {
|
||||
min-width: auto !important;
|
||||
width: fit-content !important;
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.infio-workspace-loading,
|
||||
.infio-workspace-empty {
|
||||
padding: var(--size-4-3) var(--size-4-2);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-small);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.infio-workspace-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: var(--size-4-2) var(--size-4-2);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.infio-workspace-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-2-1);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.infio-workspace-item-name {
|
||||
font-size: var(--font-small);
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.infio-workspace-item-info {
|
||||
font-size: var(--font-smallest);
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.infio-workspace-check {
|
||||
color: var(--text-accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.infio-workspace-select-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.infio-workspace-select-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.infio-workspace-select-content::-webkit-scrollbar-thumb {
|
||||
background: var(--background-modifier-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.infio-workspace-select-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--background-modifier-border-hover);
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkspaceSelect
|
||||
907
src/components/chat-view/WorkspaceView.tsx
Normal file
907
src/components/chat-view/WorkspaceView.tsx
Normal file
@ -0,0 +1,907 @@
|
||||
import {
|
||||
ArrowRight,
|
||||
Box,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
FolderOpen,
|
||||
MessageSquare,
|
||||
Pencil,
|
||||
Plus,
|
||||
RotateCcw,
|
||||
Tag,
|
||||
Trash2
|
||||
} from 'lucide-react'
|
||||
import { Notice } from 'obsidian'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { useApp } from '../../contexts/AppContext'
|
||||
import { useSettings } from '../../contexts/SettingsContext'
|
||||
import { Workspace, WorkspaceContent } from '../../database/json/workspace/types'
|
||||
import { WorkspaceManager } from '../../database/json/workspace/WorkspaceManager'
|
||||
import { t } from '../../lang/helpers'
|
||||
|
||||
import WorkspaceEditModal from './WorkspaceEditModal'
|
||||
|
||||
interface WorkspaceInfo extends Workspace {
|
||||
isCurrent: boolean
|
||||
}
|
||||
|
||||
const WorkspaceView = () => {
|
||||
const app = useApp()
|
||||
const { settings, setSettings } = useSettings()
|
||||
const [workspaces, setWorkspaces] = useState<WorkspaceInfo[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [workspaceManager, setWorkspaceManager] = useState<WorkspaceManager | null>(null)
|
||||
const [editingWorkspace, setEditingWorkspace] = useState<Workspace | null>(null)
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
|
||||
|
||||
// 初始化工作区管理器
|
||||
useEffect(() => {
|
||||
const manager = new WorkspaceManager(app)
|
||||
setWorkspaceManager(manager)
|
||||
}, [app])
|
||||
|
||||
// 获取当前工作区名称
|
||||
const getCurrentWorkspaceName = (): string => {
|
||||
return settings.workspace || 'vault'
|
||||
}
|
||||
|
||||
// 获取工作区列表
|
||||
const getWorkspaces = useCallback(async (): Promise<WorkspaceInfo[]> => {
|
||||
if (!workspaceManager) return []
|
||||
|
||||
try {
|
||||
// 确保默认 vault 工作区存在
|
||||
await workspaceManager.ensureDefaultVaultWorkspace()
|
||||
|
||||
// 获取所有工作区
|
||||
const workspaceMetadata = await workspaceManager.listWorkspaces()
|
||||
|
||||
const workspaceList: WorkspaceInfo[] = []
|
||||
const currentWorkspaceName = getCurrentWorkspaceName()
|
||||
|
||||
for (const meta of workspaceMetadata) {
|
||||
const workspace = await workspaceManager.findById(meta.id)
|
||||
if (workspace) {
|
||||
workspaceList.push({
|
||||
...workspace,
|
||||
isCurrent: workspace.name === currentWorkspaceName
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return workspaceList
|
||||
} catch (error) {
|
||||
console.error('获取工作区列表失败:', error)
|
||||
return []
|
||||
}
|
||||
}, [workspaceManager, settings.workspace])
|
||||
|
||||
// 刷新工作区列表
|
||||
const refreshWorkspaces = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const workspaceList = await getWorkspaces()
|
||||
setWorkspaces(workspaceList)
|
||||
} catch (error) {
|
||||
console.error('刷新工作区列表失败:', error)
|
||||
new Notice(String(t('workspace.notices.refreshFailed')))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [getWorkspaces])
|
||||
|
||||
// 切换到指定工作区
|
||||
const switchToWorkspace = async (workspace: WorkspaceInfo) => {
|
||||
if (workspace.isCurrent) {
|
||||
new Notice(String(t('workspace.notices.alreadyInWorkspace')))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新设置中的工作区
|
||||
setSettings({
|
||||
...settings,
|
||||
workspace: workspace.name
|
||||
})
|
||||
|
||||
// 刷新工作区列表以更新状态
|
||||
await refreshWorkspaces()
|
||||
} catch (error) {
|
||||
console.error('切换工作区失败:', error)
|
||||
new Notice(String(t('workspace.notices.switchFailed')))
|
||||
}
|
||||
}
|
||||
|
||||
// 删除工作区
|
||||
const deleteWorkspace = async (workspace: WorkspaceInfo) => {
|
||||
if (!workspaceManager) return
|
||||
|
||||
if (workspace.isCurrent) {
|
||||
new Notice(String(t('workspace.notices.cannotDeleteCurrent')))
|
||||
return
|
||||
}
|
||||
|
||||
if (workspace.name === 'vault') {
|
||||
new Notice(String(t('workspace.notices.cannotDeleteDefault')))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await workspaceManager.deleteWorkspace(workspace.id)
|
||||
if (success) {
|
||||
new Notice(String(t('workspace.notices.deleted', { name: workspace.name })))
|
||||
await refreshWorkspaces()
|
||||
} else {
|
||||
new Notice(String(t('workspace.notices.deleteFailed')))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除工作区失败:', error)
|
||||
new Notice(String(t('workspace.notices.deleteFailed')))
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新工作区
|
||||
const createNewWorkspace = () => {
|
||||
setIsCreateModalOpen(true)
|
||||
}
|
||||
|
||||
// 关闭创建模态框
|
||||
const closeCreateModal = () => {
|
||||
setIsCreateModalOpen(false)
|
||||
}
|
||||
|
||||
// 保存新工作区
|
||||
const saveNewWorkspace = async (workspaceData: Partial<Workspace>) => {
|
||||
if (!workspaceManager) return
|
||||
|
||||
try {
|
||||
const newWorkspace = await workspaceManager.createWorkspace({
|
||||
name: workspaceData.name || String(t('workspace.newWorkspace')),
|
||||
content: workspaceData.content || [],
|
||||
metadata: {
|
||||
description: workspaceData.metadata?.description || String(t('workspace.newWorkspace'))
|
||||
}
|
||||
})
|
||||
|
||||
new Notice(String(t('workspace.notices.created', { name: newWorkspace.name })))
|
||||
await refreshWorkspaces()
|
||||
closeCreateModal()
|
||||
} catch (error) {
|
||||
console.error('创建工作区失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 打开编辑工作区模态框
|
||||
const openEditModal = (workspace: WorkspaceInfo) => {
|
||||
setEditingWorkspace(workspace)
|
||||
setIsEditModalOpen(true)
|
||||
}
|
||||
|
||||
// 关闭编辑模态框
|
||||
const closeEditModal = () => {
|
||||
setIsEditModalOpen(false)
|
||||
setEditingWorkspace(null)
|
||||
}
|
||||
|
||||
// 保存工作区编辑
|
||||
const saveWorkspaceEdit = async (updates: Partial<Workspace>) => {
|
||||
if (!workspaceManager || !editingWorkspace) return
|
||||
|
||||
try {
|
||||
await workspaceManager.updateWorkspace(editingWorkspace.id, updates)
|
||||
new Notice(String(t('workspace.notices.updated', { name: updates.name || editingWorkspace.name })))
|
||||
await refreshWorkspaces()
|
||||
} catch (error) {
|
||||
console.error('更新工作区失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化工作区内容
|
||||
const formatWorkspaceContent = (content: WorkspaceContent[]): string => {
|
||||
if (content.length === 0) return String(t('workspace.empty'))
|
||||
|
||||
const folders = content.filter(c => c.type === 'folder').length
|
||||
const tags = content.filter(c => c.type === 'tag').length
|
||||
|
||||
const parts = []
|
||||
if (folders > 0) parts.push(`${folders} ${String(t('workspace.folders'))}`)
|
||||
if (tags > 0) parts.push(`${tags} ${String(t('workspace.tags'))}`)
|
||||
|
||||
return parts.join(', ') || String(t('workspace.noContent'))
|
||||
}
|
||||
|
||||
// 展开状态管理
|
||||
const [expandedWorkspaces, setExpandedWorkspaces] = useState<Set<string>>(new Set())
|
||||
const [expandedChats, setExpandedChats] = useState<Set<string>>(new Set())
|
||||
|
||||
// 切换工作区内容展开状态
|
||||
const toggleWorkspaceExpanded = (workspaceId: string) => {
|
||||
const newExpanded = new Set(expandedWorkspaces)
|
||||
if (newExpanded.has(workspaceId)) {
|
||||
newExpanded.delete(workspaceId)
|
||||
} else {
|
||||
newExpanded.add(workspaceId)
|
||||
}
|
||||
setExpandedWorkspaces(newExpanded)
|
||||
}
|
||||
|
||||
// 切换对话历史展开状态
|
||||
const toggleChatExpanded = (workspaceId: string) => {
|
||||
const newExpanded = new Set(expandedChats)
|
||||
if (newExpanded.has(workspaceId)) {
|
||||
newExpanded.delete(workspaceId)
|
||||
} else {
|
||||
newExpanded.add(workspaceId)
|
||||
}
|
||||
setExpandedChats(newExpanded)
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatLastOpened = (timestamp?: number) => {
|
||||
if (!timestamp) return '未知'
|
||||
|
||||
const now = Date.now()
|
||||
const diff = now - timestamp
|
||||
const minutes = Math.floor(diff / 60000)
|
||||
const hours = Math.floor(diff / 3600000)
|
||||
const days = Math.floor(diff / 86400000)
|
||||
|
||||
if (minutes < 1) return '刚刚'
|
||||
if (minutes < 60) return `${minutes} 分钟前`
|
||||
if (hours < 24) return `${hours} 小时前`
|
||||
if (days < 7) return `${days} 天前`
|
||||
|
||||
return new Date(timestamp).toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// 组件初始化
|
||||
useEffect(() => {
|
||||
refreshWorkspaces().catch((error) => {
|
||||
console.error('初始化工作区列表失败:', error)
|
||||
})
|
||||
}, [refreshWorkspaces])
|
||||
|
||||
return (
|
||||
<div className="infio-workspace-view-container">
|
||||
{/* 头部 */}
|
||||
<div className="infio-workspace-view-header">
|
||||
<div className="infio-workspace-view-title">
|
||||
<h2>{t('workspace.title')}</h2>
|
||||
</div>
|
||||
<div className="infio-workspace-view-header-actions">
|
||||
<button
|
||||
onClick={refreshWorkspaces}
|
||||
className="infio-workspace-view-refresh-btn"
|
||||
disabled={isLoading}
|
||||
title={t('workspace.refreshTooltip')}
|
||||
>
|
||||
<RotateCcw size={16} className={isLoading ? 'spinning' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 描述 */}
|
||||
<div className="infio-workspace-view-tip">
|
||||
{t('workspace.description')}
|
||||
</div>
|
||||
|
||||
{/* 创建新工作区按钮 */}
|
||||
<div className="infio-workspace-view-create-action">
|
||||
<button
|
||||
className="infio-workspace-view-create-btn"
|
||||
onClick={createNewWorkspace}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Plus size={16} />
|
||||
<span>{t('workspace.createNew')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 工作区列表 */}
|
||||
<div className="infio-workspace-view-list">
|
||||
<div className="infio-workspace-view-list-header">
|
||||
<h3>{t('workspace.recentWorkspaces')}</h3>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="infio-workspace-view-loading">
|
||||
{t('workspace.loading')}
|
||||
</div>
|
||||
) : workspaces.length === 0 ? (
|
||||
<div className="infio-workspace-view-empty">
|
||||
<Box size={48} className="infio-workspace-view-empty-icon" />
|
||||
<p>{t('workspace.noWorkspaces')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="infio-workspace-view-items">
|
||||
{workspaces.map((workspace, index) => (
|
||||
<div
|
||||
key={workspace.id || index}
|
||||
className={`infio-workspace-view-item ${workspace.isCurrent ? 'current' : ''}`}
|
||||
>
|
||||
<div className="infio-workspace-view-item-header">
|
||||
<div className="infio-workspace-view-item-icon">
|
||||
{workspace.isCurrent ? (
|
||||
<Box size={20} />
|
||||
) : (
|
||||
<Box size={20} />
|
||||
)}
|
||||
</div>
|
||||
<div className="infio-workspace-view-item-name">
|
||||
{workspace.name}
|
||||
{workspace.isCurrent && (
|
||||
<span className="infio-workspace-view-current-badge">{String(t('workspace.current'))}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="infio-workspace-view-item-actions">
|
||||
{!workspace.isCurrent && (
|
||||
<button
|
||||
onClick={() => switchToWorkspace(workspace)}
|
||||
className="infio-workspace-view-action-btn switch-btn"
|
||||
title="切换到此工作区"
|
||||
>
|
||||
<ArrowRight size={16} />
|
||||
</button>
|
||||
)}
|
||||
{workspace.name !== 'vault' && (
|
||||
<button
|
||||
onClick={() => openEditModal(workspace)}
|
||||
className="infio-workspace-view-action-btn"
|
||||
title={String(t('workspace.editTooltip'))}
|
||||
>
|
||||
<Pencil size={16} />
|
||||
</button>
|
||||
)}
|
||||
{!workspace.isCurrent && workspace.name !== 'vault' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm(String(t('workspace.deleteConfirm', { name: workspace.name })))) {
|
||||
deleteWorkspace(workspace)
|
||||
}
|
||||
}}
|
||||
className="infio-workspace-view-action-btn danger"
|
||||
title={String(t('workspace.deleteTooltip'))}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="infio-workspace-view-item-content">
|
||||
{/* 工作区内容 */}
|
||||
<div
|
||||
className="infio-workspace-view-item-path clickable"
|
||||
onClick={() => toggleWorkspaceExpanded(workspace.id)}
|
||||
>
|
||||
<div className="infio-workspace-view-item-path-info">
|
||||
<FolderOpen size={12} />
|
||||
{formatWorkspaceContent(workspace.content)}
|
||||
</div>
|
||||
{workspace.content.length > 0 && (
|
||||
<div className="infio-workspace-view-expand-icon">
|
||||
{expandedWorkspaces.has(workspace.id) ? (
|
||||
<ChevronDown size={14} />
|
||||
) : (
|
||||
<ChevronRight size={14} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 展开的内容详情 */}
|
||||
{expandedWorkspaces.has(workspace.id) && workspace.content.length > 0 && (
|
||||
<div className="infio-workspace-view-content-details">
|
||||
<div className="infio-workspace-view-content-list">
|
||||
{workspace.content.map((item, itemIndex) => (
|
||||
<div key={itemIndex} className="infio-workspace-view-content-item">
|
||||
{item.type === 'folder' ? (
|
||||
<FolderOpen size={14} />
|
||||
) : (
|
||||
<Tag size={14} />
|
||||
)}
|
||||
<span className="infio-workspace-view-content-text">
|
||||
{item.content}
|
||||
</span>
|
||||
<span className="infio-workspace-view-content-type">
|
||||
{item.type === 'folder' ? '文件夹' : '标签'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 对话历史 */}
|
||||
<div
|
||||
className="infio-workspace-view-chat-info clickable"
|
||||
onClick={() => toggleChatExpanded(workspace.id)}
|
||||
>
|
||||
<div className="infio-workspace-view-chat-info-content">
|
||||
<MessageSquare size={12} />
|
||||
<span>{workspace.chatHistory.length} {String(t('workspace.conversations'))}</span>
|
||||
</div>
|
||||
{workspace.chatHistory.length > 0 && (
|
||||
<div className="infio-workspace-view-expand-icon">
|
||||
{expandedChats.has(workspace.id) ? (
|
||||
<ChevronDown size={14} />
|
||||
) : (
|
||||
<ChevronRight size={14} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 展开的对话历史详情 */}
|
||||
{expandedChats.has(workspace.id) && workspace.chatHistory.length > 0 && (
|
||||
<div className="infio-workspace-view-chat-details">
|
||||
<div className="infio-workspace-view-chat-list">
|
||||
{workspace.chatHistory.slice(-5).reverse().map((chat, chatIndex) => (
|
||||
<div key={chatIndex} className="infio-workspace-view-chat-item">
|
||||
<MessageSquare size={14} />
|
||||
<span className="infio-workspace-view-chat-title">
|
||||
{chat.title || `对话 ${chat.id.slice(0, 8)}`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="infio-workspace-view-item-meta">
|
||||
{String(t('workspace.created'))}: {new Date(workspace.createdAt).toLocaleDateString('zh-CN')} |
|
||||
{String(t('workspace.updated'))}: {formatLastOpened(workspace.updatedAt)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 编辑模态框 */}
|
||||
{editingWorkspace && (
|
||||
<WorkspaceEditModal
|
||||
workspace={editingWorkspace}
|
||||
app={app}
|
||||
isOpen={isEditModalOpen}
|
||||
onClose={closeEditModal}
|
||||
onSave={saveWorkspaceEdit}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 创建工作区模态框 */}
|
||||
<WorkspaceEditModal
|
||||
workspace={undefined}
|
||||
app={app}
|
||||
isOpen={isCreateModalOpen}
|
||||
onClose={closeCreateModal}
|
||||
onSave={saveNewWorkspace}
|
||||
/>
|
||||
|
||||
{/* 样式 */}
|
||||
<style>
|
||||
{`
|
||||
.infio-workspace-view-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
color: var(--text-normal);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.infio-workspace-view-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.infio-workspace-view-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-title h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.infio-workspace-view-header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infio-workspace-view-refresh-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;
|
||||
}
|
||||
}
|
||||
|
||||
.spinning {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.infio-workspace-view-tip {
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-create-action {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-create-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
color: var(--text-normal);
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-m);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.infio-workspace-view-create-btn:hover:not(:disabled) {
|
||||
background-color: var(--background-modifier-hover);
|
||||
border-color: var(--text-accent);
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.infio-workspace-view-create-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.infio-workspace-view-current {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
padding: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-current-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--text-normal);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-current-info {
|
||||
margin-left: 26px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-current-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-current-status {
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.infio-workspace-view-list-header h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.infio-workspace-view-loading {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.infio-workspace-view-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.infio-workspace-view-empty-icon {
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.infio-workspace-view-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
padding: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.infio-workspace-view-item.current {
|
||||
border-color: var(--background-modifier-border);
|
||||
background-color: var(--background-primary);
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-icon {
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
margin-bottom: 4px;
|
||||
justify-content: flex-start;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item.current .infio-workspace-view-item-icon {
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-current-badge {
|
||||
background-color: var(--text-accent);
|
||||
color: var(--text-on-accent);
|
||||
font-size: 12px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-path {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
margin-bottom: 2px;
|
||||
padding: 4px 0;
|
||||
border-radius: var(--radius-s);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-path.clickable {
|
||||
cursor: pointer;
|
||||
padding: 6px 8px;
|
||||
margin: -2px -4px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-path.clickable:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-path-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.infio-workspace-view-expand-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.infio-workspace-view-content-details {
|
||||
margin-top: 8px;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.infio-workspace-view-content-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-content-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: var(--radius-s);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-content-text {
|
||||
flex: 1;
|
||||
color: var(--text-normal);
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.infio-workspace-view-content-type {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
background-color: var(--background-modifier-border);
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
padding: 4px 0;
|
||||
border-radius: var(--radius-s);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-info.clickable {
|
||||
cursor: pointer;
|
||||
padding: 6px 8px;
|
||||
margin: 2px -4px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-info.clickable:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-info-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-details {
|
||||
margin-top: 8px;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: var(--radius-s);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-chat-title {
|
||||
flex: 1;
|
||||
color: var(--text-normal);
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.infio-workspace-view-item-meta {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.infio-workspace-view-action-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;
|
||||
border-radius: var(--radius-s);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-modifier-hover) !important;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkspaceView
|
||||
@ -1,5 +1,5 @@
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { ChevronDown, ChevronUp, MessageSquare, SquarePen, Search } from 'lucide-react'
|
||||
import { ChevronDown, ChevronUp, MessageSquare, Search, SquarePen } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { useSettings } from '../../../contexts/SettingsContext'
|
||||
@ -114,10 +114,10 @@ export function ModeSelect() {
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
height: var(--size-4-4);
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
gap: var(--size-2-2);
|
||||
border-radius: var(--radius-m);
|
||||
border-radius: var(--radius-l);
|
||||
transition: all 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
@ -128,6 +128,7 @@ export function ModeSelect() {
|
||||
.infio-chat-input-mode-select__mode-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
margin-top: var(--size-4-1);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-accent);
|
||||
|
||||
28
src/contexts/DataviewContext.tsx
Normal file
28
src/contexts/DataviewContext.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import {
|
||||
PropsWithChildren,
|
||||
createContext,
|
||||
useContext,
|
||||
useMemo
|
||||
} from 'react'
|
||||
|
||||
import { DataviewManager } from '../utils/dataview'
|
||||
|
||||
const DataviewContext = createContext<DataviewManager | null>(null)
|
||||
|
||||
export function DataviewProvider({
|
||||
dataviewManager,
|
||||
children,
|
||||
}: PropsWithChildren<{ dataviewManager: DataviewManager | null }>) {
|
||||
const value = useMemo(() => {
|
||||
return dataviewManager
|
||||
}, [dataviewManager])
|
||||
|
||||
return <DataviewContext.Provider value={value}>{children}</DataviewContext.Provider>
|
||||
}
|
||||
|
||||
export function useDataview(): DataviewManager | null {
|
||||
const context = useContext(DataviewContext)
|
||||
// 注意:这里不抛出错误,允许返回 null
|
||||
// 调用者需要自己检查 context 是否为 null
|
||||
return context
|
||||
}
|
||||
@ -55,10 +55,15 @@ Only a single operation is allowed per tool use.
|
||||
The SEARCH section must exactly match existing content including whitespace and indentation.
|
||||
If you're not confident in the exact content to search for, use the read_file tool first to get the exact content.
|
||||
When applying changes to Markdown, be careful about maintaining list structures, heading levels, and other Markdown formatting.
|
||||
ALWAYS make as many changes in a single 'apply_diff' request as possible using multiple SEARCH/REPLACE blocks
|
||||
**IMPORTANT STRATEGY**: Use this tool ONLY for modifying specific, contiguous blocks of text.
|
||||
- **STRICT 20-LINE LIMIT**: Each SEARCH block MUST NOT exceed 20 lines. This is a hard limit to ensure accuracy and avoid errors.
|
||||
- **For larger changes**: If a single block of changes is more than 20 lines, or if you are rewriting most of the file, you MUST use \`write_to_file\` instead.
|
||||
- **For scattered changes**: If you need to make the same small change in many different places, use \`search_and_replace\` as it is more efficient.
|
||||
|
||||
ALWAYS make as many changes in a single 'apply_diff' request as possible using multiple SEARCH/REPLACE blocks, but respect the 20-line limit per block.
|
||||
|
||||
Parameters:
|
||||
- path: (required) The path of the file to modify (relative to the current working directory ${args.cwd})
|
||||
- path: (required) The path of the file to modify
|
||||
- diff: (required) The search/replace block defining the changes.
|
||||
|
||||
Diff format:
|
||||
@ -72,72 +77,58 @@ Diff format:
|
||||
[new content to replace with]
|
||||
>>>>>>> REPLACE
|
||||
|
||||
\`\`\`
|
||||
|
||||
Example:
|
||||
|
||||
Original Markdown file:
|
||||
\`\`\`
|
||||
1 | # Project Notes
|
||||
2 |
|
||||
3 | ## Tasks
|
||||
4 | - [ ] Review documentation
|
||||
5 | - [ ] Update examples
|
||||
6 | - [ ] Add new section
|
||||
\`\`\`
|
||||
|
||||
Search/Replace content:
|
||||
\`\`\`
|
||||
<<<<<<< SEARCH
|
||||
:start_line:3
|
||||
:end_line:6
|
||||
-------
|
||||
## Tasks
|
||||
- [ ] Review documentation
|
||||
- [ ] Update examples
|
||||
- [ ] Add new section
|
||||
=======
|
||||
## Current Tasks
|
||||
- [ ] Review documentation
|
||||
- [x] Update examples
|
||||
- [ ] Add new section
|
||||
- [ ] Schedule team meeting
|
||||
>>>>>>> REPLACE
|
||||
|
||||
\`\`\`
|
||||
|
||||
Search/Replace content with multi edits:
|
||||
\`\`\`
|
||||
<<<<<<< SEARCH
|
||||
:start_line:1
|
||||
:end_line:1
|
||||
-------
|
||||
# Project Notes
|
||||
=======
|
||||
# Project Notes (Updated)
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
:start_line:4
|
||||
:end_line:5
|
||||
-------
|
||||
- [ ] Review documentation
|
||||
- [ ] Update examples
|
||||
=======
|
||||
- [ ] Review documentation (priority)
|
||||
- [x] Update examples
|
||||
>>>>>>> REPLACE
|
||||
\`\`\`
|
||||
|
||||
Usage:
|
||||
<apply_diff>
|
||||
<path>File path here</path>
|
||||
<diff>
|
||||
Your search/replace content here
|
||||
You can use multi search/replace block in one diff block, but make sure to include the line numbers for each block.
|
||||
Only use a single line of '=======' between search and replacement content, because multiple '=======' will corrupt the file.
|
||||
Only use a single line of '=======' between search and replacement content.
|
||||
</diff>
|
||||
</apply_diff>`
|
||||
</apply_diff>
|
||||
|
||||
Example1: CORRECT - Making multiple, small, independent changes in one call.
|
||||
This is efficient for applying several unrelated fixes. Notice that each block is very small and respects the 20-line limit.
|
||||
|
||||
<apply_diff>
|
||||
<path>notes.md</path>
|
||||
<diff>
|
||||
<<<<<<< SEARCH
|
||||
:start_line:1
|
||||
:end_line:1
|
||||
-------
|
||||
# Meeting Notes
|
||||
=======
|
||||
# Strategic Meeting Notes
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
:start_line:4
|
||||
:end_line:4
|
||||
-------
|
||||
- Discuss Q3 roadmap
|
||||
=======
|
||||
- Finalize Q3 roadmap
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
</apply_diff>
|
||||
|
||||
Example 2: INCORRECT USAGE (ANTI-PATTERN) - The diff block is too large.
|
||||
This demonstrates what NOT to do. A diff block of this size MUST BE REJECTED.
|
||||
|
||||
<apply_diff>
|
||||
<path>long_file.md</path>
|
||||
<diff>
|
||||
<<<<<<< SEARCH
|
||||
:start_line:10
|
||||
:end_line:45
|
||||
-------
|
||||
... (35 lines of text to be replaced) ...
|
||||
=======
|
||||
... (new content) ...
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
</apply_diff>
|
||||
`
|
||||
}
|
||||
|
||||
async applyDiff(
|
||||
@ -146,7 +137,7 @@ Only use a single line of '=======' between search and replacement content, beca
|
||||
_paramStartLine?: number,
|
||||
_paramEndLine?: number,
|
||||
): Promise<DiffResult> {
|
||||
let matches = [
|
||||
const matches = [
|
||||
...diffContent.matchAll(
|
||||
/<<<<<<< SEARCH\n(:start_line:\s*(\d+)\n){0,1}(:end_line:\s*(\d+)\n){0,1}(-------\n){0,1}([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/g,
|
||||
),
|
||||
@ -162,7 +153,7 @@ Only use a single line of '=======' between search and replacement content, beca
|
||||
const lineEnding = originalContent.includes("\r\n") ? "\r\n" : "\n"
|
||||
let resultLines = originalContent.split(/\r?\n/)
|
||||
let delta = 0
|
||||
let diffResults: DiffResult[] = []
|
||||
const diffResults: DiffResult[] = []
|
||||
let appliedCount = 0
|
||||
const replacements = matches
|
||||
.map((match) => ({
|
||||
@ -313,24 +304,25 @@ Only use a single line of '=======' between search and replacement content, beca
|
||||
const matchedLines = resultLines.slice(matchIndex, matchIndex + searchLines.length)
|
||||
|
||||
// Get the exact indentation (preserving tabs/spaces) of each line
|
||||
const indentRegex = /^[\t ]*/
|
||||
const originalIndents = matchedLines.map((line) => {
|
||||
const match = line.match(/^[\t ]*/)
|
||||
const match = indentRegex.exec(line)
|
||||
return match ? match[0] : ""
|
||||
})
|
||||
|
||||
// Get the exact indentation of each line in the search block
|
||||
const searchIndents = searchLines.map((line) => {
|
||||
const match = line.match(/^[\t ]*/)
|
||||
const match = indentRegex.exec(line)
|
||||
return match ? match[0] : ""
|
||||
})
|
||||
|
||||
// Apply the replacement while preserving exact indentation
|
||||
const indentedReplaceLines = replaceLines.map((line, i) => {
|
||||
const indentedReplaceLines = replaceLines.map((line) => {
|
||||
// Get the matched line's exact indentation
|
||||
const matchedIndent = originalIndents[0] || ""
|
||||
|
||||
// Get the current line's indentation relative to the search content
|
||||
const currentIndentMatch = line.match(/^[\t ]*/)
|
||||
const currentIndentMatch = indentRegex.exec(line)
|
||||
const currentIndent = currentIndentMatch ? currentIndentMatch[0] : ""
|
||||
const searchBaseIndent = searchIndents[0] || ""
|
||||
|
||||
|
||||
@ -32,6 +32,38 @@ CAPABILITIES
|
||||
`
|
||||
}
|
||||
|
||||
function getLearnModeCapabilitiesSection(
|
||||
cwd: string,
|
||||
searchFilesTool: string,
|
||||
): string {
|
||||
let searchFilesInstructions: string;
|
||||
switch (searchFilesTool) {
|
||||
case 'match':
|
||||
searchFilesInstructions = MatchSearchFilesInstructions;
|
||||
break;
|
||||
case 'regex':
|
||||
searchFilesInstructions = RegexSearchFilesInstructions;
|
||||
break;
|
||||
case 'semantic':
|
||||
searchFilesInstructions = SemanticSearchFilesInstructions;
|
||||
break;
|
||||
default:
|
||||
searchFilesInstructions = "";
|
||||
}
|
||||
|
||||
return `====
|
||||
|
||||
CAPABILITIES
|
||||
|
||||
- You are a specialized learning assistant with access to powerful transformation tools designed to enhance learning and comprehension within Obsidian vaults.
|
||||
- Your primary strength lies in processing learning materials using transformation tools like \`simple_summary\`, \`key_insights\`, \`dense_summary\`, \`reflections\`, \`table_of_contents\`, and \`analyze_paper\` to break down complex information into digestible formats.
|
||||
- You excel at creating visual learning aids using Mermaid diagrams (concept maps, flowcharts, mind maps) that help users understand relationships between concepts and visualize learning pathways.
|
||||
- You can generate structured study materials including flashcards, study guides, learning objectives, and practice questions tailored to the user's learning goals and current knowledge level.
|
||||
- You have access to file management tools to organize learning materials, create structured note hierarchies, and maintain a well-organized knowledge base within the vault ('${cwd}').${searchFilesInstructions}
|
||||
- You can identify knowledge gaps by analyzing existing notes and suggest learning paths to fill those gaps, connecting new information to the user's existing knowledge base.
|
||||
- You specialize in active learning techniques that promote retention and understanding rather than passive information consumption, helping users engage deeply with their learning materials.`
|
||||
}
|
||||
|
||||
function getDeepResearchCapabilitiesSection(): string {
|
||||
return `====
|
||||
|
||||
@ -51,5 +83,8 @@ export function getCapabilitiesSection(
|
||||
if (mode === 'research') {
|
||||
return getDeepResearchCapabilitiesSection();
|
||||
}
|
||||
if (mode === 'learn') {
|
||||
return getLearnModeCapabilitiesSection(cwd, searchWebTool);
|
||||
}
|
||||
return getObsidianCapabilitiesSection(cwd, searchWebTool);
|
||||
}
|
||||
|
||||
@ -1,4 +1,25 @@
|
||||
|
||||
function getLearnModeObjectiveSection(): string {
|
||||
return `====
|
||||
|
||||
OBJECTIVE
|
||||
|
||||
You enhance learning and comprehension by transforming information into digestible, engaging formats and creating structured learning experiences.
|
||||
|
||||
1. **Analyze Learning Materials**: When users provide content, immediately assess it for learning potential and identify key concepts, complexity levels, and learning objectives.
|
||||
2. **Apply Transformation Tools**: Use transformation tools like \`simple_summary\`, \`key_insights\`, \`dense_summary\`, \`reflections\`, and \`analyze_paper\` to break down complex information into learnable components.
|
||||
3. **Create Learning Aids**: Generate structured study materials including:
|
||||
- Concept maps and visual diagrams using Mermaid
|
||||
- Flashcards for key terms and concepts
|
||||
- Practice questions and reflection prompts
|
||||
- Learning objectives and progress milestones
|
||||
4. **Build Knowledge Connections**: Link new information to existing knowledge in the vault, creating a comprehensive learning network through [[note links]], tags, and explicit conceptual connections.
|
||||
5. **Structure Learning Progression**: Organize content in logical learning sequences, from foundational concepts to advanced applications, supporting spaced repetition and active recall.
|
||||
6. **Monitor Learning Progress**: Track understanding and suggest next steps, additional resources, or areas that need reinforcement based on the user's learning journey.
|
||||
|
||||
Before using any tool, analyze the learning context within <thinking></thinking> tags. Consider the user's learning goals, existing knowledge level, and how the current task fits into their broader learning objectives. Prioritize transformation tools for content analysis and focus on creating materials that promote active learning rather than passive consumption.`
|
||||
}
|
||||
|
||||
function getDeepResearchObjectiveSection(): string {
|
||||
return `====
|
||||
|
||||
@ -33,5 +54,8 @@ export function getObjectiveSection(mode: string): string {
|
||||
if (mode === 'research') {
|
||||
return getDeepResearchObjectiveSection();
|
||||
}
|
||||
if (mode === 'learn') {
|
||||
return getLearnModeObjectiveSection();
|
||||
}
|
||||
return getObsidianObjectiveSection();
|
||||
}
|
||||
|
||||
@ -1,59 +1,19 @@
|
||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
|
||||
function getEditingInstructions(diffStrategy?: DiffStrategy): string {
|
||||
const instructions: string[] = []
|
||||
const availableTools: string[] = []
|
||||
|
||||
const experiments = {
|
||||
insert_content: true,
|
||||
search_and_replace: true,
|
||||
function getEditingInstructions(mode: string): string {
|
||||
if (mode !== 'write') {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Collect available editing tools
|
||||
if (diffStrategy) {
|
||||
availableTools.push(
|
||||
"apply_diff (for replacing lines in existing documents)",
|
||||
"write_to_file (for creating new documents or complete document rewrites)",
|
||||
)
|
||||
} else {
|
||||
availableTools.push("write_to_file (for creating new documents or complete document rewrites)")
|
||||
}
|
||||
if (experiments?.["insert_content"]) {
|
||||
availableTools.push("insert_content (for adding lines to existing documents)")
|
||||
}
|
||||
if (experiments?.["search_and_replace"]) {
|
||||
availableTools.push("search_and_replace (for finding and replacing individual pieces of text)")
|
||||
}
|
||||
return `- For editing documents, you have access to these tools: apply_diff (for replacing lines in existing documents), write_to_file (for creating new documents or complete document rewrites), insert_content (for adding lines to existing documents), search_and_replace (for finding and replacing individual pieces of text). You MUST follow this decision-making hierarchy to choose the correct tool:
|
||||
|
||||
// Base editing instruction mentioning all available tools
|
||||
if (availableTools.length > 1) {
|
||||
instructions.push(`- For editing documents, you have access to these tools: ${availableTools.join(", ")}.`)
|
||||
}
|
||||
1. **For Small, Scattered, Repetitive Changes**: If the task is to correct a specific term, a typo, or a pattern that appears in multiple, non-contiguous places in the file, your **first and only choice** should be \`search_and_replace\`. It is the most precise and efficient tool for this job.
|
||||
|
||||
// Additional details for experimental features
|
||||
if (experiments?.["insert_content"]) {
|
||||
instructions.push(
|
||||
"- The insert_content tool adds lines of text to documents, such as adding a new paragraph to a document or inserting a new section in a paper. This tool will insert it at the specified line location. It can support multiple operations at once.",
|
||||
)
|
||||
}
|
||||
2. **For Focused, Contiguous Block Edits**: If the task is to modify a single, specific section of a file (like rewriting a paragraph or refactoring a function), use \`apply_diff\`. Remember the **strict 20-line limit** for each search block. If your planned change for a single block exceeds this limit, proceed to rule #3.
|
||||
|
||||
if (experiments?.["search_and_replace"]) {
|
||||
instructions.push(
|
||||
"- The search_and_replace tool finds and replaces text or regex in documents. This tool allows you to search for a specific regex pattern or text and replace it with another value. Be cautious when using this tool to ensure you are replacing the correct text. It can support multiple operations at once.",
|
||||
)
|
||||
}
|
||||
3. **For Large-Scale Rewrites or Major Changes**: If the task requires modifying a large portion of the file (e.g., more than roughly 30-40% of the content), restructuring the entire document, or if a single change block violates the 20-line limit for \`apply_diff\`, you **MUST** use \`write_to_file\`. In these cases, first use \`read_file\` to get the full current content, make all your changes in your internal thought process, and then write the entire, new content back using \`write_to_file\`. This is safer and more efficient than many small diffs.
|
||||
|
||||
if (availableTools.length > 1) {
|
||||
instructions.push(
|
||||
"- You should always prefer using other editing tools over write_to_file when making changes to existing documents since write_to_file is much slower and cannot handle large files.",
|
||||
)
|
||||
}
|
||||
|
||||
instructions.push(
|
||||
"- When using the write_to_file tool to modify a note, use the tool directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// remainder of the note unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken notes, severely impacting the user's knowledge base.",
|
||||
)
|
||||
|
||||
return instructions.join("\n")
|
||||
- The rule "You should always prefer using other editing tools over write_to_file" is ONLY valid when the changes are small enough to be handled by \`search_and_replace\` or \`apply_diff\` according to the hierarchy above. For major rewrites, \`write_to_file\` is the PREFERRED tool.`
|
||||
}
|
||||
|
||||
function getSearchInstructions(searchTool: string): string {
|
||||
@ -67,6 +27,32 @@ function getSearchInstructions(searchTool: string): string {
|
||||
return ""
|
||||
}
|
||||
|
||||
function getLearnModeRulesSection(
|
||||
cwd: string,
|
||||
searchTool: string,
|
||||
): string {
|
||||
return `====
|
||||
|
||||
RULES
|
||||
|
||||
- Your current obsidian directory is: ${cwd.toPosix()}
|
||||
${getSearchInstructions(searchTool)}
|
||||
- **Learning-First Approach**: Always prioritize transformation tools when users provide learning materials. Start by analyzing content with tools like \`simple_summary\`, \`key_insights\`, or \`analyze_paper\` before creating additional learning materials.
|
||||
- **Active Learning Focus**: Generate interactive learning materials that promote engagement rather than passive consumption. Create flashcards, concept maps, practice questions, and reflection prompts.
|
||||
- **Knowledge Connection**: When creating new learning notes, actively link them to existing knowledge in the vault using [[note links]], tags (#tag), and explicit connections. Help users build a comprehensive knowledge network.
|
||||
- **Structured Learning Materials**: Organize learning content with clear hierarchies, learning objectives, key concepts, and progress tracking. Use appropriate Obsidian formatting including callouts, headings, and lists.
|
||||
- **Visual Learning Aids**: Use Mermaid diagrams extensively to create concept maps, flowcharts, and visual representations that enhance understanding of complex topics.
|
||||
- **Learning Progress Tracking**: When appropriate, suggest or create learning plans, milestones, and progress indicators to help users track their learning journey.
|
||||
- **Spaced Repetition Support**: Structure learning materials to support spaced repetition and active recall techniques.
|
||||
- When creating learning notes, follow Obsidian conventions with appropriate frontmatter, headings, and formatting that supports the learning process.
|
||||
- Focus on breaking complex topics into digestible, learnable chunks that build upon each other logically.
|
||||
- Use the tools provided efficiently to accomplish learning tasks. When completed, use the attempt_completion tool to present results.
|
||||
- Ask questions only when necessary using ask_followup_question tool, but prefer using available tools to gather needed information.
|
||||
- Your goal is to enhance learning and comprehension, not engage in extended conversations.
|
||||
- Be direct and educational in your responses, focusing on learning outcomes rather than conversational pleasantries.
|
||||
- Wait for user confirmation after each tool use before proceeding to ensure learning materials meet expectations.`
|
||||
}
|
||||
|
||||
function getDeepResearchRulesSection(): string {
|
||||
return `====
|
||||
|
||||
@ -84,6 +70,7 @@ RULES
|
||||
}
|
||||
|
||||
function getObsidianRulesSection(
|
||||
mode: string,
|
||||
cwd: string,
|
||||
searchTool: string,
|
||||
supportsComputerUse: boolean,
|
||||
@ -97,7 +84,7 @@ RULES
|
||||
- Your current obsidian directory is: ${cwd.toPosix()}
|
||||
${getSearchInstructions(searchTool)}
|
||||
- When creating new notes in Obsidian, organize them according to the existing vault structure unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the content logically, adhering to Obsidian conventions with appropriate frontmatter, headings, lists, and formatting. Unless otherwise specified, new notes should follow Markdown syntax with appropriate use of links ([[note name]]), tags (#tag), callouts, and other Obsidian-specific formatting.
|
||||
${getEditingInstructions(diffStrategy)}
|
||||
${getEditingInstructions(mode)}
|
||||
- Be sure to consider the structure of the Obsidian vault (folders, naming conventions, note organization) when determining the appropriate format and content for new or modified notes. Also consider what files may be most relevant to accomplishing the task, for example examining backlinks, linked mentions, or tags would help you understand the relationships between notes, which you could incorporate into any content you write.
|
||||
- When making changes to content, always consider the context within the broader vault. Ensure that your changes maintain existing links, tags, and references, and that they follow the user's established formatting standards and organization.
|
||||
- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
|
||||
@ -123,5 +110,8 @@ export function getRulesSection(
|
||||
if (mode === 'research') {
|
||||
return getDeepResearchRulesSection();
|
||||
}
|
||||
return getObsidianRulesSection(cwd, searchTool, supportsComputerUse, diffStrategy, experiments);
|
||||
if (mode === 'learn') {
|
||||
return getLearnModeRulesSection(cwd, searchTool);
|
||||
}
|
||||
return getObsidianRulesSection(mode, cwd, searchTool, supportsComputerUse, diffStrategy, experiments);
|
||||
}
|
||||
@ -1,6 +1,54 @@
|
||||
export function getToolUseGuidelinesSection(): string {
|
||||
function getLearnModeToolUseGuidelines(): string {
|
||||
return `# Tool Use Guidelines
|
||||
|
||||
## Learning-Focused Tool Selection
|
||||
|
||||
**Prioritize transformation tools to enhance learning and comprehension:**
|
||||
|
||||
- **simple_summary**: Create concise overviews for quick understanding
|
||||
- **key_insights**: Extract core concepts and important ideas
|
||||
- **dense_summary**: Provide comprehensive yet condensed information
|
||||
- **reflections**: Facilitate critical thinking about learning materials
|
||||
- **table_of_contents**: Structure content for better navigation and learning flow
|
||||
- **analyze_paper**: Deep analysis of research papers and academic content
|
||||
|
||||
## Learning Workflow
|
||||
|
||||
1. **Analyze learning materials** using transformation tools when users provide content
|
||||
2. **Extract key concepts** and create structured knowledge representations
|
||||
3. **Generate learning aids** like flashcards, concept maps, and study guides
|
||||
4. **Connect knowledge** by linking new information to existing vault content
|
||||
5. **Create learning plans** based on progress and learning objectives
|
||||
|
||||
## Tool Usage Principles
|
||||
|
||||
- Always start with transformation tools when processing learning materials
|
||||
- Use Mermaid diagrams to create visual learning aids (concept maps, flowcharts)
|
||||
- Generate structured study materials that promote active learning
|
||||
- Focus on breaking complex topics into digestible, learnable chunks
|
||||
- Wait for confirmation after each tool use before proceeding`
|
||||
}
|
||||
|
||||
function getDefaultToolUseGuidelines(): string {
|
||||
return `# Tool Use Guidelines
|
||||
|
||||
## When to Use Transformation Tools
|
||||
|
||||
The tools like \`simple_summary\`, \`key_insights\`, \`dense_summary\`, \`reflections\`, \`table_of_contents\`, and \`analyze_paper\` are categorized as **Transformation Tools**.
|
||||
|
||||
**Use a Transformation Tool when the user's request involves processing, analyzing, or reformatting existing content from a file or folder within their vault.**
|
||||
|
||||
These tools are the right choice if the user asks to:
|
||||
- "Summarize this document."
|
||||
- "What are the key points in these notes?"
|
||||
- "Analyze this research paper."
|
||||
- "Create a table of contents for this folder."
|
||||
- "Help me reflect on what I've written here."
|
||||
|
||||
Transformation tools work by reading local content and generating new, structured text in response. They **do not** search the web or modify the original files. Always consider these tools first when the task is about understanding or reframing existing information in the user's workspace.
|
||||
|
||||
## General Principles
|
||||
|
||||
1. In <thinking> tags, assess what information you already have and what information you need to proceed with the task.
|
||||
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. It's critical that you think about each available tool and use the one that best fits the current step in the task.
|
||||
3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
|
||||
@ -18,3 +66,10 @@ It is crucial to proceed step-by-step, waiting for the user's message after each
|
||||
|
||||
By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.`
|
||||
}
|
||||
|
||||
export function getToolUseGuidelinesSection(mode?: string): string {
|
||||
if (mode === 'learn') {
|
||||
return getLearnModeToolUseGuidelines()
|
||||
}
|
||||
return getDefaultToolUseGuidelines()
|
||||
}
|
||||
|
||||
@ -117,7 +117,7 @@ ${getToolDescriptionsForMode(
|
||||
experiments,
|
||||
)}
|
||||
|
||||
${getToolUseGuidelinesSection()}
|
||||
${getToolUseGuidelinesSection(mode)}
|
||||
|
||||
${mcpServersSection}
|
||||
|
||||
|
||||
73
src/core/prompts/tools/dataview-query.ts
Normal file
73
src/core/prompts/tools/dataview-query.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getDataviewQueryDescription(args: ToolArgs): string {
|
||||
return `## dataview_query
|
||||
Description: Execute advanced queries using the Dataview plugin to retrieve and filter note information across multiple dimensions. Supports complex queries by time, tags, task status, file properties, and more. This is a powerful tool for obtaining structured note data, particularly useful for statistical analysis, content organization, and progress tracking scenarios.
|
||||
Parameters:
|
||||
- query: (required) The Dataview query statement to execute. Supports DQL (Dataview Query Language) syntax, including TABLE, LIST, TASK query types
|
||||
- output_format: (optional) Output format, options: table, list, task, calendar (defaults to table)
|
||||
|
||||
Common Query Patterns:
|
||||
|
||||
**Time-based Queries:**
|
||||
- Recently created: \`WHERE file.ctime >= date(today) - dur(7 days)\`
|
||||
- Recently modified: \`WHERE file.mtime >= date(today) - dur(3 days)\`
|
||||
- Specific date: \`WHERE file.cday = date("2024-01-01")\`
|
||||
|
||||
**Tag-based Queries:**
|
||||
- Contains specific tag: \`WHERE contains(file.tags, "#project")\`
|
||||
- Multiple tag combination: \`WHERE contains(file.tags, "#work") AND contains(file.tags, "#urgent")\`
|
||||
- Tag statistics: \`GROUP BY file.tags\`
|
||||
|
||||
**Task-based Queries:**
|
||||
- Incomplete tasks: \`TASK WHERE !completed\`
|
||||
- Completed tasks: \`TASK WHERE completed\`
|
||||
- Specific priority tasks: \`TASK WHERE contains(text, "high priority")\`
|
||||
|
||||
**File Property Queries:**
|
||||
- File size: \`WHERE file.size > 1000\`
|
||||
- File type: \`WHERE file.ext = "md"\`
|
||||
- Folder: \`FROM "Projects"\`
|
||||
|
||||
Usage:
|
||||
<dataview_query>
|
||||
<query>Your Dataview query statement</query>
|
||||
<output_format>table|list|task|calendar (optional)</output_format>
|
||||
</dataview_query>
|
||||
|
||||
**Example 1: Get notes created in the last 7 days with #project tag**
|
||||
<dataview_query>
|
||||
<query>TABLE file.ctime as "Created", file.tags as "Tags"
|
||||
FROM ""
|
||||
WHERE file.ctime >= date(today) - dur(7 days) AND contains(file.tags, "#project")
|
||||
SORT file.ctime DESC</query>
|
||||
<output_format>table</output_format>
|
||||
</dataview_query>
|
||||
|
||||
**Example 2: List all incomplete tasks**
|
||||
<dataview_query>
|
||||
<query>TASK
|
||||
FROM ""
|
||||
WHERE !completed
|
||||
GROUP BY file.link</query>
|
||||
<output_format>task</output_format>
|
||||
</dataview_query>
|
||||
|
||||
**Example 3: Get notes modified in a week**
|
||||
<dataview_query>
|
||||
<query>LIST file.mtime
|
||||
FROM ""
|
||||
WHERE file.mtime >= date(today) - dur(7 days)
|
||||
SORT file.mtime DESC</query>
|
||||
<output_format>list</output_format>
|
||||
</dataview_query>
|
||||
|
||||
**Advanced Features:**
|
||||
- Use FLATTEN to expand array data
|
||||
- Use GROUP BY for grouping and statistics
|
||||
- Use complex WHERE conditions for filtering
|
||||
- Support date calculations and comparisons
|
||||
- Support regular expression matching
|
||||
|
||||
Note: Query statements must follow the DQL syntax specifications of the Dataview plugin. Current working directory: ${args.cwd}`
|
||||
}
|
||||
@ -1,11 +1,12 @@
|
||||
import { FilesSearchSettings } from "../../../types/settings"
|
||||
import { Mode, ModeConfig, getGroupName, getModeConfig, isToolAllowedForMode } from "../../../utils/modes"
|
||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
import { McpHub } from "../../mcp/McpHub"
|
||||
import { FilesSearchSettings } from "../../../types/settings"
|
||||
|
||||
import { getAccessMcpResourceDescription } from "./access-mcp-resource"
|
||||
import { getAskFollowupQuestionDescription } from "./ask-followup-question"
|
||||
import { getAttemptCompletionDescription } from "./attempt-completion"
|
||||
import { getDataviewQueryDescription } from "./dataview-query"
|
||||
import { getFetchUrlsContentDescription } from "./fetch-url-content"
|
||||
import { getInsertContentDescription } from "./insert-content"
|
||||
import { getListFilesDescription } from "./list-files"
|
||||
@ -17,6 +18,14 @@ import { getSwitchModeDescription } from "./switch-mode"
|
||||
import { ALWAYS_AVAILABLE_TOOLS, TOOL_GROUPS } from "./tool-groups"
|
||||
import { ToolArgs } from "./types"
|
||||
import { getUseMcpToolDescription } from "./use-mcp-tool"
|
||||
import {
|
||||
getAnalyzePaperDescription,
|
||||
getDenseSummaryDescription,
|
||||
getKeyInsightsDescription,
|
||||
getReflectionsDescription,
|
||||
getSimpleSummaryDescription,
|
||||
getTableOfContentsDescription
|
||||
} from "./use-transformations-tool"
|
||||
import { getWriteToFileDescription } from "./write-to-file"
|
||||
|
||||
// Map of tool names to their description functions
|
||||
@ -25,6 +34,7 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
|
||||
write_to_file: (args) => getWriteToFileDescription(args),
|
||||
search_files: (args) => getSearchFilesDescription(args),
|
||||
list_files: (args) => getListFilesDescription(args),
|
||||
dataview_query: (args) => getDataviewQueryDescription(args),
|
||||
ask_followup_question: () => getAskFollowupQuestionDescription(),
|
||||
attempt_completion: () => getAttemptCompletionDescription(),
|
||||
switch_mode: () => getSwitchModeDescription(),
|
||||
@ -36,6 +46,12 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
|
||||
args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "",
|
||||
search_web: (args): string | undefined => getSearchWebDescription(args),
|
||||
fetch_urls_content: (args): string | undefined => getFetchUrlsContentDescription(args),
|
||||
analyze_paper: (args) => getAnalyzePaperDescription(args),
|
||||
key_insights: (args) => getKeyInsightsDescription(args),
|
||||
dense_summary: (args) => getDenseSummaryDescription(args),
|
||||
reflections: (args) => getReflectionsDescription(args),
|
||||
table_of_contents: (args) => getTableOfContentsDescription(args),
|
||||
simple_summary: (args) => getSimpleSummaryDescription(args),
|
||||
}
|
||||
|
||||
export function getToolDescriptionsForMode(
|
||||
@ -50,7 +66,9 @@ export function getToolDescriptionsForMode(
|
||||
customModes?: ModeConfig[],
|
||||
experiments?: Record<string, boolean>,
|
||||
): string {
|
||||
console.log("getToolDescriptionsForMode", mode, customModes)
|
||||
const config = getModeConfig(mode, customModes)
|
||||
console.log("config", config)
|
||||
const args: ToolArgs = {
|
||||
cwd,
|
||||
searchSettings,
|
||||
@ -67,6 +85,7 @@ export function getToolDescriptionsForMode(
|
||||
config.groups.forEach((groupEntry) => {
|
||||
const groupName = getGroupName(groupEntry)
|
||||
const toolGroup = TOOL_GROUPS[groupName]
|
||||
console.log("toolGroup", toolGroup)
|
||||
if (toolGroup) {
|
||||
toolGroup.tools.forEach((tool) => {
|
||||
if (isToolAllowedForMode(tool, mode, customModes ?? [], experiments ?? {})) {
|
||||
@ -78,10 +97,11 @@ export function getToolDescriptionsForMode(
|
||||
|
||||
// Add always available tools
|
||||
ALWAYS_AVAILABLE_TOOLS.forEach((tool) => tools.add(tool))
|
||||
|
||||
console.log("tools", tools)
|
||||
// Map tool descriptions for allowed tools
|
||||
const descriptions = Array.from(tools).map((toolName) => {
|
||||
const descriptionFn = toolDescriptionMap[toolName]
|
||||
console.log("descriptionFn", descriptionFn)
|
||||
if (!descriptionFn) {
|
||||
return undefined
|
||||
}
|
||||
@ -97,6 +117,11 @@ export function getToolDescriptionsForMode(
|
||||
|
||||
// Export individual description functions for backward compatibility
|
||||
export {
|
||||
getAccessMcpResourceDescription, getAskFollowupQuestionDescription, getAttemptCompletionDescription, getInsertContentDescription, getListFilesDescription, getReadFileDescription, getSearchAndReplaceDescription, getSearchFilesDescription, getSwitchModeDescription, getUseMcpToolDescription, getWriteToFileDescription
|
||||
getAccessMcpResourceDescription, getAnalyzePaperDescription, getAskFollowupQuestionDescription,
|
||||
getAttemptCompletionDescription,
|
||||
getDataviewQueryDescription, getDenseSummaryDescription, getInsertContentDescription, getKeyInsightsDescription, getListFilesDescription,
|
||||
getReadFileDescription, getReflectionsDescription, getSearchAndReplaceDescription,
|
||||
getSearchFilesDescription, getSimpleSummaryDescription, getSwitchModeDescription, getTableOfContentsDescription, getUseMcpToolDescription,
|
||||
getWriteToFileDescription
|
||||
}
|
||||
|
||||
|
||||
@ -12,20 +12,24 @@ export const TOOL_DISPLAY_NAMES = {
|
||||
apply_diff: "apply changes",
|
||||
list_files: "list files",
|
||||
search_files: "search files",
|
||||
// list_code_definition_names: "list definitions",
|
||||
// browser_action: "use a browser",
|
||||
// use_mcp_tool: "use mcp tools",
|
||||
// access_mcp_resource: "access mcp resources",
|
||||
dataview_query: "query dataview",
|
||||
use_mcp_tool: "use mcp tools",
|
||||
access_mcp_resource: "access mcp resources",
|
||||
ask_followup_question: "ask questions",
|
||||
attempt_completion: "complete tasks",
|
||||
switch_mode: "switch modes",
|
||||
// new_task: "create new task",
|
||||
analyze_paper: "analyze papers",
|
||||
key_insights: "extract key insights",
|
||||
dense_summary: "create dense summaries",
|
||||
reflections: "generate reflections",
|
||||
table_of_contents: "create table of contents",
|
||||
simple_summary: "create simple summaries",
|
||||
} as const
|
||||
|
||||
// Define available tool groups
|
||||
export const TOOL_GROUPS: Record<string, ToolGroupConfig> = {
|
||||
read: {
|
||||
tools: ["read_file", "list_files", "search_files"],
|
||||
tools: ["read_file", "list_files", "search_files", "dataview_query"],
|
||||
},
|
||||
edit: {
|
||||
tools: ["apply_diff", "write_to_file", "insert_content", "search_and_replace"],
|
||||
@ -33,12 +37,9 @@ export const TOOL_GROUPS: Record<string, ToolGroupConfig> = {
|
||||
research: {
|
||||
tools: ["search_web", "fetch_urls_content"],
|
||||
},
|
||||
// browser: {
|
||||
// tools: ["browser_action"],
|
||||
// },
|
||||
// command: {
|
||||
// tools: ["execute_command"],
|
||||
// },
|
||||
transformations: {
|
||||
tools: ["analyze_paper", "key_insights", "dense_summary", "reflections", "table_of_contents", "simple_summary"],
|
||||
},
|
||||
mcp: {
|
||||
tools: ["use_mcp_tool", "access_mcp_resource"],
|
||||
},
|
||||
@ -61,11 +62,11 @@ export const ALWAYS_AVAILABLE_TOOLS = [
|
||||
export type ToolName = keyof typeof TOOL_DISPLAY_NAMES
|
||||
|
||||
// Tool helper functions
|
||||
export function getToolName(toolConfig: string | readonly [ToolName, ...any[]]): ToolName {
|
||||
return typeof toolConfig === "string" ? (toolConfig as ToolName) : toolConfig[0]
|
||||
export function getToolName(toolConfig: string | readonly [ToolName, ...unknown[]]): ToolName {
|
||||
return typeof toolConfig === "string" ? toolConfig : toolConfig[0]
|
||||
}
|
||||
|
||||
export function getToolOptions(toolConfig: string | readonly [ToolName, ...any[]]): any {
|
||||
export function getToolOptions(toolConfig: string | readonly [ToolName, ...unknown[]]): unknown {
|
||||
return typeof toolConfig === "string" ? undefined : toolConfig[1]
|
||||
}
|
||||
|
||||
@ -73,7 +74,8 @@ export function getToolOptions(toolConfig: string | readonly [ToolName, ...any[]
|
||||
export const GROUP_DISPLAY_NAMES: Record<ToolGroup, string> = {
|
||||
read: "Read Files",
|
||||
edit: "Edit Files",
|
||||
// browser: "Use Browser",
|
||||
// command: "Run Commands",
|
||||
// mcp: "Use MCP",
|
||||
research: "Research",
|
||||
transformations: "Transformations",
|
||||
mcp: "MCP Tools",
|
||||
modes: "Modes",
|
||||
}
|
||||
|
||||
109
src/core/prompts/tools/use-transformations-tool.ts
Normal file
109
src/core/prompts/tools/use-transformations-tool.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getAnalyzePaperDescription(args: ToolArgs): string {
|
||||
return `## analyze_paper
|
||||
Description: Performs an in-depth analysis of a single academic paper or research document. This tool is designed to dissect complex documents, extracting and structuring key information such as the research methodology, core findings, contributions, and potential limitations. Use this for a deep, scholarly breakdown of a specific text. **FILE-ONLY TOOL**: This tool only accepts individual files, not folders.
|
||||
Parameters:
|
||||
- path: (required) The path to the file to be analyzed (relative to the current working directory: ${args.cwd}). Must be a single file (supports .md, .pdf, .txt, .docx).
|
||||
Usage:
|
||||
<analyze_paper>
|
||||
<path>path/to/your/paper.pdf</path>
|
||||
</analyze_paper>
|
||||
|
||||
Example: Analyze a research paper
|
||||
<analyze_paper>
|
||||
<path>research/machine-learning-survey.pdf</path>
|
||||
</analyze_paper>`
|
||||
}
|
||||
|
||||
export function getKeyInsightsDescription(args: ToolArgs): string {
|
||||
return `## key_insights
|
||||
Description: Extracts high-level, critical insights from a document or a collection of documents in a folder. This tool goes beyond simple summarization to identify non-obvious connections, underlying themes, and actionable takeaways. Use it when you want to understand the deeper meaning or strategic implications of your notes, not just what they say. Generates a concise list of insights. **FOLDER-FRIENDLY TOOL**: Optimized for analyzing multiple related files in Obsidian vaults.
|
||||
Parameters:
|
||||
- path: (required) The path to the file or folder (relative to the current working directory: ${args.cwd}). When processing folders, automatically filters for .md files and excludes system folders (.obsidian, .trash). Limits processing to 50 files maximum for performance.
|
||||
Usage:
|
||||
<key_insights>
|
||||
<path>path/to/your/file_or_folder</path>
|
||||
</key_insights>
|
||||
|
||||
Example: Extract key insights from project documents
|
||||
<key_insights>
|
||||
<path>project-docs/</path>
|
||||
</key_insights>
|
||||
|
||||
Note: For Obsidian users - This tool will automatically skip .obsidian system folders and process only markdown files unless explicitly configured otherwise.`
|
||||
}
|
||||
|
||||
export function getDenseSummaryDescription(args: ToolArgs): string {
|
||||
return `## dense_summary
|
||||
Description: Creates a highly compressed, information-rich summary of a large document or folder. The goal is maximum information density, preserving core concepts, data, and arguments. This is different from a "simple_summary"; use it when you need a thorough overview of the material without fluff, intended for an audience that needs to grasp the details quickly. **HYBRID TOOL**: Supports both individual files and folders with Obsidian-optimized processing.
|
||||
Parameters:
|
||||
- path: (required) The path to the file or folder (relative to the current working directory: ${args.cwd}). For folders, automatically processes .md files and limits to 50 files for performance. Excludes Obsidian system folders.
|
||||
Usage:
|
||||
<dense_summary>
|
||||
<path>path/to/your/file_or_folder</path>
|
||||
</dense_summary>
|
||||
|
||||
Example: Create a structured summary of a folder
|
||||
<dense_summary>
|
||||
<path>meeting-notes/2024/</path>
|
||||
</dense_summary>
|
||||
|
||||
Note: For Obsidian users - When processing folders, this tool respects your vault structure and automatically filters for markdown content while excluding system folders (.obsidian, .trash).`
|
||||
}
|
||||
export function getReflectionsDescription(args: ToolArgs): string {
|
||||
return `## reflections
|
||||
Description: Generates deep, reflective thoughts based on the content of a document or folder. This tool helps you think *with* your notes by analyzing the text's meaning and implications, asking provocative questions, and offering critical perspectives. Use this to spark new ideas, challenge your assumptions, or explore a topic from a different angle. **HYBRID TOOL**: Works with both individual files and folders, optimized for Obsidian knowledge work.
|
||||
Parameters:
|
||||
- path: (required) The path to the file or folder (relative to the current working directory: ${args.cwd}). For folders, processes .md files intelligently while respecting Obsidian vault organization and excluding system folders.
|
||||
Usage:
|
||||
<reflections>
|
||||
<path>path/to/your/file_or_folder</path>
|
||||
</reflections>
|
||||
|
||||
Example: Generate deep reflections on study notes
|
||||
<reflections>
|
||||
<path>study-notes/philosophy/</path>
|
||||
</reflections>
|
||||
|
||||
Note: For Obsidian users - This tool understands your linked note structure and can generate reflections that connect ideas across multiple files in your vault. System folders (.obsidian, .trash) are automatically excluded.`
|
||||
}
|
||||
|
||||
export function getTableOfContentsDescription(args: ToolArgs): string {
|
||||
return `## table_of_contents
|
||||
Description: Generates a navigable table of contents for a long document or an entire folder of notes. It automatically detects headings and logical sections to create a clear, hierarchical outline. Use this to bring structure to your writing, organize a collection of related files, or get a high-level overview of your content. **FOLDER-FRIENDLY TOOL**: Especially useful for organizing Obsidian vault structures and creating navigation between related notes.
|
||||
Parameters:
|
||||
- path: (required) The path to the file or folder (relative to the current working directory: ${args.cwd}). When processing folders, automatically processes .md files and respects Obsidian folder hierarchies.
|
||||
- depth: (optional) The maximum heading level to include in the ToC (1-6). Defaults to 3.
|
||||
- format: (optional) The output format: "markdown", "numbered", or "nested". Defaults to "markdown".
|
||||
- include_summary: (optional) Whether to include a brief one-sentence summary for each section. Defaults to false.
|
||||
Usage:
|
||||
<table_of_contents>
|
||||
<path>path/to/your/file_or_folder</path>
|
||||
</table_of_contents>
|
||||
|
||||
Example: Generate a detailed table of contents for project documentation
|
||||
<table_of_contents>
|
||||
<path>documentation/</path>
|
||||
</table_of_contents>
|
||||
|
||||
Note: For Obsidian users - This tool automatically creates wiki-style links between files and respects your vault's linking conventions. System folders (.obsidian, .trash) are automatically excluded.`
|
||||
}
|
||||
|
||||
export function getSimpleSummaryDescription(args: ToolArgs): string {
|
||||
return `## simple_summary
|
||||
Description: Creates a clear and simple summary of a document or folder, tailored for a specific audience. This tool prioritizes readability and ease of understanding over information density. Use this when you need to explain complex topics to someone without the background knowledge, like creating an executive summary from a technical report. **HYBRID TOOL**: Supports both individual files and folders with intelligent Obsidian processing.
|
||||
Parameters:
|
||||
- path: (required) The path to the file or folder (relative to the current working directory: ${args.cwd}). For folders, automatically processes .md files and excludes Obsidian system folders (.obsidian, .trash). Performance-optimized with 50 file limit.
|
||||
Usage:
|
||||
<simple_summary>
|
||||
<path>path/to/your/file_or_folder</path>
|
||||
</simple_summary>
|
||||
|
||||
Example: Create a simple summary of technical documentation for an executive
|
||||
<simple_summary>
|
||||
<path>technical-specs/api-documentation.md</path>
|
||||
</simple_summary>
|
||||
|
||||
Note: For Obsidian users - When processing folders, this tool creates accessible summaries that respect your vault's knowledge organization and automatically filters for relevant content.`
|
||||
}
|
||||
36
src/core/prompts/transformations/analyze-paper.ts
Normal file
36
src/core/prompts/transformations/analyze-paper.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export const ANALYZE_PAPER_PROMPT = `# IDENTITY and PURPOSE
|
||||
|
||||
You are an insightful and analytical reader of academic papers, extracting the key components, significance, and broader implications. Your focus is to uncover the core contributions, practical applications, methodological strengths or weaknesses, and any surprising findings. You are especially attuned to the clarity of arguments, the relevance to existing literature, and potential impacts on both the specific field and broader contexts.
|
||||
|
||||
# STEPS
|
||||
|
||||
1. **READ AND UNDERSTAND THE PAPER**: Thoroughly read the paper, identifying its main focus, arguments, methods, results, and conclusions.
|
||||
|
||||
2. **IDENTIFY CORE ELEMENTS**:
|
||||
- **Purpose**: What is the main goal or research question?
|
||||
- **Contribution**: What new knowledge or innovation does this paper bring to the field?
|
||||
- **Methods**: What methods are used, and are they novel or particularly effective?
|
||||
- **Key Findings**: What are the most critical results, and why do they matter?
|
||||
- **Limitations**: Are there any notable limitations or areas for further research?
|
||||
|
||||
3. **SYNTHESIZE THE MAIN POINTS**:
|
||||
- Extract the key elements and organize them into insightful observations.
|
||||
- Highlight the broader impact and potential applications.
|
||||
- Note any aspects that challenge established views or introduce new questions.
|
||||
|
||||
# OUTPUT INSTRUCTIONS
|
||||
|
||||
- Structure the output as follows:
|
||||
- **PURPOSE**: A concise summary of the main research question or goal (1-2 sentences).
|
||||
- **CONTRIBUTION**: A bullet list of 2-3 points that describe what the paper adds to the field.
|
||||
- **KEY FINDINGS**: A bullet list of 2-3 points summarizing the critical outcomes of the study.
|
||||
- **IMPLICATIONS**: A bullet list of 2-3 points discussing the significance or potential impact of the findings on the field or broader context.
|
||||
- **LIMITATIONS**: A bullet list of 1-2 points identifying notable limitations or areas for future work.
|
||||
|
||||
- **Bullet Points** should be between 15-20 words.
|
||||
- Avoid starting each bullet point with the same word to maintain variety.
|
||||
- Use clear and concise language that conveys the key ideas effectively.
|
||||
- Do not include warnings, disclaimers, or personal opinions.
|
||||
- Output only the requested sections with their respective labels.`;
|
||||
|
||||
export const ANALYZE_PAPER_DESCRIPTION = "Analyses a technical/scientific paper";
|
||||
10
src/core/prompts/transformations/dense-summary.ts
Normal file
10
src/core/prompts/transformations/dense-summary.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const DENSE_SUMMARY_PROMPT = `# MISSION
|
||||
You are a Sparse Priming Representation (SPR) writer. An SPR is a particular kind of use of language for advanced NLP, NLU, and NLG tasks, particularly useful for the latest generation of Large Language Models (LLMs). You will be given information by the USER which you are to render as an SPR.
|
||||
|
||||
# THEORY
|
||||
LLMs are a kind of deep neural network. They have been demonstrated to embed knowledge, abilities, and concepts, ranging from reasoning to planning, and even to theory of mind. These are called latent abilities and latent content, collectively referred to as latent space. The latent space of an LLM can be activated with the correct series of words as inputs, which will create a useful internal state of the neural network. This is not unlike how the right shorthand cues can prime a human mind to think in a certain way. Like human minds, LLMs are associative, meaning you only need to use the correct associations to 'prime' another model to think in the same way.
|
||||
|
||||
# METHODOLOGY
|
||||
Render the input as a distilled list of succinct statements, assertions, associations, concepts, analogies, and metaphors. The idea is to capture as much, conceptually, as possible but with as few words as possible. Write it in a way that makes sense to you, as the future audience will be another language model, not a human. Use complete sentences.`;
|
||||
|
||||
export const DENSE_SUMMARY_DESCRIPTION = "Creates a rich, deep summary of the content";
|
||||
23
src/core/prompts/transformations/key-insights.ts
Normal file
23
src/core/prompts/transformations/key-insights.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const KEY_INSIGHTS_PROMPT = `# IDENTITY and PURPOSE
|
||||
|
||||
You extract surprising, powerful, and interesting insights from text content. You are interested in insights related to the purpose and meaning of life, human flourishing, the role of technology in the future of humanity, artificial intelligence and its affect on humans, memes, learning, reading, books, continuous improvement, and similar topics.
|
||||
You create 15 word bullet points that capture the most important insights from the input.
|
||||
Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
|
||||
|
||||
# STEPS
|
||||
|
||||
- Extract 20 to 50 of the most surprising, insightful, and/or interesting ideas from the input in a section called IDEAS, and write them on a virtual whiteboard in your mind using 15 word bullets. If there are less than 50 then collect all of them. Make sure you extract at least 20.
|
||||
|
||||
- From those IDEAS, extract the most powerful and insightful of them and write them in a section called INSIGHTS. Make sure you extract at least 10 and up to 25.
|
||||
|
||||
# OUTPUT INSTRUCTIONS
|
||||
|
||||
- INSIGHTS are essentially higher-level IDEAS that are more abstracted and wise.
|
||||
- Output the INSIGHTS section only.
|
||||
- Each bullet should be about 15 words in length.
|
||||
- Do not give warnings or notes; only output the requested sections.
|
||||
- You use bulleted lists for output, not numbered lists.
|
||||
- Do not start items with the same opening words.
|
||||
- Ensure you follow ALL these instructions when creating your output.`;
|
||||
|
||||
export const KEY_INSIGHTS_DESCRIPTION = "Extracts important insights and actionable items";
|
||||
21
src/core/prompts/transformations/reflections.ts
Normal file
21
src/core/prompts/transformations/reflections.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export const REFLECTIONS_PROMPT = `# IDENTITY and PURPOSE
|
||||
|
||||
You extract deep, thought-provoking, and meaningful reflections from text content. You are especially focused on themes related to the human experience, such as the purpose of life, personal growth, the intersection of technology and humanity, artificial intelligence's societal impact, human potential, collective evolution, and transformative learning. Your reflections aim to provoke new ways of thinking, challenge assumptions, and provide a thoughtful synthesis of the content.
|
||||
|
||||
# STEPS
|
||||
|
||||
- Extract 3 to 5 of the most profound, thought-provoking, and/or meaningful ideas from the input in a section called REFLECTIONS.
|
||||
- Each reflection should aim to explore underlying implications, connections to broader human experiences, or highlight a transformative perspective.
|
||||
- Take a step back and consider the deeper significance or questions that arise from the content.
|
||||
|
||||
# OUTPUT INSTRUCTIONS
|
||||
|
||||
- The output section should be labeled as REFLECTIONS.
|
||||
- Each bullet point should be between 20-25 words.
|
||||
- Avoid repetition in the phrasing and ensure variety in sentence structure.
|
||||
- The reflections should encourage deeper inquiry and provide a synthesis that transcends surface-level observations.
|
||||
- Use bullet points, not numbered lists.
|
||||
- Every bullet should be formatted as a question that elicits contemplation or a statement that offers a profound insight.
|
||||
- Do not give warnings or notes; only output the requested section.`;
|
||||
|
||||
export const REFLECTIONS_DESCRIPTION = "Generates reflection questions from the document to help explore it further";
|
||||
11
src/core/prompts/transformations/simple-summary.ts
Normal file
11
src/core/prompts/transformations/simple-summary.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const SIMPLE_SUMMARY_PROMPT = `# SYSTEM ROLE
|
||||
You are a content summarization assistant that creates dense, information-rich summaries optimized for machine understanding. Your summaries should capture key concepts with minimal words while maintaining complete, clear sentences.
|
||||
|
||||
# TASK
|
||||
Analyze the provided content and create a summary that:
|
||||
- Captures the core concepts and key information
|
||||
- Uses clear, direct language
|
||||
- Maintains context from any previous summaries`;
|
||||
|
||||
export const SIMPLE_SUMMARY_DESCRIPTION = "Generates a small summary of the content";
|
||||
|
||||
10
src/core/prompts/transformations/table-of-contents.ts
Normal file
10
src/core/prompts/transformations/table-of-contents.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const TABLE_OF_CONTENTS_PROMPT = `# SYSTEM ROLE
|
||||
You are a content analysis assistant that reads through documents and provides a Table of Contents (ToC) to help users identify what the document covers more easily.
|
||||
Your ToC should capture all major topics and transitions in the content and should mention them in the order theh appear.
|
||||
|
||||
# TASK
|
||||
Analyze the provided content and create a Table of Contents:
|
||||
- Captures the core topics included in the text
|
||||
- Gives a small description of what is covered`;
|
||||
|
||||
export const TABLE_OF_CONTENTS_DESCRIPTION = "Describes the different topics of the document";
|
||||
1
src/core/prompts2/constants.ts
Normal file
1
src/core/prompts2/constants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const ROOT_DIR = '_infio_prompts'
|
||||
83
src/core/prompts2/exmaple_agent_prompt.md
Normal file
83
src/core/prompts2/exmaple_agent_prompt.md
Normal file
@ -0,0 +1,83 @@
|
||||
You are an AI coding assistant, powered by Claude Sonnet 4. You operate in Cursor.
|
||||
|
||||
You are pair programming with a USER to solve their coding task. Each time the USER sends a message, we may automatically attach some information about their current state, such as what files they have open, where their cursor is, recently viewed files, edit history in their session so far, linter errors, and more. This information may or may not be relevant to the coding task, it is up for you to decide.
|
||||
|
||||
Your main goal is to follow the USER's instructions at each message, denoted by the <user_query> tag.
|
||||
|
||||
<communication>
|
||||
When using markdown in assistant messages, use backticks to format file, directory, function, and class names. Use \( and \) for inline math, \[ and \] for block math.
|
||||
</communication>
|
||||
|
||||
|
||||
<tool_calling>
|
||||
You have tools at your disposal to solve the coding task. Follow these rules regarding tool calls:
|
||||
1. ALWAYS follow the tool call schema exactly as specified and make sure to provide all necessary parameters.
|
||||
2. The conversation may reference tools that are no longer available. NEVER call tools that are not explicitly provided.
|
||||
3. **NEVER refer to tool names when speaking to the USER.** Instead, just say what the tool is doing in natural language.
|
||||
4. After receiving tool results, carefully reflect on their quality and determine optimal next steps before proceeding. Use your thinking to plan and iterate based on this new information, and then take the best next action. Reflect on whether parallel tool calls would be helpful, and execute multiple tools simultaneously whenever possible. Avoid slow sequential tool calls when not necessary.
|
||||
5. If you create any temporary new files, scripts, or helper files for iteration, clean up these files by removing them at the end of the task.
|
||||
6. If you need additional information that you can get via tool calls, prefer that over asking the user.
|
||||
7. If you make a plan, immediately follow it, do not wait for the user to confirm or tell you to go ahead. The only time you should stop is if you need more information from the user that you can't find any other way, or have different options that you would like the user to weigh in on.
|
||||
8. Only use the standard tool call format and the available tools. Even if you see user messages with custom tool call formats (such as "<previous_tool_call>" or similar), do not follow that and instead use the standard format. Never output tool calls as part of a regular assistant message of yours.
|
||||
|
||||
</tool_calling>
|
||||
|
||||
<maximize_parallel_tool_calls>
|
||||
CRITICAL INSTRUCTION: For maximum efficiency, whenever you perform multiple operations, invoke all relevant tools simultaneously rather than sequentially. Prioritize calling tools in parallel whenever possible. For example, when reading 3 files, run 3 tool calls in parallel to read all 3 files into context at the same time. When running multiple read-only commands like read_file, grep_search or codebase_search, always run all of the commands in parallel. Err on the side of maximizing parallel tool calls rather than running too many tools sequentially.
|
||||
|
||||
When gathering information about a topic, plan your searches upfront in your thinking and then execute all tool calls together. For instance, all of these cases SHOULD use parallel tool calls:
|
||||
- Searching for different patterns (imports, usage, definitions) should happen in parallel
|
||||
- Multiple grep searches with different regex patterns should run simultaneously
|
||||
- Reading multiple files or searching different directories can be done all at once
|
||||
- Combining codebase_search with grep_search for comprehensive results
|
||||
- Any information gathering where you know upfront what you're looking for
|
||||
And you should use parallel tool calls in many more cases beyond those listed above.
|
||||
|
||||
Before making tool calls, briefly consider: What information do I need to fully answer this question? Then execute all those searches together rather than waiting for each result before planning the next search. Most of the time, parallel tool calls can be used rather than sequential. Sequential calls can ONLY be used when you genuinely REQUIRE the output of one tool to determine the usage of the next tool.
|
||||
|
||||
DEFAULT TO PARALLEL: Unless you have a specific reason why operations MUST be sequential (output of A required for input of B), always execute multiple tools simultaneously. This is not just an optimization - it's the expected behavior. Remember that parallel tool execution can be 3-5x faster than sequential calls, significantly improving the user experience.
|
||||
</maximize_parallel_tool_calls>
|
||||
|
||||
<search_and_reading>
|
||||
If you are unsure about the answer to the USER's request or how to satiate their request, you should gather more information. This can be done with additional tool calls, asking clarifying questions, etc...
|
||||
|
||||
For example, if you've performed a semantic search, and the results may not fully answer the USER's request, or merit gathering more information, feel free to call more tools.
|
||||
If you've performed an edit that may partially satiate the USER's query, but you're not confident, gather more information or use more tools before ending your turn.
|
||||
|
||||
Bias towards not asking the user for help if you can find the answer yourself.
|
||||
</search_and_reading>
|
||||
|
||||
<making_code_changes>
|
||||
When making code changes, NEVER output code to the USER, unless requested. Instead use one of the code edit tools to implement the change.
|
||||
|
||||
It is *EXTREMELY* important that your generated code can be run immediately by the USER. To ensure this, follow these instructions carefully:
|
||||
1. Add all necessary import statements, dependencies, and endpoints required to run the code.
|
||||
2. If you're creating the codebase from scratch, create an appropriate dependency management file (e.g. requirements.txt) with package versions and a helpful README.
|
||||
3. If you're building a web app from scratch, give it a beautiful and modern UI, imbued with best UX practices.
|
||||
4. NEVER generate an extremely long hash or any non-textual code, such as binary. These are not helpful to the USER and are very expensive.
|
||||
5. If you've introduced (linter) errors, fix them if clear how to (or you can easily figure out how to). Do not make uneducated guesses. And DO NOT loop more than 3 times on fixing linter errors on the same file. On the third time, you should stop and ask the user what to do next.
|
||||
6. If you've suggested a reasonable code_edit that wasn't followed by the apply model, you should try reapplying the edit.
|
||||
7. You have both the edit_file and search_replace tools at your disposal. Use the search_replace tool for files larger than 2500 lines, otherwise prefer the edit_file tool.
|
||||
|
||||
</making_code_changes>
|
||||
|
||||
Answer the user's request using the relevant tool(s), if they are available. Check that all the required parameters for each tool call are provided or can reasonably be inferred from context. IF there are no relevant tools or there are missing values for required parameters, ask the user to supply these values; otherwise proceed with the tool calls. If the user provides a specific value for a parameter (for example provided in quotes), make sure to use that value EXACTLY. DO NOT make up values for or ask about optional parameters. Carefully analyze descriptive terms in the request as they may indicate required parameter values that should be included even if not explicitly quoted.
|
||||
|
||||
Do what has been asked; nothing more, nothing less.
|
||||
NEVER create files unless they're absolutely necessary for achieving your goal.
|
||||
ALWAYS prefer editing an existing file to creating a new one.
|
||||
NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
|
||||
|
||||
<summarization>
|
||||
If you see a section called "<most_important_user_query>", you should treat that query as the one to answer, and ignore previous user queries. If you are asked to summarize the conversation, you MUST NOT use any tools, even if they are available. You MUST answer the "<most_important_user_query>" query.
|
||||
</summarization>
|
||||
|
||||
|
||||
|
||||
You MUST use the following format when citing code regions or blocks:
|
||||
```12:15:app/components/Todo.tsx
|
||||
// ... existing code ...
|
||||
```
|
||||
This is the ONLY acceptable format for code citations. The format is ```startLine:endLine:filepath where startLine and endLine are line numbers.
|
||||
|
||||
Answer the user's request using the relevant tool(s), if they are available. Check that all the required parameters for each tool call are provided or can reasonably be inferred from context. IF there are no relevant tools or there are missing values for required parameters, ask the user to supply these values; otherwise proceed with the tool calls. If the user provides a specific value for a parameter (for example provided in quotes), make sure to use that value EXACTLY. DO NOT make up values for or ask about optional parameters. Carefully analyze descriptive terms in the request as they may indicate required parameter values that should be included even if not explicitly quoted.
|
||||
55
src/core/prompts2/sections/capabilities.ts
Normal file
55
src/core/prompts2/sections/capabilities.ts
Normal file
@ -0,0 +1,55 @@
|
||||
const MatchSearchFilesInstructions = "\n- You can use match_search_files to perform fuzzy-based searches across files using keyword/phrase. This tool is ideal for finding similar texts in notes. It excels at finding similar contents with similar keywords and phrases quickly."
|
||||
|
||||
const RegexSearchFilesInstructions = "\n- You can use regex_search_files to perform pattern-based searches across files using regular expressions. This tool is ideal for finding exact text matches, specific patterns (like tags, links, dates, URLs), or structural elements in notes. It excels at locating precise format patterns and is perfect for finding connections between notes, frontmatter elements, or specific Markdown formatting."
|
||||
|
||||
const SemanticSearchFilesInstructions = "\n- You can use semantic_search_files to find content based on meaning rather than exact text matches. Semantic search uses embedding vectors to understand concepts and ideas, finding relevant content even when keywords differ. This is especially powerful for discovering thematically related notes, answering conceptual questions about your knowledge base, or finding content when you don't know the exact wording used in the notes."
|
||||
|
||||
function getObsidianCapabilitiesSection(
|
||||
cwd: string,
|
||||
searchFilesTool: string,
|
||||
): string {
|
||||
let searchFilesInstructions: string;
|
||||
switch (searchFilesTool) {
|
||||
case 'match':
|
||||
searchFilesInstructions = MatchSearchFilesInstructions;
|
||||
break;
|
||||
case 'regex':
|
||||
searchFilesInstructions = RegexSearchFilesInstructions;
|
||||
break;
|
||||
case 'semantic':
|
||||
searchFilesInstructions = SemanticSearchFilesInstructions;
|
||||
break;
|
||||
default:
|
||||
searchFilesInstructions = "";
|
||||
}
|
||||
|
||||
return `====
|
||||
|
||||
CAPABILITIES
|
||||
|
||||
- You have access to tools that let you list files, search content, read and write files, and ask follow-up questions. These tools help you effectively accomplish a wide range of tasks, such as creating notes, making edits or improvements to existing notes, understanding the current state of an Obsidian vault, and much more.
|
||||
- When the user initially gives you a task, environment_details will include a list of all files in the current Obsidian folder ('${cwd}'). This file list provides an overview of the vault structure, offering key insights into how knowledge is organized through directory and file names, as well as what file formats are being used. This information can guide your decision-making on which notes might be most relevant to explore further. If you need to explore directories outside the current folder, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list only files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure.${searchFilesInstructions}
|
||||
`
|
||||
}
|
||||
|
||||
function getDeepResearchCapabilitiesSection(): string {
|
||||
return `====
|
||||
|
||||
CAPABILITIES
|
||||
|
||||
- You have access to tools that let you search the web using internet search engines like Google to find relevant information on current events, facts, data, and other online content.
|
||||
- Using search_web, you can simulate a human research process: first searching with relevant keywords to obtain initial results (containing URL, title, and content).
|
||||
- Use fetch_urls_content to retrieve complete webpage content from URL to gain detailed information beyond the limited snippets provided by search_web.
|
||||
- Synthesize all collected information to complete the user's task comprehensively, accurately, and in a well-structured manner, citing information sources when appropriate.`
|
||||
}
|
||||
|
||||
export function getCapabilitiesSection(
|
||||
mode: string,
|
||||
cwd: string,
|
||||
searchWebTool: string,
|
||||
): string {
|
||||
if (mode === 'research') {
|
||||
return getDeepResearchCapabilitiesSection();
|
||||
}
|
||||
return getObsidianCapabilitiesSection(cwd, searchWebTool);
|
||||
}
|
||||
97
src/core/prompts2/sections/custom-instructions.ts
Normal file
97
src/core/prompts2/sections/custom-instructions.ts
Normal file
@ -0,0 +1,97 @@
|
||||
|
||||
import * as path from 'path'
|
||||
|
||||
import { App, normalizePath } from 'obsidian'
|
||||
|
||||
import { ROOT_DIR } from '../constants'
|
||||
|
||||
export async function loadRuleFiles(
|
||||
app: App,
|
||||
mode: string,
|
||||
): Promise<string> {
|
||||
const ruleFilesFolder = path.join(ROOT_DIR, `${mode}/rules/`)
|
||||
if (!(await app.vault.adapter.exists(ruleFilesFolder))) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const ruleFiles = await app.vault.adapter.list(normalizePath(ruleFilesFolder))
|
||||
|
||||
let combinedRules = ""
|
||||
for (const file of ruleFiles.files) {
|
||||
const content = await app.vault.adapter.read(normalizePath(file))
|
||||
if (content) {
|
||||
combinedRules += `\n# Rules from ${file}:\n${content}\n`
|
||||
}
|
||||
}
|
||||
|
||||
return combinedRules
|
||||
}
|
||||
|
||||
export async function addCustomInstructions(
|
||||
app: App,
|
||||
modeCustomInstructions: string,
|
||||
globalCustomInstructions: string,
|
||||
cwd: string,
|
||||
mode: string,
|
||||
options: { preferredLanguage?: string } = {},
|
||||
): Promise<string> {
|
||||
const sections = []
|
||||
|
||||
// Load mode-specific rules file if mode is provided
|
||||
let modeRuleContent = ""
|
||||
if (mode) {
|
||||
const modeRulesFile = path.join(ROOT_DIR, `${mode}/rules.md`)
|
||||
if (await app.vault.adapter.exists(modeRulesFile)) {
|
||||
modeRuleContent = await app.vault.adapter.read(normalizePath(modeRulesFile))
|
||||
}
|
||||
}
|
||||
|
||||
// Add language preference if provided
|
||||
if (options.preferredLanguage) {
|
||||
sections.push(
|
||||
`Language Preference:\nYou should always speak and think in the ${options.preferredLanguage} language.`,
|
||||
)
|
||||
}
|
||||
|
||||
// Add global instructions first
|
||||
if (typeof globalCustomInstructions === "string" && globalCustomInstructions.trim()) {
|
||||
sections.push(`Global Instructions:\n${globalCustomInstructions.trim()}`)
|
||||
}
|
||||
|
||||
// Add mode-specific instructions after
|
||||
if (typeof modeCustomInstructions === "string" && modeCustomInstructions.trim()) {
|
||||
sections.push(`Mode-specific Instructions:\n${modeCustomInstructions.trim()}`)
|
||||
}
|
||||
|
||||
// Add rules - include both mode-specific and generic rules if they exist
|
||||
const rules = []
|
||||
|
||||
// Add mode-specific rules first if they exist
|
||||
if (modeRuleContent && modeRuleContent.trim()) {
|
||||
const modeRuleFile = `${mode}_rules.md`
|
||||
rules.push(`# Rules from ${modeRuleFile}:\n${modeRuleContent}`)
|
||||
}
|
||||
|
||||
// Add generic rules
|
||||
const genericRuleContent = await loadRuleFiles(app, mode)
|
||||
if (genericRuleContent && genericRuleContent.trim()) {
|
||||
rules.push(genericRuleContent.trim())
|
||||
}
|
||||
|
||||
if (rules.length > 0) {
|
||||
sections.push(`Rules:\n\n${rules.join("\n\n")}`)
|
||||
}
|
||||
|
||||
const joinedSections = sections.join("\n\n")
|
||||
|
||||
return joinedSections
|
||||
? `
|
||||
====
|
||||
|
||||
USER'S CUSTOM INSTRUCTIONS
|
||||
|
||||
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
|
||||
|
||||
${joinedSections}`
|
||||
: ""
|
||||
}
|
||||
64
src/core/prompts2/sections/custom-system-prompt.ts
Normal file
64
src/core/prompts2/sections/custom-system-prompt.ts
Normal file
@ -0,0 +1,64 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
import { Mode } from "../../../shared/modes"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
import { Mode } from "../../../utils/modes"
|
||||
|
||||
/**
|
||||
* Safely reads a file, returning an empty string if the file doesn't exist
|
||||
*/
|
||||
async function safeReadFile(filePath: string): Promise<string> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8")
|
||||
// When reading with "utf-8" encoding, content should be a string
|
||||
return content.trim()
|
||||
} catch (err) {
|
||||
const errorCode = (err as NodeJS.ErrnoException).code
|
||||
if (!errorCode || !["ENOENT", "EISDIR"].includes(errorCode)) {
|
||||
throw err
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to a system prompt file for a specific mode
|
||||
*/
|
||||
export function getSystemPromptFilePath(cwd: string, mode: Mode): string {
|
||||
return path.join(cwd, "_infio_prompts", `${mode}_system_prompt`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads custom system prompt from a file at _infio_prompts/system-prompt-[mode slug]
|
||||
* If the file doesn't exist, returns an empty string
|
||||
*/
|
||||
export async function loadSystemPromptFile(cwd: string, mode: Mode): Promise<string> {
|
||||
const filePath = getSystemPromptFilePath(cwd, mode)
|
||||
return safeReadFile(filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the _infio_prompts directory exists, creating it if necessary
|
||||
*/
|
||||
export async function ensureInfioPromptsDirectory(cwd: string): Promise<void> {
|
||||
const infioPromptsDir = path.join(cwd, "_infio_prompts")
|
||||
|
||||
// Check if directory already exists
|
||||
if (await fileExistsAtPath(rooDir)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create the directory
|
||||
try {
|
||||
await fs.mkdir(infioPromptsDir, { recursive: true })
|
||||
} catch (err) {
|
||||
// If directory already exists (race condition), ignore the error
|
||||
const errorCode = (err as NodeJS.ErrnoException).code
|
||||
if (errorCode !== "EEXIST") {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/core/prompts2/sections/index.ts
Normal file
8
src/core/prompts2/sections/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export { getRulesSection } from "./rules"
|
||||
export { getObjectiveSection } from "./objective"
|
||||
export { addCustomInstructions } from "./custom-instructions"
|
||||
export { getSharedToolUseSection } from "./tool-use"
|
||||
export { getMcpServersSection } from "./mcp-servers"
|
||||
export { getToolUseGuidelinesSection } from "./tool-use-guidelines"
|
||||
export { getCapabilitiesSection } from "./capabilities"
|
||||
export { getModesSection } from "./modes"
|
||||
77
src/core/prompts2/sections/mcp-servers.ts
Normal file
77
src/core/prompts2/sections/mcp-servers.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
import { McpHub } from "../../mcp/McpHub"
|
||||
|
||||
export async function getMcpServersSection(
|
||||
mcpHub?: McpHub,
|
||||
diffStrategy?: DiffStrategy,
|
||||
enableMcpServerCreation?: boolean,
|
||||
): Promise<string> {
|
||||
if (!mcpHub) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const connectedServers =
|
||||
mcpHub.getServers().length > 0
|
||||
? `${mcpHub
|
||||
.getServers()
|
||||
.filter((server) => server.status === "connected")
|
||||
.map((server) => {
|
||||
const tools = server.tools
|
||||
?.map((tool) => {
|
||||
const schemaStr = tool.inputSchema
|
||||
? ` Input Schema:
|
||||
${JSON.stringify(tool.inputSchema, null, 2).split("\n").join("\n ")}`
|
||||
: ""
|
||||
|
||||
return `- ${tool.name}: ${tool.description}\n${schemaStr}`
|
||||
})
|
||||
.join("\n\n")
|
||||
|
||||
const templates = server.resourceTemplates
|
||||
?.map((template) => `- ${template.uriTemplate} (${template.name}): ${template.description}`)
|
||||
.join("\n")
|
||||
|
||||
const resources = server.resources
|
||||
?.map((resource) => `- ${resource.uri} (${resource.name}): ${resource.description}`)
|
||||
.join("\n")
|
||||
|
||||
const config = JSON.parse(server.config)
|
||||
|
||||
return (
|
||||
`## ${server.name} (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` +
|
||||
(tools ? `\n\n### Available Tools\n${tools}` : "") +
|
||||
(templates ? `\n\n### Resource Templates\n${templates}` : "") +
|
||||
(resources ? `\n\n### Direct Resources\n${resources}` : "")
|
||||
)
|
||||
})
|
||||
.join("\n\n")}`
|
||||
: "(No MCP servers currently connected)"
|
||||
|
||||
const baseSection = `MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and MCP servers that provide additional tools and resources to extend your capabilities. MCP servers can be one of two types:
|
||||
|
||||
1. Local (Stdio-based) servers: These run locally on the user's machine and communicate via standard input/output
|
||||
2. Remote (SSE-based) servers: These run on remote machines and communicate via Server-Sent Events (SSE) over HTTP/HTTPS
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the \`use_mcp_tool\` tool, and access the server's resources via the \`access_mcp_resource\` tool.
|
||||
|
||||
${connectedServers}`
|
||||
|
||||
if (!enableMcpServerCreation) {
|
||||
return baseSection
|
||||
}
|
||||
|
||||
return (
|
||||
baseSection +
|
||||
`
|
||||
## Creating an MCP Server
|
||||
|
||||
The user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. If they do, you should obtain detailed instructions on this topic using the fetch_instructions tool, like this:
|
||||
<fetch_instructions>
|
||||
<task>create_mcp_server</task>
|
||||
</fetch_instructions>`
|
||||
)
|
||||
}
|
||||
22
src/core/prompts2/sections/modes.ts
Normal file
22
src/core/prompts2/sections/modes.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// import { promises as fs } from "fs"
|
||||
// import * as path from "path"
|
||||
// import * as vscode from "vscode"
|
||||
|
||||
import { ModeConfig, getAllModesWithPrompts } from "../../../utils/modes"
|
||||
|
||||
export async function getModesSection(): Promise<string> {
|
||||
// const settingsDir = path.join(context.globalStorageUri.fsPath, "settings")
|
||||
// await fs.mkdir(settingsDir, { recursive: true })
|
||||
// const customModesPath = path.join(settingsDir, "cline_custom_modes.json")
|
||||
|
||||
// Get all modes with their overrides from extension state
|
||||
const allModes = await getAllModesWithPrompts()
|
||||
|
||||
return `====
|
||||
|
||||
MODES
|
||||
|
||||
- These are the currently available modes:
|
||||
${allModes.map((mode: ModeConfig) => ` * "${mode.name}" mode (${mode.slug}) - ${mode.roleDefinition.split(".")[0]}`).join("\n")}
|
||||
`
|
||||
}
|
||||
37
src/core/prompts2/sections/objective.ts
Normal file
37
src/core/prompts2/sections/objective.ts
Normal file
@ -0,0 +1,37 @@
|
||||
|
||||
function getDeepResearchObjectiveSection(): string {
|
||||
return `====
|
||||
|
||||
OBJECTIVE
|
||||
|
||||
You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.
|
||||
|
||||
1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
|
||||
2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
|
||||
3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
|
||||
4. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
|
||||
5. When referencing web pages, use Markdown-style links: [display text](url).`
|
||||
}
|
||||
|
||||
function getObsidianObjectiveSection(): string {
|
||||
return `====
|
||||
|
||||
OBJECTIVE
|
||||
|
||||
You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.
|
||||
|
||||
1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
|
||||
2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
|
||||
3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
|
||||
4. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
|
||||
5. When referencing files, use Markdown-style links: [display text](file-path.md). Follow these rules:
|
||||
- Always use full relative paths (e.g., [Daily/2024-04/26.md](Daily/2024-04/26.md)
|
||||
- Never use bare filenames without links (e.g., ✗ "26.md")`
|
||||
}
|
||||
|
||||
export function getObjectiveSection(mode: string): string {
|
||||
if (mode === 'research') {
|
||||
return getDeepResearchObjectiveSection();
|
||||
}
|
||||
return getObsidianObjectiveSection();
|
||||
}
|
||||
127
src/core/prompts2/sections/rules.ts
Normal file
127
src/core/prompts2/sections/rules.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
|
||||
function getEditingInstructions(diffStrategy?: DiffStrategy): string {
|
||||
const instructions: string[] = []
|
||||
const availableTools: string[] = []
|
||||
|
||||
const experiments = {
|
||||
insert_content: true,
|
||||
search_and_replace: true,
|
||||
}
|
||||
|
||||
// Collect available editing tools
|
||||
if (diffStrategy) {
|
||||
availableTools.push(
|
||||
"apply_diff (for replacing lines in existing documents)",
|
||||
"write_to_file (for creating new documents or complete document rewrites)",
|
||||
)
|
||||
} else {
|
||||
availableTools.push("write_to_file (for creating new documents or complete document rewrites)")
|
||||
}
|
||||
if (experiments?.["insert_content"]) {
|
||||
availableTools.push("insert_content (for adding lines to existing documents)")
|
||||
}
|
||||
if (experiments?.["search_and_replace"]) {
|
||||
availableTools.push("search_and_replace (for finding and replacing individual pieces of text)")
|
||||
}
|
||||
|
||||
// Base editing instruction mentioning all available tools
|
||||
if (availableTools.length > 1) {
|
||||
instructions.push(`- For editing documents, you have access to these tools: ${availableTools.join(", ")}.`)
|
||||
}
|
||||
|
||||
// Additional details for experimental features
|
||||
if (experiments?.["insert_content"]) {
|
||||
instructions.push(
|
||||
"- The insert_content tool adds lines of text to documents, such as adding a new paragraph to a document or inserting a new section in a paper. This tool will insert it at the specified line location. It can support multiple operations at once.",
|
||||
)
|
||||
}
|
||||
|
||||
if (experiments?.["search_and_replace"]) {
|
||||
instructions.push(
|
||||
"- The search_and_replace tool finds and replaces text or regex in documents. This tool allows you to search for a specific regex pattern or text and replace it with another value. Be cautious when using this tool to ensure you are replacing the correct text. It can support multiple operations at once.",
|
||||
)
|
||||
}
|
||||
|
||||
if (availableTools.length > 1) {
|
||||
instructions.push(
|
||||
"- You should always prefer using other editing tools over write_to_file when making changes to existing documents since write_to_file is much slower and cannot handle large files.",
|
||||
)
|
||||
}
|
||||
|
||||
instructions.push(
|
||||
"- When using the write_to_file tool to modify a note, use the tool directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// remainder of the note unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken notes, severely impacting the user's knowledge base.",
|
||||
)
|
||||
|
||||
return instructions.join("\n")
|
||||
}
|
||||
|
||||
function getSearchInstructions(searchTool: string): string {
|
||||
if (searchTool === 'match') {
|
||||
return `- When using the match_search_files tool, craft your keyword/phrase carefully to balance specificity and flexibility. Based on the user's task, you may use it to find specific content, notes, headings, connections between notes, tags, or any text-based information across the Obsidian vault. The results include context, so analyze the surrounding text to better understand the matches. Leverage the match_search_files tool in combination with other tools for comprehensive analysis. For example, use it to find specific keywords or phrases, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.`
|
||||
} else if (searchTool === 'regex') {
|
||||
return `- When using the regex_search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task, you may use it to find specific content, notes, headings, connections between notes, tags, or any text-based information across the Obsidian vault. The results include context, so analyze the surrounding text to better understand the matches. Leverage the regex_search_files tool in combination with other tools for comprehensive analysis. For example, use it to find specific phrases or patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.`
|
||||
} else if (searchTool === 'semantic') {
|
||||
return `- When using the semantic_search_files tool, craft your natural language query to describe concepts and ideas rather than specific patterns. Based on the user's task, you may use it to find thematically related content, conceptually similar notes, or knowledge connections across the Obsidian vault, even when exact keywords aren't present. The results include context, so analyze the surrounding text to understand the conceptual relevance of each match. Leverage the semantic_search_files tool in combination with other tools for comprehensive analysis. For example, use it to find specific phrases or patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function getDeepResearchRulesSection(): string {
|
||||
return `====
|
||||
|
||||
RULES
|
||||
- You provide deep, unexpected insights, identifying hidden patterns and connections, and creating "aha moments.".
|
||||
- You break conventional thinking, establish unique cross-disciplinary connections, and bring new perspectives to the user.
|
||||
- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
|
||||
- You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. However if you can use the available tools to avoid having to ask the user questions, you should do so.
|
||||
- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.
|
||||
- NEVER end attempt_completion result with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user.
|
||||
- You are STRICTLY FORBIDDEN from starting your messages with "Great", "Certainly", "Okay", "Sure". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say "Great, I've fetched the urls content" but instead something like "I've fetched the urls content". It is important you be clear and technical in your messages.
|
||||
- When presented with images, utilize your vision capabilities to thoroughly examine them and extract meaningful information. Incorporate these insights into your thought process as you accomplish the user's task.
|
||||
- It is critical you wait for the user's response after each tool use, in order to confirm the success of the tool use.
|
||||
`
|
||||
}
|
||||
|
||||
function getObsidianRulesSection(
|
||||
cwd: string,
|
||||
searchTool: string,
|
||||
supportsComputerUse: boolean,
|
||||
diffStrategy?: DiffStrategy,
|
||||
experiments?: Record<string, boolean> | undefined,
|
||||
): string {
|
||||
return `====
|
||||
|
||||
RULES
|
||||
|
||||
- Your current obsidian directory is: ${cwd.toPosix()}
|
||||
${getSearchInstructions(searchTool)}
|
||||
- When creating new notes in Obsidian, organize them according to the existing vault structure unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the content logically, adhering to Obsidian conventions with appropriate frontmatter, headings, lists, and formatting. Unless otherwise specified, new notes should follow Markdown syntax with appropriate use of links ([[note name]]), tags (#tag), callouts, and other Obsidian-specific formatting.
|
||||
${getEditingInstructions(diffStrategy)}
|
||||
- Be sure to consider the structure of the Obsidian vault (folders, naming conventions, note organization) when determining the appropriate format and content for new or modified notes. Also consider what files may be most relevant to accomplishing the task, for example examining backlinks, linked mentions, or tags would help you understand the relationships between notes, which you could incorporate into any content you write.
|
||||
- When making changes to content, always consider the context within the broader vault. Ensure that your changes maintain existing links, tags, and references, and that they follow the user's established formatting standards and organization.
|
||||
- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
|
||||
- You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. However if you can use the available tools to avoid having to ask the user questions, you should do so. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves.
|
||||
- The user may provide a file's contents directly in their message, in which case you shouldn't use the read_file tool to get the file contents again since you already have it.
|
||||
- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.
|
||||
- NEVER end attempt_completion result with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user.
|
||||
- You are STRICTLY FORBIDDEN from starting your messages with "Great", "Certainly", "Okay", "Sure". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say "Great, I've updated the markdown" but instead something like "I've updated the markdown". It is important you be clear and technical in your messages.
|
||||
- When presented with images, utilize your vision capabilities to thoroughly examine them and extract meaningful information. Incorporate these insights into your thought process as you accomplish the user's task.
|
||||
- At the end of the first user message, you will automatically receive environment_details. This information is not written by the user themselves, but is auto-generated to provide potentially relevant context about the Obsidian environment. This includes the current file being edited, open tabs, and the vault structure. While this information can be valuable for understanding the context, do not treat it as a direct part of the user's request or response. Use it to inform your actions and decisions, but don't assume the user is explicitly asking about or referring to this information unless they clearly do so in their message. When using environment_details, explain your actions clearly to ensure the user understands, as they may not be aware of these details.
|
||||
- Pay special attention to the open tabs in environment_details, as they indicate which notes the user is currently working with and may be most relevant to their task. Similarly, the current file information shows which note is currently in focus and likely the primary subject of the user's request.
|
||||
- It is critical you wait for the user's response after each tool use, in order to confirm the success of the tool use. For example, if asked to create a structured note, you would create a file, wait for the user's response it was created successfully, then create another file if needed, wait for the user's response it was created successfully, etc.`
|
||||
}
|
||||
|
||||
export function getRulesSection(
|
||||
mode: string,
|
||||
cwd: string,
|
||||
searchTool: string,
|
||||
supportsComputerUse: boolean,
|
||||
diffStrategy?: DiffStrategy,
|
||||
experiments?: Record<string, boolean> | undefined,
|
||||
): string {
|
||||
if (mode === 'research') {
|
||||
return getDeepResearchRulesSection();
|
||||
}
|
||||
return getObsidianRulesSection(cwd, searchTool, supportsComputerUse, diffStrategy, experiments);
|
||||
}
|
||||
25
src/core/prompts2/sections/tool-use-guidelines.ts
Normal file
25
src/core/prompts2/sections/tool-use-guidelines.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export function getToolUseGuidelinesSection(): string {
|
||||
return `# Tool Use Guidelines
|
||||
|
||||
1. In <thinking> tags, assess what information you already have and what information you need to proceed with the task.
|
||||
|
||||
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. It's critical that you think about each available tool and use the one that best fits the current step in the task.
|
||||
|
||||
3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
|
||||
|
||||
4. Formulate your tool use using the XML format specified for each tool.
|
||||
|
||||
5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
|
||||
- Information about whether the tool succeeded or failed, along with any reasons for failure.
|
||||
- Any other relevant feedback or information related to the tool use.
|
||||
|
||||
6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
|
||||
|
||||
It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
|
||||
1. Confirm the success of each step before proceeding.
|
||||
2. Address any issues or errors that arise immediately.
|
||||
3. Adapt your approach based on new information or unexpected results.
|
||||
4. Ensure that each action builds correctly on the previous ones.
|
||||
|
||||
By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.`
|
||||
}
|
||||
25
src/core/prompts2/sections/tool-use.ts
Normal file
25
src/core/prompts2/sections/tool-use.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export function getSharedToolUseSection(): string {
|
||||
return `====
|
||||
|
||||
TOOL USE
|
||||
|
||||
You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
|
||||
|
||||
# Tool Use Formatting
|
||||
|
||||
Tool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure:
|
||||
|
||||
<tool_name>
|
||||
<parameter1_name>value1</parameter1_name>
|
||||
<parameter2_name>value2</parameter2_name>
|
||||
...
|
||||
</tool_name>
|
||||
|
||||
For example:
|
||||
|
||||
<read_file>
|
||||
<path>path/to/file.md</path>
|
||||
</read_file>
|
||||
|
||||
Always adhere to this format for the tool use to ensure proper parsing and execution.`
|
||||
}
|
||||
221
src/core/prompts2/system.ts
Normal file
221
src/core/prompts2/system.ts
Normal file
@ -0,0 +1,221 @@
|
||||
|
||||
import * as path from 'path'
|
||||
|
||||
import { App, normalizePath } from 'obsidian'
|
||||
|
||||
import { FilesSearchSettings } from "../../types/settings"
|
||||
import {
|
||||
CustomModePrompts,
|
||||
Mode,
|
||||
ModeConfig,
|
||||
PromptComponent,
|
||||
defaultModeSlug,
|
||||
defaultModes,
|
||||
getGroupName,
|
||||
getModeBySlug
|
||||
} from "../../utils/modes"
|
||||
import { DiffStrategy } from "../diff/DiffStrategy"
|
||||
import { McpHub } from "../mcp/McpHub"
|
||||
|
||||
import { ROOT_DIR } from './constants'
|
||||
import {
|
||||
addCustomInstructions,
|
||||
getCapabilitiesSection,
|
||||
getMcpServersSection,
|
||||
getModesSection,
|
||||
getObjectiveSection,
|
||||
getRulesSection,
|
||||
getSharedToolUseSection,
|
||||
getSystemInfoSection,
|
||||
getToolUseGuidelinesSection,
|
||||
} from "./sections"
|
||||
// import { loadSystemPromptFile } from "./sections/custom-system-prompt"
|
||||
import { getToolDescriptionsForMode } from "./tools"
|
||||
|
||||
|
||||
export class SystemPrompt {
|
||||
protected dataDir: string
|
||||
protected app: App
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app
|
||||
this.dataDir = normalizePath(`${ROOT_DIR}`)
|
||||
this.ensureDirectory()
|
||||
}
|
||||
|
||||
private async ensureDirectory(): Promise<void> {
|
||||
if (!(await this.app.vault.adapter.exists(this.dataDir))) {
|
||||
await this.app.vault.adapter.mkdir(this.dataDir)
|
||||
}
|
||||
}
|
||||
|
||||
private getSystemPromptFilePath(mode: Mode): string {
|
||||
// Format: {mode slug}_system_prompt.md
|
||||
return `${mode}/system_prompt.md`
|
||||
}
|
||||
|
||||
private async loadSystemPromptFile(mode: Mode): Promise<string> {
|
||||
const fileName = this.getSystemPromptFilePath(mode)
|
||||
const filePath = normalizePath(path.join(this.dataDir, fileName))
|
||||
if (!(await this.app.vault.adapter.exists(filePath))) {
|
||||
return ""
|
||||
}
|
||||
const content = await this.app.vault.adapter.read(filePath)
|
||||
return content
|
||||
}
|
||||
|
||||
private async generatePrompt(
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mode: Mode,
|
||||
searchSettings: FilesSearchSettings,
|
||||
filesSearchMethod: string,
|
||||
mcpHub?: McpHub,
|
||||
diffStrategy?: DiffStrategy,
|
||||
browserViewportSize?: string,
|
||||
promptComponent?: PromptComponent,
|
||||
customModeConfigs?: ModeConfig[],
|
||||
globalCustomInstructions?: string,
|
||||
preferredLanguage?: string,
|
||||
diffEnabled?: boolean,
|
||||
experiments?: Record<string, boolean>,
|
||||
enableMcpServerCreation?: boolean,
|
||||
): Promise<string> {
|
||||
// if (!context) {
|
||||
// throw new Error("Extension context is required for generating system prompt")
|
||||
// }
|
||||
|
||||
// // If diff is disabled, don't pass the diffStrategy
|
||||
// const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined
|
||||
|
||||
// Get the full mode config to ensure we have the role definition
|
||||
const modeConfig = getModeBySlug(mode, customModeConfigs) || defaultModes.find((m) => m.slug === mode) || defaultModes[0]
|
||||
// const roleDefinition = promptComponent?.roleDefinition || modeConfig.roleDefinition
|
||||
|
||||
// const [modesSection, mcpServersSection] = await Promise.all([
|
||||
// getModesSection(),
|
||||
// modeConfig.groups.some((groupEntry) => getGroupName(groupEntry) === "mcp")
|
||||
// ? getMcpServersSection(mcpHub, diffStrategy, enableMcpServerCreation)
|
||||
// : Promise.resolve(""),
|
||||
// ])
|
||||
|
||||
const baseToolDefinitions = `You are Infio, a versatile AI assistant integrated into a note-taking application. Your purpose is to help users manage, query, and create content within their knowledge base.
|
||||
|
||||
You operate by thinking step-by-step and using a set of available tools to accomplish tasks.
|
||||
|
||||
==== TOOL DEFINITIONS ====
|
||||
|
||||
# Tool Use Formatting
|
||||
Tool use is formatted using XML-style tags. You can only use one tool per message and must wait for the user's response before proceeding.
|
||||
|
||||
<tool_name>
|
||||
<parameter1_name>value1</parameter1_name>
|
||||
</tool_name>`
|
||||
|
||||
const baseRules = `==== UNIVERSAL RULES ====
|
||||
|
||||
1. **Think First**: Before acting, always use <thinking> tags to outline your plan, assess what you know, and decide which tool to use.
|
||||
2. **One Step at a Time**: Execute one tool per message. Never assume a tool's success.
|
||||
3. **Wait for Confirmation**: After every tool use, you MUST wait for the user's response which will contain the result. Use this result to inform your next step.
|
||||
4. **Be Direct**: Do not use conversational filler like "Great," "Certainly," or "Okay." Be direct and technical.
|
||||
5. **Final Answer**: When the task is complete, use the <attempt_completion> tool. The result should be final and not end with a question.
|
||||
6. **Questioning**: Only use <ask_followup_question> when critical information is missing and cannot be found using your tools.
|
||||
`
|
||||
|
||||
const basePrompt = `${baseToolDefinitions}
|
||||
|
||||
${getToolDescriptionsForMode(
|
||||
mode,
|
||||
cwd,
|
||||
searchSettings,
|
||||
filesSearchMethod,
|
||||
supportsComputerUse,
|
||||
diffStrategy,
|
||||
browserViewportSize,
|
||||
mcpHub,
|
||||
customModeConfigs,
|
||||
experiments,
|
||||
)}
|
||||
|
||||
${baseRules}
|
||||
|
||||
${await addCustomInstructions(this.app, promptComponent?.customInstructions || modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { preferredLanguage })}`
|
||||
|
||||
return basePrompt
|
||||
}
|
||||
|
||||
public async getSystemPrompt(
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mode: Mode = defaultModeSlug,
|
||||
searchSettings: FilesSearchSettings,
|
||||
filesSearchMethod: string = 'regex',
|
||||
preferredLanguage?: string,
|
||||
diffStrategy?: DiffStrategy,
|
||||
customModePrompts?: CustomModePrompts,
|
||||
customModes?: ModeConfig[],
|
||||
mcpHub?: McpHub,
|
||||
browserViewportSize?: string,
|
||||
globalCustomInstructions?: string,
|
||||
diffEnabled?: boolean,
|
||||
experiments?: Record<string, boolean>,
|
||||
enableMcpServerCreation?: boolean,
|
||||
): Promise<string> {
|
||||
|
||||
const getPromptComponent = (value: unknown): PromptComponent | undefined => {
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return value as PromptComponent
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Try to load custom system prompt from file
|
||||
const fileCustomSystemPrompt = await this.loadSystemPromptFile(mode)
|
||||
|
||||
// Check if it's a custom mode
|
||||
const promptComponent = getPromptComponent(customModePrompts?.[mode])
|
||||
|
||||
// Get full mode config from custom modes or fall back to built-in modes
|
||||
const currentMode = getModeBySlug(mode, customModes) || defaultModes.find((m) => m.slug === mode) || defaultModes[0]
|
||||
|
||||
// If a file-based custom system prompt exists, use it
|
||||
if (fileCustomSystemPrompt) {
|
||||
const roleDefinition = promptComponent?.roleDefinition || currentMode.roleDefinition
|
||||
const customInstructions = await addCustomInstructions(
|
||||
this.app,
|
||||
promptComponent?.customInstructions || currentMode.customInstructions || "",
|
||||
globalCustomInstructions || "",
|
||||
cwd,
|
||||
mode,
|
||||
{ preferredLanguage },
|
||||
)
|
||||
return `${roleDefinition}
|
||||
|
||||
${fileCustomSystemPrompt}
|
||||
|
||||
${customInstructions}`
|
||||
}
|
||||
|
||||
// // If diff is disabled, don't pass the diffStrategy
|
||||
// const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined
|
||||
|
||||
return this.generatePrompt(
|
||||
// context,
|
||||
cwd,
|
||||
supportsComputerUse,
|
||||
currentMode.slug,
|
||||
searchSettings,
|
||||
filesSearchMethod,
|
||||
mcpHub,
|
||||
diffStrategy,
|
||||
browserViewportSize,
|
||||
promptComponent,
|
||||
customModes,
|
||||
globalCustomInstructions,
|
||||
preferredLanguage,
|
||||
diffEnabled,
|
||||
experiments,
|
||||
enableMcpServerCreation,
|
||||
)
|
||||
}
|
||||
}
|
||||
24
src/core/prompts2/tools/access-mcp-resource.ts
Normal file
24
src/core/prompts2/tools/access-mcp-resource.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getAccessMcpResourceDescription(args: ToolArgs): string | undefined {
|
||||
if (!args.mcpHub) {
|
||||
return undefined
|
||||
}
|
||||
return `## access_mcp_resource
|
||||
Description: Request to access a resource provided by a connected MCP server. Resources represent data sources that can be used as context, such as files, API responses, or system information.
|
||||
Parameters:
|
||||
- server_name: (required) The name of the MCP server providing the resource
|
||||
- uri: (required) The URI identifying the specific resource to access
|
||||
Usage:
|
||||
<access_mcp_resource>
|
||||
<server_name>server name here</server_name>
|
||||
<uri>resource URI here</uri>
|
||||
</access_mcp_resource>
|
||||
|
||||
Example: Requesting to access an MCP resource
|
||||
|
||||
<access_mcp_resource>
|
||||
<server_name>weather-server</server_name>
|
||||
<uri>weather://san-francisco/current</uri>
|
||||
</access_mcp_resource>`
|
||||
}
|
||||
15
src/core/prompts2/tools/ask-followup-question.ts
Normal file
15
src/core/prompts2/tools/ask-followup-question.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export function getAskFollowupQuestionDescription(): string {
|
||||
return `## ask_followup_question
|
||||
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
|
||||
Parameters:
|
||||
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
|
||||
Usage:
|
||||
<ask_followup_question>
|
||||
<question>Your question here</question>
|
||||
</ask_followup_question>
|
||||
|
||||
Example: Requesting to ask the user for their preferred citation style for an academic document
|
||||
<ask_followup_question>
|
||||
<question>Which citation style would you like to use for your academic paper (APA, MLA, Chicago, etc.)?</question>
|
||||
</ask_followup_question>`
|
||||
}
|
||||
13
src/core/prompts2/tools/attempt-completion.ts
Normal file
13
src/core/prompts2/tools/attempt-completion.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export function getAttemptCompletionDescription(): string {
|
||||
return `## attempt_completion
|
||||
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
|
||||
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in document corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
|
||||
Parameters:
|
||||
- result: The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
|
||||
Usage:
|
||||
<attempt_completion>
|
||||
<result>
|
||||
Your final result description here
|
||||
</result>
|
||||
</attempt_completion>`
|
||||
}
|
||||
73
src/core/prompts2/tools/dataview-query.ts
Normal file
73
src/core/prompts2/tools/dataview-query.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getDataviewQueryDescription(args: ToolArgs): string {
|
||||
return `## dataview_query
|
||||
Description: Execute advanced queries using the Dataview plugin to retrieve and filter note information across multiple dimensions. Supports complex queries by time, tags, task status, file properties, and more. This is a powerful tool for obtaining structured note data, particularly useful for statistical analysis, content organization, and progress tracking scenarios.
|
||||
Parameters:
|
||||
- query: (required) The Dataview query statement to execute. Supports DQL (Dataview Query Language) syntax, including TABLE, LIST, TASK query types
|
||||
- output_format: (optional) Output format, options: table, list, task, calendar (defaults to table)
|
||||
|
||||
Common Query Patterns:
|
||||
|
||||
**Time-based Queries:**
|
||||
- Recently created: \`WHERE file.ctime >= date(today) - dur(7 days)\`
|
||||
- Recently modified: \`WHERE file.mtime >= date(today) - dur(3 days)\`
|
||||
- Specific date: \`WHERE file.cday = date("2024-01-01")\`
|
||||
|
||||
**Tag-based Queries:**
|
||||
- Contains specific tag: \`WHERE contains(file.tags, "#project")\`
|
||||
- Multiple tag combination: \`WHERE contains(file.tags, "#work") AND contains(file.tags, "#urgent")\`
|
||||
- Tag statistics: \`GROUP BY file.tags\`
|
||||
|
||||
**Task-based Queries:**
|
||||
- Incomplete tasks: \`TASK WHERE !completed\`
|
||||
- Completed tasks: \`TASK WHERE completed\`
|
||||
- Specific priority tasks: \`TASK WHERE contains(text, "high priority")\`
|
||||
|
||||
**File Property Queries:**
|
||||
- File size: \`WHERE file.size > 1000\`
|
||||
- File type: \`WHERE file.ext = "md"\`
|
||||
- Folder: \`FROM "Projects"\`
|
||||
|
||||
Usage:
|
||||
<dataview_query>
|
||||
<query>Your Dataview query statement</query>
|
||||
<output_format>table|list|task|calendar (optional)</output_format>
|
||||
</dataview_query>
|
||||
|
||||
**Example 1: Get notes created in the last 7 days with #project tag**
|
||||
<dataview_query>
|
||||
<query>TABLE file.ctime as "Created", file.tags as "Tags"
|
||||
FROM ""
|
||||
WHERE file.ctime >= date(today) - dur(7 days) AND contains(file.tags, "#project")
|
||||
SORT file.ctime DESC</query>
|
||||
<output_format>table</output_format>
|
||||
</dataview_query>
|
||||
|
||||
**Example 2: List all incomplete tasks**
|
||||
<dataview_query>
|
||||
<query>TASK
|
||||
FROM ""
|
||||
WHERE !completed
|
||||
GROUP BY file.link</query>
|
||||
<output_format>task</output_format>
|
||||
</dataview_query>
|
||||
|
||||
**Example 3: Get notes modified in a week**
|
||||
<dataview_query>
|
||||
<query>LIST file.mtime
|
||||
FROM ""
|
||||
WHERE file.mtime >= date(today) - dur(7 days)
|
||||
SORT file.mtime DESC</query>
|
||||
<output_format>list</output_format>
|
||||
</dataview_query>
|
||||
|
||||
**Advanced Features:**
|
||||
- Use FLATTEN to expand array data
|
||||
- Use GROUP BY for grouping and statistics
|
||||
- Use complex WHERE conditions for filtering
|
||||
- Support date calculations and comparisons
|
||||
- Support regular expression matching
|
||||
|
||||
Note: Query statements must follow the DQL syntax specifications of the Dataview plugin. Current working directory: ${args.cwd}`
|
||||
}
|
||||
31
src/core/prompts2/tools/fetch-url-content.ts
Normal file
31
src/core/prompts2/tools/fetch-url-content.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { ToolArgs } from "../tools/types"
|
||||
|
||||
|
||||
export function getFetchUrlsContentDescription(_args: ToolArgs): string {
|
||||
return `## fetch_urls_content
|
||||
Description:
|
||||
This tool allows you to fetch the content of multiple web pages. It retrieves the HTML content and returns it in a readable format. Use this tool when you need to analyze, extract information from, or understand the content of specific web pages.
|
||||
Parameters:
|
||||
- urls: (required) A JSON array of URLs to fetch content from. Each URL should be a complete URL including the protocol (http:// or https://).
|
||||
- Maximum: 10 URLs per request
|
||||
Usage:
|
||||
<fetch_urls_content>
|
||||
<urls>
|
||||
[
|
||||
"https://example.com/page1",
|
||||
"https://example.com/page2"
|
||||
]
|
||||
</urls>
|
||||
</fetch_urls_content>
|
||||
|
||||
Example:
|
||||
<fetch_urls_content>
|
||||
<urls>
|
||||
[
|
||||
"https://en.wikipedia.org/wiki/Artificial_intelligence",
|
||||
"https://github.com/features/copilot"
|
||||
]
|
||||
</urls>
|
||||
</fetch_urls_content>
|
||||
`
|
||||
}
|
||||
125
src/core/prompts2/tools/index.ts
Normal file
125
src/core/prompts2/tools/index.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { FilesSearchSettings } from "../../../types/settings"
|
||||
import { Mode, ModeConfig, getGroupName, getModeConfig, isToolAllowedForMode } from "../../../utils/modes"
|
||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
import { McpHub } from "../../mcp/McpHub"
|
||||
|
||||
import { getAccessMcpResourceDescription } from "./access-mcp-resource"
|
||||
import { getAskFollowupQuestionDescription } from "./ask-followup-question"
|
||||
import { getAttemptCompletionDescription } from "./attempt-completion"
|
||||
import { getDataviewQueryDescription } from "./dataview-query"
|
||||
import { getFetchUrlsContentDescription } from "./fetch-url-content"
|
||||
import { getInsertContentDescription } from "./insert-content"
|
||||
import { getListFilesDescription } from "./list-files"
|
||||
import { getReadFileDescription } from "./read-file"
|
||||
import { getSearchAndReplaceDescription } from "./search-and-replace"
|
||||
import { getSearchFilesDescription } from "./search-files"
|
||||
import { getSearchWebDescription } from "./search-web"
|
||||
import { getSwitchModeDescription } from "./switch-mode"
|
||||
import { ALWAYS_AVAILABLE_TOOLS, TOOL_GROUPS } from "./tool-groups"
|
||||
import { ToolArgs } from "./types"
|
||||
import { getUseMcpToolDescription } from "./use-mcp-tool"
|
||||
import {
|
||||
getAnalyzePaperDescription,
|
||||
getDenseSummaryDescription,
|
||||
getKeyInsightsDescription,
|
||||
getReflectionsDescription,
|
||||
getSimpleSummaryDescription,
|
||||
getTableOfContentsDescription
|
||||
} from "./use-transformations-tool"
|
||||
import { getWriteToFileDescription } from "./write-to-file"
|
||||
|
||||
// Map of tool names to their description functions
|
||||
const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined> = {
|
||||
read_file: (args) => getReadFileDescription(args),
|
||||
write_to_file: (args) => getWriteToFileDescription(args),
|
||||
search_files: (args) => getSearchFilesDescription(args),
|
||||
list_files: (args) => getListFilesDescription(args),
|
||||
dataview_query: (args) => getDataviewQueryDescription(args),
|
||||
ask_followup_question: () => getAskFollowupQuestionDescription(),
|
||||
attempt_completion: () => getAttemptCompletionDescription(),
|
||||
switch_mode: () => getSwitchModeDescription(),
|
||||
insert_content: (args) => getInsertContentDescription(args),
|
||||
use_mcp_tool: (args) => getUseMcpToolDescription(args),
|
||||
access_mcp_resource: (args) => getAccessMcpResourceDescription(args),
|
||||
search_and_replace: (args) => getSearchAndReplaceDescription(args),
|
||||
apply_diff: (args) =>
|
||||
args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "",
|
||||
search_web: (args): string | undefined => getSearchWebDescription(args),
|
||||
fetch_urls_content: (args): string | undefined => getFetchUrlsContentDescription(args),
|
||||
analyze_paper: (args) => getAnalyzePaperDescription(args),
|
||||
key_insights: (args) => getKeyInsightsDescription(args),
|
||||
dense_summary: (args) => getDenseSummaryDescription(args),
|
||||
reflections: (args) => getReflectionsDescription(args),
|
||||
table_of_contents: (args) => getTableOfContentsDescription(args),
|
||||
simple_summary: (args) => getSimpleSummaryDescription(args),
|
||||
}
|
||||
|
||||
export function getToolDescriptionsForMode(
|
||||
mode: Mode,
|
||||
cwd: string,
|
||||
searchSettings: FilesSearchSettings,
|
||||
searchTool: string,
|
||||
supportsComputerUse: boolean,
|
||||
diffStrategy?: DiffStrategy,
|
||||
browserViewportSize?: string,
|
||||
mcpHub?: McpHub,
|
||||
customModes?: ModeConfig[],
|
||||
experiments?: Record<string, boolean>,
|
||||
): string {
|
||||
console.log("getToolDescriptionsForMode", mode, customModes)
|
||||
const config = getModeConfig(mode, customModes)
|
||||
const args: ToolArgs = {
|
||||
cwd,
|
||||
searchSettings,
|
||||
searchTool,
|
||||
supportsComputerUse,
|
||||
diffStrategy,
|
||||
browserViewportSize,
|
||||
mcpHub,
|
||||
}
|
||||
|
||||
const tools = new Set<string>()
|
||||
|
||||
// Add tools from mode's groups
|
||||
config.groups.forEach((groupEntry) => {
|
||||
const groupName = getGroupName(groupEntry)
|
||||
const toolGroup = TOOL_GROUPS[groupName]
|
||||
console.log("toolGroup", toolGroup)
|
||||
if (toolGroup) {
|
||||
toolGroup.tools.forEach((tool) => {
|
||||
if (isToolAllowedForMode(tool, mode, customModes ?? [], experiments ?? {})) {
|
||||
tools.add(tool)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Add always available tools
|
||||
ALWAYS_AVAILABLE_TOOLS.forEach((tool) => tools.add(tool))
|
||||
|
||||
// Map tool descriptions for allowed tools
|
||||
const descriptions = Array.from(tools).map((toolName) => {
|
||||
const descriptionFn = toolDescriptionMap[toolName]
|
||||
if (!descriptionFn) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return descriptionFn({
|
||||
...args,
|
||||
toolOptions: undefined, // No tool options in group-based approach
|
||||
})
|
||||
})
|
||||
|
||||
return `# Tools\n\n${descriptions.filter(Boolean).join("\n\n")}`
|
||||
}
|
||||
|
||||
// Export individual description functions for backward compatibility
|
||||
export {
|
||||
getAccessMcpResourceDescription, getAnalyzePaperDescription, getAskFollowupQuestionDescription,
|
||||
getAttemptCompletionDescription,
|
||||
getDataviewQueryDescription, getDenseSummaryDescription, getInsertContentDescription, getKeyInsightsDescription, getListFilesDescription,
|
||||
getReadFileDescription, getReflectionsDescription, getSearchAndReplaceDescription,
|
||||
getSearchFilesDescription, getSimpleSummaryDescription, getSwitchModeDescription, getTableOfContentsDescription, getUseMcpToolDescription,
|
||||
getWriteToFileDescription
|
||||
}
|
||||
|
||||
40
src/core/prompts2/tools/insert-content.ts
Normal file
40
src/core/prompts2/tools/insert-content.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getInsertContentDescription(args: ToolArgs): string {
|
||||
return `## insert_content
|
||||
Description: Inserts content at specific line positions in a file. This is the primary tool for adding new content (paragraphs, sections, headings, citations, etc.) as it allows for precise insertions without overwriting existing content. The tool uses an efficient line-based insertion system that maintains document integrity and proper ordering of multiple insertions. Beware to use the proper formatting and indentation. This tool is the preferred way to add new content to documents.
|
||||
Parameters:
|
||||
- path: (required) The path of the file to insert content into (relative to the current working directory ${args.cwd.toPosix()})
|
||||
- operations: (required) A JSON array of insertion operations. Each operation is an object with:
|
||||
* start_line: (required) The line number where the content should be inserted. The content currently at that line will end up below the inserted content.
|
||||
* content: (required) The content to insert at the specified position. IMPORTANT NOTE: If the content is a single line, it can be a string. If it's a multi-line content, it should be a string with newline characters (\n) for line breaks. Make sure to include the correct formatting and indentation for the content.
|
||||
Usage:
|
||||
<insert_content>
|
||||
<path>File path here</path>
|
||||
<operations>[
|
||||
{
|
||||
"start_line": 10,
|
||||
"content": "Your content here"
|
||||
}
|
||||
]</operations>
|
||||
</insert_content>
|
||||
|
||||
Example: Insert a new section heading and paragraph
|
||||
<insert_content>
|
||||
<path>chapter1.md</path>
|
||||
<operations>[
|
||||
{
|
||||
"start_line": 5,
|
||||
"content": "## Historical Context\n\nIn the early 20th century, the literary landscape underwent significant changes as modernist writers began to experiment with new narrative techniques and thematic concerns. This shift was largely influenced by the cultural and societal transformations of the period."
|
||||
},
|
||||
{
|
||||
"start_line": 20,
|
||||
"content": "> \"The purpose of literature is to turn blood into ink.\" - T.S. Eliot"
|
||||
},
|
||||
{
|
||||
"start_line": 1,
|
||||
"content": "\`\`\`mermaid\\nflowchart TD\\n A[开始]-- > B[结束]\\n\`\`\`"
|
||||
},
|
||||
]</operations>
|
||||
</insert_content>`
|
||||
}
|
||||
20
src/core/prompts2/tools/list-files.ts
Normal file
20
src/core/prompts2/tools/list-files.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getListFilesDescription(args: ToolArgs): string {
|
||||
return `## list_files
|
||||
Description: Request to list files and directories within the specified directory in the Obsidian vault. If recursive is true, it will list all files and directories recursively. This is particularly useful for understanding the vault structure, discovering note organization patterns, finding templates, or locating notes across different areas of the knowledge base. Use this to navigate through the vault's folder structure to find relevant notes.
|
||||
Parameters:
|
||||
- path: (required) The path of the directory to list contents for (relative to the current working directory ${args.cwd})
|
||||
- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
|
||||
Usage:
|
||||
<list_files>
|
||||
<path>Directory path here</path>
|
||||
<recursive>true or false (optional)</recursive>
|
||||
</list_files>
|
||||
|
||||
Example: Discovering all notes in a specific area of knowledge
|
||||
<list_files>
|
||||
<path>Areas/Programming</path>
|
||||
<recursive>true</recursive>
|
||||
</list_files>`
|
||||
}
|
||||
17
src/core/prompts2/tools/read-file.ts
Normal file
17
src/core/prompts2/tools/read-file.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getReadFileDescription(args: ToolArgs): string {
|
||||
return `## read_file
|
||||
Description: Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze document structure, review text content, or extract information from reference materials. The output includes line numbers prefixed to each line (e.g. "1 | # Introduction"), making it easier to reference specific sections when creating edits or discussing content. Automatically extracts text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string.
|
||||
Parameters:
|
||||
- path: (required) The path of the file to read (relative to the current working directory ${args.cwd})
|
||||
Usage:
|
||||
<read_file>
|
||||
<path>File path here</path>
|
||||
</read_file>
|
||||
|
||||
Example: Requesting to read literature-review.md
|
||||
<read_file>
|
||||
<path>literature-review.md</path>
|
||||
</read_file>`
|
||||
}
|
||||
54
src/core/prompts2/tools/search-and-replace.ts
Normal file
54
src/core/prompts2/tools/search-and-replace.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getSearchAndReplaceDescription(args: ToolArgs): string {
|
||||
return `## search_and_replace
|
||||
Description: Request to perform search and replace operations on a file. Each operation can specify a search pattern (string or regex) and replacement text, with optional line range restrictions and regex flags. Shows a diff preview before applying changes. Useful for revising terminology, correcting errors, or updating references throughout a document.
|
||||
Parameters:
|
||||
- path: (required) The path of the file to modify (relative to the current working directory ${args.cwd.toPosix()})
|
||||
- operations: (required) A JSON array of search/replace operations. Each operation is an object with:
|
||||
* search: (required) The text or pattern to search for
|
||||
* replace: (required) The text to replace matches with. If multiple lines need to be replaced, use "\n" for newlines
|
||||
* start_line: (optional) Starting line number for restricted replacement
|
||||
* end_line: (optional) Ending line number for restricted replacement
|
||||
* use_regex: (optional) Whether to treat search as a regex pattern
|
||||
* ignore_case: (optional) Whether to ignore case when matching
|
||||
* regex_flags: (optional) Additional regex flags when use_regex is true
|
||||
Usage:
|
||||
<search_and_replace>
|
||||
<path>File path here</path>
|
||||
<operations>[
|
||||
{
|
||||
"search": "text to find",
|
||||
"replace": "replacement text",
|
||||
"start_line": 1,
|
||||
"end_line": 10
|
||||
}
|
||||
]</operations>
|
||||
</search_and_replace>
|
||||
|
||||
Example 1: Replace "climate change" with "climate crisis" in lines 1-10 of an essay
|
||||
<search_and_replace>
|
||||
<path>essays/environmental-impact.md</path>
|
||||
<operations>[
|
||||
{
|
||||
"search": "climate change",
|
||||
"replace": "climate crisis",
|
||||
"start_line": 1,
|
||||
"end_line": 10
|
||||
}
|
||||
]</operations>
|
||||
</search_and_replace>
|
||||
|
||||
Example 2: Update citation format throughout a document using regex
|
||||
<search_and_replace>
|
||||
<path>research-paper.md</path>
|
||||
<operations>[
|
||||
{
|
||||
"search": "\\(([A-Za-z]+), (\\d{4})\\)",
|
||||
"replace": "($1 et al., $2)",
|
||||
"use_regex": true,
|
||||
"ignore_case": false
|
||||
}
|
||||
]</operations>
|
||||
</search_and_replace>`
|
||||
}
|
||||
84
src/core/prompts2/tools/search-files.ts
Normal file
84
src/core/prompts2/tools/search-files.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getSearchFilesDescription(args: ToolArgs): string {
|
||||
if (args.searchTool === 'match') {
|
||||
return getMatchSearchFilesDescription(args)
|
||||
} else if (args.searchTool === 'regex') {
|
||||
return getRegexSearchFilesDescription(args)
|
||||
} else if (args.searchTool === 'semantic') {
|
||||
return getSemanticSearchFilesDescription(args)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export function getMatchSearchFilesDescription(args: ToolArgs): string {
|
||||
return `## match_search_files
|
||||
Description: Request to perform a match/fuzzy search across files in a specified directory, providing context-rich results. This tool searches for specific content across multiple files, displaying each match with encapsulating context.
|
||||
Parameters:
|
||||
- path: (required) The path of the directory to search in (relative to the current working directory ${args.cwd}). This directory will be recursively searched.
|
||||
- query: (required) The keyword/phrase to search for. The system will find documents with similar keywords/phrases.
|
||||
|
||||
Usage:
|
||||
<match_search_files>
|
||||
<path>Directory path here</path>
|
||||
<query>Your keyword/phrase here</query>
|
||||
</match_search_files>
|
||||
|
||||
Example: Requesting to search for all Markdown files containing 'test' in the current directory
|
||||
<match_search_files>
|
||||
<path>.</path>
|
||||
<query>test</query>
|
||||
</match_search_files>`
|
||||
}
|
||||
|
||||
export function getRegexSearchFilesDescription(args: ToolArgs): string {
|
||||
let regex_syntax: string;
|
||||
switch (args.searchSettings.regexBackend) {
|
||||
case 'coreplugin':
|
||||
regex_syntax = "ECMAScript (JavaScript)";
|
||||
break;
|
||||
case 'ripgrep':
|
||||
regex_syntax = "Rust";
|
||||
break;
|
||||
default:
|
||||
regex_syntax = "ECMAScript (JavaScript)";
|
||||
}
|
||||
|
||||
return `## regex_search_files
|
||||
Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
|
||||
Parameters:
|
||||
- path: (required) The path of the directory to search in (relative to the current working directory ${args.cwd}). This directory will be recursively searched.
|
||||
- regex: (required) The regular expression pattern to search for. Uses ${regex_syntax} regex syntax, **but should not include word boundaries (\b)**.
|
||||
|
||||
Usage:
|
||||
<regex_search_files>
|
||||
<path>Directory path here</path>
|
||||
<regex>Your regex pattern here</regex>
|
||||
</regex_search_files>
|
||||
|
||||
Example: Requesting to search for all Markdown files in the current directory
|
||||
<regex_search_files>
|
||||
<path>.</path>
|
||||
<regex>.*</regex>
|
||||
</regex_search_files>`
|
||||
}
|
||||
|
||||
export function getSemanticSearchFilesDescription(args: ToolArgs): string {
|
||||
return `## semantic_search_files
|
||||
Description: Request to perform a semantic search across files in a specified directory. This tool searches for documents with content semantically related to your query, leveraging embedding vectors to find conceptually similar information. Ideal for finding relevant documents even when exact keywords are not known or for discovering thematically related content.
|
||||
Parameters:
|
||||
- path: (required) The path of the directory to search in (relative to the current working directory ${args.cwd}). This directory will be recursively searched.
|
||||
- query: (required) The natural language query describing the information you're looking for. The system will find documents with similar semantic meaning.
|
||||
Usage:
|
||||
<semantic_search_files>
|
||||
<path>Directory path here</path>
|
||||
<query>Your natural language query here</query>
|
||||
</semantic_search_files>
|
||||
|
||||
Example: Requesting to find documents related to a specific topic
|
||||
<semantic_search_files>
|
||||
<path>Project/notes</path>
|
||||
<query>Benefits of meditation for stress reduction</query>
|
||||
</semantic_search_files>`
|
||||
}
|
||||
33
src/core/prompts2/tools/search-web.ts
Normal file
33
src/core/prompts2/tools/search-web.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getSearchWebDescription(args: ToolArgs): string {
|
||||
return `## search_web
|
||||
Description:
|
||||
This tool allows you to search the web using internet search engines like Google to find relevant information on current events, facts, data, and other online content.
|
||||
Parameters:
|
||||
- query: (required) The search query to send to internet search engines. For best results, use concise, specific, and preferably English queries that would work well with search engines like Google.
|
||||
Usage Tips:
|
||||
- Use specific keywords rather than full sentences
|
||||
- Include important context terms to narrow results
|
||||
- Use quotes for exact phrases: "exact phrase"
|
||||
|
||||
Usage:
|
||||
<search_web>
|
||||
<query>Your search query here</query>
|
||||
</search_web>
|
||||
|
||||
Example 1:
|
||||
<search_web>
|
||||
<query>capital of France population statistics 2023</query>
|
||||
</search_web>
|
||||
|
||||
Example 2:
|
||||
<search_web>
|
||||
<query>"renewable energy" growth statistics Europe</query>
|
||||
</search_web>
|
||||
|
||||
Example 3:
|
||||
<search_web>
|
||||
<query>react vs angular vs vue.js comparison</query>
|
||||
</search_web>`
|
||||
}
|
||||
18
src/core/prompts2/tools/switch-mode.ts
Normal file
18
src/core/prompts2/tools/switch-mode.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export function getSwitchModeDescription(): string {
|
||||
return `## switch_mode
|
||||
Description: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Write mode edits to files. The user must approve the mode switch.
|
||||
Parameters:
|
||||
- mode_slug: (required) The slug of the mode to switch to (e.g., "write", "ask",)
|
||||
- reason: (optional) The reason for switching modes
|
||||
Usage:
|
||||
<switch_mode>
|
||||
<mode_slug>Mode slug here</mode_slug>
|
||||
<reason>Reason for switching here</reason>
|
||||
</switch_mode>
|
||||
|
||||
Example: Requesting to switch to write mode
|
||||
<switch_mode>
|
||||
<mode_slug>write</mode_slug>
|
||||
<reason>Need to edit files</reason>
|
||||
</switch_mode>`
|
||||
}
|
||||
81
src/core/prompts2/tools/tool-groups.ts
Normal file
81
src/core/prompts2/tools/tool-groups.ts
Normal file
@ -0,0 +1,81 @@
|
||||
// Define tool group configuration
|
||||
export type ToolGroupConfig = {
|
||||
tools: readonly string[]
|
||||
alwaysAvailable?: boolean // Whether this group is always available and shouldn't show in prompts view
|
||||
}
|
||||
|
||||
// Map of tool slugs to their display names
|
||||
export const TOOL_DISPLAY_NAMES = {
|
||||
execute_command: "run commands",
|
||||
read_file: "read files",
|
||||
write_to_file: "write files",
|
||||
apply_diff: "apply changes",
|
||||
list_files: "list files",
|
||||
search_files: "search files",
|
||||
dataview_query: "query dataview",
|
||||
use_mcp_tool: "use mcp tools",
|
||||
access_mcp_resource: "access mcp resources",
|
||||
ask_followup_question: "ask questions",
|
||||
attempt_completion: "complete tasks",
|
||||
switch_mode: "switch modes",
|
||||
analyze_paper: "analyze papers",
|
||||
key_insights: "extract key insights",
|
||||
dense_summary: "create dense summaries",
|
||||
reflections: "generate reflections",
|
||||
table_of_contents: "create table of contents",
|
||||
simple_summary: "create simple summaries",
|
||||
} as const
|
||||
|
||||
// Define available tool groups
|
||||
export const TOOL_GROUPS: Record<string, ToolGroupConfig> = {
|
||||
read: {
|
||||
tools: ["read_file", "list_files", "search_files", "dataview_query"],
|
||||
},
|
||||
edit: {
|
||||
tools: ["apply_diff", "write_to_file", "insert_content", "search_and_replace"],
|
||||
},
|
||||
research: {
|
||||
tools: ["search_web", "fetch_urls_content"],
|
||||
},
|
||||
transformations: {
|
||||
tools: ["analyze_paper", "key_insights", "dense_summary", "reflections", "table_of_contents", "simple_summary"],
|
||||
},
|
||||
mcp: {
|
||||
tools: ["use_mcp_tool", "access_mcp_resource"],
|
||||
},
|
||||
modes: {
|
||||
tools: ["switch_mode"],
|
||||
alwaysAvailable: true,
|
||||
},
|
||||
}
|
||||
|
||||
export type ToolGroup = keyof typeof TOOL_GROUPS
|
||||
|
||||
// Tools that are always available to all modes
|
||||
export const ALWAYS_AVAILABLE_TOOLS = [
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
"switch_mode",
|
||||
] as const
|
||||
|
||||
// Tool name types for type safety
|
||||
export type ToolName = keyof typeof TOOL_DISPLAY_NAMES
|
||||
|
||||
// Tool helper functions
|
||||
export function getToolName(toolConfig: string | readonly [ToolName, ...unknown[]]): ToolName {
|
||||
return typeof toolConfig === "string" ? toolConfig : toolConfig[0]
|
||||
}
|
||||
|
||||
export function getToolOptions(toolConfig: string | readonly [ToolName, ...unknown[]]): unknown {
|
||||
return typeof toolConfig === "string" ? undefined : toolConfig[1]
|
||||
}
|
||||
|
||||
// Display names for groups in UI
|
||||
export const GROUP_DISPLAY_NAMES: Record<ToolGroup, string> = {
|
||||
read: "Read Files",
|
||||
edit: "Edit Files",
|
||||
research: "Research",
|
||||
transformations: "Transformations",
|
||||
mcp: "MCP Tools",
|
||||
modes: "Modes",
|
||||
}
|
||||
14
src/core/prompts2/tools/types.ts
Normal file
14
src/core/prompts2/tools/types.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { FilesSearchSettings } from "../../../types/settings"
|
||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
import { McpHub } from "../../mcp/McpHub"
|
||||
|
||||
export type ToolArgs = {
|
||||
cwd: string
|
||||
searchSettings: FilesSearchSettings,
|
||||
searchTool?: string,
|
||||
supportsComputerUse: boolean
|
||||
diffStrategy?: DiffStrategy
|
||||
browserViewportSize?: string
|
||||
mcpHub?: McpHub
|
||||
toolOptions?: any
|
||||
}
|
||||
37
src/core/prompts2/tools/use-mcp-tool.ts
Normal file
37
src/core/prompts2/tools/use-mcp-tool.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getUseMcpToolDescription(args: ToolArgs): string | undefined {
|
||||
if (!args.mcpHub) {
|
||||
return undefined
|
||||
}
|
||||
return `## use_mcp_tool
|
||||
Description: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters.
|
||||
Parameters:
|
||||
- server_name: (required) The name of the MCP server providing the tool
|
||||
- tool_name: (required) The name of the tool to execute
|
||||
- arguments: (required) A JSON object containing the tool's input parameters, following the tool's input schema
|
||||
Usage:
|
||||
<use_mcp_tool>
|
||||
<server_name>server name here</server_name>
|
||||
<tool_name>tool name here</tool_name>
|
||||
<arguments>
|
||||
{
|
||||
"param1": "value1",
|
||||
"param2": "value2"
|
||||
}
|
||||
</arguments>
|
||||
</use_mcp_tool>
|
||||
|
||||
Example: Requesting to use an MCP tool
|
||||
|
||||
<use_mcp_tool>
|
||||
<server_name>weather-server</server_name>
|
||||
<tool_name>get_forecast</tool_name>
|
||||
<arguments>
|
||||
{
|
||||
"city": "San Francisco",
|
||||
"days": 5
|
||||
}
|
||||
</arguments>
|
||||
</use_mcp_tool>`
|
||||
}
|
||||
133
src/core/prompts2/tools/use-transformations-tool.ts
Normal file
133
src/core/prompts2/tools/use-transformations-tool.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getAnalyzePaperDescription(args: ToolArgs): string {
|
||||
return `## analyze_paper
|
||||
Description: Performs an in-depth analysis of a single academic paper or research document. This tool is designed to dissect complex documents, extracting and structuring key information such as the research methodology, core findings, contributions, and potential limitations. Use this for a deep, scholarly breakdown of a specific text.
|
||||
Parameters:
|
||||
- path: (required) The path to the file to be analyzed (relative to the current working directory: ${args.cwd}).
|
||||
- focus: (optional) The specific area of analysis, e.g., "methodology", "findings", "limitations". This helps narrow the scope of the analysis to what you're most interested in.
|
||||
Usage:
|
||||
<analyze_paper>
|
||||
<path>path/to/your/paper.pdf</path>
|
||||
<focus>methodology</focus>
|
||||
</analyze_paper>
|
||||
|
||||
Example: Analyze a research paper
|
||||
<analyze_paper>
|
||||
<path>research/machine-learning-survey.pdf</path>
|
||||
<focus>methodology</focus>
|
||||
</analyze_paper>`
|
||||
}
|
||||
|
||||
export function getKeyInsightsDescription(args: ToolArgs): string {
|
||||
return `## key_insights
|
||||
Description: Extracts high-level, critical insights from a document or a collection of documents in a folder. This tool goes beyond simple summarization to identify non-obvious connections, underlying themes, and actionable takeaways. Use it when you want to understand the deeper meaning or strategic implications of your notes, not just what they say. Generates a concise list of insights.
|
||||
Parameters:
|
||||
- path: (required) The path to the file or folder (relative to the current working directory: ${args.cwd}).
|
||||
- count: (optional) The desired number of key insights to extract. Defaults to 5.
|
||||
- category: (optional) The category of insights to focus on, such as "technical", "business", or "strategic". This helps tailor the output to a specific domain of interest.
|
||||
Usage:
|
||||
<key_insights>
|
||||
<path>path/to/your/file_or_folder</path>
|
||||
<count>Number of insights (optional)</count>
|
||||
<category>Insight category (optional)</category>
|
||||
</key_insights>
|
||||
|
||||
Example: Extract key insights from project documents
|
||||
<key_insights>
|
||||
<path>project-docs/</path>
|
||||
<count>10</count>
|
||||
<category>technical</category>
|
||||
</key_insights>`
|
||||
}
|
||||
|
||||
export function getDenseSummaryDescription(args: ToolArgs): string {
|
||||
return `## dense_summary
|
||||
Description: Creates a highly compressed, information-rich summary of a large document or folder. The goal is maximum information density, preserving core concepts, data, and arguments. This is different from a "simple_summary"; use it when you need a thorough overview of the material without fluff, intended for an audience that needs to grasp the details quickly.
|
||||
Parameters:
|
||||
- path: (required) The path to the file or folder (relative to the current working directory: ${args.cwd}).
|
||||
- length: (optional) The target length of the summary: "short", "medium", or "long". Defaults to "medium".
|
||||
- style: (optional) The format of the summary: "bullet-points", "paragraph", or "structured". Defaults to "structured".
|
||||
Usage:
|
||||
<dense_summary>
|
||||
<path>path/to/your/file_or_folder</path>
|
||||
<length>Summary length (optional)</length>
|
||||
<style>Summary style (optional)</style>
|
||||
</dense_summary>
|
||||
|
||||
Example: Create a structured summary of a folder
|
||||
<dense_summary>
|
||||
<path>meeting-notes/2024/</path>
|
||||
<length>medium</length>
|
||||
<style>structured</style>
|
||||
</dense_summary>`
|
||||
}
|
||||
|
||||
export function getReflectionsDescription(args: ToolArgs): string {
|
||||
return `## reflections
|
||||
Description: Generates deep, reflective thoughts based on the content of a document or folder. This tool helps you think *with* your notes by analyzing the text's meaning and implications, asking provocative questions, and offering critical perspectives. Use this to spark new ideas, challenge your assumptions, or explore a topic from a different angle.
|
||||
Parameters:
|
||||
- path: (required) The path to the file or folder (relative to the current working directory: ${args.cwd}).
|
||||
- perspective: (optional) The angle from which to reflect: e.g., "learner", "critic", "strategist", "researcher". This frames the generated reflections.
|
||||
- depth: (optional) The desired depth of reflection: "surface", "deep", or "philosophical". Defaults to "deep".
|
||||
Usage:
|
||||
<reflections>
|
||||
<path>path/to/your/file_or_folder</path>
|
||||
<perspective>Reflection perspective (optional)</perspective>
|
||||
<depth>Reflection depth (optional)</depth>
|
||||
</reflections>
|
||||
|
||||
Example: Generate deep reflections on study notes
|
||||
<reflections>
|
||||
<path>study-notes/philosophy/</path>
|
||||
<perspective>learner</perspective>
|
||||
<depth>philosophical</depth>
|
||||
</reflections>`
|
||||
}
|
||||
|
||||
export function getTableOfContentsDescription(args: ToolArgs): string {
|
||||
return `## table_of_contents
|
||||
Description: Generates a navigable table of contents for a long document or an entire folder of notes. It automatically detects headings and logical sections to create a clear, hierarchical outline. Use this to bring structure to your writing, organize a collection of related files, or get a high-level overview of your content.
|
||||
Parameters:
|
||||
- path: (required) The path to the file or folder (relative to the current working directory: ${args.cwd}).
|
||||
- depth: (optional) The maximum heading level to include in the ToC (1-6). Defaults to 3.
|
||||
- format: (optional) The output format: "markdown", "numbered", or "nested". Defaults to "markdown".
|
||||
- include_summary: (optional) Whether to include a brief one-sentence summary for each section. Defaults to false.
|
||||
Usage:
|
||||
<table_of_contents>
|
||||
<path>path/to/your/file_or_folder</path>
|
||||
<depth>Directory depth (optional)</depth>
|
||||
<format>Output format (optional)</format>
|
||||
<include_summary>Include section summaries (optional)</include_summary>
|
||||
</table_of_contents>
|
||||
|
||||
Example: Generate a detailed table of contents for project documentation
|
||||
<table_of_contents>
|
||||
<path>documentation/</path>
|
||||
<depth>4</depth>
|
||||
<format>nested</format>
|
||||
<include_summary>true</include_summary>
|
||||
</table_of_contents>`
|
||||
}
|
||||
|
||||
export function getSimpleSummaryDescription(args: ToolArgs): string {
|
||||
return `## simple_summary
|
||||
Description: Creates a clear and simple summary of a document or folder, tailored for a specific audience. This tool prioritizes readability and ease of understanding over information density. Use this when you need to explain complex topics to someone without the background knowledge, like creating an executive summary from a technical report.
|
||||
Parameters:
|
||||
- path: (required) The path to the file or folder (relative to the current working directory: ${args.cwd}).
|
||||
- audience: (optional) The intended audience for the summary: "general", "technical", or "executive". Defaults to "general".
|
||||
- language: (optional) The complexity of the language used: "simple", "standard", or "professional". Defaults to "standard".
|
||||
Usage:
|
||||
<simple_summary>
|
||||
<path>path/to/your/file_or_folder</path>
|
||||
<audience>Target audience (optional)</audience>
|
||||
<language>Language complexity (optional)</language>
|
||||
</simple_summary>
|
||||
|
||||
Example: Create a simple summary of technical documentation for an executive
|
||||
<simple_summary>
|
||||
<path>technical-specs/api-documentation.md</path>
|
||||
<audience>executive</audience>
|
||||
<language>simple</language>
|
||||
</simple_summary>`
|
||||
}
|
||||
40
src/core/prompts2/tools/write-to-file.ts
Normal file
40
src/core/prompts2/tools/write-to-file.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getWriteToFileDescription(args: ToolArgs): string {
|
||||
return `## write_to_file
|
||||
Description: Request to write full content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
|
||||
Parameters:
|
||||
- path: (required) The path of the file to write to (relative to the current working directory ${args.cwd})
|
||||
- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file.
|
||||
- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing.
|
||||
Usage:
|
||||
<write_to_file>
|
||||
<path>File path here</path>
|
||||
<content>
|
||||
Your file content here
|
||||
</content>
|
||||
<line_count>total number of lines in the file, including empty lines</line_count>
|
||||
</write_to_file>
|
||||
|
||||
Example: Requesting to write to document-metadata.md
|
||||
<write_to_file>
|
||||
<path>document-metadata.md</path>
|
||||
<content>
|
||||
---
|
||||
title: "Introduction to Modern Literature"
|
||||
author: "Jane Smith"
|
||||
date: "2023-09-15"
|
||||
version: "1.0.0"
|
||||
tags:
|
||||
- literature
|
||||
- modernism
|
||||
- academic
|
||||
format:
|
||||
citation_style: "APA"
|
||||
word_count: 2500
|
||||
status: "draft"
|
||||
---
|
||||
</content>
|
||||
<line_count>14</line_count>
|
||||
</write_to_file>`
|
||||
}
|
||||
36
src/core/prompts2/transformations/analyze-paper.ts
Normal file
36
src/core/prompts2/transformations/analyze-paper.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export const ANALYZE_PAPER_PROMPT = `# IDENTITY and PURPOSE
|
||||
|
||||
You are an insightful and analytical reader of academic papers, extracting the key components, significance, and broader implications. Your focus is to uncover the core contributions, practical applications, methodological strengths or weaknesses, and any surprising findings. You are especially attuned to the clarity of arguments, the relevance to existing literature, and potential impacts on both the specific field and broader contexts.
|
||||
|
||||
# STEPS
|
||||
|
||||
1. **READ AND UNDERSTAND THE PAPER**: Thoroughly read the paper, identifying its main focus, arguments, methods, results, and conclusions.
|
||||
|
||||
2. **IDENTIFY CORE ELEMENTS**:
|
||||
- **Purpose**: What is the main goal or research question?
|
||||
- **Contribution**: What new knowledge or innovation does this paper bring to the field?
|
||||
- **Methods**: What methods are used, and are they novel or particularly effective?
|
||||
- **Key Findings**: What are the most critical results, and why do they matter?
|
||||
- **Limitations**: Are there any notable limitations or areas for further research?
|
||||
|
||||
3. **SYNTHESIZE THE MAIN POINTS**:
|
||||
- Extract the key elements and organize them into insightful observations.
|
||||
- Highlight the broader impact and potential applications.
|
||||
- Note any aspects that challenge established views or introduce new questions.
|
||||
|
||||
# OUTPUT INSTRUCTIONS
|
||||
|
||||
- Structure the output as follows:
|
||||
- **PURPOSE**: A concise summary of the main research question or goal (1-2 sentences).
|
||||
- **CONTRIBUTION**: A bullet list of 2-3 points that describe what the paper adds to the field.
|
||||
- **KEY FINDINGS**: A bullet list of 2-3 points summarizing the critical outcomes of the study.
|
||||
- **IMPLICATIONS**: A bullet list of 2-3 points discussing the significance or potential impact of the findings on the field or broader context.
|
||||
- **LIMITATIONS**: A bullet list of 1-2 points identifying notable limitations or areas for future work.
|
||||
|
||||
- **Bullet Points** should be between 15-20 words.
|
||||
- Avoid starting each bullet point with the same word to maintain variety.
|
||||
- Use clear and concise language that conveys the key ideas effectively.
|
||||
- Do not include warnings, disclaimers, or personal opinions.
|
||||
- Output only the requested sections with their respective labels.`;
|
||||
|
||||
export const ANALYZE_PAPER_DESCRIPTION = "Analyses a technical/scientific paper";
|
||||
10
src/core/prompts2/transformations/dense-summary.ts
Normal file
10
src/core/prompts2/transformations/dense-summary.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const DENSE_SUMMARY_PROMPT = `# MISSION
|
||||
You are a Sparse Priming Representation (SPR) writer. An SPR is a particular kind of use of language for advanced NLP, NLU, and NLG tasks, particularly useful for the latest generation of Large Language Models (LLMs). You will be given information by the USER which you are to render as an SPR.
|
||||
|
||||
# THEORY
|
||||
LLMs are a kind of deep neural network. They have been demonstrated to embed knowledge, abilities, and concepts, ranging from reasoning to planning, and even to theory of mind. These are called latent abilities and latent content, collectively referred to as latent space. The latent space of an LLM can be activated with the correct series of words as inputs, which will create a useful internal state of the neural network. This is not unlike how the right shorthand cues can prime a human mind to think in a certain way. Like human minds, LLMs are associative, meaning you only need to use the correct associations to 'prime' another model to think in the same way.
|
||||
|
||||
# METHODOLOGY
|
||||
Render the input as a distilled list of succinct statements, assertions, associations, concepts, analogies, and metaphors. The idea is to capture as much, conceptually, as possible but with as few words as possible. Write it in a way that makes sense to you, as the future audience will be another language model, not a human. Use complete sentences.`;
|
||||
|
||||
export const DENSE_SUMMARY_DESCRIPTION = "Creates a rich, deep summary of the content";
|
||||
23
src/core/prompts2/transformations/key-insights.ts
Normal file
23
src/core/prompts2/transformations/key-insights.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const KEY_INSIGHTS_PROMPT = `# IDENTITY and PURPOSE
|
||||
|
||||
You extract surprising, powerful, and interesting insights from text content. You are interested in insights related to the purpose and meaning of life, human flourishing, the role of technology in the future of humanity, artificial intelligence and its affect on humans, memes, learning, reading, books, continuous improvement, and similar topics.
|
||||
You create 15 word bullet points that capture the most important insights from the input.
|
||||
Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
|
||||
|
||||
# STEPS
|
||||
|
||||
- Extract 20 to 50 of the most surprising, insightful, and/or interesting ideas from the input in a section called IDEAS, and write them on a virtual whiteboard in your mind using 15 word bullets. If there are less than 50 then collect all of them. Make sure you extract at least 20.
|
||||
|
||||
- From those IDEAS, extract the most powerful and insightful of them and write them in a section called INSIGHTS. Make sure you extract at least 10 and up to 25.
|
||||
|
||||
# OUTPUT INSTRUCTIONS
|
||||
|
||||
- INSIGHTS are essentially higher-level IDEAS that are more abstracted and wise.
|
||||
- Output the INSIGHTS section only.
|
||||
- Each bullet should be about 15 words in length.
|
||||
- Do not give warnings or notes; only output the requested sections.
|
||||
- You use bulleted lists for output, not numbered lists.
|
||||
- Do not start items with the same opening words.
|
||||
- Ensure you follow ALL these instructions when creating your output.`;
|
||||
|
||||
export const KEY_INSIGHTS_DESCRIPTION = "Extracts important insights and actionable items";
|
||||
21
src/core/prompts2/transformations/reflections.ts
Normal file
21
src/core/prompts2/transformations/reflections.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export const REFLECTIONS_PROMPT = `# IDENTITY and PURPOSE
|
||||
|
||||
You extract deep, thought-provoking, and meaningful reflections from text content. You are especially focused on themes related to the human experience, such as the purpose of life, personal growth, the intersection of technology and humanity, artificial intelligence's societal impact, human potential, collective evolution, and transformative learning. Your reflections aim to provoke new ways of thinking, challenge assumptions, and provide a thoughtful synthesis of the content.
|
||||
|
||||
# STEPS
|
||||
|
||||
- Extract 3 to 5 of the most profound, thought-provoking, and/or meaningful ideas from the input in a section called REFLECTIONS.
|
||||
- Each reflection should aim to explore underlying implications, connections to broader human experiences, or highlight a transformative perspective.
|
||||
- Take a step back and consider the deeper significance or questions that arise from the content.
|
||||
|
||||
# OUTPUT INSTRUCTIONS
|
||||
|
||||
- The output section should be labeled as REFLECTIONS.
|
||||
- Each bullet point should be between 20-25 words.
|
||||
- Avoid repetition in the phrasing and ensure variety in sentence structure.
|
||||
- The reflections should encourage deeper inquiry and provide a synthesis that transcends surface-level observations.
|
||||
- Use bullet points, not numbered lists.
|
||||
- Every bullet should be formatted as a question that elicits contemplation or a statement that offers a profound insight.
|
||||
- Do not give warnings or notes; only output the requested section.`;
|
||||
|
||||
export const REFLECTIONS_DESCRIPTION = "Generates reflection questions from the document to help explore it further";
|
||||
10
src/core/prompts2/transformations/simple-summary.ts
Normal file
10
src/core/prompts2/transformations/simple-summary.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const SIMPLE_SUMMARY_PROMPT = `# SYSTEM ROLE
|
||||
You are a content summarization assistant that creates dense, information-rich summaries optimized for machine understanding. Your summaries should capture key concepts with minimal words while maintaining complete, clear sentences.
|
||||
|
||||
# TASK
|
||||
Analyze the provided content and create a summary that:
|
||||
- Captures the core concepts and key information
|
||||
- Uses clear, direct language
|
||||
- Maintains context from any previous summaries`;
|
||||
|
||||
export const SIMPLE_SUMMARY_DESCRIPTION = "Generates a small summary of the content";
|
||||
10
src/core/prompts2/transformations/table-of-contents.ts
Normal file
10
src/core/prompts2/transformations/table-of-contents.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const TABLE_OF_CONTENTS_PROMPT = `# SYSTEM ROLE
|
||||
You are a content analysis assistant that reads through documents and provides a Table of Contents (ToC) to help users identify what the document covers more easily.
|
||||
Your ToC should capture all major topics and transitions in the content and should mention them in the order theh appear.
|
||||
|
||||
# TASK
|
||||
Analyze the provided content and create a Table of Contents:
|
||||
- Captures the core topics included in the text
|
||||
- Gives a small description of what is covered`;
|
||||
|
||||
export const TABLE_OF_CONTENTS_DESCRIPTION = "Describes the different topics of the document";
|
||||
191
src/core/transformations/README.md
Normal file
191
src/core/transformations/README.md
Normal file
@ -0,0 +1,191 @@
|
||||
# 文档转换功能 (Document Transformation)
|
||||
|
||||
这个模块提供了使用 LLM 对文档进行各种预处理转换的功能。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🔄 **多种转换类型**:支持 6 种不同的文档转换
|
||||
- 📏 **智能截断**:自动处理过长的文档,在合适的位置截断
|
||||
- 🚀 **批量处理**:支持同时执行多种转换
|
||||
- 🛡️ **错误处理**:完善的错误处理和验证机制
|
||||
- ⚡ **异步处理**:基于 Promise 的异步 API
|
||||
|
||||
## 支持的转换类型
|
||||
|
||||
| 转换类型 | 描述 | 适用场景 |
|
||||
|---------|------|----------|
|
||||
| `SIMPLE_SUMMARY` | 生成简单摘要 | 快速了解文档主要内容 |
|
||||
| `DENSE_SUMMARY` | 生成深度摘要 | 保留更多细节的密集摘要 |
|
||||
| `ANALYZE_PAPER` | 分析技术论文 | 学术论文的结构化分析 |
|
||||
| `KEY_INSIGHTS` | 提取关键洞察 | 发现文档中的重要观点 |
|
||||
| `TABLE_OF_CONTENTS` | 生成目录 | 了解文档结构和主要话题 |
|
||||
| `REFLECTIONS` | 生成反思问题 | 促进深度思考的问题 |
|
||||
|
||||
## 基本使用方法
|
||||
|
||||
### 1. 单个转换
|
||||
|
||||
```typescript
|
||||
import { runTransformation, TransformationType } from './transformations';
|
||||
|
||||
async function performTransformation() {
|
||||
const result = await runTransformation({
|
||||
content: "你的文档内容...",
|
||||
transformationType: TransformationType.SIMPLE_SUMMARY,
|
||||
settings: yourInfioSettings
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('转换结果:', result.result);
|
||||
} else {
|
||||
console.error('转换失败:', result.error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 批量转换
|
||||
|
||||
```typescript
|
||||
import { runBatchTransformations, TransformationType } from './transformations';
|
||||
|
||||
async function performBatchTransformations() {
|
||||
const results = await runBatchTransformations(
|
||||
"你的文档内容...",
|
||||
[
|
||||
TransformationType.SIMPLE_SUMMARY,
|
||||
TransformationType.KEY_INSIGHTS,
|
||||
TransformationType.TABLE_OF_CONTENTS
|
||||
],
|
||||
yourInfioSettings
|
||||
);
|
||||
|
||||
// 处理每个转换的结果
|
||||
Object.entries(results).forEach(([type, result]) => {
|
||||
if (result.success) {
|
||||
console.log(`${type}:`, result.result);
|
||||
} else {
|
||||
console.error(`${type} 失败:`, result.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 处理长文档
|
||||
|
||||
```typescript
|
||||
const result = await runTransformation({
|
||||
content: veryLongDocument,
|
||||
transformationType: TransformationType.DENSE_SUMMARY,
|
||||
settings: yourInfioSettings,
|
||||
maxContentLength: 30000 // 限制最大处理长度
|
||||
});
|
||||
|
||||
if (result.truncated) {
|
||||
console.log(`文档被截断: ${result.originalLength} -> ${result.processedLength} 字符`);
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### TransformationParams
|
||||
|
||||
```typescript
|
||||
interface TransformationParams {
|
||||
content: string; // 要转换的文档内容
|
||||
transformationType: TransformationType; // 转换类型
|
||||
settings: InfioSettings; // 应用设置
|
||||
model?: LLMModel; // 可选:指定使用的模型
|
||||
maxContentLength?: number; // 可选:最大内容长度限制
|
||||
}
|
||||
```
|
||||
|
||||
### TransformationResult
|
||||
|
||||
```typescript
|
||||
interface TransformationResult {
|
||||
success: boolean; // 转换是否成功
|
||||
result?: string; // 转换结果(成功时)
|
||||
error?: string; // 错误信息(失败时)
|
||||
truncated?: boolean; // 内容是否被截断
|
||||
originalLength?: number; // 原始内容长度
|
||||
processedLength?: number; // 处理后内容长度
|
||||
}
|
||||
```
|
||||
|
||||
## 文档大小处理
|
||||
|
||||
系统会自动处理过长的文档:
|
||||
|
||||
- **默认限制**:50,000 字符
|
||||
- **最小长度**:100 字符
|
||||
- **智能截断**:尝试在句子或段落边界处截断
|
||||
- **保护机制**:确保截断后不会丢失过多内容
|
||||
|
||||
## 错误处理
|
||||
|
||||
常见的错误情况及处理:
|
||||
|
||||
- **空内容**:返回错误信息 "内容不能为空"
|
||||
- **内容过短**:内容少于 100 字符时返回错误
|
||||
- **不支持的转换类型**:返回相应错误信息
|
||||
- **LLM 调用失败**:返回具体的调用错误信息
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **内容验证**:在调用前确保内容不为空且长度适当
|
||||
2. **错误处理**:始终检查 `result.success` 状态
|
||||
3. **截断提示**:检查 `result.truncated` 以了解是否有内容被截断
|
||||
4. **批量处理**:对于多种转换,使用 `runBatchTransformations` 提高效率
|
||||
5. **模型选择**:根据需要选择合适的 LLM 模型
|
||||
|
||||
## 集成示例
|
||||
|
||||
```typescript
|
||||
// 在你的组件或服务中
|
||||
import {
|
||||
runTransformation,
|
||||
TransformationType,
|
||||
getAvailableTransformations
|
||||
} from './core/prompts/transformations';
|
||||
|
||||
class DocumentProcessor {
|
||||
constructor(private settings: InfioSettings) {}
|
||||
|
||||
async processDocument(content: string, type: TransformationType) {
|
||||
try {
|
||||
const result = await runTransformation({
|
||||
content,
|
||||
transformationType: type,
|
||||
settings: this.settings
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
return result.result;
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('文档处理失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getAvailableTransformations() {
|
||||
return getAvailableTransformations();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 确保已正确配置 LLM 提供商的 API 密钥
|
||||
- 转换质量依赖于所选择的 LLM 模型
|
||||
- 处理大文档时可能需要较长时间
|
||||
- 某些转换类型对特定类型的内容效果更好(如 `ANALYZE_PAPER` 适用于学术论文)
|
||||
|
||||
## 故障排除
|
||||
|
||||
1. **LLM 调用失败**:检查 API 密钥和网络连接
|
||||
2. **转换结果为空**:可能是内容过短或模型无法理解内容
|
||||
3. **内容被意外截断**:调整 `maxContentLength` 参数
|
||||
4. **特定转换效果不佳**:尝试其他转换类型或检查内容是否适合该转换
|
||||
389
src/core/transformations/run_trans.ts
Normal file
389
src/core/transformations/run_trans.ts
Normal file
@ -0,0 +1,389 @@
|
||||
import { Result, err, ok } from "neverthrow";
|
||||
|
||||
import { LLMModel } from '../../types/llm/model';
|
||||
import { RequestMessage } from '../../types/llm/request';
|
||||
import { InfioSettings } from '../../types/settings';
|
||||
import { tokenCount } from '../../utils/token';
|
||||
import LLMManager from '../llm/manager';
|
||||
import { ANALYZE_PAPER_DESCRIPTION, ANALYZE_PAPER_PROMPT } from '../prompts/transformations/analyze-paper';
|
||||
import { DENSE_SUMMARY_DESCRIPTION, DENSE_SUMMARY_PROMPT } from '../prompts/transformations/dense-summary';
|
||||
import { KEY_INSIGHTS_DESCRIPTION, KEY_INSIGHTS_PROMPT } from '../prompts/transformations/key-insights';
|
||||
import { REFLECTIONS_DESCRIPTION, REFLECTIONS_PROMPT } from '../prompts/transformations/reflections';
|
||||
import { SIMPLE_SUMMARY_DESCRIPTION, SIMPLE_SUMMARY_PROMPT } from '../prompts/transformations/simple-summary';
|
||||
import { TABLE_OF_CONTENTS_DESCRIPTION, TABLE_OF_CONTENTS_PROMPT } from '../prompts/transformations/table-of-contents';
|
||||
|
||||
// 转换类型枚举
|
||||
export enum TransformationType {
|
||||
DENSE_SUMMARY = 'dense-summary',
|
||||
ANALYZE_PAPER = 'analyze-paper',
|
||||
SIMPLE_SUMMARY = 'simple-summary',
|
||||
KEY_INSIGHTS = 'key-insights',
|
||||
TABLE_OF_CONTENTS = 'table-of-contents',
|
||||
REFLECTIONS = 'reflections'
|
||||
}
|
||||
|
||||
// 转换配置接口
|
||||
export interface TransformationConfig {
|
||||
type: TransformationType;
|
||||
prompt: string;
|
||||
description: string;
|
||||
maxTokens?: number;
|
||||
}
|
||||
|
||||
// 所有可用的转换配置
|
||||
export const TRANSFORMATIONS: Record<TransformationType, TransformationConfig> = {
|
||||
[TransformationType.DENSE_SUMMARY]: {
|
||||
type: TransformationType.DENSE_SUMMARY,
|
||||
prompt: DENSE_SUMMARY_PROMPT,
|
||||
description: DENSE_SUMMARY_DESCRIPTION,
|
||||
maxTokens: 4000
|
||||
},
|
||||
[TransformationType.ANALYZE_PAPER]: {
|
||||
type: TransformationType.ANALYZE_PAPER,
|
||||
prompt: ANALYZE_PAPER_PROMPT,
|
||||
description: ANALYZE_PAPER_DESCRIPTION,
|
||||
maxTokens: 3000
|
||||
},
|
||||
[TransformationType.SIMPLE_SUMMARY]: {
|
||||
type: TransformationType.SIMPLE_SUMMARY,
|
||||
prompt: SIMPLE_SUMMARY_PROMPT,
|
||||
description: SIMPLE_SUMMARY_DESCRIPTION,
|
||||
maxTokens: 2000
|
||||
},
|
||||
[TransformationType.KEY_INSIGHTS]: {
|
||||
type: TransformationType.KEY_INSIGHTS,
|
||||
prompt: KEY_INSIGHTS_PROMPT,
|
||||
description: KEY_INSIGHTS_DESCRIPTION,
|
||||
maxTokens: 3000
|
||||
},
|
||||
[TransformationType.TABLE_OF_CONTENTS]: {
|
||||
type: TransformationType.TABLE_OF_CONTENTS,
|
||||
prompt: TABLE_OF_CONTENTS_PROMPT,
|
||||
description: TABLE_OF_CONTENTS_DESCRIPTION,
|
||||
maxTokens: 2000
|
||||
},
|
||||
[TransformationType.REFLECTIONS]: {
|
||||
type: TransformationType.REFLECTIONS,
|
||||
prompt: REFLECTIONS_PROMPT,
|
||||
description: REFLECTIONS_DESCRIPTION,
|
||||
maxTokens: 2500
|
||||
}
|
||||
};
|
||||
|
||||
// 转换参数接口
|
||||
export interface TransformationParams {
|
||||
content: string;
|
||||
transformationType: TransformationType;
|
||||
settings: InfioSettings;
|
||||
model?: LLMModel;
|
||||
maxContentTokens?: number;
|
||||
}
|
||||
|
||||
// 转换结果接口
|
||||
export interface TransformationResult {
|
||||
success: boolean;
|
||||
result?: string;
|
||||
error?: string;
|
||||
truncated?: boolean;
|
||||
originalTokens?: number;
|
||||
processedTokens?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM 客户端类,用于与语言模型交互
|
||||
*/
|
||||
class TransformationLLMClient {
|
||||
private llm: LLMManager;
|
||||
private model: LLMModel;
|
||||
|
||||
constructor(llm: LLMManager, model: LLMModel) {
|
||||
this.llm = llm;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
async queryChatModel(messages: RequestMessage[]): Promise<Result<string, Error>> {
|
||||
try {
|
||||
const stream = await this.llm.streamResponse(
|
||||
this.model,
|
||||
{
|
||||
messages: messages,
|
||||
model: this.model.modelId,
|
||||
stream: true,
|
||||
}
|
||||
);
|
||||
|
||||
let response_content = "";
|
||||
for await (const chunk of stream) {
|
||||
const content = chunk.choices[0]?.delta?.content ?? '';
|
||||
response_content += content;
|
||||
}
|
||||
return ok(response_content);
|
||||
} catch (error) {
|
||||
return err(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档内容处理类
|
||||
*/
|
||||
class DocumentProcessor {
|
||||
private static readonly DEFAULT_MAX_TOKENS = 12000; // 默认最大 token 数
|
||||
private static readonly MIN_CONTENT_LENGTH = 100; // 最小内容长度(字符数)
|
||||
|
||||
/**
|
||||
* 检查和处理文档内容大小
|
||||
*/
|
||||
static async processContent(content: string, maxTokens: number = this.DEFAULT_MAX_TOKENS): Promise<{
|
||||
processedContent: string;
|
||||
truncated: boolean;
|
||||
originalTokens: number;
|
||||
processedTokens: number;
|
||||
}> {
|
||||
const originalTokens = await tokenCount(content);
|
||||
|
||||
if (originalTokens <= maxTokens) {
|
||||
return {
|
||||
processedContent: content,
|
||||
truncated: false,
|
||||
originalTokens,
|
||||
processedTokens: originalTokens
|
||||
};
|
||||
}
|
||||
|
||||
// 智能截断:基于 token 数量和内容边界
|
||||
// 先按字符比例粗略估算截断位置
|
||||
const estimatedCharRatio = content.length / originalTokens;
|
||||
const estimatedCharLimit = Math.floor(maxTokens * estimatedCharRatio * 0.9); // 留一些缓冲
|
||||
|
||||
let truncatedContent = content.substring(0, estimatedCharLimit);
|
||||
|
||||
// 查找最后一个完整句子的结束位置
|
||||
const lastSentenceEnd = Math.max(
|
||||
truncatedContent.lastIndexOf('.'),
|
||||
truncatedContent.lastIndexOf('!'),
|
||||
truncatedContent.lastIndexOf('?'),
|
||||
truncatedContent.lastIndexOf('。'),
|
||||
truncatedContent.lastIndexOf('!'),
|
||||
truncatedContent.lastIndexOf('?')
|
||||
);
|
||||
|
||||
// 查找最后一个段落的结束位置
|
||||
const lastParagraphEnd = truncatedContent.lastIndexOf('\n\n');
|
||||
|
||||
// 选择最合适的截断位置
|
||||
const cutoffPosition = Math.max(lastSentenceEnd, lastParagraphEnd);
|
||||
|
||||
if (cutoffPosition > estimatedCharLimit * 0.8) { // 如果截断位置不会丢失太多内容
|
||||
truncatedContent = content.substring(0, cutoffPosition + 1);
|
||||
}
|
||||
|
||||
// 确保截断后的内容不会太短
|
||||
if (truncatedContent.length < this.MIN_CONTENT_LENGTH) {
|
||||
// 按字符比例回退到安全长度
|
||||
const safeCharLimit = Math.max(this.MIN_CONTENT_LENGTH, Math.floor(maxTokens * estimatedCharRatio * 0.8));
|
||||
truncatedContent = content.substring(0, Math.min(safeCharLimit, content.length));
|
||||
}
|
||||
|
||||
// 验证最终的 token 数量
|
||||
const finalTokens = await tokenCount(truncatedContent);
|
||||
|
||||
// 如果仍然超过限制,进行更精确的截断
|
||||
if (finalTokens > maxTokens) {
|
||||
const adjustedRatio = truncatedContent.length / finalTokens;
|
||||
const adjustedCharLimit = Math.floor(maxTokens * adjustedRatio);
|
||||
truncatedContent = content.substring(0, adjustedCharLimit);
|
||||
}
|
||||
|
||||
const processedTokens = await tokenCount(truncatedContent);
|
||||
|
||||
return {
|
||||
processedContent: truncatedContent,
|
||||
truncated: true,
|
||||
originalTokens,
|
||||
processedTokens
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证内容是否适合处理
|
||||
*/
|
||||
static validateContent(content: string): Result<void, Error> {
|
||||
if (!content || content.trim().length === 0) {
|
||||
return err(new Error('内容不能为空'));
|
||||
}
|
||||
|
||||
if (content.length < this.MIN_CONTENT_LENGTH) {
|
||||
return err(new Error(`内容长度至少需要 ${this.MIN_CONTENT_LENGTH} 个字符`));
|
||||
}
|
||||
|
||||
return ok(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主要的转换执行函数
|
||||
*/
|
||||
export async function runTransformation(params: TransformationParams): Promise<TransformationResult> {
|
||||
const { content, transformationType, settings, model, maxContentTokens } = params;
|
||||
|
||||
try {
|
||||
// 验证内容
|
||||
const contentValidation = DocumentProcessor.validateContent(content);
|
||||
if (contentValidation.isErr()) {
|
||||
return {
|
||||
success: false,
|
||||
error: contentValidation.error.message
|
||||
};
|
||||
}
|
||||
|
||||
// 获取转换配置
|
||||
const transformationConfig = TRANSFORMATIONS[transformationType];
|
||||
if (!transformationConfig) {
|
||||
return {
|
||||
success: false,
|
||||
error: `不支持的转换类型: ${transformationType}`
|
||||
};
|
||||
}
|
||||
|
||||
// 处理文档内容(检查 token 数量并截断)
|
||||
const tokenLimit = maxContentTokens || DocumentProcessor['DEFAULT_MAX_TOKENS'];
|
||||
const processedDocument = await DocumentProcessor.processContent(content, tokenLimit);
|
||||
|
||||
// 使用默认模型或传入的模型
|
||||
const llmModel: LLMModel = model || {
|
||||
provider: settings.applyModelProvider,
|
||||
modelId: settings.applyModelId,
|
||||
};
|
||||
|
||||
// 创建 LLM 管理器和客户端
|
||||
const llmManager = new LLMManager(settings);
|
||||
const client = new TransformationLLMClient(llmManager, llmModel);
|
||||
|
||||
// 构建请求消息
|
||||
const messages: RequestMessage[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: transformationConfig.prompt
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: processedDocument.processedContent
|
||||
}
|
||||
];
|
||||
|
||||
// 调用 LLM 执行转换
|
||||
const result = await client.queryChatModel(messages);
|
||||
|
||||
if (result.isErr()) {
|
||||
return {
|
||||
success: false,
|
||||
error: `LLM 调用失败: ${result.error.message}`,
|
||||
truncated: processedDocument.truncated,
|
||||
originalTokens: processedDocument.originalTokens,
|
||||
processedTokens: processedDocument.processedTokens
|
||||
};
|
||||
}
|
||||
|
||||
// 后处理结果
|
||||
const processedResult = postProcessResult(result.value, transformationType);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result: processedResult,
|
||||
truncated: processedDocument.truncated,
|
||||
originalTokens: processedDocument.originalTokens,
|
||||
processedTokens: processedDocument.processedTokens
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `转换过程中出现错误: ${error instanceof Error ? error.message : String(error)}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 后处理转换结果
|
||||
*/
|
||||
function postProcessResult(result: string, transformationType: TransformationType): string {
|
||||
let processed = result.trim();
|
||||
|
||||
// 移除可能的 markdown 代码块标记
|
||||
processed = processed.replace(/^```[\w]*\n/, '').replace(/\n```$/, '');
|
||||
|
||||
// 根据转换类型进行特定的后处理
|
||||
switch (transformationType) {
|
||||
case TransformationType.KEY_INSIGHTS:
|
||||
// 确保 insights 格式正确
|
||||
if (!processed.includes('INSIGHTS')) {
|
||||
processed = `# INSIGHTS\n\n${processed}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case TransformationType.REFLECTIONS:
|
||||
// 确保 reflections 格式正确
|
||||
if (!processed.includes('REFLECTIONS')) {
|
||||
processed = `# REFLECTIONS\n\n${processed}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case TransformationType.ANALYZE_PAPER: {
|
||||
// 确保论文分析包含所有必需的部分
|
||||
const requiredSections = ['PURPOSE', 'CONTRIBUTION', 'KEY FINDINGS', 'IMPLICATIONS', 'LIMITATIONS'];
|
||||
const hasAllSections = requiredSections.every(section =>
|
||||
processed.toUpperCase().includes(section)
|
||||
);
|
||||
|
||||
if (!hasAllSections) {
|
||||
// 如果缺少某些部分,添加提示
|
||||
processed += '\n\n*注意:某些分析部分可能不完整,建议重新处理或检查原始内容。*';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量执行转换
|
||||
*/
|
||||
export async function runBatchTransformations(
|
||||
content: string,
|
||||
transformationTypes: TransformationType[],
|
||||
settings: InfioSettings,
|
||||
model?: LLMModel
|
||||
): Promise<Record<string, TransformationResult>> {
|
||||
const results: Record<string, TransformationResult> = {};
|
||||
|
||||
// 并行执行所有转换
|
||||
const promises = transformationTypes.map(async (type) => {
|
||||
const result = await runTransformation({
|
||||
content,
|
||||
transformationType: type,
|
||||
settings,
|
||||
model
|
||||
});
|
||||
return { type, result };
|
||||
});
|
||||
|
||||
const completedResults = await Promise.all(promises);
|
||||
|
||||
for (const { type, result } of completedResults) {
|
||||
results[type] = result;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的转换类型和描述
|
||||
*/
|
||||
export function getAvailableTransformations(): Array<{ type: TransformationType, description: string }> {
|
||||
return Object.values(TRANSFORMATIONS).map(config => ({
|
||||
type: config.type,
|
||||
description: config.description
|
||||
}));
|
||||
}
|
||||
181
src/core/transformations/usage-example.ts
Normal file
181
src/core/transformations/usage-example.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import { InfioSettings } from '../../types/settings';
|
||||
|
||||
import {
|
||||
TransformationType,
|
||||
getAvailableTransformations,
|
||||
runBatchTransformations,
|
||||
runTransformation,
|
||||
} from './run_trans';
|
||||
|
||||
/**
|
||||
* 使用示例:单个转换
|
||||
*/
|
||||
export async function exampleSingleTransformation(settings: InfioSettings) {
|
||||
const sampleContent = `
|
||||
人工智能技术正在快速发展,特别是大型语言模型的出现,彻底改变了我们与计算机交互的方式。
|
||||
这些模型能够理解和生成人类语言,在多个领域展现出令人印象深刻的能力。
|
||||
|
||||
然而,随着AI技术的普及,我们也面临着新的挑战,包括伦理问题、隐私保护、
|
||||
以及如何确保AI技术的安全和可控发展。这些问题需要全社会的共同关注和努力。
|
||||
|
||||
未来,人工智能将继续在教育、医疗、商业等领域发挥重要作用,
|
||||
但我们必须在推进技术发展的同时,确保技术服务于人类的福祉。
|
||||
`;
|
||||
|
||||
try {
|
||||
// 执行简单摘要转换
|
||||
const result = await runTransformation({
|
||||
content: sampleContent,
|
||||
transformationType: TransformationType.SIMPLE_SUMMARY,
|
||||
settings: settings
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('转换成功!');
|
||||
console.log('结果:', result.result);
|
||||
|
||||
if (result.truncated) {
|
||||
console.log(`注意:内容被截断 (${result.originalLength} -> ${result.processedLength} 字符)`);
|
||||
}
|
||||
} else {
|
||||
console.error('转换失败:', result.error);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('执行转换时出错:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用示例:批量转换
|
||||
*/
|
||||
export async function exampleBatchTransformations(settings: InfioSettings) {
|
||||
const sampleContent = `
|
||||
区块链技术作为一种分布式账本技术,具有去中心化、不可篡改、透明公开等特点。
|
||||
它最初是为比特币而设计的底层技术,但现在已经扩展到各个行业和应用场景。
|
||||
|
||||
在金融领域,区块链可以用于跨境支付、供应链金融、数字货币等;
|
||||
在供应链管理中,它能够提供产品溯源和防伪验证;
|
||||
在数字身份认证方面,区块链可以建立更安全可靠的身份管理系统。
|
||||
|
||||
尽管区块链技术有很多优势,但它也面临着可扩展性、能耗、监管等挑战。
|
||||
随着技术的不断成熟和完善,相信这些问题会逐步得到解决。
|
||||
区块链技术的未来发展值得期待,它将为数字经济的发展提供重要的技术支撑。
|
||||
`;
|
||||
|
||||
try {
|
||||
// 同时执行多种转换
|
||||
const transformationTypes = [
|
||||
TransformationType.SIMPLE_SUMMARY,
|
||||
TransformationType.KEY_INSIGHTS,
|
||||
TransformationType.TABLE_OF_CONTENTS
|
||||
];
|
||||
|
||||
const results = await runBatchTransformations(
|
||||
sampleContent,
|
||||
transformationTypes,
|
||||
settings
|
||||
);
|
||||
|
||||
console.log('批量转换完成!');
|
||||
|
||||
for (const [type, result] of Object.entries(results)) {
|
||||
console.log(`\n=== ${type.toUpperCase()} ===`);
|
||||
if (result.success) {
|
||||
console.log(result.result);
|
||||
} else {
|
||||
console.error('失败:', result.error);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('执行批量转换时出错:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用示例:处理长文档(会被截断)
|
||||
*/
|
||||
export async function exampleLongDocumentProcessing(settings: InfioSettings) {
|
||||
// 模拟一个很长的文档
|
||||
const longContent = '这是一个很长的文档内容。'.repeat(10000); // 约50万字符
|
||||
|
||||
try {
|
||||
const result = await runTransformation({
|
||||
content: longContent,
|
||||
transformationType: TransformationType.DENSE_SUMMARY,
|
||||
settings: settings,
|
||||
maxContentLength: 30000 // 设置最大内容长度
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('长文档转换成功!');
|
||||
console.log('原始长度:', result.originalLength);
|
||||
console.log('处理后长度:', result.processedLength);
|
||||
console.log('是否被截断:', result.truncated);
|
||||
console.log('结果长度:', result.result?.length);
|
||||
} else {
|
||||
console.error('转换失败:', result.error);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('处理长文档时出错:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用示例:获取所有可用的转换类型
|
||||
*/
|
||||
export function exampleGetAvailableTransformations() {
|
||||
const availableTransformations = getAvailableTransformations();
|
||||
|
||||
console.log('可用的转换类型:');
|
||||
availableTransformations.forEach((transformation, index) => {
|
||||
console.log(`${index + 1}. ${transformation.type}: ${transformation.description}`);
|
||||
});
|
||||
|
||||
return availableTransformations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用示例:错误处理
|
||||
*/
|
||||
export async function exampleErrorHandling(settings: InfioSettings) {
|
||||
try {
|
||||
// 测试空内容
|
||||
const emptyResult = await runTransformation({
|
||||
content: '',
|
||||
transformationType: TransformationType.SIMPLE_SUMMARY,
|
||||
settings: settings
|
||||
});
|
||||
|
||||
console.log('空内容测试:', emptyResult);
|
||||
|
||||
// 测试太短的内容
|
||||
const shortResult = await runTransformation({
|
||||
content: '太短',
|
||||
transformationType: TransformationType.SIMPLE_SUMMARY,
|
||||
settings: settings
|
||||
});
|
||||
|
||||
console.log('短内容测试:', shortResult);
|
||||
|
||||
// 测试无效的转换类型(需要类型断言来测试)
|
||||
const invalidResult = await runTransformation({
|
||||
content: '这是一些测试内容,用于测试无效的转换类型处理。',
|
||||
transformationType: 'invalid-type' as TransformationType,
|
||||
settings: settings
|
||||
});
|
||||
|
||||
console.log('无效类型测试:', invalidResult);
|
||||
|
||||
} catch (error) {
|
||||
console.error('错误处理测试时出错:', error);
|
||||
}
|
||||
}
|
||||
@ -3,4 +3,5 @@ export const COMMAND_DIR = 'commands'
|
||||
export const CHAT_DIR = 'chats'
|
||||
export const CUSTOM_MODE_DIR = 'custom_modes'
|
||||
export const CONVERT_DATA_DIR = 'convert_data'
|
||||
export const WORKSPACE_DIR = 'workspaces'
|
||||
export const INITIAL_MIGRATION_MARKER = '.initial_migration_completed'
|
||||
|
||||
185
src/database/json/workspace/WorkspaceManager.ts
Normal file
185
src/database/json/workspace/WorkspaceManager.ts
Normal file
@ -0,0 +1,185 @@
|
||||
import { App } from 'obsidian'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { AbstractJsonRepository } from '../base'
|
||||
import { ROOT_DIR, WORKSPACE_DIR } from '../constants'
|
||||
|
||||
import {
|
||||
WORKSPACE_SCHEMA_VERSION,
|
||||
Workspace,
|
||||
WorkspaceMetadata
|
||||
} from './types'
|
||||
|
||||
export class WorkspaceManager extends AbstractJsonRepository<
|
||||
Workspace,
|
||||
WorkspaceMetadata
|
||||
> {
|
||||
constructor(app: App) {
|
||||
super(app, `${ROOT_DIR}/${WORKSPACE_DIR}`)
|
||||
}
|
||||
|
||||
protected generateFileName(workspace: Workspace): string {
|
||||
// Format: v{schemaVersion}_{name}_{updatedAt}_{id}.json
|
||||
const encodedName = encodeURIComponent(workspace.name)
|
||||
return `v${workspace.schemaVersion}_${encodedName}_${workspace.updatedAt}_${workspace.id}.json`
|
||||
}
|
||||
|
||||
protected parseFileName(fileName: string): WorkspaceMetadata | null {
|
||||
// Parse: v{schemaVersion}_{name}_{updatedAt}_{id}.json
|
||||
const regex = new RegExp(
|
||||
`^v${WORKSPACE_SCHEMA_VERSION}_(.+)_(\\d+)_([0-9a-f-]+)\\.json$`,
|
||||
)
|
||||
const match = fileName.match(regex)
|
||||
if (!match) return null
|
||||
|
||||
const name = decodeURIComponent(match[1])
|
||||
const updatedAt = parseInt(match[2], 10)
|
||||
const id = match[3]
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
updatedAt,
|
||||
createdAt: 0,
|
||||
schemaVersion: WORKSPACE_SCHEMA_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
public async createWorkspace(
|
||||
initialData: Partial<Workspace>,
|
||||
): Promise<Workspace> {
|
||||
const now = Date.now()
|
||||
const newWorkspace: Workspace = {
|
||||
id: uuidv4(),
|
||||
name: 'New Workspace',
|
||||
content: [],
|
||||
chatHistory: [],
|
||||
metadata: {},
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
schemaVersion: WORKSPACE_SCHEMA_VERSION,
|
||||
...initialData,
|
||||
}
|
||||
|
||||
await this.create(newWorkspace)
|
||||
return newWorkspace
|
||||
}
|
||||
|
||||
public async findById(id: string): Promise<Workspace | 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<Workspace | 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 updateWorkspace(
|
||||
id: string,
|
||||
updates: Partial<
|
||||
Omit<Workspace, 'id' | 'createdAt' | 'updatedAt' | 'schemaVersion'>
|
||||
>,
|
||||
): Promise<Workspace | null> {
|
||||
const workspace = await this.findById(id)
|
||||
if (!workspace) return null
|
||||
|
||||
const updatedWorkspace: Workspace = {
|
||||
...workspace,
|
||||
...updates,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
|
||||
await this.update(workspace, updatedWorkspace)
|
||||
return updatedWorkspace
|
||||
}
|
||||
|
||||
public async deleteWorkspace(id: string): Promise<boolean> {
|
||||
const allMetadata = await this.listMetadata()
|
||||
const targetMetadata = allMetadata.find((meta) => meta.id === id)
|
||||
if (!targetMetadata) return false
|
||||
|
||||
await this.delete(targetMetadata.fileName)
|
||||
return true
|
||||
}
|
||||
|
||||
public async listWorkspaces(): Promise<WorkspaceMetadata[]> {
|
||||
const metadata = await this.listMetadata()
|
||||
const sorted = metadata.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
return sorted
|
||||
}
|
||||
|
||||
public async addChatToWorkspace(
|
||||
workspaceId: string,
|
||||
chatId: string,
|
||||
chatTitle: string
|
||||
): Promise<Workspace | null> {
|
||||
const workspace = await this.findById(workspaceId)
|
||||
if (!workspace) return null
|
||||
|
||||
const existingChatIndex = workspace.chatHistory.findIndex(
|
||||
chat => chat.id === chatId
|
||||
)
|
||||
|
||||
if (existingChatIndex >= 0) {
|
||||
// 更新已存在的聊天标题
|
||||
workspace.chatHistory[existingChatIndex].title = chatTitle
|
||||
} else {
|
||||
// 添加新聊天
|
||||
workspace.chatHistory.push({ id: chatId, title: chatTitle })
|
||||
}
|
||||
|
||||
return this.updateWorkspace(workspaceId, {
|
||||
chatHistory: workspace.chatHistory
|
||||
})
|
||||
}
|
||||
|
||||
public async removeChatFromWorkspace(
|
||||
workspaceId: string,
|
||||
chatId: string
|
||||
): Promise<Workspace | null> {
|
||||
const workspace = await this.findById(workspaceId)
|
||||
if (!workspace) return null
|
||||
|
||||
workspace.chatHistory = workspace.chatHistory.filter(
|
||||
chat => chat.id !== chatId
|
||||
)
|
||||
|
||||
return this.updateWorkspace(workspaceId, {
|
||||
chatHistory: workspace.chatHistory
|
||||
})
|
||||
}
|
||||
|
||||
public async ensureDefaultVaultWorkspace(): Promise<Workspace> {
|
||||
// 检查是否已存在默认的 vault 工作区
|
||||
const existingVault = await this.findByName('vault')
|
||||
if (existingVault) {
|
||||
return existingVault
|
||||
}
|
||||
|
||||
// 创建默认的 vault 工作区
|
||||
const defaultWorkspace = await this.createWorkspace({
|
||||
name: 'vault',
|
||||
content: [
|
||||
{
|
||||
type: 'folder',
|
||||
content: '/' // 整个 vault 根目录
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
isDefault: true,
|
||||
description: 'all vault as workspace'
|
||||
}
|
||||
})
|
||||
|
||||
return defaultWorkspace
|
||||
}
|
||||
}
|
||||
2
src/database/json/workspace/index.ts
Normal file
2
src/database/json/workspace/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './types'
|
||||
export * from './WorkspaceManager'
|
||||
30
src/database/json/workspace/types.ts
Normal file
30
src/database/json/workspace/types.ts
Normal file
@ -0,0 +1,30 @@
|
||||
export const WORKSPACE_SCHEMA_VERSION = 1
|
||||
|
||||
export interface WorkspaceContent {
|
||||
type: 'tag' | 'folder'
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface WorkspaceChatHistory {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
id: string
|
||||
name: string
|
||||
content: WorkspaceContent[]
|
||||
chatHistory: WorkspaceChatHistory[]
|
||||
metadata: Record<string, any>
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
schemaVersion: number
|
||||
}
|
||||
|
||||
export interface WorkspaceMetadata {
|
||||
id: string
|
||||
name: string
|
||||
updatedAt: number
|
||||
createdAt: number
|
||||
schemaVersion: number
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { backOff } from 'exponential-backoff'
|
||||
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'
|
||||
import { MarkdownTextSplitter } from 'langchain/text_splitter'
|
||||
import { minimatch } from 'minimatch'
|
||||
import { App, Notice, TFile } from 'obsidian'
|
||||
import pLimit from 'p-limit'
|
||||
@ -111,17 +111,10 @@ export class VectorManager {
|
||||
return
|
||||
}
|
||||
|
||||
const textSplitter = RecursiveCharacterTextSplitter.fromLanguage(
|
||||
'markdown',
|
||||
{
|
||||
chunkSize: options.chunkSize,
|
||||
// TODO: Use token-based chunking after migrating to WebAssembly-based tiktoken
|
||||
// Current token counting method is too slow for practical use
|
||||
// lengthFunction: async (text) => {
|
||||
// return await tokenCount(text)
|
||||
// },
|
||||
},
|
||||
)
|
||||
const textSplitter = new MarkdownTextSplitter({
|
||||
chunkSize: options.chunkSize,
|
||||
chunkOverlap: Math.floor(options.chunkSize * 0.15)
|
||||
})
|
||||
|
||||
const skippedFiles: string[] = []
|
||||
const contentChunks: InsertVector[] = (
|
||||
@ -323,12 +316,10 @@ export class VectorManager {
|
||||
)
|
||||
|
||||
// Embed the files
|
||||
const textSplitter = RecursiveCharacterTextSplitter.fromLanguage(
|
||||
'markdown',
|
||||
{
|
||||
chunkSize,
|
||||
},
|
||||
)
|
||||
const textSplitter = new MarkdownTextSplitter({
|
||||
chunkSize: chunkSize,
|
||||
chunkOverlap: Math.floor(chunkSize * 0.15)
|
||||
});
|
||||
let fileContent = await this.app.vault.cachedRead(file)
|
||||
// 清理null字节,防止PostgreSQL UTF8编码错误
|
||||
fileContent = fileContent.replace(/\0/g, '')
|
||||
|
||||
@ -54,16 +54,18 @@ const localeMap: { [k: string]: Partial<typeof en> } = {
|
||||
hu,
|
||||
};
|
||||
|
||||
const locale = localeMap[moment.locale()];
|
||||
|
||||
export function t(str: string, params?: Record<string, any>): any {
|
||||
// 动态获取当前语言
|
||||
const currentLocale = moment.locale();
|
||||
const locale = localeMap[currentLocale];
|
||||
|
||||
if (!locale) {
|
||||
console.error({
|
||||
plugin: "infio-copilot",
|
||||
fn: t,
|
||||
where: "src/lang/helpers.ts",
|
||||
message: "Error: locale not found",
|
||||
data: moment.locale(),
|
||||
data: currentLocale,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -485,5 +485,63 @@ export default {
|
||||
parameters: "Parameters",
|
||||
toolNoDescription: "No description",
|
||||
useMcpToolFrom: "Use MCP tool from",
|
||||
},
|
||||
workspace: {
|
||||
title: "Workspace Management",
|
||||
shortTitle: "Workspace",
|
||||
description: "Manage and switch between different workspaces (note vaults)",
|
||||
createNew: "Create New Workspace",
|
||||
recentWorkspaces: "Recent Workspaces",
|
||||
loading: "Loading workspace list...",
|
||||
noWorkspaces: "No workspaces available",
|
||||
empty: "Empty workspace",
|
||||
editTooltip: "Edit workspace",
|
||||
deleteTooltip: "Delete workspace",
|
||||
refreshTooltip: "Refresh workspace list",
|
||||
newWorkspace: "New Workspace",
|
||||
notices: {
|
||||
alreadyInWorkspace: "Already in this workspace",
|
||||
switchedTo: "Switched to workspace: {name}",
|
||||
workspaceContent: "Workspace content: {content}",
|
||||
cannotDeleteCurrent: "Cannot delete current workspace",
|
||||
cannotDeleteDefault: "Cannot delete default workspace",
|
||||
deleted: "Deleted workspace: {name}",
|
||||
deleteFailed: "Failed to delete workspace",
|
||||
created: "Created workspace: {name}",
|
||||
createFailed: "Failed to create workspace",
|
||||
updated: "Updated workspace: {name}",
|
||||
updateFailed: "Failed to update workspace",
|
||||
refreshFailed: "Failed to refresh workspace list",
|
||||
switchFailed: "Failed to switch workspace"
|
||||
},
|
||||
deleteConfirm: 'Are you sure you want to delete workspace "{name}"? This action cannot be undone.',
|
||||
current: "Current",
|
||||
conversations: "conversations",
|
||||
created: "Created",
|
||||
updated: "Updated",
|
||||
folders: "folders",
|
||||
tags: "tags",
|
||||
noContent: "No content",
|
||||
editModal: {
|
||||
editTitle: "Edit Workspace",
|
||||
createTitle: "Create Workspace",
|
||||
nameLabel: "Workspace Name",
|
||||
namePlaceholder: "Enter workspace name",
|
||||
newNamePlaceholder: "Enter new workspace name",
|
||||
contentLabel: "Workspace Content",
|
||||
noContent: "No content, please add folders or tags",
|
||||
addPlaceholder: "Add folder or tag...",
|
||||
folder: "Folder",
|
||||
tag: "Tag",
|
||||
tip: "Tip: Enter keywords to search existing folders and tags, entries starting with # will be recognized as tags",
|
||||
cancel: "Cancel",
|
||||
save: "Save",
|
||||
create: "Create",
|
||||
saving: "Saving...",
|
||||
creating: "Creating...",
|
||||
nameRequired: "Workspace name cannot be empty",
|
||||
saveFailed: "Save failed, please try again",
|
||||
defaultName: "Workspace {date}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -487,5 +487,63 @@ export default {
|
||||
toolNoDescription: "无描述",
|
||||
useMcpToolFrom: "使用来自以下的 MCP 工具:",
|
||||
}
|
||||
},
|
||||
workspace: {
|
||||
title: "工作区管理",
|
||||
shortTitle: "工作区",
|
||||
description: "管理和切换不同的工作区(笔记库)",
|
||||
createNew: "创建新工作区",
|
||||
recentWorkspaces: "最近的工作区",
|
||||
loading: "正在加载工作区列表...",
|
||||
noWorkspaces: "暂无工作区",
|
||||
empty: "空工作区",
|
||||
editTooltip: "编辑工作区",
|
||||
deleteTooltip: "删除工作区",
|
||||
refreshTooltip: "刷新工作区列表",
|
||||
newWorkspace: "新建工作区",
|
||||
notices: {
|
||||
alreadyInWorkspace: "当前已在此工作区中",
|
||||
switchedTo: "切换到工作区: {name}",
|
||||
workspaceContent: "工作区内容: {content}",
|
||||
cannotDeleteCurrent: "无法删除当前工作区",
|
||||
cannotDeleteDefault: "无法删除默认工作区",
|
||||
deleted: "已删除工作区: {name}",
|
||||
deleteFailed: "删除工作区失败",
|
||||
created: "已创建工作区: {name}",
|
||||
createFailed: "创建工作区失败",
|
||||
updated: "已更新工作区: {name}",
|
||||
updateFailed: "更新工作区失败",
|
||||
refreshFailed: "刷新工作区列表失败",
|
||||
switchFailed: "切换工作区失败"
|
||||
},
|
||||
deleteConfirm: '确定要删除工作区 "{name}" 吗?此操作不可撤销。',
|
||||
current: "当前",
|
||||
conversations: "个对话",
|
||||
created: "创建",
|
||||
updated: "更新",
|
||||
folders: "个文件夹",
|
||||
tags: "个标签",
|
||||
noContent: "无内容",
|
||||
editModal: {
|
||||
editTitle: "编辑工作区",
|
||||
createTitle: "创建工作区",
|
||||
nameLabel: "工作区名称",
|
||||
namePlaceholder: "请输入工作区名称",
|
||||
newNamePlaceholder: "请输入新工作区名称",
|
||||
contentLabel: "工作区内容",
|
||||
noContent: "暂无内容,请添加文件夹或标签",
|
||||
addPlaceholder: "添加文件夹或标签...",
|
||||
folder: "文件夹",
|
||||
tag: "标签",
|
||||
tip: "提示:输入关键词搜索现有文件夹和标签,以 # 开头的会被识别为标签",
|
||||
cancel: "取消",
|
||||
save: "保存",
|
||||
create: "创建",
|
||||
saving: "保存中...",
|
||||
creating: "创建中...",
|
||||
nameRequired: "工作区名称不能为空",
|
||||
saveFailed: "保存失败,请重试",
|
||||
defaultName: "工作区 {date}"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
47
src/main.ts
47
src/main.ts
@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import { EditorView } from '@codemirror/view'
|
||||
// import { PGlite } from '@electric-sql/pglite'
|
||||
import { Editor, MarkdownView, Notice, Plugin, TFile } from 'obsidian'
|
||||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, TFile } from 'obsidian'
|
||||
|
||||
import { ApplyView } from './ApplyView'
|
||||
import { ChatView } from './ChatView'
|
||||
@ -31,6 +31,7 @@ import {
|
||||
InfioSettings,
|
||||
parseInfioSettings,
|
||||
} from './types/settings'
|
||||
import { createDataviewManager, DataviewManager } from './utils/dataview'
|
||||
import { getMentionableBlockData } from './utils/obsidian'
|
||||
import './utils/path'
|
||||
import { onEnt } from './utils/web-search'
|
||||
@ -50,6 +51,7 @@ export default class InfioPlugin extends Plugin {
|
||||
ragEngine: RAGEngine | null = null
|
||||
inlineEdit: InlineEdit | null = null
|
||||
diffStrategy?: DiffStrategy
|
||||
dataviewManager: DataviewManager | null = null
|
||||
|
||||
async onload() {
|
||||
// load settings
|
||||
@ -65,6 +67,9 @@ export default class InfioPlugin extends Plugin {
|
||||
this.settingTab = new InfioSettingTab(this.app, this)
|
||||
this.addSettingTab(this.settingTab)
|
||||
|
||||
// initialize dataview manager
|
||||
this.dataviewManager = createDataviewManager(this.app)
|
||||
|
||||
// add icon to ribbon
|
||||
this.addRibbonIcon('wand-sparkles', t('main.openInfioCopilot'), () =>
|
||||
this.openChatView(),
|
||||
@ -373,6 +378,44 @@ export default class InfioPlugin extends Plugin {
|
||||
editor.replaceRange(customBlock, insertPos);
|
||||
},
|
||||
});
|
||||
|
||||
// 添加简单测试命令
|
||||
this.addCommand({
|
||||
id: 'test-dataview-simple',
|
||||
name: '测试 Dataview(简单查询)',
|
||||
callback: async () => {
|
||||
console.log('开始测试 Dataview...');
|
||||
|
||||
if (!this.dataviewManager) {
|
||||
new Notice('DataviewManager 未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.dataviewManager.isDataviewAvailable()) {
|
||||
new Notice('Dataview 插件未安装或未启用');
|
||||
console.log('Dataview API 不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Dataview API 可用,执行简单查询...');
|
||||
|
||||
try {
|
||||
// 执行一个最简单的查询
|
||||
const result = await this.dataviewManager.executeQuery('LIST FROM ""');
|
||||
|
||||
if (result.success) {
|
||||
new Notice('Dataview 查询成功!结果已在控制台输出');
|
||||
console.log('查询结果:', result.data);
|
||||
} else {
|
||||
new Notice(`查询失败: ${result.error}`);
|
||||
console.error('查询错误:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('执行测试查询失败:', error);
|
||||
new Notice('执行测试查询时发生错误');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onunload() {
|
||||
@ -389,6 +432,8 @@ export default class InfioPlugin extends Plugin {
|
||||
// MCP Hub cleanup
|
||||
this.mcpHub?.dispose()
|
||||
this.mcpHub = null
|
||||
// Dataview cleanup
|
||||
this.dataviewManager = null
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
|
||||
@ -105,4 +105,50 @@ export type UseMcpToolArgs = {
|
||||
parameters: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | MatchSearchFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs | SearchWebToolArgs | FetchUrlsContentToolArgs | SwitchModeToolArgs | ApplyDiffToolArgs | UseMcpToolArgs;
|
||||
export type DataviewQueryToolArgs = {
|
||||
type: 'dataview_query';
|
||||
query: string;
|
||||
outputFormat: string;
|
||||
finish?: boolean;
|
||||
}
|
||||
|
||||
export type AnalyzePaperToolArgs = {
|
||||
type: 'analyze_paper';
|
||||
path: string;
|
||||
finish?: boolean;
|
||||
}
|
||||
|
||||
export type KeyInsightsToolArgs = {
|
||||
type: 'key_insights';
|
||||
path: string;
|
||||
finish?: boolean;
|
||||
}
|
||||
|
||||
export type DenseSummaryToolArgs = {
|
||||
type: 'dense_summary';
|
||||
path: string;
|
||||
finish?: boolean;
|
||||
}
|
||||
|
||||
export type ReflectionsToolArgs = {
|
||||
type: 'reflections';
|
||||
path: string;
|
||||
finish?: boolean;
|
||||
}
|
||||
|
||||
export type TableOfContentsToolArgs = {
|
||||
type: 'table_of_contents';
|
||||
path: string;
|
||||
depth?: number;
|
||||
format?: string;
|
||||
include_summary?: boolean;
|
||||
finish?: boolean;
|
||||
}
|
||||
|
||||
export type SimpleSummaryToolArgs = {
|
||||
type: 'simple_summary';
|
||||
path: string;
|
||||
finish?: boolean;
|
||||
}
|
||||
|
||||
export type ToolArgs = ReadFileToolArgs | WriteToFileToolArgs | InsertContentToolArgs | SearchAndReplaceToolArgs | ListFilesToolArgs | MatchSearchFilesToolArgs | RegexSearchFilesToolArgs | SemanticSearchFilesToolArgs | SearchWebToolArgs | FetchUrlsContentToolArgs | SwitchModeToolArgs | ApplyDiffToolArgs | UseMcpToolArgs | DataviewQueryToolArgs | AnalyzePaperToolArgs | KeyInsightsToolArgs | DenseSummaryToolArgs | ReflectionsToolArgs | TableOfContentsToolArgs | SimpleSummaryToolArgs;
|
||||
|
||||
@ -289,6 +289,8 @@ export const InfioSettingsSchema = z.object({
|
||||
// multiSearchReplaceDiffStrategy
|
||||
multiSearchReplaceDiffStrategy: z.boolean().catch(true),
|
||||
|
||||
// Workspace
|
||||
workspace: z.string().catch(''),
|
||||
// Mode
|
||||
mode: z.string().catch('ask'),
|
||||
defaultMention: z.enum(['none', 'current-file', 'vault']).catch('none'),
|
||||
|
||||
114
src/utils/dataview.ts
Normal file
114
src/utils/dataview.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { App } from "obsidian";
|
||||
import { DataviewApi, getAPI } from "obsidian-dataview";
|
||||
|
||||
export interface DataviewQueryResult {
|
||||
success: boolean;
|
||||
data?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class DataviewManager {
|
||||
private app: App;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Dataview API 实例(动态获取,确保插件已加载)
|
||||
*/
|
||||
private getAPI(): DataviewApi | null {
|
||||
try {
|
||||
const api = getAPI(this.app) as DataviewApi | null;
|
||||
return api;
|
||||
} catch (error) {
|
||||
console.error('获取 Dataview API 失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Dataview 插件是否可用
|
||||
*/
|
||||
isDataviewAvailable(): boolean {
|
||||
const api = this.getAPI();
|
||||
return api !== null && api !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 Dataview 查询
|
||||
*/
|
||||
async executeQuery(query: string): Promise<DataviewQueryResult> {
|
||||
const api = this.getAPI();
|
||||
if (!api) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Dataview 插件未安装或未启用"
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 Dataview 的查询引擎
|
||||
const result = await api.queryMarkdown(query, "", {});
|
||||
|
||||
// 检查 Result 对象的结构
|
||||
if (result && typeof result === 'object' && 'successful' in result) {
|
||||
if (result.successful) {
|
||||
return {
|
||||
success: true,
|
||||
data: String(result.value || '')
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: String(result.error || '查询失败')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是 Result 对象,直接处理
|
||||
return {
|
||||
success: true,
|
||||
data: this.formatQueryResult(result)
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Dataview 查询执行失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '未知错误'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化查询结果(备用方法)
|
||||
*/
|
||||
private formatQueryResult(result: unknown): string {
|
||||
if (result === null || result === undefined) {
|
||||
return '查询结果为空';
|
||||
}
|
||||
|
||||
// 如果是字符串,直接返回
|
||||
if (typeof result === 'string') {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 尝试 JSON 序列化
|
||||
if (typeof result === 'object') {
|
||||
try {
|
||||
return JSON.stringify(result, null, 2);
|
||||
} catch (e) {
|
||||
return `对象结果(无法序列化): ${Object.prototype.toString.call(result)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 其他类型,转换为字符串
|
||||
return String(result);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出一个全局实例创建函数
|
||||
export function createDataviewManager(app: App): DataviewManager {
|
||||
return new DataviewManager(app);
|
||||
}
|
||||
@ -69,7 +69,11 @@ export function getToolsForMode(groups: readonly GroupEntry[]): string[] {
|
||||
groups.forEach((group) => {
|
||||
const groupName = getGroupName(group)
|
||||
const groupConfig = TOOL_GROUPS[groupName]
|
||||
groupConfig.tools.forEach((tool: string) => tools.add(tool))
|
||||
if (groupConfig) {
|
||||
groupConfig.tools.forEach((tool: string) => tools.add(tool))
|
||||
} else {
|
||||
console.warn(`Tool group '${groupName}' not found in TOOL_GROUPS`)
|
||||
}
|
||||
})
|
||||
|
||||
// Always add required tools
|
||||
@ -84,28 +88,37 @@ export const defaultModes: ModeConfig[] = [
|
||||
slug: "ask",
|
||||
name: "Ask",
|
||||
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.",
|
||||
groups: ["read", "mcp"],
|
||||
"You are Infio, an AI knowledge assistant powered by advanced language models. You operate within Obsidian.",
|
||||
groups: ["read", "transformations", "mcp"],
|
||||
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 are collaborating with a USER to help them explore, understand, and organize information within their personal knowledge vault. Each time the USER sends a message, they may provide context about their current notes, vault structure, or specific knowledge needs. This information may or may not be relevant to their inquiry, it is up for you to decide.\n\nYour main goal is to provide informative responses, thoughtful explanations, and practical guidance on any topic or challenge they face, while leveraging their existing knowledge base when relevant. 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",
|
||||
name: "Write",
|
||||
roleDefinition:
|
||||
"You are Infio, a versatile content creator skilled in composing, editing, and organizing various text-based documents. You excel at structuring information clearly, creating well-formatted content, and helping users express their ideas effectively.",
|
||||
"You are Infio, an AI writing assistant powered by advanced language models. You operate within Obsidian.",
|
||||
groups: ["read", "edit", "mcp"],
|
||||
customInstructions:
|
||||
"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 are collaborating with a USER to help them create, edit, and organize various types of written content within their knowledge vault. Each time the USER sends a message, they may provide context about their current documents, writing goals, or organizational needs. This information may or may not be relevant to their writing task, it is up for you to decide.\n\nYour main goal is to help them express their ideas effectively through well-structured, clearly formatted content that integrates seamlessly with their existing knowledge system. 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: "learn",
|
||||
name: "Learn",
|
||||
roleDefinition:
|
||||
"You are Infio, an AI learning assistant powered by advanced language models. You operate within Obsidian.",
|
||||
groups: ["read", "transformations", "mcp"],
|
||||
customInstructions:
|
||||
"You are collaborating with a USER to enhance their learning experience within their knowledge vault. Each time the USER sends a message, they may provide context about their learning materials, study goals, or knowledge gaps. This information may or may not be relevant to their learning journey, it is up for you to decide.\n\nYour main goal is to help them actively learn and understand complex topics by transforming information into more digestible formats, creating connections between concepts, and facilitating deep comprehension. You excel at breaking down complex topics into manageable chunks, creating study materials like flashcards and summaries, and helping users build comprehensive understanding through structured learning approaches. Generate visual learning aids like concept maps and flowcharts using Mermaid diagrams when they enhance comprehension. Create organized study materials including key concepts, definitions, and practice questions. Help users connect new information to their existing knowledge base by identifying relationships and patterns across their notes. Focus on active learning techniques that promote retention and understanding rather than passive information consumption."
|
||||
},
|
||||
{
|
||||
slug: "research",
|
||||
name: "Research",
|
||||
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, an AI research assistant powered by advanced language models. You operate within Obsidian.",
|
||||
groups: ["research", "mcp"],
|
||||
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 are collaborating with a USER to conduct comprehensive research and analytical thinking within their knowledge vault. Each time the USER sends a message, they may provide context about their research questions, existing notes, or analytical needs. This information may or may not be relevant to their research, it is up for you to decide.\n\nYour main goal is to help them break down complex questions, explore multiple perspectives, and synthesize information to reach well-reasoned conclusions while building upon their existing knowledge base. 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.",
|
||||
},
|
||||
]
|
||||
|
||||
@ -208,6 +221,12 @@ export function isToolAllowedForMode(
|
||||
|
||||
const groupConfig = TOOL_GROUPS[groupName]
|
||||
|
||||
// If the group config doesn't exist, skip this group
|
||||
if (!groupConfig) {
|
||||
console.warn(`Tool group '${groupName}' not found in TOOL_GROUPS`)
|
||||
continue
|
||||
}
|
||||
|
||||
// If the tool isn't in this group's tools, continue to next group
|
||||
if (!groupConfig.tools.includes(tool)) {
|
||||
continue
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// @ts-nocheck
|
||||
// @ts-expect-error - parse5 and JSON5 types are not perfectly aligned with the dynamic parsing logic
|
||||
import JSON5 from 'json5'
|
||||
import { parseFragment } from 'parse5'
|
||||
|
||||
@ -93,9 +93,41 @@ export type ParsedMsgBlock =
|
||||
tool_name: string
|
||||
parameters: Record<string, unknown>,
|
||||
finish: boolean
|
||||
} | {
|
||||
type: 'dataview_query'
|
||||
query: string
|
||||
outputFormat: string
|
||||
finish: boolean
|
||||
} | {
|
||||
type: 'tool_result'
|
||||
content: string
|
||||
} | {
|
||||
type: 'analyze_paper'
|
||||
path: string
|
||||
finish: boolean
|
||||
} | {
|
||||
type: 'key_insights'
|
||||
path: string
|
||||
finish: boolean
|
||||
} | {
|
||||
type: 'dense_summary'
|
||||
path: string
|
||||
finish: boolean
|
||||
} | {
|
||||
type: 'reflections'
|
||||
path: string
|
||||
finish: boolean
|
||||
} | {
|
||||
type: 'table_of_contents'
|
||||
path: string
|
||||
depth?: number
|
||||
format?: string
|
||||
include_summary?: boolean
|
||||
finish: boolean
|
||||
} | {
|
||||
type: 'simple_summary'
|
||||
path: string
|
||||
finish: boolean
|
||||
}
|
||||
|
||||
export function parseMsgBlocks(
|
||||
@ -191,8 +223,10 @@ export function parseMsgBlocks(
|
||||
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
} else if (childNode.nodeName === 'recursive' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
const recursiveValue = childNode.childNodes[0].value
|
||||
recursive = recursiveValue ? recursiveValue.toLowerCase() === 'true' : false
|
||||
}
|
||||
@ -220,6 +254,7 @@ export function parseMsgBlocks(
|
||||
let path: string | undefined
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
@ -248,8 +283,10 @@ export function parseMsgBlocks(
|
||||
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
} else if (childNode.nodeName === 'query' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
query = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
@ -278,8 +315,10 @@ export function parseMsgBlocks(
|
||||
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
} else if (childNode.nodeName === 'regex' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
regex = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
@ -308,8 +347,10 @@ export function parseMsgBlocks(
|
||||
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
} else if (childNode.nodeName === 'query' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
query = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
@ -339,13 +380,15 @@ export function parseMsgBlocks(
|
||||
// 处理子标签
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
} else if (childNode.nodeName === 'content' && childNode.childNodes.length > 0) {
|
||||
// 如果内容有多个子节点,需要合并它们
|
||||
content = childNode.childNodes.map(n => n.value || '').join('')
|
||||
content = childNode.childNodes.map(n => (n as any).value || '').join('')
|
||||
} else if (childNode.nodeName === 'line_count' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
const lineCountStr = childNode.childNodes[0].value
|
||||
lineCount = lineCountStr ? parseInt(lineCountStr) : undefined
|
||||
lineCount = lineCountStr ? parseInt(lineCountStr as string) : undefined
|
||||
}
|
||||
}
|
||||
parsedResult.push({
|
||||
@ -375,11 +418,13 @@ export function parseMsgBlocks(
|
||||
// 处理子标签
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
} else if (childNode.nodeName === 'operations' && childNode.childNodes.length > 0) {
|
||||
try {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
const operationsJson = childNode.childNodes[0].value
|
||||
const operations = JSON5.parse(operationsJson)
|
||||
const operations = JSON5.parse(operationsJson as string)
|
||||
if (Array.isArray(operations) && operations.length > 0) {
|
||||
const operation = operations[0]
|
||||
startLine = operation.start_line || 1
|
||||
@ -417,12 +462,13 @@ export function parseMsgBlocks(
|
||||
// 处理子标签
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
} else if (childNode.nodeName === 'operations' && childNode.childNodes.length > 0) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error - parse5 node value type
|
||||
content = childNode.childNodes[0].value
|
||||
operations = JSON5.parse(content)
|
||||
operations = JSON5.parse(content as string)
|
||||
} catch (error) {
|
||||
console.error('Failed to parse operations JSON', error)
|
||||
}
|
||||
@ -454,10 +500,10 @@ export function parseMsgBlocks(
|
||||
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
} else if (childNode.nodeName === 'diff' && childNode.childNodes.length > 0) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error - parse5 node value type
|
||||
diff = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
@ -484,7 +530,7 @@ export function parseMsgBlocks(
|
||||
let result: string | undefined
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'result' && childNode.childNodes.length > 0) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error - parse5 node value type
|
||||
result = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
@ -509,7 +555,7 @@ export function parseMsgBlocks(
|
||||
let question: string | undefined
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'question' && childNode.childNodes.length > 0) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error - parse5 node value type
|
||||
question = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
@ -537,10 +583,10 @@ export function parseMsgBlocks(
|
||||
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'mode_slug' && childNode.childNodes.length > 0) {
|
||||
// @ts-ignore - 忽略 value 属性的类型错误
|
||||
// @ts-expect-error - parse5 node value type
|
||||
mode = childNode.childNodes[0].value
|
||||
} else if (childNode.nodeName === 'reason' && childNode.childNodes.length > 0) {
|
||||
// @ts-ignore - 忽略 value 属性的类型错误
|
||||
// @ts-expect-error - parse5 node value type
|
||||
reason = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
@ -567,7 +613,7 @@ export function parseMsgBlocks(
|
||||
let query: string | undefined
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'query' && childNode.childNodes.length > 0) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error - parse5 node value type
|
||||
query = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
@ -595,9 +641,9 @@ export function parseMsgBlocks(
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'urls' && childNode.childNodes.length > 0) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error - parse5 node value type
|
||||
const urlsJson = childNode.childNodes[0].value
|
||||
const parsedUrls = JSON5.parse(urlsJson)
|
||||
const parsedUrls = JSON5.parse(urlsJson as string)
|
||||
if (Array.isArray(parsedUrls)) {
|
||||
urls = parsedUrls
|
||||
}
|
||||
@ -632,19 +678,19 @@ export function parseMsgBlocks(
|
||||
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'server_name' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - 忽略 value 属性的类型错误
|
||||
// @ts-expect-error - parse5 node value type
|
||||
server_name = childNode.childNodes[0].value
|
||||
} else if (childNode.nodeName === 'tool_name' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - 忽略 value 属性的类型错误
|
||||
// @ts-expect-error - parse5 node value type
|
||||
tool_name = childNode.childNodes[0].value
|
||||
} else if ((childNode.nodeName === 'parameters'
|
||||
|| childNode.nodeName === 'input'
|
||||
|| childNode.nodeName === 'arguments')
|
||||
&& childNode.childNodes.length > 0) {
|
||||
try {
|
||||
// @ts-expect-error - 忽略 value 属性的类型错误
|
||||
// @ts-expect-error - parse5 node value type
|
||||
const parametersJson = childNode.childNodes[0].value
|
||||
parameters = JSON5.parse(parametersJson)
|
||||
parameters = JSON5.parse(parametersJson as string)
|
||||
} catch (error) {
|
||||
console.debug('Failed to parse parameters JSON', error)
|
||||
}
|
||||
@ -659,6 +705,209 @@ export function parseMsgBlocks(
|
||||
finish: node.sourceCodeLocation.endTag !== undefined
|
||||
})
|
||||
lastEndOffset = endOffset
|
||||
} else if (node.nodeName === 'dataview_query') {
|
||||
if (!node.sourceCodeLocation) {
|
||||
throw new Error('sourceCodeLocation is undefined')
|
||||
}
|
||||
const startOffset = node.sourceCodeLocation.startOffset
|
||||
const endOffset = node.sourceCodeLocation.endOffset
|
||||
if (startOffset > lastEndOffset) {
|
||||
parsedResult.push({
|
||||
type: 'string',
|
||||
content: input.slice(lastEndOffset, startOffset),
|
||||
})
|
||||
}
|
||||
|
||||
let query: string = ''
|
||||
let outputFormat: string = 'table'
|
||||
|
||||
// 解析子节点
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'query' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
query = childNode.childNodes[0].value || ''
|
||||
} else if (childNode.nodeName === 'output_format' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
outputFormat = childNode.childNodes[0].value || 'table'
|
||||
}
|
||||
}
|
||||
|
||||
parsedResult.push({
|
||||
type: 'dataview_query',
|
||||
query,
|
||||
outputFormat,
|
||||
finish: node.sourceCodeLocation.endTag !== undefined
|
||||
})
|
||||
lastEndOffset = endOffset
|
||||
} else if (node.nodeName === 'analyze_paper') {
|
||||
if (!node.sourceCodeLocation) {
|
||||
throw new Error('sourceCodeLocation is undefined')
|
||||
}
|
||||
const startOffset = node.sourceCodeLocation.startOffset
|
||||
const endOffset = node.sourceCodeLocation.endOffset
|
||||
if (startOffset > lastEndOffset) {
|
||||
parsedResult.push({
|
||||
type: 'string',
|
||||
content: input.slice(lastEndOffset, startOffset),
|
||||
})
|
||||
}
|
||||
let path: string | undefined
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
parsedResult.push({
|
||||
type: 'analyze_paper',
|
||||
path: path || '',
|
||||
finish: node.sourceCodeLocation.endTag !== undefined
|
||||
})
|
||||
lastEndOffset = endOffset
|
||||
} else if (node.nodeName === 'key_insights') {
|
||||
if (!node.sourceCodeLocation) {
|
||||
throw new Error('sourceCodeLocation is undefined')
|
||||
}
|
||||
const startOffset = node.sourceCodeLocation.startOffset
|
||||
const endOffset = node.sourceCodeLocation.endOffset
|
||||
if (startOffset > lastEndOffset) {
|
||||
parsedResult.push({
|
||||
type: 'string',
|
||||
content: input.slice(lastEndOffset, startOffset),
|
||||
})
|
||||
}
|
||||
let path: string | undefined
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
parsedResult.push({
|
||||
type: 'key_insights',
|
||||
path: path || '',
|
||||
finish: node.sourceCodeLocation.endTag !== undefined
|
||||
})
|
||||
lastEndOffset = endOffset
|
||||
} else if (node.nodeName === 'dense_summary') {
|
||||
if (!node.sourceCodeLocation) {
|
||||
throw new Error('sourceCodeLocation is undefined')
|
||||
}
|
||||
const startOffset = node.sourceCodeLocation.startOffset
|
||||
const endOffset = node.sourceCodeLocation.endOffset
|
||||
if (startOffset > lastEndOffset) {
|
||||
parsedResult.push({
|
||||
type: 'string',
|
||||
content: input.slice(lastEndOffset, startOffset),
|
||||
})
|
||||
}
|
||||
let path: string | undefined
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
parsedResult.push({
|
||||
type: 'dense_summary',
|
||||
path: path || '',
|
||||
finish: node.sourceCodeLocation.endTag !== undefined
|
||||
})
|
||||
lastEndOffset = endOffset
|
||||
} else if (node.nodeName === 'reflections') {
|
||||
if (!node.sourceCodeLocation) {
|
||||
throw new Error('sourceCodeLocation is undefined')
|
||||
}
|
||||
const startOffset = node.sourceCodeLocation.startOffset
|
||||
const endOffset = node.sourceCodeLocation.endOffset
|
||||
if (startOffset > lastEndOffset) {
|
||||
parsedResult.push({
|
||||
type: 'string',
|
||||
content: input.slice(lastEndOffset, startOffset),
|
||||
})
|
||||
}
|
||||
let path: string | undefined
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
parsedResult.push({
|
||||
type: 'reflections',
|
||||
path: path || '',
|
||||
finish: node.sourceCodeLocation.endTag !== undefined
|
||||
})
|
||||
lastEndOffset = endOffset
|
||||
} else if (node.nodeName === 'table_of_contents') {
|
||||
if (!node.sourceCodeLocation) {
|
||||
throw new Error('sourceCodeLocation is undefined')
|
||||
}
|
||||
const startOffset = node.sourceCodeLocation.startOffset
|
||||
const endOffset = node.sourceCodeLocation.endOffset
|
||||
if (startOffset > lastEndOffset) {
|
||||
parsedResult.push({
|
||||
type: 'string',
|
||||
content: input.slice(lastEndOffset, startOffset),
|
||||
})
|
||||
}
|
||||
let path: string | undefined
|
||||
let depth: number | undefined
|
||||
let format: string | undefined
|
||||
let include_summary: boolean | undefined
|
||||
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
} else if (childNode.nodeName === 'depth' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
const depthStr = childNode.childNodes[0].value
|
||||
depth = depthStr ? parseInt(depthStr as string) : undefined
|
||||
} else if (childNode.nodeName === 'format' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
format = childNode.childNodes[0].value
|
||||
} else if (childNode.nodeName === 'include_summary' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
const summaryValue = childNode.childNodes[0].value
|
||||
include_summary = summaryValue ? (summaryValue as string).toLowerCase() === 'true' : false
|
||||
}
|
||||
}
|
||||
|
||||
parsedResult.push({
|
||||
type: 'table_of_contents',
|
||||
path: path || '',
|
||||
depth,
|
||||
format,
|
||||
include_summary,
|
||||
finish: node.sourceCodeLocation.endTag !== undefined
|
||||
})
|
||||
lastEndOffset = endOffset
|
||||
} else if (node.nodeName === 'simple_summary') {
|
||||
if (!node.sourceCodeLocation) {
|
||||
throw new Error('sourceCodeLocation is undefined')
|
||||
}
|
||||
const startOffset = node.sourceCodeLocation.startOffset
|
||||
const endOffset = node.sourceCodeLocation.endOffset
|
||||
if (startOffset > lastEndOffset) {
|
||||
parsedResult.push({
|
||||
type: 'string',
|
||||
content: input.slice(lastEndOffset, startOffset),
|
||||
})
|
||||
}
|
||||
let path: string | undefined
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.nodeName === 'path' && childNode.childNodes.length > 0) {
|
||||
// @ts-expect-error - parse5 node value type
|
||||
path = childNode.childNodes[0].value
|
||||
}
|
||||
}
|
||||
parsedResult.push({
|
||||
type: 'simple_summary',
|
||||
path: path || '',
|
||||
finish: node.sourceCodeLocation.endTag !== undefined
|
||||
})
|
||||
lastEndOffset = endOffset
|
||||
} else if (node.nodeName === 'tool_result') {
|
||||
if (!node.sourceCodeLocation) {
|
||||
throw new Error('sourceCodeLocation is undefined')
|
||||
|
||||
@ -12,20 +12,24 @@ export const TOOL_DISPLAY_NAMES = {
|
||||
apply_diff: "apply changes",
|
||||
search_files: "search files",
|
||||
list_files: "list files",
|
||||
// list_code_definition_names: "list definitions",
|
||||
browser_action: "use a browser",
|
||||
use_mcp_tool: "use mcp tools",
|
||||
access_mcp_resource: "access mcp resources",
|
||||
ask_followup_question: "ask questions",
|
||||
attempt_completion: "complete tasks",
|
||||
switch_mode: "switch modes",
|
||||
new_task: "create new task",
|
||||
analyze_paper: "analyze papers",
|
||||
key_insights: "extract key insights",
|
||||
dense_summary: "create dense summaries",
|
||||
reflections: "generate reflections",
|
||||
table_of_contents: "create table of contents",
|
||||
simple_summary: "create simple summaries",
|
||||
} as const
|
||||
|
||||
// Define available tool groups
|
||||
export const TOOL_GROUPS: Record<string, ToolGroupConfig> = {
|
||||
read: {
|
||||
tools: ["read_file", "list_files", "search_files"],
|
||||
tools: ["read_file", "list_files", "search_files", "dataview_query"],
|
||||
},
|
||||
edit: {
|
||||
tools: ["apply_diff", "write_to_file", "insert_content", "search_and_replace"],
|
||||
@ -33,12 +37,9 @@ export const TOOL_GROUPS: Record<string, ToolGroupConfig> = {
|
||||
research: {
|
||||
tools: ["search_web", "fetch_urls_content"],
|
||||
},
|
||||
// browser: {
|
||||
// tools: ["browser_action"],
|
||||
// },
|
||||
// command: {
|
||||
// tools: ["execute_command"],
|
||||
// },
|
||||
transformations: {
|
||||
tools: ["analyze_paper", "key_insights", "dense_summary", "reflections", "table_of_contents", "simple_summary"],
|
||||
},
|
||||
mcp: {
|
||||
tools: ["use_mcp_tool", "access_mcp_resource"],
|
||||
},
|
||||
|
||||
29
styles.css
29
styles.css
@ -76,6 +76,28 @@
|
||||
font-size: var(--font-ui-medium);
|
||||
font-weight: var(--font-medium);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-2);
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.infio-chat-current-workspace {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: var(--size-4-2);
|
||||
padding: var(--size-2-1) var(--size-4-1);
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
font-size: var(--font-ui-smaller);
|
||||
font-weight: var(--font-normal);
|
||||
color: var(--text-accent);
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.infio-chat-header-buttons {
|
||||
@ -725,6 +747,9 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-1);
|
||||
}
|
||||
|
||||
.infio-chat-code-block-header-icon {
|
||||
@ -772,6 +797,10 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
|
||||
.infio-dataview-query-button {
|
||||
color: #008000;
|
||||
}
|
||||
|
||||
.infio-chat-code-block-content {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user