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,257 @@
1
+ /**
2
+ * Cline v1 — read_chat (v2 — Fiber 기반 역할 판별)
3
+ *
4
+ * 구조 (Cline 3.x — saoudrizwan.claude-dev):
5
+ * 1. outer webview iframe → inner contentDocument
6
+ * 2. data-testid="virtuoso-item-list" (React Virtuoso) — 활성 메시지 블록
7
+ * 3. React Fiber → data 배열에서 메시지 타입 직접 추출
8
+ * - type: "say", say: "user_feedback" → user
9
+ * - type: "say", say: "text" → assistant
10
+ * - type: "say", say: "checkpoint_created" → system (skip)
11
+ * - type: "ask", ask: "followup" → assistant (질문)
12
+ * - type: "ask", ask: "tool" → assistant (tool 승인 대기)
13
+ * 4. DOM textContent에서 콘텐츠 추출 + 정제
14
+ *
15
+ * 최종 확인: 2026-03-07
16
+ */
17
+ (() => {
18
+ try {
19
+ const inner = document.querySelector('iframe');
20
+ if (!inner) return JSON.stringify({ error: 'no inner iframe' });
21
+ const doc = inner.contentDocument || inner.contentWindow?.document;
22
+ if (!doc) return JSON.stringify({ error: 'cannot access contentDocument' });
23
+
24
+ const root = doc.getElementById('root');
25
+ if (!root) return JSON.stringify({ error: 'no root element' });
26
+
27
+ const isVisible = root.offsetHeight > 0;
28
+
29
+ // ─── 1. Fiber에서 data 배열 추출 ───
30
+ const virtuosoList = doc.querySelector('[data-testid="virtuoso-item-list"]');
31
+ let fiberData = null;
32
+
33
+ if (virtuosoList && virtuosoList.children.length > 0) {
34
+ const firstItem = virtuosoList.children[0];
35
+ const fiberKey = Object.keys(firstItem).find(k =>
36
+ k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance')
37
+ );
38
+ if (fiberKey) {
39
+ let fiber = firstItem[fiberKey];
40
+ for (let d = 0; d < 25 && fiber; d++) {
41
+ const props = fiber.memoizedProps || fiber.pendingProps;
42
+ if (props && props.data && Array.isArray(props.data) && props.data.length > 0) {
43
+ fiberData = props.data;
44
+ break;
45
+ }
46
+ fiber = fiber.return;
47
+ }
48
+ }
49
+ }
50
+
51
+ // ─── 2. 메시지 파싱 ───
52
+ const messages = [];
53
+
54
+ if (fiberData) {
55
+ // ★ Fiber 기반: 가장 정확한 역할 판별
56
+ for (let i = 0; i < fiberData.length; i++) {
57
+ const item = fiberData[i];
58
+ if (!item || typeof item !== 'object') continue;
59
+
60
+ const msgType = item.type; // "say" or "ask"
61
+ const saySub = item.say; // "user_feedback", "text", "checkpoint_created", etc.
62
+ const askSub = item.ask; // "followup", "tool", "command", etc.
63
+ const text = item.text || '';
64
+
65
+ // 시스템 이벤트 스킵
66
+ if (saySub === 'checkpoint_created') continue;
67
+ if (saySub === 'api_req_started' || saySub === 'api_req_finished') continue;
68
+ if (saySub === 'shell_integration_warning') continue;
69
+
70
+ // 역할 판별
71
+ let role = 'assistant';
72
+ if (saySub === 'user_feedback') role = 'user';
73
+ if (saySub === 'user_feedback_diff') role = 'user';
74
+
75
+ // 콘텐츠 추출
76
+ let content = '';
77
+ if (text) {
78
+ // ask.followup의 text는 JSON일 수 있음
79
+ if (askSub === 'followup' && text.startsWith('{')) {
80
+ try {
81
+ const parsed = JSON.parse(text);
82
+ content = parsed.question || parsed.text || text;
83
+ } catch { content = text; }
84
+ } else {
85
+ content = text;
86
+ }
87
+ }
88
+
89
+ // DOM 텍스트 fallback (Fiber text가 비어있을 때)
90
+ if (!content && virtuosoList && virtuosoList.children[i]) {
91
+ content = (virtuosoList.children[i].textContent || '').trim();
92
+ }
93
+
94
+ // 너무 짧거나 빈 콘텐츠 스킵
95
+ if (!content || content.length < 2) continue;
96
+
97
+ // 노이즈 정리
98
+ content = content
99
+ .replace(/CheckpointCompareRestore(Save)?/gi, '')
100
+ .replace(/^\s*API Request.*$/gm, '')
101
+ .replace(/^\s*Cost:.*$/gm, '')
102
+ .replace(/\s{3,}/g, '\n')
103
+ .trim();
104
+
105
+ if (content.length < 2) continue;
106
+
107
+ // 코드 블록 보존 (DOM에서 구조 추출)
108
+ if (virtuosoList.children[i]) {
109
+ const domItem = virtuosoList.children[i];
110
+ const preBlocks = domItem.querySelectorAll('pre');
111
+ if (preBlocks.length > 0 && role === 'assistant') {
112
+ let structured = '';
113
+ const walk = (node) => {
114
+ if (node.nodeType === 3) {
115
+ structured += node.textContent;
116
+ return;
117
+ }
118
+ if (node.nodeType !== 1) return;
119
+ const el = node;
120
+ if (el.tagName === 'PRE') {
121
+ const codeEl = el.querySelector('code');
122
+ const lang = codeEl ? (codeEl.className.match(/language-(\w+)/)?.[1] || '') : '';
123
+ const code = (codeEl || el).textContent || '';
124
+ structured += '\n```' + lang + '\n' + code.trim() + '\n```\n';
125
+ return;
126
+ }
127
+ for (const child of el.childNodes) walk(child);
128
+ };
129
+ walk(domItem);
130
+ const cleaned = structured.replace(/CheckpointCompareRestore(Save)?/gi, '').trim();
131
+ if (cleaned.length > content.length * 0.5) {
132
+ content = cleaned;
133
+ }
134
+ }
135
+ }
136
+
137
+ // 길이 제한
138
+ if (content.length > 2000) content = content.substring(0, 2000) + '…';
139
+
140
+ messages.push({
141
+ role,
142
+ content,
143
+ timestamp: item.ts || (Date.now() - (fiberData.length - i) * 1000),
144
+ // 디버그: 메시지 서브타입
145
+ _type: msgType,
146
+ _sub: saySub || askSub,
147
+ });
148
+ }
149
+ } else if (virtuosoList && virtuosoList.children.length > 0) {
150
+ // Fallback: DOM 기반 파싱 (Fiber 접근 실패 시)
151
+ for (let i = 0; i < virtuosoList.children.length; i++) {
152
+ const item = virtuosoList.children[i];
153
+ const rawText = (item.textContent || '').trim();
154
+ if (!rawText || rawText.length < 2) continue;
155
+ if (/^Checkpoint(Compare|Restore|Save)/i.test(rawText)) continue;
156
+ if (/^(Thinking\.\.\.|Loading\.\.\.)$/i.test(rawText)) continue;
157
+
158
+ let role = 'assistant';
159
+ let content = rawText
160
+ .replace(/CheckpointCompareRestore(Save)?/gi, '')
161
+ .replace(/\s{3,}/g, '\n')
162
+ .trim();
163
+ if (content.length < 2) continue;
164
+ if (content.length > 2000) content = content.substring(0, 2000) + '…';
165
+
166
+ messages.push({ role, content, timestamp: Date.now() - (virtuosoList.children.length - i) * 1000 });
167
+ }
168
+ }
169
+
170
+ // ─── 3. 입력 필드 ───
171
+ let inputContent = '';
172
+ const chatInput = doc.querySelector('[data-testid="chat-input"]');
173
+ if (chatInput) {
174
+ inputContent = chatInput.value || chatInput.textContent || '';
175
+ }
176
+
177
+ // ─── 4. 상태 판별 ───
178
+ let status = 'idle';
179
+ const buttons = Array.from(doc.querySelectorAll('button'))
180
+ .filter(b => b.offsetWidth > 0);
181
+ const buttonTexts = buttons.map(b => (b.textContent || '').trim().toLowerCase());
182
+
183
+ if (buttonTexts.includes('cancel')) status = 'generating';
184
+
185
+ // Fiber data에서 마지막 type=ask인지 확인
186
+ if (fiberData && fiberData.length > 0) {
187
+ const last = fiberData[fiberData.length - 1];
188
+ if (last.type === 'ask') {
189
+ if (last.ask === 'followup') status = 'waiting_approval';
190
+ if (last.ask === 'tool' || last.ask === 'command') status = 'waiting_approval';
191
+ }
192
+ }
193
+
194
+ // 버튼 기반 보완
195
+ const approvalPatterns = /^(proceed|approve|allow|accept|save|run command|yes|confirm)/i;
196
+ if (buttonTexts.some(b => approvalPatterns.test(b))) status = 'waiting_approval';
197
+
198
+ if (!isVisible && messages.length === 0) status = 'panel_hidden';
199
+
200
+ // ─── 5. 모델/모드 ───
201
+ let model = '';
202
+ const modeSwitch = doc.querySelector('[data-testid="mode-switch"]');
203
+ if (modeSwitch) model = (modeSwitch.textContent || '').trim();
204
+ if (!model) {
205
+ const modelSel = doc.querySelector('[data-testid*="model"], [aria-label*="model" i]');
206
+ if (modelSel) model = (modelSel.textContent || '').trim();
207
+ }
208
+ const mode = modeSwitch ? (modeSwitch.textContent || '').trim() : '';
209
+
210
+ // ─── 6. 승인 모달 ───
211
+ let activeModal = null;
212
+ if (status === 'waiting_approval') {
213
+ const approvalBtns = buttons
214
+ .map(b => (b.textContent || '').trim())
215
+ .filter(t => t && t.length > 0 && t.length < 40 &&
216
+ /proceed|approve|allow|accept|run|yes|reject|deny|cancel|no|skip|save|confirm/i.test(t));
217
+
218
+ let modalMessage = 'Cline wants to perform an action';
219
+ if (fiberData && fiberData.length > 0) {
220
+ const last = fiberData[fiberData.length - 1];
221
+ if (last.ask === 'followup' && last.text) {
222
+ try {
223
+ const parsed = JSON.parse(last.text);
224
+ modalMessage = parsed.question || last.text.substring(0, 200);
225
+ } catch { modalMessage = last.text.substring(0, 200); }
226
+ } else if (last.ask === 'tool' || last.ask === 'command') {
227
+ modalMessage = `Cline wants to use ${last.ask}`;
228
+ }
229
+ }
230
+
231
+ if (approvalBtns.length > 0) {
232
+ activeModal = { message: modalMessage, buttons: [...new Set(approvalBtns)] };
233
+ }
234
+ }
235
+
236
+ // ─── 7. 토큰/비용 ───
237
+ let tokenInfo = '';
238
+ const costEl = doc.querySelector('[data-testid*="cost"], [data-testid*="token"]');
239
+ if (costEl) tokenInfo = (costEl.textContent || '').trim();
240
+
241
+ return JSON.stringify({
242
+ agentType: 'cline',
243
+ agentName: 'Cline',
244
+ extensionId: 'saoudrizwan.claude-dev',
245
+ status,
246
+ isVisible,
247
+ messages: messages.slice(-30),
248
+ inputContent,
249
+ model,
250
+ mode,
251
+ tokenInfo,
252
+ activeModal,
253
+ });
254
+ } catch (e) {
255
+ return JSON.stringify({ error: e.message || String(e) });
256
+ }
257
+ })()
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Cline v1 — resolve_action
3
+ *
4
+ * Cline의 승인/거부 처리.
5
+ * Cline은 button 태그 외에 vscode-button 웹 컴포넌트도 사용.
6
+ * chatState.primaryButtonText / secondaryButtonText로 정확한 매칭.
7
+ *
8
+ * 파라미터: ${ ACTION } — "approve" 또는 "reject"
9
+ *
10
+ * 전략:
11
+ * 1. chatState.primaryButtonText == Approve → primary vscode-button 클릭
12
+ * 2. data-testid 기반 탐색
13
+ * 3. 텍스트 매칭 (button + vscode-button)
14
+ * 4. Fiber onSendMessage로 직접 승인 전달
15
+ *
16
+ * 최종 확인: 2026-03-07
17
+ */
18
+ (() => {
19
+ try {
20
+ const inner = document.querySelector('iframe');
21
+ const doc = inner?.contentDocument || inner?.contentWindow?.document;
22
+ if (!doc) return false;
23
+
24
+ const action = ${ ACTION };
25
+ const approvePatterns = ['proceed', 'approve', 'allow', 'accept', 'save', 'run', 'yes', 'confirm', 'resume'];
26
+ const rejectPatterns = ['reject', 'deny', 'cancel', 'no', 'skip'];
27
+ const patterns = action === 'approve' ? approvePatterns : rejectPatterns;
28
+
29
+ // ─── 모든 클릭 가능 요소 수집 (button + vscode-button) ───
30
+ const allBtns = [
31
+ ...Array.from(doc.querySelectorAll('button')),
32
+ ...Array.from(doc.querySelectorAll('vscode-button')),
33
+ ].filter(b => b.offsetWidth > 0 && b.offsetHeight > 0);
34
+
35
+ // ─── 1단계: data-testid 기반 ───
36
+ for (const btn of allBtns) {
37
+ const testId = (btn.getAttribute('data-testid') || '').toLowerCase();
38
+ if (action === 'approve' && (testId.includes('approve') || testId.includes('proceed') || testId.includes('accept') || testId.includes('run') || testId.includes('primary'))) {
39
+ btn.click(); return true;
40
+ }
41
+ if (action === 'reject' && (testId.includes('reject') || testId.includes('deny') || testId.includes('cancel') || testId.includes('secondary'))) {
42
+ btn.click(); return true;
43
+ }
44
+ }
45
+
46
+ // ─── 2단계: 텍스트 매칭 ───
47
+ for (const btn of allBtns) {
48
+ const text = (btn.textContent || '').trim().toLowerCase();
49
+ if (text.length === 0 || text.length > 40) continue;
50
+ if (patterns.some(p => text.startsWith(p) || text === p || text.includes(p))) {
51
+ btn.click(); return true;
52
+ }
53
+ }
54
+
55
+ // ─── 3단계: aria-label ───
56
+ for (const btn of allBtns) {
57
+ const label = (btn.getAttribute('aria-label') || '').toLowerCase();
58
+ if (patterns.some(p => label.includes(p))) {
59
+ btn.click(); return true;
60
+ }
61
+ }
62
+
63
+ // ─── 4단계: chatState 기반 — primary/secondary 버튼 직접 매칭 ───
64
+ // chatState.primaryButtonText = "Approve", secondaryButtonText = "Reject"
65
+ // 가장 큰 vscode-button이 primary 버튼
66
+ const vscBtns = Array.from(doc.querySelectorAll('vscode-button'))
67
+ .filter(b => b.offsetWidth > 100); // 큰 버튼만
68
+ if (vscBtns.length > 0) {
69
+ if (action === 'approve') {
70
+ // 가장 큰 버튼이 primary
71
+ vscBtns.sort((a, b) => b.offsetWidth - a.offsetWidth);
72
+ vscBtns[0].click(); return true;
73
+ }
74
+ // reject: 가장 작은 큰 버튼
75
+ if (action === 'reject' && vscBtns.length > 1) {
76
+ vscBtns.sort((a, b) => a.offsetWidth - b.offsetWidth);
77
+ vscBtns[0].click(); return true;
78
+ }
79
+ }
80
+
81
+ return false;
82
+ } catch { return false; }
83
+ })()
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Cline v1 — send_message
3
+ *
4
+ * 구조:
5
+ * 1. outer webview → inner iframe의 contentDocument 접근
6
+ * 2. data-testid="chat-input" textarea에 값 설정 (React controlled)
7
+ * 3. React Fiber에서 onSend 함수 찾아 직접 호출 (가장 확실한 방법)
8
+ * 4. Fallback: data-testid="send-button" 클릭 or Enter 키
9
+ *
10
+ * ⚠️ Cline의 send-button은 DIV 태그이며, 일반 click 이벤트로는 React가
11
+ * 전송을 처리하지 않음. Fiber onSend()를 직접 호출해야 정확하게 동작.
12
+ *
13
+ * 최종 확인: 2026-03-07
14
+ */
15
+ (async () => {
16
+ try {
17
+ const inner = document.querySelector('iframe');
18
+ const doc = inner?.contentDocument || inner?.contentWindow?.document;
19
+ if (!doc) return 'error: no doc';
20
+
21
+ // ─── 1. 입력 필드 찾기 ───
22
+ let target = doc.querySelector('[data-testid="chat-input"]');
23
+ if (!target) {
24
+ const textareas = doc.querySelectorAll('textarea');
25
+ for (const ta of textareas) {
26
+ if (ta.offsetParent !== null && ta.offsetHeight > 20) {
27
+ target = ta;
28
+ break;
29
+ }
30
+ }
31
+ }
32
+ if (!target) return 'error: no chat-input';
33
+
34
+ // ─── 2. React controlled input 값 설정 ───
35
+ const proto = inner.contentWindow?.HTMLTextAreaElement?.prototype
36
+ || HTMLTextAreaElement.prototype;
37
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
38
+
39
+ if (nativeSetter) {
40
+ nativeSetter.call(target, ${ MESSAGE });
41
+ } else {
42
+ target.value = ${ MESSAGE };
43
+ }
44
+
45
+ // React 이벤트 트리거
46
+ target.dispatchEvent(new Event('input', { bubbles: true }));
47
+ target.dispatchEvent(new Event('change', { bubbles: true }));
48
+
49
+ // React setState 반영 대기
50
+ await new Promise(r => setTimeout(r, 300));
51
+
52
+ // ─── 3. Fiber onSend 직접 호출 (최우선) ───
53
+ const allEls = doc.querySelectorAll('*');
54
+ for (const el of allEls) {
55
+ const fk = Object.keys(el).find(k => k.startsWith('__reactFiber'));
56
+ if (!fk) continue;
57
+ let fiber = el[fk];
58
+ for (let d = 0; d < 15 && fiber; d++) {
59
+ const props = fiber.memoizedProps || fiber.pendingProps;
60
+ if (props && typeof props.onSend === 'function') {
61
+ props.onSend();
62
+ return 'sent';
63
+ }
64
+ fiber = fiber.return;
65
+ }
66
+ }
67
+
68
+ // ─── 4. Fallback: send-button 클릭 ───
69
+ const sendBtn = doc.querySelector('[data-testid="send-button"]');
70
+ if (sendBtn) {
71
+ try {
72
+ const rect = sendBtn.getBoundingClientRect();
73
+ const opts = {
74
+ bubbles: true, cancelable: true, view: inner.contentWindow,
75
+ clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2
76
+ };
77
+ sendBtn.dispatchEvent(new MouseEvent('mousedown', opts));
78
+ sendBtn.dispatchEvent(new MouseEvent('mouseup', opts));
79
+ sendBtn.dispatchEvent(new MouseEvent('click', opts));
80
+ return 'sent';
81
+ } catch (e) { }
82
+ }
83
+
84
+ // ─── 5. 최후 Fallback: Enter 키 ───
85
+ target.focus();
86
+ target.dispatchEvent(new KeyboardEvent('keydown', {
87
+ key: 'Enter', code: 'Enter', keyCode: 13,
88
+ bubbles: true, cancelable: true,
89
+ }));
90
+
91
+ return 'sent';
92
+ } catch (e) {
93
+ return 'error: ' + e.message;
94
+ }
95
+ })()
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Cline v1 — switch_session (Fiber hooks → showTaskWithId gRPC)
3
+ *
4
+ * 전략:
5
+ * 1. Fiber DFS에서 taskHistory, showHistoryView 찾기
6
+ * 2. Done 클릭으로 히스토리 닫기 → showHistoryView() → 재열기
7
+ * 3. Virtuoso 렌더링 대기 (최대 5초, 500ms 간격 재시도 + scroll/resize 트리거)
8
+ * 4. 렌더된 아이템의 Fiber hooks에서 showTaskWithId 콜백 추출
9
+ * 5. showTaskWithId(taskId) 호출 → gRPC로 Task 전환
10
+ *
11
+ * 파라미터: ${ SESSION_ID } — task ID (숫자 문자열) 또는 제목
12
+ * 최종 확인: 2026-03-07
13
+ */
14
+ (async () => {
15
+ try {
16
+ const inner = document.querySelector('iframe');
17
+ const doc = inner?.contentDocument || inner?.contentWindow?.document;
18
+ if (!doc) return false;
19
+
20
+ const sessionId = ${ SESSION_ID };
21
+ if (!sessionId) return false;
22
+
23
+ const findFiberKey = (el) => Object.keys(el).find(k =>
24
+ k.startsWith('__reactFiber') || k.startsWith('__reactProps') || k.startsWith('__reactContainer'));
25
+ const getFiber = (el) => {
26
+ const fk = findFiberKey(el);
27
+ if (!fk) return null;
28
+ let fiber = el[fk];
29
+ if (fk.startsWith('__reactContainer') && fiber?._internalRoot?.current)
30
+ fiber = fiber._internalRoot.current;
31
+ return fiber;
32
+ };
33
+
34
+ // ─── 1. Fiber DFS에서 taskHistory, showHistoryView 찾기 ───
35
+ const root = doc.getElementById('root');
36
+ if (!root) return false;
37
+ const rootFk = findFiberKey(root);
38
+ if (!rootFk) return false;
39
+ let rootFiber = root[rootFk];
40
+ if (rootFk.startsWith('__reactContainer') && rootFiber?._internalRoot?.current)
41
+ rootFiber = rootFiber._internalRoot.current;
42
+
43
+ let showHistoryView = null;
44
+ let taskHistory = null;
45
+ const visited = new Set();
46
+ const scanFiber = (f, depth) => {
47
+ if (!f || depth > 60 || visited.has(f)) return;
48
+ visited.add(f);
49
+ const props = f.memoizedProps || f.pendingProps;
50
+ if (props) {
51
+ if (typeof props.showHistoryView === 'function' && !showHistoryView)
52
+ showHistoryView = props.showHistoryView;
53
+ if (props.taskHistory && Array.isArray(props.taskHistory) && !taskHistory)
54
+ taskHistory = props.taskHistory;
55
+ }
56
+ if (!taskHistory && f.memoizedState) {
57
+ let st = f.memoizedState;
58
+ while (st) {
59
+ try {
60
+ const ms = st.memoizedState;
61
+ if (ms?.taskHistory && Array.isArray(ms.taskHistory)) {
62
+ taskHistory = ms.taskHistory;
63
+ break;
64
+ }
65
+ } catch { }
66
+ st = st.next;
67
+ }
68
+ }
69
+ if (f.child) scanFiber(f.child, depth + 1);
70
+ if (f.sibling) scanFiber(f.sibling, depth + 1);
71
+ };
72
+ scanFiber(rootFiber, 0);
73
+
74
+ if (!taskHistory || taskHistory.length === 0) return false;
75
+
76
+ // 대상 task 찾기 (ID 또는 제목 매칭)
77
+ const norm = s => (s || '').trim().toLowerCase().replace(/\s+/g, ' ');
78
+ const idNorm = norm(sessionId);
79
+ let targetTask = taskHistory.find(t => String(t.id) === sessionId);
80
+ if (!targetTask) {
81
+ targetTask = taskHistory.find(t => {
82
+ const title = norm(t.task || '');
83
+ return title.includes(idNorm) || idNorm.includes(title);
84
+ });
85
+ }
86
+ if (!targetTask) return false;
87
+ const targetId = String(targetTask.id);
88
+
89
+ // ─── 2. showHistoryView가 없으면 Fiber root DFS 재시도 ───
90
+ if (!showHistoryView) {
91
+ // DOM 요소 기반 탐색 (Fiber root에서 못 찾은 경우)
92
+ for (const el of doc.querySelectorAll('*')) {
93
+ let fiber = getFiber(el);
94
+ for (let d = 0; d < 20 && fiber; d++) {
95
+ const p = fiber.memoizedProps;
96
+ if (p && typeof p.showHistoryView === 'function') {
97
+ showHistoryView = p.showHistoryView;
98
+ break;
99
+ }
100
+ fiber = fiber.return;
101
+ }
102
+ if (showHistoryView) break;
103
+ }
104
+ }
105
+ if (!showHistoryView) return false;
106
+
107
+ // ─── 3. 히스토리 뷰 토글 (닫기 → 열기) ───
108
+ // Done 버튼으로 닫기
109
+ const doneBtn = Array.from(doc.querySelectorAll('button'))
110
+ .find(b => b.textContent.trim() === 'Done' && b.offsetHeight > 0);
111
+ if (doneBtn) {
112
+ doneBtn.click();
113
+ await new Promise(r => setTimeout(r, 400));
114
+ }
115
+
116
+ // 히스토리 열기
117
+ showHistoryView();
118
+
119
+ // ─── 4. Virtuoso 렌더링 대기 (최대 5초, 500ms 간격 재시도) ───
120
+ let il = null;
121
+ for (let attempt = 0; attempt < 10; attempt++) {
122
+ await new Promise(r => setTimeout(r, 500));
123
+ // scroll + resize 트리거
124
+ const sd = doc.querySelector('.overflow-y-scroll, [data-testid=virtuoso-scroller]');
125
+ if (sd) {
126
+ sd.dispatchEvent(new Event('scroll', { bubbles: true }));
127
+ doc.defaultView?.dispatchEvent(new Event('resize'));
128
+ }
129
+ il = doc.querySelector('[data-testid=virtuoso-item-list]');
130
+ if (il && il.children.length > 0) break;
131
+ }
132
+
133
+ if (!il || il.children.length === 0) return false;
134
+
135
+ // ─── 5. Fiber hooks에서 showTaskWithId 추출 ───
136
+ let showTaskFn = null;
137
+ const findFiberKeySimple = (el) => Object.keys(el).find(k => k.startsWith('__reactFiber'));
138
+
139
+ for (const child of il.children) {
140
+ // child 또는 child 내부의 모든 요소에서 탐색
141
+ const targets = [child, ...child.querySelectorAll('*')];
142
+ for (const el of targets) {
143
+ const fk = findFiberKeySimple(el);
144
+ if (!fk) continue;
145
+ let fiber = el[fk];
146
+ for (let d = 0; d < 30 && fiber; d++) {
147
+ if (fiber.memoizedState) {
148
+ let hook = fiber.memoizedState;
149
+ let hookIdx = 0;
150
+ while (hook && hookIdx < 30) {
151
+ const ms = hook.memoizedState;
152
+ if (Array.isArray(ms) && ms.length === 2 && typeof ms[0] === 'function') {
153
+ const fnSrc = ms[0].toString();
154
+ if (fnSrc.includes('showTaskWithId')) {
155
+ showTaskFn = ms[0];
156
+ break;
157
+ }
158
+ }
159
+ hook = hook.next;
160
+ hookIdx++;
161
+ }
162
+ }
163
+ if (showTaskFn) break;
164
+ fiber = fiber.return;
165
+ }
166
+ if (showTaskFn) break;
167
+ }
168
+ if (showTaskFn) break;
169
+ }
170
+
171
+ // Fallback: Fiber DFS 전체 탐색
172
+ if (!showTaskFn) {
173
+ const visited2 = new Set();
174
+ const dfs2 = (f, depth) => {
175
+ if (!f || depth > 60 || visited2.has(f) || showTaskFn) return;
176
+ visited2.add(f);
177
+ if (f.memoizedState) {
178
+ let h = f.memoizedState; let i = 0;
179
+ while (h && i < 30) {
180
+ const ms = h.memoizedState;
181
+ if (Array.isArray(ms) && ms.length === 2 && typeof ms[0] === 'function' &&
182
+ ms[0].toString().includes('showTaskWithId')) {
183
+ showTaskFn = ms[0]; return;
184
+ }
185
+ h = h.next; i++;
186
+ }
187
+ }
188
+ if (f.child) dfs2(f.child, depth + 1);
189
+ if (f.sibling) dfs2(f.sibling, depth + 1);
190
+ };
191
+ dfs2(rootFiber, 0);
192
+ }
193
+
194
+ if (!showTaskFn) return false;
195
+
196
+ // ─── 6. showTaskWithId(targetId) 호출 ───
197
+ try {
198
+ await showTaskFn(targetId);
199
+ } catch {
200
+ return false;
201
+ }
202
+
203
+ await new Promise(r => setTimeout(r, 1500));
204
+ return true;
205
+ } catch { return false; }
206
+ })()