mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
chore(deps): remove unused deps and dead code to shrink bundle (#2460)
Drop unused runtime deps (huggingface, koa, langchain, react-markdown, react-syntax-highlighter, next-i18next, p-queue, trie-search, sse, codemirror-companion-extension, tabler/icons-react, etc.) and demote build-only packages to devDependencies. Delete dead source files (quick-ask modes, ListPromptModal, NewChatConfirmModal, PatternMatchingModal, ContainerContext, command-ui chat-message, ui/tabs) and unused exports flagged by knip. Add `lint:dead` script backed by knip and a knip.json config. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a92012dc4d
commit
adf8c68a73
83 changed files with 329 additions and 5875 deletions
|
|
@ -6,7 +6,6 @@ module.exports = {
|
|||
"^.+\\.(js|jsx|ts|tsx)$": "ts-jest",
|
||||
},
|
||||
moduleNameMapper: {
|
||||
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
"^obsidian$": "<rootDir>/__mocks__/obsidian.js",
|
||||
// The yaml package's "exports" field defaults to a browser ESM entry under
|
||||
|
|
|
|||
7
knip.json
Normal file
7
knip.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"entry": ["src/main.ts", "scripts/printPromptDebug.js", "scripts/printPromptDebugEntry.ts"],
|
||||
"project": ["src/**/*.{ts,tsx,js,jsx}", "scripts/**/*.{ts,js,mjs}"],
|
||||
"ignore": ["src/styles/tailwind.css", "src/integration_tests/**"],
|
||||
"ignoreDependencies": ["buffer"]
|
||||
}
|
||||
4025
package-lock.json
generated
4025
package-lock.json
generated
File diff suppressed because it is too large
Load diff
40
package.json
40
package.json
|
|
@ -11,6 +11,7 @@
|
|||
"build:tailwind": "npx tailwindcss -i src/styles/tailwind.css -o styles.css --minify",
|
||||
"build:esbuild": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"lint": "eslint .",
|
||||
"lint:dead": "npx --yes knip@5",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write 'src/**/*.{js,ts,tsx,md}'",
|
||||
"format:check": "prettier --check 'src/**/*.{js,ts,tsx,md}'",
|
||||
|
|
@ -32,32 +33,30 @@
|
|||
},
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.36.4",
|
||||
"@eslint-react/eslint-plugin": "^1.38.4",
|
||||
"@google/generative-ai": "^0.24.0",
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@langchain/ollama": "^1.2.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/diff": "^7.0.1",
|
||||
"@types/events": "^3.0.0",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/koa": "^2.13.7",
|
||||
"@types/koa__cors": "^4.0.0",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^16.11.6",
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-syntax-highlighter": "^15.5.6",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.19.1",
|
||||
"@typescript-eslint/parser": "^8.19.1",
|
||||
"electron": "^27.3.2",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild-plugin-svg": "^0.1.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||
"globals": "^15.14.0",
|
||||
"husky": "^9.1.5",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
|
|
@ -66,6 +65,7 @@
|
|||
"obsidian": "^1.2.5",
|
||||
"prettier": "^3.3.3",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"ts-jest": "^29.1.0",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "^5.7.2",
|
||||
|
|
@ -73,14 +73,9 @@
|
|||
"yaml": "^2.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.36.4",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@google/generative-ai": "^0.24.0",
|
||||
"@huggingface/inference": "^4.11.3",
|
||||
"@koa/cors": "^5.0.0",
|
||||
"@langchain/anthropic": "^1.0.0",
|
||||
"@langchain/classic": "^1.0.9",
|
||||
"@langchain/community": "^1.0.0",
|
||||
|
|
@ -91,10 +86,7 @@
|
|||
"@langchain/openai": "^1.0.0",
|
||||
"@langchain/textsplitters": "^1.0.0",
|
||||
"@langchain/xai": "^1.0.0",
|
||||
"@lexical/plain-text": "^0.34.0",
|
||||
"@lexical/react": "^0.34.0",
|
||||
"@lexical/selection": "^0.34.0",
|
||||
"@lexical/utils": "^0.34.0",
|
||||
"@orama/orama": "^3.0.0-rc-2",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.2",
|
||||
|
|
@ -108,42 +100,24 @@
|
|||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tabler/icons-react": "^2.14.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"buffer": "^6.0.3",
|
||||
"chrono-node": "^2.7.7",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"codemirror-companion-extension": "^0.0.11",
|
||||
"diff": "^7.0.0",
|
||||
"esbuild-plugin-svg": "^0.1.0",
|
||||
"eventsource-parser": "^1.0.0",
|
||||
"fuzzysort": "^3.1.0",
|
||||
"jotai": "^2.10.3",
|
||||
"koa": "^2.14.2",
|
||||
"koa-proxies": "^0.12.3",
|
||||
"langchain": "^1.2.28",
|
||||
"lexical": "^0.34.0",
|
||||
"lucide-react": "^0.462.0",
|
||||
"luxon": "^3.5.0",
|
||||
"minisearch": "^7.2.0",
|
||||
"next-i18next": "^13.2.2",
|
||||
"openai": "^4.95.1",
|
||||
"p-queue": "^8.1.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"sse": "github:mpetazzoni/sse.js",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"trie-search": "^2.2.0",
|
||||
"turndown": "^7.2.2",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^3.25.76"
|
||||
|
|
|
|||
|
|
@ -1,26 +1,7 @@
|
|||
// Main exports for chain runners
|
||||
export type { ChainRunner } from "./BaseChainRunner";
|
||||
export { BaseChainRunner } from "./BaseChainRunner";
|
||||
export { LLMChainRunner } from "./LLMChainRunner";
|
||||
export { VaultQAChainRunner } from "./VaultQAChainRunner";
|
||||
export { CopilotPlusChainRunner } from "./CopilotPlusChainRunner";
|
||||
export { ProjectChainRunner } from "./ProjectChainRunner";
|
||||
export { AutonomousAgentChainRunner } from "./AutonomousAgentChainRunner";
|
||||
|
||||
// Utility exports (for internal use or testing)
|
||||
export { ThinkBlockStreamer } from "./utils/ThinkBlockStreamer";
|
||||
export {
|
||||
executeSequentialToolCall,
|
||||
getToolDisplayName,
|
||||
getToolEmoji,
|
||||
logToolCall,
|
||||
logToolResult,
|
||||
deduplicateSources,
|
||||
} from "./utils/toolExecution";
|
||||
export type { ToolExecutionResult } from "./utils/toolExecution";
|
||||
export {
|
||||
createToolResultMessage,
|
||||
generateToolCallId,
|
||||
extractNativeToolCalls,
|
||||
} from "./utils/nativeToolCalling";
|
||||
export type { NativeToolCall } from "./utils/nativeToolCalling";
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
// ===== CITATION RULES =====
|
||||
|
||||
export const CITATION_RULES = `CITATION RULES:
|
||||
const CITATION_RULES = `CITATION RULES:
|
||||
1. START with [^1] and increment sequentially ([^1], [^2], [^3], etc.) with NO gaps
|
||||
2. BE SELECTIVE: ONLY cite when introducing NEW factual claims, specific data, or direct quotes from sources
|
||||
3. IMPORTANT: Do NOT cite every sentence or bullet point. This creates clutter and poor readability.
|
||||
|
|
@ -21,7 +21,7 @@ export const CITATION_RULES = `CITATION RULES:
|
|||
9. If multiple source chunks come from the same document, cite each relevant chunk separately (e.g., [^1] and [^2] can both be from the same document title)
|
||||
10. End with '#### Sources' section containing: [^n]: [[Title]] (one per line, matching citation order)`;
|
||||
|
||||
export const WEB_CITATION_RULES = `WEB CITATION RULES:
|
||||
const WEB_CITATION_RULES = `WEB CITATION RULES:
|
||||
1. START with [^1] and increment sequentially ([^1], [^2], [^3], etc.) with NO gaps
|
||||
2. Cite ONLY when introducing new factual claims, statistics, or direct quotes from the search results
|
||||
3. After every cited claim, place the corresponding footnote immediately after the sentence ("The study found X [^1]")
|
||||
|
|
@ -193,7 +193,7 @@ export function getWebSearchCitationInstructions(enableInlineCitations: boolean
|
|||
|
||||
// ===== CITATION PROCESSING UTILITIES =====
|
||||
|
||||
export interface SourcesSection {
|
||||
interface SourcesSection {
|
||||
mainContent: string;
|
||||
sourcesBlock: string;
|
||||
}
|
||||
|
|
@ -254,7 +254,7 @@ export function extractSourcesSection(content: string): SourcesSection | null {
|
|||
/**
|
||||
* Normalizes sources block by adding line breaks if everything is on one line.
|
||||
*/
|
||||
export function normalizeSourcesBlock(sourcesBlock: string): string {
|
||||
function normalizeSourcesBlock(sourcesBlock: string): string {
|
||||
if (!sourcesBlock.includes("\n")) {
|
||||
// Ensure a break before every [n]
|
||||
sourcesBlock = sourcesBlock.replace(/\s*\[(\d+)\]\s*/g, "\n[$1] ");
|
||||
|
|
@ -268,7 +268,7 @@ export function normalizeSourcesBlock(sourcesBlock: string): string {
|
|||
/**
|
||||
* Parses footnote definitions from sources block.
|
||||
*/
|
||||
export function parseFootnoteDefinitions(sourcesBlock: string): string[] {
|
||||
function parseFootnoteDefinitions(sourcesBlock: string): string[] {
|
||||
return sourcesBlock
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
|
|
@ -278,10 +278,7 @@ export function parseFootnoteDefinitions(sourcesBlock: string): string[] {
|
|||
/**
|
||||
* Builds a citation renumbering map based on first-mention order in content.
|
||||
*/
|
||||
export function buildCitationMap(
|
||||
mainContent: string,
|
||||
footnoteLines: string[]
|
||||
): Map<number, number> {
|
||||
function buildCitationMap(mainContent: string, footnoteLines: string[]): Map<number, number> {
|
||||
const map = new Map<number, number>();
|
||||
const seen = new Set<number>();
|
||||
const firstMention: number[] = [];
|
||||
|
|
@ -368,10 +365,7 @@ export function normalizeCitations(content: string, map: Map<number, number>): s
|
|||
/**
|
||||
* Converts footnote definitions to simple display items.
|
||||
*/
|
||||
export function convertFootnoteDefinitions(
|
||||
sourcesBlock: string,
|
||||
map: Map<number, number>
|
||||
): string[] {
|
||||
function convertFootnoteDefinitions(sourcesBlock: string, map: Map<number, number>): string[] {
|
||||
const items: string[] = [];
|
||||
sourcesBlock.split("\n").forEach((line) => {
|
||||
const m = line.match(/^\[\^(\d+)\]:\s*(.*)$/);
|
||||
|
|
@ -409,7 +403,7 @@ export function convertFootnoteDefinitions(
|
|||
/**
|
||||
* Consolidates duplicate sources and returns mapping for citation updates.
|
||||
*/
|
||||
export function consolidateDuplicateSources(items: string[]): {
|
||||
function consolidateDuplicateSources(items: string[]): {
|
||||
uniqueItems: string[];
|
||||
consolidationMap: Map<number, number>;
|
||||
} {
|
||||
|
|
@ -591,7 +585,7 @@ function buildSourcesDetails(mainContent: string, items: SourcesDisplayItem[]):
|
|||
* These spans provide visual feedback during streaming (styled as pending links)
|
||||
* and are replaced by linkInlineCitations with actual clickable anchors after streaming.
|
||||
*/
|
||||
export function wrapCitationPlaceholders(content: string): string {
|
||||
function wrapCitationPlaceholders(content: string): string {
|
||||
return content.replace(
|
||||
/\[(\d+(?:\s*,\s*\d+)*)\](?!\()/g,
|
||||
'<span class="copilot-citation-ref">[$1]</span>'
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import { AIMessage, ToolMessage } from "@langchain/core/messages";
|
||||
import { ToolCall as LangChainToolCall } from "@langchain/core/messages/tool";
|
||||
import { logError } from "@/logger";
|
||||
|
||||
/**
|
||||
|
|
@ -27,37 +26,6 @@ export interface ToolCallChunk {
|
|||
args: string; // JSON string accumulated from chunks
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract native tool calls from an AIMessage.
|
||||
* Returns empty array if no tool calls present.
|
||||
*
|
||||
* @param message - AIMessage from LLM response
|
||||
* @returns Array of standardized tool calls
|
||||
*/
|
||||
export function extractNativeToolCalls(message: AIMessage): NativeToolCall[] {
|
||||
const toolCalls = message.tool_calls;
|
||||
|
||||
if (!toolCalls || toolCalls.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return toolCalls.map((tc: LangChainToolCall) => ({
|
||||
id: tc.id || generateToolCallId(),
|
||||
name: tc.name,
|
||||
args: (tc.args as Record<string, unknown>) || {},
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an AIMessage contains tool calls
|
||||
*
|
||||
* @param message - AIMessage to check
|
||||
* @returns true if message has tool calls
|
||||
*/
|
||||
export function hasToolCalls(message: AIMessage): boolean {
|
||||
return (message.tool_calls?.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ToolMessage for returning tool execution results to the LLM.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export interface SearchDoc {
|
|||
* Quality summary for search results.
|
||||
* Helps the LLM evaluate whether results are adequate or if re-search is needed.
|
||||
*/
|
||||
export interface QualitySummary {
|
||||
interface QualitySummary {
|
||||
high: number; // Count of results with score >= 0.7
|
||||
medium: number; // Count of results with score >= 0.3 and < 0.7
|
||||
low: number; // Count of results with score < 0.3
|
||||
|
|
@ -212,7 +212,7 @@ function toIsoString(ts: unknown): string {
|
|||
* Create a concise, single-line summary of an explanation object.
|
||||
* Includes lexical matches, semantic score, folder/graph boosts, and score adjustments.
|
||||
*/
|
||||
export function summarizeExplanation(explanation: unknown): string {
|
||||
function summarizeExplanation(explanation: unknown): string {
|
||||
if (!explanation) return "";
|
||||
|
||||
const parts: string[] = [];
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export interface ErrorMarker {
|
|||
endIndex: number;
|
||||
}
|
||||
|
||||
export interface ParsedMessage {
|
||||
interface ParsedMessage {
|
||||
segments: Array<{
|
||||
type: "text" | "toolCall" | "error";
|
||||
content: string;
|
||||
|
|
@ -45,7 +45,7 @@ function encodeResultForMarker(result: string): string {
|
|||
/**
|
||||
* Decode tool result previously encoded for marker embedding
|
||||
*/
|
||||
export function decodeResultFromMarker(result: string | undefined): string | undefined {
|
||||
function decodeResultFromMarker(result: string | undefined): string | undefined {
|
||||
if (typeof result !== "string") return result;
|
||||
if (!result.startsWith("ENC:")) return result;
|
||||
try {
|
||||
|
|
@ -65,38 +65,6 @@ function buildOmittedResultMessage(toolName: string): string {
|
|||
return `Tool '${toolName}' ${TOOL_RESULT_OMITTED_THRESHOLD_MESSAGE}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* For logging only: decode any encoded tool results embedded in markers
|
||||
*/
|
||||
export function decodeToolCallMarkerResults(message: string): string {
|
||||
if (!message || typeof message !== "string") return message;
|
||||
return message.replace(
|
||||
/<!--TOOL_CALL_END:([^:]+):(ENC:[\s\S]*?)-->/g,
|
||||
(_match, id: string, encoded: string) => {
|
||||
const decoded = decodeResultFromMarker(encoded) || encoded;
|
||||
return `<!--TOOL_CALL_END:${id}:${decoded}-->`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure any TOOL_CALL_END results are encoded. Useful for sanitizing messages
|
||||
* that might contain unencoded results due to legacy or partial updates.
|
||||
*/
|
||||
export function ensureEncodedToolCallMarkerResults(message: string): string {
|
||||
if (!message || typeof message !== "string") return message;
|
||||
return message.replace(
|
||||
/<!--TOOL_CALL_END:([^:]+):([\s\S]*?)-->/g,
|
||||
(_match, id: string, content: string) => {
|
||||
if (content.startsWith("ENC:")) {
|
||||
return _match;
|
||||
}
|
||||
const safe = encodeResultForMarker(content);
|
||||
return `<!--TOOL_CALL_END:${id}:${safe}-->`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse error chunks from a text segment
|
||||
* Format: <errorChunk>error content</errorChunk>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export interface ToolCall {
|
|||
args: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ToolExecutionResult {
|
||||
interface ToolExecutionResult {
|
||||
toolName: string;
|
||||
result: string;
|
||||
success: boolean;
|
||||
|
|
@ -146,7 +146,7 @@ export async function executeSequentialToolCall(
|
|||
/**
|
||||
* Get display name for tool (user-friendly version)
|
||||
*/
|
||||
export function getToolDisplayName(toolName: string): string {
|
||||
function getToolDisplayName(toolName: string): string {
|
||||
// Special handling for localSearch to show the actual search type being used
|
||||
if (toolName === "localSearch") {
|
||||
const settings = getSettings();
|
||||
|
|
@ -184,7 +184,7 @@ export function getToolDisplayName(toolName: string): string {
|
|||
/**
|
||||
* Get emoji for tool display
|
||||
*/
|
||||
export function getToolEmoji(toolName: string): string {
|
||||
function getToolEmoji(toolName: string): string {
|
||||
const emojiMap: Record<string, string> = {
|
||||
localSearch: "🔍",
|
||||
webSearch: "🌐",
|
||||
|
|
@ -211,32 +211,6 @@ export function getToolEmoji(toolName: string): string {
|
|||
return emojiMap[toolName] || "🔧";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user confirmation message for tool call
|
||||
*/
|
||||
export function getToolConfirmtionMessage(
|
||||
toolName: string,
|
||||
toolArgs?: Record<string, unknown>
|
||||
): string | null {
|
||||
if (toolName == "writeFile" || toolName == "editFile") {
|
||||
return "Accept / reject in the Preview";
|
||||
}
|
||||
|
||||
// Display salient terms for lexical search
|
||||
if (toolName === "localSearch" && toolArgs?.salientTerms) {
|
||||
const settings = getSettings();
|
||||
// Only show salient terms for lexical search (index-free)
|
||||
if (!settings.enableSemanticSearchV3) {
|
||||
const terms = Array.isArray(toolArgs.salientTerms) ? toolArgs.salientTerms : [];
|
||||
if (terms.length > 0) {
|
||||
return `Terms: ${terms.slice(0, 3).join(", ")}${terms.length > 3 ? "..." : ""}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log tool call details for debugging
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { processRawChatHistory, processedMessagesToTextOnly } from "./chatHistor
|
|||
/**
|
||||
* Options for building prompt debug sections with annotated provenance.
|
||||
*/
|
||||
export interface BuildPromptDebugSectionsOptions {
|
||||
interface BuildPromptDebugSectionsOptions {
|
||||
systemSections: PromptSection[];
|
||||
rawHistory?: unknown[];
|
||||
adapterName: string;
|
||||
|
|
@ -27,9 +27,7 @@ export interface PromptDebugReport {
|
|||
* @param options - Data required to assemble annotated prompt sections.
|
||||
* @returns Prompt sections with provenance metadata.
|
||||
*/
|
||||
export function buildPromptDebugSections(
|
||||
options: BuildPromptDebugSectionsOptions
|
||||
): PromptSection[] {
|
||||
function buildPromptDebugSections(options: BuildPromptDebugSectionsOptions): PromptSection[] {
|
||||
const { systemSections, rawHistory, adapterName, originalUserMessage, enhancedUserMessage } =
|
||||
options;
|
||||
const sections: PromptSection[] = [...systemSections];
|
||||
|
|
@ -76,7 +74,7 @@ export function buildPromptDebugSections(
|
|||
* @param sections - Prompt sections with provenance metadata.
|
||||
* @returns Multiline string with section headers that identify code sources.
|
||||
*/
|
||||
export function formatPromptSectionsWithAnnotations(sections: PromptSection[]): string {
|
||||
function formatPromptSectionsWithAnnotations(sections: PromptSection[]): string {
|
||||
return sections
|
||||
.map((section) => {
|
||||
const header = `[Section: ${section.label} | Source: ${section.source}]`;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import {
|
|||
safeFetchNoThrow,
|
||||
shouldUseGitHubCopilotResponsesApi,
|
||||
} from "@/utils";
|
||||
import { HarmBlockThreshold, HarmCategory } from "@google/generative-ai";
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
||||
import { BaseLanguageModel } from "@langchain/core/language_models/base";
|
||||
|
|
@ -42,6 +41,14 @@ import { ChatLMStudio } from "./ChatLMStudio";
|
|||
import { BedrockChatModel, type BedrockChatModelFields } from "./BedrockChatModel";
|
||||
import { GitHubCopilotChatModel } from "@/LLMProviders/githubCopilot/GitHubCopilotChatModel";
|
||||
import { GitHubCopilotResponsesModel } from "@/LLMProviders/githubCopilot/GitHubCopilotResponsesModel";
|
||||
import type { SafetySetting } from "@google/generative-ai";
|
||||
|
||||
const GOOGLE_SAFETY_SETTINGS_BLOCK_NONE: SafetySetting[] = [
|
||||
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" } as SafetySetting,
|
||||
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" } as SafetySetting,
|
||||
{ category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" } as SafetySetting,
|
||||
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" } as SafetySetting,
|
||||
];
|
||||
|
||||
// Patch BaseLanguageModel.prototype.getNumTokens once at module load to prevent
|
||||
// tiktoken CDN fetches. LangChain's default getNumTokens() downloads a ~3MB BPE
|
||||
|
|
@ -290,24 +297,7 @@ export default class ChatModelManager {
|
|||
[ChatModelProviders.GOOGLE]: {
|
||||
apiKey: await getDecryptedKey(customModel.apiKey || settings.googleApiKey),
|
||||
model: modelName,
|
||||
safetySettings: [
|
||||
{
|
||||
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
||||
threshold: HarmBlockThreshold.BLOCK_NONE,
|
||||
},
|
||||
{
|
||||
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
||||
threshold: HarmBlockThreshold.BLOCK_NONE,
|
||||
},
|
||||
{
|
||||
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||
threshold: HarmBlockThreshold.BLOCK_NONE,
|
||||
},
|
||||
{
|
||||
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
|
||||
threshold: HarmBlockThreshold.BLOCK_NONE,
|
||||
},
|
||||
],
|
||||
safetySettings: GOOGLE_SAFETY_SETTINGS_BLOCK_NONE,
|
||||
baseUrl: customModel.baseUrl,
|
||||
},
|
||||
[ChatModelProviders.XAI]: {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export const projectContextLoadAtom = atom<ProjectContextLoadState>({
|
|||
total: [],
|
||||
});
|
||||
|
||||
export interface IndexingProgressState {
|
||||
interface IndexingProgressState {
|
||||
isActive: boolean;
|
||||
isPaused: boolean;
|
||||
isCancelled: boolean;
|
||||
|
|
@ -70,7 +70,7 @@ export interface IndexingProgressState {
|
|||
completionStatus: "none" | "success" | "cancelled" | "error";
|
||||
}
|
||||
|
||||
export const indexingProgressAtom = atom<IndexingProgressState>({
|
||||
const indexingProgressAtom = atom<IndexingProgressState>({
|
||||
isActive: false,
|
||||
isPaused: false,
|
||||
isCancelled: false,
|
||||
|
|
@ -237,26 +237,10 @@ export function subscribeToProjectChange(
|
|||
});
|
||||
}
|
||||
|
||||
export function useCurrentProject() {
|
||||
return useAtom(currentProjectAtom, {
|
||||
store: settingsStore,
|
||||
});
|
||||
}
|
||||
|
||||
export function setProjectLoading(loading: boolean) {
|
||||
settingsStore.set(projectLoadingAtom, loading);
|
||||
}
|
||||
|
||||
export function isProjectLoading(): boolean {
|
||||
return settingsStore.get(projectLoadingAtom);
|
||||
}
|
||||
|
||||
export function subscribeToProjectLoadingChange(callback: (loading: boolean) => void): () => void {
|
||||
return settingsStore.sub(projectLoadingAtom, () => {
|
||||
callback(settingsStore.get(projectLoadingAtom));
|
||||
});
|
||||
}
|
||||
|
||||
export function useProjectLoading() {
|
||||
return useAtom(projectLoadingAtom, {
|
||||
store: settingsStore,
|
||||
|
|
@ -275,11 +259,6 @@ export function getSelectedTextContexts(): SelectedTextContext[] {
|
|||
return settingsStore.get(selectedTextContextsAtom);
|
||||
}
|
||||
|
||||
export function addSelectedTextContext(context: SelectedTextContext) {
|
||||
const current = getSelectedTextContexts();
|
||||
setSelectedTextContexts([...current, context]);
|
||||
}
|
||||
|
||||
export function removeSelectedTextContext(id: string) {
|
||||
const current = getSelectedTextContexts();
|
||||
setSelectedTextContexts(current.filter((context) => context.id !== id));
|
||||
|
|
@ -295,13 +274,6 @@ export function useSelectedTextContexts() {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the project context load state from the atom.
|
||||
*/
|
||||
export function getProjectContextLoadState(): Readonly<ProjectContextLoadState> {
|
||||
return settingsStore.get(projectContextLoadAtom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the project context load state in the atom.
|
||||
*/
|
||||
|
|
@ -322,17 +294,6 @@ export function updateProjectContextLoadState<K extends keyof ProjectContextLoad
|
|||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to changes in the project context load state.
|
||||
*/
|
||||
export function subscribeToProjectContextLoadChange(
|
||||
callback: (state: ProjectContextLoadState) => void
|
||||
): () => void {
|
||||
return settingsStore.sub(projectContextLoadAtom, () => {
|
||||
callback(settingsStore.get(projectContextLoadAtom));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get the project context load state from the atom.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { CustomCommand } from "@/commands/type";
|
||||
|
||||
export const LEGACY_SELECTED_TEXT_PLACEHOLDER = "{copilot-selection}";
|
||||
export const COMMAND_NAME_MAX_LENGTH = 50;
|
||||
export const QUICK_COMMAND_CODE_BLOCK = "copilotquickcommand";
|
||||
export const EMPTY_COMMAND: CustomCommand = {
|
||||
title: "",
|
||||
content: "",
|
||||
|
|
|
|||
|
|
@ -6,11 +6,10 @@ import {
|
|||
COPILOT_COMMAND_SLASH_ENABLED,
|
||||
EMPTY_COMMAND,
|
||||
LEGACY_SELECTED_TEXT_PLACEHOLDER,
|
||||
QUICK_COMMAND_CODE_BLOCK,
|
||||
} from "@/commands/constants";
|
||||
import { CustomCommand } from "@/commands/type";
|
||||
import { logWarn } from "@/logger";
|
||||
import { normalizePath, Notice, TAbstractFile, TFile, Vault, Editor } from "obsidian";
|
||||
import { normalizePath, Notice, TAbstractFile, TFile, Vault } from "obsidian";
|
||||
import { getSettings } from "@/settings/model";
|
||||
import {
|
||||
updateCachedCommands,
|
||||
|
|
@ -146,7 +145,7 @@ export function sortCommandsByOrder(commands: CustomCommand[]): CustomCommand[]
|
|||
});
|
||||
}
|
||||
|
||||
export function sortCommandsByRecency(commands: CustomCommand[]): CustomCommand[] {
|
||||
function sortCommandsByRecency(commands: CustomCommand[]): CustomCommand[] {
|
||||
return sortByStrategy(commands, "recent", {
|
||||
getName: (command) => command.title,
|
||||
getCreatedAtMs: () => 0,
|
||||
|
|
@ -154,7 +153,7 @@ export function sortCommandsByRecency(commands: CustomCommand[]): CustomCommand[
|
|||
});
|
||||
}
|
||||
|
||||
export function sortCommandsByAlphabetical(commands: CustomCommand[]): CustomCommand[] {
|
||||
function sortCommandsByAlphabetical(commands: CustomCommand[]): CustomCommand[] {
|
||||
return sortByStrategy(commands, "name", {
|
||||
getName: (command) => command.title,
|
||||
getCreatedAtMs: () => 0,
|
||||
|
|
@ -514,68 +513,3 @@ export async function ensureCommandFrontmatter(file: TFile, command: CustomComma
|
|||
removePendingFileWrite(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all quick command code blocks from the editor while preserving cursor position and selection
|
||||
* @param editor - The Obsidian editor instance
|
||||
* @returns true if any blocks were removed, false otherwise
|
||||
*/
|
||||
export function removeQuickCommandBlocks(editor: Editor): boolean {
|
||||
// Store original selection positions
|
||||
const originalFrom = editor.getCursor("from");
|
||||
const originalTo = editor.getCursor("to");
|
||||
|
||||
const content = editor.getValue();
|
||||
const lines = content.split("\n");
|
||||
let hasExisting = false;
|
||||
const newLines = [];
|
||||
let removedLinesBeforeFrom = 0;
|
||||
let removedLinesBeforeTo = 0;
|
||||
let i = 0;
|
||||
|
||||
while (i < lines.length) {
|
||||
if (lines[i].trim() === `\`\`\`${QUICK_COMMAND_CODE_BLOCK}`) {
|
||||
hasExisting = true;
|
||||
const blockStartLine = i;
|
||||
|
||||
// Skip the opening line
|
||||
i++;
|
||||
// Skip until we find the closing ```
|
||||
while (i < lines.length && lines[i].trim() !== "```") {
|
||||
i++;
|
||||
}
|
||||
// Skip the closing line
|
||||
i++;
|
||||
|
||||
const removedLineCount = i - blockStartLine;
|
||||
|
||||
// Calculate how many lines were removed before the selection positions
|
||||
if (blockStartLine <= originalFrom.line) {
|
||||
removedLinesBeforeFrom += removedLineCount;
|
||||
}
|
||||
if (blockStartLine <= originalTo.line) {
|
||||
removedLinesBeforeTo += removedLineCount;
|
||||
}
|
||||
} else {
|
||||
newLines.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Update editor content and restore selection if we removed existing blocks
|
||||
if (hasExisting) {
|
||||
editor.setValue(newLines.join("\n"));
|
||||
|
||||
// Calculate new selection positions accounting for removed lines
|
||||
const newFromLine = Math.max(0, originalFrom.line - removedLinesBeforeFrom);
|
||||
const newToLine = Math.max(0, originalTo.line - removedLinesBeforeTo);
|
||||
|
||||
// Restore the selection
|
||||
editor.setSelection(
|
||||
{ line: newFromLine, ch: originalFrom.ch },
|
||||
{ line: newToLine, ch: originalTo.ch }
|
||||
);
|
||||
}
|
||||
|
||||
return hasExisting;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,11 +36,7 @@ import { setSelectedTextContexts } from "@/aiParams";
|
|||
/**
|
||||
* Add a command to the plugin. Supports async callbacks; errors are logged.
|
||||
*/
|
||||
export function addCommand(
|
||||
plugin: CopilotPlugin,
|
||||
id: CommandId,
|
||||
callback: () => void | Promise<void>
|
||||
) {
|
||||
function addCommand(plugin: CopilotPlugin, id: CommandId, callback: () => void | Promise<void>) {
|
||||
plugin.addCommand({
|
||||
id,
|
||||
name: COMMAND_NAMES[id],
|
||||
|
|
@ -78,7 +74,7 @@ function addEditorCommand(
|
|||
/**
|
||||
* Add a check command to the plugin.
|
||||
*/
|
||||
export function addCheckCommand(
|
||||
function addCheckCommand(
|
||||
plugin: CopilotPlugin,
|
||||
id: CommandId,
|
||||
callback: (checking: boolean) => boolean | void
|
||||
|
|
|
|||
|
|
@ -18,16 +18,6 @@ export function isFileWritePending(filePath: string) {
|
|||
return pendingFileWritesAtom.has(filePath);
|
||||
}
|
||||
|
||||
export function createCachedCommand(command: CustomCommand): CustomCommand {
|
||||
const commands = customCommandsStore.get(customCommandsAtom);
|
||||
const existingCommand = commands.find((c) => c.title === command.title);
|
||||
if (existingCommand) {
|
||||
return existingCommand;
|
||||
}
|
||||
customCommandsStore.set(customCommandsAtom, [...commands, command]);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function deleteCachedCommand(title: string) {
|
||||
const commands = customCommandsStore.get(customCommandsAtom);
|
||||
customCommandsStore.set(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { type PropsWithChildren, useRef, useState } from "react";
|
|||
const TOLERANCE = 2;
|
||||
// detects text-overflow ellipses being used
|
||||
// ref: https://stackoverflow.com/questions/7738117/html-text-overflow-ellipsis-detection
|
||||
export function isEllipsesActive(
|
||||
function isEllipsesActive(
|
||||
textRef: React.MutableRefObject<HTMLDivElement | null>,
|
||||
lineClamp?: number
|
||||
): boolean {
|
||||
|
|
|
|||
|
|
@ -176,5 +176,3 @@ export const AgentReasoningBlock: React.FC<AgentReasoningBlockProps> = ({
|
|||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentReasoningBlock;
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ interface ObsidianAppWithPlugins {
|
|||
};
|
||||
}
|
||||
|
||||
export async function refreshVaultIndex() {
|
||||
async function refreshVaultIndex() {
|
||||
try {
|
||||
const { getSettings } = await import("@/settings/model");
|
||||
const settings = getSettings();
|
||||
|
|
@ -78,7 +78,7 @@ export async function refreshVaultIndex() {
|
|||
}
|
||||
}
|
||||
|
||||
export async function forceReindexVault() {
|
||||
async function forceReindexVault() {
|
||||
try {
|
||||
const { getSettings } = await import("@/settings/model");
|
||||
const settings = getSettings();
|
||||
|
|
@ -146,7 +146,7 @@ export async function reloadCurrentProject(app: App) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function forceRebuildCurrentProjectContext(app: App) {
|
||||
async function forceRebuildCurrentProjectContext(app: App) {
|
||||
const currentProject = getCurrentProject();
|
||||
if (!currentProject) {
|
||||
new Notice("No project is currently selected to rebuild.");
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ const INLINE_CITATION_RE = /\[(\d+(?:\s*,\s*\d+)*)\]/g;
|
|||
* to the corresponding source note. Reads the source mapping from the
|
||||
* rendered .copilot-sources section in the same message.
|
||||
*/
|
||||
export const linkInlineCitations = (root: HTMLElement): void => {
|
||||
const linkInlineCitations = (root: HTMLElement): void => {
|
||||
// Build citation number -> source anchor mapping from the rendered sources section.
|
||||
// We store the anchor element (not just the href) so we can copy Obsidian-specific
|
||||
// attributes like data-href and class="internal-link" onto the inline citation link.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const COPILOT_COLLAPSIBLE_DOM_ID_PREFIX = "copilot-collapsible";
|
||||
const COPILOT_COLLAPSIBLE_DOM_ID_PREFIX = "copilot-collapsible";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export interface CategoryOption extends TypeaheadOption {
|
|||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
export const CATEGORY_OPTIONS: CategoryOption[] = [
|
||||
const CATEGORY_OPTIONS: CategoryOption[] = [
|
||||
{
|
||||
key: "notes",
|
||||
title: "Notes",
|
||||
|
|
|
|||
|
|
@ -173,34 +173,6 @@ export function $isActiveWebTabPillNode(
|
|||
return node instanceof ActiveWebTabPillNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the editor currently contains an active web tab pill.
|
||||
* Used to determine if Active Web Tab should be included at send time,
|
||||
* avoiding async pill-sync race conditions.
|
||||
* @returns True if at least one ActiveWebTabPillNode exists in the editor
|
||||
*/
|
||||
export function $hasActiveWebTabPill(): boolean {
|
||||
const root = $getRoot();
|
||||
|
||||
function traverse(node: LexicalNode): boolean {
|
||||
if ($isActiveWebTabPillNode(node)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("getChildren" in node && typeof node.getChildren === "function") {
|
||||
const children = (node as { getChildren: () => LexicalNode[] }).getChildren();
|
||||
for (const child of children) {
|
||||
if (traverse(child)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return traverse(root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all active web tab pills from the editor.
|
||||
* @returns The number of pills removed
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export function $isFolderPillNode(node: LexicalNode): node is FolderPillNode {
|
|||
return node instanceof FolderPillNode;
|
||||
}
|
||||
|
||||
export function $findFolderPills(): FolderPillNode[] {
|
||||
function $findFolderPills(): FolderPillNode[] {
|
||||
const root = $getRoot();
|
||||
const pills: FolderPillNode[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -165,27 +165,6 @@ export function $isNotePillNode(node: LexicalNode | null | undefined): node is N
|
|||
return node instanceof NotePillNode;
|
||||
}
|
||||
|
||||
export function $findNotePills(): NotePillNode[] {
|
||||
const root = $getRoot();
|
||||
const pills: NotePillNode[] = [];
|
||||
|
||||
function traverse(node: LexicalNode) {
|
||||
if (node instanceof NotePillNode) {
|
||||
pills.push(node);
|
||||
}
|
||||
|
||||
if ("getChildren" in node && typeof node.getChildren === "function") {
|
||||
const children = node.getChildren() as LexicalNode[];
|
||||
for (const child of children) {
|
||||
traverse(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverse(root);
|
||||
return pills;
|
||||
}
|
||||
|
||||
export function $removePillsByPath(notePath: string): number {
|
||||
const root = $getRoot();
|
||||
let removedCount = 0;
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export function $isToolPillNode(node: LexicalNode): node is ToolPillNode {
|
|||
return node instanceof ToolPillNode;
|
||||
}
|
||||
|
||||
export function $findToolPills(): ToolPillNode[] {
|
||||
function $findToolPills(): ToolPillNode[] {
|
||||
const root = $getRoot();
|
||||
const pills: ToolPillNode[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export function $createURLPillNode(url: string, title?: string, isActive = false
|
|||
return new URLPillNode(url, title, isActive);
|
||||
}
|
||||
|
||||
export function $findURLPills(): URLPillNode[] {
|
||||
function $findURLPills(): URLPillNode[] {
|
||||
const root = $getRoot();
|
||||
const pills: URLPillNode[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -208,32 +208,6 @@ export function $findWebTabPills(): WebTabPillNode[] {
|
|||
return pills;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a WebTabPillNode with the given URL exists in the editor.
|
||||
* Must be called within a Lexical read/update context.
|
||||
* Uses early-exit traversal for better performance.
|
||||
*/
|
||||
export function $hasWebTabPillWithUrl(url: string): boolean {
|
||||
const root = $getRoot();
|
||||
|
||||
function traverse(node: LexicalNode): boolean {
|
||||
if (node instanceof WebTabPillNode && node.getURL() === url) {
|
||||
return true; // Early exit on match
|
||||
}
|
||||
if ("getChildren" in node && typeof node.getChildren === "function") {
|
||||
const children = (node as { getChildren: () => LexicalNode[] }).getChildren();
|
||||
for (const child of children) {
|
||||
if (traverse(child)) {
|
||||
return true; // Propagate early exit
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return traverse(root);
|
||||
}
|
||||
|
||||
/** Remove WebTabPillNodes by URL. */
|
||||
export function $removeWebTabPillsByUrl(url: string): void {
|
||||
const pills = $findWebTabPills();
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export interface PillData {
|
|||
/**
|
||||
* Generic function to create pill nodes based on type and data
|
||||
*/
|
||||
export function $createPillNode(pillData: PillData) {
|
||||
function $createPillNode(pillData: PillData) {
|
||||
const { type, title, data } = pillData;
|
||||
|
||||
switch (type) {
|
||||
|
|
|
|||
|
|
@ -11,10 +11,7 @@ declare const app: App;
|
|||
* @param maxLength - Maximum length for truncated content (default: 500)
|
||||
* @returns Promise resolving to processed content string
|
||||
*/
|
||||
export async function loadNoteContentForPreview(
|
||||
file: TFile,
|
||||
maxLength: number = 500
|
||||
): Promise<string> {
|
||||
async function loadNoteContentForPreview(file: TFile, maxLength: number = 500): Promise<string> {
|
||||
try {
|
||||
// Handle PDF and canvas files - treat as empty content (no preview)
|
||||
if (file.extension === "pdf" || file.extension === "canvas") {
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Copy, ClipboardCopy } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface ChatMessageProps {
|
||||
message: Message;
|
||||
isStreaming?: boolean;
|
||||
onCopy?: (message: Message) => void;
|
||||
onInsert?: (message: Message) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual chat message bubble for Quick Ask modal.
|
||||
* User messages align right, assistant messages align left with action buttons.
|
||||
*/
|
||||
export function ChatMessage({ message, isStreaming, onCopy, onInsert }: ChatMessageProps) {
|
||||
const isUser = message.role === "user";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"tw-group tw-flex tw-flex-col tw-gap-1",
|
||||
isUser ? "tw-items-end" : "tw-items-start"
|
||||
)}
|
||||
>
|
||||
{/* Message bubble */}
|
||||
<div
|
||||
className={cn(
|
||||
"tw-max-w-[80%] tw-rounded-lg tw-px-3 tw-py-2 tw-text-sm",
|
||||
"tw-whitespace-pre-wrap tw-leading-relaxed",
|
||||
isUser ? "tw-bg-interactive-accent tw-text-on-accent" : "tw-bg-secondary tw-text-normal"
|
||||
)}
|
||||
>
|
||||
{message.content}
|
||||
{/* Streaming cursor for assistant messages */}
|
||||
{!isUser && isStreaming && (
|
||||
<span className="tw-ml-0.5 tw-inline-block tw-h-4 tw-w-1.5 tw-animate-pulse tw-rounded-sm tw-bg-interactive-accent tw-align-middle" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action buttons for assistant messages */}
|
||||
{!isUser && !isStreaming && (
|
||||
<div className="tw-flex tw-items-center tw-gap-1 tw-opacity-0 tw-transition-opacity group-hover:tw-opacity-100">
|
||||
<Button
|
||||
variant="ghost2"
|
||||
size="icon"
|
||||
className="tw-size-5 hover:tw-bg-modifier-hover"
|
||||
onClick={() => onCopy?.(message)}
|
||||
aria-label="Copy message"
|
||||
>
|
||||
<Copy className="tw-size-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost2"
|
||||
size="icon"
|
||||
className="tw-size-5 hover:tw-bg-modifier-hover"
|
||||
onClick={() => onInsert?.(message)}
|
||||
aria-label="Insert at cursor"
|
||||
>
|
||||
<ClipboardCopy className="tw-size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type { Message };
|
||||
|
|
@ -1,11 +1,2 @@
|
|||
export { DragHandle } from "./drag-handle";
|
||||
export { CloseButton } from "./close-button";
|
||||
export { CommandLabel } from "./command-label";
|
||||
export { ContentArea, type ContentState } from "./content-area";
|
||||
export { FollowUpInput } from "./follow-up-input";
|
||||
export { ActionButtons } from "./action-buttons";
|
||||
export { DraggableModal } from "./draggable-modal";
|
||||
export { ChatMessage, type Message } from "./chat-message";
|
||||
|
||||
// Composed modals
|
||||
export { type ContentState } from "./content-area";
|
||||
export { MenuCommandModal } from "./menu-command-modal";
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import { App, FuzzySuggestModal } from "obsidian";
|
||||
|
||||
export class ListPromptModal extends FuzzySuggestModal<string> {
|
||||
private onChoosePromptTitle: (promptTitle: string) => void;
|
||||
private promptTitles: string[];
|
||||
private descriptions: string[];
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
promptTitles: string[],
|
||||
onChoosePromptTitle: (promptTitle: string) => void,
|
||||
descriptions: string[] = []
|
||||
) {
|
||||
super(app);
|
||||
this.promptTitles = promptTitles;
|
||||
this.onChoosePromptTitle = onChoosePromptTitle;
|
||||
this.descriptions = descriptions;
|
||||
}
|
||||
|
||||
getItems(): string[] {
|
||||
return this.promptTitles;
|
||||
}
|
||||
|
||||
getItemText(promptTitle: string): string {
|
||||
const index = this.promptTitles.indexOf(promptTitle);
|
||||
const description = this.descriptions[index];
|
||||
return description ? `${promptTitle} (${description})` : promptTitle;
|
||||
}
|
||||
|
||||
onChooseItem(promptTitle: string, evt: MouseEvent | KeyboardEvent) {
|
||||
// Find the original title by matching against the promptTitles array
|
||||
const index = this.promptTitles.findIndex(
|
||||
(title) => promptTitle.startsWith(title + " (") || promptTitle === title
|
||||
);
|
||||
const actualTitle = index >= 0 ? this.promptTitles[index] : promptTitle;
|
||||
this.onChoosePromptTitle(actualTitle);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { App } from "obsidian";
|
||||
import { ConfirmModal } from "./ConfirmModal";
|
||||
|
||||
export class NewChatConfirmModal extends ConfirmModal {
|
||||
constructor(app: App, onConfirm: () => void) {
|
||||
super(
|
||||
app,
|
||||
onConfirm,
|
||||
"Starting a new chat will clear the current chat history. Any unsaved messages will be lost. Are you sure you want to continue?",
|
||||
"Start New Chat"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
import { App, Modal } from "obsidian";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AppContext, useApp } from "@/context";
|
||||
import React, { useState } from "react";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
import {
|
||||
categorizePatterns,
|
||||
createPatternSettingsValue,
|
||||
getDecodedPatterns,
|
||||
getExtensionPattern,
|
||||
getFilePattern,
|
||||
getTagPattern,
|
||||
} from "@/search/searchUtils";
|
||||
import { File, FileText, Folder, Tag, Wrench, X } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { TagSearchModal } from "@/components/modals/TagSearchModal";
|
||||
import { AddContextNoteModal } from "@/components/modals/AddContextNoteModal";
|
||||
import { FolderSearchModal } from "@/components/modals/FolderSearchModal";
|
||||
import { ExtensionInputModal } from "@/components/modals/ExtensionInputModal";
|
||||
import { CustomPatternInputModal } from "@/components/modals/CustomPatternInputModal";
|
||||
import { TruncatedText } from "@/components/TruncatedText";
|
||||
|
||||
function PatternListGroup({
|
||||
title,
|
||||
patterns,
|
||||
onRemove,
|
||||
}: {
|
||||
title: string;
|
||||
patterns: string[];
|
||||
onRemove: (pattern: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="tw-grid tw-grid-cols-4 tw-gap-2">
|
||||
<div className="tw-font-bold">{title}</div>
|
||||
<ul className="tw-col-span-3 tw-m-0 tw-flex tw-list-inside tw-list-disc tw-flex-col tw-gap-1 tw-pl-0">
|
||||
{patterns.map((pattern) => (
|
||||
<li
|
||||
key={pattern}
|
||||
className="tw-flex tw-gap-2 tw-rounded-md tw-pl-2 tw-pr-1 hover:tw-bg-modifier-hover"
|
||||
>
|
||||
<TruncatedText className="tw-flex-1">{pattern}</TruncatedText>
|
||||
<Button variant="ghost2" size="fit" onClick={() => onRemove(pattern)}>
|
||||
<X className="tw-size-4" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PatternMatchingModalContent({
|
||||
value: initialValue,
|
||||
onUpdate,
|
||||
container,
|
||||
}: {
|
||||
value: string;
|
||||
onUpdate: (value: string) => void;
|
||||
container: HTMLElement;
|
||||
}) {
|
||||
const app = useApp();
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const patterns = getDecodedPatterns(value);
|
||||
const { tagPatterns, extensionPatterns, folderPatterns, notePatterns } =
|
||||
categorizePatterns(patterns);
|
||||
|
||||
const updateCategories = (newCategories: {
|
||||
tagPatterns?: string[];
|
||||
extensionPatterns?: string[];
|
||||
folderPatterns?: string[];
|
||||
notePatterns?: string[];
|
||||
}) => {
|
||||
const newValue = createPatternSettingsValue({
|
||||
tagPatterns: newCategories.tagPatterns ?? tagPatterns,
|
||||
extensionPatterns: newCategories.extensionPatterns ?? extensionPatterns,
|
||||
folderPatterns: newCategories.folderPatterns ?? folderPatterns,
|
||||
notePatterns: newCategories.notePatterns ?? notePatterns,
|
||||
});
|
||||
setValue(newValue);
|
||||
onUpdate(newValue);
|
||||
};
|
||||
|
||||
const hasValue =
|
||||
tagPatterns.length > 0 ||
|
||||
extensionPatterns.length > 0 ||
|
||||
folderPatterns.length > 0 ||
|
||||
notePatterns.length > 0;
|
||||
|
||||
return (
|
||||
<div className="tw-mt-2 tw-flex tw-flex-col tw-gap-4">
|
||||
<div className="tw-flex tw-max-h-[400px] tw-flex-col tw-gap-2 tw-overflow-y-auto tw-rounded-md tw-border tw-border-solid tw-border-border tw-p-4">
|
||||
{!hasValue && <div className="tw-text-center tw-text-sm">No patterns specified</div>}
|
||||
{tagPatterns.length > 0 && (
|
||||
<PatternListGroup
|
||||
title="Tags"
|
||||
patterns={tagPatterns}
|
||||
onRemove={(pattern) => {
|
||||
const newPatterns = tagPatterns.filter((p) => p !== pattern);
|
||||
updateCategories({
|
||||
tagPatterns: newPatterns,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{extensionPatterns.length > 0 && (
|
||||
<PatternListGroup
|
||||
title="Extensions"
|
||||
patterns={extensionPatterns}
|
||||
onRemove={(pattern) => {
|
||||
const newPatterns = extensionPatterns.filter((p) => p !== pattern);
|
||||
updateCategories({
|
||||
extensionPatterns: newPatterns,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{folderPatterns.length > 0 && (
|
||||
<PatternListGroup
|
||||
title="Folders"
|
||||
patterns={folderPatterns}
|
||||
onRemove={(pattern) => {
|
||||
const newPatterns = folderPatterns.filter((p) => p !== pattern);
|
||||
updateCategories({
|
||||
folderPatterns: newPatterns,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{notePatterns.length > 0 && (
|
||||
<PatternListGroup
|
||||
title="Notes"
|
||||
patterns={notePatterns}
|
||||
onRemove={(pattern) => {
|
||||
const newPatterns = notePatterns.filter((p) => p !== pattern);
|
||||
updateCategories({
|
||||
notePatterns: newPatterns,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="tw-flex tw-justify-end tw-gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary">Add...</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" container={container}>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
new TagSearchModal(app, (tag) => {
|
||||
const tagPattern = getTagPattern(tag);
|
||||
if (tagPatterns.includes(tagPattern)) {
|
||||
return;
|
||||
}
|
||||
updateCategories({
|
||||
tagPatterns: [...tagPatterns, tagPattern],
|
||||
});
|
||||
}).open();
|
||||
}}
|
||||
>
|
||||
<div className="tw-flex tw-items-center tw-gap-2">
|
||||
<Tag className="tw-size-4" />
|
||||
Tag
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
new FolderSearchModal(app, (folder) => {
|
||||
if (folderPatterns.includes(folder)) {
|
||||
return;
|
||||
}
|
||||
updateCategories({
|
||||
folderPatterns: [...folderPatterns, folder],
|
||||
});
|
||||
}).open();
|
||||
}}
|
||||
>
|
||||
<div className="tw-flex tw-items-center tw-gap-2">
|
||||
<Folder className="tw-size-4" />
|
||||
Folder
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
new AddContextNoteModal({
|
||||
app,
|
||||
onNoteSelect: (note) => {
|
||||
const notePattern = getFilePattern(note);
|
||||
if (notePatterns.includes(notePattern)) {
|
||||
return;
|
||||
}
|
||||
updateCategories({
|
||||
notePatterns: [...notePatterns, notePattern],
|
||||
});
|
||||
},
|
||||
excludeNotePaths: [],
|
||||
titleOnly: true,
|
||||
}).open();
|
||||
}}
|
||||
>
|
||||
<div className="tw-flex tw-items-center tw-gap-2">
|
||||
<FileText className="tw-size-4" />
|
||||
Note
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
new ExtensionInputModal(app, (extension) => {
|
||||
const extensionPattern = getExtensionPattern(extension);
|
||||
if (extensionPatterns.includes(extensionPattern)) {
|
||||
return;
|
||||
}
|
||||
updateCategories({
|
||||
extensionPatterns: [...extensionPatterns, extensionPattern],
|
||||
});
|
||||
}).open();
|
||||
}}
|
||||
>
|
||||
<div className="tw-flex tw-items-center tw-gap-2">
|
||||
<File className="tw-size-4" />
|
||||
Extension
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
new CustomPatternInputModal(app, (value) => {
|
||||
const patterns = getDecodedPatterns(value);
|
||||
const {
|
||||
tagPatterns: newTagPatterns,
|
||||
extensionPatterns: newExtensionPatterns,
|
||||
folderPatterns: newFolderPatterns,
|
||||
notePatterns: newNotePatterns,
|
||||
} = categorizePatterns(patterns);
|
||||
updateCategories({
|
||||
tagPatterns: [...tagPatterns, ...newTagPatterns],
|
||||
extensionPatterns: [...extensionPatterns, ...newExtensionPatterns],
|
||||
folderPatterns: [...folderPatterns, ...newFolderPatterns],
|
||||
notePatterns: [...notePatterns, ...newNotePatterns],
|
||||
});
|
||||
}).open();
|
||||
}}
|
||||
>
|
||||
<div className="tw-flex tw-items-center tw-gap-2">
|
||||
<Wrench className="tw-size-4" />
|
||||
Custom
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export class PatternMatchingModal extends Modal {
|
||||
private root: Root;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
private onUpdate: (value: string) => void,
|
||||
/** The raw pattern matching value, separated by commas */
|
||||
private value: string,
|
||||
title: string
|
||||
) {
|
||||
super(app);
|
||||
// https://docs.obsidian.md/Reference/TypeScript+API/Modal/setTitle
|
||||
// @ts-ignore
|
||||
this.setTitle(title);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
this.root = createRoot(contentEl);
|
||||
|
||||
const handleUpdate = (value: string) => {
|
||||
this.onUpdate(value);
|
||||
};
|
||||
|
||||
this.root.render(
|
||||
<AppContext.Provider value={this.app}>
|
||||
<PatternMatchingModalContent
|
||||
value={this.value}
|
||||
onUpdate={handleUpdate}
|
||||
container={this.contentEl}
|
||||
/>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.root.unmount();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,297 +0,0 @@
|
|||
import { TruncatedText } from "@/components/TruncatedText";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
categorizePatterns,
|
||||
createPatternSettingsValue,
|
||||
getDecodedPatterns,
|
||||
getExtensionPattern,
|
||||
getFilePattern,
|
||||
getTagPattern,
|
||||
} from "@/search/searchUtils";
|
||||
import { File, FileText, Folder, Tag, Wrench, X } from "lucide-react";
|
||||
import { App, Modal, TFile } from "obsidian";
|
||||
import React, { useState } from "react";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
import { AppContext, useApp } from "@/context";
|
||||
import { CustomPatternInputModal } from "./CustomPatternInputModal";
|
||||
import { ExtensionInputModal } from "./ExtensionInputModal";
|
||||
import { FolderSearchModal } from "./FolderSearchModal";
|
||||
import { ProjectFileSelectModal } from "./ProjectFileSelectModal";
|
||||
import { TagSearchModal } from "./TagSearchModal";
|
||||
|
||||
function PatternListGroup({
|
||||
title,
|
||||
patterns,
|
||||
onRemove,
|
||||
}: {
|
||||
title: string;
|
||||
patterns: string[];
|
||||
onRemove: (pattern: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="tw-grid tw-grid-cols-4 tw-gap-2">
|
||||
<div className="tw-font-bold">{title}</div>
|
||||
<ul className="tw-col-span-3 tw-m-0 tw-flex tw-list-inside tw-list-disc tw-flex-col tw-gap-1 tw-pl-0">
|
||||
{patterns.map((pattern) => (
|
||||
<li
|
||||
key={pattern}
|
||||
className="tw-flex tw-gap-2 tw-rounded-md tw-pl-2 tw-pr-1 hover:tw-bg-modifier-hover"
|
||||
>
|
||||
<TruncatedText className="tw-flex-1">{pattern}</TruncatedText>
|
||||
<Button variant="ghost2" size="fit" onClick={() => onRemove(pattern)}>
|
||||
<X className="tw-size-4" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectPatternMatchingModalContent({
|
||||
value: initialValue,
|
||||
onUpdate,
|
||||
container,
|
||||
}: {
|
||||
value: string;
|
||||
onUpdate: (value: string) => void;
|
||||
container: HTMLElement;
|
||||
}) {
|
||||
const app = useApp();
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const patterns = getDecodedPatterns(value);
|
||||
const { tagPatterns, extensionPatterns, folderPatterns, notePatterns } =
|
||||
categorizePatterns(patterns);
|
||||
|
||||
const updateCategories = (newCategories: {
|
||||
tagPatterns?: string[];
|
||||
extensionPatterns?: string[];
|
||||
folderPatterns?: string[];
|
||||
notePatterns?: string[];
|
||||
}) => {
|
||||
const newValue = createPatternSettingsValue({
|
||||
tagPatterns: newCategories.tagPatterns ?? tagPatterns,
|
||||
extensionPatterns: newCategories.extensionPatterns ?? extensionPatterns,
|
||||
folderPatterns: newCategories.folderPatterns ?? folderPatterns,
|
||||
notePatterns: newCategories.notePatterns ?? notePatterns,
|
||||
});
|
||||
setValue(newValue);
|
||||
onUpdate(newValue);
|
||||
};
|
||||
|
||||
const hasValue =
|
||||
tagPatterns.length > 0 ||
|
||||
extensionPatterns.length > 0 ||
|
||||
folderPatterns.length > 0 ||
|
||||
notePatterns.length > 0;
|
||||
|
||||
return (
|
||||
<div className="tw-mt-2 tw-flex tw-flex-col tw-gap-4">
|
||||
<div className="tw-flex tw-max-h-[400px] tw-flex-col tw-gap-2 tw-overflow-y-auto tw-rounded-md tw-border tw-border-solid tw-border-border tw-p-4">
|
||||
{!hasValue && <div className="tw-text-center tw-text-sm">No patterns specified</div>}
|
||||
{tagPatterns.length > 0 && (
|
||||
<PatternListGroup
|
||||
title="Tags"
|
||||
patterns={tagPatterns}
|
||||
onRemove={(pattern) => {
|
||||
const newPatterns = tagPatterns.filter((p) => p !== pattern);
|
||||
updateCategories({
|
||||
tagPatterns: newPatterns,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{extensionPatterns.length > 0 && (
|
||||
<PatternListGroup
|
||||
title="Extensions"
|
||||
patterns={extensionPatterns}
|
||||
onRemove={(pattern) => {
|
||||
const newPatterns = extensionPatterns.filter((p) => p !== pattern);
|
||||
updateCategories({
|
||||
extensionPatterns: newPatterns,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{folderPatterns.length > 0 && (
|
||||
<PatternListGroup
|
||||
title="Folders"
|
||||
patterns={folderPatterns}
|
||||
onRemove={(pattern) => {
|
||||
const newPatterns = folderPatterns.filter((p) => p !== pattern);
|
||||
updateCategories({
|
||||
folderPatterns: newPatterns,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{notePatterns.length > 0 && (
|
||||
<PatternListGroup
|
||||
title="Files"
|
||||
patterns={notePatterns}
|
||||
onRemove={(pattern) => {
|
||||
const newPatterns = notePatterns.filter((p) => p !== pattern);
|
||||
updateCategories({
|
||||
notePatterns: newPatterns,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="tw-flex tw-justify-end tw-gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary">Add...</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" container={container}>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
new TagSearchModal(app, (tag) => {
|
||||
const tagPattern = getTagPattern(tag);
|
||||
if (tagPatterns.includes(tagPattern)) {
|
||||
return;
|
||||
}
|
||||
updateCategories({
|
||||
tagPatterns: [...tagPatterns, tagPattern],
|
||||
});
|
||||
}).open();
|
||||
}}
|
||||
>
|
||||
<div className="tw-flex tw-items-center tw-gap-2">
|
||||
<Tag className="tw-size-4" />
|
||||
Tag
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
new FolderSearchModal(app, (folder) => {
|
||||
if (folderPatterns.includes(folder)) {
|
||||
return;
|
||||
}
|
||||
updateCategories({
|
||||
folderPatterns: [...folderPatterns, folder],
|
||||
});
|
||||
}).open();
|
||||
}}
|
||||
>
|
||||
<div className="tw-flex tw-items-center tw-gap-2">
|
||||
<Folder className="tw-size-4" />
|
||||
Folder
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
new ProjectFileSelectModal({
|
||||
app,
|
||||
onFileSelect: (file: TFile) => {
|
||||
const filePattern = getFilePattern(file);
|
||||
if (notePatterns.includes(filePattern)) {
|
||||
return;
|
||||
}
|
||||
updateCategories({
|
||||
notePatterns: [...notePatterns, filePattern],
|
||||
});
|
||||
},
|
||||
excludeFilePaths: [],
|
||||
titleOnly: true,
|
||||
}).open();
|
||||
}}
|
||||
>
|
||||
<div className="tw-flex tw-items-center tw-gap-2">
|
||||
<FileText className="tw-size-4" />
|
||||
Files
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
new ExtensionInputModal(app, (extension) => {
|
||||
const extensionPattern = getExtensionPattern(extension);
|
||||
if (extensionPatterns.includes(extensionPattern)) {
|
||||
return;
|
||||
}
|
||||
updateCategories({
|
||||
extensionPatterns: [...extensionPatterns, extensionPattern],
|
||||
});
|
||||
}).open();
|
||||
}}
|
||||
>
|
||||
<div className="tw-flex tw-items-center tw-gap-2">
|
||||
<File className="tw-size-4" />
|
||||
Extension
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
new CustomPatternInputModal(app, (value) => {
|
||||
const patterns = getDecodedPatterns(value);
|
||||
const {
|
||||
tagPatterns: newTagPatterns,
|
||||
extensionPatterns: newExtensionPatterns,
|
||||
folderPatterns: newFolderPatterns,
|
||||
notePatterns: newNotePatterns,
|
||||
} = categorizePatterns(patterns);
|
||||
updateCategories({
|
||||
tagPatterns: [...tagPatterns, ...newTagPatterns],
|
||||
extensionPatterns: [...extensionPatterns, ...newExtensionPatterns],
|
||||
folderPatterns: [...folderPatterns, ...newFolderPatterns],
|
||||
notePatterns: [...notePatterns, ...newNotePatterns],
|
||||
});
|
||||
}).open();
|
||||
}}
|
||||
>
|
||||
<div className="tw-flex tw-items-center tw-gap-2">
|
||||
<Wrench className="tw-size-4" />
|
||||
Custom
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export class ProjectPatternMatchingModal extends Modal {
|
||||
private root: Root;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
private onUpdate: (value: string) => void,
|
||||
/** The raw pattern matching value, separated by commas */
|
||||
private value: string,
|
||||
title: string
|
||||
) {
|
||||
super(app);
|
||||
// @ts-ignore
|
||||
this.setTitle(title);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
this.root = createRoot(contentEl);
|
||||
|
||||
const handleUpdate = (value: string) => {
|
||||
this.onUpdate(value);
|
||||
};
|
||||
|
||||
this.root.render(
|
||||
<AppContext.Provider value={this.app}>
|
||||
<ProjectPatternMatchingModalContent
|
||||
value={this.value}
|
||||
onUpdate={handleUpdate}
|
||||
container={this.contentEl}
|
||||
/>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.root.unmount();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
/**
|
||||
* ModeSelector - Dropdown for selecting Quick Ask mode.
|
||||
* Uses DropdownMenu for consistent styling with ModelSelector.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { MessageCircle, Pencil, Zap, ChevronDown } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import type { QuickAskMode, QuickAskModeConfig } from "./types";
|
||||
|
||||
interface ModeSelectorProps {
|
||||
modes: QuickAskModeConfig[];
|
||||
value: QuickAskMode;
|
||||
onChange: (mode: QuickAskMode) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
"message-circle": MessageCircle,
|
||||
pencil: Pencil,
|
||||
zap: Zap,
|
||||
};
|
||||
|
||||
/**
|
||||
* ModeSelector component for Quick Ask panel.
|
||||
*/
|
||||
export function ModeSelector({ modes, value, onChange, disabled }: ModeSelectorProps) {
|
||||
const currentMode = modes.find((m) => m.id === value);
|
||||
const IconComponent = iconMap[currentMode?.icon ?? ""] ?? MessageCircle;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
className="tw-min-w-0 tw-gap-1 tw-px-1.5 tw-text-muted"
|
||||
>
|
||||
<IconComponent className="tw-size-3.5" />
|
||||
<span className="tw-text-xs">{currentMode?.label ?? "Ask"}</span>
|
||||
{!disabled && <ChevronDown className="tw-size-3.5" />}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="start">
|
||||
{modes.map((mode) => {
|
||||
const ModeIcon = iconMap[mode.icon] ?? MessageCircle;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={mode.id}
|
||||
onSelect={() => onChange(mode.id)}
|
||||
disabled={!mode.implemented}
|
||||
className="tw-gap-2"
|
||||
>
|
||||
<ModeIcon className="tw-size-4" />
|
||||
<div className="tw-flex tw-flex-col">
|
||||
<span>{mode.label}</span>
|
||||
{!mode.implemented && <span className="tw-text-xs tw-text-muted">Coming soon</span>}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,9 +20,6 @@ import { HelpTooltip } from "@/components/ui/help-tooltip";
|
|||
import { useQuickAskSession } from "./useQuickAskSession";
|
||||
import { QuickAskMessageComponent } from "./QuickAskMessage";
|
||||
import { QuickAskInput } from "./QuickAskInput";
|
||||
// TODO: Uncomment when Edit/Edit-Direct modes are implemented
|
||||
// import { ModeSelector } from "./ModeSelector";
|
||||
// import { modeRegistry } from "./modeRegistry";
|
||||
import type { QuickAskPanelProps } from "./types";
|
||||
import type { ReplaceInvalidReason } from "@/editor/replaceGuard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -43,8 +40,6 @@ export function QuickAskPanel({
|
|||
}: QuickAskPanelProps) {
|
||||
// UI state
|
||||
const [inputText, setInputText] = useState("");
|
||||
// TODO: Uncomment when Edit/Edit-Direct modes are implemented
|
||||
// const [mode, setMode] = useState<QuickAskMode>("ask");
|
||||
const chatAreaRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isChatPinnedToBottomRef = useRef(true);
|
||||
|
|
@ -80,17 +75,6 @@ export function QuickAskPanel({
|
|||
// Previously used selectionFrom/selectionTo props that were stale and never updated.
|
||||
const selectionRange = replaceGuard.getRange();
|
||||
const hasSelection = !!selectionRange && selectionRange.from !== selectionRange.to;
|
||||
// TODO: Uncomment when Edit/Edit-Direct modes are implemented
|
||||
// const availableModes = modeRegistry.getAvailable(hasSelection);
|
||||
|
||||
// Keep mode valid when selection state changes
|
||||
// TODO: Uncomment when Edit/Edit-Direct modes are implemented
|
||||
// useEffect(() => {
|
||||
// const modes = modeRegistry.getAvailable(hasSelection);
|
||||
// if (!modes.some((m) => m.id === mode)) {
|
||||
// setMode(modes[0]?.id ?? "ask");
|
||||
// }
|
||||
// }, [hasSelection, mode]);
|
||||
|
||||
const lastMessageId = messages[messages.length - 1]?.id;
|
||||
const lastAssistantIdx = useMemo(() => {
|
||||
|
|
@ -350,15 +334,6 @@ export function QuickAskPanel({
|
|||
{/* Toolbar - always at bottom */}
|
||||
<div className="tw-mt-auto tw-flex tw-items-center tw-justify-between tw-gap-2 tw-border-t tw-border-solid tw-border-border tw-px-3 tw-py-1.5">
|
||||
<div className="tw-flex tw-items-center tw-gap-1">
|
||||
{/* TODO: Uncomment when Edit/Edit-Direct modes are implemented
|
||||
<ModeSelector
|
||||
modes={availableModes}
|
||||
value={mode}
|
||||
onChange={setMode}
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
*/}
|
||||
|
||||
<ModelSelector
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
/**
|
||||
* Quick Ask components exports.
|
||||
*/
|
||||
|
||||
export { QuickAskOverlay } from "./QuickAskOverlay";
|
||||
export { QuickAskPanel } from "./QuickAskPanel";
|
||||
export { QuickAskMessageComponent } from "./QuickAskMessage";
|
||||
export { QuickAskInput } from "./QuickAskInput";
|
||||
export { useQuickAskSession } from "./useQuickAskSession";
|
||||
// Reason: ModeSelector and modeRegistry are not exported because edit/edit-direct modes
|
||||
// are not yet implemented. Export them when those modes are ready.
|
||||
export type {
|
||||
QuickAskMode,
|
||||
QuickAskMessage,
|
||||
QuickAskPanelProps,
|
||||
QuickAskWidgetPayload,
|
||||
} from "./types";
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
/**
|
||||
* Mode registry for Quick Ask feature.
|
||||
* Manages available modes and their configurations.
|
||||
*/
|
||||
|
||||
import type { QuickAskMode, QuickAskModeConfig } from "./types";
|
||||
import { QUICK_COMMAND_SYSTEM_PROMPT } from "@/commands/quickCommandPrompts";
|
||||
import { askModeConfig, editModeConfig, editDirectModeConfig } from "./modes";
|
||||
|
||||
/**
|
||||
* Registry class for managing Quick Ask modes.
|
||||
*/
|
||||
class QuickAskModeRegistry {
|
||||
private modes = new Map<QuickAskMode, QuickAskModeConfig>();
|
||||
|
||||
constructor() {
|
||||
// Register built-in modes
|
||||
this.register(askModeConfig);
|
||||
this.register(editModeConfig);
|
||||
this.register(editDirectModeConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a mode configuration.
|
||||
*/
|
||||
register(config: QuickAskModeConfig): void {
|
||||
this.modes.set(config.id, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a mode configuration by ID.
|
||||
*/
|
||||
get(id: QuickAskMode): QuickAskModeConfig | undefined {
|
||||
return this.modes.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all registered modes.
|
||||
*/
|
||||
getAll(): QuickAskModeConfig[] {
|
||||
return Array.from(this.modes.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets modes available based on selection state.
|
||||
*/
|
||||
getAvailable(hasSelection: boolean): QuickAskModeConfig[] {
|
||||
return this.getAll().filter((mode) => !mode.requiresSelection || hasSelection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the system prompt for a mode.
|
||||
*/
|
||||
getSystemPrompt(id: QuickAskMode): string {
|
||||
const mode = this.get(id);
|
||||
return mode?.systemPrompt ?? QUICK_COMMAND_SYSTEM_PROMPT;
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const modeRegistry = new QuickAskModeRegistry();
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
/**
|
||||
* Ask mode configuration for Quick Ask.
|
||||
* Multi-turn conversation mode - the default and primary mode.
|
||||
*/
|
||||
|
||||
import type { QuickAskModeConfig } from "../types";
|
||||
|
||||
export const askModeConfig: QuickAskModeConfig = {
|
||||
id: "ask",
|
||||
label: "Ask",
|
||||
icon: "message-circle",
|
||||
description: "Multi-turn conversation",
|
||||
requiresSelection: false,
|
||||
// systemPrompt is undefined - modeRegistry.getSystemPrompt() will use default
|
||||
implemented: true,
|
||||
};
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
/**
|
||||
* Edit-Direct mode configuration for Quick Ask.
|
||||
* Direct apply edits without preview - Future implementation.
|
||||
*/
|
||||
|
||||
import type { QuickAskModeConfig } from "../types";
|
||||
|
||||
export const editDirectModeConfig: QuickAskModeConfig = {
|
||||
id: "edit-direct",
|
||||
label: "Edit Direct",
|
||||
icon: "zap",
|
||||
description: "Apply edits directly",
|
||||
requiresSelection: true,
|
||||
implemented: false,
|
||||
};
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
/**
|
||||
* Edit mode configuration for Quick Ask.
|
||||
* Generate edits with preview - Future implementation.
|
||||
*/
|
||||
|
||||
import type { QuickAskModeConfig } from "../types";
|
||||
|
||||
export const editModeConfig: QuickAskModeConfig = {
|
||||
id: "edit",
|
||||
label: "Edit",
|
||||
icon: "pencil",
|
||||
description: "Generate edits with preview",
|
||||
requiresSelection: true,
|
||||
implemented: false,
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
/**
|
||||
* Mode configurations exports.
|
||||
*/
|
||||
|
||||
export { askModeConfig } from "./askMode";
|
||||
export { editModeConfig } from "./editMode";
|
||||
export { editDirectModeConfig } from "./editDirectMode";
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"tw-inline-flex tw-h-9 tw-items-center tw-justify-center tw-rounded-lg tw-p-1 tw-text-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"!tw-shadow-none",
|
||||
"data-[state=active]:!tw-bg-interactive-accent data-[state=active]:!tw-text-on-accent data-[state=active]:!tw-shadow-md",
|
||||
"tw-inline-flex tw-items-center tw-justify-center tw-whitespace-nowrap tw-rounded-md tw-px-3 tw-py-1 tw-text-sm tw-font-medium tw-ring-offset-ring tw-transition-all focus-visible:tw-outline-none focus-visible:tw-ring-2 focus-visible:tw-ring-ring focus-visible:tw-ring-offset-2 disabled:tw-pointer-events-none disabled:tw-opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"tw-mt-2 tw-ring-offset-ring focus-visible:tw-outline-none focus-visible:tw-ring-2 focus-visible:tw-ring-ring focus-visible:tw-ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
|
|
@ -1,36 +1,5 @@
|
|||
import { Change } from "diff";
|
||||
|
||||
function strikeThrough(content: string): string {
|
||||
const lines = content.trim().split("\n");
|
||||
return lines.map((line) => "- " + line).join("\n");
|
||||
}
|
||||
|
||||
// Get relevant changes only and combine them into a single markdown block
|
||||
export function getRelevantChangesMarkdown(blocks: Change[][]): string {
|
||||
const renderedChanges = blocks
|
||||
.map((block) => {
|
||||
const hasAddedChanges = block.some((change) => change.added);
|
||||
const hasRemovedChanges = block.some((change) => change.removed);
|
||||
let blockChange = "";
|
||||
if (hasAddedChanges) {
|
||||
blockChange = block.map((change) => (change.added ? change.value : "")).join("\n");
|
||||
} else if (hasRemovedChanges) {
|
||||
blockChange = block
|
||||
.map((change) => (change.removed ? strikeThrough(change.value) : ""))
|
||||
.join("\n");
|
||||
} else {
|
||||
const content = block.map((change) => change.value).join("\n");
|
||||
// Skip blocks with only whitespace.
|
||||
if (content.trim().length > 0) {
|
||||
blockChange = "...";
|
||||
}
|
||||
}
|
||||
return blockChange;
|
||||
})
|
||||
.join("\n");
|
||||
return renderedChanges;
|
||||
}
|
||||
|
||||
// Group changes into blocks for better UI presentation
|
||||
export function getChangeBlocks(changes: Change[]): Change[][] {
|
||||
const blocks: Change[][] = [];
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ export const AI_SENDER = "ai";
|
|||
|
||||
// Default folder names
|
||||
export const COPILOT_FOLDER_ROOT = "copilot";
|
||||
export const DEFAULT_CHAT_HISTORY_FOLDER = `${COPILOT_FOLDER_ROOT}/copilot-conversations`;
|
||||
export const DEFAULT_CUSTOM_PROMPTS_FOLDER = `${COPILOT_FOLDER_ROOT}/copilot-custom-prompts`;
|
||||
export const DEFAULT_MEMORY_FOLDER = `${COPILOT_FOLDER_ROOT}/memory`;
|
||||
export const DEFAULT_SYSTEM_PROMPTS_FOLDER = `${COPILOT_FOLDER_ROOT}/system-prompts`;
|
||||
export const DEFAULT_PROJECTS_FOLDER = `${COPILOT_FOLDER_ROOT}/projects`;
|
||||
export const DEFAULT_CONVERTED_DOC_OUTPUT_FOLDER = "";
|
||||
const DEFAULT_CHAT_HISTORY_FOLDER = `${COPILOT_FOLDER_ROOT}/copilot-conversations`;
|
||||
const DEFAULT_CUSTOM_PROMPTS_FOLDER = `${COPILOT_FOLDER_ROOT}/copilot-custom-prompts`;
|
||||
const DEFAULT_MEMORY_FOLDER = `${COPILOT_FOLDER_ROOT}/memory`;
|
||||
const DEFAULT_SYSTEM_PROMPTS_FOLDER = `${COPILOT_FOLDER_ROOT}/system-prompts`;
|
||||
const DEFAULT_PROJECTS_FOLDER = `${COPILOT_FOLDER_ROOT}/projects`;
|
||||
const DEFAULT_CONVERTED_DOC_OUTPUT_FOLDER = "";
|
||||
export const DEFAULT_QA_EXCLUSIONS_SETTING = COPILOT_FOLDER_ROOT;
|
||||
export const DEFAULT_SYSTEM_PROMPT = `You are Obsidian Copilot, a helpful assistant that integrates AI to Obsidian note-taking.
|
||||
1. Never mention that you do not have access to something. Always rely on the user provided context.
|
||||
|
|
@ -110,20 +110,17 @@ export const VARIABLE_NOTE_TAG = "variable_note";
|
|||
export const EMBEDDED_PDF_TAG = "embedded_pdf";
|
||||
export const EMBEDDED_NOTE_TAG = "embedded_note";
|
||||
export const DATAVIEW_BLOCK_TAG = "dataview_block";
|
||||
export const VAULT_NOTE_TAG = "vault_note";
|
||||
export const RETRIEVED_DOCUMENT_TAG = "retrieved_document";
|
||||
export const WEB_TAB_CONTEXT_TAG = "web_tab_context";
|
||||
export const ACTIVE_WEB_TAB_CONTEXT_TAG = "active_web_tab";
|
||||
export const YOUTUBE_VIDEO_CONTEXT_TAG = "youtube_video_context";
|
||||
/** Marker text used as placeholder for active web tab in serialized content */
|
||||
export const ACTIVE_WEB_TAB_MARKER = "{activeWebTab}";
|
||||
export const EMPTY_INDEX_ERROR_MESSAGE =
|
||||
"Copilot index does not exist. Please index your vault first!\n\n1. Set a working embedding model in QA settings. If it's not a local model, don't forget to set the API key. \n\n2. Click 'Refresh Index for Vault' and wait for indexing to complete. If you encounter the rate limiting error, please turn your request per second down in QA setting.";
|
||||
export const CHUNK_SIZE = 6000;
|
||||
export const TEXT_WEIGHT = 0.4;
|
||||
export const MAX_CHARS_FOR_LOCAL_SEARCH_CONTEXT = 448000;
|
||||
export const LLM_TIMEOUT_MS = 30000; // 30 seconds timeout for LLM operations
|
||||
export const DEFAULT_MAX_SOURCE_CHUNKS = 30; // Default max chunks for search results (with diverse top-K)
|
||||
const DEFAULT_MAX_SOURCE_CHUNKS = 30; // Default max chunks for search results (with diverse top-K)
|
||||
export const AGENT_LOOP_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes timeout for agent loop
|
||||
export const AGENT_MAX_ITERATIONS_LIMIT = 16; // Maximum allowed value for agent iterations setting
|
||||
export const LOADING_MESSAGES = {
|
||||
|
|
@ -176,11 +173,8 @@ export enum ChatModels {
|
|||
COPILOT_PLUS_FLASH = "copilot-plus-flash",
|
||||
GPT_5_5 = "gpt-5.5",
|
||||
GPT_5_4_mini = "gpt-5.4-mini",
|
||||
GPT_5_4_nano = "gpt-5.4-nano",
|
||||
GPT_41 = "gpt-4.1",
|
||||
GPT_41_mini = "gpt-4.1-mini",
|
||||
GPT_41_nano = "gpt-4.1-nano",
|
||||
O4_mini = "o4-mini",
|
||||
GEMINI_3_PRO_PREVIEW = "gemini-3.1-pro-preview",
|
||||
GEMINI_3_FLASH_PREVIEW = "gemini-3-flash-preview",
|
||||
GEMINI_3_FLASH_LITE = "gemini-3.1-flash-lite",
|
||||
|
|
@ -458,7 +452,6 @@ export enum EmbeddingModelProviders {
|
|||
}
|
||||
|
||||
export enum EmbeddingModels {
|
||||
OPENAI_EMBEDDING_ADA_V2 = "text-embedding-ada-002",
|
||||
OPENAI_EMBEDDING_SMALL = "text-embedding-3-small",
|
||||
OPENAI_EMBEDDING_LARGE = "text-embedding-3-large",
|
||||
AZURE_OPENAI = "azure-openai",
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export { compactBySection, truncateWithEllipsis } from "./compactionUtils";
|
|||
export { getSourceType as detectSourceType } from "./contextBlockRegistry";
|
||||
|
||||
// Re-export chat history compaction for backwards compatibility
|
||||
export { compactAssistantOutput, compactChatHistoryContent } from "./ChatHistoryCompactor";
|
||||
export { compactChatHistoryContent } from "./ChatHistoryCompactor";
|
||||
|
||||
/**
|
||||
* Extract the source identifier from an XML block.
|
||||
|
|
|
|||
|
|
@ -2,31 +2,11 @@
|
|||
* Editor utilities - CM6 extensions and helpers.
|
||||
*/
|
||||
|
||||
// Persistent highlight factory
|
||||
export { createPersistentHighlight } from "./persistentHighlight";
|
||||
export type { PersistentHighlightRange, PersistentHighlightInstance } from "./persistentHighlight";
|
||||
export { SelectionHighlight } from "./selectionHighlight";
|
||||
|
||||
// Selection highlight (QuickAsk / CustomCommandModal)
|
||||
export {
|
||||
SelectionHighlight,
|
||||
showSelectionHighlight,
|
||||
updateSelectionHighlight,
|
||||
hideSelectionHighlight,
|
||||
selectionHighlightExtension,
|
||||
} from "./selectionHighlight";
|
||||
export type { SelectionHighlightRange } from "./selectionHighlight";
|
||||
|
||||
// Quick Ask
|
||||
export { QuickAskController } from "./quickAskController";
|
||||
export {
|
||||
quickAskWidgetEffect,
|
||||
quickAskOverlayPlugin,
|
||||
createQuickAskExtension,
|
||||
} from "./quickAskExtension";
|
||||
|
||||
// Chat selection highlight
|
||||
export {
|
||||
ChatSelectionHighlightController,
|
||||
hideChatSelectionHighlight,
|
||||
} from "./chatSelectionHighlightController";
|
||||
export type { ChatSelectionHighlightControllerOptions } from "./chatSelectionHighlightController";
|
||||
|
|
|
|||
|
|
@ -103,10 +103,3 @@ export const quickAskOverlayPlugin = ViewPlugin.fromClass(
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates the Quick Ask CM6 extension array.
|
||||
*/
|
||||
export function createQuickAskExtension() {
|
||||
return [quickAskOverlayPlugin];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
* factory, preserving the original public API for backwards compatibility.
|
||||
*/
|
||||
|
||||
import { StateEffect, type Extension } from "@codemirror/state";
|
||||
import { StateEffect } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { logError } from "@/logger";
|
||||
import {
|
||||
|
|
@ -22,24 +22,7 @@ import {
|
|||
|
||||
const selectionHighlight = createPersistentHighlight("copilot-selection-highlight");
|
||||
|
||||
// ============================================================================
|
||||
// Public Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* A persistent highlight range in CM6 document offsets.
|
||||
* Re-exported for backwards compatibility.
|
||||
*/
|
||||
export type SelectionHighlightRange = PersistentHighlightRange;
|
||||
|
||||
// ============================================================================
|
||||
// Public API (backwards-compatible)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Extension that enables persistent selection highlight support.
|
||||
*/
|
||||
export const selectionHighlightExtension: Extension = selectionHighlight.extension;
|
||||
type SelectionHighlightRange = PersistentHighlightRange;
|
||||
|
||||
/**
|
||||
* Dispatch helper with error isolation (e.g. view destroyed).
|
||||
|
|
@ -52,55 +35,32 @@ function safeDispatch(view: EditorView, spec: Parameters<EditorView["dispatch"]>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build effects for showing/hiding selection highlight without dispatching.
|
||||
* Use this when you need to combine highlight effects with other effects in a single dispatch.
|
||||
* @param view - The EditorView
|
||||
* @param range - The range to highlight, or null to hide
|
||||
* @returns Array of StateEffects to include in a dispatch call
|
||||
*/
|
||||
export function buildSelectionHighlightEffects(
|
||||
function buildSelectionHighlightEffects(
|
||||
view: EditorView,
|
||||
range: { from: number; to: number } | null
|
||||
): StateEffect<unknown>[] {
|
||||
return selectionHighlight.buildEffects(view, range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a persistent highlight in `view` for [from, to].
|
||||
* Automatically installs the extension in this view if missing.
|
||||
*/
|
||||
export function showSelectionHighlight(view: EditorView, from: number, to: number): void {
|
||||
function showSelectionHighlight(view: EditorView, from: number, to: number): void {
|
||||
const effects = selectionHighlight.buildEffects(view, { from, to });
|
||||
if (effects.length > 0) {
|
||||
safeDispatch(view, { effects });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the persistent highlight in `view` to [from, to].
|
||||
* Alias of `showSelectionHighlight` (semantic clarity).
|
||||
*/
|
||||
export function updateSelectionHighlight(view: EditorView, from: number, to: number): void {
|
||||
function updateSelectionHighlight(view: EditorView, from: number, to: number): void {
|
||||
showSelectionHighlight(view, from, to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide (clear) the persistent highlight in `view`.
|
||||
* Does NOT install the extension if missing.
|
||||
*/
|
||||
export function hideSelectionHighlight(view: EditorView): void {
|
||||
function hideSelectionHighlight(view: EditorView): void {
|
||||
const effects = selectionHighlight.buildEffects(view, null);
|
||||
if (effects.length > 0) {
|
||||
safeDispatch(view, { effects });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current persistent highlight range in `view` after mapping through document changes.
|
||||
* Returns null when the extension isn't installed or the highlight is hidden.
|
||||
*/
|
||||
export function getSelectionHighlightRange(view: EditorView): SelectionHighlightRange | null {
|
||||
function getSelectionHighlightRange(view: EditorView): SelectionHighlightRange | null {
|
||||
return selectionHighlight.getRange(view);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,12 +54,6 @@ const WEBCRYPTO_PREFIX = "enc_web_";
|
|||
const ENCRYPTION_PREFIX = "enc_";
|
||||
const DECRYPTION_PREFIX = "dec_";
|
||||
|
||||
/**
|
||||
* @deprecated getDecryptedKey() now returns "" on failure instead of this sentinel.
|
||||
* Kept only for backward compatibility with code that may still reference it.
|
||||
*/
|
||||
export const DECRYPTION_FAILURE_MESSAGE = "Copilot failed to decrypt API keys!";
|
||||
|
||||
/**
|
||||
* Check whether a value looks like a well-formed encrypted Copilot secret
|
||||
* (recognized prefix + plausible base64 payload).
|
||||
|
|
@ -294,19 +288,6 @@ export async function getDecryptedKey(apiKey: string): Promise<string> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an API key and throw when decryption fails.
|
||||
* Use in workflows that must not proceed with undecryptable values
|
||||
* (e.g., configuration export).
|
||||
*/
|
||||
export async function getDecryptedKeyOrThrow(apiKey: string): Promise<string> {
|
||||
const decrypted = await getDecryptedKey(apiKey);
|
||||
if (!decrypted) {
|
||||
throw new Error("Failed to decrypt API key.");
|
||||
}
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
12
src/error.ts
12
src/error.ts
|
|
@ -49,15 +49,3 @@ export class MissingModelKeyError extends Error {
|
|||
Object.setPrototypeOf(this, MissingModelKeyError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when a user explicitly cancels a modal dialog.
|
||||
* Callers can catch this to distinguish cancellation from actual errors.
|
||||
*/
|
||||
export class UserCancelledError extends Error {
|
||||
constructor() {
|
||||
super("User cancelled");
|
||||
this.name = "UserCancelledError";
|
||||
Object.setPrototypeOf(this, UserCancelledError.prototype);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { CopilotPlusExpiredModal } from "@/components/modals/CopilotPlusExpiredM
|
|||
import {
|
||||
ChatModelProviders,
|
||||
ChatModels,
|
||||
DEFAULT_SETTINGS,
|
||||
EmbeddingModelProviders,
|
||||
EmbeddingModels,
|
||||
PlusUtmMedium,
|
||||
|
|
@ -16,16 +15,12 @@ import { Notice } from "obsidian";
|
|||
import React from "react";
|
||||
|
||||
export const DEFAULT_COPILOT_PLUS_CHAT_MODEL = ChatModels.COPILOT_PLUS_FLASH;
|
||||
export const DEFAULT_COPILOT_PLUS_CHAT_MODEL_KEY =
|
||||
const DEFAULT_COPILOT_PLUS_CHAT_MODEL_KEY =
|
||||
DEFAULT_COPILOT_PLUS_CHAT_MODEL + "|" + ChatModelProviders.COPILOT_PLUS;
|
||||
export const DEFAULT_COPILOT_PLUS_EMBEDDING_MODEL = EmbeddingModels.COPILOT_PLUS_SMALL;
|
||||
export const DEFAULT_COPILOT_PLUS_EMBEDDING_MODEL_KEY =
|
||||
DEFAULT_COPILOT_PLUS_EMBEDDING_MODEL + "|" + EmbeddingModelProviders.COPILOT_PLUS;
|
||||
|
||||
// Default models for free users (imported from DEFAULT_SETTINGS)
|
||||
export const DEFAULT_FREE_CHAT_MODEL_KEY = DEFAULT_SETTINGS.defaultModelKey;
|
||||
export const DEFAULT_FREE_EMBEDDING_MODEL_KEY = DEFAULT_SETTINGS.embeddingModelKey;
|
||||
|
||||
// ============================================================================
|
||||
// SELF-HOST MODE VALIDATION
|
||||
// ============================================================================
|
||||
|
|
@ -152,7 +147,7 @@ export async function checkIsPlusUser(
|
|||
}
|
||||
|
||||
/** Check if the user is on a plan that qualifies for self-host mode. */
|
||||
export async function isSelfHostEligiblePlan(): Promise<boolean> {
|
||||
async function isSelfHostEligiblePlan(): Promise<boolean> {
|
||||
if (!getSettings().plusLicenseKey) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
export * from "./type";
|
||||
export * from "./constants";
|
||||
export * from "./projectUtils";
|
||||
export * from "./state";
|
||||
export { ProjectFileManager } from "./ProjectFileManager";
|
||||
export { ProjectRegister } from "./projectRegister";
|
||||
export {
|
||||
ensureProjectsMigratedIfNeeded,
|
||||
migrateProjectsFromSettingsToVault,
|
||||
} from "./projectMigration";
|
||||
|
|
@ -180,52 +180,3 @@ export async function readFrontmatterFieldFromFile(
|
|||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a YAML list field from frontmatter directly from file content.
|
||||
* Used as fallback when metadataCache is not yet populated.
|
||||
* Handles the `key:\n- item1\n- item2` YAML list format.
|
||||
*
|
||||
* @param vault - Vault instance
|
||||
* @param file - Target file
|
||||
* @param key - Frontmatter key to extract
|
||||
* @returns Array of string values
|
||||
*/
|
||||
export async function readFrontmatterListFromFile(
|
||||
vault: Vault,
|
||||
file: TFile,
|
||||
key: string
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const raw = await vault.cachedRead(file);
|
||||
const fmMatch = raw.replace(/^\uFEFF/, "").match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!fmMatch) return [];
|
||||
|
||||
const lines = fmMatch[1].split(/\r?\n/);
|
||||
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const keyRegex = new RegExp(`^${escaped}:\\s*$`);
|
||||
|
||||
let collecting = false;
|
||||
const items: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (keyRegex.test(line)) {
|
||||
collecting = true;
|
||||
continue;
|
||||
}
|
||||
if (collecting) {
|
||||
// Reason: YAML list items start with "- ", stop collecting on any non-list line
|
||||
const listMatch = line.match(/^\s*-\s+(.+)$/);
|
||||
if (listMatch) {
|
||||
const val = listMatch[1].trim();
|
||||
if (val) items.push(val);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,11 +39,9 @@ export {
|
|||
getProjectFolderPath,
|
||||
getProjectConfigFilePath,
|
||||
isProjectConfigFile,
|
||||
getProjectFolderNameFromConfigPath,
|
||||
sanitizeVaultPathSegment,
|
||||
splitUrlsStringToArray,
|
||||
readFrontmatterFieldFromFile,
|
||||
readFrontmatterListFromFile,
|
||||
} from "@/projects/projectPaths";
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,15 +15,6 @@ const projectConfigsAtom = atom<ProjectConfig[]>((get) =>
|
|||
get(projectRecordsAtom).map((r) => r.project)
|
||||
);
|
||||
|
||||
/**
|
||||
* React hook: get all project file records.
|
||||
* This is the single entry point for UI components to read projects.
|
||||
* @returns Array of ProjectFileRecord
|
||||
*/
|
||||
export function useProjectRecords(): ProjectFileRecord[] {
|
||||
return useAtomValue(projectRecordsAtom, { store: projectsStore });
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook: get all ProjectConfig objects (convenience wrapper).
|
||||
* Uses a derived atom so the array reference is stable across re-renders.
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ export function getExtensionPattern(extension: string): string {
|
|||
* Get a list of internal Copilot file paths that must be excluded from searches.
|
||||
* Includes the rolling log file path (e.g., "copilot/copilot-log.md").
|
||||
*/
|
||||
export function getInternalExcludePaths(): string[] {
|
||||
function getInternalExcludePaths(): string[] {
|
||||
return [logFileManager.getLogPath()];
|
||||
}
|
||||
|
||||
|
|
@ -394,7 +394,7 @@ export function getInternalExcludePaths(): string[] {
|
|||
* Get a list of internal Copilot folder prefixes that must be excluded from searches.
|
||||
* Any file whose path starts with one of these prefixes is considered internal.
|
||||
*/
|
||||
export function getInternalExcludeFolderPrefixes(): string[] {
|
||||
function getInternalExcludeFolderPrefixes(): string[] {
|
||||
const settings = getSettings();
|
||||
const projectsFolder = (settings.projectsFolder || "").trim();
|
||||
if (projectsFolder) {
|
||||
|
|
@ -412,7 +412,7 @@ export function getInternalExcludeFolderPrefixes(): string[] {
|
|||
* Checks both exact path matches (log file) and folder prefix matches (projects folder).
|
||||
* @param filePath - Full path to the file in the vault
|
||||
*/
|
||||
export function isInternalExcludedPath(filePath: string): boolean {
|
||||
function isInternalExcludedPath(filePath: string): boolean {
|
||||
const excludes = new Set(getInternalExcludePaths());
|
||||
if (excludes.has(filePath)) return true;
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export interface ChunkOptions {
|
|||
/**
|
||||
* Default chunking options using CHUNK_SIZE constant
|
||||
*/
|
||||
export const DEFAULT_CHUNK_OPTIONS: ChunkOptions = {
|
||||
const DEFAULT_CHUNK_OPTIONS: ChunkOptions = {
|
||||
maxChars: CHUNK_SIZE,
|
||||
overlap: 0,
|
||||
maxBytesTotal: 10 * 1024 * 1024, // 10MB default
|
||||
|
|
@ -643,13 +643,3 @@ export function getSharedChunkManager(app: App): ChunkManager {
|
|||
}
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the shared instance (for testing or plugin reload)
|
||||
*/
|
||||
export function resetSharedChunkManager(): void {
|
||||
if (sharedInstance) {
|
||||
sharedInstance.clearCache();
|
||||
}
|
||||
sharedInstance = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export interface AdaptiveCutoffConfig {
|
|||
/**
|
||||
* Default configuration
|
||||
*/
|
||||
export const DEFAULT_ADAPTIVE_CUTOFF_CONFIG: AdaptiveCutoffConfig = {
|
||||
const DEFAULT_ADAPTIVE_CUTOFF_CONFIG: AdaptiveCutoffConfig = {
|
||||
floor: 3,
|
||||
ceiling: 30,
|
||||
relativeThreshold: 0.3,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export interface FolderBoostConfig {
|
|||
/**
|
||||
* Default configuration for folder boost
|
||||
*/
|
||||
export const DEFAULT_FOLDER_BOOST_CONFIG: FolderBoostConfig = {
|
||||
const DEFAULT_FOLDER_BOOST_CONFIG: FolderBoostConfig = {
|
||||
enabled: true,
|
||||
minDocsForBoost: 2,
|
||||
maxBoostFactor: 1.15, // Max 15% boost, aligned with graph boost
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export interface GraphBoostConfig {
|
|||
/**
|
||||
* Default configuration
|
||||
*/
|
||||
export const DEFAULT_CONFIG: GraphBoostConfig = {
|
||||
const DEFAULT_CONFIG: GraphBoostConfig = {
|
||||
enabled: true,
|
||||
maxCandidates: 10,
|
||||
backlinkWeight: 1.0,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Platform } from "obsidian";
|
|||
/**
|
||||
* Default CLI executable name registered by Obsidian.
|
||||
*/
|
||||
export const DEFAULT_OBSIDIAN_CLI_BINARY = "obsidian";
|
||||
const DEFAULT_OBSIDIAN_CLI_BINARY = "obsidian";
|
||||
|
||||
/**
|
||||
* Known desktop fallback paths for Obsidian CLI on macOS installs.
|
||||
|
|
@ -16,12 +16,12 @@ const OBSIDIAN_CLI_MACOS_FALLBACK_BINARIES = [
|
|||
/**
|
||||
* Default timeout for CLI command execution.
|
||||
*/
|
||||
export const DEFAULT_OBSIDIAN_CLI_TIMEOUT_MS = 15_000;
|
||||
const DEFAULT_OBSIDIAN_CLI_TIMEOUT_MS = 15_000;
|
||||
|
||||
/**
|
||||
* Default maximum process output buffer (1 MB).
|
||||
*/
|
||||
export const DEFAULT_OBSIDIAN_CLI_MAX_BUFFER_BYTES = 1_048_576;
|
||||
const DEFAULT_OBSIDIAN_CLI_MAX_BUFFER_BYTES = 1_048_576;
|
||||
|
||||
/**
|
||||
* Supported primitive parameter value types for CLI serialization.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { App, Notice, PluginSettingTab } from "obsidian";
|
|||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import SettingsMainV2 from "@/settings/v2/SettingsMainV2";
|
||||
import { ContainerContext } from "@/settings/v2/components/ContainerContext";
|
||||
import { AppContext } from "@/context";
|
||||
|
||||
export class CopilotSettingTab extends PluginSettingTab {
|
||||
|
|
@ -71,9 +70,7 @@ export class CopilotSettingTab extends PluginSettingTab {
|
|||
|
||||
sections.render(
|
||||
<AppContext.Provider value={this.app}>
|
||||
<ContainerContext.Provider value={containerEl}>
|
||||
<SettingsMainV2 plugin={this.plugin} />
|
||||
</ContainerContext.Provider>
|
||||
<SettingsMainV2 plugin={this.plugin} />
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -427,7 +427,7 @@ export type ProviderModelAdapters = {
|
|||
* Provider model adapters - converts different provider model data to standard format
|
||||
* These adapters extract model information from API responses and return in a unified format
|
||||
*/
|
||||
export const providerAdapters: ProviderModelAdapters = {
|
||||
const providerAdapters: ProviderModelAdapters = {
|
||||
[ChatModelProviders.OPENAI]: (data): StandardModel[] =>
|
||||
data.data?.map((model) => ({
|
||||
id: model.id,
|
||||
|
|
@ -523,7 +523,7 @@ const toStr = (val: unknown, fallback: string): string =>
|
|||
? String(val)
|
||||
: fallback;
|
||||
|
||||
export const getDefaultModelAdapter = (provider: SettingKeyProviders) => {
|
||||
const getDefaultModelAdapter = (provider: SettingKeyProviders) => {
|
||||
return (data: Record<string, unknown>): StandardModel[] => {
|
||||
// Try to detect common data structure patterns
|
||||
if (data.data && Array.isArray(data.data)) {
|
||||
|
|
@ -565,7 +565,7 @@ export const getDefaultModelAdapter = (provider: SettingKeyProviders) => {
|
|||
* Get adapter function
|
||||
* Uses provider-specific adapter if available, otherwise falls back to default adapter
|
||||
*/
|
||||
export const getModelAdapter = (provider: SettingKeyProviders) => {
|
||||
const getModelAdapter = (provider: SettingKeyProviders) => {
|
||||
return providerAdapters[provider] || getDefaultModelAdapter(provider);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
import { createContext, useContext } from "react";
|
||||
|
||||
export const ContainerContext = createContext<HTMLElement | null>(null);
|
||||
|
||||
export const useContainerContext = () => {
|
||||
return useContext(ContainerContext);
|
||||
};
|
||||
|
|
@ -34,7 +34,7 @@ interface ModelEditModalContentProps {
|
|||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const ModelEditModalContent: React.FC<ModelEditModalContentProps> = ({
|
||||
const ModelEditModalContent: React.FC<ModelEditModalContentProps> = ({
|
||||
model,
|
||||
onUpdate,
|
||||
isEmbeddingModel,
|
||||
|
|
|
|||
|
|
@ -310,67 +310,3 @@ export class VaultDataManager {
|
|||
this.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current notes from the atom (non-reactive)
|
||||
*/
|
||||
export function getNotes(): TFile[] {
|
||||
return settingsStore.get(notesAtom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current folders from the atom (non-reactive)
|
||||
*/
|
||||
export function getFolders(): TFolder[] {
|
||||
return settingsStore.get(foldersAtom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current frontmatter tags from the atom (non-reactive)
|
||||
*/
|
||||
export function getTagsFrontmatter(): string[] {
|
||||
return settingsStore.get(tagsFrontmatterAtom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all current tags from the atom (non-reactive)
|
||||
*/
|
||||
export function getTagsAll(): string[] {
|
||||
return settingsStore.get(tagsAllAtom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to notes changes
|
||||
*/
|
||||
export function subscribeToNotesChange(callback: (notes: TFile[]) => void): () => void {
|
||||
return settingsStore.sub(notesAtom, () => {
|
||||
callback(settingsStore.get(notesAtom));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to folders changes
|
||||
*/
|
||||
export function subscribeToFoldersChange(callback: (folders: TFolder[]) => void): () => void {
|
||||
return settingsStore.sub(foldersAtom, () => {
|
||||
callback(settingsStore.get(foldersAtom));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to frontmatter tags changes
|
||||
*/
|
||||
export function subscribeToTagsFrontmatterChange(callback: (tags: string[]) => void): () => void {
|
||||
return settingsStore.sub(tagsFrontmatterAtom, () => {
|
||||
callback(settingsStore.get(tagsFrontmatterAtom));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to all tags changes
|
||||
*/
|
||||
export function subscribeToTagsAllChange(callback: (tags: string[]) => void): () => void {
|
||||
return settingsStore.sub(tagsAllAtom, () => {
|
||||
callback(settingsStore.get(tagsAllAtom));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { atom, createStore, useAtom } from "jotai";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { UserSystemPrompt } from "@/system-prompts/type";
|
||||
import { getSettings, updateSetting } from "@/settings/model";
|
||||
|
||||
|
|
@ -19,22 +19,6 @@ export function useSystemPrompts(): UserSystemPrompt[] {
|
|||
return useAtomValue(systemPromptsAtom, { store: systemPromptsStore });
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook to get the selected prompt title
|
||||
* @returns Currently selected prompt title
|
||||
*/
|
||||
export function useSelectedPromptTitle(): string {
|
||||
return useAtomValue(selectedPromptTitleAtom, { store: systemPromptsStore });
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook to set the selected prompt title
|
||||
* @returns Setter function for selected prompt title
|
||||
*/
|
||||
export function useSetSelectedPromptTitle() {
|
||||
return useSetAtom(selectedPromptTitleAtom, { store: systemPromptsStore });
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook to get and set the selected prompt title (useState-like)
|
||||
* Use this when you need both read and write in the same component
|
||||
|
|
@ -121,14 +105,6 @@ export function getDisableBuiltinSystemPrompt(): boolean {
|
|||
return systemPromptsStore.get(disableBuiltinSystemPromptAtom);
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook to use disable builtin system prompt state
|
||||
* @returns Tuple of [disabled, setDisabled]
|
||||
*/
|
||||
export function useDisableBuiltinSystemPrompt() {
|
||||
return useAtom(disableBuiltinSystemPromptAtom, { store: systemPromptsStore });
|
||||
}
|
||||
|
||||
/**
|
||||
* Pending file writes to prevent infinite loops when modifying files
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export function getPromptFilePath(title: string): string {
|
|||
* @param title - The title of the prompt
|
||||
* @param folder - Optional folder path (defaults to current settings folder)
|
||||
*/
|
||||
export function getPromptFilePathInFolder(title: string, folder?: string): string {
|
||||
function getPromptFilePathInFolder(title: string, folder?: string): string {
|
||||
const folderPath = folder ? normalizePath(folder) : getSystemPromptsFolder();
|
||||
return normalizePath(`${folderPath}/${title}.md`);
|
||||
}
|
||||
|
|
@ -203,31 +203,6 @@ export async function ensurePromptFrontmatter(file: TFile, prompt: UserSystemPro
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the last used timestamp for a system prompt
|
||||
*/
|
||||
export async function updatePromptLastUsed(title: string): Promise<void> {
|
||||
const filePath = getPromptFilePath(title);
|
||||
const file = app.vault.getAbstractFileByPath(filePath);
|
||||
if (!(file instanceof TFile)) return;
|
||||
|
||||
// Check if already pending to avoid nested add/remove issues
|
||||
const alreadyPending = isPendingFileWrite(file.path);
|
||||
|
||||
try {
|
||||
if (!alreadyPending) {
|
||||
addPendingFileWrite(file.path);
|
||||
}
|
||||
await app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||
frontmatter[COPILOT_SYSTEM_PROMPT_LAST_USED] = Date.now();
|
||||
});
|
||||
} finally {
|
||||
if (!alreadyPending) {
|
||||
removePendingFileWrite(file.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique name for a copied prompt by adding "(copy)" or "(copy N)" suffix
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class SelfHostPdfParser {
|
|||
}
|
||||
}
|
||||
|
||||
export class MarkdownParser implements FileParser {
|
||||
class MarkdownParser implements FileParser {
|
||||
supportedExtensions = ["md", "base"];
|
||||
|
||||
async parseFile(file: TFile, vault: Vault): Promise<string> {
|
||||
|
|
@ -84,7 +84,7 @@ export class MarkdownParser implements FileParser {
|
|||
}
|
||||
}
|
||||
|
||||
export class PDFParser implements FileParser {
|
||||
class PDFParser implements FileParser {
|
||||
supportedExtensions = ["pdf"];
|
||||
private brevilabsClient: BrevilabsClient;
|
||||
private pdfCache: PDFCache;
|
||||
|
|
@ -147,7 +147,7 @@ export class PDFParser implements FileParser {
|
|||
}
|
||||
}
|
||||
|
||||
export class CanvasParser implements FileParser {
|
||||
class CanvasParser implements FileParser {
|
||||
supportedExtensions = ["canvas"];
|
||||
|
||||
async parseFile(file: TFile, vault: Vault): Promise<string> {
|
||||
|
|
@ -173,7 +173,7 @@ export class CanvasParser implements FileParser {
|
|||
* Reason: The static method needs these extensions at UI layer (before any real API client exists),
|
||||
* so we can't safely construct Docs4LLMParser just to read a property.
|
||||
*/
|
||||
export const DOCS4LLM_SUPPORTED_EXTENSIONS: readonly string[] = [
|
||||
const DOCS4LLM_SUPPORTED_EXTENSIONS: readonly string[] = [
|
||||
// Base types
|
||||
"pdf",
|
||||
|
||||
|
|
|
|||
|
|
@ -467,4 +467,4 @@ const readNoteTool = createLangChainTool({
|
|||
},
|
||||
});
|
||||
|
||||
export { readNoteSchema, readNoteTool };
|
||||
export { readNoteTool };
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ type MetadataCacheWithTags = MetadataCache & {
|
|||
getFrontmatterTags?: () => Record<string, number> | null;
|
||||
};
|
||||
|
||||
export const TagListToolSchema = z
|
||||
const TagListToolSchema = z
|
||||
.object({
|
||||
includeInline: z
|
||||
.boolean()
|
||||
|
|
@ -265,4 +265,4 @@ export const createGetTagListTool = () =>
|
|||
},
|
||||
});
|
||||
|
||||
export { collectTagEntries, enforceSizeLimit };
|
||||
export { enforceSizeLimit };
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { z } from "zod";
|
|||
/**
|
||||
* Options for creating a LangChain tool
|
||||
*/
|
||||
export interface CreateToolOptions<TSchema extends z.ZodType> {
|
||||
interface CreateToolOptions<TSchema extends z.ZodType> {
|
||||
name: string;
|
||||
description: string;
|
||||
schema: TSchema;
|
||||
|
|
@ -51,58 +51,3 @@ export function createLangChainTool<TSchema extends z.ZodType>(
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Common schema types for reuse across tools
|
||||
*/
|
||||
export const CommonSchemas = {
|
||||
emptyParams: z.object({}),
|
||||
stringParam: (description: string) => z.string().describe(description),
|
||||
numberParam: (description: string) => z.number().describe(description),
|
||||
booleanParam: (description: string) => z.boolean().describe(description),
|
||||
optionalString: (description: string) => z.string().optional().describe(description),
|
||||
nonEmptyString: (description: string) => z.string().min(1).describe(description),
|
||||
url: (description: string) => z.string().url().describe(description),
|
||||
email: (description: string) => z.string().email().describe(description),
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Extract description from a Zod schema field, handling wrapped types.
|
||||
*/
|
||||
function getZodDescription(schema: z.ZodType): string {
|
||||
// Handle optional/nullable/default wrappers
|
||||
if (
|
||||
schema instanceof z.ZodOptional ||
|
||||
schema instanceof z.ZodNullable ||
|
||||
schema instanceof z.ZodDefault
|
||||
) {
|
||||
return getZodDescription(schema._def.innerType as z.ZodType);
|
||||
}
|
||||
|
||||
// @ts-ignore - accessing private _def property
|
||||
return schema._def.description || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract parameter descriptions from Zod schema for tool description generation.
|
||||
* Used by chain runners to generate XML tool format prompts.
|
||||
*/
|
||||
export function extractParametersFromZod(schema: z.ZodType): Record<string, string> {
|
||||
const descriptions: Record<string, string> = {};
|
||||
|
||||
if (schema instanceof z.ZodObject) {
|
||||
const shape = schema.shape as Record<string, z.ZodType>;
|
||||
for (const [key, value] of Object.entries(shape)) {
|
||||
const zodField = value;
|
||||
descriptions[key] = getZodDescription(zodField) || "No description";
|
||||
}
|
||||
} else if (schema instanceof z.ZodVoid) {
|
||||
// No parameters for void schema
|
||||
return {};
|
||||
} else if (schema instanceof z.ZodUnion || schema instanceof z.ZodDiscriminatedUnion) {
|
||||
// For unions, we could extract common fields or return a special description
|
||||
descriptions._union = "Multiple parameter formats supported";
|
||||
}
|
||||
|
||||
return descriptions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,15 +44,6 @@ export interface WebSelectedTextContext extends BaseSelectedTextContext {
|
|||
*/
|
||||
export type SelectedTextContext = NoteSelectedTextContext | WebSelectedTextContext;
|
||||
|
||||
/**
|
||||
* Type guard for note selected text context
|
||||
*/
|
||||
export function isNoteSelectedTextContext(
|
||||
ctx: SelectedTextContext
|
||||
): ctx is NoteSelectedTextContext {
|
||||
return ctx.sourceType === "note";
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for web selected text context
|
||||
*/
|
||||
|
|
|
|||
156
src/utils.ts
156
src/utils.ts
|
|
@ -15,11 +15,9 @@ import {
|
|||
ProviderMetadata,
|
||||
SettingKeyProviders,
|
||||
TEXT_READABLE_EXTENSIONS,
|
||||
USER_SENDER,
|
||||
} from "@/constants";
|
||||
import { logInfo, logWarn } from "@/logger";
|
||||
import { CopilotSettings } from "@/settings/model";
|
||||
import { ChatMessage } from "@/types/message";
|
||||
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
||||
import { MemoryVariables } from "@langchain/core/memory";
|
||||
import { RunnableSequence } from "@langchain/core/runnables";
|
||||
|
|
@ -57,7 +55,7 @@ interface APIError extends Error {
|
|||
}
|
||||
|
||||
// Error message constants
|
||||
export const ERROR_MESSAGES = {
|
||||
const ERROR_MESSAGES = {
|
||||
INVALID_LICENSE_KEY_USER:
|
||||
"Invalid Copilot Plus license key. Please check your license key in settings.",
|
||||
UNKNOWN_ERROR: "An unknown error occurred",
|
||||
|
|
@ -65,13 +63,13 @@ export const ERROR_MESSAGES = {
|
|||
} as const;
|
||||
|
||||
// Error handling utilities
|
||||
export interface ErrorDetail {
|
||||
interface ErrorDetail {
|
||||
status?: number;
|
||||
message?: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export function extractErrorDetail(error: unknown): ErrorDetail {
|
||||
function extractErrorDetail(error: unknown): ErrorDetail {
|
||||
const err = error as Record<string, unknown> | null | undefined;
|
||||
const errorDetail: ErrorDetail = (err?.detail as ErrorDetail) || {};
|
||||
return {
|
||||
|
|
@ -81,7 +79,7 @@ export function extractErrorDetail(error: unknown): ErrorDetail {
|
|||
};
|
||||
}
|
||||
|
||||
export function isLicenseKeyError(error: unknown): boolean {
|
||||
function isLicenseKeyError(error: unknown): boolean {
|
||||
const errorDetail = extractErrorDetail(error);
|
||||
const err = error as Record<string, unknown> | null | undefined;
|
||||
const message = err?.message as string | undefined;
|
||||
|
|
@ -104,10 +102,6 @@ export function getApiErrorMessage(error: unknown): string {
|
|||
);
|
||||
}
|
||||
|
||||
export const getModelNameFromKey = (modelKey: string): string => {
|
||||
return modelKey.split("|")[0];
|
||||
};
|
||||
|
||||
export const isFolderMatch = (fileFullpath: string, inputPath: string): boolean => {
|
||||
const fileSegments = fileFullpath.split("/").map((segment) => segment.toLowerCase());
|
||||
return fileSegments.includes(inputPath.toLowerCase());
|
||||
|
|
@ -160,7 +154,7 @@ export function stripHash(tag: string): string {
|
|||
/**
|
||||
* Options for {@link stripFrontmatter}.
|
||||
*/
|
||||
export interface StripFrontmatterOptions {
|
||||
interface StripFrontmatterOptions {
|
||||
/**
|
||||
* When true (default), trims leading whitespace after the frontmatter block.
|
||||
* When false, preserves leading whitespace from the body, but still removes
|
||||
|
|
@ -260,30 +254,15 @@ export function getNotesFromTags(vault: Vault, tags: string[], noteFiles?: TFile
|
|||
return filesWithTag;
|
||||
}
|
||||
|
||||
// TODO: Chain type conversion still needed for chain runner selection
|
||||
// This function is still used but the underlying chain infrastructure is deprecated
|
||||
export const stringToChainType = (chain: string): ChainType => {
|
||||
switch (chain) {
|
||||
case "llm_chain":
|
||||
return ChainType.LLM_CHAIN;
|
||||
case "vault_qa":
|
||||
return ChainType.VAULT_QA_CHAIN;
|
||||
case "copilot_plus":
|
||||
return ChainType.COPILOT_PLUS_CHAIN;
|
||||
default:
|
||||
throw new Error(`Unknown chain type: ${chain}`);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: These chain validation functions are deprecated
|
||||
// Remove after confirming chainManager no longer uses them
|
||||
export const isLLMChain = (chain: RunnableSequence): chain is RunnableSequence => {
|
||||
const isLLMChain = (chain: RunnableSequence): chain is RunnableSequence => {
|
||||
const c = chain as unknown as Record<string, unknown>;
|
||||
const last = c.last as Record<string, unknown> | undefined;
|
||||
return Boolean(last?.modelName || last?.model);
|
||||
};
|
||||
|
||||
export const isRetrievalQAChain = (chain: BaseChain): chain is RetrievalQAChain => {
|
||||
const isRetrievalQAChain = (chain: BaseChain): chain is RetrievalQAChain => {
|
||||
const c = chain as unknown as Record<string, unknown>;
|
||||
const last = c.last as Record<string, unknown> | undefined;
|
||||
return last?.retriever !== undefined;
|
||||
|
|
@ -293,26 +272,6 @@ export const isSupportedChain = (chain: RunnableSequence): chain is RunnableSequ
|
|||
return isLLMChain(chain) || isRetrievalQAChain(chain);
|
||||
};
|
||||
|
||||
// Returns the last N messages from the chat history,
|
||||
// last one being the newest ai message
|
||||
export const getChatContext = (chatHistory: ChatMessage[], contextSize: number) => {
|
||||
if (chatHistory.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const lastAiMessageIndex = chatHistory
|
||||
.slice()
|
||||
.reverse()
|
||||
.findIndex((msg) => msg.sender !== USER_SENDER);
|
||||
if (lastAiMessageIndex === -1) {
|
||||
// No ai messages found, return an empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
const lastIndex = chatHistory.length - 1 - lastAiMessageIndex;
|
||||
const startIndex = Math.max(0, lastIndex - contextSize + 1);
|
||||
return chatHistory.slice(startIndex, lastIndex + 1);
|
||||
};
|
||||
|
||||
export interface FormattedDateTime {
|
||||
fileName: string;
|
||||
display: string;
|
||||
|
|
@ -379,9 +338,6 @@ export function stringToFormattedDateTime(timestamp: string): FormattedDateTime
|
|||
};
|
||||
}
|
||||
|
||||
// Re-export for backward compatibility (constants now live in @/constants)
|
||||
export { ALLOWED_NOTE_CONTEXT_EXTENSIONS, TEXT_READABLE_EXTENSIONS };
|
||||
|
||||
/**
|
||||
* Check if a file has a text-readable extension (md, canvas, base).
|
||||
*/
|
||||
|
|
@ -439,23 +395,6 @@ export function isAllowedFileForChainContext(file: TFile | null, chainType: Chai
|
|||
return isPlusChain(chainType);
|
||||
}
|
||||
|
||||
export async function getAllNotesContent(vault: Vault): Promise<string> {
|
||||
const vaultNotes: string[] = [];
|
||||
|
||||
const markdownFiles = vault.getMarkdownFiles();
|
||||
|
||||
for (const file of markdownFiles) {
|
||||
const fileContent = await vault.cachedRead(file);
|
||||
// Import is not available at the top level due to circular dependency
|
||||
const { VAULT_NOTE_TAG } = await import("@/constants");
|
||||
vaultNotes.push(
|
||||
`<${VAULT_NOTE_TAG}>\n<path>${file.path}</path>\n<content>\n${fileContent}\n</content>\n</${VAULT_NOTE_TAG}>`
|
||||
);
|
||||
}
|
||||
|
||||
return vaultNotes.join("\n\n");
|
||||
}
|
||||
|
||||
export function areEmbeddingModelsSame(
|
||||
model1: string | undefined,
|
||||
model2: string | undefined
|
||||
|
|
@ -474,54 +413,6 @@ export function areEmbeddingModelsSame(
|
|||
return model1 === model2;
|
||||
}
|
||||
|
||||
// Basic prompts
|
||||
export function sendNotesContentPrompt(notes: { name: string; content: string }[]): string {
|
||||
const formattedNotes = notes.map((note) => `## ${note.name}\n\n${note.content}`).join("\n\n");
|
||||
|
||||
return (
|
||||
`Please read the notes below and be ready to answer questions about them. ` +
|
||||
`If there's no information about a certain topic, just say the note ` +
|
||||
`does not mention it. ` +
|
||||
`The content of the notes is between "/***/":\n\n/***/\n\n${formattedNotes}\n\n/***/\n\n` +
|
||||
`Please reply with the following word for word:` +
|
||||
`"OK I've read these notes. ` +
|
||||
`Feel free to ask related questions, such as 'give me a summary of these notes in bullet points', 'what key questions do these notes answer', etc. "\n`
|
||||
);
|
||||
}
|
||||
|
||||
function getNoteTitleAndTags(noteWithTag: {
|
||||
name: string;
|
||||
content: string;
|
||||
tags?: string[];
|
||||
}): string {
|
||||
return (
|
||||
`[[${noteWithTag.name}]]` +
|
||||
(noteWithTag.tags && noteWithTag.tags.length > 0 ? `\ntags: ${noteWithTag.tags.join(",")}` : "")
|
||||
);
|
||||
}
|
||||
|
||||
function getChatContextStr(chatNoteContextPath: string, chatNoteContextTags: string[]): string {
|
||||
const pathStr = chatNoteContextPath ? `\nChat context by path: ${chatNoteContextPath}` : "";
|
||||
const tagsStr =
|
||||
chatNoteContextTags?.length > 0
|
||||
? `\nChat context by tags: ${chatNoteContextTags.join(",")}`
|
||||
: "";
|
||||
return pathStr + tagsStr;
|
||||
}
|
||||
|
||||
export function getSendChatContextNotesPrompt(
|
||||
notes: { name: string; content: string }[],
|
||||
chatNoteContextPath: string,
|
||||
chatNoteContextTags: string[]
|
||||
): string {
|
||||
const noteTitles = notes.map((note) => getNoteTitleAndTags(note)).join("\n\n");
|
||||
return (
|
||||
`Please read the notes below and be ready to answer questions about them. ` +
|
||||
getChatContextStr(chatNoteContextPath, chatNoteContextTags) +
|
||||
`\n\n${noteTitles}`
|
||||
);
|
||||
}
|
||||
|
||||
export interface ChatHistoryEntry {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
|
|
@ -642,16 +533,11 @@ export function extractTemplateNoteFiles(query: string, vault: Vault): TFile[] {
|
|||
}
|
||||
|
||||
// Helper function to check if a note title is unique in the vault
|
||||
export function isNoteTitleUnique(title: string, vault: Vault): boolean {
|
||||
function isNoteTitleUnique(title: string, vault: Vault): boolean {
|
||||
const files = vault.getMarkdownFiles();
|
||||
return files.filter((f) => f.basename === title).length === 1;
|
||||
}
|
||||
|
||||
// Helper function to determine if we should show the full path for a file
|
||||
export function shouldShowPath(file: TFile): boolean {
|
||||
return (file as unknown as Record<string, unknown>).needsPathDisplay === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the variable name to generate a note path if it's enclosed in double brackets,
|
||||
* otherwise return the variable name as is.
|
||||
|
|
@ -681,12 +567,6 @@ export function extractUniqueTitlesFromDocs(docs: Document[]): string[] {
|
|||
return Array.from(titlesSet);
|
||||
}
|
||||
|
||||
export function extractJsonFromCodeBlock(content: string): unknown {
|
||||
const codeBlockMatch = content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
||||
const jsonContent = codeBlockMatch ? codeBlockMatch[1].trim() : content.trim();
|
||||
return JSON.parse(jsonContent);
|
||||
}
|
||||
|
||||
const YOUTUBE_URL_REGEX =
|
||||
/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)([^\s&]+)/;
|
||||
|
||||
|
|
@ -778,14 +658,6 @@ export function isTwitterUrl(url: string): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract first YouTube URL from text (legacy function for backward compatibility)
|
||||
*/
|
||||
export function extractYoutubeUrl(text: string): string | null {
|
||||
const match = text.match(YOUTUBE_URL_REGEX);
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all YouTube URLs from text (legacy function for backward compatibility)
|
||||
*/
|
||||
|
|
@ -975,14 +847,6 @@ export function getProviderLabel(provider: string, model?: CustomModel): string
|
|||
return baseLabel + (model?.believerExclusive && baseLabel === "Copilot Plus" ? "(Believer)" : "");
|
||||
}
|
||||
|
||||
export function getProviderHost(provider: string): string {
|
||||
return ProviderInfo[provider as Provider]?.host || "";
|
||||
}
|
||||
|
||||
export function getProviderKeyManagementURL(provider: string): string {
|
||||
return ProviderInfo[provider as Provider]?.keyManagementURL || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans a message by removing Think blocks, Action blocks (writeFile), tool call markers,
|
||||
* and agent reasoning blocks for copying to clipboard or inserting at cursor.
|
||||
|
|
@ -1167,7 +1031,7 @@ export function isOSeriesModel(model: BaseChatModel | string): boolean {
|
|||
return modelName.startsWith("o1") || modelName.startsWith("o3") || modelName.startsWith("o4");
|
||||
}
|
||||
|
||||
export function isGPT5Model(model: BaseChatModel | string): boolean {
|
||||
function isGPT5Model(model: BaseChatModel | string): boolean {
|
||||
if (typeof model === "string") {
|
||||
return model.startsWith("gpt-5");
|
||||
}
|
||||
|
|
@ -1184,7 +1048,7 @@ export function isGPT5Model(model: BaseChatModel | string): boolean {
|
|||
* @param model - Model instance or model name string.
|
||||
* @returns True when the model name indicates a Codex model.
|
||||
*/
|
||||
export function isCodexModel(model: BaseChatModel | string): boolean {
|
||||
function isCodexModel(model: BaseChatModel | string): boolean {
|
||||
const m = model as unknown as Record<string, unknown>;
|
||||
const modelName: string =
|
||||
typeof model === "string" ? model : (m.modelName as string) || (m.model as string) || "";
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { App, TFile } from "obsidian";
|
|||
* @param basename - File basename to check
|
||||
* @returns true if the basename starts with a known project prefix
|
||||
*/
|
||||
export function hasKnownProjectPrefix(basename: string): boolean {
|
||||
function hasKnownProjectPrefix(basename: string): boolean {
|
||||
const records = getCachedProjectRecords();
|
||||
|
||||
// Reason: check precise prefix matching against known cached project IDs first.
|
||||
|
|
@ -39,7 +39,7 @@ function coerceProjectId(projectId: unknown): string | undefined {
|
|||
* Read the projectId from a chat file's frontmatter.
|
||||
* Tries metadataCache first, falls back to adapter read for hidden-directory files.
|
||||
*/
|
||||
export async function readChatFileProjectId(app: App, file: TFile): Promise<string | undefined> {
|
||||
async function readChatFileProjectId(app: App, file: TFile): Promise<string | undefined> {
|
||||
const fm = app.metadataCache.getFileCache(file)?.frontmatter;
|
||||
let projectId: unknown = fm?.projectId;
|
||||
|
||||
|
|
@ -209,15 +209,6 @@ export function extractChatLastAccessedAtMs(file: TFile): number | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract chat last accessed date from a file.
|
||||
* Uses extractChatLastAccessedAtMs and returns a Date when available, null otherwise.
|
||||
*/
|
||||
export function extractChatLastAccessedAt(file: TFile): Date | null {
|
||||
const lastAccessedAtMs = extractChatLastAccessedAtMs(file);
|
||||
return lastAccessedAtMs ? new Date(lastAccessedAtMs) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted display text for a chat file (title + formatted date).
|
||||
* Used in chat history modals and similar UI components.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
* Reason: LLMs typically output LaTeX using `\[` / `\(` delimiters,
|
||||
* but Obsidian's MarkdownRenderer only recognizes `$` / `$$` delimiters.
|
||||
*/
|
||||
export function normalizeLatexDelimiters(content: string): string {
|
||||
function normalizeLatexDelimiters(content: string): string {
|
||||
// Reason: Split on fenced code blocks and inline code so we never
|
||||
// rewrite LaTeX-like delimiters inside code spans/blocks.
|
||||
const parts = content.split(/(```[\s\S]*?```|`[^`]*`)/g);
|
||||
|
|
@ -35,7 +35,7 @@ export function normalizeLatexDelimiters(content: string): string {
|
|||
* Converts ```dataview to ```text and ```dataviewjs to ```javascript
|
||||
* so they display as static code examples instead of executing queries.
|
||||
*/
|
||||
export function escapeDataviewCodeBlocks(text: string): string {
|
||||
function escapeDataviewCodeBlocks(text: string): string {
|
||||
text = text.replace(/```dataview(\s*(?:\n|$))/g, "```text$1");
|
||||
text = text.replace(/```dataviewjs(\s*(?:\n|$))/g, "```javascript$1");
|
||||
return text;
|
||||
|
|
@ -46,7 +46,7 @@ export function escapeDataviewCodeBlocks(text: string): string {
|
|||
* Converts ```tasks to ```text so they display as static code examples
|
||||
* instead of executing task queries.
|
||||
*/
|
||||
export function escapeTasksCodeBlocks(text: string): string {
|
||||
function escapeTasksCodeBlocks(text: string): string {
|
||||
return text.replace(/```tasks(\s*(?:\n|$))/g, "```text$1");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,24 +7,6 @@ import {
|
|||
import { getSettings } from "@/settings/model";
|
||||
import { CustomModel } from "@/aiParams";
|
||||
|
||||
/**
|
||||
* Check if a provider requires an API key.
|
||||
* Local providers (OLLAMA, LM_STUDIO, OPENAI_FORMAT) don't require API keys.
|
||||
*
|
||||
* @param provider - The provider to check
|
||||
* @returns true if the provider requires an API key, false for local providers
|
||||
*
|
||||
* @example
|
||||
* if (providerRequiresApiKey(model.provider)) {
|
||||
* // This is a cloud provider, check for API key
|
||||
* } else {
|
||||
* // This is a local provider, no API key needed
|
||||
* }
|
||||
*/
|
||||
export function providerRequiresApiKey(provider: string): provider is SettingKeyProviders {
|
||||
return provider in ProviderSettingsKeyMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key for a provider, with model-specific key taking precedence over global settings.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ function normalizeTimestampMs(value: unknown): number | null {
|
|||
* - `name`: ascending by `name` (using default locale comparison via Intl.Collator)
|
||||
* - `manual`: ascending by `manualOrder` (falls back to `name` when unavailable)
|
||||
*/
|
||||
export function createSortComparator<T>(
|
||||
function createSortComparator<T>(
|
||||
strategy: SortStrategy,
|
||||
getters: RecentUsageSortGetters<T>
|
||||
): (a: T, b: T) => number {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Utility functions for handling tool results
|
||||
*/
|
||||
|
||||
export const DEFAULT_TOOL_RESULT_MAX_LENGTH = 10000; // Default max length for tool results in chat memory
|
||||
const DEFAULT_TOOL_RESULT_MAX_LENGTH = 10000; // Default max length for tool results in chat memory
|
||||
|
||||
/**
|
||||
* Get the configured max length for tool results
|
||||
|
|
@ -73,7 +73,7 @@ export function processToolResults(
|
|||
* @param errorMessage - The error message to wrap
|
||||
* @returns Formatted error string with errorChunk tags
|
||||
*/
|
||||
export function wrapErrorChunk(errorMessage: string): string {
|
||||
function wrapErrorChunk(errorMessage: string): string {
|
||||
return `<errorChunk>${errorMessage}</errorChunk>`;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue