newmark-agent 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -33,7 +33,12 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.configureHostedServer = configureHostedServer;
37
+ exports.stopHostedServer = stopHostedServer;
38
+ exports.hostedServerStatus = hostedServerStatus;
39
+ exports.setHostedServerEnabled = setHostedServerEnabled;
36
40
  exports.runServer = runServer;
41
+ exports.setHostedServerAutomation = setHostedServerAutomation;
37
42
  const http = __importStar(require("http"));
38
43
  const fs = __importStar(require("fs"));
39
44
  const path = __importStar(require("path"));
@@ -56,6 +61,19 @@ let mobileToken = '';
56
61
  let appRoot = '';
57
62
  const mobileWorkEventSubscribers = new Set();
58
63
  const mobileScopedRuntimes = new Map();
64
+ let hostedWorkEventSink = null;
65
+ let unsubscribeHostedWorkEvents = null;
66
+ let unsubscribeServerAgentWorkEvents = null;
67
+ let hostedServer = null;
68
+ let hostedServerRoot = '';
69
+ let hostedServerOptions = {};
70
+ let hostedServerError = '';
71
+ let hostedServerStartedAt = 0;
72
+ let hostedAccessHost = '';
73
+ let hostedAccessHostCheckedAt = 0;
74
+ let hostedConversationUiState;
75
+ let hostedConversationUiAction;
76
+ let hostedConversationPrompt;
59
77
  function mobileRuntimeKey(workspaceId, conversationId) {
60
78
  return `${String(workspaceId || '')}::${String(conversationId || '')}`;
61
79
  }
@@ -67,6 +85,10 @@ function publishMobileWorkEvent(event) {
67
85
  catch { /* ignore disconnected mobile listeners */ }
68
86
  }
69
87
  }
88
+ function publishServerWorkEvent(event) {
89
+ publishMobileWorkEvent(event);
90
+ hostedWorkEventSink?.(event);
91
+ }
70
92
  function mobileAuthorized(req) {
71
93
  if (!mobileToken)
72
94
  return false;
@@ -837,6 +859,9 @@ async function handleApi(req, res, body) {
837
859
  mode: agent.mode,
838
860
  model: agent.modelSelectionValue(),
839
861
  modelLabel: agent.modelLabel(),
862
+ models: agent.allModelNames(),
863
+ providers: (0, config_1.sanitizeProvidersForState)(agent.config.providers()),
864
+ intelligence: agent.intelligence,
840
865
  status: agent.status,
841
866
  activeConversationId: agent.activeConversationId,
842
867
  conversations: agent.listConversationStates(),
@@ -876,9 +901,18 @@ async function handleApi(req, res, body) {
876
901
  mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
877
902
  return;
878
903
  }
879
- const snapshot = current.getConversationSnapshot(String(conversationId), { window: windowSize, before, workspace: ws });
904
+ // GUI conversations normally execute inside isolated runtime pools.
905
+ // Their durable target state can be newer than the hosted main Agent's
906
+ // foreground memory, so workspace-bound mobile reads must hydrate a
907
+ // fresh read-only target Agent from storage.
908
+ const snapshotAgent = ws ? mobileScopedAgent(ws, String(conversationId)) : current;
909
+ const snapshot = snapshotAgent.getConversationSnapshot(String(conversationId), {
910
+ window: windowSize,
911
+ before,
912
+ workspace: ws || undefined,
913
+ });
880
914
  // 完整对话信息:workRuns 统一用持久化记录透出(含被中断的构建)
881
- const persistedRuns = current.getPersistedConversationWorkRuns(String(conversationId), ws || undefined);
915
+ const persistedRuns = snapshotAgent.getPersistedConversationWorkRuns(String(conversationId), ws || undefined);
882
916
  if (persistedRuns.length)
883
917
  snapshot.workRuns = persistedRuns;
884
918
  mobileJson(res, snapshot);
@@ -915,6 +949,36 @@ async function handleApi(req, res, body) {
915
949
  mobileJson(res, mobileRightSidebarState(agent, ws, conversationId));
916
950
  return;
917
951
  }
952
+ case '/api/mobile/conversation-ui-state': {
953
+ const workspaceId = url.searchParams.get('workspaceId') || '';
954
+ const conversationId = url.searchParams.get('conversationId') || '';
955
+ const ws = resolveMobileWorkspace(agent, workspaceId);
956
+ if (!ws) {
957
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
958
+ return;
959
+ }
960
+ if (!conversationId || !agent.hasConversationInWorkspace(conversationId, ws)) {
961
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
962
+ return;
963
+ }
964
+ if (hostedConversationUiState) {
965
+ mobileJson(res, await hostedConversationUiState({ workspaceId, conversationId }));
966
+ return;
967
+ }
968
+ const scoped = mobileScopedAgent(ws, conversationId);
969
+ const snapshot = scoped.getConversationSnapshot(conversationId, { workspace: ws, window: 1 });
970
+ mobileJson(res, {
971
+ goal: snapshot.goal,
972
+ flowSelection: snapshot.flowSelection,
973
+ flow: null,
974
+ queued: { steering: [], followUp: [] },
975
+ runtime: null,
976
+ mode: scoped.mode,
977
+ status: scoped.status,
978
+ inputMode: scoped.inputMode,
979
+ });
980
+ return;
981
+ }
918
982
  case '/api/mobile/workspace-files': {
919
983
  const workspaceId = url.searchParams.get('workspaceId') || '';
920
984
  const relativePath = url.searchParams.get('path') || '';
@@ -1030,6 +1094,52 @@ async function handleApi(req, res, body) {
1030
1094
  });
1031
1095
  return;
1032
1096
  }
1097
+ case '/api/mobile/model': {
1098
+ const next = String(JSON.parse(body || '{}').model || '');
1099
+ if (!next) {
1100
+ mobileJson(res, { error: 'Model is required' }, 400);
1101
+ return;
1102
+ }
1103
+ agent.setModel(next);
1104
+ mobileJson(res, {
1105
+ ok: true,
1106
+ model: agent.modelSelectionValue(),
1107
+ modelLabel: agent.modelLabel(),
1108
+ });
1109
+ return;
1110
+ }
1111
+ case '/api/mobile/intelligence': {
1112
+ const tier = String(JSON.parse(body || '{}').tier || 'medium');
1113
+ agent.setIntelligence(tier, true);
1114
+ mobileJson(res, { ok: true, intelligence: agent.intelligence });
1115
+ return;
1116
+ }
1117
+ case '/api/mobile/conversation-ui-action': {
1118
+ const params = JSON.parse(body || '{}');
1119
+ const workspaceId = String(params.workspaceId || '');
1120
+ const conversationId = String(params.conversationId || '');
1121
+ const action = String(params.action || '');
1122
+ const allowed = new Set(['goal_update', 'goal_guide', 'conversation_guide', 'goal_toggle_pause', 'goal_clear', 'flow_pause', 'flow_resume', 'flow_guide', 'conversation_stop', 'input_mode', 'queue_enqueue', 'queue_update', 'queue_delete', 'queue_reorder', 'queue_toggle_pause', 'queue_guide']);
1123
+ if (!workspaceId || !conversationId || !allowed.has(action)) {
1124
+ mobileJson(res, { error: 'workspaceId, conversationId, and a valid action are required' }, 400);
1125
+ return;
1126
+ }
1127
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1128
+ if (!ws) {
1129
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1130
+ return;
1131
+ }
1132
+ if (!agent.hasConversationInWorkspace(conversationId, ws)) {
1133
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
1134
+ return;
1135
+ }
1136
+ if (!hostedConversationUiAction) {
1137
+ mobileJson(res, { error: 'This action requires the GUI-hosted mobile server' }, 409);
1138
+ return;
1139
+ }
1140
+ mobileJson(res, await hostedConversationUiAction({ workspaceId, conversationId }, action, String(params.value || ''), params));
1141
+ return;
1142
+ }
1033
1143
  case '/api/mobile/conversation-rename': {
1034
1144
  const params = JSON.parse(body || '{}');
1035
1145
  const workspaceId = String(params.workspaceId || '');
@@ -1144,6 +1254,15 @@ async function handleApi(req, res, body) {
1144
1254
  requestAgent = mobileScopedAgent(ws, conversationId);
1145
1255
  }
1146
1256
  }
1257
+ if (hostedConversationPrompt && resolvedWorkspace) {
1258
+ const result = await hostedConversationPrompt({ workspaceId, conversationId }, message, {
1259
+ requestedMode: String(params.requestedMode || ''),
1260
+ goalObjective: String(params.goalObjective || ''),
1261
+ inputMode: String(params.inputMode || ''),
1262
+ });
1263
+ mobileJson(res, { ok: true, conversationId, ...result });
1264
+ return;
1265
+ }
1147
1266
  if (requestAgent === agent && params.conversationId)
1148
1267
  requestAgent.setConversation(conversationId);
1149
1268
  let scopedRuntimeKey = '';
@@ -1151,7 +1270,7 @@ async function handleApi(req, res, body) {
1151
1270
  let unsubscribeScoped = null;
1152
1271
  if (requestAgent !== agent && requestWorkspace) {
1153
1272
  scopedRuntimeKey = mobileRuntimeKey(String(requestWorkspace.id || ''), conversationId);
1154
- unsubscribeScoped = requestAgent.subscribeWorkEvents(event => publishMobileWorkEvent({
1273
+ unsubscribeScoped = requestAgent.subscribeWorkEvents(event => publishServerWorkEvent({
1155
1274
  ...event,
1156
1275
  workspaceId: String(requestWorkspace?.id || ''),
1157
1276
  conversationId,
@@ -1302,40 +1421,52 @@ async function handleApi(req, res, body) {
1302
1421
  jsonResponse(res, { error: e.message }, 500);
1303
1422
  }
1304
1423
  }
1305
- function startServer(root) {
1424
+ function startServer(root, options = {}) {
1306
1425
  mobileToken = (0, mobilePairing_1.ensureMobileToken)(root);
1307
1426
  appRoot = root;
1308
- agent = new agent_1.Agent(root);
1427
+ agent = options.agent || new agent_1.Agent(root);
1428
+ hostedWorkEventSink = options.onWorkEvent || null;
1429
+ hostedConversationUiState = options.conversationUiState;
1430
+ hostedConversationUiAction = options.conversationUiAction;
1431
+ hostedConversationPrompt = options.conversationPrompt;
1432
+ unsubscribeHostedWorkEvents?.();
1433
+ unsubscribeHostedWorkEvents = options.subscribeWorkEvents?.(publishMobileWorkEvent) || null;
1309
1434
  workspaceFileRouter = new workspaceFileRouter_1.WorkspaceFileRouter(() => path.resolve(agent?.workspace.current?.path || root));
1310
- automation = new automation_1.AutomationManager(agent.config, async (prompt, model, item) => {
1311
- if (!agent)
1312
- return '';
1313
- const previousModel = agent.model;
1314
- const previousWorkspace = agent.workspace.current?.id || agent.workspace.current?.path || '';
1315
- const previousConversation = agent.activeConversationId;
1316
- agent.selectWorkspaceFromStorage(item.workspaceId);
1317
- const conversationId = item.conversationMode === 'existing'
1318
- ? item.conversationId
1319
- : `automation-${item.id}-${Date.now().toString(36)}`;
1320
- agent.setConversationFromStorage(conversationId);
1321
- automation?.update(item.id, { lastConversationId: conversationId });
1322
- if (model)
1323
- agent.setModel(model);
1324
- try {
1325
- const tokens = await agent.process(prompt);
1326
- return tokens.map(t => t.text).join('');
1327
- }
1328
- finally {
1435
+ automation = options.automation || null;
1436
+ if (!automation && !options.agent)
1437
+ automation = new automation_1.AutomationManager(agent.config, async (prompt, model, item) => {
1438
+ if (!agent)
1439
+ return '';
1440
+ const previousModel = agent.model;
1441
+ const previousWorkspace = agent.workspace.current?.id || agent.workspace.current?.path || '';
1442
+ const previousConversation = agent.activeConversationId;
1443
+ agent.selectWorkspaceFromStorage(item.workspaceId);
1444
+ const conversationId = item.conversationMode === 'existing'
1445
+ ? item.conversationId
1446
+ : `automation-${item.id}-${Date.now().toString(36)}`;
1447
+ agent.setConversationFromStorage(conversationId);
1448
+ automation?.update(item.id, { lastConversationId: conversationId });
1329
1449
  if (model)
1330
- agent.setModel(previousModel);
1331
- if (previousWorkspace)
1332
- agent.selectWorkspaceFromStorage(previousWorkspace);
1333
- agent.setConversationFromStorage(previousConversation);
1334
- }
1335
- });
1336
- agent.subscribeWorkEvents(publishMobileWorkEvent);
1337
- agent.setAutomationManager(automation);
1338
- automation.start();
1450
+ agent.setModel(model);
1451
+ try {
1452
+ const tokens = await agent.process(prompt);
1453
+ return tokens.map(t => t.text).join('');
1454
+ }
1455
+ finally {
1456
+ if (model)
1457
+ agent.setModel(previousModel);
1458
+ if (previousWorkspace)
1459
+ agent.selectWorkspaceFromStorage(previousWorkspace);
1460
+ agent.setConversationFromStorage(previousConversation);
1461
+ }
1462
+ });
1463
+ unsubscribeServerAgentWorkEvents?.();
1464
+ unsubscribeServerAgentWorkEvents = agent.subscribeWorkEvents(publishServerWorkEvent);
1465
+ if (automation) {
1466
+ agent.setAutomationManager(automation);
1467
+ if (!options.automation)
1468
+ automation.start();
1469
+ }
1339
1470
  const uiDir = path.join(__dirname, 'ui');
1340
1471
  const server = http.createServer(async (req, res) => {
1341
1472
  if (req.method === 'OPTIONS') {
@@ -1374,6 +1505,14 @@ function startServer(root) {
1374
1505
  const lan = (0, mobilePairing_1.lanIpv4)();
1375
1506
  const accessHost = tailscale || lan || '<lan-or-tailscale-ip>';
1376
1507
  const tokenPath = path.join(root, '.newmark-mobile-token');
1508
+ hostedServer = server;
1509
+ hostedServerError = '';
1510
+ hostedServerStartedAt = Date.now();
1511
+ server.once('error', error => {
1512
+ hostedServerError = error instanceof Error ? error.message : String(error);
1513
+ if (hostedServer === server)
1514
+ hostedServer = null;
1515
+ });
1377
1516
  server.listen(PORT, bindHost, () => {
1378
1517
  console.log(`\n Newmark Agent v1.0 - Server Mode`);
1379
1518
  console.log(` Bind: ${bindHost}:${PORT}`);
@@ -1385,13 +1524,140 @@ function startServer(root) {
1385
1524
  console.log(` Mobile events (SSE): http://${accessHost}:${PORT}/api/mobile/events?token=<token>`);
1386
1525
  console.log(` Press Ctrl+C to stop\n`);
1387
1526
  });
1527
+ return server;
1528
+ }
1529
+ function configuredAccessHost() {
1530
+ const now = Date.now();
1531
+ if (now - hostedAccessHostCheckedAt < 30_000)
1532
+ return hostedAccessHost;
1533
+ hostedAccessHost = (0, mobilePairing_1.tailscaleIpv4)() || (0, mobilePairing_1.lanIpv4)() || '';
1534
+ hostedAccessHostCheckedAt = now;
1535
+ return hostedAccessHost;
1536
+ }
1537
+ function waitForHostedServerListening(timeoutMs = 5000) {
1538
+ if (hostedServer?.listening)
1539
+ return Promise.resolve();
1540
+ return new Promise((resolve, reject) => {
1541
+ const deadline = Date.now() + timeoutMs;
1542
+ const tick = () => {
1543
+ if (hostedServer?.listening)
1544
+ return resolve();
1545
+ if (hostedServerError)
1546
+ return reject(new Error(hostedServerError));
1547
+ if (Date.now() >= deadline)
1548
+ return reject(new Error('Mobile server listen timeout'));
1549
+ setTimeout(tick, 50);
1550
+ };
1551
+ tick();
1552
+ });
1553
+ }
1554
+ async function probeHostedServer(host) {
1555
+ if (!hostedServer?.listening || !mobileToken)
1556
+ return { ok: false, error: hostedServerError || 'Mobile server is not listening' };
1557
+ return await new Promise(resolve => {
1558
+ const request = http.get({
1559
+ hostname: host,
1560
+ port: PORT,
1561
+ path: `/api/mobile/hello?token=${encodeURIComponent(mobileToken)}`,
1562
+ timeout: 1800,
1563
+ }, response => {
1564
+ let body = '';
1565
+ response.setEncoding('utf8');
1566
+ response.on('data', chunk => { body += chunk; });
1567
+ response.on('end', () => {
1568
+ try {
1569
+ const parsed = JSON.parse(body || '{}');
1570
+ const ok = response.statusCode === 200 && parsed.ok === true;
1571
+ resolve({ ok, error: ok ? '' : String(parsed.error || `HTTP ${response.statusCode || 0}`) });
1572
+ }
1573
+ catch (error) {
1574
+ resolve({ ok: false, error: error instanceof Error ? error.message : String(error) });
1575
+ }
1576
+ });
1577
+ });
1578
+ request.on('timeout', () => request.destroy(new Error('Mobile server probe timeout')));
1579
+ request.on('error', error => resolve({ ok: false, error: error.message }));
1580
+ });
1581
+ }
1582
+ function configureHostedServer(root, options = {}) {
1583
+ hostedServerRoot = root;
1584
+ hostedServerOptions = options;
1585
+ }
1586
+ async function stopHostedServer() {
1587
+ const server = hostedServer;
1588
+ hostedServer = null;
1589
+ if (server) {
1590
+ await new Promise(resolve => {
1591
+ const timer = setTimeout(resolve, 2500);
1592
+ server.close(() => {
1593
+ clearTimeout(timer);
1594
+ resolve();
1595
+ });
1596
+ server.closeAllConnections?.();
1597
+ });
1598
+ }
1599
+ unsubscribeHostedWorkEvents?.();
1600
+ unsubscribeHostedWorkEvents = null;
1601
+ unsubscribeServerAgentWorkEvents?.();
1602
+ unsubscribeServerAgentWorkEvents = null;
1603
+ hostedServerStartedAt = 0;
1604
+ }
1605
+ async function hostedServerStatus(enabled = true, probeHost = '') {
1606
+ const listening = !!hostedServer?.listening;
1607
+ const host = enabled ? (String(probeHost || '').trim() || configuredAccessHost()) : '';
1608
+ let reachable = false;
1609
+ let error = hostedServerError;
1610
+ if (enabled && listening) {
1611
+ if (!host)
1612
+ error = 'No LAN or Tailscale IPv4 address is available';
1613
+ else {
1614
+ const probe = await probeHostedServer(host);
1615
+ reachable = probe.ok;
1616
+ if (!probe.ok)
1617
+ error = probe.error;
1618
+ }
1619
+ }
1620
+ const state = !enabled ? 'off' : listening && reachable ? 'listening' : 'error';
1621
+ return {
1622
+ enabled,
1623
+ listening,
1624
+ reachable,
1625
+ state,
1626
+ host,
1627
+ port: PORT,
1628
+ error: state === 'error' ? (error || 'Mobile server is not reachable') : '',
1629
+ checkedAt: new Date().toISOString(),
1630
+ startedAt: hostedServerStartedAt,
1631
+ };
1632
+ }
1633
+ async function setHostedServerEnabled(enabled, probeHost = '') {
1634
+ await stopHostedServer();
1635
+ hostedServerError = '';
1636
+ if (enabled) {
1637
+ if (!hostedServerRoot)
1638
+ throw new Error('Hosted mobile server is not configured');
1639
+ startServer(hostedServerRoot, hostedServerOptions);
1640
+ try {
1641
+ await waitForHostedServerListening();
1642
+ }
1643
+ catch (error) {
1644
+ hostedServerError = error instanceof Error ? error.message : String(error);
1645
+ }
1646
+ }
1647
+ return await hostedServerStatus(enabled, probeHost);
1388
1648
  }
1389
- let hostedServerStarted = false;
1390
1649
  /** 托管启动 server(GUI/TUI 内嵌调用;幂等防重入,进程常驻即服务常驻) */
1391
- function runServer(root) {
1392
- if (hostedServerStarted)
1650
+ function runServer(root, options = {}) {
1651
+ if (!hostedServerRoot || Object.keys(options).length > 0)
1652
+ configureHostedServer(root, options);
1653
+ else
1654
+ hostedServerRoot = root;
1655
+ if (hostedServer?.listening)
1393
1656
  return;
1394
- hostedServerStarted = true;
1395
- startServer(root);
1657
+ startServer(hostedServerRoot, hostedServerOptions);
1658
+ }
1659
+ /** Attach the GUI-owned automation manager after deferred startup. */
1660
+ function setHostedServerAutomation(manager) {
1661
+ automation = manager;
1396
1662
  }
1397
1663
  //# sourceMappingURL=server.js.map
@@ -294,6 +294,13 @@ class ToolExecutor {
294
294
  max_refs: { type: 'number' },
295
295
  attribute: { type: 'string' },
296
296
  }, ['action']),
297
+ t('screen_capture', 'Read-only active Windows screenshot available without provisioning or starting full Computer Use. Capture the whole desktop or one visible application. The image is a one-use model input and is deleted immediately; semantic Windows UI objects are returned alongside it.', {
298
+ target: { type: 'string', enum: ['desktop', 'application'], description: 'Capture the whole desktop or one visible application. Defaults to desktop.' },
299
+ app_target: { type: 'string', description: 'For target=application: application title, process name, process id, or window handle.' },
300
+ capture_max_width: { type: 'number', minimum: 320, maximum: 2048, description: 'Maximum ephemeral screenshot width; defaults to 1280. Aspect ratio is preserved and the source is never enlarged.' },
301
+ capture_max_height: { type: 'number', minimum: 240, maximum: 2048, description: 'Maximum ephemeral screenshot height; defaults to 960. Aspect ratio is preserved and the source is never enlarged.' },
302
+ max_chars: { type: 'number', minimum: 1000, maximum: 100000, description: 'Maximum returned semantic UI text characters.' },
303
+ }, []),
297
304
  t('computer_use', 'Native Windows desktop control with persistent observation/action helpers. Use observe/app_observe before acting. sequence may perform up to three stable low-risk move/click/scroll/wait/app_activate steps and stops when focus, window, menu, dialog, scene, or risk changes. Vision screenshots remain one-time inputs and are deleted immediately.', {
298
305
  action: { type: 'string', enum: ['observe', 'app_list', 'app_observe', 'sequence', 'takeover_start', 'takeover_stop', 'move', 'click', 'scroll', 'type', 'key', 'wait', 'app_activate', 'app_click', 'app_scroll', 'app_type', 'app_key'] },
299
306
  capture_max_width: { type: 'number', minimum: 320, maximum: 2048, description: 'For observe/app_observe only. Maximum ephemeral screenshot width; defaults to 1280. Aspect ratio is preserved and the source is never enlarged.' },
@@ -790,6 +797,44 @@ class ToolExecutor {
790
797
  result: parsed,
791
798
  }, null, 2);
792
799
  }
800
+ case 'screen_capture': {
801
+ const target = g('target').toLowerCase() === 'application' ? 'application' : 'desktop';
802
+ if (process.env.NEWMARK_WSL_DISTRO) {
803
+ const result = await (0, wslHostToolBridge_1.requestWindowsHostTool)('screen_capture', args, {
804
+ conversationId: context.conversationId || 'default',
805
+ workspaceId: context.workspaceId || (0, terminalTakeover_1.terminalTakeoverWorkspaceId)(context.workspacePath || wsPath),
806
+ actorId: context.actorId || terminalTakeover_1.ROOT_TERMINAL_ACTOR_ID,
807
+ runtimeKey: browserUseScope(context, wsPath).runtimeKey,
808
+ allowEphemeralVisionImage: context.allowEphemeralVisionImage === true,
809
+ mode: context.mode || 'build',
810
+ }, 120_000, context.signal);
811
+ return typeof result === 'string' ? result : JSON.stringify(result);
812
+ }
813
+ if (process.env.NEWMARK_ISOLATED_RUNTIME === '1') {
814
+ const result = await (0, utilityHostToolBridge_1.requestUtilityHostTool)('screen_capture', args, {
815
+ conversationId: context.conversationId || 'default',
816
+ workspaceId: context.workspaceId || (0, terminalTakeover_1.terminalTakeoverWorkspaceId)(context.workspacePath || wsPath),
817
+ actorId: context.actorId || terminalTakeover_1.ROOT_TERMINAL_ACTOR_ID,
818
+ workspacePath: context.workspacePath || wsPath,
819
+ backend: 'utility',
820
+ mode: context.mode || 'build',
821
+ allowEphemeralVisionImage: context.allowEphemeralVisionImage === true,
822
+ }, 120_000, context.signal);
823
+ return typeof result === 'string' ? result : JSON.stringify(result);
824
+ }
825
+ return await (0, computerUse_1.runComputerUse)({
826
+ action: target === 'application' ? 'app_observe' : 'observe',
827
+ appTarget: g('app_target'),
828
+ windowHandle: target === 'application' ? g('app_target') : '',
829
+ maxChars: Number(args.max_chars || 30000),
830
+ workspacePath: wsPath,
831
+ allowEphemeralVisionImage: context.allowEphemeralVisionImage === true,
832
+ captureMaxWidth: Number(args.capture_max_width),
833
+ captureMaxHeight: Number(args.capture_max_height),
834
+ invocation: context.invocation,
835
+ ownerId: `screen-capture:${computerUseOwner(context, wsPath)}:${String(context.actorId || 'root')}`,
836
+ });
837
+ }
793
838
  case 'computer_use': {
794
839
  const action = normalizeComputerUseAction(g('action'));
795
840
  const owner = `${computerUseOwner(context, wsPath)}:${String(context.actorId || 'root')}`;
@@ -976,7 +1021,7 @@ class ToolExecutor {
976
1021
  hostSupportsTool(name) {
977
1022
  if (name.startsWith('browser_') && !this.hostProfile.electronBrowser)
978
1023
  return false;
979
- if (name === 'computer_use' && !this.hostProfile.windowsComputerUse)
1024
+ if ((name === 'computer_use' || name === 'screen_capture') && !this.hostProfile.windowsComputerUse)
980
1025
  return false;
981
1026
  return true;
982
1027
  }
@@ -26,6 +26,7 @@ exports.NATIVE_TOOL_CATALOG = [
26
26
  { name: 'browser_reload', label: 'Browser reload', description: 'Reload the built-in browser.', category: 'browser', defaultEnabled: true },
27
27
  { name: 'browser_cdp', label: 'Browser CDP', description: 'Run an advanced Chrome DevTools Protocol command.', category: 'browser', defaultEnabled: true },
28
28
  { name: 'browser_use', label: 'Browser Use', description: 'Observe and control the built-in browser through scoped opaque element refs and structured receipts.', category: 'browser', defaultEnabled: true },
29
+ { name: 'screen_capture', label: 'Active screenshot', description: 'Capture the Windows desktop or one visible application without starting the full Computer Use control surface.', category: 'desktop', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
29
30
  { name: 'computer_use', label: 'Computer Use', description: 'Observe and control Windows desktop UI with screenshots and semantic objects.', category: 'desktop', defaultEnabled: true },
30
31
  { name: 'image_inspect', label: 'Image inspect', description: 'Crop and magnify images submitted in the current conversation for closer visual inspection.', category: 'core', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
31
32
  { name: 'image_display', label: 'Image display', description: 'Present a workspace PNG/JPEG inside the current Build Block.', category: 'core', defaultEnabled: true, protected: true, availability: 'mode-scoped' },