cursorconnect 0.1.11 → 0.1.12
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.
- package/bridge-runtime/connector-version.json +1 -1
- package/bridge-runtime/dist/agent-title-match.d.ts +4 -0
- package/bridge-runtime/dist/agent-title-match.js +61 -1
- package/bridge-runtime/dist/cdp-bridge.js +2 -1
- package/bridge-runtime/dist/chat-sync.js +3 -0
- package/bridge-runtime/dist/command-executor.d.ts +2 -0
- package/bridge-runtime/dist/command-executor.js +85 -56
- package/bridge-runtime/dist/composer-images.js +23 -6
- package/bridge-runtime/dist/cursor-window-kind.d.ts +10 -0
- package/bridge-runtime/dist/cursor-window-kind.js +10 -0
- package/bridge-runtime/dist/dom-extractor.d.ts +1 -1
- package/bridge-runtime/dist/dom-extractor.js +0 -1
- package/bridge-runtime/dist/editor-chat-list.d.ts +6 -0
- package/bridge-runtime/dist/editor-chat-list.js +79 -0
- package/bridge-runtime/dist/editor-list-sync.d.ts +3 -0
- package/bridge-runtime/dist/editor-list-sync.js +11 -0
- package/bridge-runtime/dist/editor-tab-focus-dom.d.ts +8 -0
- package/bridge-runtime/dist/editor-tab-focus-dom.js +80 -0
- package/bridge-runtime/dist/extract-page.d.ts +1 -1
- package/bridge-runtime/dist/extract-page.js +177 -30
- package/bridge-runtime/dist/generation-stop-dom.d.ts +5 -0
- package/bridge-runtime/dist/generation-stop-dom.js +67 -0
- package/bridge-runtime/dist/index.js +2 -0
- package/bridge-runtime/dist/queue-remove-dom.d.ts +11 -0
- package/bridge-runtime/dist/queue-remove-dom.js +88 -0
- package/bridge-runtime/dist/relay-upstream.js +2 -0
- package/bridge-runtime/dist/relay.js +35 -15
- package/bridge-runtime/dist/state-manager.d.ts +1 -1
- package/bridge-runtime/dist/types.d.ts +14 -0
- package/bridge-runtime/dist/window-monitor.js +6 -0
- package/bridge-runtime/selectors.json +8 -1
- package/package.json +1 -1
- package/version-policy.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"cliVersion":"0.1.
|
|
1
|
+
{"cliVersion":"0.1.12","bundledAt":"2026-05-26T12:45:47Z"}
|
|
@@ -5,6 +5,10 @@ export declare function agentTitleMatchScore(sidebarTitle: string, jsonlLabel: s
|
|
|
5
5
|
export declare function findAgentById(jsonl: AgentsIndex, agentId: string): AgentSummary | undefined;
|
|
6
6
|
/** Best JSONL agent for a Cursor sidebar row (generated title vs first user message). */
|
|
7
7
|
export declare function matchJsonlAgentForSidebarTitle(rowTitle: string, jsonl: AgentsIndex, composerIdByTitle?: Record<string, string>): AgentSummary | undefined;
|
|
8
|
+
export declare function resolveComposerUuidFromPartial(partialId: string, opts?: {
|
|
9
|
+
composerIdByTitle?: Record<string, string>;
|
|
10
|
+
agentsIndex?: AgentsIndex;
|
|
11
|
+
}): string | undefined;
|
|
8
12
|
export declare function isSyntheticAgentId(agentId: string): boolean;
|
|
9
13
|
export type ResolveJsonlOpts = {
|
|
10
14
|
title?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { basename, join } from 'path';
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
3
3
|
import { normalizeComposerTitle, titlesAlign } from './composer-title-index.js';
|
|
4
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
5
|
export function normalizeAgentTitle(title) {
|
|
5
6
|
return title
|
|
6
7
|
.trim()
|
|
@@ -83,6 +84,61 @@ export function matchJsonlAgentForSidebarTitle(rowTitle, jsonl, composerIdByTitl
|
|
|
83
84
|
}
|
|
84
85
|
return best?.agent;
|
|
85
86
|
}
|
|
87
|
+
/** Editor auxiliary tabs expose truncated `data-resource-name` — match folder prefix. */
|
|
88
|
+
function findJsonlAgentIdByFolderPrefix(projectsDir, prefix) {
|
|
89
|
+
const p = prefix.toLowerCase().replace(/[^0-9a-f-]/g, '');
|
|
90
|
+
if (p.length < 32)
|
|
91
|
+
return null;
|
|
92
|
+
if (!existsSync(projectsDir))
|
|
93
|
+
return null;
|
|
94
|
+
for (const project of readdirSync(projectsDir, { withFileTypes: true })) {
|
|
95
|
+
if (!project.isDirectory())
|
|
96
|
+
continue;
|
|
97
|
+
const base = join(projectsDir, project.name, 'agent-transcripts');
|
|
98
|
+
if (!existsSync(base))
|
|
99
|
+
continue;
|
|
100
|
+
const hit = walkFolderPrefix(base, p, 0);
|
|
101
|
+
if (hit)
|
|
102
|
+
return hit;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function walkFolderPrefix(dir, prefix, depth) {
|
|
107
|
+
if (depth > 5 || !existsSync(dir))
|
|
108
|
+
return null;
|
|
109
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
110
|
+
if (!entry.isDirectory())
|
|
111
|
+
continue;
|
|
112
|
+
const name = entry.name.toLowerCase();
|
|
113
|
+
if (name.startsWith(prefix) || prefix.startsWith(name)) {
|
|
114
|
+
const jsonl = join(dir, entry.name, `${entry.name}.jsonl`);
|
|
115
|
+
if (existsSync(jsonl))
|
|
116
|
+
return entry.name;
|
|
117
|
+
}
|
|
118
|
+
const nested = walkFolderPrefix(join(dir, entry.name), prefix, depth + 1);
|
|
119
|
+
if (nested)
|
|
120
|
+
return nested;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
export function resolveComposerUuidFromPartial(partialId, opts) {
|
|
125
|
+
if (UUID_RE.test(partialId))
|
|
126
|
+
return partialId;
|
|
127
|
+
const p = partialId.toLowerCase();
|
|
128
|
+
if (p.length < 32 || !/^[0-9a-f-]/.test(p))
|
|
129
|
+
return undefined;
|
|
130
|
+
for (const uuid of Object.values(opts?.composerIdByTitle ?? {})) {
|
|
131
|
+
if (uuid.toLowerCase().startsWith(p))
|
|
132
|
+
return uuid;
|
|
133
|
+
}
|
|
134
|
+
for (const repo of opts?.agentsIndex?.repos ?? []) {
|
|
135
|
+
for (const agent of repo.agents) {
|
|
136
|
+
if (agent.id.toLowerCase().startsWith(p))
|
|
137
|
+
return agent.id;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
86
142
|
function findJsonlByAgentId(projectsDir, agentId) {
|
|
87
143
|
if (!existsSync(projectsDir))
|
|
88
144
|
return null;
|
|
@@ -169,7 +225,6 @@ function collectJsonlAgents(projectsDir) {
|
|
|
169
225
|
}
|
|
170
226
|
return out;
|
|
171
227
|
}
|
|
172
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
173
228
|
export function isSyntheticAgentId(agentId) {
|
|
174
229
|
return (!agentId ||
|
|
175
230
|
/^tab-\d+$/.test(agentId) ||
|
|
@@ -180,6 +235,11 @@ export function isSyntheticAgentId(agentId) {
|
|
|
180
235
|
export function resolveJsonlAgentId(projectsDir, agentId, opts) {
|
|
181
236
|
if (findJsonlByAgentId(projectsDir, agentId))
|
|
182
237
|
return agentId;
|
|
238
|
+
const fromPartial = resolveComposerUuidFromPartial(agentId, {
|
|
239
|
+
composerIdByTitle: opts?.composerIdByTitle,
|
|
240
|
+
}) ?? findJsonlAgentIdByFolderPrefix(projectsDir, agentId);
|
|
241
|
+
if (fromPartial && findJsonlByAgentId(projectsDir, fromPartial))
|
|
242
|
+
return fromPartial;
|
|
183
243
|
const title = opts?.title?.trim();
|
|
184
244
|
const map = opts?.composerIdByTitle;
|
|
185
245
|
if (title && map) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import { CdpClient } from './cdp-client.js';
|
|
3
|
+
import { annotateWindowKind } from './cursor-window-kind.js';
|
|
3
4
|
import { detectForegroundTargetId } from './focused-window.js';
|
|
4
5
|
export class CDPBridge extends EventEmitter {
|
|
5
6
|
config;
|
|
@@ -41,7 +42,7 @@ export class CDPBridge extends EventEmitter {
|
|
|
41
42
|
const targets = (await res.json());
|
|
42
43
|
this.windows = targets
|
|
43
44
|
.filter((t) => t.type === 'page' && t.url.includes('workbench'))
|
|
44
|
-
.map((t) => ({
|
|
45
|
+
.map((t) => annotateWindowKind({
|
|
45
46
|
id: t.id,
|
|
46
47
|
title: parseWindowTitle(t.title),
|
|
47
48
|
workspace: parseWindowTitle(t.title),
|
|
@@ -19,6 +19,9 @@ function lookupComposerIdByTitle(title, map) {
|
|
|
19
19
|
}
|
|
20
20
|
export function resolveCursorActiveComposerId(state) {
|
|
21
21
|
const activeTab = state.tabs.find((t) => t.active);
|
|
22
|
+
if (activeTab?.composerId && isComposerUuid(activeTab.composerId)) {
|
|
23
|
+
return activeTab.composerId;
|
|
24
|
+
}
|
|
22
25
|
if (isComposerUuid(state.activeComposerId))
|
|
23
26
|
return state.activeComposerId;
|
|
24
27
|
const fromTabTitle = lookupComposerIdByTitle(activeTab?.title, state.composerIdByTitle);
|
|
@@ -20,6 +20,8 @@ export declare class CommandExecutor {
|
|
|
20
20
|
private clearComposerAttachments;
|
|
21
21
|
/** Empty Cursor composer (DOM + keyboard). */
|
|
22
22
|
private clearComposerInput;
|
|
23
|
+
/** Replace composer text (after unqueue / restore). */
|
|
24
|
+
private setComposerInputText;
|
|
23
25
|
/** Put saved Cursor draft back after send from Connect (avoids merging with phone text). */
|
|
24
26
|
private restoreComposerDraft;
|
|
25
27
|
private focusComposerInput;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { clearComposerAttachmentsScript, clearComposerInputScript, extractComposerDraftImagesScript, } from './composer-images.js';
|
|
3
|
+
import { generationStopDomAction } from './generation-stop-dom.js';
|
|
4
|
+
import { focusEditorAuxiliaryTab } from './editor-tab-focus-dom.js';
|
|
5
|
+
import { queueRemoveDomAction } from './queue-remove-dom.js';
|
|
3
6
|
import { isPathUnderUploadDir } from './image-upload-store.js';
|
|
4
7
|
const RETRY_DELAY_MS = 400;
|
|
5
8
|
export class CommandExecutor {
|
|
@@ -37,8 +40,38 @@ export class CommandExecutor {
|
|
|
37
40
|
return await this.approveAll(cmd);
|
|
38
41
|
case 'start_multitasking':
|
|
39
42
|
return await this.clickByLabel(cmd, 'Start Multitasking');
|
|
40
|
-
case 'remove_queued':
|
|
41
|
-
|
|
43
|
+
case 'remove_queued': {
|
|
44
|
+
const client = this.client;
|
|
45
|
+
if (!client)
|
|
46
|
+
return { id: cmd.id, ok: false, error: 'No CDP client' };
|
|
47
|
+
if (cmd.agentId) {
|
|
48
|
+
const focused = await this.focusAgent(cmd.agentId, cmd.id, cmd.tabTitle);
|
|
49
|
+
if (!focused.ok && !cmd.text?.trim()) {
|
|
50
|
+
return { id: cmd.id, ok: false, error: focused.error };
|
|
51
|
+
}
|
|
52
|
+
if (focused.ok)
|
|
53
|
+
await sleep(350);
|
|
54
|
+
}
|
|
55
|
+
const removed = (await client.callFunction(queueRemoveDomAction, 'remove', { text: cmd.text?.trim(), selectorPath: cmd.selectorPath }));
|
|
56
|
+
if (!removed?.ok) {
|
|
57
|
+
return {
|
|
58
|
+
id: cmd.id,
|
|
59
|
+
ok: false,
|
|
60
|
+
error: removed?.reason ?? 'Queue item not found in Cursor',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
await sleep(350);
|
|
64
|
+
const restoreText = cmd.text?.trim();
|
|
65
|
+
if (restoreText) {
|
|
66
|
+
await this.setComposerInputText(restoreText);
|
|
67
|
+
}
|
|
68
|
+
const composerDraft = await this.readComposerDraft();
|
|
69
|
+
return {
|
|
70
|
+
id: cmd.id,
|
|
71
|
+
ok: true,
|
|
72
|
+
composerDraft: restoreText || composerDraft || undefined,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
42
75
|
case 'stop_agent':
|
|
43
76
|
return await this.stopAgent(cmd);
|
|
44
77
|
case 'click_selector':
|
|
@@ -76,12 +109,10 @@ export class CommandExecutor {
|
|
|
76
109
|
}
|
|
77
110
|
const inputStrategies = this.selectors.chatInput.strategies;
|
|
78
111
|
const submitStrategies = this.selectors.submitButton.strategies;
|
|
79
|
-
const stopStrategies = this.selectors.stopButton.strategies;
|
|
80
112
|
const wantQueue = cmd.queue === true || cmd.queue === 'true' || cmd.queue === '1';
|
|
81
113
|
const prep = (await client.evaluate(`
|
|
82
114
|
(() => {
|
|
83
115
|
const inputSels = ${JSON.stringify(inputStrategies)};
|
|
84
|
-
const stopSels = ${JSON.stringify(stopStrategies)};
|
|
85
116
|
const scopes = [
|
|
86
117
|
document.querySelector('[class*="agent-panel"]'),
|
|
87
118
|
document.querySelector('[class*="composer-panel"]'),
|
|
@@ -108,29 +139,15 @@ export class CommandExecutor {
|
|
|
108
139
|
input.focus();
|
|
109
140
|
(input).click();
|
|
110
141
|
|
|
111
|
-
|
|
112
|
-
for (const scope of scopes) {
|
|
113
|
-
for (const sel of stopSels) {
|
|
114
|
-
try {
|
|
115
|
-
const btn = scope.querySelector(sel);
|
|
116
|
-
if (!btn) continue;
|
|
117
|
-
const disabled =
|
|
118
|
-
btn.hasAttribute('disabled') || btn.getAttribute('aria-disabled') === 'true';
|
|
119
|
-
if (disabled) continue;
|
|
120
|
-
agentBusy = true;
|
|
121
|
-
break;
|
|
122
|
-
} catch {}
|
|
123
|
-
}
|
|
124
|
-
if (agentBusy) break;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return { ok: true, agentBusy };
|
|
142
|
+
return { ok: true, agentBusy: false };
|
|
128
143
|
})()
|
|
129
144
|
`));
|
|
130
145
|
if (!prep?.ok) {
|
|
131
146
|
return { id: cmd.id, ok: false, error: prep?.error ?? 'Chat input not found' };
|
|
132
147
|
}
|
|
133
|
-
const
|
|
148
|
+
const agentBusy = (await client.callFunction(generationStopDomAction, 'has')) ===
|
|
149
|
+
true;
|
|
150
|
+
const shouldQueue = wantQueue || agentBusy;
|
|
134
151
|
const savedDraft = await this.readComposerDraft();
|
|
135
152
|
const savedDraftImages = await this.readComposerDraftImages();
|
|
136
153
|
const sentFromConnect = Boolean(text || imagePaths.length > 0);
|
|
@@ -215,18 +232,32 @@ export class CommandExecutor {
|
|
|
215
232
|
return { id: commandId, ok: false, error: 'No client or agentId' };
|
|
216
233
|
}
|
|
217
234
|
const tabSelectors = this.selectors.chatTabList.strategies;
|
|
235
|
+
const editorFocused = (await client.callFunction(focusEditorAuxiliaryTab, { title: chatTitle ?? '', agentId })) === true;
|
|
236
|
+
if (editorFocused) {
|
|
237
|
+
return { id: commandId, ok: true };
|
|
238
|
+
}
|
|
218
239
|
const ok = await client.evaluate(`
|
|
219
240
|
(() => {
|
|
220
241
|
const wantId = ${JSON.stringify(agentId)};
|
|
221
242
|
const wantTitle = ${JSON.stringify(chatTitle ?? '')};
|
|
222
243
|
const selectors = ${JSON.stringify(tabSelectors)};
|
|
223
|
-
const
|
|
244
|
+
const stripEditorSuffix = (s) =>
|
|
245
|
+
(s || '').replace(/,\\s*Chat Editors:\\s*Editor Group\\s*\\d+\\s*$/i, '').trim();
|
|
246
|
+
const norm = (s) =>
|
|
247
|
+
stripEditorSuffix(s)
|
|
248
|
+
.replace(/\\s+/g, ' ')
|
|
249
|
+
.replace(/\\d+\\s*(?:s|m|h|d|w)\\b/gi, '')
|
|
250
|
+
.replace(/\\d+[smhdw]$/i, '')
|
|
251
|
+
.trim()
|
|
252
|
+
.toLowerCase();
|
|
224
253
|
const titleNorm = wantTitle ? norm(wantTitle) : '';
|
|
225
254
|
const wantNorm = wantId.startsWith('title:') ? wantId.slice(6) : norm(wantId);
|
|
226
255
|
const tryClick = (el) => {
|
|
227
256
|
if (!el) return false;
|
|
228
257
|
el.scrollIntoView({ block: 'nearest', behavior: 'instant' });
|
|
229
|
-
|
|
258
|
+
el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
|
|
259
|
+
el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
|
|
260
|
+
el.click();
|
|
230
261
|
return true;
|
|
231
262
|
};
|
|
232
263
|
const matchBtn = (btn) => {
|
|
@@ -407,37 +438,7 @@ export class CommandExecutor {
|
|
|
407
438
|
return { id: cmd.id, ok: false, error: focused.error };
|
|
408
439
|
await sleep(350);
|
|
409
440
|
}
|
|
410
|
-
const
|
|
411
|
-
const ok = await client.evaluate(`
|
|
412
|
-
(() => {
|
|
413
|
-
const strategies = ${JSON.stringify(strategies)};
|
|
414
|
-
const scopes = [
|
|
415
|
-
document.querySelector('[class*="agent-panel"]'),
|
|
416
|
-
document.querySelector('[class*="composer-panel"]'),
|
|
417
|
-
document.querySelector('.composer-bar'),
|
|
418
|
-
document.querySelector('[class*="composer-bar"]'),
|
|
419
|
-
document,
|
|
420
|
-
].filter(Boolean);
|
|
421
|
-
const tryClick = (btn) => {
|
|
422
|
-
if (!btn) return false;
|
|
423
|
-
const disabled =
|
|
424
|
-
btn.hasAttribute('disabled') || btn.getAttribute('aria-disabled') === 'true';
|
|
425
|
-
if (disabled) return false;
|
|
426
|
-
btn.scrollIntoView({ block: 'center' });
|
|
427
|
-
(btn).click();
|
|
428
|
-
return true;
|
|
429
|
-
};
|
|
430
|
-
for (const scope of scopes) {
|
|
431
|
-
for (const sel of strategies) {
|
|
432
|
-
try {
|
|
433
|
-
const btn = scope.querySelector(sel);
|
|
434
|
-
if (tryClick(btn)) return true;
|
|
435
|
-
} catch {}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return false;
|
|
439
|
-
})()
|
|
440
|
-
`);
|
|
441
|
+
const ok = (await client.callFunction(generationStopDomAction, 'click')) === true;
|
|
441
442
|
if (!ok) {
|
|
442
443
|
return { id: cmd.id, ok: false, error: 'Stop button not found (agent not running?)' };
|
|
443
444
|
}
|
|
@@ -492,10 +493,38 @@ export class CommandExecutor {
|
|
|
492
493
|
if (!(await this.focusComposerInput()))
|
|
493
494
|
return;
|
|
494
495
|
await sleep(80);
|
|
495
|
-
|
|
496
|
+
// CDP modifiers: 4=Meta (⌘ on Mac), 2=Ctrl — Lexical often ignores Ctrl+A on macOS.
|
|
497
|
+
await client.pressKey('a', 'KeyA', 65, 4);
|
|
496
498
|
await sleep(40);
|
|
499
|
+
let left = (await this.readComposerDraft())?.trim() ?? '';
|
|
500
|
+
if (left) {
|
|
501
|
+
await client.pressKey('a', 'KeyA', 65, 2);
|
|
502
|
+
await sleep(40);
|
|
503
|
+
}
|
|
497
504
|
await client.pressKey('Backspace', 'Backspace', 8);
|
|
498
505
|
await sleep(80);
|
|
506
|
+
left = (await this.readComposerDraft())?.trim() ?? '';
|
|
507
|
+
if (left) {
|
|
508
|
+
try {
|
|
509
|
+
await client.evaluate(clearComposerInputScript(inputStrategies));
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
/* best-effort */
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/** Replace composer text (after unqueue / restore). */
|
|
517
|
+
async setComposerInputText(text) {
|
|
518
|
+
const draft = text.trim();
|
|
519
|
+
if (!draft)
|
|
520
|
+
return;
|
|
521
|
+
await this.clearComposerInput();
|
|
522
|
+
const client = this.client;
|
|
523
|
+
if (!client || !(await this.focusComposerInput()))
|
|
524
|
+
return;
|
|
525
|
+
await sleep(80);
|
|
526
|
+
await client.typeText(draft);
|
|
527
|
+
await sleep(120);
|
|
499
528
|
}
|
|
500
529
|
/** Put saved Cursor draft back after send from Connect (avoids merging with phone text). */
|
|
501
530
|
async restoreComposerDraft(text, imagePaths) {
|
|
@@ -78,24 +78,41 @@ export function clearComposerInputScript(inputSelectors) {
|
|
|
78
78
|
input.scrollIntoView({ block: 'center' });
|
|
79
79
|
input.focus();
|
|
80
80
|
input.click();
|
|
81
|
+
const root = input.querySelector('[data-lexical-editor="true"]') || input;
|
|
81
82
|
try {
|
|
82
83
|
const sel = window.getSelection();
|
|
83
84
|
const range = document.createRange();
|
|
84
|
-
range.selectNodeContents(
|
|
85
|
+
range.selectNodeContents(root);
|
|
85
86
|
sel?.removeAllRanges();
|
|
86
87
|
sel?.addRange(range);
|
|
87
88
|
document.execCommand('selectAll', false, null);
|
|
88
89
|
document.execCommand('delete', false, null);
|
|
89
90
|
} catch {}
|
|
90
|
-
const
|
|
91
|
+
const fireInput = () => {
|
|
92
|
+
root.dispatchEvent(
|
|
93
|
+
new InputEvent('input', { bubbles: true, inputType: 'deleteContentBackward' })
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
let left = (root.innerText || root.textContent || '')
|
|
91
97
|
.replace(/\\u200b/g, ' ')
|
|
92
98
|
.replace(/\\s+/g, ' ')
|
|
93
99
|
.trim();
|
|
94
100
|
if (left) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
root.textContent = '';
|
|
102
|
+
fireInput();
|
|
103
|
+
left = (root.innerText || root.textContent || '').trim();
|
|
104
|
+
}
|
|
105
|
+
if (left) {
|
|
106
|
+
try {
|
|
107
|
+
root.dispatchEvent(
|
|
108
|
+
new KeyboardEvent('keydown', { key: 'a', code: 'KeyA', metaKey: true, bubbles: true })
|
|
109
|
+
);
|
|
110
|
+
root.dispatchEvent(
|
|
111
|
+
new KeyboardEvent('keydown', { key: 'a', code: 'KeyA', ctrlKey: true, bubbles: true })
|
|
112
|
+
);
|
|
113
|
+
document.execCommand('delete', false, null);
|
|
114
|
+
fireInput();
|
|
115
|
+
} catch {}
|
|
99
116
|
}
|
|
100
117
|
return true;
|
|
101
118
|
})()`;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CursorWindowKind } from './types.js';
|
|
2
|
+
/** CDP target title for the dedicated Agents window (not editor auxiliary bar). */
|
|
3
|
+
export declare function isAgentsWindowTitle(title: string): boolean;
|
|
4
|
+
export declare function windowKindFromTitle(title: string): CursorWindowKind;
|
|
5
|
+
export declare function annotateWindowKind<T extends {
|
|
6
|
+
title: string;
|
|
7
|
+
kind?: CursorWindowKind;
|
|
8
|
+
}>(win: T): T & {
|
|
9
|
+
kind: CursorWindowKind;
|
|
10
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** CDP target title for the dedicated Agents window (not editor auxiliary bar). */
|
|
2
|
+
export function isAgentsWindowTitle(title) {
|
|
3
|
+
return /^cursor agents$/i.test(title.trim());
|
|
4
|
+
}
|
|
5
|
+
export function windowKindFromTitle(title) {
|
|
6
|
+
return isAgentsWindowTitle(title) ? 'agents' : 'editor';
|
|
7
|
+
}
|
|
8
|
+
export function annotateWindowKind(win) {
|
|
9
|
+
return { ...win, kind: win.kind ?? windowKindFromTitle(win.title) };
|
|
10
|
+
}
|
|
@@ -7,7 +7,7 @@ export declare class DOMExtractor {
|
|
|
7
7
|
private onState;
|
|
8
8
|
private containerMissStreak;
|
|
9
9
|
private composerTitleIndex;
|
|
10
|
-
constructor(selectors: SelectorConfig, onState: (state: CursorState | null, error?: string) => void);
|
|
10
|
+
constructor(selectors: SelectorConfig, onState: (state: Partial<CursorState> | null, error?: string) => void);
|
|
11
11
|
start(client: CdpClient, intervalMs: number): void;
|
|
12
12
|
stop(): void;
|
|
13
13
|
pollNow(): void;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CursorState, RepoGroup } from './types.js';
|
|
2
|
+
export type CursorListMode = 'agents-window' | 'editor';
|
|
3
|
+
export declare function resolveCursorListMode(state: CursorState): CursorListMode;
|
|
4
|
+
/** Editor layout: one Cursor editor window → repo folder; auxiliary-bar tabs → chats. */
|
|
5
|
+
export declare function buildEditorRepos(state: CursorState): RepoGroup[];
|
|
6
|
+
export declare function computeEditorListPatch(state: CursorState): Pick<CursorState, 'editorRepos' | 'cursorListMode'> | null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { normalizeAgentTitle, resolveComposerUuidFromPartial, } from './agent-title-match.js';
|
|
2
|
+
import { windowKindFromTitle } from './cursor-window-kind.js';
|
|
3
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
function resolveTabComposerId(tab, state) {
|
|
5
|
+
const raw = tab.composerId || tab.id;
|
|
6
|
+
if (UUID_RE.test(raw))
|
|
7
|
+
return raw;
|
|
8
|
+
const fromTitle = state.composerIdByTitle?.[normalizeAgentTitle(tab.title)];
|
|
9
|
+
if (fromTitle && UUID_RE.test(fromTitle))
|
|
10
|
+
return fromTitle;
|
|
11
|
+
const fromPartial = resolveComposerUuidFromPartial(raw, {
|
|
12
|
+
composerIdByTitle: state.composerIdByTitle,
|
|
13
|
+
});
|
|
14
|
+
if (fromPartial)
|
|
15
|
+
return fromPartial;
|
|
16
|
+
return raw;
|
|
17
|
+
}
|
|
18
|
+
export function resolveCursorListMode(state) {
|
|
19
|
+
const windows = state.windows ?? [];
|
|
20
|
+
const active = windows.find((w) => w.id === state.activeWindowId);
|
|
21
|
+
const activeKind = active?.kind ?? (active ? windowKindFromTitle(active.title) : undefined);
|
|
22
|
+
if (activeKind === 'editor')
|
|
23
|
+
return 'editor';
|
|
24
|
+
if (activeKind === 'agents')
|
|
25
|
+
return 'agents-window';
|
|
26
|
+
const hasEditor = windows.some((w) => (w.kind ?? windowKindFromTitle(w.title)) === 'editor');
|
|
27
|
+
if (hasEditor && !windows.some((w) => (w.kind ?? windowKindFromTitle(w.title)) === 'agents')) {
|
|
28
|
+
return 'editor';
|
|
29
|
+
}
|
|
30
|
+
return 'agents-window';
|
|
31
|
+
}
|
|
32
|
+
const EDITOR_SKIP_TAB = /^(new agent|marketplace)$/i;
|
|
33
|
+
function tabToAgent(tab, winId, state) {
|
|
34
|
+
const isHome = winId === state.activeWindowId;
|
|
35
|
+
const composerId = resolveTabComposerId(tab, state);
|
|
36
|
+
let isWorking = tab.isWorking === true;
|
|
37
|
+
// Home window only: DOM may set isWorking on active tab after detectAgentWorking.
|
|
38
|
+
if (!isWorking && tab.active && isHome && state.agentWorking === true) {
|
|
39
|
+
isWorking = true;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
id: composerId,
|
|
43
|
+
title: tab.title,
|
|
44
|
+
updatedAt: Date.now(),
|
|
45
|
+
messageCount: 0,
|
|
46
|
+
isWorking,
|
|
47
|
+
hasUnread: tab.hasUnread,
|
|
48
|
+
needsAttention: tab.needsAttention,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Editor layout: one Cursor editor window → repo folder; auxiliary-bar tabs → chats. */
|
|
52
|
+
export function buildEditorRepos(state) {
|
|
53
|
+
const editorWindows = (state.windows ?? []).filter((w) => (w.kind ?? windowKindFromTitle(w.title)) === 'editor');
|
|
54
|
+
if (!editorWindows.length)
|
|
55
|
+
return [];
|
|
56
|
+
const snapById = new Map((state.windowSnapshots ?? []).map((s) => [s.windowId, s]));
|
|
57
|
+
const homeId = state.activeWindowId;
|
|
58
|
+
return editorWindows.map((win) => {
|
|
59
|
+
const isHome = win.id === homeId;
|
|
60
|
+
const tabs = (isHome ? state.tabs ?? [] : snapById.get(win.id)?.tabs ?? []).filter((t) => t.title?.trim() && !EDITOR_SKIP_TAB.test(t.title.trim()));
|
|
61
|
+
return {
|
|
62
|
+
id: win.id,
|
|
63
|
+
name: win.title,
|
|
64
|
+
path: '',
|
|
65
|
+
agents: tabs.map((tab) => tabToAgent(tab, win.id, state)),
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export function computeEditorListPatch(state) {
|
|
70
|
+
const editorRepos = buildEditorRepos(state);
|
|
71
|
+
const cursorListMode = resolveCursorListMode(state);
|
|
72
|
+
const prevRepos = state.editorRepos ?? [];
|
|
73
|
+
const prevMode = state.cursorListMode;
|
|
74
|
+
if (prevMode === cursorListMode &&
|
|
75
|
+
JSON.stringify(prevRepos) === JSON.stringify(editorRepos)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return { editorRepos, cursorListMode };
|
|
79
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { computeEditorListPatch } from './editor-chat-list.js';
|
|
2
|
+
/** Keeps `editorRepos` + `cursorListMode` in sync without touching agents:index flow. */
|
|
3
|
+
export function installEditorListSync(stateManager) {
|
|
4
|
+
const sync = () => {
|
|
5
|
+
const patch = computeEditorListPatch(stateManager.getState());
|
|
6
|
+
if (patch)
|
|
7
|
+
stateManager.patchNow(patch);
|
|
8
|
+
};
|
|
9
|
+
stateManager.on('state:patch', sync);
|
|
10
|
+
stateManager.on('state:full', sync);
|
|
11
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus an editor auxiliary-bar chat tab (VS Code workbench).
|
|
3
|
+
* CDP callFunction entry — keep helpers inside.
|
|
4
|
+
*/
|
|
5
|
+
export function focusEditorAuxiliaryTab(opts) {
|
|
6
|
+
const wantTitle = (opts.title ?? '').trim();
|
|
7
|
+
const wantId = (opts.agentId ?? '').trim();
|
|
8
|
+
const stripEditorSuffix = (s) => s.replace(/,\s*Chat Editors:\s*Editor Group\s*\d+\s*$/i, '').trim();
|
|
9
|
+
const norm = (s) => stripEditorSuffix(s)
|
|
10
|
+
.replace(/\s+/g, ' ')
|
|
11
|
+
.replace(/\d+\s*(?:s|m|h|d|w)\b/gi, '')
|
|
12
|
+
.replace(/\d+[smhdw]$/i, '')
|
|
13
|
+
.trim()
|
|
14
|
+
.toLowerCase();
|
|
15
|
+
const titleNorm = wantTitle ? norm(wantTitle) : '';
|
|
16
|
+
const wantLower = wantId.toLowerCase();
|
|
17
|
+
const aux = document.querySelector('#workbench\\.parts\\.auxiliarybar');
|
|
18
|
+
if (!aux || document.querySelector('[class*="agent-panel"]'))
|
|
19
|
+
return false;
|
|
20
|
+
function titleMatches(aria, visible) {
|
|
21
|
+
if (!titleNorm)
|
|
22
|
+
return false;
|
|
23
|
+
const ariaNorm = aria ? norm(aria) : '';
|
|
24
|
+
const visibleNorm = visible ? norm(visible) : '';
|
|
25
|
+
const candidates = [ariaNorm, visibleNorm].filter(Boolean);
|
|
26
|
+
for (const c of candidates) {
|
|
27
|
+
if (c === titleNorm)
|
|
28
|
+
return true;
|
|
29
|
+
if (c.length >= 8 && titleNorm.length >= 8 && (c.includes(titleNorm) || titleNorm.includes(c))) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
function idMatches(resource) {
|
|
36
|
+
if (wantLower.length < 8 || !resource)
|
|
37
|
+
return false;
|
|
38
|
+
const r = resource.toLowerCase();
|
|
39
|
+
if (r === wantLower)
|
|
40
|
+
return true;
|
|
41
|
+
if (wantLower.length >= 32 && (r.startsWith(wantLower) || wantLower.startsWith(r)))
|
|
42
|
+
return true;
|
|
43
|
+
if (wantLower.length >= 8 && r.length >= 32 && r.startsWith(wantLower))
|
|
44
|
+
return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
function activateTab(tab) {
|
|
48
|
+
const el = tab;
|
|
49
|
+
el.scrollIntoView({ block: 'nearest', behavior: 'instant' });
|
|
50
|
+
const label = tab.querySelector('.label-name, .tab-label, [class*="label-name"]') || tab;
|
|
51
|
+
const target = label ?? el;
|
|
52
|
+
target.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
|
|
53
|
+
target.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
|
|
54
|
+
target.click();
|
|
55
|
+
}
|
|
56
|
+
const tabs = aux.querySelectorAll('div.tab[role="tab"]');
|
|
57
|
+
for (const tab of tabs) {
|
|
58
|
+
const aria = tab.getAttribute('aria-label') || '';
|
|
59
|
+
const visible = (tab.textContent || '').trim();
|
|
60
|
+
const resource = tab.getAttribute('data-resource-name') || '';
|
|
61
|
+
if (!titleMatches(aria, visible) && !idMatches(resource))
|
|
62
|
+
continue;
|
|
63
|
+
activateTab(tab);
|
|
64
|
+
const active = aux.querySelector('div.tab[role="tab"].active, div.tab[role="tab"][aria-selected="true"]');
|
|
65
|
+
if (!active)
|
|
66
|
+
return true;
|
|
67
|
+
const activeAria = active.getAttribute('aria-label') || '';
|
|
68
|
+
const activeVisible = (active.textContent || '').trim();
|
|
69
|
+
if (titleNorm && !titleMatches(activeAria, activeVisible)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (wantLower.length >= 32 && resource) {
|
|
73
|
+
const activeRes = active.getAttribute('data-resource-name') || '';
|
|
74
|
+
if (!idMatches(activeRes) && activeRes.toLowerCase() !== wantLower)
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
@@ -3,5 +3,5 @@ import type { ExtractedPageState, SelectorConfig } from './types.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Runs inside Cursor renderer — must be self-contained.
|
|
5
5
|
*/
|
|
6
|
-
export declare function extractionFunction(containerSelectors: string[], tabSelectors: string[], inputSelectors: string[], approveSelectors: string[], approveTextMatch: string[], rejectSelectors: string[], rejectTextMatch: string[]): ExtractedPageState | null;
|
|
6
|
+
export declare function extractionFunction(containerSelectors: string[], tabSelectors: string[], editorAuxiliaryTabSelectors: string[], inputSelectors: string[], approveSelectors: string[], approveTextMatch: string[], rejectSelectors: string[], rejectTextMatch: string[]): ExtractedPageState | null;
|
|
7
7
|
export declare function extractPageState(client: CdpClient, selectors: SelectorConfig): Promise<ExtractedPageState | null>;
|