@xcanwin/manyoyo 5.2.15 → 5.2.23

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.
@@ -676,6 +676,7 @@ class PlaywrightPlugin {
676
676
  }
677
677
 
678
678
  return {
679
+ outputDir: '/tmp/playwright-mcp',
679
680
  server: {
680
681
  host: def.listenHost,
681
682
  port,
@@ -1339,13 +1340,18 @@ class PlaywrightPlugin {
1339
1340
  const scenes = this.resolveTargets('all');
1340
1341
  for (const sceneName of scenes) {
1341
1342
  const url = `http://${host}:${this.scenePort(sceneName)}/mcp`;
1342
- this.writeStdout(`claude mcp add --transport http -s user playwright-${sceneName} ${url}`);
1343
+ this.writeStdout(`claude mcp add -t http -s user playwright-${sceneName} ${url}`);
1343
1344
  }
1344
1345
  this.writeStdout('');
1345
1346
  for (const sceneName of scenes) {
1346
1347
  const url = `http://${host}:${this.scenePort(sceneName)}/mcp`;
1347
1348
  this.writeStdout(`codex mcp add playwright-${sceneName} --url ${url}`);
1348
1349
  }
1350
+ this.writeStdout('');
1351
+ for (const sceneName of scenes) {
1352
+ const url = `http://${host}:${this.scenePort(sceneName)}/mcp`;
1353
+ this.writeStdout(`gemini mcp add -t http -s user playwright-${sceneName} ${url}`);
1354
+ }
1349
1355
 
1350
1356
  return 0;
1351
1357
  }
@@ -558,7 +558,7 @@ textarea:focus-visible {
558
558
  min-height: 0;
559
559
  padding: 14px;
560
560
  display: grid;
561
- grid-template-rows: auto auto minmax(0, 1fr) auto;
561
+ grid-template-rows: auto minmax(0, 1fr) auto;
562
562
  gap: 0;
563
563
  background:
564
564
  linear-gradient(165deg, rgba(255, 251, 243, 0.95) 0%, rgba(247, 237, 223, 0.95) 100%);
@@ -631,42 +631,31 @@ body.mobile-actions-open .header-actions {
631
631
  display: grid;
632
632
  }
633
633
 
634
- .mode-switch {
635
- display: flex;
636
- justify-content: space-between;
637
- align-items: center;
638
- gap: 10px;
639
- margin: 10px 8px 4px;
640
- min-height: 36px;
641
- padding: 8px;
642
- border: 1px solid var(--line);
643
- border-radius: 12px;
644
- background: rgba(255, 251, 244, 0.9);
634
+ #modeToggle {
635
+ position: relative;
645
636
  }
646
637
 
647
- .mode-switch-left {
648
- display: inline-flex;
649
- align-items: center;
638
+ .mode-menu {
639
+ display: none;
640
+ position: absolute;
641
+ top: calc(100% + 6px);
642
+ right: 8px;
643
+ z-index: 8;
644
+ width: min(200px, calc(100vw - 36px));
645
+ padding: 8px;
646
+ border: 1px solid var(--line);
647
+ border-radius: 10px;
648
+ background: #fffaf2;
649
+ box-shadow: var(--shadow-strong);
650
+ grid-template-columns: 1fr;
650
651
  gap: 8px;
651
- min-width: 0;
652
- }
653
-
654
- .mode-switch button {
655
- min-width: 102px;
656
- color: var(--text);
657
- background: #f7eee1;
658
- border-color: #e0c6a5;
659
652
  }
660
653
 
661
- .mode-switch button.is-active {
662
- color: #ffffff;
663
- background: var(--accent);
664
- border-color: var(--accent-strong);
654
+ body.mode-menu-open .mode-menu {
655
+ display: grid;
665
656
  }
666
657
 
667
- body.command-mode #modeCommandBtn,
668
- body.agent-mode #modeAgentBtn,
669
- body.terminal-mode #modeTerminalBtn {
658
+ .mode-menu button.is-active {
670
659
  color: #ffffff;
671
660
  background: var(--accent);
672
661
  border-color: var(--accent-strong);
@@ -694,6 +683,39 @@ body.terminal-mode #modeTerminalBtn {
694
683
  margin: 4px 0 0;
695
684
  }
696
685
 
686
+ .term-keybar {
687
+ display: flex;
688
+ gap: 4px;
689
+ padding: 4px 6px;
690
+ background: #1a1a1a;
691
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
692
+ flex-shrink: 0;
693
+ }
694
+
695
+ .term-key-btn {
696
+ background: rgba(255, 255, 255, 0.07);
697
+ color: #bbb;
698
+ border: 1px solid rgba(255, 255, 255, 0.13);
699
+ border-radius: 4px;
700
+ padding: 2px 9px;
701
+ font-size: 12px;
702
+ font-family: monospace;
703
+ cursor: pointer;
704
+ user-select: none;
705
+ line-height: 1.6;
706
+ }
707
+
708
+ .term-key-btn:hover {
709
+ background: rgba(255, 255, 255, 0.14);
710
+ color: #eee;
711
+ }
712
+
713
+ .term-key-btn.is-active {
714
+ background: var(--accent);
715
+ color: #fff;
716
+ border-color: var(--accent-strong);
717
+ }
718
+
697
719
  #terminalScreen {
698
720
  flex: 1;
699
721
  min-height: 0;
@@ -1066,23 +1088,6 @@ body.command-mode .msg.origin-agent .bubble {
1066
1088
  display: none;
1067
1089
  }
1068
1090
 
1069
- .mode-switch {
1070
- margin: 8px 8px 2px;
1071
- flex-wrap: wrap;
1072
- padding: 8px;
1073
- gap: 8px;
1074
- }
1075
-
1076
- .mode-switch-left {
1077
- flex: 1 1 auto;
1078
- min-width: 0;
1079
- }
1080
-
1081
- .mode-switch button {
1082
- flex: 0 0 auto;
1083
- min-width: 84px;
1084
- }
1085
-
1086
1091
  #commandInput {
1087
1092
  min-height: 68px;
1088
1093
  max-height: 160px;
@@ -48,6 +48,13 @@
48
48
  aria-controls="sessionList"
49
49
  >会话</button>
50
50
  <h1 id="activeTitle">未选择会话</h1>
51
+ <button
52
+ type="button"
53
+ id="modeToggle"
54
+ class="secondary"
55
+ aria-expanded="false"
56
+ aria-controls="modeMenu"
57
+ >AGENT 模式</button>
51
58
  <button
52
59
  type="button"
53
60
  id="mobileActionsToggle"
@@ -58,22 +65,29 @@
58
65
  </div>
59
66
  <div id="activeMeta">请选择左侧会话</div>
60
67
  </div>
68
+ <div class="mode-menu" id="modeMenu">
69
+ <button type="button" id="modeAgentBtn" class="secondary is-active" aria-pressed="true">AGENT 模式</button>
70
+ <button type="button" id="modeCommandBtn" class="secondary" aria-pressed="false">命令模式</button>
71
+ <button type="button" id="modeTerminalBtn" class="secondary" aria-pressed="false">终端模式</button>
72
+ </div>
61
73
  <div class="header-actions" id="headerActions">
62
74
  <button type="button" id="refreshBtn" class="secondary">刷新</button>
63
75
  <button type="button" id="removeBtn" class="danger-outline">删除容器</button>
64
76
  <button type="button" id="removeAllBtn" class="danger">删除对话</button>
65
77
  </div>
66
78
  </header>
67
- <section class="mode-switch" id="modeSwitch">
68
- <div class="mode-switch-left">
69
- <button type="button" id="modeAgentBtn" class="secondary is-active">AGENT 模式</button>
70
- <button type="button" id="modeCommandBtn" class="secondary">命令模式</button>
71
- <button type="button" id="modeTerminalBtn" class="secondary">交互终端</button>
72
- </div>
73
-
74
- </section>
75
79
  <section id="messages"></section>
76
80
  <section id="terminalPanel" hidden>
81
+ <div id="terminalKeybar" class="term-keybar" aria-label="终端快捷键">
82
+ <button type="button" class="term-key-btn" data-key="esc">esc</button>
83
+ <button type="button" class="term-key-btn" data-key="tab">tab</button>
84
+ <button type="button" class="term-key-btn term-ctrl-btn" data-key="ctrl">ctrl</button>
85
+ <button type="button" class="term-key-btn term-alt-btn" data-key="alt">alt</button>&nbsp;&nbsp;
86
+ <button type="button" class="term-key-btn" data-key="left">&nbsp;◀&nbsp;</button>
87
+ <button type="button" class="term-key-btn" data-key="up">&nbsp;▲&nbsp;</button>
88
+ <button type="button" class="term-key-btn" data-key="down">&nbsp;▼&nbsp;</button>
89
+ <button type="button" class="term-key-btn" data-key="right">&nbsp;▶&nbsp;</button>
90
+ </div>
77
91
  <div id="terminalScreen" aria-label="终端输出区域"></div>
78
92
 
79
93
  </section>
@@ -46,6 +46,7 @@
46
46
  loadingMessages: false,
47
47
  mobileSidebarOpen: false,
48
48
  mobileActionsOpen: false,
49
+ modeMenuOpen: false,
49
50
  configModalOpen: false,
50
51
  createModalOpen: false,
51
52
  configLoading: false,
@@ -69,7 +70,9 @@
69
70
  terminalReady: false,
70
71
  fitTimer: null,
71
72
  lastSentCols: 0,
72
- lastSentRows: 0
73
+ lastSentRows: 0,
74
+ ctrlMode: false,
75
+ altMode: false
73
76
  }
74
77
  };
75
78
 
@@ -79,6 +82,8 @@
79
82
  const mobileSessionToggle = document.getElementById('mobileSessionToggle');
80
83
  const mobileActionsToggle = document.getElementById('mobileActionsToggle');
81
84
  const headerActions = document.getElementById('headerActions');
85
+ const modeToggle = document.getElementById('modeToggle');
86
+ const modeMenu = document.getElementById('modeMenu');
82
87
  const mobileSidebarClose = document.getElementById('mobileSidebarClose');
83
88
  const sidebarBackdrop = document.getElementById('sidebarBackdrop');
84
89
  const openConfigBtn = document.getElementById('openConfigBtn');
@@ -523,6 +528,11 @@
523
528
  }) || null;
524
529
  }
525
530
 
531
+ function isActiveSessionHistoryOnly() {
532
+ const session = getActiveSession();
533
+ return sessionStatusInfo(session && session.status).tone === 'history';
534
+ }
535
+
526
536
  function isComposerMode() {
527
537
  return state.mode === 'command' || state.mode === 'agent';
528
538
  }
@@ -686,9 +696,18 @@
686
696
  if (!data || !state.terminal.socket || state.terminal.socket.readyState !== window.WebSocket.OPEN) {
687
697
  return;
688
698
  }
699
+ let send = data;
700
+ if (state.terminal.ctrlMode && data.length === 1) {
701
+ const code = data.charCodeAt(0);
702
+ if ((code >= 65 && code <= 90) || (code >= 97 && code <= 122)) {
703
+ send = String.fromCharCode(code & 0x1f);
704
+ }
705
+ } else if (state.terminal.altMode && data.length === 1) {
706
+ send = '\x1b' + data;
707
+ }
689
708
  state.terminal.socket.send(JSON.stringify({
690
709
  type: 'input',
691
- data: data
710
+ data: send
692
711
  }));
693
712
  });
694
713
  state.terminal.term.onResize(function (size) {
@@ -768,6 +787,7 @@
768
787
  state.terminal.sessionName = sessionName;
769
788
  state.terminal.lastSentCols = 0;
770
789
  state.terminal.lastSentRows = 0;
790
+ state.terminal.term.reset();
771
791
  writeTerminalLine('[system] 正在连接终端...');
772
792
  syncUi();
773
793
 
@@ -878,6 +898,20 @@
878
898
  setMobileActionsMenu(false);
879
899
  }
880
900
 
901
+ const MODE_LABELS = { agent: 'AGENT 模式', command: '命令模式', terminal: '终端模式' };
902
+
903
+ function setModeMenu(open) {
904
+ state.modeMenuOpen = Boolean(open);
905
+ document.body.classList.toggle('mode-menu-open', state.modeMenuOpen);
906
+ if (modeToggle) {
907
+ modeToggle.setAttribute('aria-expanded', state.modeMenuOpen ? 'true' : 'false');
908
+ }
909
+ }
910
+
911
+ function closeModeMenu() {
912
+ setModeMenu(false);
913
+ }
914
+
881
915
  function syncUi() {
882
916
  if (!state.active) {
883
917
  activeTitle.textContent = '未选择会话';
@@ -911,6 +945,9 @@
911
945
  modeTerminalBtn.classList.toggle('is-active', terminalMode);
912
946
  modeTerminalBtn.setAttribute('aria-pressed', terminalMode ? 'true' : 'false');
913
947
  }
948
+ if (modeToggle) {
949
+ modeToggle.textContent = MODE_LABELS[state.mode] || '模式';
950
+ }
914
951
  if (terminalPanel) {
915
952
  terminalPanel.hidden = !terminalMode;
916
953
  }
@@ -1164,7 +1201,11 @@
1164
1201
  if (state.mode === 'terminal' && ensureTerminalReady()) {
1165
1202
  scheduleTerminalFit(false);
1166
1203
  if (!state.terminal.connected && !state.terminal.connecting) {
1167
- connectTerminal();
1204
+ if (isActiveSessionHistoryOnly()) {
1205
+ state.terminal.term.reset();
1206
+ } else {
1207
+ connectTerminal();
1208
+ }
1168
1209
  }
1169
1210
  }
1170
1211
  renderSessionActiveState();
@@ -1497,7 +1538,7 @@
1497
1538
  throw requestError;
1498
1539
  }
1499
1540
 
1500
- if (state.mode === 'terminal' && ensureTerminalReady() && !state.terminal.connected && !state.terminal.connecting) {
1541
+ if (state.mode === 'terminal' && ensureTerminalReady() && !state.terminal.connected && !state.terminal.connecting && !isActiveSessionHistoryOnly()) {
1501
1542
  scheduleTerminalFit(false);
1502
1543
  connectTerminal();
1503
1544
  }
@@ -1825,8 +1866,16 @@
1825
1866
  composer.requestSubmit();
1826
1867
  });
1827
1868
 
1869
+ if (modeToggle) {
1870
+ modeToggle.addEventListener('click', function () {
1871
+ closeMobileActionsMenu();
1872
+ setModeMenu(!state.modeMenuOpen);
1873
+ });
1874
+ }
1875
+
1828
1876
  if (modeCommandBtn) {
1829
1877
  modeCommandBtn.addEventListener('click', function () {
1878
+ closeModeMenu();
1830
1879
  state.mode = 'command';
1831
1880
  renderMessages(state.messages, { forceFullRender: true });
1832
1881
  syncUi();
@@ -1836,6 +1885,7 @@
1836
1885
 
1837
1886
  if (modeAgentBtn) {
1838
1887
  modeAgentBtn.addEventListener('click', function () {
1888
+ closeModeMenu();
1839
1889
  state.mode = 'agent';
1840
1890
  renderMessages(state.messages, { forceFullRender: true });
1841
1891
  syncUi();
@@ -1845,13 +1895,14 @@
1845
1895
 
1846
1896
  if (modeTerminalBtn) {
1847
1897
  modeTerminalBtn.addEventListener('click', function () {
1898
+ closeModeMenu();
1848
1899
  state.mode = 'terminal';
1849
1900
  renderMessages(state.messages, { forceFullRender: true });
1850
1901
  syncUi();
1851
1902
  if (ensureTerminalReady()) {
1852
1903
  scheduleTerminalFit(false);
1853
1904
  state.terminal.term.focus();
1854
- if (!state.terminal.connected && !state.terminal.connecting) {
1905
+ if (!state.terminal.connected && !state.terminal.connecting && !isActiveSessionHistoryOnly()) {
1855
1906
  connectTerminal();
1856
1907
  }
1857
1908
  }
@@ -1863,6 +1914,41 @@
1863
1914
  loadSessions(state.active).catch(function (e) { alert(e.message); });
1864
1915
  });
1865
1916
 
1917
+ const TERM_KEY_SEQUENCES = {
1918
+ esc: '\x1b',
1919
+ tab: '\x09',
1920
+ up: '\x1b[A',
1921
+ down: '\x1b[B',
1922
+ left: '\x1b[D',
1923
+ right: '\x1b[C'
1924
+ };
1925
+ const termKeybar = document.getElementById('terminalKeybar');
1926
+ if (termKeybar) {
1927
+ termKeybar.addEventListener('click', function (e) {
1928
+ const btn = e.target.closest('[data-key]');
1929
+ if (!btn) return;
1930
+ const key = btn.dataset.key;
1931
+ if (key === 'ctrl') {
1932
+ state.terminal.ctrlMode = !state.terminal.ctrlMode;
1933
+ btn.classList.toggle('is-active', state.terminal.ctrlMode);
1934
+ if (state.terminal.term) state.terminal.term.focus();
1935
+ return;
1936
+ }
1937
+ if (key === 'alt') {
1938
+ state.terminal.altMode = !state.terminal.altMode;
1939
+ btn.classList.toggle('is-active', state.terminal.altMode);
1940
+ if (state.terminal.term) state.terminal.term.focus();
1941
+ return;
1942
+ }
1943
+ const seq = TERM_KEY_SEQUENCES[key];
1944
+ if (!seq) return;
1945
+ if (state.terminal.socket && state.terminal.socket.readyState === window.WebSocket.OPEN) {
1946
+ state.terminal.socket.send(JSON.stringify({ type: 'input', data: seq }));
1947
+ }
1948
+ if (state.terminal.term) state.terminal.term.focus();
1949
+ });
1950
+ }
1951
+
1866
1952
  if (mobileSessionToggle) {
1867
1953
  mobileSessionToggle.addEventListener('click', function () {
1868
1954
  setMobileSessionPanel(!state.mobileSidebarOpen);
@@ -1920,6 +2006,9 @@
1920
2006
  if (event.key === 'Escape' && state.mobileActionsOpen) {
1921
2007
  closeMobileActionsMenu();
1922
2008
  }
2009
+ if (event.key === 'Escape' && state.modeMenuOpen) {
2010
+ closeModeMenu();
2011
+ }
1923
2012
  });
1924
2013
 
1925
2014
  function onLayoutMediaChange() {
@@ -1949,11 +2038,17 @@
1949
2038
  }
1950
2039
 
1951
2040
  document.addEventListener('click', function (event) {
1952
- if (!state.mobileActionsOpen) return;
1953
2041
  const target = event.target;
1954
- if (mobileActionsToggle && mobileActionsToggle.contains(target)) return;
1955
- if (headerActions && headerActions.contains(target)) return;
1956
- closeMobileActionsMenu();
2042
+ if (state.mobileActionsOpen) {
2043
+ if (mobileActionsToggle && mobileActionsToggle.contains(target)) return;
2044
+ if (headerActions && headerActions.contains(target)) return;
2045
+ closeMobileActionsMenu();
2046
+ }
2047
+ if (state.modeMenuOpen) {
2048
+ if (modeToggle && modeToggle.contains(target)) return;
2049
+ if (modeMenu && modeMenu.contains(target)) return;
2050
+ closeModeMenu();
2051
+ }
1957
2052
  });
1958
2053
 
1959
2054
  removeBtn.addEventListener('click', async function () {
package/lib/web/server.js CHANGED
@@ -1898,14 +1898,13 @@ async function startWebServer(options) {
1898
1898
  const requestOrigin = req.headers.origin;
1899
1899
  if (requestOrigin) {
1900
1900
  const allowedOrigins = new Set();
1901
- if (ctx.serverHost === '0.0.0.0') {
1902
- // 0.0.0.0 监听时,以请求的 Host 头构造允许来源
1903
- const hostHeader = req.headers.host || '';
1904
- if (hostHeader) {
1905
- allowedOrigins.add(`http://${hostHeader}`);
1906
- allowedOrigins.add(`https://${hostHeader}`);
1907
- }
1908
- } else {
1901
+ // 始终以请求的 Host 头构造允许来源,兼容 nginx 等反向代理场景
1902
+ const hostHeader = req.headers.host || '';
1903
+ if (hostHeader) {
1904
+ allowedOrigins.add(`http://${hostHeader}`);
1905
+ allowedOrigins.add(`https://${hostHeader}`);
1906
+ }
1907
+ if (ctx.serverHost !== '0.0.0.0') {
1909
1908
  allowedOrigins.add(`http://${formatUrlHost(ctx.serverHost)}:${listenPort}`);
1910
1909
  if (ctx.serverHost === '127.0.0.1') {
1911
1910
  allowedOrigins.add(`http://localhost:${listenPort}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.2.15",
3
+ "version": "5.2.23",
4
4
  "imageVersion": "1.8.1-common",
5
5
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",
6
6
  "keywords": [