PluginProbe
Extendify / 3.1.3
Extendify v3.1.3
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / QuickEdit / lib / ask-ai.js

ask-ai.js in Extendify 3.1.3, at src/QuickEdit/lib/ask-ai.js

136 lines 4.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useQuickEditStore } from '../state/store';
2 import { buildAgentBlockDescriptor } from './agent-block-descriptor';
3
4 const AGENT_ATTR = 'data-extendify-agent-block-id';
5 const AGENT_PART_ID = 'data-extendify-part-block-id';
6
7 export const isAgentAvailable = () => !!window.extAgentData;
8
9 export const hasAgentBlockSelected = () => {
10 if (!isAgentAvailable()) return false;
11 return !!useQuickEditStore.getState().agentBlock;
12 };
13
14 // listener(hasBlock: boolean). Returns an unsubscribe fn.
15 export const subscribeToAgentBlock = (listener) => {
16 if (!isAgentAvailable()) return () => {};
17 let prev = !!useQuickEditStore.getState().agentBlock;
18 return useQuickEditStore.subscribe((state) => {
19 const next = !!state.agentBlock;
20 if (next !== prev) {
21 prev = next;
22 listener(next);
23 }
24 });
25 };
26
27 // Synchronous-readable cache of the agent sidebar's open state. The click
28 // rule runs in a sync capture-phase listener and can't await a dynamic
29 // import to choose between today's commit and the silent-stage bridge.
30 // Watcher is kicked off lazily on first read so a Jest resetModules + doMock
31 // can swap the agent store before the import resolves.
32 let cachedSidebarOpen = false;
33 let watcherStarted = false;
34 const startSidebarWatcher = () => {
35 if (watcherStarted || !isAgentAvailable()) return;
36 watcherStarted = true;
37 import('@agent/state/global').then(({ useGlobalStore }) => {
38 cachedSidebarOpen = !!useGlobalStore.getState().open;
39 useGlobalStore.subscribe((state) => {
40 cachedSidebarOpen = !!state.open;
41 });
42 });
43 };
44
45 export const isAgentSidebarOpen = () => {
46 startSidebarWatcher();
47 return cachedSidebarOpen;
48 };
49
50 const findAgentTagged = (el) => {
51 let node = el;
52 while (node && node !== document.body) {
53 if (node.hasAttribute?.(AGENT_ATTR) || node.hasAttribute?.(AGENT_PART_ID)) {
54 return node;
55 }
56 node = node.parentElement;
57 }
58 return null;
59 };
60
61 // Bridges the gap between the click and the Agent's own selection
62 // outline (which renders after panel mount + ResizeObserver settles).
63 const flashSelection = (el) => {
64 if (!el) return;
65 el.classList.add('extendify-quick-edit-ask-flash');
66 window.setTimeout(() => {
67 el.classList.remove('extendify-quick-edit-ask-flash');
68 }, 1500);
69 };
70
71 // Focus the agent chat textarea so the user can type their request right
72 // away. The textarea is created when the panel mounts; the brief retry
73 // covers the open-from-closed case where it isn't in the DOM yet (when the
74 // sidebar is already open it's found on the first try). preventScroll keeps
75 // the block the user just clicked in view rather than yanking to the chat.
76 const focusChatInput = (tries = 20) => {
77 const ta = document.querySelector('#extendify-agent-chat-textarea');
78 if (ta) {
79 ta.focus({ preventScroll: true });
80 return;
81 }
82 if (tries > 0) setTimeout(() => focusChatInput(tries - 1), 50);
83 };
84
85 // Stage a block for the already-open agent. No setOpen — the sidebar is
86 // already up — but we DO focus the chat so the cursor lands ready to type,
87 // matching the Ask AI pill and the selector behavior on a fresh open. The
88 // visual bridge is the same flash + the agent's DOMHighlighter outline that
89 // engages once agentBlock is set.
90 export const stageAgentBlock = (el) => {
91 if (!isAgentAvailable()) return;
92 const match = findAgentTagged(el);
93 if (!match) return;
94 flashSelection(match);
95 const next = buildAgentBlockDescriptor(match);
96 const current = useQuickEditStore.getState().agentBlock;
97 if (!current || current.id !== next.id || current.target !== next.target) {
98 useQuickEditStore.setState({
99 agentBlock: next,
100 agentBlockCode: null,
101 });
102 }
103 focusChatInput();
104 };
105
106 export const askAiAboutElement = async (el) => {
107 if (!isAgentAvailable()) return;
108
109 const match = findAgentTagged(el);
110 flashSelection(match || el);
111
112 // setBlock before setOpen so DOMHighlighter sees the block on mount.
113 const { useGlobalStore } = await import('@agent/state/global');
114 const { useEditModeStore } = await import('@quick-edit/state/edit-mode');
115
116 if (match) {
117 // Edit mode is the gate that mounts DOMHighlighter — turn it on so
118 // the X-close indicator renders alongside the selection. Skip
119 // re-setting agentBlock when the user is re-clicking Ask AI on
120 // the already-staged block: pushing a new descriptor would churn
121 // DOMHighlighter and any workflow that pinned the block.
122 useEditModeStore.getState().setOn(true);
123 const next = buildAgentBlockDescriptor(match);
124 const current = useQuickEditStore.getState().agentBlock;
125 if (!current || current.id !== next.id || current.target !== next.target) {
126 useQuickEditStore.setState({
127 agentBlock: next,
128 agentBlockCode: null,
129 });
130 }
131 }
132 useGlobalStore.getState().setOpen(true);
133
134 focusChatInput();
135 };
136