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.
Files changed (33) hide show
  1. package/bridge-runtime/connector-version.json +1 -1
  2. package/bridge-runtime/dist/agent-title-match.d.ts +4 -0
  3. package/bridge-runtime/dist/agent-title-match.js +61 -1
  4. package/bridge-runtime/dist/cdp-bridge.js +2 -1
  5. package/bridge-runtime/dist/chat-sync.js +3 -0
  6. package/bridge-runtime/dist/command-executor.d.ts +2 -0
  7. package/bridge-runtime/dist/command-executor.js +85 -56
  8. package/bridge-runtime/dist/composer-images.js +23 -6
  9. package/bridge-runtime/dist/cursor-window-kind.d.ts +10 -0
  10. package/bridge-runtime/dist/cursor-window-kind.js +10 -0
  11. package/bridge-runtime/dist/dom-extractor.d.ts +1 -1
  12. package/bridge-runtime/dist/dom-extractor.js +0 -1
  13. package/bridge-runtime/dist/editor-chat-list.d.ts +6 -0
  14. package/bridge-runtime/dist/editor-chat-list.js +79 -0
  15. package/bridge-runtime/dist/editor-list-sync.d.ts +3 -0
  16. package/bridge-runtime/dist/editor-list-sync.js +11 -0
  17. package/bridge-runtime/dist/editor-tab-focus-dom.d.ts +8 -0
  18. package/bridge-runtime/dist/editor-tab-focus-dom.js +80 -0
  19. package/bridge-runtime/dist/extract-page.d.ts +1 -1
  20. package/bridge-runtime/dist/extract-page.js +177 -30
  21. package/bridge-runtime/dist/generation-stop-dom.d.ts +5 -0
  22. package/bridge-runtime/dist/generation-stop-dom.js +67 -0
  23. package/bridge-runtime/dist/index.js +2 -0
  24. package/bridge-runtime/dist/queue-remove-dom.d.ts +11 -0
  25. package/bridge-runtime/dist/queue-remove-dom.js +88 -0
  26. package/bridge-runtime/dist/relay-upstream.js +2 -0
  27. package/bridge-runtime/dist/relay.js +35 -15
  28. package/bridge-runtime/dist/state-manager.d.ts +1 -1
  29. package/bridge-runtime/dist/types.d.ts +14 -0
  30. package/bridge-runtime/dist/window-monitor.js +6 -0
  31. package/bridge-runtime/selectors.json +8 -1
  32. package/package.json +1 -1
  33. package/version-policy.json +5 -5
@@ -1 +1 @@
1
- {"cliVersion":"0.1.11","bundledAt":"2026-05-26T11:25:16Z"}
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
- return await this.clickSelector(cmd, cmd.selectorPath);
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
- let agentBusy = false;
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 shouldQueue = wantQueue || prep.agentBusy === true;
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 norm = (s) => (s || '').trim().replace(/\\s+/g, ' ').replace(/\\d+\\s*(?:s|m|h|d|w)\\b/gi, '').replace(/\\d+[smhdw]$/i, '').trim().toLowerCase();
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
- (el).click();
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 strategies = this.selectors.stopButton.strategies;
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
- await client.pressKey('a', 'KeyA', 65, 2);
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(input);
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 left = (input.innerText || input.textContent || '')
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
- input.textContent = '';
96
- input.dispatchEvent(
97
- new InputEvent('input', { bubbles: true, inputType: 'deleteContentBackward' })
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;
@@ -54,7 +54,6 @@ export class DOMExtractor {
54
54
  ...partial,
55
55
  composerIdByTitle: this.composerTitleIndex.snapshot(),
56
56
  pendingApprovals: partial.pendingApprovals ?? [],
57
- windows: [],
58
57
  updatedAt: Date.now(),
59
58
  });
60
59
  }
@@ -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,3 @@
1
+ import type { StateManager } from './state-manager.js';
2
+ /** Keeps `editorRepos` + `cursorListMode` in sync without touching agents:index flow. */
3
+ export declare function installEditorListSync(stateManager: StateManager): void;
@@ -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,8 @@
1
+ /**
2
+ * Focus an editor auxiliary-bar chat tab (VS Code workbench).
3
+ * CDP callFunction entry — keep helpers inside.
4
+ */
5
+ export declare function focusEditorAuxiliaryTab(opts: {
6
+ title?: string;
7
+ agentId?: string;
8
+ }): boolean;
@@ -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>;