@harness-mix/cli 0.1.4 → 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.
- package/CHANGELOG.md +8 -0
- package/README.md +26 -96
- package/docs/chatgpt-web-sidebar.md +7 -1
- package/docs/cross-platform.md +15 -0
- package/docs/harness-handoff-design.md +377 -0
- package/docs/harness-management.md +28 -1
- package/docs/multi-agent-collaboration.md +9 -9
- package/docs/native-codex.md +21 -10
- package/output/native-build/harness-mix-shim.exe +0 -0
- package/output/native-build/renderer-extension.js +2596 -135
- package/package.json +20 -10
- package/scripts/chatgpt-context-ui-smoke.cjs +16 -0
- package/scripts/codex-accounts-test.cjs +81 -0
- package/scripts/codex-accounts-ui-smoke.cjs +70 -0
- package/scripts/collaboration-test.cjs +8 -3
- package/scripts/collaboration-ui-smoke.cjs +40 -26
- package/scripts/e2e-integrations.cjs +52 -0
- package/scripts/e2e-native-host.cjs +39 -0
- package/scripts/handoff-checkpoint-test.cjs +83 -0
- package/scripts/install-native.cjs +4 -1
- package/scripts/integrations-test.cjs +123 -0
- package/scripts/integrations-ui-smoke.cjs +124 -0
- package/scripts/native-protocol-test.cjs +180 -5
- package/scripts/native-secret-test.cjs +2 -2
- package/scripts/native-shim-test.cjs +20 -4
- package/scripts/native-sidebar-test.cjs +7 -0
- package/scripts/native-skill-roots-test.cjs +90 -0
- package/scripts/native-ui-smoke.cjs +58 -2
- package/scripts/publish-native-package.cjs +8 -5
- package/scripts/support/integrations-mcp-fixture.cjs +15 -0
- package/scripts/switch-harness-test.cjs +27 -1
- package/scripts/test-live-mentions.cjs +152 -0
- package/src/main/adapters/acp.js +3 -3
- package/src/main/adapters/antigravity.js +17 -17
- package/src/main/adapters/claude.js +50 -10
- package/src/main/adapters/codebuddy.js +1 -0
- package/src/main/adapters/codex-app-server.js +20 -13
- package/src/main/adapters/codex.js +25 -5
- package/src/main/adapters/cursor.js +5 -1
- package/src/main/adapters/dsh.js +9 -1
- package/src/main/adapters/grok.js +7 -3
- package/src/main/adapters/hermes.js +8 -0
- package/src/main/adapters/kiro.js +2 -1
- package/src/main/adapters/managed-mcp.js +25 -0
- package/src/main/adapters/native-acp.js +4 -4
- package/src/main/adapters/omp.js +11 -0
- package/src/main/adapters/openclaw.js +6 -0
- package/src/main/adapters/opencode.js +13 -3
- package/src/main/adapters/pi.js +5 -0
- package/src/main/adapters/qoder.js +1 -0
- package/src/main/adapters/trae.js +4 -0
- package/src/main/adapters/zcode.js +3 -0
- package/src/main/harness-adapter/manifest.js +17 -2
- package/src/main/host/collaboration.js +57 -11
- package/src/main/host/handoff-access.js +53 -0
- package/src/main/host/handoff-checkpoints.js +206 -0
- package/src/main/host/handoff-mcp.cjs +38 -0
- package/src/main/host/handoff-tools.js +13 -0
- package/src/main/host/handoff.js +28 -6
- package/src/main/host/integrations.js +339 -0
- package/src/main/host/runtime.js +95 -15
- package/src/main/host/store.js +4 -4
- package/src/main/native/codex-accounts.js +260 -0
- package/src/main/native/host.js +1 -1
- package/src/main/native/launcher.js +2 -2
- package/src/main/native/protocol.js +176 -14
- package/src/main/native/redact.js +1 -1
- package/src/main/native/rs/crates/shim/src/main.rs +155 -12
- package/src/main/native/thread-list.js +3 -0
- package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/index.d.ts +4 -0
- package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts +36 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts +3 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts +53 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts +11 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +105 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +7 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +6 -0
- package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +5 -0
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +8 -4
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +4 -1
- package/src/native-ui/renderer-extension/src/index.ts +28 -0
- package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +4 -4
- package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +46 -6
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +115 -18
- package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +87 -0
- package/src/native-ui/renderer-extension/src/renderer-codex-account-state.ts +10 -0
- package/src/native-ui/renderer-extension/src/renderer-composer-dom.ts +10 -0
- package/src/native-ui/renderer-extension/src/renderer-harness-handoff.ts +330 -0
- package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +556 -123
- package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +59 -0
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +17 -1
- package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
- package/src/native-ui/renderer-extension/src/settings/accounts-list.ts +16 -1
- package/src/native-ui/renderer-extension/src/settings/accounts-page.ts +32 -3
- package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +1474 -0
- package/src/native-ui/renderer-extension/src/settings/localization.ts +32 -13
- package/src/native-ui/renderer-extension/src/settings/pages.ts +49 -18
- package/src/native-ui/renderer-extension/src/settings/shell.css +103 -0
- package/src/native-ui/renderer-extension/test/renderer-agent-picker.test.ts +44 -13
- package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +21 -0
- package/src/native-ui/renderer-extension/test/renderer-codex-account-state.test.ts +13 -1
- package/src/native-ui/renderer-extension/test/renderer-harness-handoff.test.ts +20 -0
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +9 -2
- package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
- package/src/native-ui/renderer-extension/test/settings/pages.test.ts +77 -8
- package/src/native-ui/renderer-extension/test/settings/shell.test.ts +6 -0
- package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts +19 -4
- package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/codex-accounts.js +2 -0
- package/src/native-ui/shared-contracts/dist/codex-accounts.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/index.d.ts +2 -2
- package/src/native-ui/shared-contracts/dist/index.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/index.js +1 -1
- package/src/native-ui/shared-contracts/dist/index.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +29 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.js +13 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/shared-contracts/src/codex-accounts.ts +2 -0
- package/src/native-ui/shared-contracts/src/index.ts +8 -1
- package/src/native-ui/shared-contracts/src/thread-harness-switch.ts +17 -0
|
@@ -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
|
-
|
|
3
|
+
export interface CollaborationAgent {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
available: boolean;
|
|
7
|
+
lead: boolean;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
:
|
|
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
|
-
|
|
24
|
-
//
|
|
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 =
|
|
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[] = []
|
|
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-
|
|
38
|
-
//
|
|
39
|
-
const
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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)
|
|
77
|
-
|
|
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');
|
|
281
|
+
strip = document.createElement('div');
|
|
282
|
+
strip.dataset.harnessMixSelectedMentions = 'true';
|
|
83
283
|
strip.setAttribute('aria-label', '已提及的协作者');
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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('
|
|
91
|
-
badge.
|
|
92
|
-
badge.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 =
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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');
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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', () => {
|
|
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'
|
|
159
|
-
|
|
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
|
-
|
|
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';
|
|
443
|
+
row.type = 'button';
|
|
444
|
+
row.setAttribute('role', 'option');
|
|
165
445
|
row.setAttribute('aria-selected', String(index === selected));
|
|
166
|
-
const available = entry.kind === 'agent'
|
|
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
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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;
|
|
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();
|
|
199
|
-
|
|
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
|
-
|
|
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);
|
|
531
|
+
catalogs.set(editable, catalog);
|
|
532
|
+
syncBadges(editable);
|
|
533
|
+
|
|
211
534
|
const query = match[1]!.toLowerCase();
|
|
212
535
|
canDelegate = catalog.canDelegate !== false;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
}
|