dsh-ui-tweaks 0.3.2 → 0.4.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/lib/client.js CHANGED
@@ -1,5 +1,203 @@
1
1
  window.__ModuleLoader__.load({ id: "dsh-ui-tweaks", factory: (require) => {
2
2
  var __modules = Object.create(null); var __cache = Object.create(null);
3
+ __modules["./archive.js"] = function(module, exports, require, __load_) {
4
+ "use strict";
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ARCHIVE_CSS = void 0;
7
+ exports.installArchiveStyles = installArchiveStyles;
8
+ exports.ArchiveSection = ArchiveSection;
9
+ const jsx_runtime_1 = require("react/jsx-runtime");
10
+ /**
11
+ * dsh-ui-tweaks — Archive manager (browser half).
12
+ *
13
+ * Renders the "归档" settings section (shown in the Settings dialog; usable
14
+ * when the `archiveManagerEnabled` toggle in 界面调整 is on — otherwise it
15
+ * shows an invite card with a one-click enable). The section lists every
16
+ * archived session — enriched from the framework `useSessions` /
17
+ * `useWorkspaces` feeds — with per-row actions and batch actions that go
18
+ * through the same-origin archive route (`/_dsh/ui-tweaks/archive`):
19
+ *
20
+ * - **恢复** (Restore): remove the session from the archive set — its log and
21
+ * workspace slot are kept, so the conversation reappears in the normal
22
+ * sidebar list.
23
+ * - **删除** (Delete): PERMANENTLY delete the session — the server removes its
24
+ * durable log, workspace accounting, archive-set entry and caches; the list
25
+ * then refreshes so the row (and the session itself) disappear.
26
+ * - **全部恢复 / 全部删除**: the same actions for every archived session.
27
+ *
28
+ * Live (open/running) sessions are refused by the server; the client maps the
29
+ * `session-live` error code to localized copy.
30
+ * @module dsh-ui-tweaks/client/archive
31
+ */
32
+ const react_1 = require("react");
33
+ /** Route matching the host half (src/archive.ts). */
34
+ const ARCHIVE_ROUTE = '/_dsh/ui-tweaks/archive';
35
+ /** Fetch one archive route call, attaching the server error code when present. */
36
+ async function archiveRequest(init) {
37
+ const response = await fetch(ARCHIVE_ROUTE, { credentials: 'same-origin', ...init });
38
+ let body;
39
+ try {
40
+ body = await response.json();
41
+ }
42
+ catch {
43
+ // Non-JSON response (e.g. the route is not mounted yet) — fall through to
44
+ // a clean error instead of a JSON-parse exception.
45
+ body = undefined;
46
+ }
47
+ if (!response.ok || body === undefined || !body.ok) {
48
+ const failure = body;
49
+ const error = new Error(failure?.error?.message ?? `UI Tweaks archive request failed with HTTP ${response.status}`);
50
+ error.code = failure?.error?.code;
51
+ throw error;
52
+ }
53
+ return body.value;
54
+ }
55
+ /** Map a thrown archive error to user-facing copy. */
56
+ function errorMessage(error, t) {
57
+ const code = error?.code;
58
+ if (code === 'session-live')
59
+ return t('archiveLiveError');
60
+ return error instanceof Error ? error.message : String(error);
61
+ }
62
+ /** Re-pull the host session list after a permanent delete (the outward
63
+ * ISessions face omits refresh; the concrete runtime provides it). */
64
+ function refreshSessions(sessionsService) {
65
+ void sessionsService.refresh();
66
+ }
67
+ /** Workspace display title: the path's last non-empty segment. */
68
+ function basenameOf(path) {
69
+ if (path === undefined || path === '')
70
+ return '';
71
+ const cleaned = path.replace(/[\\/]+$/u, '');
72
+ const segments = cleaned.split(/[\\/]/u);
73
+ return segments.at(-1) ?? '';
74
+ }
75
+ /** Locale-aware relative time for a past timestamp, e.g. "3小时前". */
76
+ function relativeTime(ms, now, formatter) {
77
+ const diffSeconds = Math.round((ms - now) / 1000);
78
+ const abs = Math.abs(diffSeconds);
79
+ if (abs < 60)
80
+ return formatter.format(diffSeconds, 'second');
81
+ if (abs < 3600)
82
+ return formatter.format(Math.round(diffSeconds / 60), 'minute');
83
+ if (abs < 86400)
84
+ return formatter.format(Math.round(diffSeconds / 3600), 'hour');
85
+ if (abs < 604800)
86
+ return formatter.format(Math.round(diffSeconds / 86400), 'day');
87
+ return formatter.format(Math.round(diffSeconds / 604800), 'week');
88
+ }
89
+ exports.ARCHIVE_CSS = `
90
+ .dut-arc{display:grid;gap:12px;max-width:680px;padding:6px 2px 36px}
91
+ .dut-arc-head{display:flex;align-items:center;gap:10px;padding:14px 16px 0}
92
+ .dut-arc-head h2{margin:0;font-size:15px;font-weight:600;letter-spacing:-.01em;color:var(--dsw-alias-label-primary)}
93
+ .dut-arc-count{font-size:11.5px;color:var(--dsw-alias-label-secondary);padding:2px 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1)}
94
+ .dut-arc-spacer{flex:1}
95
+ .dut-arc-btn{display:inline-flex;align-items:center;height:28px;padding:0 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}
96
+ .dut-arc-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
97
+ .dut-arc-btn:disabled{opacity:.5;cursor:default}
98
+ .dut-arc-btn.dut-arc-del:hover{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}
99
+ .dut-arc-btn.dut-arc-confirm{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);font-weight:600}
100
+ .dut-arc-body{display:grid;gap:2px;margin-top:4px}
101
+ .dut-arc-row{display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:12px}
102
+ .dut-arc-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
103
+ .dut-arc-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;text-align:left;background:transparent;border:none;padding:0;cursor:default;color:var(--dsw-alias-label-primary);font:inherit}
104
+ .dut-arc-title{font-size:13.5px;font-weight:600;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
105
+ .dut-arc-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
106
+ .dut-arc-empty{padding:32px 16px;text-align:center;font-size:12.5px;color:var(--dsw-alias-label-secondary)}
107
+ .dut-arc-alert{margin:8px 16px 0;padding:8px 12px;border-radius:10px;font-size:12px;line-height:1.5;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
108
+ .dut-arc-off{padding:20px 16px;display:grid;gap:12px}
109
+ .dut-arc-off p{margin:0;font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-secondary)}
110
+ `;
111
+ /** Install the Archive stylesheet once (idempotent); returns the disposer. */
112
+ function installArchiveStyles() {
113
+ const id = 'dsh-ui-tweaks-archive';
114
+ const existing = document.querySelector(`style[data-plugin-css="${id}"]`);
115
+ if (existing !== null)
116
+ return () => { };
117
+ const style = document.createElement('style');
118
+ style.dataset.plugin = 'dsh-ui-tweaks';
119
+ style.dataset.pluginCss = id;
120
+ style.textContent = exports.ARCHIVE_CSS;
121
+ document.head.appendChild(style);
122
+ return () => { style.remove(); };
123
+ }
124
+ function ArchiveSection({ controller, t, sessionsService, useSessions, useWorkspaces }) {
125
+ const settingsState = (0, react_1.useSyncExternalStore)(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
126
+ const enabled = settingsState.value?.archiveManagerEnabled === true;
127
+ const sessions = useSessions(state => state);
128
+ const workspaces = useWorkspaces(state => state);
129
+ const [confirmId, setConfirmId] = (0, react_1.useState)(null);
130
+ const [confirmAll, setConfirmAll] = (0, react_1.useState)(false);
131
+ const [busy, setBusy] = (0, react_1.useState)(null);
132
+ const [error, setError] = (0, react_1.useState)(null);
133
+ const timeFormatter = (0, react_1.useMemo)(() => new Intl.RelativeTimeFormat(navigator.language, { numeric: 'auto' }), []);
134
+ const now = Date.now();
135
+ (0, react_1.useEffect)(() => {
136
+ if (confirmId === null)
137
+ return;
138
+ const timer = window.setTimeout(() => { setConfirmId(current => (current === confirmId ? null : current)); }, 3000);
139
+ return () => { window.clearTimeout(timer); };
140
+ }, [confirmId]);
141
+ (0, react_1.useEffect)(() => {
142
+ if (!confirmAll)
143
+ return;
144
+ const timer = window.setTimeout(() => { setConfirmAll(false); }, 3000);
145
+ return () => { window.clearTimeout(timer); };
146
+ }, [confirmAll]);
147
+ const rows = (0, react_1.useMemo)(() => {
148
+ const byId = sessions.byId;
149
+ return workspaces.archivedSessionIds.map(id => ({ id, summary: byId[id] })).sort((a, b) => {
150
+ const ta = a.summary?.updatedAt ?? 0;
151
+ const tb = b.summary?.updatedAt ?? 0;
152
+ return tb - ta;
153
+ });
154
+ }, [workspaces.archivedSessionIds, sessions.byId]);
155
+ const run = (key, body, after) => {
156
+ setBusy(key);
157
+ setError(null);
158
+ void archiveRequest({
159
+ method: 'POST',
160
+ headers: { 'Content-Type': 'application/json' },
161
+ body: JSON.stringify(body),
162
+ }).then(() => { after?.(); }).catch((reason) => {
163
+ setError(errorMessage(reason, t));
164
+ }).finally(() => { setBusy(current => (current === key ? null : current)); });
165
+ };
166
+ const restore = (id) => {
167
+ run(`restore:${id}`, { action: 'restore', sessionId: id });
168
+ };
169
+ const remove = (id) => {
170
+ if (confirmId !== id) {
171
+ setConfirmId(id);
172
+ return;
173
+ }
174
+ setConfirmId(null);
175
+ run(`delete:${id}`, { action: 'delete', sessionId: id }, () => { refreshSessions(sessionsService); });
176
+ };
177
+ const restoreAll = () => {
178
+ run('restore-all', { action: 'restore-all' });
179
+ };
180
+ const removeAll = () => {
181
+ if (!confirmAll) {
182
+ setConfirmAll(true);
183
+ return;
184
+ }
185
+ setConfirmAll(false);
186
+ run('delete-all', { action: 'delete-all' }, () => { refreshSessions(sessionsService); });
187
+ };
188
+ if (!enabled) {
189
+ return ((0, jsx_runtime_1.jsx)("div", { className: "dut-arc", children: (0, jsx_runtime_1.jsxs)("div", { className: "dut-panel dut-arc-off", children: [(0, jsx_runtime_1.jsx)("p", { children: t('archiveDisabledHint') }), (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "dut-btn dut-btn-active", onClick: () => {
190
+ void controller.set('archiveManagerEnabled', true).catch(() => { setError(t('unavailable')); });
191
+ }, children: t('archiveEnable') }) })] }) }));
192
+ }
193
+ return ((0, jsx_runtime_1.jsx)("div", { className: "dut-arc", children: (0, jsx_runtime_1.jsxs)("div", { className: "dut-panel", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-arc-head", children: [(0, jsx_runtime_1.jsx)("h2", { children: t('archiveTitle') }), (0, jsx_runtime_1.jsxs)("span", { className: "dut-arc-count", children: [rows.length, " ", t('archiveCount')] }), (0, jsx_runtime_1.jsx)("span", { className: "dut-arc-spacer" }), rows.length > 1 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "dut-arc-btn", disabled: busy !== null, onClick: restoreAll, children: busy === 'restore-all' ? t('archiveRestoring') : t('archiveRestoreAll') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: 'dut-arc-btn dut-arc-del' + (confirmAll ? ' dut-arc-confirm' : ''), disabled: busy !== null, onClick: removeAll, children: busy === 'delete-all' ? t('archiveDeleteAll') + '…' : confirmAll ? t('archiveDeleteAll') + '?' : t('archiveDeleteAll') })] })) : null] }), error !== null ? (0, jsx_runtime_1.jsx)("div", { className: "dut-arc-alert", children: error }) : null, (0, jsx_runtime_1.jsx)("div", { className: "dut-arc-body", children: rows.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "dut-arc-empty", children: t('archiveEmpty') })) : rows.map(row => {
194
+ const summary = row.summary;
195
+ const title = summary?.displayTitle ?? row.id;
196
+ const sub = [basenameOf(summary?.cwd), summary !== undefined ? relativeTime(summary.updatedAt, now, timeFormatter) : ''].filter(Boolean).join(' · ');
197
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "dut-arc-row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-arc-main", title: title, children: [(0, jsx_runtime_1.jsx)("span", { className: "dut-arc-title", children: title }), sub !== '' ? (0, jsx_runtime_1.jsx)("span", { className: "dut-arc-sub", children: sub }) : null] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "dut-arc-btn", disabled: busy !== null, onClick: () => { restore(row.id); }, children: busy === `restore:${row.id}` ? t('archiveRestoring') : t('archiveRestore') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: 'dut-arc-btn dut-arc-del' + (confirmId === row.id ? ' dut-arc-confirm' : ''), disabled: busy !== null, onClick: () => { remove(row.id); }, children: busy === `delete:${row.id}` ? t('archiveDelete') + '…' : confirmId === row.id ? t('archiveDelete') + '?' : t('archiveDelete') })] }, row.id));
198
+ }) })] }) }));
199
+ }
200
+ };
3
201
  __modules["./gitbar.js"] = function(module, exports, require, __load_) {
4
202
  "use strict";
5
203
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -937,6 +1135,8 @@ const jsx_runtime_1 = require("react/jsx-runtime");
937
1135
  const react_1 = require("react");
938
1136
  const timeline_tsx_1 = __load_("./timeline.js");
939
1137
  const gitbar_tsx_1 = __load_("./gitbar.js");
1138
+ const archive_tsx_1 = __load_("./archive.js");
1139
+ const mcp_tsx_1 = __load_("./mcp.js");
940
1140
  const NS = 'ui-tweaks';
941
1141
  const SETTINGS_ROUTE = '/_dsh/ui-tweaks/settings';
942
1142
  const DEFAULT_FONT_SIZE = 16;
@@ -964,13 +1164,82 @@ const en = {
964
1164
  presetWide: 'Wide',
965
1165
  presetWideXl: 'Extra wide',
966
1166
  timeline: 'Timeline',
967
- timelineHint: 'Show a navigation rail on the right of the message area: hover to preview user messages, click to jump to them. Off by default; automatically hidden in short conversations.',
1167
+ timelineHint: 'Show a navigation rail on the right of the message area: hover to preview user messages, click to jump to them. Off by default; turn it on to enable. Automatically hidden in short conversations.',
968
1168
  timelineOn: 'On',
969
- timelineDefault: 'Default',
1169
+ timelineOff: 'Off',
970
1170
  gitBar: 'Git bar',
971
- gitBarHint: 'Show branch / diff / commit-message pills above the input when the session is inside a git repository. On by default; the bar auto-hides outside git repos.',
1171
+ gitBarHint: 'Show branch / diff / commit-message pills above the input when the session is inside a git repository. Off by default; turn it on to enable. The bar auto-hides outside git repos.',
972
1172
  gitBarOn: 'On',
973
1173
  gitBarOff: 'Off',
1174
+ archiveManager: 'Archive manager',
1175
+ archiveManagerHint: 'Show an Archive section in the Settings dialog where archived sessions can be restored or permanently deleted. Off by default; turn it on to enable.',
1176
+ archiveManagerOn: 'On',
1177
+ archiveManagerOff: 'Off',
1178
+ mcpManager: 'MCP manager',
1179
+ mcpManagerHint: 'Show an MCP management section in the Settings dialog listing the configured MCP servers with their status and tools, and let you restart them. Off by default; turn it on to enable.',
1180
+ mcpManagerOn: 'On',
1181
+ mcpManagerOff: 'Off',
1182
+ mcpNav: 'MCP',
1183
+ mcpTitle: 'MCP servers',
1184
+ mcpEmpty: 'No MCP servers configured.',
1185
+ mcpStatusActive: 'Running',
1186
+ mcpStatusFailed: 'Failed',
1187
+ mcpStatusLoading: 'Loading',
1188
+ mcpStatusStopped: 'Stopped',
1189
+ mcpStatusDisabled: 'Disabled',
1190
+ mcpTools: 'tools',
1191
+ mcpEnv: 'Env',
1192
+ mcpAdd: 'Add server',
1193
+ mcpAddTitle: 'Add MCP server',
1194
+ mcpEditTitle: 'Edit MCP server',
1195
+ mcpEdit: 'Edit',
1196
+ mcpDelete: 'Delete',
1197
+ mcpEnabledAction: 'Enable',
1198
+ mcpDisabledAction: 'Disable',
1199
+ mcpSaved: 'Saved.',
1200
+ mcpRemoved: 'Removed.',
1201
+ mcpFormTab: 'Form',
1202
+ mcpYamlTab: 'YAML',
1203
+ mcpYamlHint: 'Fill in the MCP config directly (serverName / transport / command / args / env / toolCallTimeoutMs / url / headers …). It is validated before saving.',
1204
+ mcpYamlPlaceholder: 'serverName: my-server\ntransport: stdio\ncommand: npx\nargs:\n - "-y"\n - "@some/mcp-server"\nenv:\n KEY: value\ntoolCallTimeoutMs: 60000',
1205
+ mcpFieldId: 'Instance ID',
1206
+ mcpFieldIdHint: 'Loader entry id (letters, digits, - and _ only); it must be unique.',
1207
+ mcpFieldName: 'Name',
1208
+ mcpFieldNameHint: 'Tool namespace `mcp__<name>__*`; letters, digits, - and _ (1–32).',
1209
+ mcpFieldType: 'Type',
1210
+ mcpFieldTimeout: 'Timeout (ms)',
1211
+ mcpFieldCommand: 'Command',
1212
+ mcpFieldCommandPlaceholder: 'e.g. npx',
1213
+ mcpFieldArgs: 'Arguments (one per line)',
1214
+ mcpFieldEnv: 'Environment vars (optional, KEY=value per line)',
1215
+ mcpFieldUrl: 'URL',
1216
+ mcpFieldHeaders: 'Headers (optional, Key: value per line)',
1217
+ mcpFieldEnabled: 'Enabled',
1218
+ mcpSave: 'Save',
1219
+ mcpCancel: 'Cancel',
1220
+ mcpInvalidId: 'Instance ID may only contain letters, digits, - and _.',
1221
+ mcpInvalidName: 'Name may only contain letters, digits, - and _ (1–32 chars).',
1222
+ mcpInvalidTimeout: 'Timeout must be a positive integer (ms).',
1223
+ mcpInvalidUrl: 'URL must start with http:// or https://.',
1224
+ mcpInvalidCommand: 'Command is required.',
1225
+ mcpUnavailable: 'MCP manager unavailable.',
1226
+ mcpDisabledHint: 'MCP management is off. Turn it on in 界面调整 (UI Tweaks) to view and restart MCP servers here.',
1227
+ mcpEnable: 'Enable MCP manager',
1228
+ mcpServerDetail: 'Configured in the profile cordis.patch.yml as @deepseek-ai/dsh-mcp-client instances; add / edit / disable / delete write to that file and apply live.',
1229
+ archiveNav: 'Archive',
1230
+ archiveTitle: 'Archived sessions',
1231
+ archiveEmpty: 'No archived sessions.',
1232
+ archiveRestore: 'Restore',
1233
+ archiveRestoring: 'Working…',
1234
+ archiveDelete: 'Delete',
1235
+ archiveDeleteAll: 'Delete all',
1236
+ archiveRestoreAll: 'Restore all',
1237
+ archiveCount: 'sessions',
1238
+ archiveUnavailable: 'Archive unavailable.',
1239
+ archiveLiveError: 'This session is running; close it before permanently deleting it.',
1240
+ archiveDisabledHint: 'Archive management is off. Turn it on in 界面调整 (UI Tweaks) to restore or permanently delete archived sessions here.',
1241
+ archiveEnable: 'Enable archive management',
1242
+ archiveRestored: 'Restored.',
974
1243
  railLabel: 'Chat timeline',
975
1244
  roleUser: 'User',
976
1245
  noText: '(no text)',
@@ -1040,13 +1309,82 @@ const zh = {
1040
1309
  presetWide: '稍宽',
1041
1310
  presetWideXl: '更宽',
1042
1311
  timeline: '时间线',
1043
- timelineHint: '在消息区右侧显示导航轨:悬停预览用户消息、点击跳转。默认关闭,会话较短时自动隐藏。',
1312
+ timelineHint: '在消息区右侧显示导航轨:悬停预览用户消息、点击跳转。默认关闭,需手动开启;会话较短时自动隐藏。',
1044
1313
  timelineOn: '开启',
1045
- timelineDefault: '默认',
1314
+ timelineOff: '关闭',
1046
1315
  gitBar: 'Git 状态栏',
1047
- gitBarHint: '会话在 git 仓库内时,在输入框上方显示 分支 / 差异 / 提交说明 胶囊。默认开启;非 git 仓库时自动隐藏。',
1316
+ gitBarHint: '会话在 git 仓库内时,在输入框上方显示 分支 / 差异 / 提交说明 胶囊。默认关闭,需手动开启;非 git 仓库时自动隐藏。',
1048
1317
  gitBarOn: '开启',
1049
1318
  gitBarOff: '关闭',
1319
+ archiveManager: '归档管理',
1320
+ archiveManagerHint: '在设置中显示「归档」页面:可查看、恢复或彻底删除已归档会话。默认关闭,需手动开启。',
1321
+ archiveManagerOn: '开启',
1322
+ archiveManagerOff: '关闭',
1323
+ mcpManager: 'MCP 管理',
1324
+ mcpManagerHint: '在设置中显示「MCP 管理」页面:查看已配置的 MCP 服务器及其状态与工具,并可重启。默认关闭,需手动开启。',
1325
+ mcpManagerOn: '开启',
1326
+ mcpManagerOff: '关闭',
1327
+ mcpNav: 'MCP 管理',
1328
+ mcpTitle: 'MCP 服务器',
1329
+ mcpEmpty: '未配置任何 MCP 服务器。',
1330
+ mcpStatusActive: '运行中',
1331
+ mcpStatusFailed: '错误',
1332
+ mcpStatusLoading: '加载中',
1333
+ mcpStatusStopped: '未运行',
1334
+ mcpStatusDisabled: '已停用',
1335
+ mcpTools: '个工具',
1336
+ mcpEnv: '环境变量',
1337
+ mcpAdd: '添加服务器',
1338
+ mcpAddTitle: '添加 MCP 服务器',
1339
+ mcpEditTitle: '编辑 MCP 服务器',
1340
+ mcpEdit: '编辑',
1341
+ mcpDelete: '删除',
1342
+ mcpEnabledAction: '启用',
1343
+ mcpDisabledAction: '停用',
1344
+ mcpSaved: '已保存。',
1345
+ mcpRemoved: '已删除。',
1346
+ mcpFormTab: '表单',
1347
+ mcpYamlTab: 'YAML',
1348
+ mcpYamlHint: '直接填写 MCP 配置(serverName / transport / command / args / env / toolCallTimeoutMs / url / headers …),保存前会校验格式。',
1349
+ mcpYamlPlaceholder: 'serverName: my-server\ntransport: stdio\ncommand: npx\nargs:\n - "-y"\n - "@some/mcp-server"\nenv:\n KEY: value\ntoolCallTimeoutMs: 60000',
1350
+ mcpFieldId: '实例 ID',
1351
+ mcpFieldIdHint: '加载器条目 ID(仅字母、数字、- 和 _),需唯一。',
1352
+ mcpFieldName: '名称',
1353
+ mcpFieldNameHint: '工具命名空间 `mcp__<名称>__*`;字母、数字、- 和 _(1–32 字符)。',
1354
+ mcpFieldType: '类型',
1355
+ mcpFieldTimeout: '超时时间 (ms)',
1356
+ mcpFieldCommand: '命令',
1357
+ mcpFieldCommandPlaceholder: '如 npx',
1358
+ mcpFieldArgs: '参数(每行一个)',
1359
+ mcpFieldEnv: '环境变量(可选,每行 KEY=值)',
1360
+ mcpFieldUrl: 'URL',
1361
+ mcpFieldHeaders: '请求头(可选,每行 Key: 值)',
1362
+ mcpFieldEnabled: '启用',
1363
+ mcpSave: '保存',
1364
+ mcpCancel: '取消',
1365
+ mcpInvalidId: '实例 ID 只能包含字母、数字、- 和 _。',
1366
+ mcpInvalidName: '名称只能包含字母、数字、- 和 _(1–32 字符)。',
1367
+ mcpInvalidTimeout: '超时时间必须是正整数(毫秒)。',
1368
+ mcpInvalidUrl: 'URL 必须以 http:// 或 https:// 开头。',
1369
+ mcpInvalidCommand: '命令不能为空。',
1370
+ mcpUnavailable: 'MCP 管理暂不可用。',
1371
+ mcpDisabledHint: 'MCP 管理尚未开启。在「界面调整」中开启“MCP 管理”后,可在此查看并重启 MCP 服务器。',
1372
+ mcpEnable: '开启 MCP 管理',
1373
+ mcpServerDetail: 'MCP 服务器配置在 profile 的 cordis.patch.yml(@deepseek-ai/dsh-mcp-client 实例);添加 / 编辑 / 停用 / 删除会写入该文件,改动实时生效。',
1374
+ archiveNav: '归档',
1375
+ archiveTitle: '已归档会话',
1376
+ archiveEmpty: '暂无归档会话。',
1377
+ archiveRestore: '恢复',
1378
+ archiveRestoring: '处理中…',
1379
+ archiveDelete: '删除',
1380
+ archiveDeleteAll: '全部删除',
1381
+ archiveRestoreAll: '全部恢复',
1382
+ archiveCount: '个会话',
1383
+ archiveUnavailable: '归档暂不可用。',
1384
+ archiveLiveError: '该会话正在运行,无法彻底删除,请先关闭该会话。',
1385
+ archiveDisabledHint: '归档管理尚未开启。在「界面调整」中开启“归档管理”后,可在此查看、恢复或彻底删除已归档会话。',
1386
+ archiveEnable: '开启归档管理',
1387
+ archiveRestored: '已恢复。',
1050
1388
  railLabel: '对话时间线',
1051
1389
  roleUser: '用户',
1052
1390
  noText: '(无文本内容)',
@@ -1111,7 +1449,9 @@ function resolveValue(value) {
1111
1449
  tableStyle: value?.tableStyle === 'claude' ? 'claude' : 'default',
1112
1450
  dialogWidth: resolveDialogWidth(value?.dialogWidth),
1113
1451
  timelineEnabled: value?.timelineEnabled ?? false,
1114
- gitBarEnabled: value?.gitBarEnabled ?? true,
1452
+ gitBarEnabled: value?.gitBarEnabled ?? false,
1453
+ archiveManagerEnabled: value?.archiveManagerEnabled ?? false,
1454
+ mcpManagerEnabled: value?.mcpManagerEnabled ?? false,
1115
1455
  };
1116
1456
  }
1117
1457
  /** Scale one theme px value proportionally to the chosen base size. */
@@ -1420,6 +1760,12 @@ function SettingsSection({ controller, t }) {
1420
1760
  const setGitBar = (value) => {
1421
1761
  void controller.set('gitBarEnabled', value).then(() => { setStatus('applied'); }).catch(() => { setStatus('unavailable'); });
1422
1762
  };
1763
+ const setArchiveManager = (value) => {
1764
+ void controller.set('archiveManagerEnabled', value).then(() => { setStatus('applied'); }).catch(() => { setStatus('unavailable'); });
1765
+ };
1766
+ const setMcpManager = (value) => {
1767
+ void controller.set('mcpManagerEnabled', value).then(() => { setStatus('applied'); }).catch(() => { setStatus('unavailable'); });
1768
+ };
1423
1769
  const reset = (field) => {
1424
1770
  void controller.unset(field).then(() => { setStatus('resetDone'); }).catch(() => { setStatus('unavailable'); });
1425
1771
  };
@@ -1431,12 +1777,44 @@ function SettingsSection({ controller, t }) {
1431
1777
  }
1432
1778
  return ((0, jsx_runtime_1.jsxs)("div", { className: "dut-settings", children: [(0, jsx_runtime_1.jsxs)("header", { className: "dut-settings-header", children: [(0, jsx_runtime_1.jsx)("div", { className: "dut-logo", children: "\uD83C\uDFA8" }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h2", { children: t('settingsTitle') }), (0, jsx_runtime_1.jsx)("p", { children: t('settingsIntro') })] })] }), !writable ? (0, jsx_runtime_1.jsx)("div", { className: "dut-alert warning", children: t('readOnly') }) : null, status === undefined ? null : (0, jsx_runtime_1.jsx)("div", { className: "dut-status", children: t(status) }), (0, jsx_runtime_1.jsxs)("section", { className: "dut-panel", children: [(0, jsx_runtime_1.jsx)("div", { className: "dut-section-label", children: t('sectionText') }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-field-top", children: [(0, jsx_runtime_1.jsx)("span", { children: t('fontSize') }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-controls", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-stepper", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": "\u2212", disabled: !writable || resolved.fontSize <= MIN_FONT_SIZE, onClick: () => { stepFontSize(-1); }, children: "\u2212" }), (0, jsx_runtime_1.jsx)("input", { type: "number", min: MIN_FONT_SIZE, max: MAX_FONT_SIZE, step: 1, value: draft, disabled: !writable, onChange: (event) => { setDraft(event.target.value); }, onBlur: (event) => { commitFontSize(event.target.value); }, onKeyDown: (event) => { if (event.key === 'Enter')
1433
1779
  commitFontSize(event.target.value); } }), (0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": "+", disabled: !writable || resolved.fontSize >= MAX_FONT_SIZE, onClick: () => { stepFontSize(1); }, children: "+" })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: 'dut-btn' + (resolved.fontSize === DEFAULT_FONT_SIZE ? ' dut-btn-active' : ''), disabled: !writable, onClick: () => { reset('fontSize'); }, children: t('defaultAction') })] })] }), (0, jsx_runtime_1.jsx)("small", { children: t('fontSizeHint') })] })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dut-panel", children: [(0, jsx_runtime_1.jsx)("div", { className: "dut-section-label", children: t('sectionContent') }), (0, jsx_runtime_1.jsx)("div", { className: "dut-field", children: (0, jsx_runtime_1.jsxs)("div", { className: "dut-field-top", children: [(0, jsx_runtime_1.jsx)("span", { children: t('tableStyle') }), (0, jsx_runtime_1.jsx)("div", { className: "dut-controls", children: (0, jsx_runtime_1.jsxs)("div", { className: "dut-seg", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: resolved.tableStyle === 'claude' ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { pickTableStyle('claude'); }, children: t('tableStyleClaude') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: resolved.tableStyle === 'default' ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { pickTableStyle('default'); }, children: t('tableStyleDefault') })] }) })] }) })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dut-panel", children: [(0, jsx_runtime_1.jsx)("div", { className: "dut-section-label", children: t('sectionLayout') }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-field-top", children: [(0, jsx_runtime_1.jsx)("span", { children: t('dialogWidth') }), (0, jsx_runtime_1.jsx)("div", { className: "dut-controls", children: (0, jsx_runtime_1.jsxs)("div", { className: "dut-stepper", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": "\u2212", disabled: !writable || resolved.dialogWidth <= MIN_DIALOG_WIDTH, onClick: () => { stepDialogWidth(-20); }, children: "\u2212" }), (0, jsx_runtime_1.jsx)("input", { type: "number", min: MIN_DIALOG_WIDTH, max: MAX_DIALOG_WIDTH, step: 20, value: widthDraft, disabled: !writable, onChange: (event) => { setWidthDraft(event.target.value); }, onBlur: (event) => { commitDialogWidth(event.target.value); }, onKeyDown: (event) => { if (event.key === 'Enter')
1434
- commitDialogWidth(event.target.value); } }), (0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": "+", disabled: !writable || resolved.dialogWidth >= MAX_DIALOG_WIDTH, onClick: () => { stepDialogWidth(20); }, children: "+" })] }) })] }), (0, jsx_runtime_1.jsx)("div", { className: "dut-presets", children: (0, jsx_runtime_1.jsxs)("div", { className: "dut-seg", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: resolved.dialogWidth === 880 ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { applyWidthPreset(880); }, children: [t('presetWide'), " \u00B7 880"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: resolved.dialogWidth === 1024 ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { applyWidthPreset(1024); }, children: [t('presetWideXl'), " \u00B7 1024"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: resolved.dialogWidth === 748 ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { applyWidthPreset(748); }, children: [t('presetDefault'), " \u00B7 748"] })] }) }), (0, jsx_runtime_1.jsx)("small", { children: t('dialogWidthHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-field-top", children: [(0, jsx_runtime_1.jsx)("span", { children: t('timeline') }), (0, jsx_runtime_1.jsx)("div", { className: "dut-controls", children: (0, jsx_runtime_1.jsxs)("div", { className: "dut-seg", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: resolved.timelineEnabled ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { setTimeline(true); }, children: t('timelineOn') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: !resolved.timelineEnabled ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { setTimeline(false); }, children: t('timelineDefault') })] }) })] }), (0, jsx_runtime_1.jsx)("small", { children: t('timelineHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-field-top", children: [(0, jsx_runtime_1.jsx)("span", { children: t('gitBar') }), (0, jsx_runtime_1.jsx)("div", { className: "dut-controls", children: (0, jsx_runtime_1.jsxs)("div", { className: "dut-seg", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: resolved.gitBarEnabled ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { setGitBar(true); }, children: t('gitBarOn') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: !resolved.gitBarEnabled ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { setGitBar(false); }, children: t('gitBarOff') })] }) })] }), (0, jsx_runtime_1.jsx)("small", { children: t('gitBarHint') })] })] })] }));
1780
+ commitDialogWidth(event.target.value); } }), (0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": "+", disabled: !writable || resolved.dialogWidth >= MAX_DIALOG_WIDTH, onClick: () => { stepDialogWidth(20); }, children: "+" })] }) })] }), (0, jsx_runtime_1.jsx)("div", { className: "dut-presets", children: (0, jsx_runtime_1.jsxs)("div", { className: "dut-seg", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: resolved.dialogWidth === 880 ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { applyWidthPreset(880); }, children: [t('presetWide'), " \u00B7 880"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: resolved.dialogWidth === 1024 ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { applyWidthPreset(1024); }, children: [t('presetWideXl'), " \u00B7 1024"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: resolved.dialogWidth === 748 ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { applyWidthPreset(748); }, children: [t('presetDefault'), " \u00B7 748"] })] }) }), (0, jsx_runtime_1.jsx)("small", { children: t('dialogWidthHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-field-top", children: [(0, jsx_runtime_1.jsx)("span", { children: t('timeline') }), (0, jsx_runtime_1.jsx)("div", { className: "dut-controls", children: (0, jsx_runtime_1.jsxs)("div", { className: "dut-seg", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: resolved.timelineEnabled ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { setTimeline(true); }, children: t('timelineOn') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: !resolved.timelineEnabled ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { setTimeline(false); }, children: t('timelineOff') })] }) })] }), (0, jsx_runtime_1.jsx)("small", { children: t('timelineHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-field-top", children: [(0, jsx_runtime_1.jsx)("span", { children: t('gitBar') }), (0, jsx_runtime_1.jsx)("div", { className: "dut-controls", children: (0, jsx_runtime_1.jsxs)("div", { className: "dut-seg", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: resolved.gitBarEnabled ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { setGitBar(true); }, children: t('gitBarOn') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: !resolved.gitBarEnabled ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { setGitBar(false); }, children: t('gitBarOff') })] }) })] }), (0, jsx_runtime_1.jsx)("small", { children: t('gitBarHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-field-top", children: [(0, jsx_runtime_1.jsx)("span", { children: t('archiveManager') }), (0, jsx_runtime_1.jsx)("div", { className: "dut-controls", children: (0, jsx_runtime_1.jsxs)("div", { className: "dut-seg", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: resolved.archiveManagerEnabled ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { setArchiveManager(true); }, children: t('archiveManagerOn') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: !resolved.archiveManagerEnabled ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { setArchiveManager(false); }, children: t('archiveManagerOff') })] }) })] }), (0, jsx_runtime_1.jsx)("small", { children: t('archiveManagerHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-field-top", children: [(0, jsx_runtime_1.jsx)("span", { children: t('mcpManager') }), (0, jsx_runtime_1.jsx)("div", { className: "dut-controls", children: (0, jsx_runtime_1.jsxs)("div", { className: "dut-seg", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: resolved.mcpManagerEnabled ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { setMcpManager(true); }, children: t('mcpManagerOn') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: !resolved.mcpManagerEnabled ? 'dut-seg-active' : '', disabled: !writable, onClick: () => { setMcpManager(false); }, children: t('mcpManagerOff') })] }) })] }), (0, jsx_runtime_1.jsx)("small", { children: t('mcpManagerHint') })] })] })] }));
1781
+ }
1782
+ /**
1783
+ * Register a Settings section only while its enable flag is on, so the
1784
+ * Settings nav row appears / disappears live with the toggle in the UI Tweaks
1785
+ * section. Waits for the `settings.section` declaration (like the static
1786
+ * inject), then keeps the section registered exactly while
1787
+ * `isEnabled(controller value)` holds: toggling the switch in the UI Tweaks
1788
+ * section mounts or disposes the section, and the settings shell's nav
1789
+ * (projected from the section ledger) updates in place.
1790
+ */
1791
+ function installConditionalSection(ctx, controller, isEnabled, registerSection) {
1792
+ ctx.slots.inject('settings.section', () => {
1793
+ let dispose;
1794
+ const sync = () => {
1795
+ const enabled = isEnabled(controller.getSnapshot().value);
1796
+ if (enabled && dispose === undefined) {
1797
+ dispose = registerSection();
1798
+ }
1799
+ else if (!enabled && dispose !== undefined) {
1800
+ dispose();
1801
+ dispose = undefined;
1802
+ }
1803
+ };
1804
+ sync();
1805
+ const unsubscribe = controller.subscribe(sync);
1806
+ return () => {
1807
+ unsubscribe();
1808
+ dispose?.();
1809
+ };
1810
+ });
1435
1811
  }
1436
1812
  function apply(ctx) {
1437
1813
  ctx.effect(installBaseStyles, 'dsh-ui-tweaks: base styles');
1438
1814
  ctx.effect(timeline_tsx_1.installTimelineStyles, 'dsh-ui-tweaks: timeline styles');
1439
1815
  ctx.effect(gitbar_tsx_1.installGitBarStyles, 'dsh-ui-tweaks: gitbar styles');
1816
+ ctx.effect(archive_tsx_1.installArchiveStyles, 'dsh-ui-tweaks: archive styles');
1817
+ ctx.effect(mcp_tsx_1.installMcpStyles, 'dsh-ui-tweaks: mcp styles');
1440
1818
  ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-ui-tweaks: locale');
1441
1819
  const t = ctx.locale.bind(NS);
1442
1820
  const controller = new SettingsClient();
@@ -1477,6 +1855,387 @@ function apply(ctx) {
1477
1855
  locale: NS,
1478
1856
  inject: () => ({ controller, sessionsService: ctx.sessions }),
1479
1857
  }, gitbar_tsx_1.GitBar));
1858
+ // Archive manager: a Settings section ("归档") that lists archived sessions
1859
+ // and can restore or permanently delete them. Registered only while the
1860
+ // archiveManagerEnabled toggle in the UI Tweaks section is on, so the nav
1861
+ // row appears / disappears live with the switch (the settings shell
1862
+ // projects its nav from the section ledger).
1863
+ installConditionalSection(ctx, controller, (value) => value?.archiveManagerEnabled === true, () => ctx.slots.register({
1864
+ name: 'settings.section',
1865
+ id: 'archive',
1866
+ order: 50,
1867
+ label: () => t('archiveNav'),
1868
+ locale: NS,
1869
+ inject: () => ({ controller, t, sessionsService: ctx.sessions }),
1870
+ }, archive_tsx_1.ArchiveSection));
1871
+ // MCP manager: a Settings section ("MCP 管理") that lists the configured MCP
1872
+ // servers with their status and tools, and restarts them. Registered only
1873
+ // while the mcpManagerEnabled toggle in the UI Tweaks section is on, so the
1874
+ // nav row appears / disappears live with the switch.
1875
+ installConditionalSection(ctx, controller, (value) => value?.mcpManagerEnabled === true, () => ctx.slots.register({
1876
+ name: 'settings.section',
1877
+ id: 'mcp',
1878
+ order: 60,
1879
+ label: () => t('mcpNav'),
1880
+ locale: NS,
1881
+ inject: () => ({ controller, t }),
1882
+ }, mcp_tsx_1.McpSection));
1883
+ }
1884
+ };
1885
+ __modules["./mcp.js"] = function(module, exports, require, __load_) {
1886
+ "use strict";
1887
+ Object.defineProperty(exports, "__esModule", { value: true });
1888
+ exports.MCP_CSS = void 0;
1889
+ exports.installMcpStyles = installMcpStyles;
1890
+ exports.McpSection = McpSection;
1891
+ const jsx_runtime_1 = require("react/jsx-runtime");
1892
+ /**
1893
+ * dsh-ui-tweaks — MCP manager (browser half).
1894
+ *
1895
+ * Renders the "MCP 管理" settings section (usable when the mcpManagerEnabled
1896
+ * toggle in 界面调整 is on — otherwise it shows an invite card). The section
1897
+ * lists every configured MCP server with its status (active / failed / loading
1898
+ * / stopped / disabled), command / url, env names, registered tools, and
1899
+ * offers per-server actions: **重启** (runtime reload), **启用/停用**,
1900
+ * **编辑**, and **删除** (two-click confirm). An **添加服务器** button opens
1901
+ * the editor, which supports BOTH a structured **表单** and a raw **YAML**
1902
+ * mode (the config is validated on the server before the profile's
1903
+ * `cordis.patch.yml` is rewritten; DSH's patch watcher then hot-reloads the
1904
+ * loader so the server starts/stops live).
1905
+ * @module dsh-ui-tweaks/client/mcp
1906
+ */
1907
+ const react_1 = require("react");
1908
+ const react_dom_1 = require("react-dom");
1909
+ /** Route matching the host half (src/mcp.ts). */
1910
+ const MCP_ROUTE = '/_dsh/ui-tweaks/mcp';
1911
+ async function mcpRequest(init) {
1912
+ const response = await fetch(MCP_ROUTE, { credentials: 'same-origin', ...init });
1913
+ let body;
1914
+ try {
1915
+ body = await response.json();
1916
+ }
1917
+ catch {
1918
+ body = undefined;
1919
+ }
1920
+ if (!response.ok || body === undefined || !body.ok) {
1921
+ const failure = body;
1922
+ const message = body === undefined
1923
+ ? `MCP route unavailable (HTTP ${response.status}; restart DSH to load the plugin server code)`
1924
+ : failure?.error?.message ?? `UI Tweaks MCP request failed with HTTP ${response.status}`;
1925
+ const error = new Error(message);
1926
+ error.code = failure?.error?.code;
1927
+ throw error;
1928
+ }
1929
+ return body.value;
1930
+ }
1931
+ /** Split a textarea into one trimmed entry per non-empty line. */
1932
+ function linesToArray(text) {
1933
+ return text.split(/\r?\n/u).map(line => line.trim()).filter(line => line.length > 0);
1934
+ }
1935
+ /** Parse `KEY=value` (or `Key: value`) lines into a record; throws on malformed lines. */
1936
+ function linesToRecord(text, separator) {
1937
+ const out = {};
1938
+ for (const line of text.split(/\r?\n/u)) {
1939
+ const trimmed = line.trim();
1940
+ if (trimmed.length === 0)
1941
+ continue;
1942
+ const at = trimmed.indexOf(separator);
1943
+ if (at <= 0)
1944
+ throw new Error(`格式不正确:${trimmed}`);
1945
+ const key = trimmed.slice(0, at).trim();
1946
+ const value = trimmed.slice(at + 1).trim();
1947
+ if (key.length === 0)
1948
+ throw new Error(`格式不正确:${trimmed}`);
1949
+ out[key] = value;
1950
+ }
1951
+ return out;
1952
+ }
1953
+ function recordToLines(record, separator) {
1954
+ return Object.entries(record).map(([key, value]) => `${key}${separator}${value}`).join('\n');
1955
+ }
1956
+ exports.MCP_CSS = `
1957
+ .dut-mcp{display:grid;gap:12px;max-width:680px;padding:6px 2px 36px}
1958
+ .dut-mcp-head{display:flex;align-items:center;gap:10px;padding:14px 16px 0}
1959
+ .dut-mcp-head h2{margin:0;font-size:15px;font-weight:600;letter-spacing:-.01em;color:var(--dsw-alias-label-primary)}
1960
+ .dut-mcp-count{font-size:11.5px;color:var(--dsw-alias-label-secondary);padding:2px 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1)}
1961
+ .dut-mcp-spacer{flex:1}
1962
+ .dut-mcp-btn{display:inline-flex;align-items:center;height:28px;padding:0 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}
1963
+ .dut-mcp-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
1964
+ .dut-mcp-btn:disabled{opacity:.5;cursor:default}
1965
+ .dut-mcp-btn.dut-mcp-del:hover{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}
1966
+ .dut-mcp-btn.dut-mcp-confirm{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);font-weight:600}
1967
+ .dut-mcp-body{display:grid;gap:2px;margin-top:4px}
1968
+ .dut-mcp-row{display:grid;gap:6px;padding:10px 16px;border-radius:12px}
1969
+ .dut-mcp-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
1970
+ .dut-mcp-row-main{display:flex;align-items:center;gap:8px;min-width:0}
1971
+ .dut-mcp-name{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
1972
+ .dut-mcp-badge{flex:none;font-size:11px;line-height:1;padding:4px 9px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary)}
1973
+ .dut-mcp-badge.dut-mcp-ok{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent);border-color:transparent;color:var(--dsw-alias-state-success-primary)}
1974
+ .dut-mcp-badge.dut-mcp-err{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent);border-color:transparent;color:var(--dsw-alias-state-error-primary)}
1975
+ .dut-mcp-badge.dut-mcp-warn{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 14%,transparent);border-color:transparent;color:var(--dsw-alias-state-warn-label)}
1976
+ .dut-mcp-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
1977
+ .dut-mcp-toggle{flex:none;font-size:11.5px;color:var(--dsw-alias-label-tertiary);background:transparent;border:none;padding:0;cursor:pointer;text-align:left}
1978
+ .dut-mcp-toggle:hover{color:var(--dsw-alias-label-primary)}
1979
+ .dut-mcp-tools{margin-left:2px;display:flex;flex-wrap:wrap;gap:4px}
1980
+ .dut-mcp-tool{font-size:11px;line-height:1;padding:3px 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary)}
1981
+ .dut-mcp-note{margin:2px 16px 0;padding:8px 12px;border-radius:10px;font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1)}
1982
+ .dut-mcp-empty{padding:32px 16px;text-align:center;font-size:12.5px;color:var(--dsw-alias-label-secondary)}
1983
+ .dut-mcp-alert{margin:8px 16px 0;padding:8px 12px;border-radius:10px;font-size:12px;line-height:1.5;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
1984
+ .dut-mcp-notice{margin:8px 16px 0;padding:8px 12px;border-radius:10px;font-size:12px;line-height:1.5;background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent);color:var(--dsw-alias-state-success-primary)}
1985
+ .dut-mcp-off{padding:20px 16px;display:grid;gap:12px}
1986
+ .dut-mcp-off p{margin:0;font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-secondary)}
1987
+ .dut-mcp-overlay{z-index:1100;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}
1988
+ .dut-mcp-mask{background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur);position:absolute;inset:0}
1989
+ .dut-mcp-panel{z-index:1;background:var(--dsw-alias-bg-layer-2);box-sizing:border-box;width:720px;max-width:calc(100vw - 48px);height:min(700px,100vh - 48px);box-shadow:var(--dsw-shadow-lv3);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:20px;display:flex;flex-direction:column;overflow:hidden}
1990
+ .dut-mcp-panel-head{box-sizing:border-box;flex:none;display:flex;align-items:center;gap:10px;height:52px;padding:0 12px 0 20px}
1991
+ .dut-mcp-panel-head h2{margin:0;font-size:16px;font-weight:600;letter-spacing:-.01em;color:var(--dsw-alias-label-primary)}
1992
+ .dut-mcp-panel-close{cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-primary);background:transparent;border:none;border-radius:28px;display:inline-flex;justify-content:center;align-items:center;padding:0;font-size:13px}
1993
+ .dut-mcp-panel-close:hover{background:var(--dsw-alias-interactive-bg-hover)}
1994
+ .dut-mcp-tabs{flex:none;display:inline-flex;padding:3px;gap:3px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-2);margin:0 20px 10px;align-self:flex-start}
1995
+ .dut-mcp-tabs button{border:none;border-radius:7px;padding:5px 14px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12.5px;cursor:pointer}
1996
+ .dut-mcp-tabs button.dut-mcp-tab-on{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);color:var(--dsw-alias-state-business-primary);font-weight:600}
1997
+ .dut-mcp-form{flex:1;min-height:0;overflow-y:auto;padding:0 20px 20px;display:grid;gap:12px;align-content:start}
1998
+ .dut-mcp-field{display:grid;gap:6px}
1999
+ .dut-mcp-field>span{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}
2000
+ .dut-mcp-field small{font-size:11px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
2001
+ .dut-mcp-input{box-sizing:border-box;width:100%;height:34px;padding:0 10px;border-radius:9px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}
2002
+ .dut-mcp-input:focus{outline:none;border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 55%,transparent)}
2003
+ .dut-mcp-input:disabled{opacity:.55;cursor:not-allowed}
2004
+ textarea.dut-mcp-input{height:auto;min-height:72px;padding:8px 10px;line-height:1.5;resize:vertical;font-family:var(--dsw-font-markdown-code-block-font-family, Consolas, monospace);font-size:12px}
2005
+ textarea.dut-mcp-input-lg{min-height:120px}
2006
+ textarea.dut-mcp-yaml{min-height:320px}
2007
+ .dut-mcp-row2{display:grid;grid-template-columns:1fr 1fr;gap:12px;align-items:start}
2008
+ .dut-mcp-field .dut-seg{width:fit-content}
2009
+ .dut-mcp-row2 .dut-seg{height:34px;box-sizing:border-box;align-items:center}
2010
+ .dut-mcp-row2 .dut-seg button{flex:none;white-space:nowrap;padding:0 16px;height:26px}
2011
+ .dut-mcp-foot{flex:none;display:flex;align-items:center;gap:8px;justify-content:flex-end;padding:12px 20px 16px;border-top:1px solid var(--dsw-alias-border-l1)}
2012
+ .dut-mcp-error{margin:0 20px 10px;padding:8px 12px;border-radius:10px;font-size:12px;line-height:1.5;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
2013
+ `;
2014
+ /** Install the MCP stylesheet once (idempotent); returns the disposer. */
2015
+ function installMcpStyles() {
2016
+ const id = 'dsh-ui-tweaks-mcp';
2017
+ const existing = document.querySelector(`style[data-plugin-css="${id}"]`);
2018
+ if (existing !== null)
2019
+ return () => { };
2020
+ const style = document.createElement('style');
2021
+ style.dataset.plugin = 'dsh-ui-tweaks';
2022
+ style.dataset.pluginCss = id;
2023
+ style.textContent = exports.MCP_CSS;
2024
+ document.head.appendChild(style);
2025
+ return () => { style.remove(); };
2026
+ }
2027
+ function statusLabel(status, t) {
2028
+ switch (status) {
2029
+ case 'active': return t('mcpStatusActive');
2030
+ case 'failed': return t('mcpStatusFailed');
2031
+ case 'loading': return t('mcpStatusLoading');
2032
+ case 'stopped': return t('mcpStatusStopped');
2033
+ case 'disabled': return t('mcpStatusDisabled');
2034
+ }
2035
+ }
2036
+ function statusClass(status) {
2037
+ switch (status) {
2038
+ case 'active': return ' dut-mcp-ok';
2039
+ case 'failed': return ' dut-mcp-err';
2040
+ case 'loading': return ' dut-mcp-warn';
2041
+ default: return '';
2042
+ }
2043
+ }
2044
+ function McpSection({ controller, t }) {
2045
+ const settingsState = (0, react_1.useSyncExternalStore)(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
2046
+ const enabled = settingsState.value?.mcpManagerEnabled === true;
2047
+ const [servers, setServers] = (0, react_1.useState)(null);
2048
+ const [busy, setBusy] = (0, react_1.useState)(null);
2049
+ const [confirmId, setConfirmId] = (0, react_1.useState)(null);
2050
+ const [error, setError] = (0, react_1.useState)(null);
2051
+ const [notice, setNotice] = (0, react_1.useState)(null);
2052
+ const [expanded, setExpanded] = (0, react_1.useState)(null);
2053
+ const [editing, setEditing] = (0, react_1.useState)(null);
2054
+ const generation = (0, react_1.useMemo)(() => ({ current: 0 }), []);
2055
+ const load = () => {
2056
+ const gen = ++generation.current;
2057
+ setError(null);
2058
+ void mcpRequest().then((snapshot) => {
2059
+ if (gen !== generation.current)
2060
+ return;
2061
+ setServers(snapshot.servers);
2062
+ }).catch((reason) => {
2063
+ if (gen !== generation.current)
2064
+ return;
2065
+ setError(reason instanceof Error ? reason.message : String(reason));
2066
+ setServers([]);
2067
+ });
2068
+ };
2069
+ (0, react_1.useEffect)(() => {
2070
+ if (!enabled)
2071
+ return;
2072
+ load();
2073
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2074
+ }, [enabled]);
2075
+ const run = (key, body, after) => {
2076
+ setBusy(key);
2077
+ setError(null);
2078
+ setNotice(null);
2079
+ void mcpRequest({
2080
+ method: 'POST',
2081
+ headers: { 'Content-Type': 'application/json' },
2082
+ body: JSON.stringify(body),
2083
+ }).then(() => { after?.(); }).catch((reason) => {
2084
+ setError(reason instanceof Error ? reason.message : String(reason));
2085
+ }).finally(() => { setBusy(current => (current === key ? null : current)); });
2086
+ };
2087
+ /** Re-fetch the list every `interval` ms until `stop` returns true (or attempts run out).
2088
+ * The DSH patch watcher applies changes asynchronously and an MCP server takes
2089
+ * time to spawn/connect, so a single delayed reload can miss the transition. */
2090
+ const pollAfter = (stop, attempts = 12, interval = 1000) => {
2091
+ let left = attempts;
2092
+ const tick = () => {
2093
+ const gen = ++generation.current;
2094
+ void mcpRequest().then((snapshot) => {
2095
+ if (gen !== generation.current)
2096
+ return;
2097
+ setServers(snapshot.servers);
2098
+ left -= 1;
2099
+ if (left <= 0 || stop(snapshot.servers))
2100
+ return;
2101
+ window.setTimeout(tick, interval);
2102
+ }).catch(() => {
2103
+ left -= 1;
2104
+ if (left > 0)
2105
+ window.setTimeout(tick, interval);
2106
+ });
2107
+ };
2108
+ tick();
2109
+ };
2110
+ const remove = (id) => {
2111
+ if (confirmId !== id) {
2112
+ setConfirmId(id);
2113
+ window.setTimeout(() => { setConfirmId(current => (current === id ? null : current)); }, 3000);
2114
+ return;
2115
+ }
2116
+ setConfirmId(null);
2117
+ run(`remove:${id}`, { action: 'remove', id }, () => {
2118
+ setNotice(t('mcpRemoved'));
2119
+ pollAfter(servers => !servers.some(server => server.id === id), 10);
2120
+ });
2121
+ };
2122
+ const setEnabled = (id, enabled) => {
2123
+ run(`set-enabled:${id}`, { action: 'set-enabled', id, enabled }, () => {
2124
+ // Keep refreshing until the target reaches a TERMINAL status. The
2125
+ // `disabled` flag flips within ~1s, but the MCP spawn + connect takes
2126
+ // longer — stopping at the flag would freeze the row mid-transition.
2127
+ pollAfter(servers => {
2128
+ const target = servers.find(server => server.id === id);
2129
+ if (target === undefined)
2130
+ return true;
2131
+ return enabled
2132
+ ? target.status === 'active' || target.status === 'failed'
2133
+ : target.status === 'disabled';
2134
+ }, 45, 1000);
2135
+ });
2136
+ };
2137
+ if (!enabled) {
2138
+ return ((0, jsx_runtime_1.jsx)("div", { className: "dut-mcp", children: (0, jsx_runtime_1.jsxs)("div", { className: "dut-panel dut-mcp-off", children: [(0, jsx_runtime_1.jsx)("p", { children: t('mcpDisabledHint') }), (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "dut-btn dut-btn-active", onClick: () => {
2139
+ void controller.set('mcpManagerEnabled', true).catch(() => { setError(t('unavailable')); });
2140
+ }, children: t('mcpEnable') }) })] }) }));
2141
+ }
2142
+ const list = servers ?? [];
2143
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-panel", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-head", children: [(0, jsx_runtime_1.jsx)("h2", { children: t('mcpTitle') }), (0, jsx_runtime_1.jsxs)("span", { className: "dut-mcp-count", children: [list.length, " \u00B7 ", list.filter(s => s.status === 'active').length, " ", t('mcpStatusActive')] }), (0, jsx_runtime_1.jsx)("span", { className: "dut-mcp-spacer" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "dut-mcp-btn", onClick: () => { setEditing('new'); }, children: t('mcpAdd') })] }), (0, jsx_runtime_1.jsx)("div", { className: "dut-mcp-note", children: t('mcpServerDetail') }), error !== null ? (0, jsx_runtime_1.jsx)("div", { className: "dut-mcp-alert", children: error }) : null, notice !== null ? (0, jsx_runtime_1.jsx)("div", { className: "dut-mcp-notice", children: notice }) : null, (0, jsx_runtime_1.jsx)("div", { className: "dut-mcp-body", children: servers === null ? ((0, jsx_runtime_1.jsx)("div", { className: "dut-mcp-empty", children: t('loading') })) : list.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "dut-mcp-empty", children: t('mcpEmpty') })) : list.map(server => {
2144
+ const command = server.url !== undefined ? server.url : [server.command, ...server.args].filter(Boolean).join(' ');
2145
+ const open = expanded === server.id;
2146
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-row-main", children: [(0, jsx_runtime_1.jsx)("span", { className: "dut-mcp-name", children: server.serverName }), (0, jsx_runtime_1.jsx)("span", { className: 'dut-mcp-badge' + statusClass(server.status), children: statusLabel(server.status, t) }), (0, jsx_runtime_1.jsxs)("span", { className: "dut-mcp-sub", children: ["\u00B7 ", server.toolCount, " ", t('mcpTools')] }), (0, jsx_runtime_1.jsx)("span", { className: "dut-mcp-spacer" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "dut-mcp-btn", disabled: busy !== null, onClick: () => { setEnabled(server.id, server.disabled); }, children: server.disabled ? t('mcpEnabledAction') : t('mcpDisabledAction') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "dut-mcp-btn", disabled: busy !== null, onClick: () => { setEditing(server); }, children: t('mcpEdit') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: 'dut-mcp-btn dut-mcp-del' + (confirmId === server.id ? ' dut-mcp-confirm' : ''), disabled: busy !== null, onClick: () => { remove(server.id); }, children: confirmId === server.id ? t('mcpDelete') + '?' : t('mcpDelete') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-sub", title: command, children: [server.id, " \u00B7 ", server.transport, " \u00B7 ", command] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "dut-mcp-toggle", onClick: () => { setExpanded(open ? null : server.id); }, children: [open ? '▾ ' : '▸ ', server.toolCount, " ", t('mcpTools'), server.env && Object.keys(server.env).length > 0 ? ` · ${t('mcpEnv')}: ${Object.keys(server.env).join(', ')}` : ''] }), open ? ((0, jsx_runtime_1.jsx)("div", { className: "dut-mcp-tools", children: server.tools.length === 0 ? (0, jsx_runtime_1.jsx)("span", { className: "dut-mcp-sub", children: "\u2014" }) : server.tools.map(tool => (0, jsx_runtime_1.jsx)("span", { className: "dut-mcp-tool", children: tool }, tool)) })) : null] }, server.id));
2147
+ }) })] }), editing !== null ? ((0, jsx_runtime_1.jsx)(McpEditor, { initial: editing === 'new' ? null : editing, t: t, onClose: () => { setEditing(null); }, onSaved: () => { setEditing(null); setNotice(t('mcpSaved')); pollAfter(() => false, 10); } }, editing === 'new' ? 'new' : editing.id)) : null] }));
2148
+ }
2149
+ function McpEditor({ initial, t, onClose, onSaved }) {
2150
+ const isEdit = initial !== null;
2151
+ const [mode, setMode] = (0, react_1.useState)('form');
2152
+ const [id, setId] = (0, react_1.useState)(initial?.id ?? '');
2153
+ const [serverName, setServerName] = (0, react_1.useState)(initial?.serverName ?? '');
2154
+ const [transport, setTransport] = (0, react_1.useState)(initial?.transport === 'streamable-http' ? 'streamable-http' : 'stdio');
2155
+ const [timeoutText, setTimeoutText] = (0, react_1.useState)(initial?.toolCallTimeoutMs !== undefined ? String(initial.toolCallTimeoutMs) : '');
2156
+ const [command, setCommand] = (0, react_1.useState)(initial?.command ?? '');
2157
+ const [argsText, setArgsText] = (0, react_1.useState)(initial ? initial.args.join('\n') : '');
2158
+ const [envText, setEnvText] = (0, react_1.useState)(initial ? recordToLines(initial.env, '=') : '');
2159
+ const [url, setUrl] = (0, react_1.useState)(initial?.url ?? '');
2160
+ const [headersText, setHeadersText] = (0, react_1.useState)(initial ? recordToLines(initial.headers, ':') : '');
2161
+ const [disabled, setDisabled] = (0, react_1.useState)(initial?.disabled ?? false);
2162
+ const [yamlText, setYamlText] = (0, react_1.useState)(initial?.yaml ?? '');
2163
+ const [error, setError] = (0, react_1.useState)(null);
2164
+ const [busy, setBusy] = (0, react_1.useState)(false);
2165
+ (0, react_1.useEffect)(() => {
2166
+ const onKeyDown = (event) => {
2167
+ if (event.key === 'Escape')
2168
+ onClose();
2169
+ };
2170
+ document.addEventListener('keydown', onKeyDown);
2171
+ return () => { document.removeEventListener('keydown', onKeyDown); };
2172
+ }, [onClose]);
2173
+ const validateForm = () => {
2174
+ if (!/^[A-Za-z0-9_-]{1,64}$/u.test(id))
2175
+ return t('mcpInvalidId');
2176
+ if (!/^[A-Za-z0-9_-]{1,32}$/u.test(serverName))
2177
+ return t('mcpInvalidName');
2178
+ if (timeoutText !== '' && (!/^\d+$/u.test(timeoutText) || Number(timeoutText) <= 0))
2179
+ return t('mcpInvalidTimeout');
2180
+ if (transport === 'streamable-http') {
2181
+ if (!/^https?:\/\//u.test(url))
2182
+ return t('mcpInvalidUrl');
2183
+ }
2184
+ else if (command.trim().length === 0) {
2185
+ return t('mcpInvalidCommand');
2186
+ }
2187
+ return null;
2188
+ };
2189
+ const save = () => {
2190
+ setError(null);
2191
+ setBusy(true);
2192
+ let payload;
2193
+ if (mode === 'yaml') {
2194
+ payload = { action: 'save', server: { id, disabled }, yaml: yamlText };
2195
+ }
2196
+ else {
2197
+ const invalid = validateForm();
2198
+ if (invalid !== null) {
2199
+ setError(invalid);
2200
+ setBusy(false);
2201
+ return;
2202
+ }
2203
+ let env = {};
2204
+ let headers = {};
2205
+ try {
2206
+ env = linesToRecord(envText, '=');
2207
+ headers = linesToRecord(headersText, ':');
2208
+ }
2209
+ catch (parseError) {
2210
+ setError(parseError instanceof Error ? parseError.message : String(parseError));
2211
+ setBusy(false);
2212
+ return;
2213
+ }
2214
+ payload = {
2215
+ action: 'save',
2216
+ server: {
2217
+ id,
2218
+ serverName,
2219
+ transport,
2220
+ command,
2221
+ args: linesToArray(argsText),
2222
+ env,
2223
+ url,
2224
+ headers,
2225
+ ...(timeoutText !== '' ? { toolCallTimeoutMs: Number(timeoutText) } : {}),
2226
+ disabled,
2227
+ },
2228
+ };
2229
+ }
2230
+ void mcpRequest({
2231
+ method: 'POST',
2232
+ headers: { 'Content-Type': 'application/json' },
2233
+ body: JSON.stringify(payload),
2234
+ }).then(() => { onSaved(); }).catch((reason) => {
2235
+ setError(reason instanceof Error ? reason.message : String(reason));
2236
+ }).finally(() => { setBusy(false); });
2237
+ };
2238
+ return (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-overlay", role: "presentation", children: [(0, jsx_runtime_1.jsx)("div", { className: "dut-mcp-mask", "aria-hidden": "true", onClick: onClose }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-panel", role: "dialog", "aria-modal": "true", "aria-label": isEdit ? t('mcpEditTitle') : t('mcpAddTitle'), children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-panel-head", children: [(0, jsx_runtime_1.jsx)("h2", { children: isEdit ? t('mcpEditTitle') : t('mcpAddTitle') }), (0, jsx_runtime_1.jsx)("span", { className: "dut-mcp-spacer" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "dut-mcp-panel-close", onClick: onClose, "aria-label": "\u2715", children: "\u2715" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-tabs", role: "tablist", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", role: "tab", "aria-selected": mode === 'form', className: mode === 'form' ? 'dut-mcp-tab-on' : '', onClick: () => { setMode('form'); }, children: t('mcpFormTab') }), (0, jsx_runtime_1.jsx)("button", { type: "button", role: "tab", "aria-selected": mode === 'yaml', className: mode === 'yaml' ? 'dut-mcp-tab-on' : '', onClick: () => { setMode('yaml'); }, children: t('mcpYamlTab') })] }), error !== null ? (0, jsx_runtime_1.jsx)("div", { className: "dut-mcp-error", children: error }) : null, mode === 'yaml' ? ((0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-form", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-row2", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-field", children: [(0, jsx_runtime_1.jsx)("span", { children: t('mcpFieldId') }), (0, jsx_runtime_1.jsx)("input", { className: "dut-mcp-input", value: id, disabled: isEdit, onChange: event => { setId(event.target.value); } }), (0, jsx_runtime_1.jsx)("small", { children: t('mcpFieldIdHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-field", children: [(0, jsx_runtime_1.jsx)("span", { children: t('mcpFieldEnabled') }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-seg", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: !disabled ? 'dut-seg-active' : '', onClick: () => { setDisabled(false); }, children: t('mcpEnabledAction') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: disabled ? 'dut-seg-active' : '', onClick: () => { setDisabled(true); }, children: t('mcpDisabledAction') })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-field", children: [(0, jsx_runtime_1.jsx)("span", { children: t('mcpYamlTab') }), (0, jsx_runtime_1.jsx)("textarea", { className: "dut-mcp-input dut-mcp-yaml", value: yamlText, placeholder: t('mcpYamlPlaceholder'), spellCheck: false, onChange: event => { setYamlText(event.target.value); } }), (0, jsx_runtime_1.jsx)("small", { children: t('mcpYamlHint') })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-form", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-row2", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-field", children: [(0, jsx_runtime_1.jsx)("span", { children: t('mcpFieldId') }), (0, jsx_runtime_1.jsx)("input", { className: "dut-mcp-input", value: id, disabled: isEdit, onChange: event => { setId(event.target.value); } }), (0, jsx_runtime_1.jsx)("small", { children: t('mcpFieldIdHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-field", children: [(0, jsx_runtime_1.jsx)("span", { children: t('mcpFieldEnabled') }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-seg", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: !disabled ? 'dut-seg-active' : '', onClick: () => { setDisabled(false); }, children: t('mcpEnabledAction') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: disabled ? 'dut-seg-active' : '', onClick: () => { setDisabled(true); }, children: t('mcpDisabledAction') })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-row2", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-field", children: [(0, jsx_runtime_1.jsx)("span", { children: t('mcpFieldName') }), (0, jsx_runtime_1.jsx)("input", { className: "dut-mcp-input", value: serverName, onChange: event => { setServerName(event.target.value); } }), (0, jsx_runtime_1.jsx)("small", { children: t('mcpFieldNameHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-field", children: [(0, jsx_runtime_1.jsx)("span", { children: t('mcpFieldType') }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-seg", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: transport === 'stdio' ? 'dut-seg-active' : '', onClick: () => { setTransport('stdio'); }, children: "stdio" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: transport === 'streamable-http' ? 'dut-seg-active' : '', onClick: () => { setTransport('streamable-http'); }, children: "HTTP" })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-field", children: [(0, jsx_runtime_1.jsx)("span", { children: t('mcpFieldTimeout') }), (0, jsx_runtime_1.jsx)("input", { className: "dut-mcp-input", inputMode: "numeric", value: timeoutText, placeholder: "60000", onChange: event => { setTimeoutText(event.target.value); } })] }), transport === 'stdio' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-field", children: [(0, jsx_runtime_1.jsx)("span", { children: t('mcpFieldCommand') }), (0, jsx_runtime_1.jsx)("input", { className: "dut-mcp-input", value: command, placeholder: t('mcpFieldCommandPlaceholder'), onChange: event => { setCommand(event.target.value); } })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-field", children: [(0, jsx_runtime_1.jsx)("span", { children: t('mcpFieldArgs') }), (0, jsx_runtime_1.jsx)("textarea", { className: "dut-mcp-input", value: argsText, placeholder: '-y\n@some/mcp-server', spellCheck: false, onChange: event => { setArgsText(event.target.value); } })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-field", children: [(0, jsx_runtime_1.jsx)("span", { children: t('mcpFieldEnv') }), (0, jsx_runtime_1.jsx)("textarea", { className: "dut-mcp-input dut-mcp-input-lg", value: envText, placeholder: 'KEY=value', spellCheck: false, onChange: event => { setEnvText(event.target.value); } })] })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-field", children: [(0, jsx_runtime_1.jsx)("span", { children: t('mcpFieldUrl') }), (0, jsx_runtime_1.jsx)("input", { className: "dut-mcp-input", value: url, placeholder: "https://mcp.example.com/sse", onChange: event => { setUrl(event.target.value); } })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-field", children: [(0, jsx_runtime_1.jsx)("span", { children: t('mcpFieldHeaders') }), (0, jsx_runtime_1.jsx)("textarea", { className: "dut-mcp-input dut-mcp-input-lg", value: headersText, placeholder: 'Authorization: Bearer …', spellCheck: false, onChange: event => { setHeadersText(event.target.value); } })] })] }))] })), (0, jsx_runtime_1.jsxs)("div", { className: "dut-mcp-foot", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "dut-mcp-btn", disabled: busy, onClick: onClose, children: t('mcpCancel') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "dut-btn dut-btn-active", disabled: busy, onClick: save, children: t('mcpSave') })] })] })] }), document.body);
1480
2239
  }
1481
2240
  };
1482
2241
  __modules["./timeline.js"] = function(module, exports, require, __load_) {