@xcanwin/manyoyo 5.2.0 → 5.2.5

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.
@@ -437,20 +437,24 @@ textarea:focus-visible {
437
437
  box-shadow: inset 3px 0 0 var(--accent), 0 0 0 2px rgba(196, 85, 31, 0.14);
438
438
  }
439
439
 
440
- .session-item.history-only {
441
- color: #b3ab9f;
442
- }
443
-
444
- .session-item.history-only .session-name,
445
- .session-item.history-only .session-count,
446
- .session-item.history-only .session-time {
447
- color: #b3ab9f;
440
+ .session-item.status-history:not(.active),
441
+ .session-item.status-stopped:not(.active),
442
+ .session-item.status-unknown:not(.active) {
443
+ border-color: rgba(181, 146, 99, 0.22);
444
+ background: rgba(255, 252, 247, 0.72);
445
+ box-shadow: none;
448
446
  }
449
447
 
450
- .session-item.history-only .session-status {
451
- background: #f7f3ed;
452
- border-color: #e3dbd1;
453
- color: #b3ab9f;
448
+ .session-item.status-history:not(.active) .session-name,
449
+ .session-item.status-history:not(.active) .session-meta,
450
+ .session-item.status-history:not(.active) .session-time,
451
+ .session-item.status-stopped:not(.active) .session-name,
452
+ .session-item.status-stopped:not(.active) .session-meta,
453
+ .session-item.status-stopped:not(.active) .session-time,
454
+ .session-item.status-unknown:not(.active) .session-name,
455
+ .session-item.status-unknown:not(.active) .session-meta,
456
+ .session-item.status-unknown:not(.active) .session-time {
457
+ opacity: 0.25;
454
458
  }
455
459
 
456
460
  .session-name {
@@ -847,6 +851,23 @@ body.terminal-mode .composer {
847
851
  opacity: 0.78;
848
852
  }
849
853
 
854
+ body.agent-mode .msg.origin-command .bubble,
855
+ body.agent-mode .msg.origin-command .msg-meta,
856
+ body.agent-mode .msg.origin-command .msg-exit {
857
+ opacity: 0.25;
858
+ }
859
+
860
+ body.command-mode .msg.origin-agent .bubble,
861
+ body.command-mode .msg.origin-agent .msg-meta,
862
+ body.command-mode .msg.origin-agent .msg-exit {
863
+ opacity: 0.25;
864
+ }
865
+
866
+ body.agent-mode .msg.origin-command .bubble,
867
+ body.command-mode .msg.origin-agent .bubble {
868
+ box-shadow: none;
869
+ }
870
+
850
871
  .bubble pre {
851
872
  margin: 0;
852
873
  white-space: pre-wrap;
@@ -8,6 +8,7 @@
8
8
  />
9
9
  <title>MANYOYO Web</title>
10
10
  <link rel="stylesheet" href="/app/frontend/app.css" />
11
+ <link rel="stylesheet" href="/app/frontend/markdown.css" />
11
12
  <link rel="stylesheet" href="/app/vendor/xterm.css" />
12
13
  </head>
13
14
  <body>
@@ -65,8 +66,8 @@
65
66
  </header>
66
67
  <section class="mode-switch" id="modeSwitch">
67
68
  <div class="mode-switch-left">
68
- <button type="button" id="modeCommandBtn" class="secondary is-active">命令模式</button>
69
- <button type="button" id="modeAgentBtn" class="secondary">AGENT 模式</button>
69
+ <button type="button" id="modeAgentBtn" class="secondary is-active">AGENT 模式</button>
70
+ <button type="button" id="modeCommandBtn" class="secondary">命令模式</button>
70
71
  <button type="button" id="modeTerminalBtn" class="secondary">交互终端</button>
71
72
  </div>
72
73
  <div class="mode-terminal-controls">
@@ -164,6 +165,8 @@
164
165
 
165
166
  <script src="/app/vendor/xterm.js"></script>
166
167
  <script src="/app/vendor/xterm-addon-fit.js"></script>
168
+ <script src="/app/vendor/marked.min.js"></script>
169
+ <script src="/app/frontend/markdown-renderer.js"></script>
167
170
  <script src="/app/frontend/app.js"></script>
168
171
  </body>
169
172
  </html>
@@ -40,7 +40,7 @@
40
40
  active: '',
41
41
  messages: [],
42
42
  messageRenderKeys: [],
43
- mode: 'command',
43
+ mode: 'agent',
44
44
  sending: false,
45
45
  loadingSessions: false,
46
46
  loadingMessages: false,
@@ -155,9 +155,20 @@
155
155
  codex: 'codex exec {prompt}',
156
156
  opencode: 'opencode run {prompt}'
157
157
  };
158
+ const markdownRenderer = window.ManyoyoMarkdown
159
+ && typeof window.ManyoyoMarkdown.shouldRenderMessage === 'function'
160
+ && typeof window.ManyoyoMarkdown.render === 'function'
161
+ ? window.ManyoyoMarkdown
162
+ : null;
163
+
164
+ function appendPlainMessageContent(bubble, content) {
165
+ const pre = document.createElement('pre');
166
+ pre.textContent = content == null ? '' : String(content);
167
+ bubble.appendChild(pre);
168
+ }
158
169
 
159
170
  function roleName(role, message) {
160
- if (role === 'user') return '';
171
+ if (role === 'user') return '';
161
172
  if (role === 'assistant') {
162
173
  if (message && message.mode === 'agent') {
163
174
  return 'AGENT 回复';
@@ -934,7 +945,7 @@
934
945
  removeBtn.disabled = !state.active || busy;
935
946
  removeAllBtn.disabled = !state.active || busy;
936
947
  sendBtn.disabled = !composerMode || !state.active || busy || (agentMode && !agentEnabled);
937
- commandInput.disabled = !composerMode || !state.active || state.sending || (agentMode && !agentEnabled);
948
+ commandInput.disabled = !composerMode || !state.active || (agentMode && !agentEnabled);
938
949
  if (commandInput) {
939
950
  commandInput.placeholder = agentMode
940
951
  ? '输入提示词,例如:请帮我分析当前项目结构并给出重构建议'
@@ -1146,6 +1157,10 @@
1146
1157
  row.style.setProperty('--item-index', String(index));
1147
1158
  row.classList.toggle('active', state.active === session.name);
1148
1159
  row.classList.toggle('history-only', status.tone === 'history');
1160
+ row.classList.toggle('status-running', status.tone === 'running');
1161
+ row.classList.toggle('status-stopped', status.tone === 'stopped');
1162
+ row.classList.toggle('status-history', status.tone === 'history');
1163
+ row.classList.toggle('status-unknown', status.tone === 'unknown');
1149
1164
  if (row.__sessionNameNode) {
1150
1165
  row.__sessionNameNode.textContent = session.name;
1151
1166
  }
@@ -1329,16 +1344,33 @@
1329
1344
  return `id:${msg.id}`;
1330
1345
  }
1331
1346
  const role = msg && msg.role ? String(msg.role) : '';
1347
+ const mode = msg && msg.mode ? String(msg.mode) : '';
1332
1348
  const timestamp = msg && msg.timestamp ? String(msg.timestamp) : '';
1333
1349
  const exitCode = msg && typeof msg.exitCode === 'number' ? String(msg.exitCode) : '';
1334
1350
  const pending = msg && msg.pending ? '1' : '0';
1335
1351
  const content = msg && msg.content ? String(msg.content) : '';
1336
- return `idx:${index}|${role}|${timestamp}|${exitCode}|${pending}|${content}`;
1352
+ return `idx:${index}|${role}|${mode}|${timestamp}|${exitCode}|${pending}|${content}`;
1353
+ }
1354
+
1355
+ function resolveMessageOrigin(msg) {
1356
+ const role = msg && msg.role ? String(msg.role) : '';
1357
+ if (!(role === 'user' || role === 'assistant')) {
1358
+ return '';
1359
+ }
1360
+ const mode = msg && msg.mode ? String(msg.mode) : '';
1361
+ if (mode === 'agent') {
1362
+ return 'agent';
1363
+ }
1364
+ return 'command';
1337
1365
  }
1338
1366
 
1339
1367
  function createMessageRow(msg, index) {
1340
1368
  const row = document.createElement('article');
1341
1369
  row.className = 'msg ' + (msg.role || 'system') + (msg.pending ? ' pending' : '');
1370
+ const origin = resolveMessageOrigin(msg);
1371
+ if (origin) {
1372
+ row.classList.add('origin-' + origin);
1373
+ }
1342
1374
  row.style.setProperty('--msg-index', String(index));
1343
1375
 
1344
1376
  const meta = document.createElement('div');
@@ -1354,9 +1386,25 @@
1354
1386
  const bubble = document.createElement('div');
1355
1387
  bubble.className = 'bubble';
1356
1388
 
1357
- const pre = document.createElement('pre');
1358
- pre.textContent = msg.content || '';
1359
- bubble.appendChild(pre);
1389
+ const shouldRenderMarkdown = Boolean(markdownRenderer && markdownRenderer.shouldRenderMessage(msg));
1390
+ if (shouldRenderMarkdown) {
1391
+ const markdownNode = document.createElement('div');
1392
+ markdownNode.className = 'md-content';
1393
+ let renderedMarkdown = '';
1394
+ try {
1395
+ renderedMarkdown = String(markdownRenderer.render(msg.content) || '');
1396
+ } catch (e) {
1397
+ renderedMarkdown = '';
1398
+ }
1399
+ if (renderedMarkdown) {
1400
+ markdownNode.innerHTML = renderedMarkdown;
1401
+ bubble.appendChild(markdownNode);
1402
+ } else {
1403
+ appendPlainMessageContent(bubble, msg.content);
1404
+ }
1405
+ } else {
1406
+ appendPlainMessageContent(bubble, msg.content);
1407
+ }
1360
1408
 
1361
1409
  row.appendChild(meta);
1362
1410
  row.appendChild(bubble);
@@ -1703,6 +1751,7 @@
1703
1751
  })
1704
1752
  });
1705
1753
  closeCreateModal();
1754
+ state.mode = 'agent';
1706
1755
  await loadSessions(data.name);
1707
1756
  if (isMobileLayout()) {
1708
1757
  closeMobileSessionPanel();
@@ -1993,7 +2042,7 @@
1993
2042
  renderSessions();
1994
2043
  renderMessages(state.messages);
1995
2044
  setMobileSessionPanel(false);
1996
- document.body.classList.add('command-mode');
2045
+ document.body.classList.add('agent-mode');
1997
2046
  syncUi();
1998
2047
  loadSessions().catch(function (e) {
1999
2048
  alert(e.message);
@@ -0,0 +1,115 @@
1
+ (function () {
2
+ const MARKDOWN_URL_PROTOCOL_PATTERN = /^(https?:|mailto:|tel:)/i;
3
+ const runtime = {
4
+ configured: false,
5
+ available: false
6
+ };
7
+
8
+ function escapeHtml(value) {
9
+ return String(value == null ? '' : value)
10
+ .replace(/&/g, '&amp;')
11
+ .replace(/</g, '&lt;')
12
+ .replace(/>/g, '&gt;')
13
+ .replace(/"/g, '&quot;')
14
+ .replace(/'/g, '&#39;');
15
+ }
16
+
17
+ function sanitizeMarkdownUrl(value) {
18
+ const raw = String(value == null ? '' : value).trim();
19
+ if (!raw) {
20
+ return '';
21
+ }
22
+ if (raw[0] === '#') {
23
+ return raw;
24
+ }
25
+ if (raw[0] === '/') {
26
+ return raw.startsWith('//') ? '' : raw;
27
+ }
28
+ if (raw.startsWith('./') || raw.startsWith('../')) {
29
+ return raw;
30
+ }
31
+ if (MARKDOWN_URL_PROTOCOL_PATTERN.test(raw)) {
32
+ return raw;
33
+ }
34
+ return '';
35
+ }
36
+
37
+ function getMarkedApi() {
38
+ const api = window.marked;
39
+ if (!api || typeof api.parse !== 'function') {
40
+ return null;
41
+ }
42
+ return api;
43
+ }
44
+
45
+ function ensureRendererConfigured() {
46
+ if (runtime.configured) {
47
+ return runtime.available;
48
+ }
49
+
50
+ const markedApi = getMarkedApi();
51
+ if (!markedApi || typeof markedApi.Renderer !== 'function' || typeof markedApi.use !== 'function') {
52
+ runtime.configured = true;
53
+ runtime.available = false;
54
+ return false;
55
+ }
56
+
57
+ try {
58
+ const renderer = new markedApi.Renderer();
59
+ renderer.html = function (html) {
60
+ return escapeHtml(html);
61
+ };
62
+ renderer.link = function (href, title, text) {
63
+ const safeHref = sanitizeMarkdownUrl(href);
64
+ if (!safeHref) {
65
+ return text || '';
66
+ }
67
+ let output = '<a href="' + escapeHtml(safeHref) + '" target="_blank" rel="noopener noreferrer"';
68
+ if (title) {
69
+ output += ' title="' + escapeHtml(title) + '"';
70
+ }
71
+ output += '>' + (text || '') + '</a>';
72
+ return output;
73
+ };
74
+
75
+ markedApi.use({
76
+ gfm: true,
77
+ breaks: true,
78
+ renderer
79
+ });
80
+ runtime.available = true;
81
+ } catch (e) {
82
+ runtime.available = false;
83
+ }
84
+ runtime.configured = true;
85
+ return runtime.available;
86
+ }
87
+
88
+ function shouldRenderMessage(msg) {
89
+ return Boolean(msg && msg.mode === 'agent' && msg.role === 'assistant');
90
+ }
91
+
92
+ function render(content) {
93
+ const source = String(content == null ? '' : content);
94
+ if (!source) {
95
+ return '';
96
+ }
97
+ if (!ensureRendererConfigured()) {
98
+ return '';
99
+ }
100
+ const markedApi = getMarkedApi();
101
+ if (!markedApi) {
102
+ return '';
103
+ }
104
+ try {
105
+ return String(markedApi.parse(source) || '');
106
+ } catch (e) {
107
+ return '';
108
+ }
109
+ }
110
+
111
+ window.ManyoyoMarkdown = {
112
+ shouldRenderMessage,
113
+ render
114
+ };
115
+ }());
@@ -0,0 +1,76 @@
1
+ .bubble .md-content {
2
+ color: var(--text);
3
+ font-size: 13px;
4
+ line-height: 1.6;
5
+ word-break: break-word;
6
+ }
7
+
8
+ .bubble .md-content > :first-child {
9
+ margin-top: 0;
10
+ }
11
+
12
+ .bubble .md-content > :last-child {
13
+ margin-bottom: 0;
14
+ }
15
+
16
+ .bubble .md-content p,
17
+ .bubble .md-content ul,
18
+ .bubble .md-content ol,
19
+ .bubble .md-content blockquote,
20
+ .bubble .md-content pre,
21
+ .bubble .md-content table {
22
+ margin: 0.6em 0;
23
+ }
24
+
25
+ .bubble .md-content ul,
26
+ .bubble .md-content ol {
27
+ padding-left: 1.4em;
28
+ }
29
+
30
+ .bubble .md-content code {
31
+ font-family: var(--font-mono);
32
+ font-size: 12px;
33
+ padding: 1px 5px;
34
+ border-radius: 6px;
35
+ background: rgba(194, 149, 79, 0.16);
36
+ }
37
+
38
+ .bubble .md-content pre {
39
+ border-radius: 8px;
40
+ padding: 9px 10px;
41
+ border: 1px solid #e6d2b7;
42
+ background: rgba(255, 244, 224, 0.72);
43
+ overflow-x: auto;
44
+ }
45
+
46
+ .bubble .md-content pre code {
47
+ background: transparent;
48
+ border-radius: 0;
49
+ padding: 0;
50
+ font-size: 12px;
51
+ line-height: 1.5;
52
+ }
53
+
54
+ .bubble .md-content blockquote {
55
+ margin-left: 0;
56
+ padding-left: 10px;
57
+ border-left: 3px solid #dcb788;
58
+ color: #66492d;
59
+ }
60
+
61
+ .bubble .md-content table {
62
+ border-collapse: collapse;
63
+ width: 100%;
64
+ }
65
+
66
+ .bubble .md-content th,
67
+ .bubble .md-content td {
68
+ border: 1px solid #e6d2b7;
69
+ padding: 4px 6px;
70
+ text-align: left;
71
+ }
72
+
73
+ .bubble .md-content a {
74
+ color: #8a4f17;
75
+ text-decoration-color: rgba(138, 79, 23, 0.45);
76
+ }
package/lib/web/server.js CHANGED
@@ -9,7 +9,11 @@ const http = require('http');
9
9
  const WebSocket = require('ws');
10
10
  const JSON5 = require('json5');
11
11
  const { buildContainerRunArgs } = require('../container-run');
12
- const { resolveAgentPromptCommandTemplate } = require('../agent-resume');
12
+ const {
13
+ resolveAgentProgram,
14
+ resolveAgentPromptCommandTemplate,
15
+ buildAgentResumeCommand
16
+ } = require('../agent-resume');
13
17
 
14
18
  const WEB_HISTORY_MAX_MESSAGES = 500;
15
19
  const WEB_OUTPUT_MAX_CHARS = 16000;
@@ -19,6 +23,9 @@ const WEB_TERMINAL_DEFAULT_COLS = 120;
19
23
  const WEB_TERMINAL_DEFAULT_ROWS = 36;
20
24
  const WEB_TERMINAL_MIN_COLS = 40;
21
25
  const WEB_TERMINAL_MIN_ROWS = 12;
26
+ const WEB_AGENT_CONTEXT_MAX_MESSAGES = 24;
27
+ const WEB_AGENT_CONTEXT_MAX_CHARS = 6000;
28
+ const WEB_AGENT_CONTEXT_PER_MESSAGE_MAX_CHARS = 600;
22
29
  const WEB_AUTH_COOKIE_NAME = 'manyoyo_web_auth';
23
30
  const WEB_AUTH_TTL_SECONDS = 12 * 60 * 60;
24
31
  const FRONTEND_DIR = path.join(__dirname, 'frontend');
@@ -61,6 +68,7 @@ const DEFAULT_WEB_CONFIG_TEMPLATE = `{
61
68
  let XTERM_JS_FILE = null;
62
69
  let XTERM_CSS_FILE = null;
63
70
  let XTERM_ADDON_FIT_JS_FILE = null;
71
+ let MARKED_MIN_JS_FILE = null;
64
72
  try {
65
73
  const xtermPackageDir = path.dirname(require.resolve('@xterm/xterm/package.json'));
66
74
  XTERM_JS_FILE = path.join(xtermPackageDir, 'lib', 'xterm.js');
@@ -75,6 +83,11 @@ try {
75
83
  } catch (e) {
76
84
  XTERM_ADDON_FIT_JS_FILE = null;
77
85
  }
86
+ try {
87
+ MARKED_MIN_JS_FILE = require.resolve('marked/marked.min.js');
88
+ } catch (e) {
89
+ MARKED_MIN_JS_FILE = null;
90
+ }
78
91
 
79
92
  const MIME_TYPES = {
80
93
  '.css': 'text/css; charset=utf-8',
@@ -106,7 +119,12 @@ function loadWebSessionHistory(webHistoryDir, containerName) {
106
119
  containerName,
107
120
  updatedAt: null,
108
121
  messages: [],
109
- agentPromptCommand: ''
122
+ agentPromptCommand: '',
123
+ agentProgram: '',
124
+ resumeSupported: false,
125
+ lastResumeAt: null,
126
+ lastResumeOk: null,
127
+ lastResumeError: ''
110
128
  };
111
129
  }
112
130
 
@@ -118,14 +136,24 @@ function loadWebSessionHistory(webHistoryDir, containerName) {
118
136
  messages: Array.isArray(data.messages) ? data.messages : [],
119
137
  agentPromptCommand: typeof data.agentPromptCommand === 'string'
120
138
  ? data.agentPromptCommand
121
- : ''
139
+ : '',
140
+ agentProgram: typeof data.agentProgram === 'string' ? data.agentProgram : '',
141
+ resumeSupported: data.resumeSupported === true,
142
+ lastResumeAt: typeof data.lastResumeAt === 'string' ? data.lastResumeAt : null,
143
+ lastResumeOk: typeof data.lastResumeOk === 'boolean' ? data.lastResumeOk : null,
144
+ lastResumeError: typeof data.lastResumeError === 'string' ? data.lastResumeError : ''
122
145
  };
123
146
  } catch (e) {
124
147
  return {
125
148
  containerName,
126
149
  updatedAt: null,
127
150
  messages: [],
128
- agentPromptCommand: ''
151
+ agentPromptCommand: '',
152
+ agentProgram: '',
153
+ resumeSupported: false,
154
+ lastResumeAt: null,
155
+ lastResumeOk: null,
156
+ lastResumeError: ''
129
157
  };
130
158
  }
131
159
  }
@@ -174,7 +202,28 @@ function appendWebSessionMessage(webHistoryDir, containerName, role, content, ex
174
202
  function setWebSessionAgentPromptCommand(webHistoryDir, containerName, agentPromptCommand) {
175
203
  const history = loadWebSessionHistory(webHistoryDir, containerName);
176
204
  history.agentPromptCommand = normalizeAgentPromptCommandTemplate(agentPromptCommand, 'agentPromptCommand');
205
+ const agentProgram = resolveAgentProgram(history.agentPromptCommand);
206
+ const resumeCommand = buildAgentResumeCommand(agentProgram);
207
+ history.agentProgram = agentProgram || '';
208
+ history.resumeSupported = Boolean(resumeCommand);
209
+ if (!history.resumeSupported) {
210
+ history.lastResumeAt = null;
211
+ history.lastResumeOk = null;
212
+ history.lastResumeError = '';
213
+ }
214
+ saveWebSessionHistory(webHistoryDir, containerName, history);
215
+ }
216
+
217
+ function patchWebSessionAgentState(webHistoryDir, containerName, patch) {
218
+ const history = loadWebSessionHistory(webHistoryDir, containerName);
219
+ if (!patch || typeof patch !== 'object') {
220
+ return history;
221
+ }
222
+ Object.keys(patch).forEach(key => {
223
+ history[key] = patch[key];
224
+ });
177
225
  saveWebSessionHistory(webHistoryDir, containerName, history);
226
+ return history;
178
227
  }
179
228
 
180
229
  function stripAnsi(text) {
@@ -220,6 +269,72 @@ function renderAgentPromptCommand(template, prompt) {
220
269
  return templateText.replace(/\{prompt\}/g, safePrompt);
221
270
  }
222
271
 
272
+ function getAgentRuntimeMeta(history) {
273
+ const sessionHistory = history && typeof history === 'object' ? history : {};
274
+ const template = normalizeAgentPromptCommandTemplate(sessionHistory.agentPromptCommand, 'agentPromptCommand');
275
+ const agentProgram = resolveAgentProgram(template);
276
+ const resumeCommand = buildAgentResumeCommand(agentProgram);
277
+ return {
278
+ agentProgram: agentProgram || '',
279
+ resumeCommand: resumeCommand || '',
280
+ resumeSupported: Boolean(resumeCommand)
281
+ };
282
+ }
283
+
284
+ function hasAgentConversationHistory(history) {
285
+ const messages = history && Array.isArray(history.messages) ? history.messages : [];
286
+ for (const message of messages) {
287
+ if (!message || typeof message !== 'object') continue;
288
+ if (message.mode !== 'agent') continue;
289
+ if (message.role === 'user' || message.role === 'assistant') {
290
+ return true;
291
+ }
292
+ }
293
+ return false;
294
+ }
295
+
296
+ function clipAgentContextMessageText(text) {
297
+ const raw = clipText(stripAnsi(String(text || '')), WEB_AGENT_CONTEXT_PER_MESSAGE_MAX_CHARS);
298
+ return raw
299
+ .replace(/\r/g, '')
300
+ .replace(/\n{3,}/g, '\n\n')
301
+ .trim();
302
+ }
303
+
304
+ function buildAgentPromptWithHistory(history, prompt) {
305
+ const sessionHistory = history && Array.isArray(history.messages) ? history.messages : [];
306
+ const relevantMessages = sessionHistory
307
+ .filter(message => message && message.mode === 'agent' && (message.role === 'user' || message.role === 'assistant'))
308
+ .slice(-WEB_AGENT_CONTEXT_MAX_MESSAGES);
309
+ if (!relevantMessages.length) {
310
+ return String(prompt || '');
311
+ }
312
+
313
+ const lines = [];
314
+ for (const message of relevantMessages) {
315
+ const roleName = message.role === 'user' ? '用户' : '助手';
316
+ const content = clipAgentContextMessageText(message.content);
317
+ if (!content) continue;
318
+ lines.push(`${roleName}: ${content}`);
319
+ }
320
+ if (!lines.length) {
321
+ return String(prompt || '');
322
+ }
323
+
324
+ let historyText = lines.join('\n\n');
325
+ if (historyText.length > WEB_AGENT_CONTEXT_MAX_CHARS) {
326
+ historyText = historyText.slice(historyText.length - WEB_AGENT_CONTEXT_MAX_CHARS);
327
+ }
328
+
329
+ return [
330
+ '以下是当前会话最近对话历史(按时间顺序):',
331
+ historyText,
332
+ '---',
333
+ '请基于以上历史回答当前问题。',
334
+ `当前问题: ${String(prompt || '').trim()}`
335
+ ].join('\n');
336
+ }
337
+
223
338
  function secureStringEqual(a, b) {
224
339
  const aStr = String(a || '');
225
340
  const bStr = String(b || '');
@@ -745,6 +860,8 @@ function buildCreateRuntime(ctx, state, payload) {
745
860
  'agentPromptCommand'
746
861
  );
747
862
  const agentPromptCommand = configuredAgentPromptCommand || inferredAgentPromptCommand;
863
+ const agentProgram = resolveAgentProgram(agentPromptCommand);
864
+ const resumeSupported = Boolean(buildAgentResumeCommand(agentProgram));
748
865
 
749
866
  let containerEnvs = Array.isArray(ctx.containerEnvs) ? ctx.containerEnvs.slice() : [];
750
867
  if (hasRequestEnv || hasRequestEnvFile || hasConfigEnv || hasConfigEnvFile) {
@@ -813,6 +930,8 @@ function buildCreateRuntime(ctx, state, payload) {
813
930
  shell: shell || '',
814
931
  shellSuffix: shellSuffix || '',
815
932
  agentEnabled: isAgentPromptCommandEnabled(agentPromptCommand),
933
+ agentProgram: agentProgram || '',
934
+ resumeSupported,
816
935
  yolo: yolo || '',
817
936
  envCount: Math.floor(containerEnvs.length / 2),
818
937
  volumeCount: Math.floor(containerVolumes.length / 2),
@@ -995,6 +1114,7 @@ function getValidSessionName(ctx, res, encodedName) {
995
1114
 
996
1115
  function buildSessionSummary(ctx, state, containerMap, name) {
997
1116
  const history = loadWebSessionHistory(state.webHistoryDir, name);
1117
+ const agentMeta = getAgentRuntimeMeta(history);
998
1118
  const latestMessage = history.messages.length ? history.messages[history.messages.length - 1] : null;
999
1119
  const containerInfo = containerMap[name] || {};
1000
1120
  const updatedAt = history.updatedAt || (latestMessage && latestMessage.timestamp) || null;
@@ -1004,7 +1124,9 @@ function buildSessionSummary(ctx, state, containerMap, name) {
1004
1124
  image: containerInfo.image || '',
1005
1125
  updatedAt,
1006
1126
  messageCount: history.messages.length,
1007
- agentEnabled: isAgentPromptCommandEnabled(history.agentPromptCommand)
1127
+ agentEnabled: isAgentPromptCommandEnabled(history.agentPromptCommand),
1128
+ agentProgram: agentMeta.agentProgram,
1129
+ resumeSupported: agentMeta.resumeSupported
1008
1130
  };
1009
1131
  }
1010
1132
 
@@ -1033,6 +1155,9 @@ function resolveVendorAsset(name) {
1033
1155
  if (name === 'xterm-addon-fit.js') {
1034
1156
  return XTERM_ADDON_FIT_JS_FILE && fs.existsSync(XTERM_ADDON_FIT_JS_FILE) ? XTERM_ADDON_FIT_JS_FILE : null;
1035
1157
  }
1158
+ if (name === 'marked.min.js') {
1159
+ return MARKED_MIN_JS_FILE && fs.existsSync(MARKED_MIN_JS_FILE) ? MARKED_MIN_JS_FILE : null;
1160
+ }
1036
1161
  return null;
1037
1162
  }
1038
1163
 
@@ -1478,18 +1603,59 @@ async function handleWebApi(req, res, pathname, ctx, state) {
1478
1603
  return;
1479
1604
  }
1480
1605
 
1481
- const command = renderAgentPromptCommand(history.agentPromptCommand, prompt);
1482
1606
  await ensureWebContainer(ctx, state, containerName);
1483
- appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, { mode: 'agent' });
1607
+ const agentMeta = getAgentRuntimeMeta(history);
1608
+ const hasPriorConversation = hasAgentConversationHistory(history);
1609
+ let resumeAttempted = false;
1610
+ let resumeSucceeded = false;
1611
+ let resumeError = '';
1612
+ if (hasPriorConversation && agentMeta.resumeSupported && agentMeta.resumeCommand) {
1613
+ resumeAttempted = true;
1614
+ const resumeResult = await execCommandInWebContainer(ctx, containerName, agentMeta.resumeCommand);
1615
+ if (resumeResult.exitCode === 0) {
1616
+ resumeSucceeded = true;
1617
+ } else {
1618
+ resumeError = clipText(String(resumeResult.output || '(无输出)'), 1200);
1619
+ }
1620
+ }
1621
+
1622
+ const effectivePrompt = resumeSucceeded
1623
+ ? prompt
1624
+ : buildAgentPromptWithHistory(history, prompt);
1625
+ const command = renderAgentPromptCommand(history.agentPromptCommand, effectivePrompt);
1626
+ const contextMode = resumeSucceeded ? 'resume' : (hasPriorConversation ? 'history-injected' : 'first-turn');
1627
+ appendWebSessionMessage(state.webHistoryDir, containerName, 'user', prompt, {
1628
+ mode: 'agent',
1629
+ contextMode
1630
+ });
1484
1631
  const result = await execCommandInWebContainer(ctx, containerName, command);
1485
1632
  appendWebSessionMessage(
1486
1633
  state.webHistoryDir,
1487
1634
  containerName,
1488
1635
  'assistant',
1489
1636
  result.output,
1490
- { exitCode: result.exitCode, mode: 'agent' }
1637
+ {
1638
+ exitCode: result.exitCode,
1639
+ mode: 'agent',
1640
+ contextMode,
1641
+ resumeAttempted,
1642
+ resumeSucceeded
1643
+ }
1491
1644
  );
1492
- sendJson(res, 200, { exitCode: result.exitCode, output: result.output });
1645
+ patchWebSessionAgentState(state.webHistoryDir, containerName, {
1646
+ agentProgram: agentMeta.agentProgram,
1647
+ resumeSupported: agentMeta.resumeSupported,
1648
+ lastResumeAt: resumeAttempted ? new Date().toISOString() : history.lastResumeAt || null,
1649
+ lastResumeOk: resumeAttempted ? resumeSucceeded : history.lastResumeOk,
1650
+ lastResumeError: resumeAttempted ? (resumeSucceeded ? '' : resumeError) : history.lastResumeError || ''
1651
+ });
1652
+ sendJson(res, 200, {
1653
+ exitCode: result.exitCode,
1654
+ output: result.output,
1655
+ contextMode,
1656
+ resumeAttempted,
1657
+ resumeSucceeded
1658
+ });
1493
1659
  }
1494
1660
  },
1495
1661
  {
@@ -1629,7 +1795,7 @@ async function startWebServer(options) {
1629
1795
  const appFrontendMatch = pathname.match(/^\/app\/frontend\/([A-Za-z0-9._-]+)$/);
1630
1796
  if (req.method === 'GET' && appFrontendMatch) {
1631
1797
  const assetName = appFrontendMatch[1];
1632
- if (!(assetName === 'app.css' || assetName === 'app.js')) {
1798
+ if (!(assetName === 'app.css' || assetName === 'app.js' || assetName === 'markdown.css' || assetName === 'markdown-renderer.js')) {
1633
1799
  sendHtml(res, 404, '<h1>404 Not Found</h1>');
1634
1800
  return;
1635
1801
  }
@@ -1640,7 +1806,7 @@ async function startWebServer(options) {
1640
1806
  const appVendorMatch = pathname.match(/^\/app\/vendor\/([A-Za-z0-9._-]+)$/);
1641
1807
  if (req.method === 'GET' && appVendorMatch) {
1642
1808
  const assetName = appVendorMatch[1];
1643
- if (!(assetName === 'xterm.css' || assetName === 'xterm.js' || assetName === 'xterm-addon-fit.js')) {
1809
+ if (!(assetName === 'xterm.css' || assetName === 'xterm.js' || assetName === 'xterm-addon-fit.js' || assetName === 'marked.min.js')) {
1644
1810
  sendHtml(res, 404, '<h1>404 Not Found</h1>');
1645
1811
  return;
1646
1812
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.2.0",
3
+ "version": "5.2.5",
4
4
  "imageVersion": "1.8.1-common",
5
5
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",
6
6
  "keywords": [
@@ -58,6 +58,7 @@
58
58
  "@xterm/xterm": "^6.0.0",
59
59
  "commander": "^12.0.0",
60
60
  "json5": "^2.2.3",
61
+ "marked": "^12.0.2",
61
62
  "playwright": "1.58.2",
62
63
  "ws": "^8.19.0"
63
64
  },