@srgay/cursor-extension 1.0.0
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/LICENSE +21 -0
- package/README.md +244 -0
- package/bin/cli.mjs +105 -0
- package/package.json +38 -0
- package/src/ime-enter-fix/cursor-ime-enter-fix.js +101 -0
- package/src/ime-enter-fix/install-cursor-ime-enter-fix.mjs +89 -0
- package/src/max-mode-guard/cursor-max-mode-guard.js +519 -0
- package/src/max-mode-guard/install-cursor-max-mode-guard.mjs +89 -0
- package/src/mcp-followup/cursor-mcp-followup.js +1636 -0
- package/src/mcp-followup/install-cursor-mcp-followup.mjs +93 -0
- package/src/shared/cursor-workbench-paths.mjs +46 -0
- package/src/shared/patch-cursor-workbench.mjs +177 -0
- package/src/shared/unpatch-cursor-workbench.mjs +96 -0
|
@@ -0,0 +1,1636 @@
|
|
|
1
|
+
(function installCursorMcpFollowup() {
|
|
2
|
+
const NAME = "__cursorMcpFollowup";
|
|
3
|
+
|
|
4
|
+
if (window[NAME] && typeof window[NAME].uninstall === "function") {
|
|
5
|
+
window[NAME].uninstall();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const config = {
|
|
9
|
+
styleId: "cursor-mcp-followup-style",
|
|
10
|
+
panelId: "cursor-mcp-followup-panel",
|
|
11
|
+
mountScanMs: 600,
|
|
12
|
+
reconnectMs: 3000,
|
|
13
|
+
scanStart: 8765,
|
|
14
|
+
scanCount: 5,
|
|
15
|
+
probeTimeoutMs: 1200,
|
|
16
|
+
customValue: "__custom__",
|
|
17
|
+
lang: "zh-CN",
|
|
18
|
+
// 发送场景重连成功(onopen)后,若服务端未及时推送会话状态,则等待此毫秒数后兜底直接发送。
|
|
19
|
+
sendAfterOpenMs: 350,
|
|
20
|
+
// 常用提示词所在 ui_settings.json 的绝对路径;注入脚本会用真实 home 路径替换此占位符。
|
|
21
|
+
// 面板借道现有 WS 的 run_command(cat 该文件)拉取提示词,不读本地 fs、不依赖 CORS。
|
|
22
|
+
settingsPath: "__MCP_SETTINGS_PATH__",
|
|
23
|
+
// run_command 拉取提示词的兜底超时(毫秒)。
|
|
24
|
+
promptLoadTimeoutMs: 4000,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const state = {
|
|
28
|
+
installedAt: new Date().toISOString(),
|
|
29
|
+
socket: null,
|
|
30
|
+
socketPort: null,
|
|
31
|
+
connectSeq: 0,
|
|
32
|
+
currentState: "offline",
|
|
33
|
+
currentSession: null,
|
|
34
|
+
wantSend: false,
|
|
35
|
+
mounted: false,
|
|
36
|
+
scanning: false,
|
|
37
|
+
foundPorts: [],
|
|
38
|
+
portProjects: {},
|
|
39
|
+
portStatuses: {},
|
|
40
|
+
selectedStatus: null,
|
|
41
|
+
expanded: false,
|
|
42
|
+
// 常用提示词 + 完整 ui_settings(办法①:首次拿到会话时经 WS run_command 拉取并缓存)。
|
|
43
|
+
prompts: [],
|
|
44
|
+
promptsLoaded: false,
|
|
45
|
+
loadingPrompts: false,
|
|
46
|
+
cmdBuffer: null,
|
|
47
|
+
settings: null,
|
|
48
|
+
writing: false,
|
|
49
|
+
// 配置抽屉与编辑态。
|
|
50
|
+
drawerOpen: false,
|
|
51
|
+
editingId: null,
|
|
52
|
+
// 自动提交倒计时。
|
|
53
|
+
autoSubmitTimer: null,
|
|
54
|
+
autoSubmitLeft: 0,
|
|
55
|
+
asActiveId: null,
|
|
56
|
+
autoSubmitSuppressed: false,
|
|
57
|
+
// 输入法组字(拼音/注音等)进行中:此间回车用于上屏候选词,不应触发发送。
|
|
58
|
+
composing: false,
|
|
59
|
+
timers: [],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
63
|
+
|
|
64
|
+
function h(tag, attrs, ...children) {
|
|
65
|
+
const el = document.createElement(tag);
|
|
66
|
+
if (attrs) {
|
|
67
|
+
for (const key of Object.keys(attrs)) {
|
|
68
|
+
if (key === "class") el.className = attrs[key];
|
|
69
|
+
else el.setAttribute(key, String(attrs[key]));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const child of children) {
|
|
73
|
+
if (child == null) continue;
|
|
74
|
+
el.appendChild(typeof child === "string" ? document.createTextNode(child) : child);
|
|
75
|
+
}
|
|
76
|
+
return el;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function svgEl(tag, attrs, ...children) {
|
|
80
|
+
const el = document.createElementNS(SVG_NS, tag);
|
|
81
|
+
if (attrs) {
|
|
82
|
+
for (const key of Object.keys(attrs)) el.setAttribute(key, String(attrs[key]));
|
|
83
|
+
}
|
|
84
|
+
for (const child of children) {
|
|
85
|
+
if (child != null) el.appendChild(child);
|
|
86
|
+
}
|
|
87
|
+
return el;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function clear(el) {
|
|
91
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ensureStyle() {
|
|
95
|
+
if (document.getElementById(config.styleId)) return;
|
|
96
|
+
|
|
97
|
+
const chevron =
|
|
98
|
+
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6l4 4 4-4' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E";
|
|
99
|
+
|
|
100
|
+
const style = document.createElement("style");
|
|
101
|
+
style.id = config.styleId;
|
|
102
|
+
style.textContent = `
|
|
103
|
+
#${config.panelId} {
|
|
104
|
+
flex: 0 0 auto;
|
|
105
|
+
margin: 4px 12px 6px;
|
|
106
|
+
padding: 7px 10px 6px;
|
|
107
|
+
border: 1px solid var(--vscode-input-border, rgba(228, 228, 228, .18));
|
|
108
|
+
border-radius: 12px;
|
|
109
|
+
background: var(--vscode-input-background, rgba(228, 228, 228, .035));
|
|
110
|
+
color: var(--vscode-foreground, rgba(228, 228, 228, .92));
|
|
111
|
+
font-family: var(--vscode-font-family, -apple-system, "system-ui", sans-serif);
|
|
112
|
+
font-size: 13px;
|
|
113
|
+
line-height: 1.4;
|
|
114
|
+
}
|
|
115
|
+
#${config.panelId} * { box-sizing: border-box; }
|
|
116
|
+
|
|
117
|
+
#${config.panelId} .cmf-head {
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
gap: 7px;
|
|
121
|
+
margin-bottom: 5px;
|
|
122
|
+
}
|
|
123
|
+
#${config.panelId} .cmf-badge {
|
|
124
|
+
font-size: 9.5px; font-weight: 700; letter-spacing: .05em;
|
|
125
|
+
padding: 1px 5px; border-radius: 4px;
|
|
126
|
+
background: var(--vscode-badge-background, #88c0d0);
|
|
127
|
+
color: var(--vscode-badge-foreground, #141414);
|
|
128
|
+
}
|
|
129
|
+
#${config.panelId} .cmf-status {
|
|
130
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
131
|
+
font-size: 11.5px;
|
|
132
|
+
color: var(--vscode-descriptionForeground, rgba(228, 228, 228, .6));
|
|
133
|
+
white-space: nowrap;
|
|
134
|
+
}
|
|
135
|
+
#${config.panelId} .cmf-dot {
|
|
136
|
+
width: 6px; height: 6px; border-radius: 50%;
|
|
137
|
+
background: #66d19e; box-shadow: 0 0 0 3px rgba(102, 209, 158, .14);
|
|
138
|
+
}
|
|
139
|
+
#${config.panelId} .cmf-dot.off { background: var(--vscode-descriptionForeground, #888); box-shadow: none; }
|
|
140
|
+
#${config.panelId} .cmf-spinner {
|
|
141
|
+
width: 11px; height: 11px;
|
|
142
|
+
border: 2px solid rgba(228, 228, 228, .22);
|
|
143
|
+
border-top-color: var(--vscode-foreground, rgba(228, 228, 228, .85));
|
|
144
|
+
border-radius: 50%;
|
|
145
|
+
animation: cmf-spin 1s linear infinite;
|
|
146
|
+
}
|
|
147
|
+
@keyframes cmf-spin { to { transform: rotate(360deg); } }
|
|
148
|
+
|
|
149
|
+
#${config.panelId} .cmf-spacer { flex: 1 1 auto; }
|
|
150
|
+
#${config.panelId} .cmf-project {
|
|
151
|
+
max-width: 140px;
|
|
152
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
153
|
+
font-size: 11.5px;
|
|
154
|
+
color: var(--vscode-descriptionForeground, rgba(228, 228, 228, .55));
|
|
155
|
+
}
|
|
156
|
+
#${config.panelId} .cmf-port {
|
|
157
|
+
appearance: none; -webkit-appearance: none;
|
|
158
|
+
height: 22px; padding: 0 20px 0 6px;
|
|
159
|
+
border-radius: 4px;
|
|
160
|
+
border: 1px solid transparent;
|
|
161
|
+
background-color: transparent;
|
|
162
|
+
background-image: url("${chevron}");
|
|
163
|
+
background-repeat: no-repeat;
|
|
164
|
+
background-position: right 5px center;
|
|
165
|
+
color: var(--vscode-descriptionForeground, rgba(228, 228, 228, .7));
|
|
166
|
+
font-family: inherit; font-size: 11.5px; cursor: pointer; outline: none;
|
|
167
|
+
transition: background-color .12s ease;
|
|
168
|
+
}
|
|
169
|
+
#${config.panelId} .cmf-port:hover { background-color: var(--vscode-list-hoverBackground, rgba(228, 228, 228, .07)); }
|
|
170
|
+
#${config.panelId} .cmf-port:focus { background-color: var(--vscode-list-hoverBackground, rgba(228, 228, 228, .07)); }
|
|
171
|
+
#${config.panelId} .cmf-prompts { max-width: 170px; flex: 0 1 auto; }
|
|
172
|
+
|
|
173
|
+
#${config.panelId} .cmf-scan {
|
|
174
|
+
flex: 0 0 auto;
|
|
175
|
+
width: 22px; height: 22px;
|
|
176
|
+
border: 0; border-radius: 4px;
|
|
177
|
+
display: inline-grid; place-items: center;
|
|
178
|
+
background: transparent;
|
|
179
|
+
color: var(--vscode-descriptionForeground, rgba(228, 228, 228, .65));
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
transition: background-color .12s ease, color .12s ease;
|
|
182
|
+
}
|
|
183
|
+
#${config.panelId} .cmf-scan:hover {
|
|
184
|
+
background: var(--vscode-list-hoverBackground, rgba(228, 228, 228, .08));
|
|
185
|
+
color: var(--vscode-foreground, rgba(228, 228, 228, .92));
|
|
186
|
+
}
|
|
187
|
+
#${config.panelId} .cmf-scan:disabled { cursor: default; }
|
|
188
|
+
#${config.panelId} .cmf-scan.scanning { color: var(--vscode-foreground, rgba(228, 228, 228, .85)); }
|
|
189
|
+
#${config.panelId} .cmf-scan.scanning svg { animation: cmf-spin 1s linear infinite; }
|
|
190
|
+
#${config.panelId} .cmf-scan svg { width: 13px; height: 13px; }
|
|
191
|
+
|
|
192
|
+
#${config.panelId} .cmf-custom {
|
|
193
|
+
flex: 0 0 auto;
|
|
194
|
+
width: 66px; height: 22px;
|
|
195
|
+
padding: 0 6px;
|
|
196
|
+
border-radius: 4px;
|
|
197
|
+
border: 1px solid var(--vscode-input-border, rgba(228, 228, 228, .22));
|
|
198
|
+
background: var(--vscode-input-background, rgba(228, 228, 228, .05));
|
|
199
|
+
color: var(--vscode-input-foreground, rgba(228, 228, 228, .9));
|
|
200
|
+
font-family: inherit; font-size: 11.5px; outline: none;
|
|
201
|
+
}
|
|
202
|
+
#${config.panelId} .cmf-custom:focus { border-color: var(--vscode-focusBorder, #569cd6); }
|
|
203
|
+
#${config.panelId} .cmf-custom::placeholder { color: var(--vscode-descriptionForeground, rgba(228, 228, 228, .42)); }
|
|
204
|
+
|
|
205
|
+
#${config.panelId} .cmf-input {
|
|
206
|
+
display: block;
|
|
207
|
+
width: 100%;
|
|
208
|
+
min-height: 22px; max-height: 160px;
|
|
209
|
+
padding: 2px 0;
|
|
210
|
+
border: 0; outline: 0; resize: none;
|
|
211
|
+
background: transparent;
|
|
212
|
+
color: var(--vscode-input-foreground, rgba(228, 228, 228, .92));
|
|
213
|
+
font-family: inherit; font-size: 13px; line-height: 1.45;
|
|
214
|
+
}
|
|
215
|
+
#${config.panelId} .cmf-input::placeholder { color: var(--vscode-descriptionForeground, rgba(228, 228, 228, .42)); }
|
|
216
|
+
|
|
217
|
+
#${config.panelId} .cmf-foot {
|
|
218
|
+
display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin-top: 4px;
|
|
219
|
+
}
|
|
220
|
+
#${config.panelId} .cmf-send {
|
|
221
|
+
flex: 0 0 auto;
|
|
222
|
+
width: 24px; height: 24px;
|
|
223
|
+
border: 0; border-radius: 6px;
|
|
224
|
+
display: inline-grid; place-items: center;
|
|
225
|
+
background: transparent;
|
|
226
|
+
color: var(--vscode-descriptionForeground, rgba(228, 228, 228, .7));
|
|
227
|
+
cursor: pointer;
|
|
228
|
+
transition: background-color .12s ease, color .12s ease, opacity .12s ease;
|
|
229
|
+
}
|
|
230
|
+
#${config.panelId} .cmf-send:hover {
|
|
231
|
+
background: var(--vscode-list-hoverBackground, rgba(228, 228, 228, .08));
|
|
232
|
+
color: var(--vscode-foreground, rgba(228, 228, 228, .92));
|
|
233
|
+
}
|
|
234
|
+
#${config.panelId} .cmf-send.ready {
|
|
235
|
+
background: var(--vscode-button-background, #81a1c1);
|
|
236
|
+
color: var(--vscode-button-foreground, #141414);
|
|
237
|
+
}
|
|
238
|
+
#${config.panelId} .cmf-send.ready:hover { background: var(--vscode-button-hoverBackground, #87a6c4); }
|
|
239
|
+
#${config.panelId} .cmf-send:disabled { opacity: .45; cursor: not-allowed; }
|
|
240
|
+
#${config.panelId} .cmf-send:disabled:hover { background: transparent; color: var(--vscode-descriptionForeground, rgba(228, 228, 228, .7)); }
|
|
241
|
+
#${config.panelId} .cmf-send svg { width: 15px; height: 15px; }
|
|
242
|
+
|
|
243
|
+
#${config.panelId} .cmf-gear.active { color: var(--vscode-foreground, rgba(228,228,228,.95)); background: var(--vscode-list-hoverBackground, rgba(228,228,228,.12)); }
|
|
244
|
+
#${config.panelId} .cmf-drawer {
|
|
245
|
+
margin: 2px 0 6px; padding: 8px;
|
|
246
|
+
border: 1px solid var(--vscode-input-border, rgba(228,228,228,.16));
|
|
247
|
+
border-radius: 8px;
|
|
248
|
+
background: var(--vscode-editorWidget-background, rgba(228,228,228,.03));
|
|
249
|
+
}
|
|
250
|
+
#${config.panelId} .cmf-drawer-title {
|
|
251
|
+
display: flex; align-items: center; gap: 6px;
|
|
252
|
+
font-size: 11px; font-weight: 600; letter-spacing: .03em;
|
|
253
|
+
color: var(--vscode-descriptionForeground, rgba(228,228,228,.6));
|
|
254
|
+
margin: 2px 0 6px;
|
|
255
|
+
}
|
|
256
|
+
#${config.panelId} .cmf-drawer-title:not(:first-child) { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--vscode-input-border, rgba(228,228,228,.12)); }
|
|
257
|
+
#${config.panelId} .cmf-mini {
|
|
258
|
+
height: 22px; padding: 0 9px;
|
|
259
|
+
border: 1px solid var(--vscode-input-border, rgba(228,228,228,.22));
|
|
260
|
+
border-radius: 5px; background: transparent;
|
|
261
|
+
color: var(--vscode-foreground, rgba(228,228,228,.85));
|
|
262
|
+
font-family: inherit; font-size: 11.5px; cursor: pointer;
|
|
263
|
+
transition: background-color .12s ease;
|
|
264
|
+
}
|
|
265
|
+
#${config.panelId} .cmf-mini:hover { background: var(--vscode-list-hoverBackground, rgba(228,228,228,.08)); }
|
|
266
|
+
#${config.panelId} .cmf-mini.primary { background: var(--vscode-button-background, #81a1c1); color: var(--vscode-button-foreground, #141414); border-color: transparent; }
|
|
267
|
+
#${config.panelId} .cmf-mini.primary:hover { background: var(--vscode-button-hoverBackground, #87a6c4); }
|
|
268
|
+
#${config.panelId} .cmf-plist { display: flex; flex-direction: column; gap: 3px; }
|
|
269
|
+
#${config.panelId} .cmf-pitem { display: flex; align-items: center; gap: 6px; padding: 3px 4px 3px 7px; border-radius: 5px; }
|
|
270
|
+
#${config.panelId} .cmf-pitem:hover { background: var(--vscode-list-hoverBackground, rgba(228,228,228,.06)); }
|
|
271
|
+
#${config.panelId} .cmf-pname { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--vscode-foreground, rgba(228,228,228,.9)); }
|
|
272
|
+
#${config.panelId} .cmf-pbtn { flex: 0 0 auto; width: 22px; height: 20px; border: 0; border-radius: 4px; background: transparent; color: var(--vscode-descriptionForeground, rgba(228,228,228,.6)); cursor: pointer; font-size: 12px; line-height: 1; }
|
|
273
|
+
#${config.panelId} .cmf-pbtn:hover { background: var(--vscode-list-hoverBackground, rgba(228,228,228,.1)); color: var(--vscode-foreground, rgba(228,228,228,.95)); }
|
|
274
|
+
#${config.panelId} .cmf-pempty { font-size: 11.5px; color: var(--vscode-descriptionForeground, rgba(228,228,228,.5)); padding: 4px 7px; }
|
|
275
|
+
#${config.panelId} .cmf-pedit { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
|
|
276
|
+
#${config.panelId} .cmf-field {
|
|
277
|
+
width: 100%; padding: 4px 7px; border-radius: 5px;
|
|
278
|
+
border: 1px solid var(--vscode-input-border, rgba(228,228,228,.22));
|
|
279
|
+
background: var(--vscode-input-background, rgba(228,228,228,.05));
|
|
280
|
+
color: var(--vscode-input-foreground, rgba(228,228,228,.92));
|
|
281
|
+
font-family: inherit; font-size: 12px; outline: none;
|
|
282
|
+
}
|
|
283
|
+
#${config.panelId} .cmf-field:focus { border-color: var(--vscode-focusBorder, #569cd6); }
|
|
284
|
+
#${config.panelId} .cmf-ta { resize: vertical; min-height: 48px; line-height: 1.45; }
|
|
285
|
+
#${config.panelId} .cmf-pedit-actions { display: flex; justify-content: flex-end; gap: 6px; }
|
|
286
|
+
#${config.panelId} .cmf-asrow { display: flex; align-items: center; gap: 7px; margin: 5px 0; font-size: 12px; color: var(--vscode-foreground, rgba(228,228,228,.85)); cursor: default; }
|
|
287
|
+
#${config.panelId} .cmf-aslbl { font-size: 11.5px; color: var(--vscode-descriptionForeground, rgba(228,228,228,.6)); }
|
|
288
|
+
#${config.panelId} .cmf-check { width: 14px; height: 14px; accent-color: var(--vscode-button-background, #81a1c1); cursor: pointer; }
|
|
289
|
+
#${config.panelId} .cmf-num { width: 70px; flex: 0 0 auto; }
|
|
290
|
+
#${config.panelId} .cmf-asselect { max-width: 150px; height: 24px; border: 1px solid var(--vscode-input-border, rgba(228,228,228,.22)); background-color: var(--vscode-input-background, rgba(228,228,228,.05)); }
|
|
291
|
+
#${config.panelId} .cmf-asstatus { font-size: 11px; color: var(--vscode-descriptionForeground, rgba(228,228,228,.6)); }
|
|
292
|
+
#${config.panelId} .cmf-asstatus.on { color: #66d19e; }
|
|
293
|
+
#${config.panelId} .cmf-ascount { font-size: 11px; color: #e0a458; padding: 0 6px; cursor: pointer; user-select: none; white-space: nowrap; }
|
|
294
|
+
#${config.panelId} .cmf-ascount:hover { color: #f0b76a; text-decoration: underline; }
|
|
295
|
+
`;
|
|
296
|
+
document.documentElement.appendChild(style);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const els = {};
|
|
300
|
+
|
|
301
|
+
function setStatus(kind) {
|
|
302
|
+
clear(els.status);
|
|
303
|
+
if (kind === "spinner") {
|
|
304
|
+
els.status.appendChild(h("span", { class: "cmf-spinner", "aria-hidden": "true" }));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (kind === "ready") {
|
|
308
|
+
els.status.appendChild(h("span", { class: "cmf-dot", "aria-hidden": "true" }));
|
|
309
|
+
els.status.appendChild(h("span", null, "Ready"));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
els.status.appendChild(h("span", { class: "cmf-dot off", "aria-hidden": "true" }));
|
|
313
|
+
els.status.appendChild(h("span", null, "Offline"));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function setSendReady(ready) {
|
|
317
|
+
els.sendBtn.className = ready ? "cmf-send ready" : "cmf-send";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 发送按钮只要输入框有内容就可点击,不依赖连接状态,避免「点不了发送」
|
|
321
|
+
function updateSendEnabled() {
|
|
322
|
+
els.sendBtn.disabled = els.prompt.value.trim().length === 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function defaultPorts() {
|
|
326
|
+
const arr = [];
|
|
327
|
+
for (let i = 0; i < config.scanCount; i++) arr.push(String(config.scanStart + i));
|
|
328
|
+
return arr;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function fillPortOptions(ports, selected) {
|
|
332
|
+
const keep = selected != null ? String(selected) : els.portSelect ? els.portSelect.value : "";
|
|
333
|
+
clear(els.portSelect);
|
|
334
|
+
const list = ports && ports.length ? ports.map(String) : [String(config.scanStart)];
|
|
335
|
+
for (const p of list) {
|
|
336
|
+
els.portSelect.appendChild(h("option", { value: p }));
|
|
337
|
+
}
|
|
338
|
+
els.portSelect.appendChild(h("option", { value: config.customValue }, "自定义端口…"));
|
|
339
|
+
els.portSelect.value = keep && list.includes(keep) ? keep : list[0];
|
|
340
|
+
refreshOptionLabels();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function makeScanIcon() {
|
|
344
|
+
const svg = svgEl("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true" });
|
|
345
|
+
svg.appendChild(svgEl("circle", { cx: "11", cy: "11", r: "6", stroke: "currentColor", "stroke-width": "2" }));
|
|
346
|
+
svg.appendChild(
|
|
347
|
+
svgEl("path", { d: "M20 20l-3.4-3.4", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round" })
|
|
348
|
+
);
|
|
349
|
+
return svg;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function makeRefreshIcon() {
|
|
353
|
+
const svg = svgEl("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true" });
|
|
354
|
+
const opts = { stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" };
|
|
355
|
+
svg.appendChild(svgEl("path", Object.assign({ d: "M4 12a8 8 0 0 1 13.7-5.6L20 8" }, opts)));
|
|
356
|
+
svg.appendChild(svgEl("path", Object.assign({ d: "M20 4v4h-4" }, opts)));
|
|
357
|
+
svg.appendChild(svgEl("path", Object.assign({ d: "M20 12a8 8 0 0 1-13.7 5.6L4 16" }, opts)));
|
|
358
|
+
svg.appendChild(svgEl("path", Object.assign({ d: "M4 20v-4h4" }, opts)));
|
|
359
|
+
return svg;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function makeGearIcon() {
|
|
363
|
+
const svg = svgEl("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true" });
|
|
364
|
+
const line = { stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round" };
|
|
365
|
+
const knobFill = "var(--vscode-input-background, #1e1e1e)";
|
|
366
|
+
svg.appendChild(svgEl("line", Object.assign({ x1: "4", y1: "8", x2: "20", y2: "8" }, line)));
|
|
367
|
+
svg.appendChild(svgEl("line", Object.assign({ x1: "4", y1: "16", x2: "20", y2: "16" }, line)));
|
|
368
|
+
svg.appendChild(svgEl("circle", { cx: "9", cy: "8", r: "2.6", fill: knobFill, stroke: "currentColor", "stroke-width": "2" }));
|
|
369
|
+
svg.appendChild(svgEl("circle", { cx: "15", cy: "16", r: "2.6", fill: knobFill, stroke: "currentColor", "stroke-width": "2" }));
|
|
370
|
+
return svg;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function buildDrawer() {
|
|
374
|
+
// —— 提示词管理 ——
|
|
375
|
+
const pAdd = h("button", { class: "cmf-mini", type: "button" }, "+ 新增");
|
|
376
|
+
const pTitle = h("div", { class: "cmf-drawer-title" }, h("span", null, "常用提示词"), h("span", { class: "cmf-spacer" }), pAdd);
|
|
377
|
+
const plist = h("div", { class: "cmf-plist" });
|
|
378
|
+
|
|
379
|
+
const pName = h("input", { class: "cmf-field", type: "text", placeholder: "名称" });
|
|
380
|
+
const pContent = h("textarea", { class: "cmf-field cmf-ta", rows: "3", placeholder: "提示词内容" });
|
|
381
|
+
const pSave = h("button", { class: "cmf-mini primary", type: "button" }, "保存");
|
|
382
|
+
const pCancel = h("button", { class: "cmf-mini", type: "button" }, "取消");
|
|
383
|
+
const pEdit = h("div", { class: "cmf-pedit" }, pName, pContent, h("div", { class: "cmf-pedit-actions" }, pCancel, pSave));
|
|
384
|
+
pEdit.style.display = "none";
|
|
385
|
+
|
|
386
|
+
// —— 自动提交 ——
|
|
387
|
+
const asToggle = h("input", { class: "cmf-check", type: "checkbox" });
|
|
388
|
+
const asTimeout = h("input", { class: "cmf-field cmf-num", type: "number", min: "5", max: "86400", step: "5" });
|
|
389
|
+
const asSelect = h("select", { class: "cmf-port cmf-asselect", "aria-label": "自动提交提示词" });
|
|
390
|
+
const asStatus = h("span", { class: "cmf-asstatus" }, "已停用");
|
|
391
|
+
const asRow1 = h("label", { class: "cmf-asrow" }, asToggle, h("span", null, "启用自动提交"), h("span", { class: "cmf-spacer" }), asStatus);
|
|
392
|
+
const asRow2 = h(
|
|
393
|
+
"div",
|
|
394
|
+
{ class: "cmf-asrow" },
|
|
395
|
+
h("span", { class: "cmf-aslbl" }, "超时(秒)"),
|
|
396
|
+
asTimeout,
|
|
397
|
+
h("span", { class: "cmf-aslbl" }, "提示词"),
|
|
398
|
+
asSelect
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const drawer = h(
|
|
402
|
+
"div",
|
|
403
|
+
{ class: "cmf-drawer" },
|
|
404
|
+
pTitle,
|
|
405
|
+
plist,
|
|
406
|
+
pEdit,
|
|
407
|
+
h("div", { class: "cmf-drawer-title" }, h("span", null, "自动提交")),
|
|
408
|
+
asRow1,
|
|
409
|
+
asRow2
|
|
410
|
+
);
|
|
411
|
+
drawer.style.display = "none";
|
|
412
|
+
|
|
413
|
+
els.drawer = drawer;
|
|
414
|
+
els.plist = plist;
|
|
415
|
+
els.pAdd = pAdd;
|
|
416
|
+
els.pEdit = pEdit;
|
|
417
|
+
els.pName = pName;
|
|
418
|
+
els.pContent = pContent;
|
|
419
|
+
els.pSave = pSave;
|
|
420
|
+
els.pCancel = pCancel;
|
|
421
|
+
els.asToggle = asToggle;
|
|
422
|
+
els.asTimeout = asTimeout;
|
|
423
|
+
els.asSelect = asSelect;
|
|
424
|
+
els.asStatus = asStatus;
|
|
425
|
+
|
|
426
|
+
return drawer;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function buildPanel() {
|
|
430
|
+
const portSelect = h("select", { class: "cmf-port", "aria-label": "选择 MCP 前端端口" });
|
|
431
|
+
els.portSelect = portSelect;
|
|
432
|
+
fillPortOptions(defaultPorts(), String(config.scanStart));
|
|
433
|
+
|
|
434
|
+
const customInput = h("input", {
|
|
435
|
+
class: "cmf-custom",
|
|
436
|
+
type: "text",
|
|
437
|
+
inputmode: "numeric",
|
|
438
|
+
maxlength: "5",
|
|
439
|
+
placeholder: "端口号",
|
|
440
|
+
"aria-label": "自定义 MCP 端口",
|
|
441
|
+
});
|
|
442
|
+
customInput.style.display = "none";
|
|
443
|
+
|
|
444
|
+
const scanBtn = h(
|
|
445
|
+
"button",
|
|
446
|
+
{
|
|
447
|
+
class: "cmf-scan",
|
|
448
|
+
type: "button",
|
|
449
|
+
"aria-label": "扫描端口",
|
|
450
|
+
title: "扫描端口 " + config.scanStart + "–" + (config.scanStart + config.scanCount - 1),
|
|
451
|
+
},
|
|
452
|
+
makeScanIcon()
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const status = h("span", { class: "cmf-status", "aria-live": "polite" });
|
|
456
|
+
status.appendChild(h("span", { class: "cmf-spinner", "aria-hidden": "true" }));
|
|
457
|
+
|
|
458
|
+
const projectName = h("span", { class: "cmf-project", title: "" }, "No project");
|
|
459
|
+
|
|
460
|
+
const gear = h(
|
|
461
|
+
"button",
|
|
462
|
+
{ class: "cmf-scan cmf-gear", type: "button", "aria-label": "配置", title: "配置(提示词 / 自动提交)" },
|
|
463
|
+
makeGearIcon()
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
const head = h(
|
|
467
|
+
"div",
|
|
468
|
+
{ class: "cmf-head" },
|
|
469
|
+
h("span", { class: "cmf-badge" }, "MCP"),
|
|
470
|
+
status,
|
|
471
|
+
h("span", { class: "cmf-spacer" }),
|
|
472
|
+
scanBtn,
|
|
473
|
+
portSelect,
|
|
474
|
+
customInput,
|
|
475
|
+
projectName,
|
|
476
|
+
gear
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
const prompt = h("textarea", { class: "cmf-input", rows: "1", placeholder: "Add a follow-up" });
|
|
480
|
+
|
|
481
|
+
const sendIcon = svgEl("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true" });
|
|
482
|
+
sendIcon.appendChild(svgEl("path", { d: "M5 12h13", stroke: "currentColor", "stroke-width": "2.2", "stroke-linecap": "round" }));
|
|
483
|
+
sendIcon.appendChild(
|
|
484
|
+
svgEl("path", {
|
|
485
|
+
d: "M13 6l6 6-6 6",
|
|
486
|
+
stroke: "currentColor",
|
|
487
|
+
"stroke-width": "2.2",
|
|
488
|
+
"stroke-linecap": "round",
|
|
489
|
+
"stroke-linejoin": "round",
|
|
490
|
+
})
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
const sendBtn = h(
|
|
494
|
+
"button",
|
|
495
|
+
{ class: "cmf-send", type: "button", disabled: "", "aria-label": "Send feedback", title: "Send feedback" },
|
|
496
|
+
sendIcon
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
const promptSelect = h("select", { class: "cmf-port cmf-prompts", "aria-label": "常用提示词", title: "插入常用提示词" });
|
|
500
|
+
promptSelect.appendChild(h("option", { value: "" }, "常用提示词…"));
|
|
501
|
+
promptSelect.style.display = "none";
|
|
502
|
+
|
|
503
|
+
const promptRefresh = h(
|
|
504
|
+
"button",
|
|
505
|
+
{ class: "cmf-scan cmf-refresh", type: "button", "aria-label": "刷新配置", title: "刷新常用提示词 / 配置" },
|
|
506
|
+
makeRefreshIcon()
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const asCount = h("span", { class: "cmf-ascount", title: "自动提交倒计时(点击取消)" }, "");
|
|
510
|
+
asCount.style.display = "none";
|
|
511
|
+
|
|
512
|
+
const foot = h("div", { class: "cmf-foot" }, promptSelect, promptRefresh, h("span", { class: "cmf-spacer" }), asCount, sendBtn);
|
|
513
|
+
|
|
514
|
+
const drawer = buildDrawer();
|
|
515
|
+
|
|
516
|
+
const panel = h("section", { id: config.panelId, "aria-label": "MCP follow-up composer" }, head, drawer, prompt, foot);
|
|
517
|
+
|
|
518
|
+
els.panel = panel;
|
|
519
|
+
els.gear = gear;
|
|
520
|
+
els.status = status;
|
|
521
|
+
els.portSelect = portSelect;
|
|
522
|
+
els.customInput = customInput;
|
|
523
|
+
els.scanBtn = scanBtn;
|
|
524
|
+
els.projectName = projectName;
|
|
525
|
+
els.prompt = prompt;
|
|
526
|
+
els.sendBtn = sendBtn;
|
|
527
|
+
els.sendIcon = sendIcon;
|
|
528
|
+
els.promptSelect = promptSelect;
|
|
529
|
+
els.promptRefresh = promptRefresh;
|
|
530
|
+
els.asCount = asCount;
|
|
531
|
+
// 状态文本已从面板移除;游离占位元素让现有状态写入成为无害空操作。
|
|
532
|
+
els.hint = document.createElement("span");
|
|
533
|
+
|
|
534
|
+
wireEvents();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function visible(el) {
|
|
538
|
+
if (!el || !el.isConnected) return false;
|
|
539
|
+
const style = window.getComputedStyle(el);
|
|
540
|
+
if (style.display === "none" || style.visibility === "hidden") return false;
|
|
541
|
+
const rect = el.getBoundingClientRect();
|
|
542
|
+
return rect.width > 0 && rect.height > 0;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function findBar() {
|
|
546
|
+
const bars = Array.from(document.querySelectorAll(".composer-bar")).filter(visible);
|
|
547
|
+
bars.sort((a, b) => {
|
|
548
|
+
const ra = a.getBoundingClientRect();
|
|
549
|
+
const rb = b.getBoundingClientRect();
|
|
550
|
+
return rb.width * rb.height - ra.width * ra.height;
|
|
551
|
+
});
|
|
552
|
+
return bars[0] || null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function findInputArea(bar) {
|
|
556
|
+
const fib = bar.querySelector(".full-input-box, .composer-input-blur-wrapper");
|
|
557
|
+
if (!fib) return null;
|
|
558
|
+
let cur = fib;
|
|
559
|
+
while (cur && cur.parentElement && cur.parentElement !== bar) {
|
|
560
|
+
cur = cur.parentElement;
|
|
561
|
+
}
|
|
562
|
+
return cur && cur.parentElement === bar ? cur : null;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function ensureMounted() {
|
|
566
|
+
const bar = findBar();
|
|
567
|
+
if (!bar) {
|
|
568
|
+
state.mounted = false;
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const inputArea = findInputArea(bar);
|
|
573
|
+
if (!inputArea) {
|
|
574
|
+
state.mounted = false;
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (els.panel.parentElement === bar && els.panel.nextElementSibling === inputArea) {
|
|
579
|
+
state.mounted = true;
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
bar.insertBefore(els.panel, inputArea);
|
|
584
|
+
state.mounted = true;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function basename(dir) {
|
|
588
|
+
return (
|
|
589
|
+
String(dir || "")
|
|
590
|
+
.split(/[\\/]/)
|
|
591
|
+
.filter(Boolean)
|
|
592
|
+
.pop() || ""
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// 选项文本:Port {port}[ · 状态][ · 项目名](状态在前、项目在后)。
|
|
597
|
+
// 下拉展开时所有项都带「状态 · 项目名」,便于按项目/状态区分端口;
|
|
598
|
+
// 收起时框里显示的是「选中项」,为避免与右侧项目名重复,选中项收起时不带项目名。
|
|
599
|
+
function portText(port, status, withProject) {
|
|
600
|
+
let text = "Port " + port;
|
|
601
|
+
if (status) text += " · " + status;
|
|
602
|
+
if (withProject) {
|
|
603
|
+
const project = state.portProjects[String(port)];
|
|
604
|
+
if (project) text += " · " + project;
|
|
605
|
+
}
|
|
606
|
+
return text;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// 收起时所有选项都不带项目名,让 select 宽度贴合短文字(避免「文字短框长」);
|
|
610
|
+
// 展开时所有选项带上项目名,下拉列表自然变宽以完整显示。
|
|
611
|
+
function refreshOptionLabels() {
|
|
612
|
+
const selected = els.portSelect.value;
|
|
613
|
+
for (const option of Array.from(els.portSelect.options)) {
|
|
614
|
+
if (option.value === config.customValue) continue;
|
|
615
|
+
const isSelected = option.value === selected;
|
|
616
|
+
const status = isSelected ? state.selectedStatus : state.portStatuses[option.value] || null;
|
|
617
|
+
option.textContent = portText(option.value, status, state.expanded);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function setOptionLabel(port, status) {
|
|
622
|
+
if (String(port) === els.portSelect.value) state.selectedStatus = status;
|
|
623
|
+
refreshOptionLabels();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function deriveStatusLabel(info) {
|
|
627
|
+
if (!info) return "";
|
|
628
|
+
if (info.feedback_completed === true || info.status === "feedback_submitted" || info.status === "completed") {
|
|
629
|
+
return "AI processing";
|
|
630
|
+
}
|
|
631
|
+
if (info.status === "active" || info.project_directory) return "waiting";
|
|
632
|
+
return "";
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function sessionTitle(data) {
|
|
636
|
+
const name = basename(data && data.project_directory);
|
|
637
|
+
return name ? " · " + name : "";
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function updateProjectName(data) {
|
|
641
|
+
const dir = data && data.project_directory ? data.project_directory : "";
|
|
642
|
+
els.projectName.textContent = basename(dir) || "No project";
|
|
643
|
+
els.projectName.title = dir || "";
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function setVisualState(stateName, message) {
|
|
647
|
+
const port = els.portSelect.value;
|
|
648
|
+
state.currentState = stateName;
|
|
649
|
+
|
|
650
|
+
if (stateName === "ready") {
|
|
651
|
+
setStatus("ready");
|
|
652
|
+
setSendReady(true);
|
|
653
|
+
els.hint.textContent = message || port + sessionTitle(state.currentSession) + " · waiting";
|
|
654
|
+
} else if (stateName === "connecting") {
|
|
655
|
+
setStatus("spinner");
|
|
656
|
+
setSendReady(false);
|
|
657
|
+
els.hint.textContent = message || port + " 正在连接 MCP WebSocket。";
|
|
658
|
+
} else if (stateName === "processing") {
|
|
659
|
+
setStatus("spinner");
|
|
660
|
+
setSendReady(false);
|
|
661
|
+
els.hint.textContent = message || port + " 反馈已提交,等待 AI 下一次调用。";
|
|
662
|
+
} else {
|
|
663
|
+
setStatus("offline");
|
|
664
|
+
setSendReady(false);
|
|
665
|
+
els.hint.textContent = message || port + " 当前没有可用的 MCP feedback session。";
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
updateSendEnabled();
|
|
669
|
+
|
|
670
|
+
// 自动提交:仅「等待反馈」时倒计时;离开该状态视为新一轮,清除用户打断标记并取消倒计时。
|
|
671
|
+
if (stateName === "ready") {
|
|
672
|
+
reevaluateAutoSubmit();
|
|
673
|
+
} else {
|
|
674
|
+
state.autoSubmitSuppressed = false;
|
|
675
|
+
cancelAutoSubmit();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function applySession(info) {
|
|
680
|
+
state.currentSession = info || null;
|
|
681
|
+
updateProjectName(info);
|
|
682
|
+
|
|
683
|
+
// 首次拿到有效会话时拉取一次常用提示词(loadPrompts 内有守卫,仅执行一次)。
|
|
684
|
+
if (info && info.project_directory) loadPrompts();
|
|
685
|
+
|
|
686
|
+
const project = basename(info && info.project_directory);
|
|
687
|
+
if (project) state.portProjects[state.socketPort] = project;
|
|
688
|
+
state.portStatuses[state.socketPort] = deriveStatusLabel(info);
|
|
689
|
+
|
|
690
|
+
const status = info && info.status;
|
|
691
|
+
const completed = info && info.feedback_completed === true;
|
|
692
|
+
|
|
693
|
+
if (status === "feedback_submitted" || status === "completed" || completed) {
|
|
694
|
+
setOptionLabel(state.socketPort, "AI processing");
|
|
695
|
+
setVisualState("processing", state.socketPort + " 反馈已提交,等待 AI 下一次调用。");
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
setOptionLabel(state.socketPort, "waiting");
|
|
700
|
+
const summary = info && info.summary ? String(info.summary).replace(/\s+/g, " ").trim() : "";
|
|
701
|
+
setVisualState(
|
|
702
|
+
"ready",
|
|
703
|
+
summary ? "待回复 · " + summary.slice(0, 60) : state.socketPort + sessionTitle(info) + " · waiting"
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
if (state.wantSend && els.prompt.value.trim()) {
|
|
707
|
+
sendFeedback(els.prompt.value.trim());
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function handleSocketMessage(data) {
|
|
712
|
+
if (data.type === "connection_established") {
|
|
713
|
+
els.hint.textContent = state.socketPort + " 已连接,等待会话状态。";
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// 仅在我们主动用 run_command 拉取提示词期间(cmdBuffer 非 null)收集命令输出。
|
|
718
|
+
if (data.type === "command_output") {
|
|
719
|
+
if (state.cmdBuffer !== null) state.cmdBuffer += data.output || "";
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (data.type === "command_complete") {
|
|
723
|
+
if (state.cmdBuffer !== null) finishLoadPrompts();
|
|
724
|
+
else if (state.writing) onWriteComplete(data.exit_code);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (data.type === "command_error") {
|
|
728
|
+
if (state.cmdBuffer !== null) {
|
|
729
|
+
state.cmdBuffer = null;
|
|
730
|
+
state.loadingPrompts = false;
|
|
731
|
+
if (els.promptRefresh) els.promptRefresh.classList.remove("scanning");
|
|
732
|
+
}
|
|
733
|
+
state.writing = false;
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (data.type === "session_updated") {
|
|
738
|
+
applySession(data.session_info || {});
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (data.type === "status_update") {
|
|
743
|
+
applySession(data.status_info || {});
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (data.type === "notification") {
|
|
748
|
+
if (data.code === "session.feedbackSubmitted" || data.status === "feedback_submitted") {
|
|
749
|
+
setOptionLabel(state.socketPort, "AI processing");
|
|
750
|
+
setVisualState("processing", state.socketPort + " 反馈已提交,等待 AI 下一次调用。");
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// 办法①:借道当前 WS 的 run_command 让服务端 cat ui_settings.json,从命令输出里取常用提示词。
|
|
756
|
+
// 仅首次拿到活跃会话时拉取一次并缓存(低频,不重复触发)。
|
|
757
|
+
function loadPrompts() {
|
|
758
|
+
if (state.promptsLoaded || state.loadingPrompts) return;
|
|
759
|
+
if (!state.socket || state.socket.readyState !== WebSocket.OPEN) return;
|
|
760
|
+
if (config.settingsPath.indexOf("__MCP_") === 0) return; // 占位符未被注入脚本替换
|
|
761
|
+
|
|
762
|
+
state.loadingPrompts = true;
|
|
763
|
+
state.cmdBuffer = "";
|
|
764
|
+
if (els.promptRefresh) els.promptRefresh.classList.add("scanning");
|
|
765
|
+
try {
|
|
766
|
+
// shell=False,路径无空格,直接传绝对路径即可(~ 不会被展开)。
|
|
767
|
+
state.socket.send(JSON.stringify({ type: "run_command", command: "cat " + config.settingsPath }));
|
|
768
|
+
} catch (error) {
|
|
769
|
+
state.loadingPrompts = false;
|
|
770
|
+
state.cmdBuffer = null;
|
|
771
|
+
if (els.promptRefresh) els.promptRefresh.classList.remove("scanning");
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
// 兜底:服务端若未推 command_complete,超时后用已收集内容尝试解析。
|
|
775
|
+
window.setTimeout(() => {
|
|
776
|
+
if (state.loadingPrompts) finishLoadPrompts();
|
|
777
|
+
}, config.promptLoadTimeoutMs);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function finishLoadPrompts() {
|
|
781
|
+
const raw = state.cmdBuffer;
|
|
782
|
+
state.cmdBuffer = null;
|
|
783
|
+
state.loadingPrompts = false;
|
|
784
|
+
if (els.promptRefresh) els.promptRefresh.classList.remove("scanning");
|
|
785
|
+
if (!raw) return;
|
|
786
|
+
try {
|
|
787
|
+
const data = JSON.parse(raw);
|
|
788
|
+
state.settings = data && typeof data === "object" ? data : {};
|
|
789
|
+
const list = (state.settings.promptSettings && state.settings.promptSettings.prompts) || [];
|
|
790
|
+
state.prompts = Array.isArray(list) ? list : [];
|
|
791
|
+
state.promptsLoaded = true;
|
|
792
|
+
fillPromptOptions();
|
|
793
|
+
renderPromptList();
|
|
794
|
+
syncAutoSubmitUI();
|
|
795
|
+
reevaluateAutoSubmit();
|
|
796
|
+
} catch (error) {
|
|
797
|
+
// 解析失败(如输出被截断),保持未加载状态,下次会话仍可重试。
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function utf8ToBase64(str) {
|
|
802
|
+
return btoa(unescape(encodeURIComponent(str)));
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// 写回 ui_settings.json:ws_b64 方案——python 单表达式 base64 解码写文件,过黑名单、保留全部配置。
|
|
806
|
+
function writeSettings(mutator) {
|
|
807
|
+
if (!state.settings) return false;
|
|
808
|
+
if (state.writing) return false;
|
|
809
|
+
if (!state.socket || state.socket.readyState !== WebSocket.OPEN) {
|
|
810
|
+
setVisualState(state.currentState, els.portSelect.value + " 未连接到会话,无法保存配置。");
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
if (config.settingsPath.indexOf("__MCP_") === 0) return false;
|
|
814
|
+
try {
|
|
815
|
+
mutator(state.settings);
|
|
816
|
+
} catch (error) {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// 服务端 _safe_parse_command 会对整条命令(小写)做危险子串匹配,base64 字母表恰好可拼出
|
|
821
|
+
// "format"/"fdisk" 等模式(极小概率)。命中则向 JSON 追加空白改变尾部后重算 base64 重试。
|
|
822
|
+
const dangerous = [";", "&&", "||", "|", ">", "<", "`", "$(", "rm -rf", "del /f", "format", "fdisk"];
|
|
823
|
+
let payload;
|
|
824
|
+
try {
|
|
825
|
+
payload = JSON.stringify(state.settings, null, 2);
|
|
826
|
+
} catch (error) {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
let command = "";
|
|
830
|
+
let safe = false;
|
|
831
|
+
for (let attempt = 0; attempt < 8 && !safe; attempt++) {
|
|
832
|
+
const b64 = utf8ToBase64(payload);
|
|
833
|
+
const py =
|
|
834
|
+
"__import__('pathlib').Path('" +
|
|
835
|
+
config.settingsPath +
|
|
836
|
+
"').write_bytes(__import__('base64').b64decode('" +
|
|
837
|
+
b64 +
|
|
838
|
+
"'))";
|
|
839
|
+
command = 'python3 -c "' + py + '"';
|
|
840
|
+
const lower = command.toLowerCase();
|
|
841
|
+
safe = dangerous.every((p) => lower.indexOf(p) === -1);
|
|
842
|
+
if (!safe) payload += "\n";
|
|
843
|
+
}
|
|
844
|
+
if (!safe) return false;
|
|
845
|
+
|
|
846
|
+
state.writing = true;
|
|
847
|
+
try {
|
|
848
|
+
state.socket.send(JSON.stringify({ type: "run_command", command: command }));
|
|
849
|
+
} catch (error) {
|
|
850
|
+
state.writing = false;
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function onWriteComplete(exitCode) {
|
|
857
|
+
state.writing = false;
|
|
858
|
+
if (exitCode === 0) {
|
|
859
|
+
// 写成功后本地 state.settings 即为最新内容;同步 prompts 引用并刷新依赖它的 UI。
|
|
860
|
+
state.prompts =
|
|
861
|
+
(state.settings && state.settings.promptSettings && state.settings.promptSettings.prompts) || [];
|
|
862
|
+
fillPromptOptions();
|
|
863
|
+
renderPromptList();
|
|
864
|
+
syncAutoSubmitUI();
|
|
865
|
+
reevaluateAutoSubmit();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function fillPromptOptions() {
|
|
870
|
+
if (!els.promptSelect) return;
|
|
871
|
+
clear(els.promptSelect);
|
|
872
|
+
els.promptSelect.appendChild(h("option", { value: "" }, "常用提示词…"));
|
|
873
|
+
for (let i = 0; i < state.prompts.length; i++) {
|
|
874
|
+
const p = state.prompts[i];
|
|
875
|
+
if (!p || !p.content) continue;
|
|
876
|
+
els.promptSelect.appendChild(h("option", { value: String(i) }, p.name || "未命名"));
|
|
877
|
+
}
|
|
878
|
+
els.promptSelect.value = "";
|
|
879
|
+
els.promptSelect.style.display = state.prompts.length ? "" : "none";
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function onPromptPick() {
|
|
883
|
+
suppressAutoSubmit();
|
|
884
|
+
const idx = Number(els.promptSelect.value);
|
|
885
|
+
els.promptSelect.value = "";
|
|
886
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= state.prompts.length) return;
|
|
887
|
+
const content = state.prompts[idx] && state.prompts[idx].content;
|
|
888
|
+
if (!content) return;
|
|
889
|
+
|
|
890
|
+
// 追加填入:已有内容则换行后追加。
|
|
891
|
+
const cur = els.prompt.value;
|
|
892
|
+
els.prompt.value = cur && cur.trim() ? cur.replace(/\s*$/, "") + "\n" + content : content;
|
|
893
|
+
autoResize();
|
|
894
|
+
updateSendEnabled();
|
|
895
|
+
els.prompt.focus();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// 手动刷新:清除缓存标记并重新经 WS 拉取一次配置(提示词)。
|
|
899
|
+
function refreshPrompts() {
|
|
900
|
+
if (els.portSelect.value === config.customValue) return;
|
|
901
|
+
if (!state.socket || state.socket.readyState !== WebSocket.OPEN) {
|
|
902
|
+
setVisualState(state.currentState, els.portSelect.value + " 未连接到会话,无法刷新配置。");
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
state.promptsLoaded = false;
|
|
906
|
+
loadPrompts();
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// ———————————————————— 配置抽屉:开关 ————————————————————
|
|
910
|
+
function toggleDrawer() {
|
|
911
|
+
state.drawerOpen = !state.drawerOpen;
|
|
912
|
+
els.drawer.style.display = state.drawerOpen ? "" : "none";
|
|
913
|
+
els.gear.classList.toggle("active", state.drawerOpen);
|
|
914
|
+
if (state.drawerOpen) {
|
|
915
|
+
closePromptEditor();
|
|
916
|
+
// 抽屉打开但还没拉到配置时,尝试拉一次(需有活跃 WS)。
|
|
917
|
+
if (!state.settings && !state.loadingPrompts) {
|
|
918
|
+
state.promptsLoaded = false;
|
|
919
|
+
loadPrompts();
|
|
920
|
+
}
|
|
921
|
+
renderPromptList();
|
|
922
|
+
syncAutoSubmitUI();
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function ensurePromptSettings() {
|
|
927
|
+
if (!state.settings) state.settings = {};
|
|
928
|
+
const s = state.settings;
|
|
929
|
+
if (!s.promptSettings || typeof s.promptSettings !== "object") {
|
|
930
|
+
s.promptSettings = { prompts: [], lastUsedPromptId: "", promptCounter: 0 };
|
|
931
|
+
}
|
|
932
|
+
if (!Array.isArray(s.promptSettings.prompts)) s.promptSettings.prompts = [];
|
|
933
|
+
return s.promptSettings;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function findPromptIndex(id) {
|
|
937
|
+
const list = state.prompts || [];
|
|
938
|
+
let idx = list.findIndex((x) => x && x.id === id);
|
|
939
|
+
if (idx < 0 && /^\d+$/.test(String(id))) {
|
|
940
|
+
const n = Number(id);
|
|
941
|
+
if (n >= 0 && n < list.length) idx = n;
|
|
942
|
+
}
|
|
943
|
+
return idx;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// ———————————————————— 提示词列表与增删改 ————————————————————
|
|
947
|
+
function renderPromptList() {
|
|
948
|
+
if (!els.plist) return;
|
|
949
|
+
clear(els.plist);
|
|
950
|
+
const list = state.prompts || [];
|
|
951
|
+
if (!list.length) {
|
|
952
|
+
els.plist.appendChild(
|
|
953
|
+
h("div", { class: "cmf-pempty" }, state.settings ? "暂无提示词,点右上「+ 新增」。" : "连接会话后自动加载…")
|
|
954
|
+
);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
for (let i = 0; i < list.length; i++) {
|
|
958
|
+
const p = list[i];
|
|
959
|
+
if (!p) continue;
|
|
960
|
+
const id = p.id || String(i);
|
|
961
|
+
const name = h("span", { class: "cmf-pname", title: p.content || "" }, p.name || "未命名");
|
|
962
|
+
const edit = h("button", { class: "cmf-pbtn", type: "button", "aria-label": "编辑", title: "编辑" }, "改");
|
|
963
|
+
const del = h("button", { class: "cmf-pbtn", type: "button", "aria-label": "删除", title: "删除" }, "删");
|
|
964
|
+
edit.addEventListener("click", () => openPromptEditor(id));
|
|
965
|
+
del.addEventListener("click", () => deletePrompt(id));
|
|
966
|
+
els.plist.appendChild(h("div", { class: "cmf-pitem" }, name, edit, del));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function openPromptEditor(id) {
|
|
971
|
+
state.editingId = id || null;
|
|
972
|
+
let name = "";
|
|
973
|
+
let content = "";
|
|
974
|
+
if (id) {
|
|
975
|
+
const idx = findPromptIndex(id);
|
|
976
|
+
if (idx >= 0) {
|
|
977
|
+
name = state.prompts[idx].name || "";
|
|
978
|
+
content = state.prompts[idx].content || "";
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
els.pName.value = name;
|
|
982
|
+
els.pContent.value = content;
|
|
983
|
+
els.pEdit.style.display = "";
|
|
984
|
+
els.pName.focus();
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function closePromptEditor() {
|
|
988
|
+
state.editingId = null;
|
|
989
|
+
if (els.pEdit) els.pEdit.style.display = "none";
|
|
990
|
+
if (els.pName) els.pName.value = "";
|
|
991
|
+
if (els.pContent) els.pContent.value = "";
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function addPrompt(ps, name, content) {
|
|
995
|
+
const counter = (Number(ps.promptCounter) || 0) + 1;
|
|
996
|
+
ps.promptCounter = counter;
|
|
997
|
+
ps.prompts.push({
|
|
998
|
+
id: "prompt_" + counter + "_" + Date.now(),
|
|
999
|
+
name: name || "未命名",
|
|
1000
|
+
content: content,
|
|
1001
|
+
createdAt: new Date().toISOString(),
|
|
1002
|
+
lastUsedAt: "",
|
|
1003
|
+
isAutoSubmit: false,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function savePromptEditor() {
|
|
1008
|
+
if (!state.settings) return;
|
|
1009
|
+
const name = (els.pName.value || "").trim();
|
|
1010
|
+
const content = (els.pContent.value || "").trim();
|
|
1011
|
+
if (!content) {
|
|
1012
|
+
els.pContent.focus();
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const editingId = state.editingId;
|
|
1016
|
+
const ok = writeSettings(() => {
|
|
1017
|
+
const ps = ensurePromptSettings();
|
|
1018
|
+
const idx = editingId ? findPromptIndex(editingId) : -1;
|
|
1019
|
+
if (idx >= 0) {
|
|
1020
|
+
ps.prompts[idx].name = name || ps.prompts[idx].name || "未命名";
|
|
1021
|
+
ps.prompts[idx].content = content;
|
|
1022
|
+
ps.prompts[idx].updatedAt = new Date().toISOString();
|
|
1023
|
+
} else {
|
|
1024
|
+
addPrompt(ps, name, content);
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
if (ok) closePromptEditor();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function deletePrompt(id) {
|
|
1031
|
+
if (!state.settings) return;
|
|
1032
|
+
const idx = findPromptIndex(id);
|
|
1033
|
+
if (idx < 0) return;
|
|
1034
|
+
let proceed = true;
|
|
1035
|
+
try {
|
|
1036
|
+
proceed = window.confirm("删除提示词「" + (state.prompts[idx].name || "未命名") + "」?");
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
proceed = true;
|
|
1039
|
+
}
|
|
1040
|
+
if (!proceed) return;
|
|
1041
|
+
const ok = writeSettings(() => {
|
|
1042
|
+
const ps = ensurePromptSettings();
|
|
1043
|
+
const removed = ps.prompts.splice(idx, 1)[0];
|
|
1044
|
+
const rid = removed && removed.id;
|
|
1045
|
+
if (rid && state.settings.autoSubmitPromptId === rid) {
|
|
1046
|
+
state.settings.autoSubmitPromptId = "";
|
|
1047
|
+
state.settings.autoSubmitEnabled = false;
|
|
1048
|
+
}
|
|
1049
|
+
if (rid && ps.lastUsedPromptId === rid) ps.lastUsedPromptId = "";
|
|
1050
|
+
});
|
|
1051
|
+
if (ok && state.editingId === id) closePromptEditor();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// ———————————————————— 自动提交:配置 UI ————————————————————
|
|
1055
|
+
function syncAutoSubmitUI() {
|
|
1056
|
+
if (!els.asToggle) return;
|
|
1057
|
+
const s = state.settings || {};
|
|
1058
|
+
const enabled = s.autoSubmitEnabled === true;
|
|
1059
|
+
els.asToggle.checked = enabled;
|
|
1060
|
+
const timeout = Number(s.autoSubmitTimeout);
|
|
1061
|
+
els.asTimeout.value = Number.isFinite(timeout) && timeout > 0 ? String(timeout) : "300";
|
|
1062
|
+
|
|
1063
|
+
clear(els.asSelect);
|
|
1064
|
+
els.asSelect.appendChild(h("option", { value: "" }, "(未选择)"));
|
|
1065
|
+
const list = state.prompts || [];
|
|
1066
|
+
for (let i = 0; i < list.length; i++) {
|
|
1067
|
+
const p = list[i];
|
|
1068
|
+
if (!p) continue;
|
|
1069
|
+
els.asSelect.appendChild(h("option", { value: p.id || String(i) }, p.name || "未命名"));
|
|
1070
|
+
}
|
|
1071
|
+
els.asSelect.value = s.autoSubmitPromptId || "";
|
|
1072
|
+
|
|
1073
|
+
const ready = !!getAutoSubmitPrompt();
|
|
1074
|
+
els.asStatus.textContent = ready ? "已启用" : enabled ? "已启用(未选提示词)" : "已停用";
|
|
1075
|
+
els.asStatus.classList.toggle("on", ready);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function onAutoSubmitToggle() {
|
|
1079
|
+
const checked = els.asToggle.checked;
|
|
1080
|
+
state.autoSubmitSuppressed = false;
|
|
1081
|
+
const ok = writeSettings((s) => {
|
|
1082
|
+
s.autoSubmitEnabled = checked;
|
|
1083
|
+
});
|
|
1084
|
+
if (!ok) syncAutoSubmitUI();
|
|
1085
|
+
else {
|
|
1086
|
+
syncAutoSubmitUI();
|
|
1087
|
+
reevaluateAutoSubmit();
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function onAutoSubmitTimeoutChange() {
|
|
1092
|
+
let v = Math.round(Number(els.asTimeout.value));
|
|
1093
|
+
if (!Number.isFinite(v)) v = 300;
|
|
1094
|
+
v = Math.max(5, Math.min(86400, v));
|
|
1095
|
+
els.asTimeout.value = String(v);
|
|
1096
|
+
state.autoSubmitSuppressed = false;
|
|
1097
|
+
const ok = writeSettings((s) => {
|
|
1098
|
+
s.autoSubmitTimeout = v;
|
|
1099
|
+
});
|
|
1100
|
+
if (!ok) syncAutoSubmitUI();
|
|
1101
|
+
else reevaluateAutoSubmit();
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function onAutoSubmitSelect() {
|
|
1105
|
+
const id = els.asSelect.value;
|
|
1106
|
+
state.autoSubmitSuppressed = false;
|
|
1107
|
+
const ok = writeSettings((s) => {
|
|
1108
|
+
s.autoSubmitPromptId = id;
|
|
1109
|
+
const ps = ensurePromptSettings();
|
|
1110
|
+
for (let i = 0; i < ps.prompts.length; i++) {
|
|
1111
|
+
if (ps.prompts[i]) ps.prompts[i].isAutoSubmit = ps.prompts[i].id === id;
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
syncAutoSubmitUI();
|
|
1115
|
+
if (ok) reevaluateAutoSubmit();
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// ———————————————————— 自动提交:运行时 ————————————————————
|
|
1119
|
+
function getAutoSubmitPrompt() {
|
|
1120
|
+
const s = state.settings;
|
|
1121
|
+
if (!s || s.autoSubmitEnabled !== true || !s.autoSubmitPromptId) return null;
|
|
1122
|
+
const idx = findPromptIndex(s.autoSubmitPromptId);
|
|
1123
|
+
if (idx < 0) return null;
|
|
1124
|
+
const p = state.prompts[idx];
|
|
1125
|
+
return p && p.content ? p : null;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function fmtLeft(sec) {
|
|
1129
|
+
sec = Math.max(0, Math.round(sec));
|
|
1130
|
+
const m = Math.floor(sec / 60);
|
|
1131
|
+
const s = sec % 60;
|
|
1132
|
+
return m > 0 ? m + ":" + (s < 10 ? "0" + s : s) : s + "s";
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function updateAsCount() {
|
|
1136
|
+
if (!els.asCount) return;
|
|
1137
|
+
if (state.autoSubmitTimer && state.autoSubmitLeft > 0) {
|
|
1138
|
+
els.asCount.textContent = "自动提交 " + fmtLeft(state.autoSubmitLeft);
|
|
1139
|
+
els.asCount.style.display = "";
|
|
1140
|
+
} else {
|
|
1141
|
+
els.asCount.textContent = "";
|
|
1142
|
+
els.asCount.style.display = "none";
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// 仅在「等待反馈」(ready) 且未被用户打断、配置就绪时倒计时;其余情况取消。
|
|
1147
|
+
function reevaluateAutoSubmit() {
|
|
1148
|
+
if (state.currentState === "ready" && !state.autoSubmitSuppressed && getAutoSubmitPrompt()) startAutoSubmit();
|
|
1149
|
+
else cancelAutoSubmit();
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function startAutoSubmit() {
|
|
1153
|
+
const p = getAutoSubmitPrompt();
|
|
1154
|
+
if (!p) {
|
|
1155
|
+
cancelAutoSubmit();
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
// 已在为同一提示词倒计时则不重置(避免周期性重连刷新会话状态时反复归零)。
|
|
1159
|
+
if (state.autoSubmitTimer && state.asActiveId === p.id) return;
|
|
1160
|
+
if (state.autoSubmitTimer) {
|
|
1161
|
+
window.clearInterval(state.autoSubmitTimer);
|
|
1162
|
+
state.autoSubmitTimer = null;
|
|
1163
|
+
}
|
|
1164
|
+
const timeout = Math.max(5, Math.round(Number(state.settings.autoSubmitTimeout)) || 300);
|
|
1165
|
+
state.asActiveId = p.id;
|
|
1166
|
+
state.autoSubmitLeft = timeout;
|
|
1167
|
+
// 先建立计时器再刷新标签:updateAsCount 依赖 autoSubmitTimer 为真才显示。
|
|
1168
|
+
state.autoSubmitTimer = window.setInterval(() => {
|
|
1169
|
+
state.autoSubmitLeft -= 1;
|
|
1170
|
+
if (state.autoSubmitLeft <= 0) {
|
|
1171
|
+
fireAutoSubmit();
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
updateAsCount();
|
|
1175
|
+
}, 1000);
|
|
1176
|
+
updateAsCount();
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function cancelAutoSubmit() {
|
|
1180
|
+
if (state.autoSubmitTimer) {
|
|
1181
|
+
window.clearInterval(state.autoSubmitTimer);
|
|
1182
|
+
state.autoSubmitTimer = null;
|
|
1183
|
+
}
|
|
1184
|
+
state.autoSubmitLeft = 0;
|
|
1185
|
+
state.asActiveId = null;
|
|
1186
|
+
updateAsCount();
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// 用户介入(输入 / 手动插入提示词 / 手动发送 / 点倒计时)后,本轮不再自动提交。
|
|
1190
|
+
function suppressAutoSubmit() {
|
|
1191
|
+
state.autoSubmitSuppressed = true;
|
|
1192
|
+
cancelAutoSubmit();
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function fireAutoSubmit() {
|
|
1196
|
+
const p = getAutoSubmitPrompt();
|
|
1197
|
+
cancelAutoSubmit();
|
|
1198
|
+
if (!p || !p.content) return;
|
|
1199
|
+
if (state.currentState !== "ready") return;
|
|
1200
|
+
if (els.portSelect.value === config.customValue) return;
|
|
1201
|
+
// 自动场景:以配置的提示词内容覆盖输入框后走与手动一致的发送流程(含重连抢占)。
|
|
1202
|
+
els.prompt.value = p.content;
|
|
1203
|
+
autoResize();
|
|
1204
|
+
updateSendEnabled();
|
|
1205
|
+
doSend();
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// 浏览器内只能用 WebSocket 探测端口:连得上(或返回 4004「无 session」)即视为有 MCP 服务。
|
|
1209
|
+
// 连上后顺便读取会话的 project_directory,用于在下拉选项里显示项目名。
|
|
1210
|
+
function probePort(port, timeoutMs) {
|
|
1211
|
+
return new Promise((resolve) => {
|
|
1212
|
+
let done = false;
|
|
1213
|
+
let opened = false;
|
|
1214
|
+
let project = "";
|
|
1215
|
+
let statusLabel = "";
|
|
1216
|
+
let ws = null;
|
|
1217
|
+
const finish = (alive) => {
|
|
1218
|
+
if (done) return;
|
|
1219
|
+
done = true;
|
|
1220
|
+
window.clearTimeout(timer);
|
|
1221
|
+
if (ws) {
|
|
1222
|
+
ws.onopen = ws.onmessage = ws.onerror = ws.onclose = null;
|
|
1223
|
+
try {
|
|
1224
|
+
ws.close();
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
/* noop */
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
resolve({ alive, project, statusLabel });
|
|
1230
|
+
};
|
|
1231
|
+
const timer = window.setTimeout(() => finish(opened), timeoutMs);
|
|
1232
|
+
try {
|
|
1233
|
+
ws = new WebSocket("ws://127.0.0.1:" + port + "/ws?lang=" + config.lang);
|
|
1234
|
+
ws.onopen = () => {
|
|
1235
|
+
opened = true;
|
|
1236
|
+
};
|
|
1237
|
+
ws.onmessage = (event) => {
|
|
1238
|
+
try {
|
|
1239
|
+
const data = JSON.parse(event.data);
|
|
1240
|
+
const info = data.session_info || data.status_info;
|
|
1241
|
+
if (info && info.project_directory) {
|
|
1242
|
+
project = info.project_directory;
|
|
1243
|
+
statusLabel = deriveStatusLabel(info);
|
|
1244
|
+
finish(true);
|
|
1245
|
+
}
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
/* noop */
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
ws.onerror = () => finish(false);
|
|
1251
|
+
ws.onclose = (event) => finish(event.code === 4004 ? true : opened);
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
finish(false);
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
async function scanPorts() {
|
|
1259
|
+
if (state.scanning) return;
|
|
1260
|
+
state.scanning = true;
|
|
1261
|
+
if (els.scanBtn) {
|
|
1262
|
+
els.scanBtn.classList.add("scanning");
|
|
1263
|
+
els.scanBtn.disabled = true;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const from = config.scanStart;
|
|
1267
|
+
const to = config.scanStart + config.scanCount - 1;
|
|
1268
|
+
els.hint.textContent = "正在扫描端口 " + from + "–" + to + "…";
|
|
1269
|
+
|
|
1270
|
+
const ports = defaultPorts();
|
|
1271
|
+
const results = await Promise.all(
|
|
1272
|
+
ports.map((port) =>
|
|
1273
|
+
probePort(port, config.probeTimeoutMs).then((res) => ({
|
|
1274
|
+
port,
|
|
1275
|
+
alive: res.alive,
|
|
1276
|
+
project: res.project,
|
|
1277
|
+
statusLabel: res.statusLabel,
|
|
1278
|
+
}))
|
|
1279
|
+
)
|
|
1280
|
+
);
|
|
1281
|
+
const found = [];
|
|
1282
|
+
for (const item of results) {
|
|
1283
|
+
if (item.alive) {
|
|
1284
|
+
found.push(item.port);
|
|
1285
|
+
if (item.project) state.portProjects[item.port] = basename(item.project);
|
|
1286
|
+
else delete state.portProjects[item.port];
|
|
1287
|
+
if (item.statusLabel) state.portStatuses[item.port] = item.statusLabel;
|
|
1288
|
+
else delete state.portStatuses[item.port];
|
|
1289
|
+
} else {
|
|
1290
|
+
delete state.portProjects[item.port];
|
|
1291
|
+
delete state.portStatuses[item.port];
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
state.foundPorts = found;
|
|
1295
|
+
|
|
1296
|
+
const wasCustom = els.portSelect.value === config.customValue;
|
|
1297
|
+
fillPortOptions(found, wasCustom ? null : els.portSelect.value);
|
|
1298
|
+
if (wasCustom) els.portSelect.value = config.customValue;
|
|
1299
|
+
|
|
1300
|
+
if (els.scanBtn) {
|
|
1301
|
+
els.scanBtn.classList.remove("scanning");
|
|
1302
|
+
els.scanBtn.disabled = false;
|
|
1303
|
+
}
|
|
1304
|
+
state.scanning = false;
|
|
1305
|
+
|
|
1306
|
+
els.hint.textContent = found.length
|
|
1307
|
+
? "扫描完成:发现活跃端口 " + found.join("、") + "。"
|
|
1308
|
+
: "扫描完成:" + from + "–" + to + " 未发现活跃 MCP 端口。";
|
|
1309
|
+
|
|
1310
|
+
// 探测会短暂抢占连接,扫描结束后立即重连选中端口抢回「最后连接」。
|
|
1311
|
+
if (els.portSelect.value !== config.customValue) connectSelectedPort(false);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function showCustomInput() {
|
|
1315
|
+
els.customInput.style.display = "";
|
|
1316
|
+
els.customInput.value = "";
|
|
1317
|
+
els.customInput.focus();
|
|
1318
|
+
els.hint.textContent = "输入端口号后回车连接。";
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function hideCustomInput() {
|
|
1322
|
+
els.customInput.style.display = "none";
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function applyCustomPort() {
|
|
1326
|
+
const port = els.customInput.value.trim();
|
|
1327
|
+
if (!/^\d{2,5}$/.test(port)) {
|
|
1328
|
+
els.hint.textContent = "请输入有效的端口号(2–5 位数字)。";
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
let opt = Array.from(els.portSelect.options).find((item) => item.value === port);
|
|
1332
|
+
if (!opt) {
|
|
1333
|
+
opt = h("option", { value: port });
|
|
1334
|
+
const customOpt = Array.from(els.portSelect.options).find((item) => item.value === config.customValue);
|
|
1335
|
+
els.portSelect.insertBefore(opt, customOpt);
|
|
1336
|
+
}
|
|
1337
|
+
els.portSelect.value = port;
|
|
1338
|
+
hideCustomInput();
|
|
1339
|
+
connectSelectedPort(false);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function onPortChange() {
|
|
1343
|
+
state.expanded = false;
|
|
1344
|
+
if (els.portSelect.value === config.customValue) {
|
|
1345
|
+
showCustomInput();
|
|
1346
|
+
} else {
|
|
1347
|
+
hideCustomInput();
|
|
1348
|
+
connectSelectedPort(false);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function connectSelectedPort(auto) {
|
|
1353
|
+
const port = els.portSelect.value;
|
|
1354
|
+
if (port === config.customValue) return;
|
|
1355
|
+
const seq = ++state.connectSeq;
|
|
1356
|
+
|
|
1357
|
+
if (state.socket) {
|
|
1358
|
+
state.socket.onclose = null;
|
|
1359
|
+
state.socket.onerror = null;
|
|
1360
|
+
state.socket.onmessage = null;
|
|
1361
|
+
state.socket.close();
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
state.socket = null;
|
|
1365
|
+
state.socketPort = port;
|
|
1366
|
+
|
|
1367
|
+
if (!auto) {
|
|
1368
|
+
state.currentSession = null;
|
|
1369
|
+
updateProjectName(null);
|
|
1370
|
+
setOptionLabel(port, "connecting");
|
|
1371
|
+
setVisualState("connecting", port + " 正在连接 MCP WebSocket。");
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
try {
|
|
1375
|
+
const nextSocket = new WebSocket("ws://127.0.0.1:" + port + "/ws?lang=" + config.lang);
|
|
1376
|
+
state.socket = nextSocket;
|
|
1377
|
+
|
|
1378
|
+
nextSocket.onopen = () => {
|
|
1379
|
+
if (seq !== state.connectSeq) return;
|
|
1380
|
+
els.hint.textContent = port + " WebSocket 已打开,等待 MCP 返回会话状态。";
|
|
1381
|
+
// 发送场景:重连成功即已是「最后连接」。优先等服务端推送会话状态由 applySession 发送;
|
|
1382
|
+
// 若短时间内未推送,则兜底直接发送,避免卡在「正在重连」。
|
|
1383
|
+
if (state.wantSend && els.prompt.value.trim()) {
|
|
1384
|
+
window.setTimeout(() => {
|
|
1385
|
+
if (
|
|
1386
|
+
seq === state.connectSeq &&
|
|
1387
|
+
state.wantSend &&
|
|
1388
|
+
state.socket &&
|
|
1389
|
+
state.socket.readyState === WebSocket.OPEN &&
|
|
1390
|
+
els.prompt.value.trim()
|
|
1391
|
+
) {
|
|
1392
|
+
sendFeedback(els.prompt.value.trim());
|
|
1393
|
+
}
|
|
1394
|
+
}, config.sendAfterOpenMs);
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
|
|
1398
|
+
nextSocket.onmessage = (event) => {
|
|
1399
|
+
if (seq !== state.connectSeq) return;
|
|
1400
|
+
try {
|
|
1401
|
+
handleSocketMessage(JSON.parse(event.data));
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
els.hint.textContent = port + " 收到无法解析的 MCP 消息。";
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1407
|
+
nextSocket.onerror = () => {
|
|
1408
|
+
if (seq !== state.connectSeq) return;
|
|
1409
|
+
setOptionLabel(port, "offline");
|
|
1410
|
+
setVisualState("offline", port + " 连接失败,可能端口未启动或没有 MCP WebUI。");
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
nextSocket.onclose = (event) => {
|
|
1414
|
+
if (seq !== state.connectSeq) return;
|
|
1415
|
+
state.socket = null;
|
|
1416
|
+
if (state.currentState === "processing") return;
|
|
1417
|
+
setOptionLabel(port, event.code === 4004 ? "no session" : "offline");
|
|
1418
|
+
setVisualState(
|
|
1419
|
+
"offline",
|
|
1420
|
+
event.code === 4004
|
|
1421
|
+
? port + " 已连接到 MCP,但当前没有 active session。"
|
|
1422
|
+
: port + " WebSocket 已断开。"
|
|
1423
|
+
);
|
|
1424
|
+
};
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
setOptionLabel(port, "offline");
|
|
1427
|
+
setVisualState("offline", port + " 无法创建 WebSocket 连接。");
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// 服务端是「最后连接优先」模型:每个连接绑定其建立时的 session,且只向最后连接推送。
|
|
1432
|
+
// 因此面板必须持续保持为「最后连接」,否则会收不到新会话、提交也会作用到已失效的旧会话。
|
|
1433
|
+
// 定期重连以抢占活跃连接并同步当前会话;仅在用户正在输入时不打断(输入期间一般无其他端抢占)。
|
|
1434
|
+
function refreshTick() {
|
|
1435
|
+
if (state.scanning) return;
|
|
1436
|
+
if (els.portSelect.value === config.customValue) return;
|
|
1437
|
+
if (els.customInput === document.activeElement) return;
|
|
1438
|
+
if (els.prompt === document.activeElement && els.prompt.value.trim()) return;
|
|
1439
|
+
connectSelectedPort(true);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function autoResize() {
|
|
1443
|
+
els.prompt.style.height = "auto";
|
|
1444
|
+
els.prompt.style.height = Math.min(els.prompt.scrollHeight, 160) + "px";
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function sendFeedback(text) {
|
|
1448
|
+
state.socket.send(
|
|
1449
|
+
JSON.stringify({
|
|
1450
|
+
type: "submit_feedback",
|
|
1451
|
+
feedback: text,
|
|
1452
|
+
images: [],
|
|
1453
|
+
settings: {},
|
|
1454
|
+
})
|
|
1455
|
+
);
|
|
1456
|
+
|
|
1457
|
+
state.wantSend = false;
|
|
1458
|
+
els.prompt.value = "";
|
|
1459
|
+
autoResize();
|
|
1460
|
+
updateSendEnabled();
|
|
1461
|
+
setOptionLabel(els.portSelect.value, "AI processing");
|
|
1462
|
+
setVisualState("processing", els.portSelect.value + " 已发送反馈,等待 MCP 返回给 AI。");
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function doSend() {
|
|
1466
|
+
const text = els.prompt.value.trim();
|
|
1467
|
+
if (!text) {
|
|
1468
|
+
els.hint.textContent = "请输入反馈内容后再发送。";
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
if (els.portSelect.value === config.customValue) {
|
|
1472
|
+
els.hint.textContent = "请先选择端口或输入有效端口号。";
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// 一旦发送(手动或自动触发后走到这里),本轮不再倒计时。
|
|
1477
|
+
state.autoSubmitSuppressed = true;
|
|
1478
|
+
cancelAutoSubmit();
|
|
1479
|
+
|
|
1480
|
+
// MCP 是「最后连接优先」模型:本地 socket 即使仍是 OPEN,服务端的活跃连接也可能已被
|
|
1481
|
+
// 其他客户端(WebUI / 其他面板)顶掉,此时直接发会作用到已失效的会话,表现为「总断」。
|
|
1482
|
+
// 因此发送前总是重连一次,抢回「最后连接」后再发(连上由 applySession / onopen 兜底触发)。
|
|
1483
|
+
state.wantSend = true;
|
|
1484
|
+
setVisualState("connecting", els.portSelect.value + " 正在重连,连上后自动发送…");
|
|
1485
|
+
connectSelectedPort(false);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function revertFromCustom() {
|
|
1489
|
+
els.customInput.value = "";
|
|
1490
|
+
hideCustomInput();
|
|
1491
|
+
if (els.portSelect.value === config.customValue) {
|
|
1492
|
+
const first = Array.from(els.portSelect.options).find((item) => item.value !== config.customValue);
|
|
1493
|
+
if (first) {
|
|
1494
|
+
els.portSelect.value = first.value;
|
|
1495
|
+
connectSelectedPort(false);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function wireEvents() {
|
|
1501
|
+
els.portSelect.addEventListener("change", onPortChange);
|
|
1502
|
+
// 展开下拉前让所有项带上「状态 · 项目名」;收起后选中项去掉项目名(右侧已显示)。
|
|
1503
|
+
els.portSelect.addEventListener("mousedown", () => {
|
|
1504
|
+
state.expanded = true;
|
|
1505
|
+
refreshOptionLabels();
|
|
1506
|
+
});
|
|
1507
|
+
els.portSelect.addEventListener("blur", () => {
|
|
1508
|
+
state.expanded = false;
|
|
1509
|
+
refreshOptionLabels();
|
|
1510
|
+
});
|
|
1511
|
+
els.scanBtn.addEventListener("click", () => {
|
|
1512
|
+
scanPorts();
|
|
1513
|
+
});
|
|
1514
|
+
els.customInput.addEventListener("keydown", (event) => {
|
|
1515
|
+
event.stopPropagation();
|
|
1516
|
+
if (event.key === "Enter" || event.code === "Enter" || event.code === "NumpadEnter") {
|
|
1517
|
+
event.preventDefault();
|
|
1518
|
+
applyCustomPort();
|
|
1519
|
+
} else if (event.key === "Escape") {
|
|
1520
|
+
event.preventDefault();
|
|
1521
|
+
revertFromCustom();
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
els.customInput.addEventListener("blur", () => {
|
|
1525
|
+
if (/^\d{2,5}$/.test(els.customInput.value.trim())) applyCustomPort();
|
|
1526
|
+
else revertFromCustom();
|
|
1527
|
+
});
|
|
1528
|
+
els.promptSelect.addEventListener("change", onPromptPick);
|
|
1529
|
+
els.promptRefresh.addEventListener("click", refreshPrompts);
|
|
1530
|
+
els.sendBtn.addEventListener("click", doSend);
|
|
1531
|
+
|
|
1532
|
+
// 配置抽屉:齿轮开关 + 提示词增删改 + 自动提交配置。
|
|
1533
|
+
els.gear.addEventListener("click", toggleDrawer);
|
|
1534
|
+
els.pAdd.addEventListener("click", () => openPromptEditor(null));
|
|
1535
|
+
els.pSave.addEventListener("click", savePromptEditor);
|
|
1536
|
+
els.pCancel.addEventListener("click", closePromptEditor);
|
|
1537
|
+
els.asToggle.addEventListener("change", onAutoSubmitToggle);
|
|
1538
|
+
els.asTimeout.addEventListener("change", onAutoSubmitTimeoutChange);
|
|
1539
|
+
els.asSelect.addEventListener("change", onAutoSubmitSelect);
|
|
1540
|
+
els.asCount.addEventListener("click", suppressAutoSubmit);
|
|
1541
|
+
[els.pName, els.pContent].forEach((field) => {
|
|
1542
|
+
field.addEventListener("keydown", (event) => {
|
|
1543
|
+
event.stopPropagation();
|
|
1544
|
+
if (event.key === "Escape") {
|
|
1545
|
+
event.preventDefault();
|
|
1546
|
+
closePromptEditor();
|
|
1547
|
+
} else if ((event.key === "Enter" || event.code === "Enter") && (event.metaKey || event.ctrlKey)) {
|
|
1548
|
+
event.preventDefault();
|
|
1549
|
+
savePromptEditor();
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
els.prompt.addEventListener("input", () => {
|
|
1555
|
+
autoResize();
|
|
1556
|
+
updateSendEnabled();
|
|
1557
|
+
suppressAutoSubmit();
|
|
1558
|
+
});
|
|
1559
|
+
els.prompt.addEventListener("focus", () => {
|
|
1560
|
+
if (els.portSelect.value !== config.customValue) connectSelectedPort(true);
|
|
1561
|
+
});
|
|
1562
|
+
// 跟踪输入法组字状态:组字期间(含上屏候选词的回车)不触发发送。
|
|
1563
|
+
els.prompt.addEventListener("compositionstart", () => {
|
|
1564
|
+
state.composing = true;
|
|
1565
|
+
});
|
|
1566
|
+
els.prompt.addEventListener("compositionend", () => {
|
|
1567
|
+
state.composing = false;
|
|
1568
|
+
});
|
|
1569
|
+
els.prompt.addEventListener("keydown", (event) => {
|
|
1570
|
+
event.stopPropagation();
|
|
1571
|
+
// 组字进行中的回车用于上屏(拼音/注音等),放行给输入法处理,不发送。
|
|
1572
|
+
if (state.composing || event.isComposing || event.keyCode === 229) return;
|
|
1573
|
+
if ((event.key === "Enter" || event.code === "Enter" || event.code === "NumpadEnter") && !event.shiftKey) {
|
|
1574
|
+
event.preventDefault();
|
|
1575
|
+
doSend();
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function uninstall() {
|
|
1581
|
+
cancelAutoSubmit();
|
|
1582
|
+
for (const timer of state.timers) {
|
|
1583
|
+
window.clearInterval(timer);
|
|
1584
|
+
}
|
|
1585
|
+
state.timers = [];
|
|
1586
|
+
|
|
1587
|
+
if (state.socket) {
|
|
1588
|
+
state.socket.onclose = null;
|
|
1589
|
+
state.socket.onerror = null;
|
|
1590
|
+
state.socket.onmessage = null;
|
|
1591
|
+
try {
|
|
1592
|
+
state.socket.close();
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
/* noop */
|
|
1595
|
+
}
|
|
1596
|
+
state.socket = null;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
els.panel?.remove();
|
|
1600
|
+
document.getElementById(config.styleId)?.remove();
|
|
1601
|
+
delete window[NAME];
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
ensureStyle();
|
|
1605
|
+
buildPanel();
|
|
1606
|
+
ensureMounted();
|
|
1607
|
+
connectSelectedPort(false);
|
|
1608
|
+
scanPorts();
|
|
1609
|
+
|
|
1610
|
+
state.timers.push(window.setInterval(ensureMounted, config.mountScanMs));
|
|
1611
|
+
state.timers.push(window.setInterval(refreshTick, config.reconnectMs));
|
|
1612
|
+
|
|
1613
|
+
window[NAME] = {
|
|
1614
|
+
config,
|
|
1615
|
+
state,
|
|
1616
|
+
remount: ensureMounted,
|
|
1617
|
+
reconnect: () => connectSelectedPort(false),
|
|
1618
|
+
scan: scanPorts,
|
|
1619
|
+
toggleConfig: toggleDrawer,
|
|
1620
|
+
_reevalAutoSubmit: reevaluateAutoSubmit,
|
|
1621
|
+
uninstall,
|
|
1622
|
+
status() {
|
|
1623
|
+
return {
|
|
1624
|
+
installedAt: state.installedAt,
|
|
1625
|
+
mounted: state.mounted,
|
|
1626
|
+
socketPort: state.socketPort,
|
|
1627
|
+
currentState: state.currentState,
|
|
1628
|
+
currentSession: state.currentSession,
|
|
1629
|
+
scanning: state.scanning,
|
|
1630
|
+
foundPorts: state.foundPorts,
|
|
1631
|
+
};
|
|
1632
|
+
},
|
|
1633
|
+
};
|
|
1634
|
+
|
|
1635
|
+
return window[NAME].status();
|
|
1636
|
+
})();
|