cursorconnect 0.1.10 → 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 (45) 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/dist/bridge-build.js +2 -1
  33. package/dist/bundled-bridge-check.js +2 -3
  34. package/dist/diagnose.js +26 -23
  35. package/dist/i18n.js +50 -0
  36. package/dist/index.js +31 -47
  37. package/dist/launch.js +9 -8
  38. package/dist/print-pairing.js +8 -7
  39. package/dist/run-service.js +5 -4
  40. package/dist/startup-check.js +32 -23
  41. package/dist/version-check.js +7 -3
  42. package/locales/en.json +128 -0
  43. package/locales/ru.json +128 -0
  44. package/package.json +2 -1
  45. package/version-policy.json +5 -5
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Runs inside Cursor renderer — must be self-contained.
3
3
  */
4
- export function extractionFunction(containerSelectors, tabSelectors, inputSelectors, approveSelectors, approveTextMatch, rejectSelectors, rejectTextMatch) {
4
+ export function extractionFunction(containerSelectors, tabSelectors, editorAuxiliaryTabSelectors, inputSelectors, approveSelectors, approveTextMatch, rejectSelectors, rejectTextMatch) {
5
5
  function findFirst(sels) {
6
6
  for (const sel of sels) {
7
7
  try {
@@ -340,11 +340,33 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
340
340
  /while dragging, use the arrow keys/i.test(text));
341
341
  }
342
342
  function removePathFor(el) {
343
- const btn = el.querySelector('button[aria-label="Remove"]');
344
- if (!btn)
345
- return undefined;
346
- const path = buildSelectorPath(btn);
347
- return path || undefined;
343
+ const legacy = el.querySelector('button[aria-label="Remove"]');
344
+ if (legacy) {
345
+ const path = buildSelectorPath(legacy);
346
+ if (path)
347
+ return path;
348
+ }
349
+ const removeAction = el.querySelector('[data-queue-action="remove"]');
350
+ if (removeAction) {
351
+ const clickTarget = removeAction.closest('button') ||
352
+ removeAction.querySelector('button, .anysphere-icon-button') ||
353
+ removeAction;
354
+ const path = buildSelectorPath(clickTarget);
355
+ if (path)
356
+ return path;
357
+ }
358
+ const trashIcon = el.querySelector('.codicon-trashcan');
359
+ if (trashIcon) {
360
+ const clickTarget = trashIcon.closest('button') ||
361
+ trashIcon.closest('.anysphere-icon-button') ||
362
+ trashIcon.parentElement;
363
+ if (clickTarget) {
364
+ const path = buildSelectorPath(clickTarget);
365
+ if (path)
366
+ return path;
367
+ }
368
+ }
369
+ return undefined;
348
370
  }
349
371
  function extractQueuedRowText(row) {
350
372
  const textRoot = row.querySelector('[class*="ui-tray-row"]') ||
@@ -352,7 +374,7 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
352
374
  row;
353
375
  const clone = textRoot.cloneNode(true);
354
376
  clone
355
- .querySelectorAll('img, button, [class*="icon"], [aria-hidden="true"], [class*="image-pill"]')
377
+ .querySelectorAll('img, button, .anysphere-icon-button, [class*="queue-item-actions"], [class*="icon"], [aria-hidden="true"], [class*="image-pill"]')
356
378
  .forEach((el) => el.remove());
357
379
  return (clone.textContent || '').trim().replace(/\s+/g, ' ');
358
380
  }
@@ -421,6 +443,7 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
421
443
  items.push({ id: `q-line-${idx}-${items.length}`, text: t });
422
444
  }
423
445
  const itemSelectors = [
446
+ '.composer-toolbar-queue-item',
424
447
  '[class*="ui-tray--queued"] [class*="queue-sortable-row"]',
425
448
  '[class*="ui-tray--queued"] [class*="ui-tray-row"]',
426
449
  '[class*="queue-sortable-row"]',
@@ -811,6 +834,92 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
811
834
  t = t.replace(/\d+\s*(?:s|m|h|d|w)\b/gi, '').replace(/\d+[smhdw]$/i, '').trim();
812
835
  return t.slice(0, 120);
813
836
  }
837
+ /** Tab title from auxiliary bar (aria-label is usually the full name). */
838
+ function cleanAuxiliaryTabTitle(raw, fromAriaLabel) {
839
+ let t = raw.trim().replace(/\s+/g, ' ');
840
+ t = t.replace(/\d+\s*(?:s|m|h|d|w)\b/gi, '').replace(/\d+[smhdw]$/i, '').trim();
841
+ t = t.replace(/⌘[A-Z0-9]+$/i, '').trim();
842
+ // VS Code puts editor-group context in aria-label, not on the visible tab chip.
843
+ t = t.replace(/,\s*Chat Editors:\s*Editor Group\s*\d+\s*$/i, '').trim();
844
+ if (!fromAriaLabel) {
845
+ // Status suffix at end only — do not use /i on "Running" (would strip "running" in "Bot running on…").
846
+ t = t.replace(/\s+(?:Edited|Thinking|Grepping|Running)\s*$/i, '').trim();
847
+ t = t.replace(/\+\d[\d\-·\s\w]*(?:Files?|File)\d*\w*$/i, '').trim();
848
+ }
849
+ return t.slice(0, 120);
850
+ }
851
+ function pickAuxiliaryTabTitle(aria, visible) {
852
+ const cleanedAria = aria ? cleanAuxiliaryTabTitle(aria, true) : '';
853
+ const cleanedVisible = visible ? cleanAuxiliaryTabTitle(visible, false) : '';
854
+ if (cleanedAria && cleanedVisible) {
855
+ if (cleanedVisible.length > cleanedAria.length)
856
+ return cleanedVisible;
857
+ if (cleanedAria.startsWith(cleanedVisible) ||
858
+ cleanedVisible.length < Math.min(cleanedAria.length, 10)) {
859
+ return cleanedAria;
860
+ }
861
+ return cleanedAria.length >= cleanedVisible.length ? cleanedAria : cleanedVisible;
862
+ }
863
+ return cleanedAria || cleanedVisible;
864
+ }
865
+ function isEditorAuxiliaryLayout() {
866
+ if (document.querySelector('[class*="agent-panel"]'))
867
+ return false;
868
+ return !!document.querySelector('#workbench\\.parts\\.auxiliarybar');
869
+ }
870
+ function extractEditorAuxiliaryTabs() {
871
+ if (!isEditorAuxiliaryLayout())
872
+ return [];
873
+ const composerBar = document.querySelector('div.composer-bar.editor[data-composer-id]') ||
874
+ document.querySelector('.composer-bar[data-composer-id]') ||
875
+ document.querySelector('div.composer-bar.editor, .composer-bar');
876
+ const activeComposerId = composerBar?.getAttribute('data-composer-id') ||
877
+ composerBar?.closest('[data-composer-id]')?.getAttribute('data-composer-id') ||
878
+ '';
879
+ const tabEls = [];
880
+ for (const sel of editorAuxiliaryTabSelectors) {
881
+ try {
882
+ document.querySelectorAll(sel).forEach((el) => tabEls.push(el));
883
+ }
884
+ catch {
885
+ /* skip */
886
+ }
887
+ if (tabEls.length)
888
+ break;
889
+ }
890
+ if (!tabEls.length)
891
+ return [];
892
+ const out = [];
893
+ tabEls.forEach((cell, i) => {
894
+ const aria = cell.getAttribute('aria-label')?.trim() ?? '';
895
+ const visible = (cell.textContent || '').trim().replace(/\s+/g, ' ');
896
+ const title = pickAuxiliaryTabTitle(aria, visible);
897
+ if (!title || /^(new agent|marketplace)$/i.test(title))
898
+ return;
899
+ const active = cell.classList.contains('active') ||
900
+ cell.classList.contains('selected') ||
901
+ cell.getAttribute('aria-selected') === 'true';
902
+ const resourceName = cell.getAttribute('data-resource-name')?.trim() || '';
903
+ let composerId = cell.getAttribute('data-composer-id') ||
904
+ cell.closest('[data-composer-id]')?.getAttribute('data-composer-id') ||
905
+ '';
906
+ if (active && activeComposerId)
907
+ composerId = activeComposerId;
908
+ else if (!composerId && resourceName.length >= 32) {
909
+ composerId = resourceName;
910
+ if (activeComposerId && activeComposerId.toLowerCase().startsWith(resourceName.toLowerCase())) {
911
+ composerId = activeComposerId;
912
+ }
913
+ }
914
+ out.push({
915
+ id: composerId || `aux-tab-${i}`,
916
+ composerId: composerId || undefined,
917
+ title,
918
+ active,
919
+ });
920
+ });
921
+ return out;
922
+ }
814
923
  function readAgentBtnMeta(btn) {
815
924
  const title = cleanSidebarTitle(btn);
816
925
  if (!title || /^more$/i.test(title) || /^less$/i.test(title))
@@ -827,8 +936,12 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
827
936
  return { title, composerId, active, isWorking, hasUnread, needsAttention };
828
937
  }
829
938
  const tabs = [];
939
+ const editorAuxTabs = extractEditorAuxiliaryTabs();
940
+ if (editorAuxTabs.length > 0) {
941
+ tabs.push(...editorAuxTabs);
942
+ }
830
943
  const glassTabs = document.querySelectorAll('.glass-sidebar-agent-list-container li.ui-sidebar-menu-item > div.glass-sidebar-agent-menu-btn');
831
- if (glassTabs.length > 0) {
944
+ if (tabs.length === 0 && glassTabs.length > 0) {
832
945
  glassTabs.forEach((cell, i) => {
833
946
  const meta = readAgentBtnMeta(cell);
834
947
  if (!meta)
@@ -844,7 +957,7 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
844
957
  });
845
958
  });
846
959
  }
847
- else {
960
+ else if (tabs.length === 0) {
848
961
  for (const sel of tabSelectors) {
849
962
  document.querySelectorAll(sel).forEach((cell, i) => {
850
963
  const title = cell.getAttribute('aria-label') ??
@@ -1025,28 +1138,56 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
1025
1138
  r,
1026
1139
  ].filter((el) => Boolean(el));
1027
1140
  }
1028
- /** Active generation — Stop in composer (not stale loading dots in the thread). */
1029
- function detectAgentWorking(root) {
1030
- const stopSel = 'button[data-state="stop"], button[aria-label="Stop generation"], button[aria-label*="Stop"], a.codicon-debug-stop, [class*="stop-button"]';
1031
- const sendSel = 'button[data-state="submit"], button.ui-prompt-input-submit-button[data-state="submit"], button[aria-label*="Send"]';
1032
- for (const scope of composerScopes(root)) {
1033
- const stopBtn = scope.querySelector(stopSel);
1034
- if (stopBtn) {
1035
- const disabled = stopBtn.hasAttribute('disabled') ||
1036
- stopBtn.getAttribute('aria-disabled') === 'true';
1037
- if (!disabled)
1038
- return true;
1039
- }
1040
- }
1041
- for (const scope of composerScopes(root)) {
1042
- const sendBtn = scope.querySelector(sendSel);
1043
- if (sendBtn && sendBtn.getAttribute('data-state') !== 'stop') {
1044
- const disabled = sendBtn.hasAttribute('disabled') ||
1045
- sendBtn.getAttribute('aria-disabled') === 'true';
1046
- if (!disabled)
1047
- return false;
1141
+ /** Keep in sync with bridge/src/generation-stop-dom.ts */
1142
+ function findGenerationStopButton() {
1143
+ const scopes = [
1144
+ document.querySelector('[class*="agent-panel"]'),
1145
+ document.querySelector('.composer-bar'),
1146
+ document.querySelector('[class*="composer-bar"]'),
1147
+ document,
1148
+ ].filter((el) => Boolean(el));
1149
+ const sels = [
1150
+ 'button.ui-prompt-input-submit-button[data-state="stop"]',
1151
+ 'button[aria-label="Stop generation"]',
1152
+ 'button.ui-prompt-input-submit-button[aria-label="Stop generation"]',
1153
+ 'button[data-state="stop"]',
1154
+ '.composer-button-area .anysphere-icon-button:has(.codicon-debug-stop)',
1155
+ '.send-with-mode .anysphere-icon-button:has(.codicon-debug-stop)',
1156
+ ];
1157
+ for (const scope of scopes) {
1158
+ for (const sel of sels) {
1159
+ try {
1160
+ const hit = scope.querySelector(sel);
1161
+ if (!hit)
1162
+ continue;
1163
+ const btn = hit.classList.contains('codicon-debug-stop')
1164
+ ? hit.closest('.anysphere-icon-button, button, [role="button"], a') || hit
1165
+ : hit;
1166
+ const st = getComputedStyle(btn);
1167
+ if (st.display === 'none' || st.visibility === 'hidden' || Number(st.opacity) < 0.05) {
1168
+ continue;
1169
+ }
1170
+ const r = btn.getBoundingClientRect();
1171
+ if (r.width < 2 || r.height < 2)
1172
+ continue;
1173
+ if (btn.closest('.ui-shell-tool-call'))
1174
+ continue;
1175
+ const aria = (btn.getAttribute('aria-label') || '').trim().toLowerCase();
1176
+ if (aria === 'stop command')
1177
+ continue;
1178
+ return btn;
1179
+ }
1180
+ catch {
1181
+ /* skip */
1182
+ }
1048
1183
  }
1049
1184
  }
1185
+ return null;
1186
+ }
1187
+ /** Active generation — Stop in composer (not shell «Stop command» in tool rows). */
1188
+ function detectAgentWorking(root) {
1189
+ if (findGenerationStopButton())
1190
+ return true;
1050
1191
  const title = document.querySelector('span.auxiliary-bar-chat-title')?.textContent?.trim() || '';
1051
1192
  if (/generat|working|thinking|running|в работе/i.test(title) && !/waiting|approval/i.test(title)) {
1052
1193
  return true;
@@ -1100,6 +1241,10 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
1100
1241
  agentStatus = 'background_shell';
1101
1242
  agentStatusMessage = backgroundStatusText;
1102
1243
  }
1244
+ const activeListTab = tabs.find((t) => t.active);
1245
+ if (activeListTab) {
1246
+ activeListTab.isWorking = hasActiveGeneration;
1247
+ }
1103
1248
  let contextPercent;
1104
1249
  let terminalCount;
1105
1250
  for (const el of Array.from(document.querySelectorAll('button, span, div'))) {
@@ -1266,5 +1411,7 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
1266
1411
  }
1267
1412
  }
1268
1413
  export async function extractPageState(client, selectors) {
1269
- return (await client.callFunction(extractionFunction, selectors.chatContainer.strategies, selectors.chatTabList.strategies, selectors.chatInput.strategies, selectors.approveButton.strategies, selectors.approveButton.textMatch ?? [], selectors.rejectButton.strategies, selectors.rejectButton.textMatch ?? []));
1414
+ return (await client.callFunction(extractionFunction, selectors.chatContainer.strategies, selectors.chatTabList.strategies, selectors.editorAuxiliaryTabList?.strategies ?? [
1415
+ '#workbench\\.parts\\.auxiliarybar div.tab[role="tab"]',
1416
+ ], selectors.chatInput.strategies, selectors.approveButton.strategies, selectors.approveButton.textMatch ?? [], selectors.rejectButton.strategies, selectors.rejectButton.textMatch ?? []));
1270
1417
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Find / click agent **generation** stop (not shell «Stop command»).
3
+ * Single entry for CDP callFunction — helpers must live inside this function.
4
+ */
5
+ export declare function generationStopDomAction(action: 'has' | 'click'): boolean;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Find / click agent **generation** stop (not shell «Stop command»).
3
+ * Single entry for CDP callFunction — helpers must live inside this function.
4
+ */
5
+ export function generationStopDomAction(action) {
6
+ function isVisibleButton(btn) {
7
+ const st = getComputedStyle(btn);
8
+ if (st.display === 'none' || st.visibility === 'hidden' || Number(st.opacity) < 0.05) {
9
+ return false;
10
+ }
11
+ const r = btn.getBoundingClientRect();
12
+ return r.width >= 2 && r.height >= 2;
13
+ }
14
+ function resolveStopClickTarget(el) {
15
+ if (!el)
16
+ return null;
17
+ if (el.classList.contains('codicon-debug-stop')) {
18
+ return el.closest('.anysphere-icon-button, button, [role="button"], a') || el;
19
+ }
20
+ return el;
21
+ }
22
+ function findGenerationStopButton() {
23
+ const selectors = [
24
+ 'button.ui-prompt-input-submit-button[data-state="stop"]',
25
+ 'button[aria-label="Stop generation"]',
26
+ 'button.ui-prompt-input-submit-button[aria-label="Stop generation"]',
27
+ 'button[data-state="stop"]',
28
+ '.composer-button-area .anysphere-icon-button:has(.codicon-debug-stop)',
29
+ '.send-with-mode .anysphere-icon-button:has(.codicon-debug-stop)',
30
+ ];
31
+ const scopes = [
32
+ document.querySelector('[class*="agent-panel"]'),
33
+ document.querySelector('.composer-bar'),
34
+ document.querySelector('[class*="composer-bar"]'),
35
+ document,
36
+ ].filter((el) => Boolean(el));
37
+ for (const scope of scopes) {
38
+ for (const sel of selectors) {
39
+ try {
40
+ const hit = scope.querySelector(sel);
41
+ const btn = resolveStopClickTarget(hit);
42
+ if (!btn || !isVisibleButton(btn))
43
+ continue;
44
+ if (btn.closest('.ui-shell-tool-call'))
45
+ continue;
46
+ const aria = (btn.getAttribute('aria-label') || '').trim().toLowerCase();
47
+ if (aria === 'stop command')
48
+ continue;
49
+ return btn;
50
+ }
51
+ catch {
52
+ /* skip */
53
+ }
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ const btn = findGenerationStopButton();
59
+ if (action === 'has')
60
+ return !!btn;
61
+ if (!btn)
62
+ return false;
63
+ const target = resolveStopClickTarget(btn) || btn;
64
+ target.scrollIntoView({ block: 'center' });
65
+ target.click();
66
+ return true;
67
+ }
@@ -8,6 +8,7 @@ import { WindowMonitor } from './window-monitor.js';
8
8
  import { Relay } from './relay.js';
9
9
  import { MessageDebugStore } from './message-debug-store.js';
10
10
  import { connectorClientVersion } from './connector-client-version.js';
11
+ import { installEditorListSync } from './editor-list-sync.js';
11
12
  import { installKeepAwakeShutdown, startKeepAwake } from './keep-awake.js';
12
13
  async function main() {
13
14
  const config = loadConfig();
@@ -30,6 +31,7 @@ async function main() {
30
31
  }
31
32
  console.log(`Projects: ${config.cursorProjectsDir}`);
32
33
  const stateManager = new StateManager(config.debounceMs);
34
+ installEditorListSync(stateManager);
33
35
  const commandExecutor = new CommandExecutor(selectors);
34
36
  const cdpBridge = new CDPBridge(config);
35
37
  const jsonlIndex = new JsonlIndex(config.cursorProjectsDir);
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Remove a deferred-queue row in the composer (Editor toolbar or Agents tray).
3
+ * Used via CDP callFunction — helpers must stay inside this function.
4
+ */
5
+ export declare function queueRemoveDomAction(action: 'remove', opts: {
6
+ text?: string;
7
+ selectorPath?: string;
8
+ }): {
9
+ ok: boolean;
10
+ reason?: string;
11
+ };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Remove a deferred-queue row in the composer (Editor toolbar or Agents tray).
3
+ * Used via CDP callFunction — helpers must stay inside this function.
4
+ */
5
+ export function queueRemoveDomAction(action, opts) {
6
+ function normText(s) {
7
+ return s.trim().replace(/\s+/g, ' ').toLowerCase();
8
+ }
9
+ function clickRemoveButton(btn) {
10
+ const el = btn;
11
+ el.scrollIntoView({ block: 'center', behavior: 'instant' });
12
+ el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
13
+ el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
14
+ el.click();
15
+ }
16
+ function findRemoveButton(row) {
17
+ const legacy = row.querySelector('button[aria-label="Remove"]');
18
+ if (legacy)
19
+ return legacy;
20
+ const removeAction = row.querySelector('[data-queue-action="remove"]');
21
+ if (removeAction) {
22
+ return (removeAction.closest('button') ||
23
+ removeAction.querySelector('button, .anysphere-icon-button') ||
24
+ removeAction);
25
+ }
26
+ const trash = row.querySelector('.codicon-trashcan');
27
+ if (trash) {
28
+ return (trash.closest('button') ||
29
+ trash.closest('.anysphere-icon-button') ||
30
+ trash.parentElement);
31
+ }
32
+ return null;
33
+ }
34
+ const rowSelectors = [
35
+ '.composer-toolbar-queue-item',
36
+ '[class*="ui-tray--queued"] [class*="queue-sortable-row"]',
37
+ '[class*="ui-tray--queued"] [class*="ui-tray-row"]',
38
+ '[class*="queue-sortable-row"]',
39
+ '[class*="ui-tray-row"]',
40
+ ];
41
+ function rowsInScope() {
42
+ const input = document.querySelector(".composer-bar [contenteditable='true'], [contenteditable='true']");
43
+ const scope = input?.closest('[class*="agent-panel"]') ||
44
+ input?.closest('[class*="composer-panel"]') ||
45
+ input?.closest('.composer-bar, [class*="composer-bar"]') ||
46
+ document.querySelector('#composer-toolbar-section') ||
47
+ document.body;
48
+ const out = [];
49
+ for (const sel of rowSelectors) {
50
+ scope.querySelectorAll(sel).forEach((row) => {
51
+ if (!out.includes(row))
52
+ out.push(row);
53
+ });
54
+ }
55
+ return out;
56
+ }
57
+ if (action !== 'remove')
58
+ return { ok: false, reason: 'unknown action' };
59
+ const want = normText(opts.text ?? '');
60
+ const snippet = want.slice(0, 48);
61
+ if (snippet.length >= 4) {
62
+ for (const row of rowsInScope()) {
63
+ const rowText = normText(row.textContent || '');
64
+ const rowSnippet = rowText.slice(0, 48);
65
+ if (!rowText.includes(snippet) && !rowSnippet.includes(snippet.slice(0, 24))) {
66
+ continue;
67
+ }
68
+ const btn = findRemoveButton(row);
69
+ if (!btn)
70
+ return { ok: false, reason: 'remove button not found' };
71
+ clickRemoveButton(btn);
72
+ return { ok: true };
73
+ }
74
+ }
75
+ const path = opts.selectorPath?.trim();
76
+ if (path) {
77
+ const el = document.querySelector(path);
78
+ if (!el)
79
+ return { ok: false, reason: 'selector not found' };
80
+ const row = el.closest('.composer-toolbar-queue-item, [class*="queue-sortable-row"], [class*="ui-tray-row"]');
81
+ const btn = row ? findRemoveButton(row) : el;
82
+ if (!btn)
83
+ return { ok: false, reason: 'remove target not found' };
84
+ clickRemoveButton(btn);
85
+ return { ok: true };
86
+ }
87
+ return { ok: false, reason: want ? 'queue row not found' : 'no text or selector' };
88
+ }
@@ -40,6 +40,7 @@ export class RelayUpstream {
40
40
  code: identity.pairingCode,
41
41
  clientToken: identity.clientToken,
42
42
  expiresAt: identity.pairingCodeExpiresAt,
43
+ machineLabel: identity.machineLabel,
43
44
  });
44
45
  }
45
46
  }
@@ -57,6 +58,7 @@ export class RelayUpstream {
57
58
  roomId: this.config.relayRoomId,
58
59
  clientKind: 'connector',
59
60
  clientVersion: connectorClientVersion(),
61
+ machineLabel: loadPairingIdentity()?.machineLabel ?? '',
60
62
  },
61
63
  });
62
64
  this.socket.on('connect', () => {
@@ -1211,7 +1211,16 @@ export class Relay {
1211
1211
  }
1212
1212
  this.refreshAgentsIndex(true);
1213
1213
  }
1214
- async trySwitchWindowForAgent(agentId) {
1214
+ async trySwitchWindowForAgent(agentId, windowId) {
1215
+ await this.cdpBridge.refreshWindows();
1216
+ if (windowId) {
1217
+ const win = this.cdpBridge.windows.find((w) => w.id === windowId);
1218
+ if (win && win.id !== this.cdpBridge.activeTargetId) {
1219
+ await this.cdpBridge.switchWindow(win.id);
1220
+ console.log(`[relay] CDP → editor window "${win.title}" (${windowId.slice(0, 8)})`);
1221
+ }
1222
+ return;
1223
+ }
1215
1224
  const projectDir = this.jsonlIndex.findProjectDirForAgent(agentId);
1216
1225
  if (!projectDir)
1217
1226
  return;
@@ -1222,7 +1231,6 @@ export class Relay {
1222
1231
  const repoName = repoSlug.replace(/-/g, ' ').toLowerCase();
1223
1232
  if (!repoName)
1224
1233
  return;
1225
- await this.cdpBridge.refreshWindows();
1226
1234
  const match = this.cdpBridge.windows.find((w) => {
1227
1235
  const t = w.title.toLowerCase();
1228
1236
  return t.includes(repoName) || repoName.split(' ').every((wrd) => t.includes(wrd));
@@ -1316,17 +1324,29 @@ export class Relay {
1316
1324
  this.domExtractor.pollNow();
1317
1325
  return;
1318
1326
  }
1327
+ if (payload.agentId &&
1328
+ (payload.type === 'remove_queued' ||
1329
+ payload.type === 'send_message' ||
1330
+ payload.type === 'stop_agent' ||
1331
+ payload.type === 'focus_agent')) {
1332
+ await this.trySwitchWindowForAgent(payload.agentId, payload.windowId);
1333
+ }
1319
1334
  const result = await this.commandExecutor.execute(payload);
1320
1335
  reply('command:result', result);
1321
- if (payload.type === 'stop_agent' && result.ok) {
1322
- suppressAgentCompletionPush();
1323
- this.stateManager.patchNow({
1324
- agentWorking: false,
1325
- agentStatus: undefined,
1326
- agentStatusMessage: undefined,
1327
- composerDraft: undefined,
1328
- composerDraftImages: undefined,
1329
- });
1336
+ if ((payload.type === 'stop_agent' ||
1337
+ payload.type === 'remove_queued' ||
1338
+ payload.type === 'focus_agent') &&
1339
+ result.ok) {
1340
+ if (payload.type === 'stop_agent') {
1341
+ suppressAgentCompletionPush();
1342
+ this.stateManager.patchNow({
1343
+ agentWorking: false,
1344
+ agentStatus: undefined,
1345
+ agentStatusMessage: undefined,
1346
+ composerDraft: undefined,
1347
+ composerDraftImages: undefined,
1348
+ });
1349
+ }
1330
1350
  this.domExtractor.pollNow();
1331
1351
  }
1332
1352
  }
@@ -1410,13 +1430,13 @@ export class Relay {
1410
1430
  this.jsonlIndex.historyReplyInFlight.delete(agentId);
1411
1431
  }
1412
1432
  }
1413
- async runAgentsSubscribe({ agentId, title, focus, }) {
1433
+ async runAgentsSubscribe({ agentId, title, focus, windowId, }) {
1414
1434
  if (!agentId)
1415
1435
  return;
1416
1436
  const alreadySubscribed = this.jsonlIndex.getSubscribedAgents().has(agentId);
1417
1437
  this.jsonlIndex.subscribe(agentId, title, { emitHistory: !alreadySubscribed });
1418
1438
  this.stateManager.patchNow({ lastError: undefined });
1419
- await this.trySwitchWindowForAgent(agentId);
1439
+ await this.trySwitchWindowForAgent(agentId, windowId);
1420
1440
  if (focus === false) {
1421
1441
  void this.refreshDomChatOnSubscribe(agentId, title, { clear: !alreadySubscribed });
1422
1442
  return;
@@ -1486,10 +1506,10 @@ export class Relay {
1486
1506
  this.refreshLentaPendingPatch();
1487
1507
  }
1488
1508
  }
1489
- async runAgentsFocus({ agentId, title }, reply) {
1509
+ async runAgentsFocus({ agentId, title, windowId, }, reply) {
1490
1510
  if (!agentId)
1491
1511
  return;
1492
- await this.trySwitchWindowForAgent(agentId);
1512
+ await this.trySwitchWindowForAgent(agentId, windowId);
1493
1513
  const result = await this.commandExecutor.execute({
1494
1514
  id: `focus-${Date.now()}`,
1495
1515
  type: 'focus_agent',
@@ -7,7 +7,7 @@ export declare class StateManager extends EventEmitter {
7
7
  private pendingPatch;
8
8
  constructor(debounceMs: number);
9
9
  getState(): CursorState;
10
- onExtraction(partial: CursorState | null, error?: string): void;
10
+ onExtraction(partial: Partial<CursorState> | null, error?: string): void;
11
11
  onConnectionChanged(connected: boolean): void;
12
12
  updateWindows(windows: CursorWindow[], activeWindowId: string): void;
13
13
  updateWindowSnapshots(snapshots: WindowSnapshot[]): void;
@@ -6,6 +6,8 @@ export interface SelectorConfig {
6
6
  chatContainer: SelectorGroup;
7
7
  chatInput: SelectorGroup;
8
8
  chatTabList: SelectorGroup;
9
+ /** Editor: open agent chats in auxiliary bar tab strip (not agents sidebar). */
10
+ editorAuxiliaryTabList?: SelectorGroup;
9
11
  newChatButton: SelectorGroup;
10
12
  modeDropdown: SelectorGroup;
11
13
  modelDropdown: SelectorGroup;
@@ -135,11 +137,14 @@ export interface PendingApproval {
135
137
  description: string;
136
138
  actions: ApprovalAction[];
137
139
  }
140
+ export type CursorWindowKind = 'editor' | 'agents';
138
141
  export interface CursorWindow {
139
142
  id: string;
140
143
  title: string;
141
144
  workspace?: string;
142
145
  wsUrl?: string;
146
+ /** `agents` = dedicated Cursor Agents window; `editor` = code editor + auxiliary bar. */
147
+ kind?: CursorWindowKind;
143
148
  }
144
149
  export interface WindowSnapshot {
145
150
  windowId: string;
@@ -148,8 +153,12 @@ export interface WindowSnapshot {
148
153
  messageCount: number;
149
154
  lastPreview?: string;
150
155
  agentStatus?: string;
156
+ /** DOM: active chat generating (composer Stop / status line). */
157
+ agentWorking?: boolean;
151
158
  pendingApprovals: number;
152
159
  updatedAt: number;
160
+ tabs?: ChatTab[];
161
+ kind?: CursorWindowKind;
153
162
  }
154
163
  export interface CursorState {
155
164
  connected: boolean;
@@ -191,6 +200,10 @@ export interface CursorState {
191
200
  lastError?: string;
192
201
  /** Cursor sidebar: repos + chats in DOM order (mirrors desktop UI). */
193
202
  sidebarRepos?: RepoGroup[];
203
+ /** Editor mode: open editor windows as folders, tabs as chats (see `cursorListMode`). */
204
+ editorRepos?: RepoGroup[];
205
+ /** Which chat list layout the app should show (`editor` vs agents window). */
206
+ cursorListMode?: 'editor' | 'agents-window';
194
207
  /** Last DOM poll: extract/filter counters (sync debug). */
195
208
  messageDebug?: MessageExtractDebug;
196
209
  /** Composer input draft (contenteditable), when non-empty. */
@@ -205,6 +218,7 @@ export interface CommandPayload {
205
218
  type: CommandType;
206
219
  text?: string;
207
220
  tabTitle?: string;
221
+ /** Editor CDP window id (from chat route / agents:index). */
208
222
  windowId?: string;
209
223
  agentId?: string;
210
224
  selectorPath?: string;