newmark-agent 0.5.12 → 0.5.13

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.
@@ -60,6 +60,7 @@ const toolArgumentValidator_1 = require("../core/toolArgumentValidator");
60
60
  const localOcr_1 = require("../core/localOcr");
61
61
  const visualTextFallback_1 = require("../core/visualTextFallback");
62
62
  const computerUseSession_1 = require("../core/computerUseSession");
63
+ const searchMcpPool_1 = require("../core/searchMcpPool");
63
64
  function normalizeComputerUseAction(action) {
64
65
  return String(action || '').trim().toLowerCase();
65
66
  }
@@ -149,7 +150,7 @@ function decodePdfLiteral(input) {
149
150
  .replace(/\\t/g, '\t')
150
151
  .replace(/\\([0-7]{1,3})/g, (_match, octal) => String.fromCharCode(parseInt(octal, 8)));
151
152
  }
152
- function extractPdfTextLayer(buffer) {
153
+ function extractPdfTextLayerLegacy(buffer) {
153
154
  const binary = buffer.toString('latin1');
154
155
  const chunks = [];
155
156
  for (const match of binary.matchAll(/\((?:\\.|[^\\)]){1,4000}\)\s*Tj/g)) {
@@ -171,6 +172,61 @@ function extractPdfTextLayer(buffer) {
171
172
  }
172
173
  return chunks.join(' ').replace(/\s+/g, ' ').trim().slice(0, 100_000);
173
174
  }
175
+ async function extractPdfTextLayer(buffer, maxChars, signal) {
176
+ if (signal?.aborted)
177
+ throw abortReason(signal);
178
+ let loadingTask = null;
179
+ try {
180
+ const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
181
+ loadingTask = pdfjs.getDocument({
182
+ data: new Uint8Array(buffer),
183
+ isEvalSupported: false,
184
+ useSystemFonts: true,
185
+ stopAtErrors: false,
186
+ });
187
+ const abortPromise = signal ? new Promise((_resolve, reject) => {
188
+ signal.addEventListener('abort', () => reject(abortReason(signal)), { once: true });
189
+ }) : null;
190
+ const document = await (abortPromise ? Promise.race([loadingTask.promise, abortPromise]) : loadingTask.promise);
191
+ const chunks = [];
192
+ let length = 0;
193
+ for (let pageNumber = 1; pageNumber <= document.numPages && length < maxChars; pageNumber += 1) {
194
+ if (signal?.aborted)
195
+ throw abortReason(signal);
196
+ const page = await document.getPage(pageNumber);
197
+ const content = await page.getTextContent({ disableNormalization: false });
198
+ const pageText = (Array.isArray(content.items) ? content.items : [])
199
+ .map((item) => `${String(item.str || '')}${item.hasEOL ? '\n' : ' '}`)
200
+ .join('')
201
+ .replace(/[ \t]+\n/g, '\n')
202
+ .replace(/[ \t]{2,}/g, ' ')
203
+ .trim();
204
+ if (pageText) {
205
+ const bounded = pageText.slice(0, Math.max(0, maxChars - length));
206
+ chunks.push(bounded);
207
+ length += bounded.length + 1;
208
+ }
209
+ page.cleanup();
210
+ }
211
+ return chunks.join('\n').trim().slice(0, maxChars);
212
+ }
213
+ catch (error) {
214
+ if (signal?.aborted)
215
+ throw abortReason(signal);
216
+ // Retain the bounded legacy decoder for minimal/malformed fixture PDFs and
217
+ // old documents whose streams pdf.js rejects. It never opens the browser.
218
+ const fallback = extractPdfTextLayerLegacy(buffer).slice(0, maxChars);
219
+ if (fallback)
220
+ return fallback;
221
+ return '';
222
+ }
223
+ finally {
224
+ try {
225
+ await loadingTask?.destroy();
226
+ }
227
+ catch { }
228
+ }
229
+ }
174
230
  async function abortableToolDelay(durationMs, signal) {
175
231
  if (signal?.aborted)
176
232
  throw abortReason(signal);
@@ -219,6 +275,7 @@ class ToolExecutor {
219
275
  root;
220
276
  localOcr;
221
277
  argumentValidators = new toolArgumentValidator_1.ToolArgumentValidatorRegistry();
278
+ searchMcpPool;
222
279
  hostProfile = {
223
280
  kind: 'desktop',
224
281
  platform: process.platform,
@@ -231,14 +288,38 @@ class ToolExecutor {
231
288
  this.workspace = workspace;
232
289
  this.root = root;
233
290
  this.localOcr = new localOcr_1.LocalOcrEngine(root);
291
+ this.searchMcpPool = new searchMcpPool_1.SearchMcpPool(root);
234
292
  }
235
- async webSearch(query) {
236
- return this.wsearch(query);
293
+ async webSearch(query, signal) {
294
+ return (await this.webSearchDetailed(query, signal)).text;
295
+ }
296
+ async webSearchDetailed(query, signal) {
297
+ return this.wsearchDetailed(query, signal);
298
+ }
299
+ /** Search the configured MCP pool without entering any HTTP fallback. */
300
+ async webSearchMcpOnly(query, signal) {
301
+ const result = await this.searchMcpPool.search(query, signal);
302
+ return {
303
+ invocationId: result.invocationId,
304
+ checkedAt: result.checkedAt,
305
+ ok: result.ok,
306
+ provider: result.provider || '',
307
+ text: result.text || '',
308
+ attempts: result.attempts,
309
+ };
237
310
  }
238
311
  /** OCR entry point for the runtime's final visual fallback. */
239
312
  async finalVisualFallbackOcr(dataUrl, signal) {
240
313
  return await this.localOcr.recognizeDataUrl(dataUrl, signal, 'sparse-ui');
241
314
  }
315
+ async readPdfFile(pdfPath, signal) {
316
+ if (signal?.aborted)
317
+ throw abortReason(signal);
318
+ return await fs.promises.readFile(pdfPath, signal ? { signal } : undefined);
319
+ }
320
+ async extractPdfText(buffer, maxChars, signal) {
321
+ return await extractPdfTextLayer(buffer, maxChars, signal);
322
+ }
242
323
  setHostProfile(profile) {
243
324
  this.hostProfile = { ...profile };
244
325
  }
@@ -282,8 +363,9 @@ class ToolExecutor {
282
363
  t('browser_forward', 'Navigate the controlled browser forward.', {}, []),
283
364
  t('browser_reload', 'Reload the controlled browser.', {}, []),
284
365
  t('browser_cdp', 'Run a raw Chrome DevTools Protocol command against the controlled browser. Advanced use only.', { method: { type: 'string' }, params: { type: 'object' } }, ['method']),
285
- t('browser_use', 'Native observe-then-act control for Newmark\'s built-in browser. Call observe first, then pass its page_generation, observation_id, and opaque ref to actions. Receipts are owner/runtime scoped; stale observations are rejected. This path does not require arbitrary page scripts or raw CDP.', {
366
+ t('browser_use', 'Native observe-then-act browser control. visible defaults to true and uses the right-sidebar built-in browser. Set visible=false to run on an independent host-owned background page that never connects to, displays, or renders the right-sidebar webview. Keep the same visible value throughout one observe/action sequence. Receipts are owner/runtime/surface scoped; stale observations are rejected.', {
286
367
  action: { type: 'string', enum: browserUseActions },
368
+ visible: { type: 'boolean', description: 'Whether to bind to the visible right-sidebar browser. Defaults to true. false uses an independent background execution surface and does not create or touch the sidebar webview.' },
287
369
  action_id: { type: 'string', description: 'Unique idempotency id for this action. Reusing it returns the original receipt without repeating the action.' },
288
370
  page_generation: { type: 'number', description: 'Generation returned by the latest observe receipt.' },
289
371
  observation_id: { type: 'string', description: 'Opaque observation capability returned by the latest observe receipt.' },
@@ -346,8 +428,9 @@ class ToolExecutor {
346
428
  },
347
429
  },
348
430
  }, ['action']),
349
- t('image_inspect', 'Inspect a durable user-submitted image by stable attachment_id, or use image_index within the latest user message containing images. Use source_info first when dimensions are unknown, then crop with pixel coordinates. Derived crops are current-turn only and are never written to disk.', {
350
- action: { type: 'string', enum: ['source_info', 'crop'] },
431
+ t('image_inspect', 'Inspect a durable user-submitted image by stable attachment_id or latest-message image_index, or send one active-workspace PNG/JPEG to the current validated vision model with action=inspect. Use source_info first when submitted-image dimensions are unknown, then crop with pixel coordinates. Workspace observations and derived crops are current-turn only; image bytes never enter durable tool history.', {
432
+ action: { type: 'string', enum: ['source_info', 'crop', 'inspect'] },
433
+ path: { type: 'string', description: 'For action=inspect, a workspace-relative PNG/JPEG path. Absolute paths are accepted only when they remain inside the active workspace.' },
351
434
  attachment_id: { type: 'string', description: 'Stable user-image attachment id from the visible conversation. Prefer this when revisiting an older submitted image.' },
352
435
  image_index: { type: 'number', description: '1-based image index in the latest user message containing submitted images. Defaults to 1.' },
353
436
  x: { type: 'number', description: 'Crop left edge in source-image pixels.' },
@@ -366,10 +449,11 @@ class ToolExecutor {
366
449
  path: { type: 'string', description: 'For source=image, a workspace PNG/JPEG/BMP path.' },
367
450
  fallback_reason: { type: 'string', enum: ['vision_unavailable', 'vision_failed'] },
368
451
  }, ['source', 'fallback_reason']),
369
- t('pdf_read', 'Read a PDF with enforced fallback order: embedded text layer first; if unreadable, render the requested page in Newmark Browser and send a screenshot to a validated vision model; use bundled Chinese/English OCR only when vision is unavailable, or later through ocr_read after vision failed. Designed for scanned PDFs, not layout/table reconstruction.', {
452
+ t('pdf_read', 'Read a PDF with enforced fallback order: embedded text layer first; if unreadable, render the requested page in Newmark Browser and send a screenshot to a validated vision model; use bundled Chinese/English OCR only when vision is unavailable, or later through ocr_read after vision failed. One cumulative timeout covers the entire PDF read, including file I/O, pdf.js parsing, and rendered-page observation. Designed for scanned PDFs, not layout/table reconstruction.', {
370
453
  path: { type: 'string', description: 'Workspace PDF path.' },
371
454
  page: { type: 'number', minimum: 1, maximum: 100, description: 'Page to render when no usable text layer exists. Defaults to 1.' },
372
455
  max_chars: { type: 'number', minimum: 500, maximum: 100000 },
456
+ timeout_ms: { type: 'number', minimum: 1000, maximum: 120000, description: 'Bounded entire PDF read timeout. Defaults to 30000 ms. A timeout returns a recoverable tool result and does not abort the Agent run.' },
373
457
  }, ['path']),
374
458
  t('terminal_takeover', 'Take over a persistent owner-scoped PTY session that is independent from the one-shot bash tool. Actions: start creates/reuses a named PTY, write sends a command to the same session, read returns its output buffer, resize updates PTY geometry, detach releases the UI attachment without stopping the shell, stop interrupts it, list shows sessions. Use this when the user wants continuous terminal state such as cd/env/process context or interactive TTY programs.', {
375
459
  action: { type: 'string', enum: ['start', 'write', 'read', 'resize', 'detach', 'stop', 'list'] },
@@ -517,6 +601,7 @@ class ToolExecutor {
517
601
  copy.function.description = 'Plan read-only browser: observe, navigate, wait, extract only.';
518
602
  copy.function.parameters.properties = {
519
603
  action: { type: 'string', enum: [...toolPolicy_1.PLAN_BROWSER_USE_ACTIONS] },
604
+ visible: copy.function.parameters.properties.visible,
520
605
  action_id: copy.function.parameters.properties.action_id,
521
606
  page_generation: copy.function.parameters.properties.page_generation,
522
607
  observation_id: copy.function.parameters.properties.observation_id,
@@ -688,6 +773,7 @@ class ToolExecutor {
688
773
  const scope = browserUseScope(context, wsPath);
689
774
  const request = {
690
775
  ...scope,
776
+ visible: typeof args.visible === 'boolean' ? args.visible : true,
691
777
  action: String(args.action || '').trim().toLowerCase(),
692
778
  ...(g('action_id') ? { actionId: g('action_id') } : {}),
693
779
  ...(args.page_generation !== undefined ? { pageGeneration: Number(args.page_generation) } : {}),
@@ -760,47 +846,79 @@ class ToolExecutor {
760
846
  const pdfPath = resolve(g('path'));
761
847
  if (path.extname(pdfPath).toLowerCase() !== '.pdf')
762
848
  return '[pdf_read error] path must end in .pdf.';
763
- const stat = fs.statSync(pdfPath);
764
- if (!stat.isFile() || stat.size <= 0 || stat.size > 250 * 1024 * 1024) {
765
- return '[pdf_read error] PDF must be a regular file no larger than 250 MB.';
766
- }
767
849
  const maxChars = Math.max(500, Math.min(100_000, Number(args.max_chars || 50_000)));
768
- const textLayer = extractPdfTextLayer(fs.readFileSync(pdfPath)).slice(0, maxChars);
769
- const readableCount = (textLayer.match(/[A-Za-z0-9\u3400-\u9fff]/g) || []).length;
770
- if (readableCount >= 20) {
850
+ const page = Math.max(1, Math.min(100, Math.floor(Number(args.page || 1))));
851
+ const timeoutMs = Math.max(1000, Math.min(120_000, Number(args.timeout_ms || 30_000)));
852
+ const guard = abortGuard(context.signal, timeoutMs);
853
+ let stage = 'file_stat';
854
+ try {
855
+ const guardedContext = { ...context, signal: guard.signal };
856
+ const stat = await fs.promises.stat(pdfPath);
857
+ if (!stat.isFile() || stat.size <= 0 || stat.size > 250 * 1024 * 1024) {
858
+ return '[pdf_read error] PDF must be a regular file no larger than 250 MB.';
859
+ }
860
+ stage = 'file_read';
861
+ const bytes = await this.readPdfFile(pdfPath, guard.signal);
862
+ stage = 'text_parse';
863
+ const textLayer = await this.extractPdfText(bytes, maxChars, guard.signal);
864
+ const readableCount = (textLayer.match(/[A-Za-z0-9\u3400-\u9fff]/g) || []).length;
865
+ if (readableCount >= 20) {
866
+ return JSON.stringify({
867
+ ok: true,
868
+ source: 'pdf_text_layer',
869
+ recognition_order: 'text>vision>local_ocr',
870
+ text: textLayer,
871
+ truncated: textLayer.length >= maxChars,
872
+ }, null, 2);
873
+ }
874
+ stage = 'rendered_page_observation';
875
+ const url = `${(0, url_1.pathToFileURL)(pdfPath).toString()}#page=${page}&zoom=page-fit`;
876
+ const opened = await this.browserRun({ action: 'open', url }, guard.signal, guardedContext, wsPath);
877
+ if (!opened.includes('[browser:open] OK')) {
878
+ return JSON.stringify({ ok: false, source: 'pdf_render', code: 'pdf_open_failed', error: opened || 'Unable to open PDF.' }, null, 2);
879
+ }
880
+ await abortableToolDelay(900, guard.signal);
881
+ const observed = await this.execute('browser_use', JSON.stringify({
882
+ action: 'observe',
883
+ action_id: `pdf-read-${crypto.randomUUID()}`,
884
+ max_chars: maxChars,
885
+ max_refs: 80,
886
+ }), wsPath, guardedContext);
887
+ if (guard.signal.aborted)
888
+ throw abortReason(guard.signal);
889
+ let parsed = observed;
890
+ try {
891
+ parsed = JSON.parse(observed);
892
+ }
893
+ catch { }
771
894
  return JSON.stringify({
772
895
  ok: true,
773
- source: 'pdf_text_layer',
896
+ source: 'pdf_rendered_page',
897
+ page,
774
898
  recognition_order: 'text>vision>local_ocr',
775
- text: textLayer,
776
- truncated: textLayer.length >= maxChars,
899
+ result: parsed,
777
900
  }, null, 2);
778
901
  }
779
- const page = Math.max(1, Math.min(100, Math.floor(Number(args.page || 1))));
780
- const url = `${(0, url_1.pathToFileURL)(pdfPath).toString()}#page=${page}&zoom=page-fit`;
781
- const opened = await this.browserRun({ action: 'open', url }, context.signal, context, wsPath);
782
- if (!opened.includes('[browser:open] OK')) {
783
- return JSON.stringify({ ok: false, source: 'pdf_render', error: opened || 'Unable to open PDF.' }, null, 2);
902
+ catch (error) {
903
+ if (context.signal?.aborted)
904
+ throw abortReason(context.signal);
905
+ if (guard.signal.aborted) {
906
+ return JSON.stringify({
907
+ ok: false,
908
+ source: stage === 'rendered_page_observation' ? 'pdf_rendered_page' : 'pdf_read',
909
+ code: 'pdf_read_timeout',
910
+ stage,
911
+ page,
912
+ timeout_ms: timeoutMs,
913
+ recoverable: true,
914
+ error: `PDF read timed out during ${stage} after ${timeoutMs} ms. The Agent run can continue or retry with a larger timeout_ms.`,
915
+ }, null, 2);
916
+ }
917
+ throw error;
784
918
  }
785
- await abortableToolDelay(900, context.signal);
786
- const observed = await this.execute('browser_use', JSON.stringify({
787
- action: 'observe',
788
- action_id: `pdf-read-${crypto.randomUUID()}`,
789
- max_chars: maxChars,
790
- max_refs: 80,
791
- }), wsPath, context);
792
- let parsed = observed;
793
- try {
794
- parsed = JSON.parse(observed);
919
+ finally {
920
+ guard.dispose();
795
921
  }
796
- catch { }
797
- return JSON.stringify({
798
- ok: true,
799
- source: 'pdf_rendered_page',
800
- page,
801
- recognition_order: 'text>vision>local_ocr',
802
- result: parsed,
803
- }, null, 2);
804
922
  }
805
923
  case 'screen_capture': {
806
924
  const target = g('target').toLowerCase() === 'application' ? 'application' : 'desktop';
@@ -1357,6 +1475,9 @@ class ToolExecutor {
1357
1475
  }
1358
1476
  }
1359
1477
  async wsearch(query, signal) {
1478
+ return (await this.wsearchDetailed(query, signal)).text;
1479
+ }
1480
+ async wsearchDetailed(query, signal) {
1360
1481
  const clean = (s) => s
1361
1482
  .replace(/<[^>]+>/g, ' ')
1362
1483
  .replace(/&amp;/g, '&')
@@ -1367,34 +1488,23 @@ class ToolExecutor {
1367
1488
  .replace(/\s+/g, ' ')
1368
1489
  .trim();
1369
1490
  const errors = [];
1491
+ let mcp = { invocationId: '', checkedAt: '', ok: false, attempts: [] };
1370
1492
  try {
1371
- const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
1372
- const guard = abortGuard(signal, 15000);
1373
- let html = '';
1374
- try {
1375
- const resp = await this.proxyFetch(url, {
1376
- headers: { 'User-Agent': 'NewmarkAgent/1.0' },
1377
- signal: guard.signal,
1378
- });
1379
- html = await resp.text();
1493
+ mcp = await this.webSearchMcpOnly(query, signal);
1494
+ if (mcp.ok && mcp.text) {
1495
+ return { invocationId: mcp.invocationId, checkedAt: mcp.checkedAt, ok: true, provider: mcp.provider || 'MCP search', text: mcp.text, attempts: mcp.attempts };
1380
1496
  }
1381
- finally {
1382
- guard.dispose();
1383
- }
1384
- const re = /<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>(.*?)<\/a>[\s\S]*?class="result__snippet">(.*?)<\/a>/g;
1385
- const results = [];
1386
- let m;
1387
- while ((m = re.exec(html)) !== null && results.length < 8) {
1388
- results.push(`${clean(m[2])}\n${clean(m[1])}\n${clean(m[3])}`);
1497
+ for (const attempt of mcp.attempts) {
1498
+ if (attempt.status === 'error')
1499
+ errors.push(`${attempt.name}: ${attempt.error || 'failed'}`);
1500
+ else if (attempt.status === 'empty')
1501
+ errors.push(`${attempt.name}: no results`);
1389
1502
  }
1390
- if (results.length > 0)
1391
- return results.join('\n\n');
1392
- errors.push('DuckDuckGo returned no parseable results');
1393
1503
  }
1394
- catch (e) {
1504
+ catch (error) {
1395
1505
  if (signal?.aborted)
1396
1506
  throw abortReason(signal);
1397
- errors.push(`DuckDuckGo: ${e instanceof Error ? e.message : String(e)}`);
1507
+ errors.push(`Search MCP pool: ${error instanceof Error ? error.message : String(error)}`);
1398
1508
  }
1399
1509
  try {
1400
1510
  const url = `https://www.bing.com/search?q=${encodeURIComponent(query)}`;
@@ -1422,7 +1532,7 @@ class ToolExecutor {
1422
1532
  results.push(`${clean(title[2])}\n${clean(title[1])}\n${snippet ? clean(snippet[1]) : ''}`.trim());
1423
1533
  }
1424
1534
  if (results.length > 0)
1425
- return results.join('\n\n');
1535
+ return { invocationId: mcp.invocationId, checkedAt: mcp.checkedAt, ok: true, provider: 'Bing HTTP', text: results.join('\n\n'), attempts: mcp.attempts };
1426
1536
  errors.push('Bing returned no parseable results');
1427
1537
  }
1428
1538
  catch (e) {
@@ -1430,7 +1540,36 @@ class ToolExecutor {
1430
1540
  throw abortReason(signal);
1431
1541
  errors.push(`Bing: ${e instanceof Error ? e.message : String(e)}`);
1432
1542
  }
1433
- return `[web_search] No results. ${errors.join('; ')}`;
1543
+ try {
1544
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
1545
+ const guard = abortGuard(signal, 15000);
1546
+ let html = '';
1547
+ try {
1548
+ const resp = await this.proxyFetch(url, {
1549
+ headers: { 'User-Agent': 'NewmarkAgent/1.0' },
1550
+ signal: guard.signal,
1551
+ });
1552
+ html = await resp.text();
1553
+ }
1554
+ finally {
1555
+ guard.dispose();
1556
+ }
1557
+ const re = /<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>(.*?)<\/a>[\s\S]*?class="result__snippet">(.*?)<\/a>/g;
1558
+ const results = [];
1559
+ let match;
1560
+ while ((match = re.exec(html)) !== null && results.length < 8) {
1561
+ results.push(`${clean(match[2])}\n${clean(match[1])}\n${clean(match[3])}`);
1562
+ }
1563
+ if (results.length > 0)
1564
+ return { invocationId: mcp.invocationId, checkedAt: mcp.checkedAt, ok: true, provider: 'DuckDuckGo HTTP', text: results.join('\n\n'), attempts: mcp.attempts };
1565
+ errors.push('DuckDuckGo returned no parseable results');
1566
+ }
1567
+ catch (e) {
1568
+ if (signal?.aborted)
1569
+ throw abortReason(signal);
1570
+ errors.push(`DuckDuckGo: ${e instanceof Error ? e.message : String(e)}`);
1571
+ }
1572
+ return { invocationId: mcp.invocationId, checkedAt: mcp.checkedAt, ok: false, provider: '', text: `[web_search] No results. ${errors.join('; ')}`, attempts: mcp.attempts };
1434
1573
  }
1435
1574
  async wfetch(url, signal) {
1436
1575
  try {
@@ -20,7 +20,7 @@ const navigation = [
20
20
  const workspace = {
21
21
  id: "workspace-newmark-agent-demo",
22
22
  name: "Newmark Agent",
23
- path: "C:\\Users\\12252\\Desktop\\Files\\Code\\Newmark Agent",
23
+ path: "C:\\Users\\DemoUser\\Projects\\Newmark Agent",
24
24
  isInternal: false,
25
25
  hostBinding: "demo-host",
26
26
  icon: "[W]",
@@ -40,7 +40,7 @@ const workspaces = [
40
40
  {
41
41
  id: "workspace-condensed-lab-demo",
42
42
  name: "Condensed Lab",
43
- path: "C:\\Users\\12252\\Desktop\\Files\\Condensed Lab",
43
+ path: "C:\\Users\\DemoUser\\Projects\\Condensed Lab",
44
44
  isInternal: false,
45
45
  hostBinding: "demo-host",
46
46
  icon: "[W]",
@@ -5894,6 +5894,14 @@ button.diff-block-header:active:not(:disabled),
5894
5894
  button.work-review-head:active:not(:disabled) {
5895
5895
  scale: 1;
5896
5896
  filter: none;
5897
+ }
5898
+ button.conversation-work-run-head:active:not(:disabled) {
5899
+ background: transparent !important;
5900
+ background-image: none !important;
5901
+ }
5902
+ button.shell-block-header:active:not(:disabled),
5903
+ button.diff-block-header:active:not(:disabled),
5904
+ button.work-review-head:active:not(:disabled) {
5897
5905
  background-color: revert !important;
5898
5906
  }
5899
5907
 
@@ -10034,7 +10042,9 @@ var STREAMING_LARGE_TEXT_INTERVAL_MS = 100;
10034
10042
 
10035
10043
  function updateMsg(div, text, role, mode, model, options) {
10036
10044
  if (!div) return;
10045
+ var preserveRunFinalResponse = div.classList && div.classList.contains('run-final-response');
10037
10046
  if (role) div.className = 'chat-msg ' + role;
10047
+ if (preserveRunFinalResponse) div.classList.add('run-final-response');
10038
10048
  text = redactSensitiveText(text);
10039
10049
  div._newmarkMessageText = String(text || '');
10040
10050
  var body = div.querySelector('.msg-body');
@@ -11232,6 +11242,7 @@ function presentedWorkRunEvents(run, includeGuides) {
11232
11242
  var rawEvents = (run.events || []).filter(publicWorkEvent).filter(function(event) {
11233
11243
  return includeGuides !== false || !(String(event && event.type || '').toLowerCase().indexOf('guide') === 0 || !!(event && event.guide));
11234
11244
  }).sort(compareConversationWorkEvents);
11245
+ var terminalAssistantResponse = terminalAssistantResponseForRun(run);
11235
11246
  var terminalInterrupted = ['interrupted', 'force_interrupted'].indexOf(String(run && run.status || '')) >= 0;
11236
11247
  var events = [];
11237
11248
  var publicText = '';
@@ -11258,6 +11269,10 @@ function presentedWorkRunEvents(run, includeGuides) {
11258
11269
  // response boundary, while retaining every distinct provider reply.
11259
11270
  publicText = '';
11260
11271
  while (events.length && ['public_text', 'partial_text'].indexOf(String(events[events.length - 1].type || '').toLowerCase()) >= 0) events.pop();
11272
+ // If an interrupted run never produced final_response, its last public
11273
+ // response becomes the standalone Agent boundary below the Build. Do
11274
+ // not keep the same text inside the expanded Build as a duplicate.
11275
+ if (terminalAssistantResponse && terminalAssistantResponse.interrupted && terminalAssistantResponse.event === rawEvent) continue;
11261
11276
  events.push(rawEvent);
11262
11277
  continue;
11263
11278
  }
@@ -12011,6 +12026,47 @@ function renderConversationHistoryFirst(target) {
12011
12026
  }
12012
12027
 
12013
12028
  function renderChatMessages(messages, target) {
12029
+ var liveArea = els['chat-area'];
12030
+ if (!liveArea) return;
12031
+ var previousRendered = Array.isArray(state.renderedChatMessages) ? state.renderedChatMessages.slice() : [];
12032
+ var previousHtml = liveArea.innerHTML;
12033
+ var previousScrollTop = liveArea.scrollTop;
12034
+ var wasAtBottom = shouldAutoScroll(liveArea);
12035
+ var stagingArea = liveArea.cloneNode(false);
12036
+ els['chat-area'] = stagingArea;
12037
+ try {
12038
+ renderChatMessagesUnsafe(messages, target);
12039
+ liveArea.replaceChildren.apply(liveArea, Array.prototype.slice.call(stagingArea.childNodes));
12040
+ if (wasAtBottom) _chatShouldAutoScroll = true;
12041
+ else liveArea.scrollTop = previousScrollTop;
12042
+ } catch (error) {
12043
+ state.renderedChatMessages = previousRendered;
12044
+ liveArea.innerHTML = previousHtml;
12045
+ liveArea.scrollTop = previousScrollTop;
12046
+ console.error('[ui_chat_render_failed]', error);
12047
+ showUiNotice(currentLang() === 'zh' ? '对话渲染出现异常,已保留原内容。' : 'Chat rendering failed; the previous content was preserved.', 'error', 'chat-render-failed');
12048
+ } finally {
12049
+ els['chat-area'] = liveArea;
12050
+ }
12051
+ }
12052
+
12053
+ function terminalAssistantResponseForRun(run) {
12054
+ var events = Array.isArray(run && run.events) ? run.events : [];
12055
+ for (var finalIndex = events.length - 1; finalIndex >= 0; finalIndex--) {
12056
+ if (String(events[finalIndex] && events[finalIndex].type || '').toLowerCase() !== 'final_response') continue;
12057
+ var finalContent = String(events[finalIndex] && events[finalIndex].content || '').trim();
12058
+ if (finalContent) return { content: finalContent, timestamp: events[finalIndex].timestamp || run.endedAt || '', interrupted: false, event: events[finalIndex] };
12059
+ }
12060
+ if (['interrupted', 'force_interrupted'].indexOf(String(run && run.status || '').toLowerCase()) < 0) return null;
12061
+ for (var responseIndex = events.length - 1; responseIndex >= 0; responseIndex--) {
12062
+ if (String(events[responseIndex] && events[responseIndex].type || '').toLowerCase() !== 'response') continue;
12063
+ var responseContent = String(events[responseIndex] && events[responseIndex].content || '').trim();
12064
+ if (responseContent) return { content: responseContent, timestamp: events[responseIndex].timestamp || run.endedAt || '', interrupted: true, event: events[responseIndex] };
12065
+ }
12066
+ return null;
12067
+ }
12068
+
12069
+ function renderChatMessagesUnsafe(messages, target) {
12014
12070
  var renderTarget = target || currentConversationTarget(activeConversationId());
12015
12071
  var normalizedMessages = cacheConversationMessages(messages, renderTarget);
12016
12072
  state.renderedChatMessages = normalizedMessages;
@@ -12035,6 +12091,7 @@ function renderChatMessages(messages, target) {
12035
12091
  var persistedRunsById = {};
12036
12092
  var persistedRunIndexes = {};
12037
12093
  var renderedRunIds = {};
12094
+ var recoveredAssistantRunIds = {};
12038
12095
  var messageRunRoles = {};
12039
12096
  state.guideMessageIndexByClientId = {};
12040
12097
  persistedRuns.forEach(function(run, runIndex) {
@@ -12053,26 +12110,20 @@ function renderChatMessages(messages, target) {
12053
12110
  if (!messageRunRoles[runId]) messageRunRoles[runId] = {};
12054
12111
  if (role === 'user' || role === 'assistant') messageRunRoles[runId][role] = true;
12055
12112
  });
12056
- function finalResponseForRun(run) {
12057
- var events = Array.isArray(run && run.events) ? run.events : [];
12058
- for (var eventIndex = events.length - 1; eventIndex >= 0; eventIndex--) {
12059
- if (String(events[eventIndex] && events[eventIndex].type || '').toLowerCase() !== 'final_response') continue;
12060
- var content = String(events[eventIndex] && events[eventIndex].content || '').trim();
12061
- if (content) return { content: content, timestamp: events[eventIndex].timestamp || run.endedAt || '' };
12062
- }
12063
- return null;
12064
- }
12065
12113
  function renderOwnedWorkRun(run, includeRecoveredFinal) {
12066
12114
  var runId = String(run && run.runId || '');
12067
- if (!runId || renderedRunIds[runId]) return;
12115
+ if (!runId) return;
12068
12116
  var roles = messageRunRoles[runId] || {};
12069
- if (!roles.user && String(run.primaryPrompt || '').trim()) {
12070
- addMsg('user', run.primaryPrompt, '', '', -1, { runId: runId, target: renderTarget, timestamp: run.startedAt || '', recovered: true });
12117
+ if (!renderedRunIds[runId]) {
12118
+ if (!roles.user && String(run.primaryPrompt || '').trim()) {
12119
+ addMsg('user', run.primaryPrompt, '', '', -1, { runId: runId, target: renderTarget, timestamp: run.startedAt || '', recovered: true });
12120
+ }
12121
+ renderConversationWorkRun(run, workRunBeforeElement(run));
12122
+ renderedRunIds[runId] = true;
12071
12123
  }
12072
- renderConversationWorkRun(run, workRunBeforeElement(run));
12073
- renderedRunIds[runId] = true;
12074
- if (includeRecoveredFinal && !roles.assistant) {
12075
- var recoveredFinal = finalResponseForRun(run);
12124
+ if (includeRecoveredFinal && !roles.assistant && !recoveredAssistantRunIds[runId]) {
12125
+ recoveredAssistantRunIds[runId] = true;
12126
+ var recoveredFinal = terminalAssistantResponseForRun(run);
12076
12127
  if (recoveredFinal) addMsg('assistant', recoveredFinal.content, '', '', -1, { runId: runId, target: renderTarget, timestamp: recoveredFinal.timestamp, recovered: true });
12077
12128
  }
12078
12129
  }
@@ -12354,6 +12405,18 @@ function renderAgentWorkEvent(event) {
12354
12405
  var finalMsg = addMsg('assistant', content, event.mode || state.mode, event.model || state.model, undefined, { runId: event.runId || '', target: workRun.target || currentConversationTarget(eventConversationId) });
12355
12406
  markFinalResponseMsg(eventConversationId, content, finalMsg);
12356
12407
  }
12408
+ } else if (workRun && (['interrupted', 'force_interrupted'].indexOf(String(type).toLowerCase()) >= 0
12409
+ || (type === 'status' && ['interrupted', 'force_interrupted'].indexOf(String(event.status || workRun.status || '').toLowerCase()) >= 0))) {
12410
+ ui.activeWorkflowText = '';
12411
+ ui.activeWorkflowMsg = null;
12412
+ ui._streamFlushPending = false;
12413
+ ui._lastLargeStreamRenderAt = 0;
12414
+ var interruptedResponse = terminalAssistantResponseForRun(workRun);
12415
+ if (interruptedResponse && interruptedResponse.content) {
12416
+ var interruptedMsg = addMsg('assistant', interruptedResponse.content, event.mode || state.mode, event.model || state.model, undefined, { runId: event.runId || workRun.runId || '', target: workRun.target || currentConversationTarget(eventConversationId) });
12417
+ markFinalResponseMsg(eventConversationId, interruptedResponse.content, interruptedMsg);
12418
+ }
12419
+ finishToolBatch(eventConversationId);
12357
12420
  } else if (type === 'tool_call' && !workRun) {
12358
12421
  upsertToolEvent(event, '');
12359
12422
  } else if (type === 'tool_result' && !workRun) {
@@ -16176,7 +16239,7 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
16176
16239
  applyAutoRouteRatingState(s);
16177
16240
  hydrateConversationBranchState(s);
16178
16241
  syncWorkRunsSnapshot(s.workRuns, lockedTarget);
16179
- renderChatMessages(s.chatMessages || []);
16242
+ if (Array.isArray(s.chatMessages)) renderChatMessages(s.chatMessages, lockedTarget);
16180
16243
  }
16181
16244
  if (s && s.queued) {
16182
16245
  var refreshedQueue = setBackendQueueForTarget(s.queued, lockedTarget);