newmark-agent 0.4.8 → 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,6 +33,10 @@ 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;
37
41
  exports.setHostedServerAutomation = setHostedServerAutomation;
38
42
  const http = __importStar(require("http"));
@@ -59,6 +63,14 @@ const mobileWorkEventSubscribers = new Set();
59
63
  const mobileScopedRuntimes = new Map();
60
64
  let hostedWorkEventSink = null;
61
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;
62
74
  let hostedConversationUiState;
63
75
  let hostedConversationUiAction;
64
76
  let hostedConversationPrompt;
@@ -1107,7 +1119,7 @@ async function handleApi(req, res, body) {
1107
1119
  const workspaceId = String(params.workspaceId || '');
1108
1120
  const conversationId = String(params.conversationId || '');
1109
1121
  const action = String(params.action || '');
1110
- 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_toggle_pause', 'queue_guide']);
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']);
1111
1123
  if (!workspaceId || !conversationId || !allowed.has(action)) {
1112
1124
  mobileJson(res, { error: 'workspaceId, conversationId, and a valid action are required' }, 400);
1113
1125
  return;
@@ -1448,7 +1460,8 @@ function startServer(root, options = {}) {
1448
1460
  agent.setConversationFromStorage(previousConversation);
1449
1461
  }
1450
1462
  });
1451
- agent.subscribeWorkEvents(publishServerWorkEvent);
1463
+ unsubscribeServerAgentWorkEvents?.();
1464
+ unsubscribeServerAgentWorkEvents = agent.subscribeWorkEvents(publishServerWorkEvent);
1452
1465
  if (automation) {
1453
1466
  agent.setAutomationManager(automation);
1454
1467
  if (!options.automation)
@@ -1492,6 +1505,14 @@ function startServer(root, options = {}) {
1492
1505
  const lan = (0, mobilePairing_1.lanIpv4)();
1493
1506
  const accessHost = tailscale || lan || '<lan-or-tailscale-ip>';
1494
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
+ });
1495
1516
  server.listen(PORT, bindHost, () => {
1496
1517
  console.log(`\n Newmark Agent v1.0 - Server Mode`);
1497
1518
  console.log(` Bind: ${bindHost}:${PORT}`);
@@ -1503,14 +1524,137 @@ function startServer(root, options = {}) {
1503
1524
  console.log(` Mobile events (SSE): http://${accessHost}:${PORT}/api/mobile/events?token=<token>`);
1504
1525
  console.log(` Press Ctrl+C to stop\n`);
1505
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);
1506
1648
  }
1507
- let hostedServerStarted = false;
1508
1649
  /** 托管启动 server(GUI/TUI 内嵌调用;幂等防重入,进程常驻即服务常驻) */
1509
1650
  function runServer(root, options = {}) {
1510
- if (hostedServerStarted)
1651
+ if (!hostedServerRoot || Object.keys(options).length > 0)
1652
+ configureHostedServer(root, options);
1653
+ else
1654
+ hostedServerRoot = root;
1655
+ if (hostedServer?.listening)
1511
1656
  return;
1512
- hostedServerStarted = true;
1513
- startServer(root, options);
1657
+ startServer(hostedServerRoot, hostedServerOptions);
1514
1658
  }
1515
1659
  /** Attach the GUI-owned automation manager after deferred startup. */
1516
1660
  function setHostedServerAutomation(manager) {
@@ -4221,6 +4221,44 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
4221
4221
  .setting-control { flex: 1; }
4222
4222
  .settings-load-error { color: var(--notice-error-text); }
4223
4223
 
4224
+ .remote-touch-copy {
4225
+ flex: 1;
4226
+ min-width: 0;
4227
+ display: flex;
4228
+ flex-direction: column;
4229
+ gap: 2px;
4230
+ }
4231
+ .remote-touch-copy-title {
4232
+ color: var(--text);
4233
+ font-size: 11px;
4234
+ font-weight: 700;
4235
+ line-height: 1.25;
4236
+ }
4237
+ .remote-touch-copy-desc {
4238
+ color: var(--text-dim);
4239
+ font-size: 10px;
4240
+ font-weight: 400;
4241
+ line-height: 1.35;
4242
+ }
4243
+ .remote-touch-action {
4244
+ flex: 0 0 120px;
4245
+ width: 120px;
4246
+ min-width: 120px;
4247
+ min-height: 28px;
4248
+ box-sizing: border-box;
4249
+ }
4250
+ .remote-touch-status {
4251
+ background: transparent !important;
4252
+ border-color: currentColor;
4253
+ box-shadow: 0 0 0 1px currentColor, 0 0 10px color-mix(in srgb, currentColor 42%, transparent);
4254
+ }
4255
+ .remote-touch-status:hover { background: transparent !important; }
4256
+ .remote-touch-status.listening { color: var(--accent2); }
4257
+ .remote-touch-status.error,
4258
+ .remote-touch-status.checking { color: #ffb454; }
4259
+ .remote-touch-status.off { color: var(--notice-error-text); }
4260
+ .remote-touch-status[aria-busy="true"] { cursor: wait; }
4261
+
4224
4262
  .segmented-control {
4225
4263
  display: grid;
4226
4264
  grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -6302,6 +6340,9 @@ var state = {
6302
6340
  configuredAgentBackend: 'windows',
6303
6341
  agentBackendRestartRequired: false,
6304
6342
  remoteTouchEnabled: true,
6343
+ remoteTouchServer: { enabled: true, listening: false, reachable: false, state: 'checking', host: '', port: 47890, error: '', checkedAt: '', startedAt: 0 },
6344
+ remoteTouchStatusBusy: false,
6345
+ _remoteTouchStatusTimer: null,
6305
6346
  wslAvailable: false,
6306
6347
  wslDistros: [],
6307
6348
  defaultTerminalShell: 'powershell',
@@ -6484,6 +6525,10 @@ var NEWMARK_I18N = {
6484
6525
  'settings.runInWslDesc': 'Keeps the Windows UI and native desktop tools local while running the persistent Agent kernel, model calls, shell, and file tools inside WSL.',
6485
6526
  'settings.remoteTouch': 'Mobile remote-touch',
6486
6527
  'settings.remoteTouchDesc': 'Allow mobile access on the same LAN / Tailscale network',
6528
+ 'settings.remoteTouchListening': 'Listening',
6529
+ 'settings.remoteTouchError': 'Unreachable',
6530
+ 'settings.remoteTouchOff': 'Off',
6531
+ 'settings.remoteTouchChecking': 'Checking',
6487
6532
  'settings.remoteBehavior': 'Remote behavior',
6488
6533
  'settings.remoteConnect': 'Start connection',
6489
6534
  'settings.remoteConnectDesc': 'Show pairing QR',
@@ -7211,6 +7256,10 @@ var NEWMARK_I18N = {
7211
7256
  'settings.runInWslDesc': 'Windows UI 与桌面原生工具保留在本机,持续 Agent 内核、模型请求、Shell 和文件工具运行在 WSL。',
7212
7257
  'settings.remoteTouch': '移动端远程触及',
7213
7258
  'settings.remoteTouchDesc': '允许移动端通过同一内网 / Tailscale 连接',
7259
+ 'settings.remoteTouchListening': '监听中',
7260
+ 'settings.remoteTouchError': '不可触及',
7261
+ 'settings.remoteTouchOff': '已关闭',
7262
+ 'settings.remoteTouchChecking': '检测中',
7214
7263
  'settings.remoteBehavior': '远程行为',
7215
7264
  'settings.remoteConnect': '发起连接',
7216
7265
  'settings.remoteConnectDesc': '显示配对二维码',
@@ -7389,7 +7438,7 @@ var NEWMARK_I18N = {
7389
7438
  'model.unavailable': '不可用',
7390
7439
  'model.no': '否',
7391
7440
  'model.yes': '是',
7392
- 'model.notChecked': '未校验',
7441
+ 'model.notChecked': '未检测',
7393
7442
  'archive.current': '归档当前对话',
7394
7443
  'archive.archiving': '归档中...',
7395
7444
  'archive.empty': '暂无归档。',
@@ -15464,7 +15513,7 @@ function formatModelStatus(status) {
15464
15513
  if (raw === 'available' || raw === 'verified') return t('model.available');
15465
15514
  if (raw === 'degraded') return t('model.degraded');
15466
15515
  if (raw === 'unavailable' || raw === 'failed' || raw === 'error') return t('model.unavailable');
15467
- if (raw === 'not checked' || raw === 'not_checked' || raw === 'pending') return t('model.notChecked');
15516
+ if (raw === 'not checked' || raw === 'not_checked' || raw === 'unvalidated' || raw === 'pending') return t('model.notChecked');
15468
15517
  return status || t('model.notChecked');
15469
15518
  }
15470
15519
 
@@ -15476,6 +15525,7 @@ function effectiveUiModelStatus(modelEntry) {
15476
15525
  var textEvidence = capabilities.text === true || capabilities.text_input === true || capabilities.text_output === true
15477
15526
  || !!(modelEntry.evaluation && (modelEntry.evaluation.text_input || modelEntry.evaluation.text_output));
15478
15527
  if (rawValidation === 'auth_error' || rawValidation === 'invalid_config') return rawValidation;
15528
+ if (String((modelEntry.validation && modelEntry.validation.level) || '').toLowerCase() === 'discovered') return 'unvalidated';
15479
15529
  if (textEvidence && rawValidation === 'unavailable') return 'degraded';
15480
15530
  return rawValidation || rawEvaluation;
15481
15531
  }
@@ -15539,15 +15589,15 @@ function renderGeneralSettings() {
15539
15589
  '<div class="setting-desc" id="agent-wsl-status">' + esc(state.agentBackend && state.agentBackend.connected ? t('settings.wslConnected') : t('settings.wslDisconnected')) + '</div></div></div>' : '';
15540
15590
  return wslHtml + '<div class="setting-subsection">' +
15541
15591
  '<div class="setting-subsection-title">' + esc(t('settings.remoteBehavior')) + '</div>' +
15542
- '<div class="setting-row">' +
15543
- '<label class="setting-label" for="remote-touch-enabled" style="display:inline-flex;align-items:center;gap:6px;min-width:120px;">' +
15544
- '<input type="checkbox" id="remote-touch-enabled"' + (state.remoteTouchEnabled ? ' checked' : '') + ' onchange="window.setRemoteTouchEnabled(this.checked)">' +
15545
- '<span>' + esc(t('settings.remoteTouch')) + '</span></label>' +
15546
- '<span class="setting-desc" style="flex:1;min-width:140px;">' + esc(t('settings.remoteTouchDesc')) + '</span>' +
15592
+ '<div class="setting-row remote-touch-row">' +
15593
+ '<button type="button" id="remote-touch-status-button" class="sec-btn remote-touch-action remote-touch-status ' + esc(remoteTouchStatusClass()) + '" aria-pressed="' + (state.remoteTouchEnabled ? 'true' : 'false') + '" aria-busy="' + (state.remoteTouchStatusBusy ? 'true' : 'false') + '" onclick="window.setRemoteTouchEnabled(!state.remoteTouchEnabled)" title="' + esc(remoteTouchStatusTitle()) + '"' + (state.remoteTouchStatusBusy ? ' disabled' : '') + '>' + esc(remoteTouchStatusText()) + '</button>' +
15594
+ '<div class="remote-touch-copy"><div class="remote-touch-copy-title">' + esc(t('settings.remoteTouch')) + '</div>' +
15595
+ '<div class="remote-touch-copy-desc">' + esc(t('settings.remoteTouchDesc')) + '</div></div>' +
15547
15596
  '</div>' +
15548
- '<div class="setting-row" style="border-bottom:none;">' +
15549
- '<button type="button" class="sec-btn primary" onclick="window.showMobilePairing()">' + esc(t('settings.remoteConnect')) + '</button>' +
15550
- '<span class="setting-desc" style="flex:1;">' + esc(t('settings.remoteConnectDesc')) + '</span>' +
15597
+ '<div class="setting-row remote-touch-row" style="border-bottom:none;">' +
15598
+ '<button type="button" id="remote-touch-connect-button" class="sec-btn primary remote-touch-action" onclick="window.showMobilePairing()">' + esc(t('settings.remoteConnect')) + '</button>' +
15599
+ '<div class="remote-touch-copy"><div class="remote-touch-copy-title">' + esc(t('mobile.pair')) + '</div>' +
15600
+ '<div class="remote-touch-copy-desc">' + esc(t('settings.remoteConnectDesc')) + '</div></div>' +
15551
15601
  '</div>' +
15552
15602
  '</div>' +
15553
15603
  '<div class="setting-row">' +
@@ -15656,7 +15706,7 @@ function renderModelSettings() {
15656
15706
  var normalizedStatus = String(statusRaw || '').toLowerCase();
15657
15707
  var statusClass = normalizedStatus === 'available' || normalizedStatus === 'verified' || normalizedStatus === 'degraded'
15658
15708
  ? 'model-eval-ok'
15659
- : (normalizedStatus === 'not checked' ? 'model-eval-pending' : 'model-eval-bad');
15709
+ : (normalizedStatus === 'not checked' || normalizedStatus === 'unvalidated' ? 'model-eval-pending' : 'model-eval-bad');
15660
15710
  var details = '';
15661
15711
  if (typeof modelEntry !== 'string') {
15662
15712
  var tierMapCount = modelEntry.thinking_tier_map && typeof modelEntry.thinking_tier_map === 'object' ? Object.keys(modelEntry.thinking_tier_map).length : 0;
@@ -16225,13 +16275,96 @@ window.setAgentBackendMode = async function(mode) {
16225
16275
  return true;
16226
16276
  };
16227
16277
 
16278
+ function remoteTouchStatusClass() {
16279
+ if (state.remoteTouchStatusBusy) return 'checking';
16280
+ var status = state.remoteTouchServer || {};
16281
+ if (!state.remoteTouchEnabled || status.state === 'off') return 'off';
16282
+ return status.state === 'listening' && status.listening && status.reachable ? 'listening' : 'error';
16283
+ }
16284
+
16285
+ function remoteTouchStatusText() {
16286
+ var statusClass = remoteTouchStatusClass();
16287
+ if (statusClass === 'checking') return t('settings.remoteTouchChecking');
16288
+ if (statusClass === 'listening') return t('settings.remoteTouchListening');
16289
+ if (statusClass === 'error') return t('settings.remoteTouchError');
16290
+ return t('settings.remoteTouchOff');
16291
+ }
16292
+
16293
+ function remoteTouchStatusTitle() {
16294
+ var status = state.remoteTouchServer || {};
16295
+ var endpoint = status.host ? status.host + ':' + (status.port || 47890) : '';
16296
+ return [remoteTouchStatusText(), endpoint, status.error || ''].filter(Boolean).join(' · ');
16297
+ }
16298
+
16299
+ window.updateRemoteTouchStatusButton = function() {
16300
+ var button = document.getElementById('remote-touch-status-button');
16301
+ if (!button) return;
16302
+ button.className = 'sec-btn remote-touch-action remote-touch-status ' + remoteTouchStatusClass();
16303
+ button.textContent = remoteTouchStatusText();
16304
+ button.setAttribute('aria-pressed', state.remoteTouchEnabled ? 'true' : 'false');
16305
+ button.setAttribute('aria-busy', state.remoteTouchStatusBusy ? 'true' : 'false');
16306
+ button.title = remoteTouchStatusTitle();
16307
+ button.disabled = !!state.remoteTouchStatusBusy;
16308
+ };
16309
+
16310
+ window.refreshRemoteTouchServerStatus = async function() {
16311
+ if (!api.mobileServerStatus) return state.remoteTouchServer;
16312
+ try {
16313
+ var result = await api.mobileServerStatus();
16314
+ if (result && result.state) {
16315
+ state.remoteTouchServer = result;
16316
+ state.remoteTouchEnabled = !!result.enabled;
16317
+ }
16318
+ } catch (error) {
16319
+ state.remoteTouchServer = {
16320
+ enabled: state.remoteTouchEnabled,
16321
+ listening: false,
16322
+ reachable: false,
16323
+ state: state.remoteTouchEnabled ? 'error' : 'off',
16324
+ host: '',
16325
+ port: 47890,
16326
+ error: error && error.message ? error.message : String(error || ''),
16327
+ checkedAt: new Date().toISOString(),
16328
+ startedAt: 0
16329
+ };
16330
+ }
16331
+ window.updateRemoteTouchStatusButton();
16332
+ return state.remoteTouchServer;
16333
+ };
16334
+
16335
+ window.startRemoteTouchStatusPolling = function() {
16336
+ if (state._remoteTouchStatusTimer) clearInterval(state._remoteTouchStatusTimer);
16337
+ window.refreshRemoteTouchServerStatus().catch(function(){});
16338
+ state._remoteTouchStatusTimer = setInterval(function() {
16339
+ window.refreshRemoteTouchServerStatus().catch(function(){});
16340
+ }, 5000);
16341
+ };
16342
+
16228
16343
  window.setRemoteTouchEnabled = async function(value) {
16229
16344
  var next = !!value;
16230
16345
  state.remoteTouchEnabled = next;
16231
- if (api.saveSetting) {
16232
- try { await api.saveSetting('remote', 'touch_enabled', next); } catch (e) {}
16346
+ state.remoteTouchStatusBusy = true;
16347
+ window.updateRemoteTouchStatusButton();
16348
+ try {
16349
+ var result = api.setRemoteTouchEnabled
16350
+ ? await api.setRemoteTouchEnabled(next)
16351
+ : (api.saveSetting ? await api.saveSetting('remote', 'touch_enabled', next) : null);
16352
+ if (result && result.state) state.remoteTouchServer = result;
16353
+ } catch (error) {
16354
+ state.remoteTouchServer = {
16355
+ enabled: next,
16356
+ listening: false,
16357
+ reachable: false,
16358
+ state: next ? 'error' : 'off',
16359
+ host: '', port: 47890,
16360
+ error: error && error.message ? error.message : String(error || ''),
16361
+ checkedAt: new Date().toISOString(), startedAt: 0
16362
+ };
16363
+ } finally {
16364
+ state.remoteTouchStatusBusy = false;
16233
16365
  }
16234
- window.settingsTab('general');
16366
+ await window.refreshRemoteTouchServerStatus();
16367
+ window.updateRemoteTouchStatusButton();
16235
16368
  };
16236
16369
 
16237
16370
  window.setAgentWslDistro = async function(value) {
@@ -16477,6 +16610,7 @@ window.saveModelEdit = function(oldProvIdx, modelIdx) {
16477
16610
  if (typeof previous === 'string') previous = { name: previous, display: previous };
16478
16611
  var tierMap = window.parseThinkingTierMap(tierMapEl ? tierMapEl.value : '');
16479
16612
  var updated = Object.assign({}, previous, {
16613
+ _previous_name: previous.name || name,
16480
16614
  name: name,
16481
16615
  display: name,
16482
16616
  description: descEl ? descEl.value : '',
@@ -23088,6 +23222,7 @@ function schedulePostStartupUiRendering() {
23088
23222
  }
23089
23223
 
23090
23224
  updateMarqueeFromConfig();
23225
+ window.startRemoteTouchStatusPolling();
23091
23226
 
23092
23227
  // Populate selects
23093
23228
  if (els['mode-select']) els['mode-select'].value = state.mode;
@@ -23351,6 +23486,7 @@ function schedulePostStartupUiRendering() {
23351
23486
 
23352
23487
  window.addEventListener('beforeunload', function() {
23353
23488
  if (state._postStartupUiRendering && state._postStartupUiRendering.cancel) state._postStartupUiRendering.cancel();
23489
+ if (state._remoteTouchStatusTimer) clearInterval(state._remoteTouchStatusTimer);
23354
23490
  cancelBrowserGuestIdleDestroy();
23355
23491
  flushPendingConversationDraftPersists();
23356
23492
  }, { once: true });