logancyang_obsidian-copilot/src/context/ChatInputContext.tsx
Zero Liu f3e5840af5
feat(relevant-notes): dedicated pane with redesigned populated view (#2559)
* feat(relevant-notes): dedicated pane with redesigned populated view

Move Relevant Notes out of the chat into its own command-opened pane
("Open Relevant Notes") and redesign the populated view: a toolbar with
the active-note context line + Build index, color-graded relevance
meters/percentages, a "Best" tag, hover quick actions, a live indexing
overlay, and a restyled hover preview card. Add-to-Chat routes a wikilink
into the open chat/agent view via a new INSERT_TEXT_TO_CHAT event.

Rank relevant notes by semantic similarity only so the displayed
percentages stay monotonic — links no longer boost the score (link-only
notes still appear, without a meter). Remove the old in-chat Relevant
Notes block and its showRelevantNotes setting; update docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refine(relevant-notes): linear meter, link badges, excluded state, preview fixes

- Meter width now maps 1:1 to the similarity score (70% → 70%).
- Add an excluded-file empty state when the active note is outside the
  index inclusion/exclusion settings (takes priority over build/list).
- Replace the "Best" badge with Outgoing-link / Backlink badges; the
  badge+percentage cluster swaps for the action buttons on hover.
- Move row action buttons into the flex flow so the title never overlaps
  them and truncates with an ellipsis in both states.
- Render preview snippet line breaks (whitespace-pre-line).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(relevant-notes): route Add to Chat to the last-focused chat view

The Relevant Notes "Add to Chat" action hardcoded agent-first when picking
the target chat (getLeavesOfType(CHAT_AGENT_VIEWTYPE)[0] ?? legacy), so with
both chats open the wikilink landed in the wrong one. Reuse the existing
lastActiveChatViewType tracking (added for the add-to-context commands in
#2555) via a shared pickContextChatViewType() helper, so the chat we reveal
and the chat we type into always agree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refine(relevant-notes): show full note content in a scrollable preview

The hover-card preview truncated content two ways: the loaded text was
sliced to 1000 chars and the paragraph was clamped to 4 lines. Drop the
slice and replace line-clamp with a bounded, scrollable area (max-h-64 +
overflow-y-auto) so the full note is reachable in a slightly larger box.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refine(relevant-notes): replace mount-timing setTimeouts with reliable signals

Address PR review on the Relevant Notes work:

- insertTextIntoActiveChat: drop the 50ms guess. The chat views' event bus
  (ChatViewEventTarget) now latches queued "insert text"; the view drains it
  on mount and ChatInputContext buffers/flushes it when the Lexical editor
  registers, so delivery no longer depends on mount timing.
- RelevantNotesView: drop the 50ms initial dispatch. useActiveFile now seeds
  from the current active file on mount, so the pane populates immediately and
  still updates via the active-leaf-change listener.
- RelevanceMeter: route the score-driven width/color through CSS variables
  (.copilot-relevance-meter-fill) instead of an inline style.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 06:54:12 +08:00

98 lines
3 KiB
TypeScript

import React, {
createContext,
useContext,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { INSERT_TEXT_WITH_PILLS_COMMAND } from "@/components/chat-components/utils/lexicalTextUtils";
import { LexicalEditor } from "lexical";
interface ChatInputContextType {
insertTextWithPills: (text: string, enableURLPills?: boolean) => void;
focusInput: () => void;
registerEditor: (editor: LexicalEditor) => void;
registerFocusHandler: (handler: () => void) => void;
}
const ChatInputContext = createContext<ChatInputContextType | undefined>(undefined);
/**
* Hook to access chat input functionality
*/
export function useChatInput(): ChatInputContextType {
const context = useContext(ChatInputContext);
if (context === undefined) {
throw new Error("useChatInput must be used within a ChatInputProvider");
}
return context;
}
interface ChatInputProviderProps {
children: React.ReactNode;
}
/**
* Provider component that manages chat input functionality without requiring refs
*/
export function ChatInputProvider({ children }: ChatInputProviderProps): JSX.Element {
const [editor, setEditor] = useState<LexicalEditor | null>(null);
const [focusHandler, setFocusHandler] = useState<(() => void) | null>(null);
// Text requested before the Lexical editor has mounted (e.g. routed in while
// the chat view is still opening). Held here and flushed once the editor
// registers, so insertion never depends on mount timing.
const pendingInsertRef = useRef<{ text: string; enableURLPills: boolean } | null>(null);
const registerEditor = useCallback((editorInstance: LexicalEditor) => {
setEditor(editorInstance);
}, []);
const registerFocusHandler = useCallback((handler: () => void) => {
setFocusHandler(() => handler);
}, []);
const insertTextWithPills = useCallback(
(text: string, enableURLPills = false) => {
if (editor) {
editor.dispatchCommand(INSERT_TEXT_WITH_PILLS_COMMAND, {
text,
options: { enableURLPills, insertAtSelection: true },
});
} else {
pendingInsertRef.current = { text, enableURLPills };
}
},
[editor]
);
// Flush any text buffered before the editor was ready.
useEffect(() => {
if (!editor || !pendingInsertRef.current) return;
const { text, enableURLPills } = pendingInsertRef.current;
pendingInsertRef.current = null;
editor.dispatchCommand(INSERT_TEXT_WITH_PILLS_COMMAND, {
text,
options: { enableURLPills, insertAtSelection: true },
});
}, [editor]);
const focusInput = useCallback(() => {
if (focusHandler) {
focusHandler();
}
}, [focusHandler]);
const contextValue = useMemo<ChatInputContextType>(
() => ({
insertTextWithPills,
focusInput,
registerEditor,
registerFocusHandler,
}),
[insertTextWithPills, focusInput, registerEditor, registerFocusHandler]
);
return <ChatInputContext.Provider value={contextValue}>{children}</ChatInputContext.Provider>;
}