@leeoohoo/ui-apps-devkit 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +61 -62
  2. package/bin/chatos-uiapp.js +3 -4
  3. package/package.json +23 -23
  4. package/src/cli.js +53 -53
  5. package/src/commands/dev.js +14 -14
  6. package/src/commands/init.js +129 -129
  7. package/src/commands/install.js +45 -45
  8. package/src/commands/pack.js +72 -72
  9. package/src/commands/validate.js +90 -138
  10. package/src/lib/args.js +63 -49
  11. package/src/lib/config.js +29 -29
  12. package/src/lib/fs.js +78 -78
  13. package/src/lib/path-boundary.js +16 -16
  14. package/src/lib/plugin.js +45 -45
  15. package/src/lib/template.js +172 -172
  16. package/src/sandbox/server.js +1204 -1028
  17. package/templates/basic/README.md +63 -65
  18. package/templates/basic/chatos.config.json +5 -5
  19. package/templates/basic/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +209 -211
  20. package/templates/basic/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +73 -73
  21. package/templates/basic/docs/CHATOS_UI_APPS_HOST_API.md +136 -136
  22. package/templates/basic/docs/CHATOS_UI_APPS_OVERVIEW.md +106 -106
  23. package/templates/basic/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +239 -239
  24. package/templates/basic/docs/CHATOS_UI_APPS_STYLE_GUIDE.md +95 -95
  25. package/templates/basic/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +40 -40
  26. package/templates/basic/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -392
  27. package/templates/basic/plugin/apps/app/compact.mjs +41 -41
  28. package/templates/basic/plugin/apps/app/index.mjs +287 -287
  29. package/templates/basic/plugin/apps/app/mcp-prompt.en.md +7 -7
  30. package/templates/basic/plugin/apps/app/mcp-prompt.zh.md +7 -7
  31. package/templates/basic/plugin/apps/app/mcp-server.mjs +15 -15
  32. package/templates/basic/plugin/backend/index.mjs +37 -37
  33. package/templates/basic/template.json +7 -7
  34. package/templates/notepad/README.md +38 -44
  35. package/templates/notepad/chatos.config.json +4 -4
  36. package/templates/notepad/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +209 -211
  37. package/templates/notepad/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +73 -73
  38. package/templates/notepad/docs/CHATOS_UI_APPS_HOST_API.md +136 -136
  39. package/templates/notepad/docs/CHATOS_UI_APPS_OVERVIEW.md +106 -106
  40. package/templates/notepad/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +239 -239
  41. package/templates/notepad/docs/CHATOS_UI_APPS_STYLE_GUIDE.md +95 -95
  42. package/templates/notepad/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +40 -40
  43. package/templates/notepad/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -392
  44. package/templates/notepad/plugin/apps/app/api.mjs +30 -30
  45. package/templates/notepad/plugin/apps/app/compact.mjs +41 -41
  46. package/templates/notepad/plugin/apps/app/dom.mjs +14 -14
  47. package/templates/notepad/plugin/apps/app/ds-tree.mjs +35 -35
  48. package/templates/notepad/plugin/apps/app/index.mjs +1056 -1056
  49. package/templates/notepad/plugin/apps/app/layers.mjs +338 -338
  50. package/templates/notepad/plugin/apps/app/markdown.mjs +120 -120
  51. package/templates/notepad/plugin/apps/app/mcp-prompt.en.md +22 -22
  52. package/templates/notepad/plugin/apps/app/mcp-prompt.zh.md +22 -22
  53. package/templates/notepad/plugin/apps/app/mcp-server.mjs +199 -199
  54. package/templates/notepad/plugin/apps/app/styles.mjs +355 -355
  55. package/templates/notepad/plugin/apps/app/tags.mjs +21 -21
  56. package/templates/notepad/plugin/apps/app/ui.mjs +280 -280
  57. package/templates/notepad/plugin/backend/index.mjs +99 -99
  58. package/templates/notepad/plugin/plugin.json +23 -23
  59. package/templates/notepad/plugin/shared/notepad-paths.mjs +39 -39
  60. package/templates/notepad/plugin/shared/notepad-store.mjs +765 -765
  61. package/templates/notepad/template.json +8 -8
@@ -1,4 +1,4 @@
1
- import fs from 'fs';
1
+ import fs from 'fs';
2
2
  import http from 'http';
3
3
  import path from 'path';
4
4
  import url from 'url';
@@ -13,29 +13,29 @@ import { copyDir, ensureDir, isDirectory, isFile } from '../lib/fs.js';
13
13
  import { loadPluginManifest, pickAppFromManifest } from '../lib/plugin.js';
14
14
  import { resolveInsideDir } from '../lib/path-boundary.js';
15
15
  import { COMPAT_STATE_ROOT_DIRNAME, STATE_ROOT_DIRNAME } from '../lib/state-constants.js';
16
-
17
- const __filename = url.fileURLToPath(import.meta.url);
16
+
17
+ const __filename = url.fileURLToPath(import.meta.url);
18
18
  const __dirname = path.dirname(__filename);
19
19
 
20
20
  const TOKEN_REGEX = /--ds-[a-z0-9-]+/gi;
21
21
  const SANDBOX_STATE_DIRNAME = STATE_ROOT_DIRNAME;
22
22
  const SANDBOX_COMPAT_DIRNAME = COMPAT_STATE_ROOT_DIRNAME;
23
- const GLOBAL_STYLES_CANDIDATES = [
24
- path.resolve(__dirname, '..', '..', '..', 'common', 'aide-ui', 'components', 'GlobalStyles.jsx'),
25
- path.resolve(process.cwd(), 'common', 'aide-ui', 'components', 'GlobalStyles.jsx'),
26
- ];
27
-
23
+ const GLOBAL_STYLES_CANDIDATES = [
24
+ path.resolve(__dirname, '..', '..', '..', 'common', 'aide-ui', 'components', 'GlobalStyles.jsx'),
25
+ path.resolve(process.cwd(), 'common', 'aide-ui', 'components', 'GlobalStyles.jsx'),
26
+ ];
27
+
28
28
  function loadTokenNames() {
29
29
  for (const candidate of GLOBAL_STYLES_CANDIDATES) {
30
30
  try {
31
31
  if (!isFile(candidate)) continue;
32
32
  const raw = fs.readFileSync(candidate, 'utf8');
33
- const matches = raw.match(TOKEN_REGEX) || [];
34
- const names = Array.from(new Set(matches.map((v) => v.toLowerCase())));
35
- if (names.length > 0) return names.sort();
36
- } catch {
37
- // ignore
38
- }
33
+ const matches = raw.match(TOKEN_REGEX) || [];
34
+ const names = Array.from(new Set(matches.map((v) => v.toLowerCase())));
35
+ if (names.length > 0) return names.sort();
36
+ } catch {
37
+ // ignore
38
+ }
39
39
  }
40
40
  return [];
41
41
  }
@@ -126,7 +126,7 @@ function expandCallMetaValue(value, vars) {
126
126
  return value;
127
127
  }
128
128
 
129
- function buildSandboxCallMeta({ rawCallMeta, rawWorkdir, context } = {}) {
129
+ function buildSandboxCallMeta({ rawCallMeta, context } = {}) {
130
130
  const ctx = context && typeof context === 'object' ? context : null;
131
131
  const defaults = ctx
132
132
  ? {
@@ -158,32 +158,22 @@ function buildSandboxCallMeta({ rawCallMeta, rawWorkdir, context } = {}) {
158
158
  }
159
159
  : {};
160
160
  const expanded = raw ? expandCallMetaValue(raw, vars) : null;
161
- let merged = mergeCallMeta(defaults, expanded);
162
- const workdirRaw = normalizeText(rawWorkdir);
163
- if (workdirRaw) {
164
- const expandedWorkdir = expandCallMetaValue(workdirRaw, vars);
165
- const workdirValue = typeof expandedWorkdir === 'string' ? expandedWorkdir.trim() : '';
166
- if (workdirValue) {
167
- merged = mergeCallMeta(merged, { workdir: workdirValue });
168
- }
169
- }
170
- return merged;
161
+ return mergeCallMeta(defaults, expanded);
171
162
  }
172
163
 
173
164
  function loadSandboxLlmConfig(filePath) {
174
- if (!filePath) return { apiKey: '', baseUrl: '', modelId: '', workdir: '' };
165
+ if (!filePath) return { apiKey: '', baseUrl: '', modelId: '' };
175
166
  try {
176
- if (!fs.existsSync(filePath)) return { apiKey: '', baseUrl: '', modelId: '', workdir: '' };
167
+ if (!fs.existsSync(filePath)) return { apiKey: '', baseUrl: '', modelId: '' };
177
168
  const raw = fs.readFileSync(filePath, 'utf8');
178
169
  const parsed = raw ? JSON.parse(raw) : {};
179
170
  return {
180
171
  apiKey: normalizeText(parsed?.apiKey),
181
172
  baseUrl: normalizeText(parsed?.baseUrl),
182
173
  modelId: normalizeText(parsed?.modelId),
183
- workdir: normalizeText(parsed?.workdir),
184
174
  };
185
175
  } catch {
186
- return { apiKey: '', baseUrl: '', modelId: '', workdir: '' };
176
+ return { apiKey: '', baseUrl: '', modelId: '' };
187
177
  }
188
178
  }
189
179
 
@@ -442,15 +432,15 @@ async function callOpenAiChat({ apiKey, baseUrl, model, messages, tools, signal
442
432
  return await res.json();
443
433
  }
444
434
 
445
- function sendJson(res, status, obj) {
446
- const raw = JSON.stringify(obj);
447
- res.writeHead(status, {
448
- 'content-type': 'application/json; charset=utf-8',
449
- 'cache-control': 'no-store',
450
- });
451
- res.end(raw);
452
- }
453
-
435
+ function sendJson(res, status, obj) {
436
+ const raw = JSON.stringify(obj);
437
+ res.writeHead(status, {
438
+ 'content-type': 'application/json; charset=utf-8',
439
+ 'cache-control': 'no-store',
440
+ });
441
+ res.end(raw);
442
+ }
443
+
454
444
  function sendText(res, status, text, contentType) {
455
445
  res.writeHead(status, {
456
446
  'content-type': contentType || 'text/plain; charset=utf-8',
@@ -478,226 +468,226 @@ function readJsonBody(req) {
478
468
 
479
469
  function guessContentType(filePath) {
480
470
  const ext = path.extname(filePath).toLowerCase();
481
- if (ext === '.html') return 'text/html; charset=utf-8';
482
- if (ext === '.css') return 'text/css; charset=utf-8';
483
- if (ext === '.mjs' || ext === '.js') return 'text/javascript; charset=utf-8';
484
- if (ext === '.json') return 'application/json; charset=utf-8';
485
- if (ext === '.md') return 'text/markdown; charset=utf-8';
486
- if (ext === '.svg') return 'image/svg+xml';
487
- if (ext === '.png') return 'image/png';
488
- return 'application/octet-stream';
489
- }
490
-
491
- function serveStaticFile(res, filePath) {
492
- if (!isFile(filePath)) return false;
493
- const ct = guessContentType(filePath);
494
- const buf = fs.readFileSync(filePath);
495
- res.writeHead(200, { 'content-type': ct, 'cache-control': 'no-store' });
496
- res.end(buf);
497
- return true;
498
- }
499
-
500
- function startRecursiveWatcher(rootDir, onChange) {
501
- const root = path.resolve(rootDir);
502
- if (!isDirectory(root)) return () => {};
503
-
504
- const watchers = new Map();
505
-
506
- const shouldIgnore = (p) => {
507
- const base = path.basename(p);
508
- if (!base) return false;
509
- if (base === 'node_modules') return true;
510
- if (base === '.git') return true;
511
- if (base === '.DS_Store') return true;
512
- return false;
513
- };
514
-
515
- const scan = (dir) => {
516
- const abs = path.resolve(dir);
517
- if (!isDirectory(abs)) return;
518
- if (shouldIgnore(abs)) return;
519
- if (!watchers.has(abs)) {
520
- try {
521
- const w = fs.watch(abs, (eventType, filename) => {
522
- const relName = filename ? String(filename) : '';
523
- const filePath = relName ? path.join(abs, relName) : abs;
524
- try {
525
- onChange({ eventType, filePath });
526
- } catch {
527
- // ignore
528
- }
529
- scheduleRescan();
530
- });
531
- watchers.set(abs, w);
532
- } catch {
533
- // ignore
534
- }
535
- }
536
-
537
- let entries = [];
538
- try {
539
- entries = fs.readdirSync(abs, { withFileTypes: true });
540
- } catch {
541
- return;
542
- }
543
- for (const ent of entries) {
544
- if (!ent?.isDirectory?.()) continue;
545
- const child = path.join(abs, ent.name);
546
- if (shouldIgnore(child)) continue;
547
- scan(child);
548
- }
549
- };
550
-
551
- let rescanTimer = null;
552
- const scheduleRescan = () => {
553
- if (rescanTimer) return;
554
- rescanTimer = setTimeout(() => {
555
- rescanTimer = null;
556
- scan(root);
557
- }, 250);
558
- };
559
-
560
- scan(root);
561
-
562
- return () => {
563
- if (rescanTimer) {
564
- try {
565
- clearTimeout(rescanTimer);
566
- } catch {
567
- // ignore
568
- }
569
- rescanTimer = null;
570
- }
571
- for (const w of watchers.values()) {
572
- try {
573
- w.close();
574
- } catch {
575
- // ignore
576
- }
577
- }
578
- watchers.clear();
579
- };
580
- }
581
-
582
- function htmlPage() {
583
- return `<!doctype html>
584
- <html lang="zh-CN">
585
- <head>
586
- <meta charset="UTF-8" />
587
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
588
- <title>ChatOS UI Apps Sandbox</title>
589
- <style>
590
- :root {
591
- color-scheme: light;
592
- --ds-accent: #00d4ff;
593
- --ds-accent-2: #7c3aed;
594
- --ds-panel-bg: rgba(255, 255, 255, 0.86);
595
- --ds-panel-border: rgba(15, 23, 42, 0.08);
596
- --ds-subtle-bg: rgba(255, 255, 255, 0.62);
597
- --ds-selected-bg: linear-gradient(90deg, rgba(0, 212, 255, 0.14), rgba(124, 58, 237, 0.08));
598
- --ds-focus-ring: rgba(0, 212, 255, 0.32);
599
- --ds-nav-hover-bg: rgba(15, 23, 42, 0.06);
600
- --ds-code-bg: #f7f9fb;
601
- --ds-code-border: #eef2f7;
602
- --sandbox-bg: #f5f7fb;
603
- --sandbox-text: #111;
604
- }
605
- :root[data-theme='dark'] {
606
- color-scheme: dark;
607
- --ds-accent: #00d4ff;
608
- --ds-accent-2: #a855f7;
609
- --ds-panel-bg: rgba(17, 19, 28, 0.82);
610
- --ds-panel-border: rgba(255, 255, 255, 0.14);
611
- --ds-subtle-bg: rgba(255, 255, 255, 0.04);
612
- --ds-selected-bg: linear-gradient(90deg, rgba(0, 212, 255, 0.18), rgba(168, 85, 247, 0.14));
613
- --ds-focus-ring: rgba(0, 212, 255, 0.5);
614
- --ds-nav-hover-bg: rgba(255, 255, 255, 0.08);
615
- --ds-code-bg: #0d1117;
616
- --ds-code-border: #30363d;
617
- --sandbox-bg: #0f1115;
618
- --sandbox-text: #eee;
619
- }
620
- body {
621
- margin:0;
622
- font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
623
- background: var(--sandbox-bg);
624
- color: var(--sandbox-text);
625
- }
626
- #appRoot { height: 100vh; display:flex; flex-direction:column; }
627
- #sandboxToolbar {
628
- flex: 0 0 auto;
629
- border-bottom: 1px solid var(--ds-panel-border);
630
- padding: 10px 12px;
631
- background: var(--ds-panel-bg);
632
- }
633
- #headerSlot {
634
- flex: 0 0 auto;
635
- border-bottom: 1px solid var(--ds-panel-border);
636
- padding: 10px 12px;
637
- background: var(--ds-panel-bg);
638
- }
639
- #container { flex: 1 1 auto; min-height:0; overflow:hidden; }
640
- #containerInner { height:100%; overflow:auto; }
641
- .muted { opacity: 0.7; font-size: 12px; }
642
- .bar { display:flex; gap:10px; align-items:center; justify-content:space-between; }
643
- .btn {
644
- border:1px solid var(--ds-panel-border);
645
- background: var(--ds-subtle-bg);
646
- padding:6px 10px;
647
- border-radius:10px;
648
- cursor:pointer;
649
- font-weight:650;
650
- color: inherit;
651
- }
652
- .btn[data-active='1'] {
653
- background: var(--ds-selected-bg);
654
- box-shadow: 0 0 0 2px var(--ds-focus-ring);
655
- }
656
- .btn:active { transform: translateY(1px); }
657
- #promptsPanel {
658
- position: fixed;
659
- right: 12px;
660
- bottom: 12px;
661
- width: 420px;
662
- max-height: 70vh;
663
- display:none;
664
- flex-direction:column;
665
- background: var(--ds-panel-bg);
666
- color: inherit;
667
- border:1px solid var(--ds-panel-border);
668
- border-radius:14px;
669
- overflow:hidden;
670
- box-shadow: 0 18px 60px rgba(0,0,0,0.18);
671
- }
672
- #promptsPanelHeader { padding: 10px 12px; display:flex; align-items:center; justify-content:space-between; border-bottom: 1px solid var(--ds-panel-border); }
673
- #promptsPanelBody { padding: 10px 12px; overflow:auto; display:flex; flex-direction:column; gap:10px; }
674
- #promptsFab { position: fixed; right: 16px; bottom: 16px; width: 44px; height: 44px; border-radius: 999px; display:flex; align-items:center; justify-content:center; }
675
- .card { border: 1px solid var(--ds-panel-border); border-radius: 12px; padding: 10px; background: var(--ds-panel-bg); }
676
- .row { display:flex; gap:10px; }
677
- .toolbar-group { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
678
- .segmented { display:flex; gap:6px; align-items:center; }
679
- #sandboxInspector {
680
- position: fixed;
681
- right: 12px;
682
- top: 72px;
683
- width: 360px;
684
- max-height: 70vh;
685
- display: none;
686
- flex-direction: column;
687
- background: var(--ds-panel-bg);
688
- border: 1px solid var(--ds-panel-border);
689
- border-radius: 12px;
690
- overflow: hidden;
691
- box-shadow: 0 14px 40px rgba(0,0,0,0.16);
692
- z-index: 10;
693
- }
694
- #sandboxInspectorHeader {
695
- padding: 10px 12px;
696
- display:flex;
697
- align-items:center;
698
- justify-content: space-between;
699
- border-bottom: 1px solid var(--ds-panel-border);
700
- }
471
+ if (ext === '.html') return 'text/html; charset=utf-8';
472
+ if (ext === '.css') return 'text/css; charset=utf-8';
473
+ if (ext === '.mjs' || ext === '.js') return 'text/javascript; charset=utf-8';
474
+ if (ext === '.json') return 'application/json; charset=utf-8';
475
+ if (ext === '.md') return 'text/markdown; charset=utf-8';
476
+ if (ext === '.svg') return 'image/svg+xml';
477
+ if (ext === '.png') return 'image/png';
478
+ return 'application/octet-stream';
479
+ }
480
+
481
+ function serveStaticFile(res, filePath) {
482
+ if (!isFile(filePath)) return false;
483
+ const ct = guessContentType(filePath);
484
+ const buf = fs.readFileSync(filePath);
485
+ res.writeHead(200, { 'content-type': ct, 'cache-control': 'no-store' });
486
+ res.end(buf);
487
+ return true;
488
+ }
489
+
490
+ function startRecursiveWatcher(rootDir, onChange) {
491
+ const root = path.resolve(rootDir);
492
+ if (!isDirectory(root)) return () => {};
493
+
494
+ const watchers = new Map();
495
+
496
+ const shouldIgnore = (p) => {
497
+ const base = path.basename(p);
498
+ if (!base) return false;
499
+ if (base === 'node_modules') return true;
500
+ if (base === '.git') return true;
501
+ if (base === '.DS_Store') return true;
502
+ return false;
503
+ };
504
+
505
+ const scan = (dir) => {
506
+ const abs = path.resolve(dir);
507
+ if (!isDirectory(abs)) return;
508
+ if (shouldIgnore(abs)) return;
509
+ if (!watchers.has(abs)) {
510
+ try {
511
+ const w = fs.watch(abs, (eventType, filename) => {
512
+ const relName = filename ? String(filename) : '';
513
+ const filePath = relName ? path.join(abs, relName) : abs;
514
+ try {
515
+ onChange({ eventType, filePath });
516
+ } catch {
517
+ // ignore
518
+ }
519
+ scheduleRescan();
520
+ });
521
+ watchers.set(abs, w);
522
+ } catch {
523
+ // ignore
524
+ }
525
+ }
526
+
527
+ let entries = [];
528
+ try {
529
+ entries = fs.readdirSync(abs, { withFileTypes: true });
530
+ } catch {
531
+ return;
532
+ }
533
+ for (const ent of entries) {
534
+ if (!ent?.isDirectory?.()) continue;
535
+ const child = path.join(abs, ent.name);
536
+ if (shouldIgnore(child)) continue;
537
+ scan(child);
538
+ }
539
+ };
540
+
541
+ let rescanTimer = null;
542
+ const scheduleRescan = () => {
543
+ if (rescanTimer) return;
544
+ rescanTimer = setTimeout(() => {
545
+ rescanTimer = null;
546
+ scan(root);
547
+ }, 250);
548
+ };
549
+
550
+ scan(root);
551
+
552
+ return () => {
553
+ if (rescanTimer) {
554
+ try {
555
+ clearTimeout(rescanTimer);
556
+ } catch {
557
+ // ignore
558
+ }
559
+ rescanTimer = null;
560
+ }
561
+ for (const w of watchers.values()) {
562
+ try {
563
+ w.close();
564
+ } catch {
565
+ // ignore
566
+ }
567
+ }
568
+ watchers.clear();
569
+ };
570
+ }
571
+
572
+ function htmlPage() {
573
+ return `<!doctype html>
574
+ <html lang="zh-CN">
575
+ <head>
576
+ <meta charset="UTF-8" />
577
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
578
+ <title>ChatOS UI Apps Sandbox</title>
579
+ <style>
580
+ :root {
581
+ color-scheme: light;
582
+ --ds-accent: #00d4ff;
583
+ --ds-accent-2: #7c3aed;
584
+ --ds-panel-bg: rgba(255, 255, 255, 0.86);
585
+ --ds-panel-border: rgba(15, 23, 42, 0.08);
586
+ --ds-subtle-bg: rgba(255, 255, 255, 0.62);
587
+ --ds-selected-bg: linear-gradient(90deg, rgba(0, 212, 255, 0.14), rgba(124, 58, 237, 0.08));
588
+ --ds-focus-ring: rgba(0, 212, 255, 0.32);
589
+ --ds-nav-hover-bg: rgba(15, 23, 42, 0.06);
590
+ --ds-code-bg: #f7f9fb;
591
+ --ds-code-border: #eef2f7;
592
+ --sandbox-bg: #f5f7fb;
593
+ --sandbox-text: #111;
594
+ }
595
+ :root[data-theme='dark'] {
596
+ color-scheme: dark;
597
+ --ds-accent: #00d4ff;
598
+ --ds-accent-2: #a855f7;
599
+ --ds-panel-bg: rgba(17, 19, 28, 0.82);
600
+ --ds-panel-border: rgba(255, 255, 255, 0.14);
601
+ --ds-subtle-bg: rgba(255, 255, 255, 0.04);
602
+ --ds-selected-bg: linear-gradient(90deg, rgba(0, 212, 255, 0.18), rgba(168, 85, 247, 0.14));
603
+ --ds-focus-ring: rgba(0, 212, 255, 0.5);
604
+ --ds-nav-hover-bg: rgba(255, 255, 255, 0.08);
605
+ --ds-code-bg: #0d1117;
606
+ --ds-code-border: #30363d;
607
+ --sandbox-bg: #0f1115;
608
+ --sandbox-text: #eee;
609
+ }
610
+ body {
611
+ margin:0;
612
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
613
+ background: var(--sandbox-bg);
614
+ color: var(--sandbox-text);
615
+ }
616
+ #appRoot { height: 100vh; display:flex; flex-direction:column; }
617
+ #sandboxToolbar {
618
+ flex: 0 0 auto;
619
+ border-bottom: 1px solid var(--ds-panel-border);
620
+ padding: 10px 12px;
621
+ background: var(--ds-panel-bg);
622
+ }
623
+ #headerSlot {
624
+ flex: 0 0 auto;
625
+ border-bottom: 1px solid var(--ds-panel-border);
626
+ padding: 10px 12px;
627
+ background: var(--ds-panel-bg);
628
+ }
629
+ #container { flex: 1 1 auto; min-height:0; overflow:hidden; }
630
+ #containerInner { height:100%; overflow:auto; }
631
+ .muted { opacity: 0.7; font-size: 12px; }
632
+ .bar { display:flex; gap:10px; align-items:center; justify-content:space-between; }
633
+ .btn {
634
+ border:1px solid var(--ds-panel-border);
635
+ background: var(--ds-subtle-bg);
636
+ padding:6px 10px;
637
+ border-radius:10px;
638
+ cursor:pointer;
639
+ font-weight:650;
640
+ color: inherit;
641
+ }
642
+ .btn[data-active='1'] {
643
+ background: var(--ds-selected-bg);
644
+ box-shadow: 0 0 0 2px var(--ds-focus-ring);
645
+ }
646
+ .btn:active { transform: translateY(1px); }
647
+ #promptsPanel {
648
+ position: fixed;
649
+ right: 12px;
650
+ bottom: 12px;
651
+ width: 420px;
652
+ max-height: 70vh;
653
+ display:none;
654
+ flex-direction:column;
655
+ background: var(--ds-panel-bg);
656
+ color: inherit;
657
+ border:1px solid var(--ds-panel-border);
658
+ border-radius:14px;
659
+ overflow:hidden;
660
+ box-shadow: 0 18px 60px rgba(0,0,0,0.18);
661
+ }
662
+ #promptsPanelHeader { padding: 10px 12px; display:flex; align-items:center; justify-content:space-between; border-bottom: 1px solid var(--ds-panel-border); }
663
+ #promptsPanelBody { padding: 10px 12px; overflow:auto; display:flex; flex-direction:column; gap:10px; }
664
+ #promptsFab { position: fixed; right: 16px; bottom: 16px; width: 44px; height: 44px; border-radius: 999px; display:flex; align-items:center; justify-content:center; }
665
+ .card { border: 1px solid var(--ds-panel-border); border-radius: 12px; padding: 10px; background: var(--ds-panel-bg); }
666
+ .row { display:flex; gap:10px; }
667
+ .toolbar-group { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
668
+ .segmented { display:flex; gap:6px; align-items:center; }
669
+ #sandboxInspector {
670
+ position: fixed;
671
+ right: 12px;
672
+ top: 72px;
673
+ width: 360px;
674
+ max-height: 70vh;
675
+ display: none;
676
+ flex-direction: column;
677
+ background: var(--ds-panel-bg);
678
+ border: 1px solid var(--ds-panel-border);
679
+ border-radius: 12px;
680
+ overflow: hidden;
681
+ box-shadow: 0 14px 40px rgba(0,0,0,0.16);
682
+ z-index: 10;
683
+ }
684
+ #sandboxInspectorHeader {
685
+ padding: 10px 12px;
686
+ display:flex;
687
+ align-items:center;
688
+ justify-content: space-between;
689
+ border-bottom: 1px solid var(--ds-panel-border);
690
+ }
701
691
  #sandboxInspectorBody {
702
692
  padding: 10px 12px;
703
693
  overflow: auto;
@@ -734,50 +724,88 @@ function htmlPage() {
734
724
  flex-direction: column;
735
725
  gap: 10px;
736
726
  }
727
+ #mcpPanel {
728
+ position: fixed;
729
+ right: 12px;
730
+ top: 72px;
731
+ width: 520px;
732
+ max-height: 70vh;
733
+ display: none;
734
+ flex-direction: column;
735
+ background: var(--ds-panel-bg);
736
+ border: 1px solid var(--ds-panel-border);
737
+ border-radius: 12px;
738
+ overflow: hidden;
739
+ box-shadow: 0 14px 40px rgba(0,0,0,0.16);
740
+ z-index: 12;
741
+ }
742
+ #mcpPanelHeader {
743
+ padding: 10px 12px;
744
+ display:flex;
745
+ align-items:center;
746
+ justify-content: space-between;
747
+ border-bottom: 1px solid var(--ds-panel-border);
748
+ }
749
+ #mcpPanelBody {
750
+ padding: 10px 12px;
751
+ overflow: auto;
752
+ display: flex;
753
+ flex-direction: column;
754
+ gap: 10px;
755
+ }
756
+ #mcpPaths {
757
+ max-height: 140px;
758
+ overflow: auto;
759
+ }
760
+ #mcpOutput {
761
+ max-height: 240px;
762
+ overflow: auto;
763
+ }
737
764
  .section-title { font-size: 12px; font-weight: 700; opacity: 0.8; }
738
765
  .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; white-space: pre-wrap; }
739
766
  input, textarea, select {
740
767
  width:100%;
741
- padding:8px;
742
- border-radius:10px;
743
- border:1px solid var(--ds-panel-border);
744
- background: var(--ds-subtle-bg);
745
- color: inherit;
746
- }
747
- textarea { min-height: 70px; resize: vertical; }
748
- label { font-size: 12px; opacity: 0.8; }
749
- .danger { border-color: rgba(255,0,0,0.35); }
750
- </style>
751
- </head>
752
- <body>
753
- <div id="appRoot">
754
- <div id="sandboxToolbar">
755
- <div class="bar">
756
- <div>
757
- <div style="font-weight:800">ChatOS UI Apps Sandbox</div>
758
- <div class="muted">Host API mock · 模拟 module mount({ container, host, slots })</div>
759
- </div>
760
- <div class="row toolbar-group">
761
- <span class="muted">Theme</span>
762
- <div class="segmented" role="group" aria-label="Theme">
763
- <button id="btnThemeLight" class="btn" type="button">Light</button>
764
- <button id="btnThemeDark" class="btn" type="button">Dark</button>
765
- <button id="btnThemeSystem" class="btn" type="button">System</button>
766
- </div>
768
+ padding:8px;
769
+ border-radius:10px;
770
+ border:1px solid var(--ds-panel-border);
771
+ background: var(--ds-subtle-bg);
772
+ color: inherit;
773
+ }
774
+ textarea { min-height: 70px; resize: vertical; }
775
+ label { font-size: 12px; opacity: 0.8; }
776
+ .danger { border-color: rgba(255,0,0,0.35); }
777
+ </style>
778
+ </head>
779
+ <body>
780
+ <div id="appRoot">
781
+ <div id="sandboxToolbar">
782
+ <div class="bar">
783
+ <div>
784
+ <div style="font-weight:800">ChatOS UI Apps Sandbox</div>
785
+ <div class="muted">Host API mock · 模拟 module mount({ container, host, slots })</div>
786
+ </div>
787
+ <div class="row toolbar-group">
788
+ <span class="muted">Theme</span>
789
+ <div class="segmented" role="group" aria-label="Theme">
790
+ <button id="btnThemeLight" class="btn" type="button">Light</button>
791
+ <button id="btnThemeDark" class="btn" type="button">Dark</button>
792
+ <button id="btnThemeSystem" class="btn" type="button">System</button>
793
+ </div>
767
794
  <div id="themeStatus" class="muted"></div>
768
795
  <div id="sandboxContext" class="muted"></div>
769
796
  <button id="btnLlmConfig" class="btn" type="button">AI Config</button>
797
+ <button id="btnMcpTest" class="btn" type="button">MCP Test</button>
770
798
  <button id="btnInspectorToggle" class="btn" type="button">Inspect</button>
771
799
  <button id="btnReload" class="btn" type="button">Reload</button>
772
800
  </div>
773
801
  </div>
774
802
  </div>
775
- <div id="headerSlot"></div>
776
- <div id="container"><div id="containerInner"></div></div>
777
- </div>
778
-
779
- <button id="promptsFab" class="btn" type="button">:)</button>
780
-
803
+ <div id="headerSlot"></div>
804
+ <div id="container"><div id="containerInner"></div></div>
805
+ </div>
806
+
807
+ <button id="promptsFab" class="btn" type="button">:)</button>
808
+
781
809
  <div id="promptsPanel">
782
810
  <div id="promptsPanelHeader">
783
811
  <div style="font-weight:800">UI Prompts</div>
@@ -808,11 +836,6 @@ function htmlPage() {
808
836
  <label for="llmModelId">Model ID</label>
809
837
  <input id="llmModelId" type="text" placeholder="gpt-4o-mini" />
810
838
  </div>
811
- <div class="card">
812
- <label for="llmWorkdir">Workdir</label>
813
- <input id="llmWorkdir" type="text" placeholder="(default: dataDir)" />
814
- <div class="muted">留空使用 dataDir;支持 $dataDir/$pluginDir/$projectRoot</div>
815
- </div>
816
839
  <div class="row">
817
840
  <button id="btnLlmSave" class="btn" type="button">Save</button>
818
841
  <button id="btnLlmClear" class="btn" type="button">Clear Key</button>
@@ -821,51 +844,95 @@ function htmlPage() {
821
844
  </div>
822
845
  </div>
823
846
 
847
+ <div id="mcpPanel" aria-hidden="true">
848
+ <div id="mcpPanelHeader">
849
+ <div style="font-weight:800">MCP Test</div>
850
+ <div class="row">
851
+ <button id="btnMcpClear" class="btn" type="button">Clear</button>
852
+ <button id="btnMcpClose" class="btn" type="button">Close</button>
853
+ </div>
854
+ </div>
855
+ <div id="mcpPanelBody">
856
+ <div class="card">
857
+ <div class="section-title">Paths</div>
858
+ <pre id="mcpPaths" class="mono"></pre>
859
+ <label for="mcpWorkdir">Workdir Override (optional)</label>
860
+ <input id="mcpWorkdir" type="text" placeholder="留空则使用默认 workdir" />
861
+ </div>
862
+ <div class="card">
863
+ <div class="section-title">Model</div>
864
+ <label for="mcpModelId">Model ID (optional)</label>
865
+ <input id="mcpModelId" type="text" placeholder="留空则使用 AI Config 中的 Model ID" />
866
+ <div id="mcpConfigHint" class="muted"></div>
867
+ </div>
868
+ <div class="card">
869
+ <label for="mcpSystem">System Prompt (optional)</label>
870
+ <textarea id="mcpSystem" placeholder="留空则使用 MCP Prompt / 空"></textarea>
871
+ </div>
872
+ <div class="card">
873
+ <label for="mcpMessage">User Message</label>
874
+ <textarea id="mcpMessage" placeholder="输入测试消息"></textarea>
875
+ <div class="row" style="margin-top:8px; align-items:center; justify-content:space-between;">
876
+ <label style="display:flex; gap:6px; align-items:center;">
877
+ <input id="mcpDisableTools" type="checkbox" style="width:auto;" />
878
+ Disable tools
879
+ </label>
880
+ <button id="btnMcpSend" class="btn" type="button">Send</button>
881
+ </div>
882
+ <div id="mcpStatus" class="muted"></div>
883
+ </div>
884
+ <div class="card">
885
+ <div class="section-title">Output</div>
886
+ <pre id="mcpOutput" class="mono"></pre>
887
+ </div>
888
+ </div>
889
+ </div>
890
+
824
891
  <div id="sandboxInspector" aria-hidden="true">
825
892
  <div id="sandboxInspectorHeader">
826
893
  <div style="font-weight:800">Sandbox Inspector</div>
827
894
  <div class="row">
828
895
  <button id="btnInspectorRefresh" class="btn" type="button">Refresh</button>
829
- <button id="btnInspectorClose" class="btn" type="button">Close</button>
830
- </div>
831
- </div>
832
- <div id="sandboxInspectorBody">
833
- <div>
834
- <div class="section-title">Host Context</div>
835
- <pre id="inspectorContext" class="mono"></pre>
836
- </div>
837
- <div>
838
- <div class="section-title">Theme</div>
839
- <pre id="inspectorTheme" class="mono"></pre>
840
- </div>
841
- <div>
842
- <div class="section-title">Tokens</div>
843
- <pre id="inspectorTokens" class="mono"></pre>
844
- </div>
845
- </div>
846
- </div>
847
-
848
- <script type="module" src="/sandbox.mjs"></script>
849
- </body>
850
- </html>`;
851
- }
852
-
853
- function sandboxClientJs() {
854
- return `const $ = (sel) => document.querySelector(sel);
855
-
856
- const container = $('#containerInner');
857
- const headerSlot = $('#headerSlot');
858
- const fab = $('#promptsFab');
859
- const panel = $('#promptsPanel');
860
- const panelBody = $('#promptsPanelBody');
861
- const panelClose = $('#promptsClose');
862
- const btnThemeLight = $('#btnThemeLight');
863
- const btnThemeDark = $('#btnThemeDark');
864
- const btnThemeSystem = $('#btnThemeSystem');
865
- const themeStatus = $('#themeStatus');
866
- const sandboxContext = $('#sandboxContext');
867
- const btnInspectorToggle = $('#btnInspectorToggle');
868
- const sandboxInspector = $('#sandboxInspector');
896
+ <button id="btnInspectorClose" class="btn" type="button">Close</button>
897
+ </div>
898
+ </div>
899
+ <div id="sandboxInspectorBody">
900
+ <div>
901
+ <div class="section-title">Host Context</div>
902
+ <pre id="inspectorContext" class="mono"></pre>
903
+ </div>
904
+ <div>
905
+ <div class="section-title">Theme</div>
906
+ <pre id="inspectorTheme" class="mono"></pre>
907
+ </div>
908
+ <div>
909
+ <div class="section-title">Tokens</div>
910
+ <pre id="inspectorTokens" class="mono"></pre>
911
+ </div>
912
+ </div>
913
+ </div>
914
+
915
+ <script type="module" src="/sandbox.mjs"></script>
916
+ </body>
917
+ </html>`;
918
+ }
919
+
920
+ function sandboxClientJs() {
921
+ return `const $ = (sel) => document.querySelector(sel);
922
+
923
+ const container = $('#containerInner');
924
+ const headerSlot = $('#headerSlot');
925
+ const fab = $('#promptsFab');
926
+ const panel = $('#promptsPanel');
927
+ const panelBody = $('#promptsPanelBody');
928
+ const panelClose = $('#promptsClose');
929
+ const btnThemeLight = $('#btnThemeLight');
930
+ const btnThemeDark = $('#btnThemeDark');
931
+ const btnThemeSystem = $('#btnThemeSystem');
932
+ const themeStatus = $('#themeStatus');
933
+ const sandboxContext = $('#sandboxContext');
934
+ const btnInspectorToggle = $('#btnInspectorToggle');
935
+ const sandboxInspector = $('#sandboxInspector');
869
936
  const btnInspectorClose = $('#btnInspectorClose');
870
937
  const btnInspectorRefresh = $('#btnInspectorRefresh');
871
938
  const inspectorContext = $('#inspectorContext');
@@ -880,62 +947,75 @@ const btnLlmClear = $('#btnLlmClear');
880
947
  const llmApiKey = $('#llmApiKey');
881
948
  const llmBaseUrl = $('#llmBaseUrl');
882
949
  const llmModelId = $('#llmModelId');
883
- const llmWorkdir = $('#llmWorkdir');
884
950
  const llmStatus = $('#llmStatus');
885
951
  const llmKeyStatus = $('#llmKeyStatus');
952
+ const btnMcpTest = $('#btnMcpTest');
953
+ const mcpPanel = $('#mcpPanel');
954
+ const btnMcpClose = $('#btnMcpClose');
955
+ const btnMcpClear = $('#btnMcpClear');
956
+ const btnMcpSend = $('#btnMcpSend');
957
+ const mcpPaths = $('#mcpPaths');
958
+ const mcpWorkdir = $('#mcpWorkdir');
959
+ const mcpModelId = $('#mcpModelId');
960
+ const mcpSystem = $('#mcpSystem');
961
+ const mcpMessage = $('#mcpMessage');
962
+ const mcpDisableTools = $('#mcpDisableTools');
963
+ const mcpStatus = $('#mcpStatus');
964
+ const mcpOutput = $('#mcpOutput');
965
+ const mcpConfigHint = $('#mcpConfigHint');
886
966
 
887
967
  const setPanelOpen = (open) => { panel.style.display = open ? 'flex' : 'none'; };
888
968
  fab.addEventListener('click', () => setPanelOpen(panel.style.display !== 'flex'));
889
969
  panelClose.addEventListener('click', () => setPanelOpen(false));
890
970
  window.addEventListener('chatos:uiPrompts:open', () => setPanelOpen(true));
891
- window.addEventListener('chatos:uiPrompts:close', () => setPanelOpen(false));
892
- window.addEventListener('chatos:uiPrompts:toggle', () => setPanelOpen(panel.style.display !== 'flex'));
893
-
894
- const THEME_STORAGE_KEY = 'chatos:sandbox:theme-mode';
895
- const themeListeners = new Set();
896
- const themeButtons = [
897
- { mode: 'light', el: btnThemeLight },
898
- { mode: 'dark', el: btnThemeDark },
899
- { mode: 'system', el: btnThemeSystem },
900
- ];
901
- const systemQuery = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
902
-
903
- const normalizeThemeMode = (mode) => (mode === 'light' || mode === 'dark' || mode === 'system' ? mode : 'system');
904
-
905
- const loadThemeMode = () => {
906
- try {
907
- return normalizeThemeMode(String(localStorage.getItem(THEME_STORAGE_KEY) || ''));
908
- } catch {
909
- return 'system';
910
- }
911
- };
912
-
913
- let themeMode = loadThemeMode();
914
- let currentTheme = 'light';
915
- let inspectorEnabled = false;
916
- let inspectorTimer = null;
917
-
918
- const resolveTheme = () => {
919
- if (themeMode === 'light' || themeMode === 'dark') return themeMode;
920
- return systemQuery && systemQuery.matches ? 'dark' : 'light';
921
- };
922
-
923
- const emitThemeChange = (theme) => {
924
- for (const fn of themeListeners) { try { fn(theme); } catch {} }
925
- };
926
-
927
- const updateThemeControls = () => {
928
- for (const { mode, el } of themeButtons) {
929
- if (!el) continue;
930
- const active = mode === themeMode;
931
- el.dataset.active = active ? '1' : '0';
932
- el.setAttribute('aria-pressed', active ? 'true' : 'false');
933
- }
934
- if (themeStatus) {
935
- themeStatus.textContent = themeMode === 'system' ? 'system -> ' + currentTheme : currentTheme;
936
- }
937
- };
938
-
971
+ window.addEventListener('chatos:uiPrompts:close', () => setPanelOpen(false));
972
+ window.addEventListener('chatos:uiPrompts:toggle', () => setPanelOpen(panel.style.display !== 'flex'));
973
+
974
+ const THEME_STORAGE_KEY = 'chatos:sandbox:theme-mode';
975
+ const themeListeners = new Set();
976
+ const themeButtons = [
977
+ { mode: 'light', el: btnThemeLight },
978
+ { mode: 'dark', el: btnThemeDark },
979
+ { mode: 'system', el: btnThemeSystem },
980
+ ];
981
+ const systemQuery = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
982
+
983
+ const normalizeThemeMode = (mode) => (mode === 'light' || mode === 'dark' || mode === 'system' ? mode : 'system');
984
+
985
+ const loadThemeMode = () => {
986
+ try {
987
+ return normalizeThemeMode(String(localStorage.getItem(THEME_STORAGE_KEY) || ''));
988
+ } catch {
989
+ return 'system';
990
+ }
991
+ };
992
+
993
+ let themeMode = loadThemeMode();
994
+ let currentTheme = 'light';
995
+ let inspectorEnabled = false;
996
+ let inspectorTimer = null;
997
+
998
+ const resolveTheme = () => {
999
+ if (themeMode === 'light' || themeMode === 'dark') return themeMode;
1000
+ return systemQuery && systemQuery.matches ? 'dark' : 'light';
1001
+ };
1002
+
1003
+ const emitThemeChange = (theme) => {
1004
+ for (const fn of themeListeners) { try { fn(theme); } catch {} }
1005
+ };
1006
+
1007
+ const updateThemeControls = () => {
1008
+ for (const { mode, el } of themeButtons) {
1009
+ if (!el) continue;
1010
+ const active = mode === themeMode;
1011
+ el.dataset.active = active ? '1' : '0';
1012
+ el.setAttribute('aria-pressed', active ? 'true' : 'false');
1013
+ }
1014
+ if (themeStatus) {
1015
+ themeStatus.textContent = themeMode === 'system' ? 'system -> ' + currentTheme : currentTheme;
1016
+ }
1017
+ };
1018
+
939
1019
  const updateContextStatus = () => {
940
1020
  if (!sandboxContext) return;
941
1021
  sandboxContext.textContent = __SANDBOX__.pluginId + ':' + __SANDBOX__.appId;
@@ -943,6 +1023,7 @@ const updateContextStatus = () => {
943
1023
 
944
1024
  const isInspectorOpen = () => sandboxInspector && sandboxInspector.style.display === 'flex';
945
1025
  const isLlmPanelOpen = () => llmPanel && llmPanel.style.display === 'flex';
1026
+ const isMcpPanelOpen = () => mcpPanel && mcpPanel.style.display === 'flex';
946
1027
 
947
1028
  const setLlmStatus = (text, isError) => {
948
1029
  if (!llmStatus) return;
@@ -959,7 +1040,6 @@ const refreshLlmConfig = async () => {
959
1040
  const cfg = j?.config || {};
960
1041
  if (llmBaseUrl) llmBaseUrl.value = cfg.baseUrl || '';
961
1042
  if (llmModelId) llmModelId.value = cfg.modelId || '';
962
- if (llmWorkdir) llmWorkdir.value = cfg.workdir || '';
963
1043
  if (llmKeyStatus) llmKeyStatus.textContent = cfg.hasApiKey ? 'API key set' : 'API key missing';
964
1044
  setLlmStatus('');
965
1045
  } catch (err) {
@@ -973,7 +1053,6 @@ const saveLlmConfig = async ({ clearKey } = {}) => {
973
1053
  const payload = {
974
1054
  baseUrl: llmBaseUrl ? llmBaseUrl.value : '',
975
1055
  modelId: llmModelId ? llmModelId.value : '',
976
- workdir: llmWorkdir ? llmWorkdir.value : '',
977
1056
  };
978
1057
  const apiKey = llmApiKey ? llmApiKey.value : '';
979
1058
  if (clearKey) {
@@ -1007,103 +1086,190 @@ const formatJson = (value) => {
1007
1086
  try {
1008
1087
  return JSON.stringify(value, null, 2);
1009
1088
  } catch {
1010
- return String(value);
1011
- }
1012
- };
1013
-
1089
+ return String(value);
1090
+ }
1091
+ };
1092
+
1093
+ const sandboxPaths = __SANDBOX__.paths || {};
1094
+
1095
+ const setMcpStatus = (text, isError) => {
1096
+ if (!mcpStatus) return;
1097
+ mcpStatus.textContent = text || '';
1098
+ mcpStatus.style.color = isError ? '#ef4444' : '';
1099
+ };
1100
+
1101
+ const appendMcpOutput = (label, payload) => {
1102
+ if (!mcpOutput) return;
1103
+ const ts = new Date().toISOString();
1104
+ const content = typeof payload === 'string' ? payload : formatJson(payload);
1105
+ mcpOutput.textContent += '[' + ts + '] ' + label + '\\n' + content + '\\n\\n';
1106
+ mcpOutput.scrollTop = mcpOutput.scrollHeight;
1107
+ };
1108
+
1109
+ const refreshMcpPaths = () => {
1110
+ if (!mcpPaths) return;
1111
+ mcpPaths.textContent = formatJson(sandboxPaths);
1112
+ };
1113
+
1114
+ const refreshMcpConfigHint = async () => {
1115
+ if (!mcpConfigHint) return;
1116
+ try {
1117
+ mcpConfigHint.textContent = 'Loading...';
1118
+ const r = await fetch('/api/sandbox/llm-config');
1119
+ const j = await r.json();
1120
+ if (!j?.ok) throw new Error(j?.message || 'Failed to load config');
1121
+ const cfg = j?.config || {};
1122
+ const modelText = cfg.modelId ? 'model=' + cfg.modelId : 'model=unset';
1123
+ const baseText = cfg.baseUrl ? 'base=' + cfg.baseUrl : 'base=default';
1124
+ const keyText = cfg.hasApiKey ? 'key=ok' : 'key=missing';
1125
+ mcpConfigHint.textContent = modelText + ' · ' + baseText + ' · ' + keyText;
1126
+ } catch (err) {
1127
+ mcpConfigHint.textContent = err?.message || String(err);
1128
+ }
1129
+ };
1130
+
1131
+ const setMcpPanelOpen = (open) => {
1132
+ if (!mcpPanel) return;
1133
+ mcpPanel.style.display = open ? 'flex' : 'none';
1134
+ mcpPanel.setAttribute('aria-hidden', open ? 'false' : 'true');
1135
+ if (open) {
1136
+ refreshMcpConfigHint();
1137
+ refreshMcpPaths();
1138
+ }
1139
+ };
1140
+
1141
+ const runMcpTest = async () => {
1142
+ const sendBtn = btnMcpSend;
1143
+ try {
1144
+ const message = mcpMessage ? String(mcpMessage.value || '').trim() : '';
1145
+ if (!message) {
1146
+ setMcpStatus('Message is required.', true);
1147
+ return;
1148
+ }
1149
+ if (sendBtn) sendBtn.disabled = true;
1150
+ const payload = {
1151
+ messages: [{ role: 'user', text: message }],
1152
+ modelId: mcpModelId ? String(mcpModelId.value || '').trim() : '',
1153
+ systemPrompt: mcpSystem ? String(mcpSystem.value || '').trim() : '',
1154
+ disableTools: Boolean(mcpDisableTools?.checked),
1155
+ };
1156
+ const workdirOverride = mcpWorkdir ? String(mcpWorkdir.value || '').trim() : '';
1157
+ if (workdirOverride) {
1158
+ payload.callMeta = { workdir: workdirOverride };
1159
+ }
1160
+ setMcpStatus('Sending...');
1161
+ appendMcpOutput('request', payload);
1162
+ const r = await fetch('/api/llm/chat', {
1163
+ method: 'POST',
1164
+ headers: { 'content-type': 'application/json' },
1165
+ body: JSON.stringify(payload),
1166
+ });
1167
+ const j = await r.json();
1168
+ if (!j?.ok) throw new Error(j?.message || 'MCP test failed');
1169
+ appendMcpOutput('assistant', j?.content || '');
1170
+ if (Array.isArray(j?.toolTrace) && j.toolTrace.length > 0) {
1171
+ appendMcpOutput('toolTrace', j.toolTrace);
1172
+ }
1173
+ setMcpStatus('Done');
1174
+ } catch (err) {
1175
+ setMcpStatus(err?.message || String(err), true);
1176
+ } finally {
1177
+ if (sendBtn) sendBtn.disabled = false;
1178
+ }
1179
+ };
1180
+
1014
1181
  const tokenNameList = Array.isArray(__SANDBOX__.tokenNames) ? __SANDBOX__.tokenNames : [];
1015
- const sandboxContextBase = __SANDBOX__.context || { pluginId: __SANDBOX__.pluginId, appId: __SANDBOX__.appId };
1016
-
1017
- const collectTokens = () => {
1018
- const style = getComputedStyle(document.documentElement);
1019
- const names = new Set(tokenNameList);
1020
- for (let i = 0; i < style.length; i += 1) {
1021
- const name = style[i];
1022
- if (name && name.startsWith('--ds-')) names.add(name);
1023
- }
1024
- return [...names]
1025
- .sort()
1026
- .map((name) => {
1027
- const value = style.getPropertyValue(name).trim();
1028
- return name + ': ' + (value || '(unset)');
1029
- })
1030
- .join('\\n');
1031
- };
1032
-
1182
+
1183
+ const collectTokens = () => {
1184
+ const style = getComputedStyle(document.documentElement);
1185
+ const names = new Set(tokenNameList);
1186
+ for (let i = 0; i < style.length; i += 1) {
1187
+ const name = style[i];
1188
+ if (name && name.startsWith('--ds-')) names.add(name);
1189
+ }
1190
+ return [...names]
1191
+ .sort()
1192
+ .map((name) => {
1193
+ const value = style.getPropertyValue(name).trim();
1194
+ return name + ': ' + (value || '(unset)');
1195
+ })
1196
+ .join('\\n');
1197
+ };
1198
+
1033
1199
  const readHostContext = () => {
1034
1200
  if (!inspectorEnabled) return null;
1035
1201
  if (typeof host?.context?.get === 'function') return host.context.get();
1036
- return { ...sandboxContextBase, theme: currentTheme, bridge: { enabled: true } };
1202
+ return { pluginId: __SANDBOX__.pluginId, appId: __SANDBOX__.appId, theme: currentTheme, bridge: { enabled: true } };
1203
+ };
1204
+
1205
+ const readThemeInfo = () => ({
1206
+ themeMode,
1207
+ currentTheme,
1208
+ dataTheme: document.documentElement.dataset.theme || '',
1209
+ dataThemeMode: document.documentElement.dataset.themeMode || '',
1210
+ prefersColorScheme: systemQuery ? (systemQuery.matches ? 'dark' : 'light') : 'unknown',
1211
+ });
1212
+
1213
+ const updateInspector = () => {
1214
+ if (!inspectorEnabled) return;
1215
+ if (inspectorContext) inspectorContext.textContent = formatJson(readHostContext());
1216
+ if (inspectorTheme) inspectorTheme.textContent = formatJson(readThemeInfo());
1217
+ if (inspectorTokens) inspectorTokens.textContent = collectTokens();
1218
+ };
1219
+
1220
+ const startInspectorTimer = () => {
1221
+ if (inspectorTimer) return;
1222
+ inspectorTimer = setInterval(updateInspector, 1000);
1223
+ };
1224
+
1225
+ const stopInspectorTimer = () => {
1226
+ if (!inspectorTimer) return;
1227
+ clearInterval(inspectorTimer);
1228
+ inspectorTimer = null;
1229
+ };
1230
+
1231
+ const setInspectorOpen = (open) => {
1232
+ if (!sandboxInspector) return;
1233
+ sandboxInspector.style.display = open ? 'flex' : 'none';
1234
+ sandboxInspector.setAttribute('aria-hidden', open ? 'false' : 'true');
1235
+ if (open) {
1236
+ updateInspector();
1237
+ startInspectorTimer();
1238
+ } else {
1239
+ stopInspectorTimer();
1240
+ }
1037
1241
  };
1038
-
1039
- const readThemeInfo = () => ({
1040
- themeMode,
1041
- currentTheme,
1042
- dataTheme: document.documentElement.dataset.theme || '',
1043
- dataThemeMode: document.documentElement.dataset.themeMode || '',
1044
- prefersColorScheme: systemQuery ? (systemQuery.matches ? 'dark' : 'light') : 'unknown',
1045
- });
1046
-
1047
- const updateInspector = () => {
1048
- if (!inspectorEnabled) return;
1049
- if (inspectorContext) inspectorContext.textContent = formatJson(readHostContext());
1050
- if (inspectorTheme) inspectorTheme.textContent = formatJson(readThemeInfo());
1051
- if (inspectorTokens) inspectorTokens.textContent = collectTokens();
1052
- };
1053
-
1054
- const startInspectorTimer = () => {
1055
- if (inspectorTimer) return;
1056
- inspectorTimer = setInterval(updateInspector, 1000);
1057
- };
1058
-
1059
- const stopInspectorTimer = () => {
1060
- if (!inspectorTimer) return;
1061
- clearInterval(inspectorTimer);
1062
- inspectorTimer = null;
1063
- };
1064
-
1065
- const setInspectorOpen = (open) => {
1066
- if (!sandboxInspector) return;
1067
- sandboxInspector.style.display = open ? 'flex' : 'none';
1068
- sandboxInspector.setAttribute('aria-hidden', open ? 'false' : 'true');
1069
- if (open) {
1070
- updateInspector();
1071
- startInspectorTimer();
1072
- } else {
1073
- stopInspectorTimer();
1074
- }
1075
- };
1076
-
1077
- const updateInspectorIfOpen = () => {
1078
- if (!inspectorEnabled) return;
1079
- if (isInspectorOpen()) updateInspector();
1080
- };
1081
-
1082
- const applyThemeMode = (mode, { persist = true } = {}) => {
1083
- themeMode = normalizeThemeMode(mode);
1084
- if (persist) {
1085
- try {
1086
- localStorage.setItem(THEME_STORAGE_KEY, themeMode);
1087
- } catch {
1088
- // ignore
1089
- }
1090
- }
1091
- const nextTheme = resolveTheme();
1092
- const prevTheme = currentTheme;
1093
- currentTheme = nextTheme;
1094
- document.documentElement.dataset.theme = nextTheme;
1095
- document.documentElement.dataset.themeMode = themeMode;
1096
- updateThemeControls();
1097
- updateInspectorIfOpen();
1098
- if (nextTheme !== prevTheme) emitThemeChange(nextTheme);
1099
- };
1100
-
1101
- if (systemQuery && typeof systemQuery.addEventListener === 'function') {
1102
- systemQuery.addEventListener('change', () => {
1103
- if (themeMode === 'system') applyThemeMode('system', { persist: false });
1104
- });
1105
- }
1106
-
1242
+
1243
+ const updateInspectorIfOpen = () => {
1244
+ if (!inspectorEnabled) return;
1245
+ if (isInspectorOpen()) updateInspector();
1246
+ };
1247
+
1248
+ const applyThemeMode = (mode, { persist = true } = {}) => {
1249
+ themeMode = normalizeThemeMode(mode);
1250
+ if (persist) {
1251
+ try {
1252
+ localStorage.setItem(THEME_STORAGE_KEY, themeMode);
1253
+ } catch {
1254
+ // ignore
1255
+ }
1256
+ }
1257
+ const nextTheme = resolveTheme();
1258
+ const prevTheme = currentTheme;
1259
+ currentTheme = nextTheme;
1260
+ document.documentElement.dataset.theme = nextTheme;
1261
+ document.documentElement.dataset.themeMode = themeMode;
1262
+ updateThemeControls();
1263
+ updateInspectorIfOpen();
1264
+ if (nextTheme !== prevTheme) emitThemeChange(nextTheme);
1265
+ };
1266
+
1267
+ if (systemQuery && typeof systemQuery.addEventListener === 'function') {
1268
+ systemQuery.addEventListener('change', () => {
1269
+ if (themeMode === 'system') applyThemeMode('system', { persist: false });
1270
+ });
1271
+ }
1272
+
1107
1273
  if (btnThemeLight) btnThemeLight.addEventListener('click', () => applyThemeMode('light'));
1108
1274
  if (btnThemeDark) btnThemeDark.addEventListener('click', () => applyThemeMode('dark'));
1109
1275
  if (btnThemeSystem) btnThemeSystem.addEventListener('click', () => applyThemeMode('system'));
@@ -1112,156 +1278,167 @@ if (btnLlmClose) btnLlmClose.addEventListener('click', () => setLlmPanelOpen(fal
1112
1278
  if (btnLlmRefresh) btnLlmRefresh.addEventListener('click', () => refreshLlmConfig());
1113
1279
  if (btnLlmSave) btnLlmSave.addEventListener('click', () => saveLlmConfig());
1114
1280
  if (btnLlmClear) btnLlmClear.addEventListener('click', () => saveLlmConfig({ clearKey: true }));
1281
+ if (btnMcpTest) btnMcpTest.addEventListener('click', () => setMcpPanelOpen(!isMcpPanelOpen()));
1282
+ if (btnMcpClose) btnMcpClose.addEventListener('click', () => setMcpPanelOpen(false));
1283
+ if (btnMcpClear)
1284
+ btnMcpClear.addEventListener('click', () => {
1285
+ if (mcpOutput) mcpOutput.textContent = '';
1286
+ setMcpStatus('');
1287
+ });
1288
+ if (btnMcpSend)
1289
+ btnMcpSend.addEventListener('click', () => {
1290
+ runMcpTest();
1291
+ });
1115
1292
  if (btnInspectorToggle) btnInspectorToggle.addEventListener('click', () => setInspectorOpen(!isInspectorOpen()));
1116
1293
  if (btnInspectorClose) btnInspectorClose.addEventListener('click', () => setInspectorOpen(false));
1117
1294
  if (btnInspectorRefresh) btnInspectorRefresh.addEventListener('click', () => updateInspector());
1118
-
1119
- applyThemeMode(themeMode || 'system', { persist: false });
1120
- updateContextStatus();
1121
-
1122
- const entries = [];
1123
- const listeners = new Set();
1124
- const emitUpdate = () => {
1125
- const payload = { path: '(sandbox)', entries: [...entries] };
1126
- for (const fn of listeners) { try { fn(payload); } catch {} }
1127
- renderPrompts();
1128
- };
1129
-
1130
- const uuid = () => (globalThis.crypto?.randomUUID ? crypto.randomUUID() : String(Date.now()) + '-' + Math.random().toString(16).slice(2));
1131
-
1132
- function renderPrompts() {
1133
- panelBody.textContent = '';
1134
- const pending = new Map();
1135
- for (const e of entries) {
1136
- if (e?.type !== 'ui_prompt') continue;
1137
- const id = String(e?.requestId || '');
1138
- if (!id) continue;
1139
- if (e.action === 'request') pending.set(id, e);
1140
- if (e.action === 'response') pending.delete(id);
1141
- }
1142
-
1143
- if (pending.size === 0) {
1144
- const empty = document.createElement('div');
1145
- empty.className = 'muted';
1146
- empty.textContent = '暂无待办(request 后会出现在这里)';
1147
- panelBody.appendChild(empty);
1148
- return;
1149
- }
1150
-
1151
- for (const [requestId, req] of pending.entries()) {
1152
- const card = document.createElement('div');
1153
- card.className = 'card';
1154
-
1155
- const title = document.createElement('div');
1156
- title.style.fontWeight = '800';
1157
- title.textContent = req?.prompt?.title || '(untitled)';
1158
-
1159
- const msg = document.createElement('div');
1160
- msg.className = 'muted';
1161
- msg.style.marginTop = '6px';
1162
- msg.textContent = req?.prompt?.message || '';
1163
-
1164
- const source = document.createElement('div');
1165
- source.className = 'muted';
1166
- source.style.marginTop = '6px';
1167
- source.textContent = req?.prompt?.source ? String(req.prompt.source) : '';
1168
-
1169
- const form = document.createElement('div');
1170
- form.style.marginTop = '10px';
1171
- form.style.display = 'grid';
1172
- form.style.gap = '10px';
1173
-
1174
- const kind = String(req?.prompt?.kind || '');
1175
-
1176
- const mkBtn = (label, danger) => {
1177
- const btn = document.createElement('button');
1178
- btn.type = 'button';
1179
- btn.className = 'btn' + (danger ? ' danger' : '');
1180
- btn.textContent = label;
1181
- return btn;
1182
- };
1183
-
1184
- const submit = async (response) => {
1185
- entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'response', requestId, response });
1186
- emitUpdate();
1187
- };
1188
-
1189
- if (kind === 'kv') {
1190
- const fields = Array.isArray(req?.prompt?.fields) ? req.prompt.fields : [];
1191
- const values = {};
1192
- for (const f of fields) {
1193
- const key = String(f?.key || '');
1194
- if (!key) continue;
1195
- const wrap = document.createElement('div');
1196
- const lab = document.createElement('label');
1197
- lab.textContent = f?.label ? String(f.label) : key;
1198
- const input = document.createElement(f?.multiline ? 'textarea' : 'input');
1199
- input.placeholder = f?.placeholder ? String(f.placeholder) : '';
1200
- input.value = f?.default ? String(f.default) : '';
1201
- input.addEventListener('input', () => { values[key] = String(input.value || ''); });
1202
- values[key] = String(input.value || '');
1203
- wrap.appendChild(lab);
1204
- wrap.appendChild(input);
1205
- form.appendChild(wrap);
1206
- }
1207
- const row = document.createElement('div');
1208
- row.className = 'row';
1209
- const ok = mkBtn('Submit');
1210
- ok.addEventListener('click', () => submit({ status: 'ok', values }));
1211
- const cancel = mkBtn('Cancel', true);
1212
- cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1213
- row.appendChild(ok);
1214
- row.appendChild(cancel);
1215
- form.appendChild(row);
1216
- } else if (kind === 'choice') {
1217
- const options = Array.isArray(req?.prompt?.options) ? req.prompt.options : [];
1218
- const multiple = Boolean(req?.prompt?.multiple);
1219
- const selected = new Set();
1220
- const wrap = document.createElement('div');
1221
- const lab = document.createElement('label');
1222
- lab.textContent = '选择';
1223
- const select = document.createElement('select');
1224
- if (multiple) select.multiple = true;
1225
- for (const opt of options) {
1226
- const v = String(opt?.value || '');
1227
- const o = document.createElement('option');
1228
- o.value = v;
1229
- o.textContent = opt?.label ? String(opt.label) : v;
1230
- select.appendChild(o);
1231
- }
1232
- select.addEventListener('change', () => {
1233
- selected.clear();
1234
- for (const o of select.selectedOptions) selected.add(String(o.value));
1235
- });
1236
- wrap.appendChild(lab);
1237
- wrap.appendChild(select);
1238
- form.appendChild(wrap);
1239
- const row = document.createElement('div');
1240
- row.className = 'row';
1241
- const ok = mkBtn('Submit');
1242
- ok.addEventListener('click', () => submit({ status: 'ok', value: multiple ? Array.from(selected) : Array.from(selected)[0] || '' }));
1243
- const cancel = mkBtn('Cancel', true);
1244
- cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1245
- row.appendChild(ok);
1246
- row.appendChild(cancel);
1247
- form.appendChild(row);
1248
- } else {
1249
- const row = document.createElement('div');
1250
- row.className = 'row';
1251
- const ok = mkBtn('OK');
1252
- ok.addEventListener('click', () => submit({ status: 'ok' }));
1253
- const cancel = mkBtn('Cancel', true);
1254
- cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1255
- row.appendChild(ok);
1256
- row.appendChild(cancel);
1257
- form.appendChild(row);
1258
- }
1259
-
1260
- card.appendChild(title);
1261
- if (msg.textContent) card.appendChild(msg);
1262
- if (source.textContent) card.appendChild(source);
1263
- card.appendChild(form);
1264
- panelBody.appendChild(card);
1295
+
1296
+ applyThemeMode(themeMode || 'system', { persist: false });
1297
+ updateContextStatus();
1298
+
1299
+ const entries = [];
1300
+ const listeners = new Set();
1301
+ const emitUpdate = () => {
1302
+ const payload = { path: '(sandbox)', entries: [...entries] };
1303
+ for (const fn of listeners) { try { fn(payload); } catch {} }
1304
+ renderPrompts();
1305
+ };
1306
+
1307
+ const uuid = () => (globalThis.crypto?.randomUUID ? crypto.randomUUID() : String(Date.now()) + '-' + Math.random().toString(16).slice(2));
1308
+
1309
+ function renderPrompts() {
1310
+ panelBody.textContent = '';
1311
+ const pending = new Map();
1312
+ for (const e of entries) {
1313
+ if (e?.type !== 'ui_prompt') continue;
1314
+ const id = String(e?.requestId || '');
1315
+ if (!id) continue;
1316
+ if (e.action === 'request') pending.set(id, e);
1317
+ if (e.action === 'response') pending.delete(id);
1318
+ }
1319
+
1320
+ if (pending.size === 0) {
1321
+ const empty = document.createElement('div');
1322
+ empty.className = 'muted';
1323
+ empty.textContent = '暂无待办(request 后会出现在这里)';
1324
+ panelBody.appendChild(empty);
1325
+ return;
1326
+ }
1327
+
1328
+ for (const [requestId, req] of pending.entries()) {
1329
+ const card = document.createElement('div');
1330
+ card.className = 'card';
1331
+
1332
+ const title = document.createElement('div');
1333
+ title.style.fontWeight = '800';
1334
+ title.textContent = req?.prompt?.title || '(untitled)';
1335
+
1336
+ const msg = document.createElement('div');
1337
+ msg.className = 'muted';
1338
+ msg.style.marginTop = '6px';
1339
+ msg.textContent = req?.prompt?.message || '';
1340
+
1341
+ const source = document.createElement('div');
1342
+ source.className = 'muted';
1343
+ source.style.marginTop = '6px';
1344
+ source.textContent = req?.prompt?.source ? String(req.prompt.source) : '';
1345
+
1346
+ const form = document.createElement('div');
1347
+ form.style.marginTop = '10px';
1348
+ form.style.display = 'grid';
1349
+ form.style.gap = '10px';
1350
+
1351
+ const kind = String(req?.prompt?.kind || '');
1352
+
1353
+ const mkBtn = (label, danger) => {
1354
+ const btn = document.createElement('button');
1355
+ btn.type = 'button';
1356
+ btn.className = 'btn' + (danger ? ' danger' : '');
1357
+ btn.textContent = label;
1358
+ return btn;
1359
+ };
1360
+
1361
+ const submit = async (response) => {
1362
+ entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'response', requestId, response });
1363
+ emitUpdate();
1364
+ };
1365
+
1366
+ if (kind === 'kv') {
1367
+ const fields = Array.isArray(req?.prompt?.fields) ? req.prompt.fields : [];
1368
+ const values = {};
1369
+ for (const f of fields) {
1370
+ const key = String(f?.key || '');
1371
+ if (!key) continue;
1372
+ const wrap = document.createElement('div');
1373
+ const lab = document.createElement('label');
1374
+ lab.textContent = f?.label ? String(f.label) : key;
1375
+ const input = document.createElement(f?.multiline ? 'textarea' : 'input');
1376
+ input.placeholder = f?.placeholder ? String(f.placeholder) : '';
1377
+ input.value = f?.default ? String(f.default) : '';
1378
+ input.addEventListener('input', () => { values[key] = String(input.value || ''); });
1379
+ values[key] = String(input.value || '');
1380
+ wrap.appendChild(lab);
1381
+ wrap.appendChild(input);
1382
+ form.appendChild(wrap);
1383
+ }
1384
+ const row = document.createElement('div');
1385
+ row.className = 'row';
1386
+ const ok = mkBtn('Submit');
1387
+ ok.addEventListener('click', () => submit({ status: 'ok', values }));
1388
+ const cancel = mkBtn('Cancel', true);
1389
+ cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1390
+ row.appendChild(ok);
1391
+ row.appendChild(cancel);
1392
+ form.appendChild(row);
1393
+ } else if (kind === 'choice') {
1394
+ const options = Array.isArray(req?.prompt?.options) ? req.prompt.options : [];
1395
+ const multiple = Boolean(req?.prompt?.multiple);
1396
+ const selected = new Set();
1397
+ const wrap = document.createElement('div');
1398
+ const lab = document.createElement('label');
1399
+ lab.textContent = '选择';
1400
+ const select = document.createElement('select');
1401
+ if (multiple) select.multiple = true;
1402
+ for (const opt of options) {
1403
+ const v = String(opt?.value || '');
1404
+ const o = document.createElement('option');
1405
+ o.value = v;
1406
+ o.textContent = opt?.label ? String(opt.label) : v;
1407
+ select.appendChild(o);
1408
+ }
1409
+ select.addEventListener('change', () => {
1410
+ selected.clear();
1411
+ for (const o of select.selectedOptions) selected.add(String(o.value));
1412
+ });
1413
+ wrap.appendChild(lab);
1414
+ wrap.appendChild(select);
1415
+ form.appendChild(wrap);
1416
+ const row = document.createElement('div');
1417
+ row.className = 'row';
1418
+ const ok = mkBtn('Submit');
1419
+ ok.addEventListener('click', () => submit({ status: 'ok', value: multiple ? Array.from(selected) : Array.from(selected)[0] || '' }));
1420
+ const cancel = mkBtn('Cancel', true);
1421
+ cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1422
+ row.appendChild(ok);
1423
+ row.appendChild(cancel);
1424
+ form.appendChild(row);
1425
+ } else {
1426
+ const row = document.createElement('div');
1427
+ row.className = 'row';
1428
+ const ok = mkBtn('OK');
1429
+ ok.addEventListener('click', () => submit({ status: 'ok' }));
1430
+ const cancel = mkBtn('Cancel', true);
1431
+ cancel.addEventListener('click', () => submit({ status: 'cancel' }));
1432
+ row.appendChild(ok);
1433
+ row.appendChild(cancel);
1434
+ form.appendChild(row);
1435
+ }
1436
+
1437
+ card.appendChild(title);
1438
+ if (msg.textContent) card.appendChild(msg);
1439
+ if (source.textContent) card.appendChild(source);
1440
+ card.appendChild(form);
1441
+ panelBody.appendChild(card);
1265
1442
  }
1266
1443
  }
1267
1444
 
@@ -1291,120 +1468,120 @@ const callSandboxChat = async (payload, signal) => {
1291
1468
  };
1292
1469
 
1293
1470
  const getTheme = () => currentTheme || resolveTheme();
1294
-
1471
+
1295
1472
  const host = {
1296
1473
  bridge: { enabled: true },
1297
- context: { get: () => ({ ...sandboxContextBase, theme: getTheme(), bridge: { enabled: true } }) },
1474
+ context: { get: () => ({ pluginId: __SANDBOX__.pluginId, appId: __SANDBOX__.appId, theme: getTheme(), bridge: { enabled: true } }) },
1298
1475
  theme: {
1299
- get: getTheme,
1300
- onChange: (listener) => {
1301
- if (typeof listener !== 'function') return () => {};
1302
- themeListeners.add(listener);
1303
- return () => themeListeners.delete(listener);
1304
- },
1305
- },
1306
- admin: {
1307
- state: async () => ({ ok: true, state: {} }),
1308
- onUpdate: () => () => {},
1309
- models: { list: async () => ({ ok: true, models: [] }) },
1310
- secrets: { list: async () => ({ ok: true, secrets: [] }) },
1311
- },
1312
- registry: {
1313
- list: async () => ({ ok: true, apps: [__SANDBOX__.registryApp] }),
1314
- },
1315
- backend: {
1316
- invoke: async (method, params) => {
1317
- const r = await fetch('/api/backend/invoke', {
1318
- method: 'POST',
1319
- headers: { 'content-type': 'application/json' },
1320
- body: JSON.stringify({ method, params }),
1321
- });
1322
- const j = await r.json();
1323
- if (j?.ok === false) throw new Error(j?.message || 'invoke failed');
1324
- return j?.result;
1325
- },
1326
- },
1327
- uiPrompts: {
1328
- read: async () => ({ path: '(sandbox)', entries: [...entries] }),
1329
- onUpdate: (listener) => { listeners.add(listener); return () => listeners.delete(listener); },
1330
- request: async (payload) => {
1331
- const requestId = payload?.requestId ? String(payload.requestId) : uuid();
1332
- const prompt = payload?.prompt && typeof payload.prompt === 'object' ? { ...payload.prompt } : null;
1333
- if (prompt && !prompt.source) prompt.source = __SANDBOX__.pluginId + ':' + __SANDBOX__.appId;
1334
- entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'request', requestId, runId: payload?.runId, prompt });
1335
- emitUpdate();
1336
- return { ok: true, requestId };
1337
- },
1338
- respond: async (payload) => {
1339
- const requestId = String(payload?.requestId || '');
1340
- if (!requestId) throw new Error('requestId is required');
1341
- const response = payload?.response && typeof payload.response === 'object' ? payload.response : null;
1342
- entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'response', requestId, runId: payload?.runId, response });
1343
- emitUpdate();
1344
- return { ok: true };
1345
- },
1346
- open: () => (setPanelOpen(true), { ok: true }),
1347
- close: () => (setPanelOpen(false), { ok: true }),
1348
- toggle: () => (setPanelOpen(panel.style.display !== 'flex'), { ok: true }),
1349
- },
1350
- ui: { navigate: (menu) => ({ ok: true, menu }) },
1351
- chat: (() => {
1352
- const clone = (v) => JSON.parse(JSON.stringify(v));
1353
-
1354
- const agents = [
1355
- {
1356
- id: 'sandbox-agent',
1357
- name: 'Sandbox Agent',
1358
- description: 'Mock agent for ChatOS UI Apps Sandbox',
1359
- },
1360
- ];
1361
-
1362
- const sessions = new Map();
1363
- const defaultSessionByAgent = new Map();
1364
- const messagesBySession = new Map();
1365
-
1366
- const listeners = new Set();
1367
- const activeRuns = new Map(); // sessionId -> { aborted: boolean, timers: number[] }
1368
-
1369
- const emit = (payload) => {
1370
- for (const sub of listeners) {
1371
- const filter = sub?.filter && typeof sub.filter === 'object' ? sub.filter : {};
1372
- if (filter?.sessionId && String(filter.sessionId) !== String(payload?.sessionId || '')) continue;
1373
- if (Array.isArray(filter?.types) && filter.types.length > 0) {
1374
- const t = String(payload?.type || '');
1375
- if (!filter.types.includes(t)) continue;
1376
- }
1377
- try {
1378
- sub.fn(payload);
1379
- } catch {
1380
- // ignore
1381
- }
1382
- }
1383
- };
1384
-
1385
- const ensureAgent = async () => {
1386
- if (agents.length > 0) return agents[0];
1387
- const created = { id: 'sandbox-agent', name: 'Sandbox Agent', description: 'Mock agent' };
1388
- agents.push(created);
1389
- return created;
1390
- };
1391
-
1392
- const ensureSession = async (agentId) => {
1393
- const aid = String(agentId || '').trim() || (await ensureAgent()).id;
1394
- const existingId = defaultSessionByAgent.get(aid);
1395
- if (existingId && sessions.has(existingId)) return sessions.get(existingId);
1396
-
1397
- const id = 'sandbox-session-' + uuid();
1398
- const session = { id, agentId: aid, createdAt: new Date().toISOString() };
1399
- sessions.set(id, session);
1400
- defaultSessionByAgent.set(aid, id);
1401
- if (!messagesBySession.has(id)) messagesBySession.set(id, []);
1402
- return session;
1403
- };
1404
-
1405
- const agentsApi = {
1406
- list: async () => ({ ok: true, agents: clone(agents) }),
1407
- ensureDefault: async () => ({ ok: true, agent: clone(await ensureAgent()) }),
1476
+ get: getTheme,
1477
+ onChange: (listener) => {
1478
+ if (typeof listener !== 'function') return () => {};
1479
+ themeListeners.add(listener);
1480
+ return () => themeListeners.delete(listener);
1481
+ },
1482
+ },
1483
+ admin: {
1484
+ state: async () => ({ ok: true, state: {} }),
1485
+ onUpdate: () => () => {},
1486
+ models: { list: async () => ({ ok: true, models: [] }) },
1487
+ secrets: { list: async () => ({ ok: true, secrets: [] }) },
1488
+ },
1489
+ registry: {
1490
+ list: async () => ({ ok: true, apps: [__SANDBOX__.registryApp] }),
1491
+ },
1492
+ backend: {
1493
+ invoke: async (method, params) => {
1494
+ const r = await fetch('/api/backend/invoke', {
1495
+ method: 'POST',
1496
+ headers: { 'content-type': 'application/json' },
1497
+ body: JSON.stringify({ method, params }),
1498
+ });
1499
+ const j = await r.json();
1500
+ if (j?.ok === false) throw new Error(j?.message || 'invoke failed');
1501
+ return j?.result;
1502
+ },
1503
+ },
1504
+ uiPrompts: {
1505
+ read: async () => ({ path: '(sandbox)', entries: [...entries] }),
1506
+ onUpdate: (listener) => { listeners.add(listener); return () => listeners.delete(listener); },
1507
+ request: async (payload) => {
1508
+ const requestId = payload?.requestId ? String(payload.requestId) : uuid();
1509
+ const prompt = payload?.prompt && typeof payload.prompt === 'object' ? { ...payload.prompt } : null;
1510
+ if (prompt && !prompt.source) prompt.source = __SANDBOX__.pluginId + ':' + __SANDBOX__.appId;
1511
+ entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'request', requestId, runId: payload?.runId, prompt });
1512
+ emitUpdate();
1513
+ return { ok: true, requestId };
1514
+ },
1515
+ respond: async (payload) => {
1516
+ const requestId = String(payload?.requestId || '');
1517
+ if (!requestId) throw new Error('requestId is required');
1518
+ const response = payload?.response && typeof payload.response === 'object' ? payload.response : null;
1519
+ entries.push({ ts: new Date().toISOString(), type: 'ui_prompt', action: 'response', requestId, runId: payload?.runId, response });
1520
+ emitUpdate();
1521
+ return { ok: true };
1522
+ },
1523
+ open: () => (setPanelOpen(true), { ok: true }),
1524
+ close: () => (setPanelOpen(false), { ok: true }),
1525
+ toggle: () => (setPanelOpen(panel.style.display !== 'flex'), { ok: true }),
1526
+ },
1527
+ ui: { navigate: (menu) => ({ ok: true, menu }) },
1528
+ chat: (() => {
1529
+ const clone = (v) => JSON.parse(JSON.stringify(v));
1530
+
1531
+ const agents = [
1532
+ {
1533
+ id: 'sandbox-agent',
1534
+ name: 'Sandbox Agent',
1535
+ description: 'Mock agent for ChatOS UI Apps Sandbox',
1536
+ },
1537
+ ];
1538
+
1539
+ const sessions = new Map();
1540
+ const defaultSessionByAgent = new Map();
1541
+ const messagesBySession = new Map();
1542
+
1543
+ const listeners = new Set();
1544
+ const activeRuns = new Map(); // sessionId -> { aborted: boolean, timers: number[] }
1545
+
1546
+ const emit = (payload) => {
1547
+ for (const sub of listeners) {
1548
+ const filter = sub?.filter && typeof sub.filter === 'object' ? sub.filter : {};
1549
+ if (filter?.sessionId && String(filter.sessionId) !== String(payload?.sessionId || '')) continue;
1550
+ if (Array.isArray(filter?.types) && filter.types.length > 0) {
1551
+ const t = String(payload?.type || '');
1552
+ if (!filter.types.includes(t)) continue;
1553
+ }
1554
+ try {
1555
+ sub.fn(payload);
1556
+ } catch {
1557
+ // ignore
1558
+ }
1559
+ }
1560
+ };
1561
+
1562
+ const ensureAgent = async () => {
1563
+ if (agents.length > 0) return agents[0];
1564
+ const created = { id: 'sandbox-agent', name: 'Sandbox Agent', description: 'Mock agent' };
1565
+ agents.push(created);
1566
+ return created;
1567
+ };
1568
+
1569
+ const ensureSession = async (agentId) => {
1570
+ const aid = String(agentId || '').trim() || (await ensureAgent()).id;
1571
+ const existingId = defaultSessionByAgent.get(aid);
1572
+ if (existingId && sessions.has(existingId)) return sessions.get(existingId);
1573
+
1574
+ const id = 'sandbox-session-' + uuid();
1575
+ const session = { id, agentId: aid, createdAt: new Date().toISOString() };
1576
+ sessions.set(id, session);
1577
+ defaultSessionByAgent.set(aid, id);
1578
+ if (!messagesBySession.has(id)) messagesBySession.set(id, []);
1579
+ return session;
1580
+ };
1581
+
1582
+ const agentsApi = {
1583
+ list: async () => ({ ok: true, agents: clone(agents) }),
1584
+ ensureDefault: async () => ({ ok: true, agent: clone(await ensureAgent()) }),
1408
1585
  create: async (payload) => {
1409
1586
  const agent = {
1410
1587
  id: 'sandbox-agent-' + uuid(),
@@ -1416,8 +1593,8 @@ const host = {
1416
1593
  return { ok: true, agent: clone(agent) };
1417
1594
  },
1418
1595
  update: async (id, patch) => {
1419
- const agentId = String(id || '').trim();
1420
- if (!agentId) throw new Error('id is required');
1596
+ const agentId = String(id || '').trim();
1597
+ if (!agentId) throw new Error('id is required');
1421
1598
  const idx = agents.findIndex((a) => a.id === agentId);
1422
1599
  if (idx < 0) throw new Error('agent not found');
1423
1600
  const a = agents[idx];
@@ -1426,48 +1603,48 @@ const host = {
1426
1603
  if (patch?.modelId) a.modelId = String(patch.modelId);
1427
1604
  return { ok: true, agent: clone(a) };
1428
1605
  },
1429
- delete: async (id) => {
1430
- const agentId = String(id || '').trim();
1431
- if (!agentId) throw new Error('id is required');
1432
- const idx = agents.findIndex((a) => a.id === agentId);
1433
- if (idx < 0) return { ok: true, deleted: false };
1434
- agents.splice(idx, 1);
1435
- return { ok: true, deleted: true };
1436
- },
1437
- createForApp: async (payload) => {
1438
- const name = payload?.name ? String(payload.name) : 'App Agent (' + __SANDBOX__.appId + ')';
1439
- return await agentsApi.create({ ...payload, name });
1440
- },
1441
- };
1442
-
1443
- const sessionsApi = {
1444
- list: async () => ({ ok: true, sessions: clone(Array.from(sessions.values())) }),
1445
- ensureDefault: async (payload) => {
1446
- const session = await ensureSession(payload?.agentId);
1447
- return { ok: true, session: clone(session) };
1448
- },
1449
- create: async (payload) => {
1450
- const agentId = payload?.agentId ? String(payload.agentId) : (await ensureAgent()).id;
1451
- const id = 'sandbox-session-' + uuid();
1452
- const session = { id, agentId, createdAt: new Date().toISOString() };
1453
- sessions.set(id, session);
1454
- if (!messagesBySession.has(id)) messagesBySession.set(id, []);
1455
- return { ok: true, session: clone(session) };
1456
- },
1457
- };
1458
-
1459
- const messagesApi = {
1460
- list: async (payload) => {
1461
- const sessionId = String(payload?.sessionId || '').trim();
1462
- if (!sessionId) throw new Error('sessionId is required');
1463
- const msgs = messagesBySession.get(sessionId) || [];
1464
- return { ok: true, messages: clone(msgs) };
1465
- },
1466
- };
1467
-
1468
- const abort = async (payload) => {
1469
- const sessionId = String(payload?.sessionId || '').trim();
1470
- if (!sessionId) throw new Error('sessionId is required');
1606
+ delete: async (id) => {
1607
+ const agentId = String(id || '').trim();
1608
+ if (!agentId) throw new Error('id is required');
1609
+ const idx = agents.findIndex((a) => a.id === agentId);
1610
+ if (idx < 0) return { ok: true, deleted: false };
1611
+ agents.splice(idx, 1);
1612
+ return { ok: true, deleted: true };
1613
+ },
1614
+ createForApp: async (payload) => {
1615
+ const name = payload?.name ? String(payload.name) : 'App Agent (' + __SANDBOX__.appId + ')';
1616
+ return await agentsApi.create({ ...payload, name });
1617
+ },
1618
+ };
1619
+
1620
+ const sessionsApi = {
1621
+ list: async () => ({ ok: true, sessions: clone(Array.from(sessions.values())) }),
1622
+ ensureDefault: async (payload) => {
1623
+ const session = await ensureSession(payload?.agentId);
1624
+ return { ok: true, session: clone(session) };
1625
+ },
1626
+ create: async (payload) => {
1627
+ const agentId = payload?.agentId ? String(payload.agentId) : (await ensureAgent()).id;
1628
+ const id = 'sandbox-session-' + uuid();
1629
+ const session = { id, agentId, createdAt: new Date().toISOString() };
1630
+ sessions.set(id, session);
1631
+ if (!messagesBySession.has(id)) messagesBySession.set(id, []);
1632
+ return { ok: true, session: clone(session) };
1633
+ },
1634
+ };
1635
+
1636
+ const messagesApi = {
1637
+ list: async (payload) => {
1638
+ const sessionId = String(payload?.sessionId || '').trim();
1639
+ if (!sessionId) throw new Error('sessionId is required');
1640
+ const msgs = messagesBySession.get(sessionId) || [];
1641
+ return { ok: true, messages: clone(msgs) };
1642
+ },
1643
+ };
1644
+
1645
+ const abort = async (payload) => {
1646
+ const sessionId = String(payload?.sessionId || '').trim();
1647
+ if (!sessionId) throw new Error('sessionId is required');
1471
1648
  const run = activeRuns.get(sessionId);
1472
1649
  if (run) {
1473
1650
  run.aborted = true;
@@ -1483,28 +1660,28 @@ const host = {
1483
1660
  clearTimeout(t);
1484
1661
  } catch {
1485
1662
  // ignore
1486
- }
1487
- }
1488
- activeRuns.delete(sessionId);
1489
- }
1490
- emit({ type: 'assistant_abort', sessionId, ts: new Date().toISOString() });
1491
- return { ok: true };
1492
- };
1493
-
1663
+ }
1664
+ }
1665
+ activeRuns.delete(sessionId);
1666
+ }
1667
+ emit({ type: 'assistant_abort', sessionId, ts: new Date().toISOString() });
1668
+ return { ok: true };
1669
+ };
1670
+
1494
1671
  const send = async (payload) => {
1495
1672
  const sessionId = String(payload?.sessionId || '').trim();
1496
1673
  const text = String(payload?.text || '').trim();
1497
1674
  if (!sessionId) throw new Error('sessionId is required');
1498
1675
  if (!text) throw new Error('text is required');
1499
-
1500
- if (!sessions.has(sessionId)) throw new Error('session not found');
1501
-
1502
- const msgs = messagesBySession.get(sessionId) || [];
1503
- const userMsg = { id: 'msg-' + uuid(), role: 'user', text, ts: new Date().toISOString() };
1504
- msgs.push(userMsg);
1505
- messagesBySession.set(sessionId, msgs);
1506
- emit({ type: 'user_message', sessionId, message: clone(userMsg) });
1507
-
1676
+
1677
+ if (!sessions.has(sessionId)) throw new Error('session not found');
1678
+
1679
+ const msgs = messagesBySession.get(sessionId) || [];
1680
+ const userMsg = { id: 'msg-' + uuid(), role: 'user', text, ts: new Date().toISOString() };
1681
+ msgs.push(userMsg);
1682
+ messagesBySession.set(sessionId, msgs);
1683
+ emit({ type: 'user_message', sessionId, message: clone(userMsg) });
1684
+
1508
1685
  const assistantMsg = { id: 'msg-' + uuid(), role: 'assistant', text: '', ts: new Date().toISOString() };
1509
1686
  msgs.push(assistantMsg);
1510
1687
  emit({ type: 'assistant_start', sessionId, message: clone(assistantMsg) });
@@ -1580,100 +1757,100 @@ const host = {
1580
1757
 
1581
1758
  return { ok: true };
1582
1759
  };
1583
-
1584
- const events = {
1585
- subscribe: (filter, fn) => {
1586
- if (typeof fn !== 'function') throw new Error('listener is required');
1587
- const sub = { filter: filter && typeof filter === 'object' ? { ...filter } : {}, fn };
1588
- listeners.add(sub);
1589
- return () => listeners.delete(sub);
1590
- },
1591
- unsubscribe: () => (listeners.clear(), { ok: true }),
1592
- };
1593
-
1594
- return {
1595
- agents: agentsApi,
1596
- sessions: sessionsApi,
1597
- messages: messagesApi,
1598
- send,
1599
- abort,
1600
- events,
1601
- };
1602
- })(),
1603
- };
1604
-
1605
- inspectorEnabled = true;
1606
- updateInspector();
1607
-
1608
- let dispose = null;
1609
-
1610
- async function loadAndMount() {
1611
- if (typeof dispose === 'function') { try { await dispose(); } catch {} dispose = null; }
1612
- container.textContent = '';
1613
-
1614
- const entryUrl = __SANDBOX__.entryUrl;
1615
- const mod = await import(entryUrl + (entryUrl.includes('?') ? '&' : '?') + 't=' + Date.now());
1616
- const mount = mod?.mount || mod?.default?.mount || (typeof mod?.default === 'function' ? mod.default : null);
1617
- if (typeof mount !== 'function') throw new Error('module entry must export mount()');
1618
- const ret = await mount({ container, host, slots: { header: headerSlot } });
1619
- if (typeof ret === 'function') dispose = ret;
1620
- else if (ret && typeof ret.dispose === 'function') dispose = () => ret.dispose();
1621
- }
1622
-
1623
- const renderError = (e) => {
1624
- const pre = document.createElement('pre');
1625
- pre.style.padding = '12px';
1626
- pre.style.whiteSpace = 'pre-wrap';
1627
- pre.textContent = '[sandbox] ' + (e?.stack || e?.message || String(e));
1628
- container.appendChild(pre);
1629
- };
1630
-
1631
- const scheduleReload = (() => {
1632
- let t = null;
1633
- return () => {
1634
- if (t) return;
1635
- t = setTimeout(() => {
1636
- t = null;
1637
- loadAndMount().catch(renderError);
1638
- }, 80);
1639
- };
1640
- })();
1641
-
1642
- try {
1643
- const es = new EventSource('/events');
1644
- es.addEventListener('reload', () => scheduleReload());
1645
- } catch {
1646
- // ignore
1647
- }
1648
-
1649
- $('#btnReload').addEventListener('click', () => loadAndMount().catch(renderError));
1650
-
1651
- loadAndMount().catch(renderError);
1652
- `;
1653
- }
1654
-
1655
- async function loadBackendFactory({ pluginDir, manifest }) {
1656
- const entryRel = manifest?.backend?.entry ? String(manifest.backend.entry).trim() : '';
1657
- if (!entryRel) return null;
1658
- const abs = resolveInsideDir(pluginDir, entryRel);
1659
- const fileUrl = url.pathToFileURL(abs).toString();
1660
- const mod = await import(fileUrl + `?t=${Date.now()}`);
1661
- if (typeof mod?.createUiAppsBackend !== 'function') {
1662
- throw new Error('backend entry must export createUiAppsBackend(ctx)');
1663
- }
1664
- return mod.createUiAppsBackend;
1665
- }
1666
-
1667
- export async function startSandboxServer({ pluginDir, port = 4399, appId = '' }) {
1668
- const { manifest } = loadPluginManifest(pluginDir);
1669
- const app = pickAppFromManifest(manifest, appId);
1670
- const effectiveAppId = String(app?.id || '');
1671
- const entryRel = String(app?.entry?.path || '').trim();
1672
- if (!entryRel) throw new Error('apps[i].entry.path is required');
1673
-
1674
- const entryAbs = resolveInsideDir(pluginDir, entryRel);
1675
- if (!isFile(entryAbs)) throw new Error(`module entry not found: ${entryRel}`);
1676
-
1760
+
1761
+ const events = {
1762
+ subscribe: (filter, fn) => {
1763
+ if (typeof fn !== 'function') throw new Error('listener is required');
1764
+ const sub = { filter: filter && typeof filter === 'object' ? { ...filter } : {}, fn };
1765
+ listeners.add(sub);
1766
+ return () => listeners.delete(sub);
1767
+ },
1768
+ unsubscribe: () => (listeners.clear(), { ok: true }),
1769
+ };
1770
+
1771
+ return {
1772
+ agents: agentsApi,
1773
+ sessions: sessionsApi,
1774
+ messages: messagesApi,
1775
+ send,
1776
+ abort,
1777
+ events,
1778
+ };
1779
+ })(),
1780
+ };
1781
+
1782
+ inspectorEnabled = true;
1783
+ updateInspector();
1784
+
1785
+ let dispose = null;
1786
+
1787
+ async function loadAndMount() {
1788
+ if (typeof dispose === 'function') { try { await dispose(); } catch {} dispose = null; }
1789
+ container.textContent = '';
1790
+
1791
+ const entryUrl = __SANDBOX__.entryUrl;
1792
+ const mod = await import(entryUrl + (entryUrl.includes('?') ? '&' : '?') + 't=' + Date.now());
1793
+ const mount = mod?.mount || mod?.default?.mount || (typeof mod?.default === 'function' ? mod.default : null);
1794
+ if (typeof mount !== 'function') throw new Error('module entry must export mount()');
1795
+ const ret = await mount({ container, host, slots: { header: headerSlot } });
1796
+ if (typeof ret === 'function') dispose = ret;
1797
+ else if (ret && typeof ret.dispose === 'function') dispose = () => ret.dispose();
1798
+ }
1799
+
1800
+ const renderError = (e) => {
1801
+ const pre = document.createElement('pre');
1802
+ pre.style.padding = '12px';
1803
+ pre.style.whiteSpace = 'pre-wrap';
1804
+ pre.textContent = '[sandbox] ' + (e?.stack || e?.message || String(e));
1805
+ container.appendChild(pre);
1806
+ };
1807
+
1808
+ const scheduleReload = (() => {
1809
+ let t = null;
1810
+ return () => {
1811
+ if (t) return;
1812
+ t = setTimeout(() => {
1813
+ t = null;
1814
+ loadAndMount().catch(renderError);
1815
+ }, 80);
1816
+ };
1817
+ })();
1818
+
1819
+ try {
1820
+ const es = new EventSource('/events');
1821
+ es.addEventListener('reload', () => scheduleReload());
1822
+ } catch {
1823
+ // ignore
1824
+ }
1825
+
1826
+ $('#btnReload').addEventListener('click', () => loadAndMount().catch(renderError));
1827
+
1828
+ loadAndMount().catch(renderError);
1829
+ `;
1830
+ }
1831
+
1832
+ async function loadBackendFactory({ pluginDir, manifest }) {
1833
+ const entryRel = manifest?.backend?.entry ? String(manifest.backend.entry).trim() : '';
1834
+ if (!entryRel) return null;
1835
+ const abs = resolveInsideDir(pluginDir, entryRel);
1836
+ const fileUrl = url.pathToFileURL(abs).toString();
1837
+ const mod = await import(fileUrl + `?t=${Date.now()}`);
1838
+ if (typeof mod?.createUiAppsBackend !== 'function') {
1839
+ throw new Error('backend entry must export createUiAppsBackend(ctx)');
1840
+ }
1841
+ return mod.createUiAppsBackend;
1842
+ }
1843
+
1844
+ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' }) {
1845
+ const { manifest } = loadPluginManifest(pluginDir);
1846
+ const app = pickAppFromManifest(manifest, appId);
1847
+ const effectiveAppId = String(app?.id || '');
1848
+ const entryRel = String(app?.entry?.path || '').trim();
1849
+ if (!entryRel) throw new Error('apps[i].entry.path is required');
1850
+
1851
+ const entryAbs = resolveInsideDir(pluginDir, entryRel);
1852
+ if (!isFile(entryAbs)) throw new Error(`module entry not found: ${entryRel}`);
1853
+
1677
1854
  const entryUrl = `/plugin/${encodeURIComponent(entryRel).replaceAll('%2F', '/')}`;
1678
1855
 
1679
1856
  let backendInstance = null;
@@ -1760,15 +1937,12 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
1760
1937
  if (Object.prototype.hasOwnProperty.call(patch, 'modelId')) {
1761
1938
  next.modelId = normalizeText(patch.modelId);
1762
1939
  }
1763
- if (Object.prototype.hasOwnProperty.call(patch, 'workdir')) {
1764
- next.workdir = normalizeText(patch.workdir);
1765
- }
1766
1940
  sandboxLlmConfig = next;
1767
1941
  saveSandboxLlmConfig(sandboxConfigPath, next);
1768
1942
  return { ...next };
1769
1943
  };
1770
1944
 
1771
- const runSandboxChat = async ({ messages, modelId, modelName, systemPrompt, disableTools, signal } = {}) => {
1945
+ const runSandboxChat = async ({ messages, modelId, modelName, systemPrompt, disableTools, signal, callMeta } = {}) => {
1772
1946
  const cfg = getSandboxLlmConfig();
1773
1947
  const apiKey = normalizeText(cfg.apiKey || process.env.SANDBOX_LLM_API_KEY);
1774
1948
  const baseUrl = normalizeText(cfg.baseUrl) || DEFAULT_LLM_BASE_URL;
@@ -1795,12 +1969,16 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
1795
1969
 
1796
1970
  let toolEntries = [];
1797
1971
  let toolMap = new Map();
1972
+ let effectiveCallMeta = sandboxCallMeta;
1798
1973
  if (!disableTools) {
1799
1974
  const runtime = await ensureMcpRuntime();
1800
1975
  if (runtime?.toolEntries?.length) {
1801
1976
  toolEntries = runtime.toolEntries;
1802
1977
  toolMap = runtime.toolMap || new Map();
1803
1978
  }
1979
+ if (callMeta && typeof callMeta === 'object') {
1980
+ effectiveCallMeta = mergeCallMeta(sandboxCallMeta, callMeta);
1981
+ }
1804
1982
  }
1805
1983
  const toolDefs = toolEntries.map((entry) => entry.definition);
1806
1984
 
@@ -1843,7 +2021,7 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
1843
2021
  const toolResult = await toolEntry.client.callTool({
1844
2022
  name: toolEntry.toolName,
1845
2023
  arguments: args,
1846
- ...(sandboxCallMeta ? { _meta: sandboxCallMeta } : {}),
2024
+ ...(effectiveCallMeta ? { _meta: effectiveCallMeta } : {}),
1847
2025
  });
1848
2026
  resultText = formatMcpToolResult(toolEntry.serverName, toolEntry.toolName, toolResult);
1849
2027
  }
@@ -1864,7 +2042,7 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
1864
2042
  pluginId: String(manifest?.id || ''),
1865
2043
  pluginDir,
1866
2044
  stateDir: path.join(sandboxRoot, 'state', 'chatos'),
1867
- sessionRoot: process.cwd(),
2045
+ sessionRoot: process.cwd(),
1868
2046
  projectRoot: process.cwd(),
1869
2047
  dataDir: '',
1870
2048
  llm: {
@@ -1872,12 +2050,14 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
1872
2050
  const input = typeof payload?.input === 'string' ? payload.input : '';
1873
2051
  const normalized = String(input || '').trim();
1874
2052
  if (!normalized) throw new Error('input is required');
2053
+ const callMeta = payload?.callMeta && typeof payload.callMeta === 'object' ? payload.callMeta : null;
1875
2054
  const result = await runSandboxChat({
1876
2055
  messages: [{ role: 'user', text: normalized }],
1877
2056
  modelId: typeof payload?.modelId === 'string' ? payload.modelId : '',
1878
2057
  modelName: typeof payload?.modelName === 'string' ? payload.modelName : '',
1879
2058
  systemPrompt: typeof payload?.systemPrompt === 'string' ? payload.systemPrompt : '',
1880
2059
  disableTools: payload?.disableTools === true,
2060
+ callMeta,
1881
2061
  });
1882
2062
  return {
1883
2063
  ok: true,
@@ -1893,7 +2073,6 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
1893
2073
  ensureDir(ctxBase.dataDir);
1894
2074
  sandboxCallMeta = buildSandboxCallMeta({
1895
2075
  rawCallMeta: app?.ai?.mcp?.callMeta,
1896
- rawWorkdir: getSandboxLlmConfig().workdir,
1897
2076
  context: {
1898
2077
  pluginId: ctxBase.pluginId,
1899
2078
  appId: effectiveAppId,
@@ -1904,30 +2083,30 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
1904
2083
  projectRoot: ctxBase.projectRoot,
1905
2084
  },
1906
2085
  });
1907
-
1908
- const sseClients = new Set();
1909
- const sseWrite = (res, event, data) => {
1910
- try {
1911
- res.write(`event: ${event}\n`);
1912
- res.write(`data: ${JSON.stringify(data ?? null)}\n\n`);
1913
- } catch {
1914
- // ignore
1915
- }
1916
- };
1917
- const sseBroadcast = (event, data) => {
1918
- for (const res of sseClients) {
1919
- sseWrite(res, event, data);
1920
- }
1921
- };
1922
-
1923
- let changeSeq = 0;
1924
- const stopWatch = startRecursiveWatcher(pluginDir, ({ eventType, filePath }) => {
1925
- const rel = filePath ? path.relative(pluginDir, filePath).replaceAll('\\', '/') : '';
1926
- const base = rel ? path.basename(rel) : '';
1927
- if (!rel) return;
1928
- if (base === '.DS_Store') return;
1929
- if (base.endsWith('.map')) return;
1930
-
2086
+
2087
+ const sseClients = new Set();
2088
+ const sseWrite = (res, event, data) => {
2089
+ try {
2090
+ res.write(`event: ${event}\n`);
2091
+ res.write(`data: ${JSON.stringify(data ?? null)}\n\n`);
2092
+ } catch {
2093
+ // ignore
2094
+ }
2095
+ };
2096
+ const sseBroadcast = (event, data) => {
2097
+ for (const res of sseClients) {
2098
+ sseWrite(res, event, data);
2099
+ }
2100
+ };
2101
+
2102
+ let changeSeq = 0;
2103
+ const stopWatch = startRecursiveWatcher(pluginDir, ({ eventType, filePath }) => {
2104
+ const rel = filePath ? path.relative(pluginDir, filePath).replaceAll('\\', '/') : '';
2105
+ const base = rel ? path.basename(rel) : '';
2106
+ if (!rel) return;
2107
+ if (base === '.DS_Store') return;
2108
+ if (base.endsWith('.map')) return;
2109
+
1931
2110
  changeSeq += 1;
1932
2111
  if (rel.startsWith('backend/')) {
1933
2112
  backendInstance = null;
@@ -1936,71 +2115,69 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
1936
2115
  resetMcpRuntime().catch(() => {});
1937
2116
  sseBroadcast('reload', { seq: changeSeq, eventType: eventType || '', path: rel });
1938
2117
  });
1939
-
1940
- const server = http.createServer(async (req, res) => {
1941
- try {
1942
- const parsed = url.parse(req.url || '/', true);
1943
- const pathname = parsed.pathname || '/';
1944
-
1945
- if (req.method === 'GET' && pathname === '/') {
1946
- return sendText(res, 200, htmlPage(), 'text/html; charset=utf-8');
1947
- }
1948
-
1949
- if (req.method === 'GET' && pathname === '/events') {
1950
- res.writeHead(200, {
1951
- 'content-type': 'text/event-stream; charset=utf-8',
1952
- 'cache-control': 'no-store',
1953
- connection: 'keep-alive',
1954
- });
1955
- res.write(': connected\n\n');
1956
- sseClients.add(res);
1957
- const ping = setInterval(() => {
1958
- try {
1959
- res.write(': ping\n\n');
1960
- } catch {
1961
- // ignore
1962
- }
1963
- }, 15000);
1964
- req.on('close', () => {
1965
- try {
1966
- clearInterval(ping);
1967
- } catch {
1968
- // ignore
1969
- }
1970
- sseClients.delete(res);
1971
- });
1972
- return;
1973
- }
1974
-
2118
+
2119
+ const server = http.createServer(async (req, res) => {
2120
+ try {
2121
+ const parsed = url.parse(req.url || '/', true);
2122
+ const pathname = parsed.pathname || '/';
2123
+
2124
+ if (req.method === 'GET' && pathname === '/') {
2125
+ return sendText(res, 200, htmlPage(), 'text/html; charset=utf-8');
2126
+ }
2127
+
2128
+ if (req.method === 'GET' && pathname === '/events') {
2129
+ res.writeHead(200, {
2130
+ 'content-type': 'text/event-stream; charset=utf-8',
2131
+ 'cache-control': 'no-store',
2132
+ connection: 'keep-alive',
2133
+ });
2134
+ res.write(': connected\n\n');
2135
+ sseClients.add(res);
2136
+ const ping = setInterval(() => {
2137
+ try {
2138
+ res.write(': ping\n\n');
2139
+ } catch {
2140
+ // ignore
2141
+ }
2142
+ }, 15000);
2143
+ req.on('close', () => {
2144
+ try {
2145
+ clearInterval(ping);
2146
+ } catch {
2147
+ // ignore
2148
+ }
2149
+ sseClients.delete(res);
2150
+ });
2151
+ return;
2152
+ }
2153
+
1975
2154
  if (req.method === 'GET' && pathname === '/sandbox.mjs') {
1976
2155
  const tokenNames = loadTokenNames();
1977
- const sandboxContext = {
1978
- pluginId: ctxBase.pluginId,
1979
- appId: effectiveAppId,
1980
- pluginDir: ctxBase.pluginDir,
1981
- dataDir: ctxBase.dataDir,
1982
- stateDir: ctxBase.stateDir,
1983
- sessionRoot: ctxBase.sessionRoot,
1984
- projectRoot: ctxBase.projectRoot,
1985
- workdir: sandboxCallMeta?.workdir || ctxBase.dataDir || '',
2156
+ const sandboxPaths = {
2157
+ workdir: sandboxCallMeta?.workdir || '',
2158
+ dataDir: ctxBase.dataDir || '',
2159
+ pluginDir: ctxBase.pluginDir || '',
2160
+ stateDir: ctxBase.stateDir || '',
2161
+ sessionRoot: ctxBase.sessionRoot || '',
2162
+ projectRoot: ctxBase.projectRoot || '',
1986
2163
  };
1987
2164
  const js = sandboxClientJs()
1988
- .replaceAll('__SANDBOX__.context', JSON.stringify(sandboxContext))
1989
2165
  .replaceAll('__SANDBOX__.pluginId', JSON.stringify(ctxBase.pluginId))
1990
2166
  .replaceAll('__SANDBOX__.appId', JSON.stringify(effectiveAppId))
1991
2167
  .replaceAll('__SANDBOX__.entryUrl', JSON.stringify(entryUrl))
1992
2168
  .replaceAll('__SANDBOX__.registryApp', JSON.stringify({ plugin: { id: ctxBase.pluginId }, id: effectiveAppId, entry: { type: 'module', url: entryUrl } }))
1993
- .replaceAll('__SANDBOX__.tokenNames', JSON.stringify(tokenNames));
2169
+ .replaceAll('__SANDBOX__.tokenNames', JSON.stringify(tokenNames))
2170
+ .replaceAll('__SANDBOX__.paths', JSON.stringify(sandboxPaths));
1994
2171
  return sendText(res, 200, js, 'text/javascript; charset=utf-8');
1995
2172
  }
1996
-
1997
- if (req.method === 'GET' && pathname.startsWith('/plugin/')) {
1998
- const rel = decodeURIComponent(pathname.slice('/plugin/'.length));
1999
- const abs = resolveInsideDir(pluginDir, rel);
2000
- if (!serveStaticFile(res, abs)) return sendText(res, 404, 'Not found');
2001
- return;
2002
- }
2003
-
2173
+
2174
+ if (req.method === 'GET' && pathname.startsWith('/plugin/')) {
2175
+ const rel = decodeURIComponent(pathname.slice('/plugin/'.length));
2176
+ const abs = resolveInsideDir(pluginDir, rel);
2177
+ if (!serveStaticFile(res, abs)) return sendText(res, 404, 'Not found');
2178
+ return;
2179
+ }
2180
+
2004
2181
  if (req.method === 'GET' && pathname === '/api/manifest') {
2005
2182
  return sendJson(res, 200, { ok: true, manifest });
2006
2183
  }
@@ -2013,7 +2190,6 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
2013
2190
  config: {
2014
2191
  baseUrl: cfg.baseUrl || '',
2015
2192
  modelId: cfg.modelId || '',
2016
- workdir: cfg.workdir || '',
2017
2193
  hasApiKey: Boolean(cfg.apiKey),
2018
2194
  },
2019
2195
  });
@@ -2026,14 +2202,12 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
2026
2202
  ...(Object.prototype.hasOwnProperty.call(patch || {}, 'apiKey') ? { apiKey: patch.apiKey } : {}),
2027
2203
  ...(Object.prototype.hasOwnProperty.call(patch || {}, 'baseUrl') ? { baseUrl: patch.baseUrl } : {}),
2028
2204
  ...(Object.prototype.hasOwnProperty.call(patch || {}, 'modelId') ? { modelId: patch.modelId } : {}),
2029
- ...(Object.prototype.hasOwnProperty.call(patch || {}, 'workdir') ? { workdir: patch.workdir } : {}),
2030
2205
  });
2031
2206
  return sendJson(res, 200, {
2032
2207
  ok: true,
2033
2208
  config: {
2034
2209
  baseUrl: next.baseUrl || '',
2035
2210
  modelId: next.modelId || '',
2036
- workdir: next.workdir || '',
2037
2211
  hasApiKey: Boolean(next.apiKey),
2038
2212
  },
2039
2213
  });
@@ -2049,12 +2223,14 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
2049
2223
  try {
2050
2224
  const payload = await readJsonBody(req);
2051
2225
  const messages = Array.isArray(payload?.messages) ? payload.messages : [];
2226
+ const callMeta = payload?.callMeta && typeof payload.callMeta === 'object' ? payload.callMeta : null;
2052
2227
  const result = await runSandboxChat({
2053
2228
  messages,
2054
2229
  modelId: typeof payload?.modelId === 'string' ? payload.modelId : '',
2055
2230
  modelName: typeof payload?.modelName === 'string' ? payload.modelName : '',
2056
2231
  systemPrompt: typeof payload?.systemPrompt === 'string' ? payload.systemPrompt : '',
2057
2232
  disableTools: payload?.disableTools === true,
2233
+ callMeta,
2058
2234
  });
2059
2235
  return sendJson(res, 200, {
2060
2236
  ok: true,
@@ -2072,55 +2248,55 @@ export async function startSandboxServer({ pluginDir, port = 4399, appId = '' })
2072
2248
  let body = '';
2073
2249
  req.on('data', (chunk) => {
2074
2250
  body += chunk;
2075
- });
2076
- req.on('end', async () => {
2077
- try {
2078
- const payload = body ? JSON.parse(body) : {};
2079
- const method = typeof payload?.method === 'string' ? payload.method.trim() : '';
2080
- if (!method) return sendJson(res, 400, { ok: false, message: 'method is required' });
2081
- const params = payload?.params;
2082
-
2083
- if (!backendFactory) backendFactory = await loadBackendFactory({ pluginDir, manifest });
2084
- if (!backendFactory) return sendJson(res, 200, { ok: false, message: 'backend not configured in plugin.json' });
2085
-
2086
- if (!backendInstance || typeof backendInstance !== 'object' || !backendInstance.methods) {
2087
- backendInstance = await backendFactory({ ...ctxBase });
2088
- }
2089
- const fn = backendInstance?.methods?.[method];
2090
- if (typeof fn !== 'function') return sendJson(res, 404, { ok: false, message: `method not found: ${method}` });
2091
- const result = await fn(params, { ...ctxBase });
2092
- return sendJson(res, 200, { ok: true, result });
2093
- } catch (e) {
2094
- return sendJson(res, 200, { ok: false, message: e?.message || String(e) });
2095
- }
2096
- });
2097
- return;
2098
- }
2099
-
2100
- sendText(res, 404, 'Not found');
2101
- } catch (e) {
2102
- sendJson(res, 500, { ok: false, message: e?.message || String(e) });
2103
- }
2104
- });
2251
+ });
2252
+ req.on('end', async () => {
2253
+ try {
2254
+ const payload = body ? JSON.parse(body) : {};
2255
+ const method = typeof payload?.method === 'string' ? payload.method.trim() : '';
2256
+ if (!method) return sendJson(res, 400, { ok: false, message: 'method is required' });
2257
+ const params = payload?.params;
2258
+
2259
+ if (!backendFactory) backendFactory = await loadBackendFactory({ pluginDir, manifest });
2260
+ if (!backendFactory) return sendJson(res, 200, { ok: false, message: 'backend not configured in plugin.json' });
2261
+
2262
+ if (!backendInstance || typeof backendInstance !== 'object' || !backendInstance.methods) {
2263
+ backendInstance = await backendFactory({ ...ctxBase });
2264
+ }
2265
+ const fn = backendInstance?.methods?.[method];
2266
+ if (typeof fn !== 'function') return sendJson(res, 404, { ok: false, message: `method not found: ${method}` });
2267
+ const result = await fn(params, { ...ctxBase });
2268
+ return sendJson(res, 200, { ok: true, result });
2269
+ } catch (e) {
2270
+ return sendJson(res, 200, { ok: false, message: e?.message || String(e) });
2271
+ }
2272
+ });
2273
+ return;
2274
+ }
2275
+
2276
+ sendText(res, 404, 'Not found');
2277
+ } catch (e) {
2278
+ sendJson(res, 500, { ok: false, message: e?.message || String(e) });
2279
+ }
2280
+ });
2105
2281
  server.once('close', () => {
2106
2282
  stopWatch();
2107
2283
  resetMcpRuntime().catch(() => {});
2108
2284
  });
2109
-
2110
- await new Promise((resolve, reject) => {
2111
- server.once('error', reject);
2112
- server.listen(port, '127.0.0.1', () => {
2113
- server.off('error', reject);
2114
- resolve();
2115
- });
2116
- });
2117
-
2118
- // eslint-disable-next-line no-console
2119
- console.log(`Sandbox running:
2120
- http://localhost:${port}/
2121
- pluginDir:
2122
- ${pluginDir}
2123
- app:
2124
- ${ctxBase.pluginId}:${effectiveAppId}
2125
- `);
2126
- }
2285
+
2286
+ await new Promise((resolve, reject) => {
2287
+ server.once('error', reject);
2288
+ server.listen(port, '127.0.0.1', () => {
2289
+ server.off('error', reject);
2290
+ resolve();
2291
+ });
2292
+ });
2293
+
2294
+ // eslint-disable-next-line no-console
2295
+ console.log(`Sandbox running:
2296
+ http://localhost:${port}/
2297
+ pluginDir:
2298
+ ${pluginDir}
2299
+ app:
2300
+ ${ctxBase.pluginId}:${effectiveAppId}
2301
+ `);
2302
+ }