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

97 lines
2.8 KiB
TypeScript

import { Button, MenuSurface } from "@tutti-os/ui-system";
import { createPortal } from "react-dom";
export interface AgentGUITextSelectionSnapshot {
left: number;
text: string;
top: number;
}
export function readAgentGUITextSelection(
root: HTMLElement
): AgentGUITextSelectionSnapshot | null {
const ownerDocument = root.ownerDocument;
const selection = ownerDocument.getSelection();
if (
!selection ||
selection.isCollapsed ||
selection.rangeCount === 0 ||
!selection.anchorNode ||
!selection.focusNode ||
!root.contains(selection.anchorNode) ||
!root.contains(selection.focusNode)
) {
return null;
}
const text = selection.toString().trim();
if (!text) return null;
const rect = selection.getRangeAt(0).getBoundingClientRect();
if (rect.width <= 0 && rect.height <= 0) return null;
const viewportWidth = ownerDocument.defaultView?.innerWidth ?? 0;
const center = rect.left + rect.width / 2;
return {
left: Math.min(Math.max(center, 132), Math.max(132, viewportWidth - 132)),
text,
top: Math.max(48, rect.top - 8)
};
}
export function AgentGUITextSelectionActions({
labels,
snapshot,
portalTarget,
onAddToConversation,
onAskInSide,
onDismiss
}: {
labels: { addToConversation: string; askInSide: string };
snapshot: AgentGUITextSelectionSnapshot;
portalTarget: HTMLElement;
onAddToConversation: (text: string) => void;
onAskInSide?: (text: string) => void;
onDismiss: () => void;
}): React.JSX.Element {
const run = (action: (text: string) => void) => {
onDismiss();
portalTarget.ownerDocument.getSelection()?.removeAllRanges();
action(snapshot.text);
};
return createPortal(
<MenuSurface
role="toolbar"
aria-label={labels.addToConversation}
className="nodrag fixed flex-row -translate-x-1/2 -translate-y-full gap-0 overflow-hidden rounded-xl p-0 text-[14px] font-medium shadow-[var(--tsh-shell-shadow)] [-webkit-app-region:no-drag]"
data-testid="agent-gui-text-selection-actions"
style={{
left: snapshot.left,
top: snapshot.top,
zIndex: "var(--z-popover)"
}}
onPointerDown={(event) => event.preventDefault()}
>
<Button
type="button"
variant="ghost"
size="lg"
className="h-10 rounded-none px-4 text-[14px] font-medium"
onClick={() => run(onAddToConversation)}
>
{labels.addToConversation}
</Button>
{onAskInSide ? (
<Button
type="button"
variant="ghost"
size="lg"
className="h-10 rounded-none border-0 border-l border-[var(--border-1)] px-4 text-[14px] font-medium"
onClick={() => run(onAskInSide)}
>
{labels.askInSide}
</Button>
) : null}
</MenuSurface>,
portalTarget
);
}