Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

236 lines
7.9 KiB
TypeScript

import { useImperativeHandle, type ForwardedRef, type RefObject } from "react";
import type { Editor } from "@tiptap/core";
import type { AgentGUIProviderSkillOption } from "../model/agentGuiNodeTypes";
import type { AgentCapabilityTokenOption } from "./agentCapabilityTokenExtension";
import type { AgentRichTextEditorHandle } from "./AgentRichTextEditor.types";
import {
editorToPromptText,
plainTextToAgentRichTextInlineContent
} from "./agentRichTextDocument";
import {
createAgentFileMentionContent,
createAgentMentionContent
} from "./agentWorkspaceFileReferences";
import {
isMentionTriggerBoundaryBeforeSelection,
isPromptVisualLineStart
} from "./agentRichTextEditorSupport";
import { AGENT_RICH_TEXT_SKIP_USER_CONTENT_EVENT_META } from "./agentRichTextEngagement";
export function useAgentRichTextEditorHandle(input: {
availableCapabilitiesRef: RefObject<readonly AgentCapabilityTokenOption[]>;
availableSkillsRef: RefObject<readonly AgentGUIProviderSkillOption[]>;
editorRef: RefObject<Editor | null>;
onBeforeProgrammaticFocus?: () => void;
ref: ForwardedRef<AgentRichTextEditorHandle>;
}): void {
useImperativeHandle(
input.ref,
() => ({
focusAtStart() {
const currentEditor = input.editorRef.current;
if (!currentEditor || currentEditor.isDestroyed) {
return;
}
if (!currentEditor.isFocused) input.onBeforeProgrammaticFocus?.();
currentEditor
.chain()
.focus()
.setTextSelection(1)
.scrollIntoView()
.run();
},
focusAtEnd() {
const currentEditor = input.editorRef.current;
if (!currentEditor || currentEditor.isDestroyed) {
return;
}
const end = currentEditor.state.doc.content.size;
if (!currentEditor.isFocused) input.onBeforeProgrammaticFocus?.();
currentEditor.chain().focus().setTextSelection(end).run();
},
getPromptTextBeforeSelection() {
const currentEditor = input.editorRef.current;
if (!currentEditor || currentEditor.isDestroyed) {
return "";
}
return currentEditor.state.doc.textBetween(
0,
currentEditor.state.selection.from,
"\n",
"\n"
);
},
insertPlainTextAtSelection(text) {
const currentEditor = input.editorRef.current;
if (
!currentEditor ||
currentEditor.isDestroyed ||
!currentEditor.isEditable ||
text.length === 0
) {
return null;
}
const { from, to } = currentEditor.state.selection;
if (!currentEditor.isFocused) input.onBeforeProgrammaticFocus?.();
currentEditor
.chain()
.focus()
.setMeta(AGENT_RICH_TEXT_SKIP_USER_CONTENT_EVENT_META, true)
.insertContentAt(
{ from, to },
plainTextToAgentRichTextInlineContent(text, {
capabilities: input.availableCapabilitiesRef.current,
skills: input.availableSkillsRef.current
})
)
.scrollIntoView()
.run();
return editorToPromptText(currentEditor);
},
openMentionPalette() {
const currentEditor = input.editorRef.current;
if (
!currentEditor ||
currentEditor.isDestroyed ||
!currentEditor.isEditable
) {
return;
}
const triggerText = isMentionTriggerBoundaryBeforeSelection(
currentEditor
)
? "@"
: " @";
if (!currentEditor.isFocused) input.onBeforeProgrammaticFocus?.();
currentEditor
.chain()
.focus()
.setMeta(AGENT_RICH_TEXT_SKIP_USER_CONTENT_EVENT_META, true)
.insertContent(triggerText)
.run();
},
insertWorkspaceReferences(items) {
const currentEditor = input.editorRef.current;
if (!currentEditor || currentEditor.isDestroyed || items.length === 0) {
return;
}
if (!currentEditor.isFocused) input.onBeforeProgrammaticFocus?.();
currentEditor
.chain()
.focus()
.insertContent(
createAgentFileMentionContent(items, {
prefixCaretAnchor: isPromptVisualLineStart(
currentEditor,
currentEditor.state.selection.from
)
})
)
.run();
},
insertMentionItems(items) {
const currentEditor = input.editorRef.current;
if (!currentEditor || currentEditor.isDestroyed || items.length === 0) {
return;
}
if (!currentEditor.isFocused) input.onBeforeProgrammaticFocus?.();
currentEditor
.chain()
.focus()
.insertContent(
createAgentMentionContent(items, {
prefixCaretAnchor: isPromptVisualLineStart(
currentEditor,
currentEditor.state.selection.from
)
})
)
.run();
},
insertComposerFiles(items) {
const currentEditor = input.editorRef.current;
if (!currentEditor || currentEditor.isDestroyed || items.length === 0) {
return;
}
if (!currentEditor.isFocused) input.onBeforeProgrammaticFocus?.();
currentEditor
.chain()
.focus()
.insertContent(
createAgentMentionContent(
items.map((item) => ({
kind: "file" as const,
href: "",
path: "",
name: item.name,
entryKind: "file" as const,
directoryPath: "",
attachmentId: item.id,
attachmentStatus: item.status,
attachmentErrorCode: item.errorCode
})),
{
prefixCaretAnchor: isPromptVisualLineStart(
currentEditor,
currentEditor.state.selection.from
)
}
)
)
.run();
},
updateComposerFiles(items) {
const currentEditor = input.editorRef.current;
if (!currentEditor || currentEditor.isDestroyed || items.length === 0) {
return false;
}
const updateById = new Map(items.map((item) => [item.id, item]));
let transaction = currentEditor.state.tr;
let changed = false;
currentEditor.state.doc.descendants((node, position) => {
if (node.type.name !== "agentFileMention") return true;
const attachmentId =
typeof node.attrs.attachmentId === "string"
? node.attrs.attachmentId
: "";
const update = updateById.get(attachmentId);
if (!update) return false;
transaction = transaction.setNodeMarkup(position, undefined, {
...node.attrs,
name: update.name,
attachmentStatus: update.status,
attachmentErrorCode: update.errorCode ?? ""
});
changed = true;
return false;
});
if (changed) currentEditor.view.dispatch(transaction);
return changed;
},
replaceTextBeforeSelection(length, text) {
const currentEditor = input.editorRef.current;
if (!currentEditor || currentEditor.isDestroyed || length <= 0) {
return null;
}
const to = currentEditor.state.selection.from;
const from = Math.max(1, to - length);
if (!currentEditor.isFocused) input.onBeforeProgrammaticFocus?.();
currentEditor
.chain()
.focus()
.insertContentAt(
{ from, to },
plainTextToAgentRichTextInlineContent(text, {
capabilities: input.availableCapabilitiesRef.current,
skills: input.availableSkillsRef.current
})
)
.run();
return editorToPromptText(currentEditor);
}
}),
[]
);
}