@harness-mix/cli 0.1.5 → 0.1.6

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 (60) hide show
  1. package/README.md +1 -1
  2. package/docs/harness-management.md +4 -4
  3. package/docs/multi-agent-collaboration.md +9 -9
  4. package/output/native-build/renderer-extension.js +1959 -435
  5. package/package.json +9 -8
  6. package/scripts/collaboration-test.cjs +8 -3
  7. package/scripts/collaboration-ui-smoke.cjs +40 -26
  8. package/scripts/integrations-test.cjs +63 -4
  9. package/scripts/integrations-ui-smoke.cjs +77 -16
  10. package/scripts/native-protocol-test.cjs +102 -1
  11. package/scripts/native-sidebar-test.cjs +7 -0
  12. package/scripts/native-skill-roots-test.cjs +90 -0
  13. package/scripts/test-live-mentions.cjs +152 -0
  14. package/src/main/adapters/antigravity.js +20 -17
  15. package/src/main/adapters/claude.js +41 -8
  16. package/src/main/adapters/codebuddy.js +1 -0
  17. package/src/main/adapters/codex.js +20 -2
  18. package/src/main/adapters/cursor.js +5 -1
  19. package/src/main/adapters/dsh.js +8 -1
  20. package/src/main/adapters/grok.js +4 -1
  21. package/src/main/adapters/hermes.js +8 -0
  22. package/src/main/adapters/kiro.js +2 -1
  23. package/src/main/adapters/managed-mcp.js +15 -2
  24. package/src/main/adapters/omp.js +11 -0
  25. package/src/main/adapters/openclaw.js +6 -0
  26. package/src/main/adapters/opencode.js +5 -1
  27. package/src/main/adapters/pi.js +5 -1
  28. package/src/main/adapters/qoder.js +1 -0
  29. package/src/main/adapters/trae.js +4 -0
  30. package/src/main/adapters/zcode.js +3 -0
  31. package/src/main/host/collaboration.js +57 -11
  32. package/src/main/host/integrations.js +164 -26
  33. package/src/main/host/runtime.js +126 -86
  34. package/src/main/native/protocol.js +189 -78
  35. package/src/main/native/thread-list.js +3 -0
  36. package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
  37. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  38. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -1
  39. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts +11 -0
  40. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -1
  41. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +29 -4
  42. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -1
  43. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +2 -1
  44. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -1
  45. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  46. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +1 -1
  47. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  48. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  49. package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +3 -2
  50. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +149 -171
  51. package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +5 -1
  52. package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +556 -123
  53. package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +36 -3
  54. package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +1434 -62
  55. package/src/native-ui/renderer-extension/src/settings/localization.ts +4 -2
  56. package/src/native-ui/renderer-extension/src/settings/pages.ts +5 -3
  57. package/src/native-ui/renderer-extension/src/settings/shell.css +96 -6
  58. package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +4 -1
  59. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
  60. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +5 -2
@@ -1,15 +1,145 @@
1
1
  import { collaborationIcon } from './collaboration-icon.js';
2
- export interface CollaborationAgent { id: string; name: string; available: boolean; lead: boolean }
3
- export interface CollaborationSession { id: string; title: string; harnessId: string; cwd: string; running: boolean | null }
4
- export interface CollaborationMentionCatalog { agents: CollaborationAgent[]; sessions: CollaborationSession[]; canDelegate?: boolean }
5
- type MentionEntry = ({ kind: 'agent' } & CollaborationAgent) | ({ kind: 'session' } & CollaborationSession);
6
2
 
7
- const entryKey = (entry: MentionEntry): string => entry.kind === 'agent' ? `agent:${entry.id}` : `session:${entry.id}`;
3
+ export interface CollaborationAgent {
4
+ id: string;
5
+ name: string;
6
+ available: boolean;
7
+ lead: boolean;
8
+ description?: string;
9
+ }
8
10
 
9
- // Encode an entry as the @agent literal the Host expects in the submitted text.
10
- const entryLiteral = (entry: MentionEntry): string => entry.kind === 'agent'
11
- ? `@${entry.id} `
12
- : `@[${entry.title.replace(/[\[\]()\r\n]/g, '')}](harness-mix://session/${entry.id}) `;
11
+ export interface CollaborationSession {
12
+ id: string;
13
+ title: string;
14
+ harnessId: string;
15
+ cwd: string;
16
+ running: boolean | null;
17
+ }
18
+
19
+ export interface NativeCodexOption {
20
+ id: string;
21
+ title: string;
22
+ description: string;
23
+ category: string;
24
+ icon?: string;
25
+ action?: string;
26
+ }
27
+
28
+ export interface CollaborationMentionCatalog {
29
+ agents: CollaborationAgent[];
30
+ sessions: CollaborationSession[];
31
+ codexOptions?: NativeCodexOption[];
32
+ canDelegate?: boolean;
33
+ }
34
+
35
+ type MentionEntry =
36
+ | ({ kind: 'agent' } & CollaborationAgent)
37
+ | ({ kind: 'session' } & CollaborationSession)
38
+ | ({ kind: 'codex' } & NativeCodexOption);
39
+
40
+ const entryKey = (entry: MentionEntry): string => {
41
+ if (entry.kind === 'agent') return `agent:${entry.id}`;
42
+ if (entry.kind === 'session') return `session:${entry.id}`;
43
+ return `codex:${entry.id}`;
44
+ };
45
+
46
+ // Encode an entry as the #agent literal the Host expects in the submitted text.
47
+ const entryLiteral = (entry: MentionEntry): string => {
48
+ if (entry.kind === 'agent') return `#${entry.id} `;
49
+ if (entry.kind === 'session') return `#[${entry.title.replace(/[\[\]()\r\n]/g, '')}](harness-mix://session/${entry.id}) `;
50
+ return '';
51
+ };
52
+
53
+ const AGENT_DESCRIPTIONS: Record<string, string> = {
54
+ codex: '官方 Codex CLI 原生协同',
55
+ 'codex-cli': '官方 Codex CLI 原生协同',
56
+ pi: '轻量快速原生编码 Harness',
57
+ omp: 'Oh My Pi 增强版交互 Harness',
58
+ dsh: 'DeepSeek Harness 原生深度推理与工具调用',
59
+ 'deepseek-harness': 'DeepSeek Harness 原生深度推理与工具调用',
60
+ claude: 'Anthropic Claude Code 原生协同',
61
+ 'claude-code': 'Anthropic Claude Code 原生协同',
62
+ antigravity: 'Google Antigravity 2.0 原生 Harness',
63
+ opencode: '开源自主编码智能体',
64
+ grok: 'xAI Grok 原生协同与深度推理',
65
+ openclaw: 'OpenClaw 个人 AI 自主智能体',
66
+ hermes: 'Nous Research Hermes 自我迭代智能体',
67
+ qoder: 'Qoder 编程助手',
68
+ codebuddy: 'CodeBuddy 原生智能体',
69
+ zcode: 'ZCode 自动化工程代理',
70
+ trae: 'Trae 原生协同',
71
+ 'kiro-cli': 'Kiro CLI 智能编程',
72
+ 'cursor-cli': 'Cursor CLI 原生协同',
73
+ };
74
+
75
+ const AGENT_COLORS: Record<string, string> = {
76
+ codex: '#3b82f6',
77
+ 'codex-cli': '#3b82f6',
78
+ 'codex-harness': '#3b82f6',
79
+ grok: '#8b5cf6',
80
+ opencode: '#a855f7',
81
+ 'claude-code': '#ea580c',
82
+ claude: '#ea580c',
83
+ antigravity: '#0284c7',
84
+ pi: '#d97706',
85
+ omp: '#d97706',
86
+ dsh: '#2563eb',
87
+ 'deepseek-harness': '#2563eb',
88
+ 'kiro-cli': '#db2777',
89
+ openclaw: '#dc2626',
90
+ hermes: '#059669',
91
+ qoder: '#7c3aed',
92
+ codebuddy: '#0891b2',
93
+ zcode: '#4f46e5',
94
+ trae: '#0284c7',
95
+ 'cursor-cli': '#475569',
96
+ };
97
+
98
+ function getAgentColor(agentId: string): string {
99
+ const normalized = agentId.toLowerCase();
100
+ for (const [key, color] of Object.entries(AGENT_COLORS)) {
101
+ if (normalized.includes(key)) return color;
102
+ }
103
+ return '#6366f1';
104
+ }
105
+
106
+ const DEFAULT_CODEX_OPTIONS: NativeCodexOption[] = [
107
+ { id: 'files', title: '文件和文件夹', description: '从本地工作区添加文件或文件夹上下文', category: '添加', icon: '📎', action: 'files' },
108
+ { id: 'snapshot', title: '附加智能快照', description: '截取当前屏幕或窗口附加至上下文', category: '添加', icon: '📸', action: 'snapshot' },
109
+ { id: 'work', title: '在项目中使用 Work', description: '为新聊天选择项目', category: '添加', icon: '📁', action: 'work' },
110
+ { id: 'goal', title: '目标', description: '设置要持续追求的目标', category: '添加', icon: '🎯', action: 'goal' },
111
+ { id: 'plan', title: '计划模式', description: '开启计划模式', category: '添加', icon: '💡', action: 'plan' },
112
+ { id: 'sketch', title: '绘图', description: '绘制草图', category: '添加', icon: '✏️', action: 'sketch' },
113
+ { id: 'canva', title: 'Canva', description: 'Create, review, edit designs', category: '插件', icon: '🎨', action: 'plugin-canva' },
114
+ { id: 'github', title: 'GitHub', description: 'Triage PRs, issues, CI, and publish flows', category: '插件', icon: '🐙', action: 'plugin-github' },
115
+ { id: 'product-design', title: 'Product Design', description: 'Explore and prototype ideas', category: '插件', icon: '📐', action: 'plugin-product-design' },
116
+ ];
117
+
118
+ function getCodexNativeOptions(): NativeCodexOption[] {
119
+ const overlay = document.querySelector('[data-composer-overlay-floating-ui="true"]');
120
+ if (!overlay) return DEFAULT_CODEX_OPTIONS;
121
+ const buttons = [...overlay.querySelectorAll<HTMLElement>('button, [role="option"]')];
122
+ if (!buttons.length) return DEFAULT_CODEX_OPTIONS;
123
+ const scanned: NativeCodexOption[] = [];
124
+ for (let i = 0; i < buttons.length; i++) {
125
+ const btn = buttons[i];
126
+ if (!btn) continue;
127
+ const text = (btn.innerText || btn.textContent || '').trim();
128
+ if (!text) continue;
129
+ const parts = text.split('\n').map(s => s.trim()).filter(Boolean);
130
+ const title = parts[0] || `选项 ${i + 1}`;
131
+ const description = parts.slice(1).join(' ') || '';
132
+ scanned.push({
133
+ id: `native-${i}`,
134
+ title,
135
+ description,
136
+ category: i < 6 ? '添加' : '插件',
137
+ icon: title.includes('文件') ? '📎' : title.includes('快照') ? '📸' : title.includes('计划') ? '💡' : title.includes('绘图') ? '✏️' : '🧩',
138
+ action: `native-click-${i}`,
139
+ });
140
+ }
141
+ return scanned.length ? scanned : DEFAULT_CODEX_OPTIONS;
142
+ }
13
143
 
14
144
  /** Adds native-Harness suggestions while preserving the original composer editor. */
15
145
  export function installHarnessMentions(load: (editor: Element, query: string) => Promise<CollaborationMentionCatalog>) {
@@ -20,30 +150,84 @@ export function installHarnessMentions(load: (editor: Element, query: string) =>
20
150
  menu.style.cssText = 'position:fixed;z-index:2147483647;box-sizing:border-box;max-height:320px;overflow:auto;width:340px;max-width:calc(100vw - 16px);padding:6px;border:1px solid #8884;border-radius:12px;background:Canvas;color:CanvasText;box-shadow:0 8px 30px #0002;font:13px system-ui';
21
151
  menu.hidden = true;
22
152
  document.body.append(menu);
23
- // The native @ surface and our Harness picker otherwise open at the same time.
24
- // Scope suppression to this composer and this active mention only.
153
+
154
+ // Enable inline text-box layout so selected mentions appear inside the text box on the same line as the cursor.
155
+ // We NEVER suppress Codex native @ floating UI since Harness Mix is triggered by #.
25
156
  const style = document.createElement('style');
26
- style.textContent = '[data-harness-mix-mention-active] [data-composer-overlay-floating-ui="true"]{display:none!important}';
157
+ style.textContent = `
158
+ [data-harness-mix-has-mentions="true"] {
159
+ display: flex !important;
160
+ flex-wrap: wrap !important;
161
+ align-items: baseline !important;
162
+ gap: 8px !important;
163
+ }
164
+ [data-harness-mix-has-mentions="true"] > [data-harness-mix-selected-mentions] {
165
+ display: inline-flex !important;
166
+ align-items: center !important;
167
+ gap: 8px !important;
168
+ flex-wrap: wrap !important;
169
+ vertical-align: baseline !important;
170
+ line-height: 1.5 !important;
171
+ }
172
+ [data-harness-mix-has-mentions="true"] > textarea,
173
+ [data-harness-mix-has-mentions="true"] > [contenteditable="true"],
174
+ [data-harness-mix-has-mentions="true"] > .ProseMirror {
175
+ display: inline-block !important;
176
+ width: auto !important;
177
+ min-width: 60px !important;
178
+ flex: 1 1 auto !important;
179
+ margin: 0 !important;
180
+ padding: 0 !important;
181
+ vertical-align: baseline !important;
182
+ }
183
+ [data-harness-mix-mention-badge]:hover {
184
+ background: rgba(128, 128, 128, 0.14) !important;
185
+ }
186
+ `;
27
187
  document.head.append(style);
188
+
28
189
  let activeComposer: Element | null = null;
29
190
  let generation = 0, disposed = false, selected = 0;
30
191
  let editor: HTMLTextAreaElement | HTMLElement | null = null;
31
192
  let range: Range | null = null, start = 0, end = 0;
32
193
  let entries: MentionEntry[] = [];
33
- let matches: MentionEntry[] = [], activeKind: 'agent' | 'session' = 'agent', canDelegate = true;
194
+ let matches: MentionEntry[] = [];
195
+ let activeKind: 'agent' | 'codex' | 'session' = 'agent';
196
+ let canDelegate = true;
34
197
  const catalogs = new Map<HTMLElement, CollaborationMentionCatalog>();
35
198
  const badges = new Map<HTMLElement, HTMLElement>();
36
199
 
37
- // Per-editor mention state, decoupled from editor text so the literal
38
- // @agent handles never leak into the visible composer.
39
- const selections = new Map<HTMLElement, Map<string, MentionEntry>>();
200
+ // Per-session mention state keyed by conversation target, so mentions persist
201
+ // when switching between conversations and returning.
202
+ const sessionSelections = new Map<string, Map<string, MentionEntry>>();
203
+
204
+ const conversationKeyFor = (target: HTMLElement): string => {
205
+ const composer = target.closest('[data-codex-composer-root]');
206
+ if (!composer) return (target as unknown as { __harnessMixEditorKey?: string }).__harnessMixEditorKey || 'default';
207
+ const portal = composer.querySelector('[data-above-composer-portal]');
208
+ const portalId = portal?.getAttribute('data-above-composer-conversation-id');
209
+ if (portalId) return `conversation:${portalId}`;
210
+ const convEl = composer.closest('[data-conversation-id]') || composer.querySelector('[data-conversation-id]');
211
+ const convId = convEl?.getAttribute('data-conversation-id');
212
+ if (convId) return `conversation:${convId}`;
213
+ const draftId = composer.getAttribute('data-composer-draft-id');
214
+ if (draftId) return `draft:${draftId}`;
215
+ const all = composer.querySelectorAll('textarea, [contenteditable="true"]');
216
+ if (all.length > 1) {
217
+ const index = Array.from(all).indexOf(target);
218
+ return `editor:${index >= 0 ? index : 'default'}`;
219
+ }
220
+ return 'default';
221
+ };
222
+
40
223
  const selectionsFor = (target: HTMLElement): Map<string, MentionEntry> => {
41
- let perEditor = selections.get(target);
42
- if (!perEditor) {
43
- perEditor = new Map();
44
- selections.set(target, perEditor);
224
+ const key = conversationKeyFor(target);
225
+ let perSession = sessionSelections.get(key);
226
+ if (!perSession) {
227
+ perSession = new Map();
228
+ sessionSelections.set(key, perSession);
45
229
  }
46
- return perEditor;
230
+ return perSession;
47
231
  };
48
232
 
49
233
  const replaceSelectionWith = (target: HTMLElement, replacement: string): boolean => {
@@ -73,111 +257,241 @@ export function installHarnessMentions(load: (editor: Element, query: string) =>
73
257
  };
74
258
 
75
259
  const syncBadges = (target: HTMLElement) => {
76
- for (const [old, strip] of badges) if (!old.isConnected) { strip.remove(); badges.delete(old); catalogs.delete(old); }
77
- const perEditor = selections.get(target);
260
+ for (const [old, strip] of badges) {
261
+ if (!old.isConnected) {
262
+ strip.remove();
263
+ badges.delete(old);
264
+ catalogs.delete(old);
265
+ }
266
+ }
267
+ const perEditor = selectionsFor(target);
78
268
  const hasAny = !!perEditor && perEditor.size > 0;
79
269
  let strip = badges.get(target);
270
+
271
+ // Toggle container flex layout so mentions strip and editor input share the exact same line inside the text box like Image 2.
272
+ const container = target.parentElement;
273
+ if (hasAny) {
274
+ container?.setAttribute('data-harness-mix-has-mentions', 'true');
275
+ } else {
276
+ container?.removeAttribute('data-harness-mix-has-mentions');
277
+ }
278
+
80
279
  if (!strip && !hasAny) return;
81
280
  if (!strip) {
82
- strip = document.createElement('div'); strip.dataset.harnessMixSelectedMentions = 'true';
281
+ strip = document.createElement('div');
282
+ strip.dataset.harnessMixSelectedMentions = 'true';
83
283
  strip.setAttribute('aria-label', '已提及的协作者');
84
- strip.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap;padding:6px 0';
85
- target.after(strip); badges.set(target, strip);
284
+ // Place INLINE before target inside the input flow like Image 2, not as a block row below.
285
+ strip.style.cssText = 'display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap;padding:0;margin:0 4px 0 0;vertical-align:baseline;line-height:normal;';
286
+ target.before(strip);
287
+ badges.set(target, strip);
288
+ }
289
+
290
+ const newKeys = perEditor ? [...perEditor.values()].map(e => e.kind === 'agent' ? `agent:${e.id}` : e.kind === 'session' ? `session:${e.id}` : `codex:${e.id}`).join(',') : '';
291
+ const currentKeys = [...strip.querySelectorAll<HTMLElement>('[data-harness-mix-mention-badge]')].map(b => b.dataset.harnessMixMentionBadge).join(',');
292
+ if (strip.isConnected && currentKeys === newKeys && strip.style.display === (hasAny ? 'inline-flex' : 'none')) {
293
+ return;
86
294
  }
87
- strip.replaceChildren(); strip.style.display = hasAny ? 'flex' : 'none';
295
+
296
+ strip.replaceChildren();
297
+ strip.style.display = hasAny ? 'inline-flex' : 'none';
88
298
  if (!perEditor) return;
299
+
89
300
  for (const entry of perEditor.values()) {
90
- const badge = document.createElement('span');
91
- badge.dataset.harnessMixMentionBadge = entry.kind === 'agent' ? `agent:${entry.id}` : `session:${entry.id}`;
92
- badge.style.cssText = 'display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border:1px solid #8883;border-radius:8px;font:12px inherit;background:#8881';
93
- const iconWrap = document.createElement('span');
94
- iconWrap.style.cssText = 'display:inline-flex;align-items:center;gap:6px';
95
- iconWrap.append(
96
- collaborationIcon(entry.kind === 'agent' ? entry.id : entry.harnessId, entry.kind === 'agent' ? entry.name : entry.title, 16),
97
- document.createTextNode(entry.kind === 'agent' ? entry.name : entry.title),
98
- );
99
- const remove = document.createElement('button');
100
- remove.type = 'button';
101
- remove.dataset.harnessMixMentionRemove = entry.kind === 'agent' ? `agent:${entry.id}` : `session:${entry.id}`;
102
- const removeLabel = entry.kind === 'agent' ? `移除 ${entry.name}` : `移除会话 ${entry.title}`;
103
- remove.setAttribute('aria-label', removeLabel);
104
- remove.title = removeLabel;
105
- remove.textContent = '×';
106
- remove.style.cssText = 'border:0;background:transparent;color:inherit;cursor:pointer;padding:0 0 0 2px;margin:0;font:600 14px/1 inherit;opacity:.55;line-height:1;display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border-radius:50%';
107
- remove.addEventListener('mouseenter', () => { remove.style.opacity = '1'; remove.style.background = '#8883'; });
108
- remove.addEventListener('mouseleave', () => { remove.style.opacity = '.55'; remove.style.background = 'transparent'; });
109
- remove.addEventListener('mousedown', event => { event.preventDefault(); event.stopPropagation(); });
110
- remove.addEventListener('click', event => {
301
+ const badge = document.createElement('button');
302
+ badge.type = 'button';
303
+ badge.dataset.harnessMixMentionBadge = entry.kind === 'agent'
304
+ ? `agent:${entry.id}`
305
+ : entry.kind === 'session'
306
+ ? `session:${entry.id}`
307
+ : `codex:${entry.id}`;
308
+
309
+ const removeLabel = entry.kind === 'agent'
310
+ ? `移除协作 Agent ${entry.name}`
311
+ : entry.kind === 'session'
312
+ ? `移除历史会话 ${entry.title}`
313
+ : `移除 Codex 选项 ${entry.title}`;
314
+ badge.setAttribute('aria-label', removeLabel);
315
+ badge.title = `${removeLabel}(点击移除)`;
316
+
317
+ const agentColor = entry.kind === 'agent'
318
+ ? getAgentColor(entry.id)
319
+ : entry.kind === 'codex'
320
+ ? '#0ea5e9'
321
+ : '#8b5cf6';
322
+
323
+ badge.style.cssText = `display:inline-flex;align-items:center;gap:4px;padding:1px 5px;border:0;border-radius:4px;background:transparent;color:${agentColor};font:500 15px/1.4 system-ui,-apple-system,BlinkMacSystemFont,sans-serif;user-select:none;cursor:pointer;transition:background 0.15s,opacity 0.15s;`;
324
+ badge.addEventListener('mouseenter', () => { badge.style.background = 'rgba(128,128,128,0.12)'; });
325
+ badge.addEventListener('mouseleave', () => { badge.style.background = 'transparent'; });
326
+ badge.addEventListener('click', event => {
111
327
  event.preventDefault();
112
328
  event.stopPropagation();
113
- const live = selections.get(target);
114
- if (!live) return;
329
+ const live = selectionsFor(target);
115
330
  live.delete(entryKey(entry));
116
331
  syncBadges(target);
332
+ target.focus();
117
333
  });
118
- badge.append(iconWrap, remove);
119
- badge.title = entry.kind === 'agent' ? `@${entry.id}${entry.available ? '' : ' · 未就绪'}` : `历史会话 · ${entry.cwd}`;
334
+
335
+ const iconWrap = document.createElement('span');
336
+ iconWrap.style.cssText = 'display:inline-flex;align-items:center;gap:4px;';
337
+ if (entry.kind === 'agent' || entry.kind === 'session') {
338
+ iconWrap.append(
339
+ collaborationIcon(entry.kind === 'agent' ? entry.id : entry.harnessId, entry.kind === 'agent' ? entry.name : entry.title, 16),
340
+ document.createTextNode(entry.kind === 'agent' ? entry.name : entry.title),
341
+ );
342
+ } else {
343
+ const iconSpan = document.createElement('span');
344
+ iconSpan.textContent = entry.icon || '📌';
345
+ iconSpan.style.cssText = 'font-size:14px;line-height:1;';
346
+ iconWrap.append(iconSpan, document.createTextNode(entry.title));
347
+ }
348
+
349
+ badge.append(iconWrap);
120
350
  strip.append(badge);
121
351
  }
122
352
  };
123
- const close = () => { generation++; menu.hidden = true; activeComposer?.removeAttribute('data-harness-mix-mention-active'); activeComposer = null; };
353
+
354
+ const close = () => {
355
+ generation++;
356
+ menu.hidden = true;
357
+ activeComposer?.removeAttribute('data-harness-mix-mention-active');
358
+ activeComposer?.removeAttribute('data-harness-mix-show-native');
359
+ activeComposer = null;
360
+ };
361
+
124
362
  const choose = (entry: MentionEntry) => {
125
- if (!editor || (entry.kind === 'agent' ? !entry.available || !canDelegate : entry.running === true)) return;
363
+ if (!editor) return;
364
+ if (entry.kind === 'agent' && (!entry.available || !canDelegate)) return;
365
+ if (entry.kind === 'session' && entry.running === true) return;
126
366
  const target = editor;
127
367
  close();
128
368
  target.focus();
129
- // Drop the @query trigger text the user typed to open the picker so the
130
- // editor stays free of literal @agent handles. The chip below carries
131
- // the same intent and is the only visible signal.
132
369
  replaceSelectionWith(target, '');
133
- // Toggle the entry in the per-editor selections; a second click removes it.
370
+
371
+ if (entry.kind === 'codex') {
372
+ const nativeOverlay = document.querySelector('[data-composer-overlay-floating-ui="true"]');
373
+ if (nativeOverlay) {
374
+ const buttons = [...nativeOverlay.querySelectorAll<HTMLElement>('button, [role="option"]')];
375
+ const match = buttons.find(b => (b.innerText || b.textContent || '').includes(entry.title));
376
+ if (match) {
377
+ match.click();
378
+ return;
379
+ }
380
+ }
381
+ if (entry.action === 'files') {
382
+ const fileInput = document.querySelector<HTMLInputElement>('input[type="file"]');
383
+ fileInput?.click();
384
+ }
385
+ return;
386
+ }
387
+
134
388
  const perEditor = selectionsFor(target);
135
389
  const key = entryKey(entry);
136
390
  if (perEditor.has(key)) perEditor.delete(key);
137
391
  else perEditor.set(key, entry);
138
392
  syncBadges(target);
139
393
  };
394
+
140
395
  const render = () => {
141
396
  menu.replaceChildren();
142
- const tabs = document.createElement('div'); tabs.setAttribute('role', 'tablist');
397
+ const tabs = document.createElement('div');
398
+ tabs.setAttribute('role', 'tablist');
143
399
  tabs.style.cssText = 'display:flex;gap:4px;padding:2px 2px 6px;border-bottom:1px solid #8882;margin-bottom:4px';
144
- for (const kind of ['agent', 'session'] as const) {
145
- const tab = document.createElement('button'); tab.type = 'button'; tab.setAttribute('role', 'tab');
146
- tab.setAttribute('aria-selected', String(activeKind === kind));
147
- tab.textContent = `${kind === 'agent' ? 'Agents' : '会话'} ${matches.filter(entry => entry.kind === kind).length}`;
148
- tab.style.cssText = `border:0;border-radius:6px;padding:6px 10px;color:inherit;font:inherit;cursor:pointer;background:${activeKind === kind ? '#8882' : 'transparent'}`;
400
+
401
+ const tabDefinitions: Array<{ kind: 'agent' | 'session'; label: string }> = [
402
+ { kind: 'agent', label: 'Agents' },
403
+ { kind: 'session', label: '会话' },
404
+ ];
405
+
406
+ for (const tabInfo of tabDefinitions) {
407
+ const tab = document.createElement('button');
408
+ tab.type = 'button';
409
+ tab.setAttribute('role', 'tab');
410
+ tab.setAttribute('aria-selected', String(activeKind === tabInfo.kind));
411
+ const count = matches.filter(entry => entry.kind === tabInfo.kind).length;
412
+ tab.textContent = `${tabInfo.label} ${count}`;
413
+ tab.style.cssText = `border:0;border-radius:6px;padding:6px 10px;color:inherit;font:inherit;cursor:pointer;background:${activeKind === tabInfo.kind ? '#8882' : 'transparent'};transition:background .15s;`;
149
414
  tab.addEventListener('mousedown', event => event.preventDefault());
150
- tab.addEventListener('click', () => { activeKind = kind; selected = 0; render(); }); tabs.append(tab);
415
+ tab.addEventListener('click', () => {
416
+ activeKind = tabInfo.kind;
417
+ selected = 0;
418
+ render();
419
+ });
420
+ tabs.append(tab);
151
421
  }
152
422
  menu.append(tabs);
423
+
153
424
  entries = matches.filter(entry => entry.kind === activeKind);
154
425
  if (!entries.length || (activeKind === 'agent' && !canDelegate)) {
155
426
  const notice = document.createElement('div');
156
427
  notice.textContent = !canDelegate && activeKind === 'agent'
157
428
  ? '当前主 Agent 未接入协作工具。请切换到 Codex(协作)、Claude Code、Pi 或 Oh My Pi 后分派任务。'
158
- : activeKind === 'agent' ? '没有匹配的 Agent' : '没有匹配的历史会话';
159
- notice.style.cssText = 'padding:10px 8px;font-size:12px;line-height:1.6;opacity:.65'; menu.append(notice);
429
+ : activeKind === 'agent'
430
+ ? '没有匹配的 Agent'
431
+ : '没有匹配的历史会话';
432
+ notice.style.cssText = 'padding:10px 8px;font-size:12px;line-height:1.6;opacity:.65';
433
+ menu.append(notice);
160
434
  }
161
- const list = document.createElement('div'); list.setAttribute('role', 'listbox'); list.setAttribute('aria-label', activeKind === 'agent' ? '协作 Agents' : '历史会话'); menu.append(list);
435
+
436
+ const list = document.createElement('div');
437
+ list.setAttribute('role', 'listbox');
438
+ list.setAttribute('aria-label', activeKind === 'agent' ? '协作 Agents' : '历史会话');
439
+ menu.append(list);
440
+
162
441
  entries.forEach((entry, index) => {
163
442
  const row = document.createElement('button');
164
- row.type = 'button'; row.setAttribute('role', 'option');
443
+ row.type = 'button';
444
+ row.setAttribute('role', 'option');
165
445
  row.setAttribute('aria-selected', String(index === selected));
166
- const available = entry.kind === 'agent' ? entry.available && canDelegate : entry.running !== true;
446
+ const available = entry.kind === 'agent'
447
+ ? entry.available && canDelegate
448
+ : entry.kind === 'session'
449
+ ? entry.running !== true
450
+ : true;
167
451
  row.disabled = !available;
168
452
  row.id = `harness-mention-option-${index}`;
169
453
  row.style.cssText = `display:flex;align-items:center;gap:8px;width:100%;text-align:left;border:0;border-radius:7px;padding:7px 8px;color:inherit;font:inherit;cursor:pointer;background:${index === selected ? '#8882' : 'transparent'};opacity:${available ? 1 : .45}`;
170
- const copy = document.createElement('span'); copy.style.cssText = 'display:flex;flex-direction:column;gap:3px;flex:1;min-width:0';
171
- const name = document.createElement('span'); name.textContent = entry.kind === 'agent' ? entry.name : entry.title; name.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
172
- const detail = document.createElement('span'); detail.textContent = entry.kind === 'agent' ? `@${entry.id}` : entry.cwd; detail.style.cssText = 'font-size:11px;opacity:.55;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
454
+
455
+ const copy = document.createElement('span');
456
+ copy.style.cssText = 'display:flex;flex-direction:column;gap:3px;flex:1;min-width:0';
457
+ const name = document.createElement('span');
458
+ name.textContent = entry.kind === 'agent' ? entry.name : entry.title;
459
+ name.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500;';
460
+
461
+ const detail = document.createElement('span');
462
+ detail.textContent = entry.kind === 'agent'
463
+ ? (AGENT_DESCRIPTIONS[entry.id] || entry.description || `#${entry.id}`)
464
+ : entry.kind === 'session'
465
+ ? entry.cwd
466
+ : (entry.description || entry.category);
467
+ detail.style.cssText = 'font-size:11px;opacity:.55;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
173
468
  copy.append(name, detail);
174
- const status = document.createElement('span'); status.textContent = entry.kind === 'agent' ? (entry.available ? '可协作' : '未就绪') : (entry.running ? '运行中' : '引用'); status.style.cssText = 'font-size:11px;opacity:.6';
175
- row.append(collaborationIcon(entry.kind === 'agent' ? entry.id : entry.harnessId, name.textContent || ''), copy, status);
469
+
470
+ const status = document.createElement('span');
471
+ status.textContent = entry.kind === 'agent'
472
+ ? (entry.available ? '可协作' : '未就绪')
473
+ : entry.kind === 'session'
474
+ ? (entry.running ? '运行中' : '引用')
475
+ : (entry.category || '原生');
476
+ status.style.cssText = 'font-size:11px;opacity:.6';
477
+
478
+ let iconEl: HTMLElement;
479
+ if (entry.kind === 'agent' || entry.kind === 'session') {
480
+ iconEl = collaborationIcon(entry.kind === 'agent' ? entry.id : entry.harnessId, name.textContent || '', 18);
481
+ } else {
482
+ const iconSpan = document.createElement('span');
483
+ iconSpan.textContent = entry.icon || '📎';
484
+ iconSpan.style.cssText = 'font-size:16px;line-height:1;width:18px;text-align:center;';
485
+ iconEl = iconSpan;
486
+ }
487
+
488
+ row.append(iconEl, copy, status);
176
489
  row.addEventListener('mousedown', event => event.preventDefault());
177
490
  row.addEventListener('click', () => choose(entry));
178
491
  list.append(row);
179
492
  });
180
493
  };
494
+
181
495
  const input = async (event: Event) => {
182
496
  const target = event.target;
183
497
  if (!(target instanceof HTMLElement) || !target.closest('[data-codex-composer-root]')) return;
@@ -185,42 +499,66 @@ export function installHarnessMentions(load: (editor: Element, query: string) =>
185
499
  if (!(editable instanceof HTMLElement)) return;
186
500
  editor = editable;
187
501
  syncBadges(editable);
502
+
188
503
  let prefix = '';
189
504
  range = null;
190
505
  if (editable instanceof HTMLTextAreaElement) {
191
506
  if (editable.selectionStart !== editable.selectionEnd) { close(); return; }
192
- end = editable.selectionStart; prefix = editable.value.slice(0, end);
507
+ end = editable.selectionStart;
508
+ prefix = editable.value.slice(0, end);
193
509
  } else {
194
510
  const selection = window.getSelection();
195
511
  if (!selection?.rangeCount || !selection.isCollapsed) { close(); return; }
196
512
  const caret = selection.getRangeAt(0);
197
513
  if (!editable.contains(caret.startContainer) || caret.startContainer.nodeType !== Node.TEXT_NODE) { close(); return; }
198
- const before = caret.cloneRange(); before.selectNodeContents(editable); before.setEnd(caret.startContainer, caret.startOffset);
199
- prefix = before.toString(); end = prefix.length;
514
+ const before = caret.cloneRange();
515
+ before.selectNodeContents(editable);
516
+ before.setEnd(caret.startContainer, caret.startOffset);
517
+ prefix = before.toString();
518
+ end = prefix.length;
200
519
  range = caret.cloneRange();
201
520
  }
202
- const match = /(?:^|[\s,。;:])@([\p{L}\p{N}_-]*)$/u.exec(prefix);
521
+
522
+ const match = /(?:^|[\s,。;:])#([\p{L}\p{N}_-]*)$/u.exec(prefix);
203
523
  if (!match) { close(); return; }
204
524
  const length = match[1]!.length + 1;
205
525
  start = end - length;
206
526
  const current = ++generation;
527
+
207
528
  try {
208
529
  const catalog = await load(editable, match[1]!.toLowerCase());
209
530
  if (disposed || current !== generation) return;
210
- catalogs.set(editable, catalog); syncBadges(editable);
531
+ catalogs.set(editable, catalog);
532
+ syncBadges(editable);
533
+
211
534
  const query = match[1]!.toLowerCase();
212
535
  canDelegate = catalog.canDelegate !== false;
213
- matches = [
214
- ...catalog.agents.filter(a => a.id.toLowerCase().includes(query) || a.name.toLowerCase().includes(query)).map(a => ({ kind: 'agent' as const, ...a })),
215
- ...catalog.sessions.filter(session => `${session.title} ${session.cwd}`.toLowerCase().includes(query)).map(session => ({ kind: 'session' as const, ...session })),
216
- ];
217
- activeKind = query && !matches.some(entry => entry.kind === 'agent') && matches.some(entry => entry.kind === 'session') ? 'session' : 'agent';
536
+
537
+ const matchingAgents: MentionEntry[] = catalog.agents
538
+ .filter(a => a.id.toLowerCase().includes(query) || a.name.toLowerCase().includes(query))
539
+ .map(a => ({ kind: 'agent' as const, title: a.name, ...a }));
540
+
541
+ const matchingSessions: MentionEntry[] = catalog.sessions
542
+ .filter(s => `${s.title} ${s.cwd}`.toLowerCase().includes(query))
543
+ .map(s => ({ kind: 'session' as const, ...s }));
544
+
545
+ matches = [...matchingAgents, ...matchingSessions];
546
+
547
+ if (query && !matchingAgents.length && matchingSessions.length) {
548
+ activeKind = 'session';
549
+ } else {
550
+ activeKind = 'agent';
551
+ }
552
+
218
553
  entries = matches.filter(entry => entry.kind === activeKind);
219
- selected = Math.max(0, entries.findIndex(entry => entry.kind === 'agent' ? entry.available && canDelegate : entry.running !== true));
220
- render(); menu.hidden = !catalog.agents.length && !catalog.sessions.length && catalog.canDelegate === undefined;
554
+ selected = Math.max(0, entries.findIndex(entry => entry.kind === 'agent' ? entry.available && canDelegate : entry.kind === 'session' ? entry.running !== true : true));
555
+
556
+ render();
557
+ menu.hidden = !matches.length && catalog.canDelegate === undefined;
221
558
  activeComposer?.removeAttribute('data-harness-mix-mention-active');
222
559
  activeComposer = !menu.hidden ? editable.closest('[data-codex-composer-root]') : null;
223
560
  activeComposer?.setAttribute('data-harness-mix-mention-active', 'true');
561
+
224
562
  menu.scrollTop = 0;
225
563
  const rect = editable.getBoundingClientRect();
226
564
  menu.style.left = `${Math.max(8, Math.min(rect.left, window.innerWidth - 348))}px`;
@@ -230,35 +568,75 @@ export function installHarnessMentions(load: (editor: Element, query: string) =>
230
568
  menu.style.top = above >= 160 ? 'auto' : `${rect.bottom + 8}px`;
231
569
  } catch { close(); }
232
570
  };
571
+
233
572
  const keydown = (event: KeyboardEvent) => {
573
+ const target = event.target;
574
+ if (target instanceof HTMLElement && target.closest('[data-codex-composer-root]')) {
575
+ const editable = target.closest('textarea,[contenteditable="true"]') as HTMLElement | null;
576
+ if (editable && event.key === 'Backspace') {
577
+ let isAtStart = false;
578
+ if (editable instanceof HTMLTextAreaElement) {
579
+ isAtStart = editable.selectionStart === 0 && editable.selectionEnd === 0;
580
+ } else {
581
+ const sel = window.getSelection();
582
+ if (sel && sel.isCollapsed && sel.rangeCount > 0) {
583
+ const r = sel.getRangeAt(0);
584
+ isAtStart = r.startOffset === 0 && (r.startContainer === editable || r.startContainer.textContent === '' || editable.firstChild === r.startContainer);
585
+ }
586
+ }
587
+ if (isAtStart) {
588
+ const perSession = selectionsFor(editable);
589
+ if (perSession && perSession.size > 0) {
590
+ const keys = [...perSession.keys()];
591
+ const lastKey = keys[keys.length - 1];
592
+ if (lastKey) perSession.delete(lastKey);
593
+ syncBadges(editable);
594
+ event.preventDefault();
595
+ return;
596
+ }
597
+ }
598
+ }
599
+ }
600
+
234
601
  if (menu.hidden || event.isComposing) return;
235
- // Let the native editor dismiss its own mention state as well.
236
602
  if (event.key === 'Escape') { close(); return; }
237
603
  if (!['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'Enter', 'Tab', 'Escape'].includes(event.key)) return;
238
604
  event.preventDefault(); event.stopImmediatePropagation();
239
- if (event.key === 'Escape') close();
240
- else if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { activeKind = activeKind === 'agent' ? 'session' : 'agent'; selected = 0; render(); }
241
- else if (event.key === 'Enter' || event.key === 'Tab') { const entry = entries[selected]; if (entry) choose(entry); }
242
- else {
243
- for (let i = 0; i < entries.length; i++) { selected = (selected + (event.key === 'ArrowDown' ? 1 : -1) + entries.length) % entries.length; const entry = entries[selected]; if (entry && (entry.kind === 'agent' ? entry.available && canDelegate : entry.running !== true)) break; }
244
- render(); menu.querySelector('[role="option"][aria-selected="true"]')?.scrollIntoView({ block: 'nearest' });
605
+
606
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
607
+ const kinds: Array<'agent' | 'session'> = ['agent', 'session'];
608
+ const curIdx = kinds.indexOf(activeKind as 'agent' | 'session');
609
+ const nextIdx = (curIdx + (event.key === 'ArrowRight' ? 1 : -1) + kinds.length) % kinds.length;
610
+ const next = kinds[nextIdx];
611
+ if (next) activeKind = next;
612
+ selected = 0;
613
+ render();
614
+ } else if (event.key === 'Enter' || event.key === 'Tab') {
615
+ const entry = entries[selected];
616
+ if (entry) choose(entry);
617
+ } else {
618
+ for (let i = 0; i < entries.length; i++) {
619
+ selected = (selected + (event.key === 'ArrowDown' ? 1 : -1) + entries.length) % entries.length;
620
+ const entry = entries[selected];
621
+ if (entry && (entry.kind === 'agent' ? entry.available && canDelegate : entry.kind === 'session' ? entry.running !== true : true)) break;
622
+ }
623
+ render();
624
+ menu.querySelector('[role="option"][aria-selected="true"]')?.scrollIntoView({ block: 'nearest' });
245
625
  }
246
626
  };
247
- const outside = (event: Event) => { if (event.target instanceof Node && !menu.contains(event.target)) close(); };
248
-
249
- // The Host detects mentions by parsing `@agent` literals out of the submitted
250
- // text. The composer no longer carries those literals while the user is
251
- // typing, so before the native form submit reads the editor we re-attach
252
- // the mentions at the start of the value. The editor then clears itself
253
- // after Codex Desktop finishes dispatching the turn, hiding the literal
254
- // text again.
627
+
628
+ const outside = (event: Event) => {
629
+ if (event.target instanceof Node && !menu.contains(event.target)) close();
630
+ };
631
+
255
632
  const findEditorIn = (root: Element | null): HTMLElement | null => {
256
633
  if (!root) return null;
257
634
  if (root.matches('textarea,[contenteditable="true"]')) return root as HTMLElement;
258
635
  return root.querySelector<HTMLElement>('textarea,[contenteditable="true"]');
259
636
  };
637
+
260
638
  const injectMentionsInto = (target: HTMLElement): boolean => {
261
- const perEditor = selections.get(target);
639
+ const perEditor = selectionsFor(target);
262
640
  if (!perEditor || perEditor.size === 0) return false;
263
641
  const text = [...perEditor.values()].map(entryLiteral).join('');
264
642
  if (!text) return false;
@@ -267,18 +645,21 @@ export function installHarnessMentions(load: (editor: Element, query: string) =>
267
645
  target.value = `${text}${target.value}`;
268
646
  target.dispatchEvent(new Event('input', { bubbles: true }));
269
647
  target.dispatchEvent(new Event('change', { bubbles: true }));
270
- return true;
648
+ } else {
649
+ const selection = window.getSelection();
650
+ const fresh = document.createRange();
651
+ fresh.selectNodeContents(target);
652
+ fresh.collapse(true);
653
+ selection?.removeAllRanges();
654
+ selection?.addRange(fresh);
655
+ document.dispatchEvent(new Event('selectionchange'));
656
+ document.execCommand('insertText', false, text);
271
657
  }
272
- const selection = window.getSelection();
273
- const fresh = document.createRange();
274
- fresh.selectNodeContents(target);
275
- fresh.collapse(true);
276
- selection?.removeAllRanges();
277
- selection?.addRange(fresh);
278
- document.dispatchEvent(new Event('selectionchange'));
279
- document.execCommand('insertText', false, text);
658
+ perEditor.clear();
659
+ syncBadges(target);
280
660
  return true;
281
661
  };
662
+
282
663
  const onSubmitCapture = (event: Event) => {
283
664
  const composerRoot = (event.target instanceof Element ? event.target : null)?.closest?.('[data-codex-composer-root]');
284
665
  const candidate = composerRoot ?? (event.target instanceof Element ? event.target : null);
@@ -288,19 +669,71 @@ export function installHarnessMentions(load: (editor: Element, query: string) =>
288
669
  injectMentionsInto(editorEl);
289
670
  };
290
671
 
672
+ const onFocusIn = (event: FocusEvent) => {
673
+ const target = event.target;
674
+ if (target instanceof HTMLElement && target.closest('[data-codex-composer-root]')) {
675
+ const editable = target.closest('textarea,[contenteditable="true"]') as HTMLElement | null;
676
+ if (editable) syncBadges(editable);
677
+ }
678
+ };
679
+
680
+ const observer = new MutationObserver((mutations) => {
681
+ let shouldSync = false;
682
+ for (const m of mutations) {
683
+ if (m.target instanceof Element) {
684
+ if (m.target.closest('[data-harness-mix-selected-mentions]') || m.target.closest('[data-harness-mix-mentions]')) {
685
+ continue;
686
+ }
687
+ }
688
+ shouldSync = true;
689
+ break;
690
+ }
691
+ if (!shouldSync) return;
692
+ const composers = document.querySelectorAll('[data-codex-composer-root]');
693
+ for (const composer of composers) {
694
+ const el = findEditorIn(composer);
695
+ if (el) syncBadges(el);
696
+ }
697
+ });
698
+
699
+ try {
700
+ observer.observe(document.body, {
701
+ childList: true,
702
+ subtree: true,
703
+ attributes: true,
704
+ attributeFilter: ['data-above-composer-conversation-id', 'data-conversation-id', 'data-composer-draft-id'],
705
+ });
706
+ } catch {}
707
+
291
708
  document.addEventListener('input', input, true);
292
709
  document.addEventListener('keydown', keydown, true);
293
710
  document.addEventListener('mousedown', outside);
294
711
  document.addEventListener('submit', onSubmitCapture, true);
712
+ document.addEventListener('focusin', onFocusIn, true);
295
713
  window.addEventListener('blur', close);
296
- return { dispose() {
297
- disposed = true; close(); menu.remove(); style.remove();
298
- for (const strip of badges.values()) strip.remove(); badges.clear(); catalogs.clear();
299
- for (const [target, perEditor] of selections) if (!target.isConnected) selections.delete(target);
300
- document.removeEventListener('input', input, true);
301
- document.removeEventListener('keydown', keydown, true);
302
- document.removeEventListener('mousedown', outside);
303
- document.removeEventListener('submit', onSubmitCapture, true);
304
- window.removeEventListener('blur', close);
305
- } };
306
- }
714
+
715
+ return {
716
+ sync(target: HTMLElement) {
717
+ syncBadges(target);
718
+ },
719
+ dispose() {
720
+ disposed = true;
721
+ close();
722
+ menu.remove();
723
+ style.remove();
724
+ observer.disconnect();
725
+ for (const el of document.querySelectorAll('[data-harness-mix-has-mentions]')) {
726
+ el.removeAttribute('data-harness-mix-has-mentions');
727
+ }
728
+ for (const strip of badges.values()) strip.remove();
729
+ badges.clear();
730
+ catalogs.clear();
731
+ document.removeEventListener('input', input, true);
732
+ document.removeEventListener('keydown', keydown, true);
733
+ document.removeEventListener('mousedown', outside);
734
+ document.removeEventListener('submit', onSubmitCapture, true);
735
+ document.removeEventListener('focusin', onFocusIn, true);
736
+ window.removeEventListener('blur', close);
737
+ },
738
+ };
739
+ }