adhdev 0.1.43 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/package.json +2 -1
  2. package/providers/_builtin/cli/claude-cli/provider.js +125 -0
  3. package/providers/_builtin/cli/codex-cli/provider.js +77 -0
  4. package/providers/_builtin/cli/gemini-cli/provider.js +121 -0
  5. package/providers/_builtin/extension/cline/provider.js +77 -0
  6. package/providers/_builtin/extension/cline/scripts/focus_editor.js +48 -0
  7. package/providers/_builtin/extension/cline/scripts/list_chats.js +100 -0
  8. package/providers/_builtin/extension/cline/scripts/new_session.js +85 -0
  9. package/providers/_builtin/extension/cline/scripts/open_panel.js +25 -0
  10. package/providers/_builtin/extension/cline/scripts/read_chat.js +257 -0
  11. package/providers/_builtin/extension/cline/scripts/resolve_action.js +83 -0
  12. package/providers/_builtin/extension/cline/scripts/send_message.js +95 -0
  13. package/providers/_builtin/extension/cline/scripts/switch_session.js +206 -0
  14. package/providers/_builtin/extension/roo-code/provider.js +578 -0
  15. package/providers/_builtin/ide/antigravity/provider.js +92 -0
  16. package/providers/_builtin/ide/antigravity/scripts/focus_editor.js +20 -0
  17. package/providers/_builtin/ide/antigravity/scripts/list_chats.js +137 -0
  18. package/providers/_builtin/ide/antigravity/scripts/new_session.js +75 -0
  19. package/providers/_builtin/ide/antigravity/scripts/read_chat.js +240 -0
  20. package/providers/_builtin/ide/antigravity/scripts/resolve_action.js +64 -0
  21. package/providers/_builtin/ide/antigravity/scripts/send_message.js +56 -0
  22. package/providers/_builtin/ide/antigravity/scripts/switch_session.js +114 -0
  23. package/providers/_builtin/ide/cursor/provider.js +242 -0
  24. package/providers/_builtin/ide/cursor/provider.js.backup +116 -0
  25. package/providers/_builtin/ide/cursor/provider.js.bak +127 -0
  26. package/providers/_builtin/ide/cursor/scripts_backup/focus_editor.js +20 -0
  27. package/providers/_builtin/ide/cursor/scripts_backup/list_chats.js +111 -0
  28. package/providers/_builtin/ide/cursor/scripts_backup/new_session.js +62 -0
  29. package/providers/_builtin/ide/cursor/scripts_backup/open_panel.js +31 -0
  30. package/providers/_builtin/ide/cursor/scripts_backup/read_chat.js +433 -0
  31. package/providers/_builtin/ide/cursor/scripts_backup/resolve_action.js +90 -0
  32. package/providers/_builtin/ide/cursor/scripts_backup/send_message.js +86 -0
  33. package/providers/_builtin/ide/cursor/scripts_backup/switch_session.js +63 -0
  34. package/providers/_builtin/ide/vscode/provider.js +36 -0
  35. package/providers/_builtin/ide/vscode-insiders/provider.js +27 -0
  36. package/providers/_builtin/ide/vscodium/provider.js +27 -0
  37. package/providers/_builtin/ide/windsurf/provider.js +64 -0
  38. package/providers/_builtin/ide/windsurf/scripts/focus_editor.js +30 -0
  39. package/providers/_builtin/ide/windsurf/scripts/list_chats.js +117 -0
  40. package/providers/_builtin/ide/windsurf/scripts/new_session.js +69 -0
  41. package/providers/_builtin/ide/windsurf/scripts/open_panel.js +58 -0
  42. package/providers/_builtin/ide/windsurf/scripts/read_chat.js +297 -0
  43. package/providers/_builtin/ide/windsurf/scripts/resolve_action.js +68 -0
  44. package/providers/_builtin/ide/windsurf/scripts/send_message.js +87 -0
  45. package/providers/_builtin/ide/windsurf/scripts/switch_session.js +58 -0
  46. package/providers/_helpers/index.js +188 -0
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Windsurf v1 — read_chat (v1 — Cascade DOM + Fiber)
3
+ *
4
+ * Windsurf는 VS Code 포크, 채팅 UI를 "Cascade"라고 부릅니다.
5
+ *
6
+ * DOM 구조:
7
+ * #windsurf.cascadePanel → .chat-client-root
8
+ * 스크롤: .cascade-scrollbar
9
+ * 메시지 리스트: .cascade-scrollbar .pb-20 > .flex.flex-col > .flex.flex-col.gap-2\.5
10
+ * 사용자 메시지: hasProse=false, hasWhitespace=true
11
+ * AI 메시지: [class*="prose"] (prose-sm)
12
+ * 피드백 UI: .mark-js-ignore (무시)
13
+ * 입력: [data-lexical-editor="true"]
14
+ *
15
+ * Fiber props:
16
+ * cascadeId: 세션 ID
17
+ * isRunning: 생성 중 여부
18
+ * hasPendingTerminalCommand: 승인 대기
19
+ * copyableText: AI 응답 마크다운 원본
20
+ *
21
+ * 최종 확인: Windsurf (2026-03-06)
22
+ */
23
+ (() => {
24
+ try {
25
+ // ─── 1. 컨테이너 ───
26
+ const cascade = document.querySelector('#windsurf\\.cascadePanel')
27
+ || document.querySelector('.chat-client-root');
28
+ if (!cascade) {
29
+ return { id: 'no_cascade', status: 'idle', title: 'No Cascade', messages: [], inputContent: '', activeModal: null };
30
+ }
31
+
32
+ // ─── 2. Fiber에서 cascadeId, isRunning 추출 (턴 요소에서 탐색) ───
33
+ let cascadeId = 'cascade';
34
+ let isRunning = false;
35
+ let hasPendingCmd = false;
36
+ try {
37
+ // 턴 요소에서 Fiber 탐색 (cascadePanel 루트보다 깊이 6에서 cascadeId 발견)
38
+ const scrollArea = cascade.querySelector('.cascade-scrollbar');
39
+ const gapEls = scrollArea ? scrollArea.querySelectorAll('[class*="gap-2"]') : [];
40
+ let firstTurn = null;
41
+ for (const el of gapEls) {
42
+ if (el.children.length >= 1 && el.closest('.cascade-scrollbar')) {
43
+ firstTurn = el.children[0]; break;
44
+ }
45
+ }
46
+ const fiberTarget = firstTurn || cascade;
47
+ const fk = Object.keys(fiberTarget).find(k => k.startsWith('__reactFiber'));
48
+ if (fk) {
49
+ let fiber = fiberTarget[fk];
50
+ for (let d = 0; d < 50 && fiber; d++) {
51
+ const p = fiber.memoizedProps || fiber.pendingProps || {};
52
+ if (p.cascadeId && typeof p.cascadeId === 'string') cascadeId = p.cascadeId;
53
+ if (p.isRunning === true) isRunning = true;
54
+ if (p.hasPendingTerminalCommand === true) hasPendingCmd = true;
55
+ fiber = fiber.return;
56
+ }
57
+ }
58
+ } catch (_) { }
59
+
60
+ // ─── 3. 상태 감지 ───
61
+ let status = 'idle';
62
+ if (isRunning) status = 'generating';
63
+
64
+ // Signal A: Stop 버튼
65
+ if (status === 'idle') {
66
+ const allBtns = Array.from(document.querySelectorAll('button'));
67
+ const stopBtn = allBtns.find(b => {
68
+ if (b.offsetWidth === 0) return false;
69
+ const label = (b.getAttribute('aria-label') || '').toLowerCase();
70
+ const text = (b.textContent || '').trim().toLowerCase();
71
+ return label.includes('stop') || label === 'cancel generation'
72
+ || text === 'stop' || text === 'stop generating';
73
+ });
74
+ if (stopBtn) status = 'generating';
75
+ }
76
+
77
+ // Signal B: 입력창 placeholder
78
+ if (status === 'idle') {
79
+ const editor = cascade.querySelector('[data-lexical-editor="true"]');
80
+ if (editor) {
81
+ const ph = (editor.getAttribute('placeholder') || '').toLowerCase();
82
+ if (ph.includes('wait') || ph.includes('generating')) status = 'generating';
83
+ }
84
+ }
85
+
86
+ const title = document.title.split(' \u2014 ')[0].trim() || 'Cascade';
87
+
88
+ // ─── 4. HTML → Markdown 변환기 ───
89
+ function htmlToMd(node) {
90
+ if (node.nodeType === 3) return node.textContent || '';
91
+ if (node.nodeType !== 1) return '';
92
+ const tag = node.tagName;
93
+ if (tag === 'STYLE' || tag === 'SCRIPT' || tag === 'SVG') return '';
94
+ if (tag === 'TABLE') {
95
+ const rows = Array.from(node.querySelectorAll('tr'));
96
+ if (rows.length === 0) return '';
97
+ const table = rows.map(tr =>
98
+ Array.from(tr.querySelectorAll('th, td')).map(cell => (cell.textContent || '').trim().replace(/\|/g, '\\|'))
99
+ );
100
+ const colCount = Math.max(...table.map(r => r.length));
101
+ const header = table[0] || [];
102
+ const sep = Array(colCount).fill('---');
103
+ const body = table.slice(1);
104
+ let md = '| ' + header.join(' | ') + ' |\n';
105
+ md += '| ' + sep.join(' | ') + ' |\n';
106
+ for (const row of body) {
107
+ while (row.length < colCount) row.push('');
108
+ md += '| ' + row.join(' | ') + ' |\n';
109
+ }
110
+ return '\n' + md + '\n';
111
+ }
112
+ if (tag === 'UL') return '\n' + Array.from(node.children).map(li => '- ' + childrenToMd(li).trim()).join('\n') + '\n';
113
+ if (tag === 'OL') return '\n' + Array.from(node.children).map((li, i) => (i + 1) + '. ' + childrenToMd(li).trim()).join('\n') + '\n';
114
+ if (tag === 'LI') return childrenToMd(node);
115
+ if (tag === 'H1') return '\n# ' + childrenToMd(node).trim() + '\n';
116
+ if (tag === 'H2') return '\n## ' + childrenToMd(node).trim() + '\n';
117
+ if (tag === 'H3') return '\n### ' + childrenToMd(node).trim() + '\n';
118
+ if (tag === 'H4') return '\n#### ' + childrenToMd(node).trim() + '\n';
119
+ if (tag === 'STRONG' || tag === 'B') return '**' + childrenToMd(node).trim() + '**';
120
+ if (tag === 'EM' || tag === 'I') return '*' + childrenToMd(node).trim() + '*';
121
+ if (tag === 'PRE') {
122
+ const codeEl = node.querySelector('code');
123
+ const lang = codeEl ? (codeEl.className.match(/language-(\w+)/)?.[1] || '') : '';
124
+ const code = (codeEl || node).textContent || '';
125
+ return '\n```' + lang + '\n' + code.trim() + '\n```\n';
126
+ }
127
+ if (tag === 'CODE') {
128
+ if (node.parentElement && node.parentElement.tagName === 'PRE') return node.textContent || '';
129
+ return '`' + (node.textContent || '').trim() + '`';
130
+ }
131
+ if (tag === 'BLOCKQUOTE') return '\n> ' + childrenToMd(node).trim().replace(/\n/g, '\n> ') + '\n';
132
+ if (tag === 'A') return '[' + childrenToMd(node).trim() + '](' + (node.getAttribute('href') || '') + ')';
133
+ if (tag === 'BR') return '\n';
134
+ if (tag === 'P') return '\n' + childrenToMd(node).trim() + '\n';
135
+ return childrenToMd(node);
136
+ }
137
+ function childrenToMd(node) {
138
+ return Array.from(node.childNodes).map(htmlToMd).join('');
139
+ }
140
+
141
+ function getCleanMd(el) {
142
+ const clone = el.cloneNode(true);
143
+ clone.querySelectorAll('button, [role="button"], style, script, svg, .codicon, [class*="feedback"]').forEach(n => n.remove());
144
+ clone.querySelectorAll('*').forEach(child => {
145
+ if (!child.parentNode) return;
146
+ const t = (child.textContent || '').trim();
147
+ if (t.length > 60) return;
148
+ const low = t.toLowerCase();
149
+ if (/^(analyzed\s+\d|edited\s+\d|ran\s+\S|terminal\s|reading|searching)/i.test(low)) child.remove();
150
+ });
151
+ let md = htmlToMd(clone);
152
+ md = md.replace(/\n{3,}/g, '\n\n').trim();
153
+ return md;
154
+ }
155
+
156
+ // ─── 5. 메시지 수집 ───
157
+ const collected = [];
158
+ const seenHashes = new Set();
159
+
160
+ const scrollArea = cascade.querySelector('.cascade-scrollbar');
161
+ if (!scrollArea) {
162
+ return { id: cascadeId, status, title, messages: [], inputContent: '', activeModal: null };
163
+ }
164
+
165
+ // 메시지 리스트 컨테이너 찾기: .gap-2.5 중 자식 2개 이상
166
+ let msgContainer = null;
167
+ const gapEls = scrollArea.querySelectorAll('[class*="gap-2"]');
168
+ for (const el of gapEls) {
169
+ if (el.children.length >= 2 && el.closest('.cascade-scrollbar')) {
170
+ msgContainer = el; break;
171
+ }
172
+ }
173
+
174
+ if (msgContainer) {
175
+ const turns = Array.from(msgContainer.children);
176
+ for (const turn of turns) {
177
+ // .mark-js-ignore = 피드백 UI → 무시
178
+ if ((turn.className || '').includes('mark-js-ignore')) continue;
179
+ if (turn.offsetHeight < 10) continue;
180
+
181
+ // 역할 판별: .prose = AI, 아니면 user
182
+ const proseEl = turn.querySelector('[class*="prose"]');
183
+ const role = proseEl ? 'assistant' : 'user';
184
+
185
+ let text = '';
186
+ if (role === 'assistant') {
187
+ // AI: Fiber copyableText가 있으면 사용 (이미 마크다운!)
188
+ try {
189
+ const fk = Object.keys(turn).find(k => k.startsWith('__reactFiber'));
190
+ if (fk) {
191
+ let fiber = turn[fk];
192
+ for (let d = 0; d < 20 && fiber; d++) {
193
+ const p = fiber.memoizedProps || {};
194
+ if (p.copyableText && typeof p.copyableText === 'string' && p.copyableText.length > 5) {
195
+ text = p.copyableText;
196
+ break;
197
+ }
198
+ fiber = fiber.return;
199
+ }
200
+ }
201
+ } catch (_) { }
202
+
203
+ // Fiber에 없으면 HTML→Markdown
204
+ if (!text) {
205
+ const mdRoot = proseEl || turn;
206
+ text = getCleanMd(mdRoot);
207
+ }
208
+ } else {
209
+ // 사용자: whitespace-pre-wrap 요소에서 텍스트 추출
210
+ const whitespace = turn.querySelector('[class*="whitespace"]');
211
+ text = (whitespace || turn).innerText?.trim() || '';
212
+ }
213
+
214
+ if (!text || text.length < 1) continue;
215
+
216
+ const hash = role + ':' + text.slice(0, 200);
217
+ if (seenHashes.has(hash)) continue;
218
+ seenHashes.add(hash);
219
+ collected.push({ role, text, el: turn });
220
+ }
221
+ }
222
+
223
+ // DOM 순서 정렬
224
+ collected.sort((a, b) => {
225
+ const pos = a.el.compareDocumentPosition(b.el);
226
+ if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
227
+ if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
228
+ return 0;
229
+ });
230
+
231
+ // 최신 30개만
232
+ const trimmed = collected.length > 30 ? collected.slice(-30) : collected;
233
+
234
+ const final = trimmed.map((m, i) => ({
235
+ id: 'msg_' + i,
236
+ role: m.role,
237
+ content: m.text.length > 6000 ? m.text.slice(0, 6000) + '\n[... truncated]' : m.text,
238
+ index: i,
239
+ kind: 'standard'
240
+ }));
241
+
242
+ // ─── 6. 입력창 ───
243
+ const editor = cascade.querySelector('[data-lexical-editor="true"]')
244
+ || cascade.querySelector('[contenteditable="true"][role="textbox"]')
245
+ || cascade.querySelector('textarea:not(.xterm-helper-textarea)');
246
+ const inputContent = editor ? (editor.innerText || editor.value || '').trim() : '';
247
+
248
+ // ─── 7. 모달/승인 감지 ───
249
+ let activeModal = null;
250
+ try {
251
+ // Fiber: hasPendingTerminalCommand
252
+ if (hasPendingCmd) {
253
+ const allBtns = Array.from(document.querySelectorAll('button')).filter(b => b.offsetWidth > 0);
254
+ const approvalBtns = allBtns.filter(b => {
255
+ const t = (b.textContent || '').trim().toLowerCase();
256
+ return /^(run|reject|skip|approve|allow|deny|cancel|accept)\b/i.test(t);
257
+ });
258
+ const btnTexts = [...new Set(approvalBtns.map(b => (b.textContent || '').trim()).filter(t => t.length < 40))];
259
+ activeModal = { message: 'Terminal command pending', buttons: btnTexts.length > 0 ? btnTexts : ['Run', 'Reject'] };
260
+ }
261
+
262
+ // Dialog 폴백
263
+ if (!activeModal) {
264
+ const dialog = document.querySelector('.monaco-dialog-box, [role="dialog"]');
265
+ if (dialog && dialog.offsetWidth > 80) {
266
+ const msg = (dialog.querySelector('.dialog-message') || dialog).innerText?.trim() || '';
267
+ const buttons = Array.from(dialog.querySelectorAll('.monaco-button, button'))
268
+ .map(b => (b.innerText || '').trim()).filter(t => t.length > 0 && t.length < 30);
269
+ if (msg || buttons.length > 0) {
270
+ activeModal = { message: msg.slice(0, 300), buttons };
271
+ }
272
+ }
273
+ }
274
+
275
+ // 인라인 승인 버튼
276
+ if (!activeModal) {
277
+ const allBtns = Array.from(document.querySelectorAll('button')).filter(b => b.offsetWidth > 0);
278
+ const approvalBtns = allBtns.filter(b => {
279
+ const t = (b.textContent || '').trim().toLowerCase();
280
+ if (t.length > 40) return false;
281
+ return /^(run|reject|skip|approve|allow|deny)\b/i.test(t)
282
+ || t === 'always allow' || t === 'always deny';
283
+ });
284
+ if (approvalBtns.length >= 2) {
285
+ const btnTexts = [...new Set(approvalBtns.map(b => (b.textContent || '').trim()))];
286
+ activeModal = { message: '', buttons: btnTexts };
287
+ }
288
+ }
289
+
290
+ if (activeModal) status = 'waiting_approval';
291
+ } catch (_) { activeModal = null; }
292
+
293
+ return { id: cascadeId, status, title, messages: final, inputContent, activeModal };
294
+ } catch (e) {
295
+ return { id: 'error', status: 'error', error: e.message, messages: [] };
296
+ }
297
+ })()
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Windsurf v1 — resolve_action
3
+ *
4
+ * 승인 다이얼로그/인라인 버튼을 클릭합니다.
5
+ *
6
+ * 파라미터: ${ BUTTON_TEXT } — 클릭할 버튼 텍스트 (lowercase)
7
+ *
8
+ * 최종 확인: Windsurf (2026-03-06)
9
+ */
10
+ (() => {
11
+ try {
12
+ const want = ${ BUTTON_TEXT };
13
+ const normalize = (s) => (s || '').replace(/[\s\u200b\u00a0]+/g, ' ').trim().toLowerCase();
14
+ const matches = (text) => {
15
+ const t = normalize(text);
16
+ if (!t) return false;
17
+ if (t === want) return true;
18
+ if (t.indexOf(want) === 0) return true;
19
+ if (want === 'run' && (/^run\s*/.test(t) || t === 'enter' || t === '⏎')) return true;
20
+ if (want === 'approve' && (t.includes('approve') || t === 'always allow' || t === 'allow')) return true;
21
+ if (want === 'reject' && (t.includes('reject') || t === 'deny' || t === 'always deny')) return true;
22
+ return false;
23
+ };
24
+ const click = (el) => {
25
+ el.focus?.();
26
+ const rect = el.getBoundingClientRect();
27
+ const x = rect.left + rect.width / 2;
28
+ const y = rect.top + rect.height / 2;
29
+ for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
30
+ el.dispatchEvent(new PointerEvent(type, {
31
+ bubbles: true, cancelable: true, view: window,
32
+ clientX: x, clientY: y, pointerId: 1, pointerType: 'mouse'
33
+ }));
34
+ }
35
+ return true;
36
+ };
37
+
38
+ // 1. Dialog 내부
39
+ const dialog = document.querySelector('.monaco-dialog-box, [role="dialog"]');
40
+ if (dialog && dialog.offsetWidth > 80) {
41
+ const btns = dialog.querySelectorAll('.monaco-button, button');
42
+ for (const b of btns) {
43
+ if (matches(b.textContent)) return click(b);
44
+ }
45
+ }
46
+
47
+ // 2. 모든 보이는 버튼
48
+ const sel = 'button, [role="button"], .monaco-button';
49
+ const allBtns = Array.from(document.querySelectorAll(sel))
50
+ .filter(b => b.offsetWidth > 0 && b.offsetHeight > 0);
51
+ for (const b of allBtns) {
52
+ if (matches(b.textContent)) return click(b);
53
+ }
54
+
55
+ // 3. Enter 키 폴백 (run)
56
+ if (want === 'run') {
57
+ document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {
58
+ key: 'Enter', code: 'Enter', keyCode: 13,
59
+ bubbles: true, cancelable: true
60
+ }));
61
+ return true;
62
+ }
63
+
64
+ return false;
65
+ } catch (e) {
66
+ return false;
67
+ }
68
+ })()
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Windsurf v1 — send_message
3
+ *
4
+ * Windsurf는 Lexical 에디터 ([data-lexical-editor="true"])를 사용합니다.
5
+ * contenteditable div이므로 execCommand('insertText') 또는 InputEvent로 입력 가능.
6
+ *
7
+ * ⚠️ Lexical은 입력 이벤트를 정밀하게 감지하므로:
8
+ * - execCommand('insertText')가 가장 안정적
9
+ * - nativeSetter 방식은 동작하지 않음
10
+ * - InputEvent('insertText')를 폴백으로 사용
11
+ *
12
+ * Enter 키는 KeyboardEvent 전체 시퀀스 필요 (keydown + keypress + keyup).
13
+ *
14
+ * 파라미터: ${ MESSAGE }
15
+ * 최종 확인: Windsurf 1.108.x (2026-03-10)
16
+ */
17
+ (async () => {
18
+ try {
19
+ const msg = ${ MESSAGE };
20
+
21
+ // ─── 1. Lexical 에디터 찾기 (폴백 체인) ───
22
+ const editor =
23
+ document.querySelector('[data-lexical-editor="true"]') ||
24
+ document.querySelector('[contenteditable="true"][role="textbox"]') ||
25
+ document.querySelector('.cascade-input [contenteditable="true"]') ||
26
+ document.querySelector('.chat-input textarea') ||
27
+ document.querySelector('textarea:not(.xterm-helper-textarea)');
28
+
29
+ if (!editor) return 'error: no input found';
30
+
31
+ const isTextarea = editor.tagName === 'TEXTAREA';
32
+
33
+ if (isTextarea) {
34
+ // ─── textarea 폴백 (미로그인 등) ───
35
+ editor.focus();
36
+ const proto = HTMLTextAreaElement.prototype;
37
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
38
+ if (nativeSetter) nativeSetter.call(editor, msg);
39
+ else editor.value = msg;
40
+
41
+ editor.dispatchEvent(new Event('input', { bubbles: true }));
42
+ editor.dispatchEvent(new Event('change', { bubbles: true }));
43
+
44
+ await new Promise(r => setTimeout(r, 300));
45
+
46
+ const enterOpts = {
47
+ key: 'Enter', code: 'Enter',
48
+ keyCode: 13, which: 13,
49
+ bubbles: true, cancelable: true, composed: true,
50
+ };
51
+ editor.dispatchEvent(new KeyboardEvent('keydown', enterOpts));
52
+ editor.dispatchEvent(new KeyboardEvent('keypress', enterOpts));
53
+ editor.dispatchEvent(new KeyboardEvent('keyup', enterOpts));
54
+
55
+ return 'sent';
56
+ }
57
+
58
+ // ─── 2. contenteditable (Lexical) 에디터 ───
59
+ editor.focus();
60
+
61
+ // 기존 내용 선택 후 삭제
62
+ document.execCommand('selectAll', false, null);
63
+ document.execCommand('delete', false, null);
64
+
65
+ // 텍스트 삽입 (Lexical은 execCommand('insertText')를 인식)
66
+ document.execCommand('insertText', false, msg);
67
+
68
+ // React/Lexical에 변경 알림
69
+ editor.dispatchEvent(new Event('input', { bubbles: true }));
70
+
71
+ await new Promise(r => setTimeout(r, 300));
72
+
73
+ // ─── 3. Enter 키 전송 (전체 시퀀스) ───
74
+ const enterOpts = {
75
+ key: 'Enter', code: 'Enter',
76
+ keyCode: 13, which: 13,
77
+ bubbles: true, cancelable: true, composed: true,
78
+ };
79
+ editor.dispatchEvent(new KeyboardEvent('keydown', enterOpts));
80
+ editor.dispatchEvent(new KeyboardEvent('keypress', enterOpts));
81
+ editor.dispatchEvent(new KeyboardEvent('keyup', enterOpts));
82
+
83
+ return 'sent';
84
+ } catch (e) {
85
+ return 'error: ' + e.message;
86
+ }
87
+ })()
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Windsurf v1 — switch_session
3
+ *
4
+ * Cascade 세션(대화) 탭을 전환합니다.
5
+ * cascade-tab-{id} 내부의 실제 클릭 가능한 자식 DIV의
6
+ * React onClick 핸들러를 직접 호출합니다.
7
+ *
8
+ * 파라미터: ${ SESSION_ID } — 전환할 세션 ID (cascade-tab의 UUID)
9
+ *
10
+ * 최종 확인: Windsurf 1.108.x (2026-03-10)
11
+ */
12
+ (() => {
13
+ try {
14
+ const id = ${ SESSION_ID };
15
+
16
+ // Helper: find React onClick on an element or its children
17
+ function clickReact(el) {
18
+ // Check element itself and children for React onClick
19
+ const targets = [el, ...el.querySelectorAll('*')];
20
+ for (const t of targets) {
21
+ const rp = Object.keys(t).find(k => k.startsWith('__reactProps'));
22
+ if (rp && typeof t[rp].onClick === 'function') {
23
+ t[rp].onClick({
24
+ preventDefault: () => { },
25
+ stopPropagation: () => { },
26
+ nativeEvent: { stopImmediatePropagation: () => { } },
27
+ target: t, currentTarget: t, button: 0, type: 'click'
28
+ });
29
+ return true;
30
+ }
31
+ }
32
+ return false;
33
+ }
34
+
35
+ // 1. cascade-tab-{id} 요소 찾기
36
+ const tab = document.getElementById('cascade-tab-' + id);
37
+ if (tab) {
38
+ if (clickReact(tab)) return 'switched';
39
+ // Fallback: DOM click
40
+ tab.click();
41
+ return 'switched-dom';
42
+ }
43
+
44
+ // 2. 제목으로 매칭
45
+ const tabs = document.querySelectorAll('[id^="cascade-tab-"]');
46
+ for (const t of tabs) {
47
+ if (t.textContent?.trim() === id) {
48
+ if (clickReact(t)) return 'switched-by-title';
49
+ t.click();
50
+ return 'switched-by-title-dom';
51
+ }
52
+ }
53
+
54
+ return 'not_found';
55
+ } catch (e) {
56
+ return 'error: ' + e.message;
57
+ }
58
+ })()