@rtrvr-ai/rover 1.0.3 → 1.1.0

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/rover.js CHANGED
@@ -11465,9 +11465,9 @@ var Bridge = class {
11465
11465
  }
11466
11466
  async openUrlInNewTab(targetUrl, options) {
11467
11467
  const external = !isUrlAllowedByDomains(targetUrl, this.allowedDomains);
11468
- const popup = window.open(targetUrl, "_blank", "noopener,noreferrer");
11468
+ const popupAttempt = this.openVerifiedPopup(targetUrl);
11469
11469
  let logicalTabId;
11470
- if (this.registerOpenedTab) {
11470
+ if (popupAttempt.opened && this.registerOpenedTab) {
11471
11471
  try {
11472
11472
  const registered = await this.registerOpenedTab({
11473
11473
  url: targetUrl,
@@ -11479,8 +11479,8 @@ var Bridge = class {
11479
11479
  } catch {
11480
11480
  }
11481
11481
  }
11482
- const registrationFailed = !logicalTabId && !!this.registerOpenedTab;
11483
- if (!popup) {
11482
+ const registrationFailed = popupAttempt.opened && !logicalTabId && !!this.registerOpenedTab;
11483
+ if (!popupAttempt.opened) {
11484
11484
  const reason = "Browser popup settings blocked opening a new tab.";
11485
11485
  if (options?.policyBlocked) {
11486
11486
  this.emitNavigationGuardrail({
@@ -11516,6 +11516,13 @@ var Bridge = class {
11516
11516
  openedInNewTab: true
11517
11517
  });
11518
11518
  }
11519
+ const warningMessages = [];
11520
+ if (registrationFailed) {
11521
+ warningMessages.push("Tab opened but registration failed; tab may not be targetable.");
11522
+ }
11523
+ if (!popupAttempt.verified) {
11524
+ warningMessages.push("Tab open was triggered, but browser did not return a popup handle.");
11525
+ }
11519
11526
  return {
11520
11527
  success: true,
11521
11528
  output: {
@@ -11523,13 +11530,64 @@ var Bridge = class {
11523
11530
  external,
11524
11531
  logicalTabId,
11525
11532
  openedInNewTab: true,
11533
+ openVerification: popupAttempt.verified ? "verified" : "unverified",
11526
11534
  policyBlocked: !!options?.policyBlocked,
11527
11535
  message,
11528
- ...registrationFailed && { warning: "Tab opened but registration failed \u2014 tab may not be targetable" }
11536
+ ...warningMessages.length ? { warning: warningMessages.join(" ") } : {}
11529
11537
  },
11530
11538
  allowFallback: true
11531
11539
  };
11532
11540
  }
11541
+ openVerifiedPopup(targetUrl) {
11542
+ const popup = window.open("about:blank", "_blank");
11543
+ if (popup) {
11544
+ try {
11545
+ popup.opener = null;
11546
+ } catch {
11547
+ }
11548
+ try {
11549
+ popup.location.href = targetUrl;
11550
+ } catch {
11551
+ }
11552
+ return { opened: true, verified: true };
11553
+ }
11554
+ try {
11555
+ const directPopup = window.open(targetUrl, "_blank");
11556
+ if (directPopup) {
11557
+ try {
11558
+ directPopup.opener = null;
11559
+ } catch {
11560
+ }
11561
+ return { opened: true, verified: true };
11562
+ }
11563
+ } catch {
11564
+ }
11565
+ try {
11566
+ const noOpenerPopup = window.open(targetUrl, "_blank", "noopener,noreferrer");
11567
+ if (noOpenerPopup) {
11568
+ try {
11569
+ noOpenerPopup.opener = null;
11570
+ } catch {
11571
+ }
11572
+ return { opened: true, verified: true };
11573
+ }
11574
+ return { opened: true, verified: false };
11575
+ } catch {
11576
+ }
11577
+ try {
11578
+ const anchor = document.createElement("a");
11579
+ anchor.href = targetUrl;
11580
+ anchor.target = "_blank";
11581
+ anchor.rel = "noopener noreferrer";
11582
+ anchor.style.display = "none";
11583
+ document.body.appendChild(anchor);
11584
+ anchor.click();
11585
+ anchor.remove();
11586
+ return { opened: true, verified: false };
11587
+ } catch {
11588
+ return { opened: false, verified: false };
11589
+ }
11590
+ }
11533
11591
  async openElementInNewTab(args) {
11534
11592
  const elementId = args.element_id ?? args.source_element_id ?? args.target_element_id ?? args.center_element_id ?? null;
11535
11593
  if (!elementId) {
@@ -11950,6 +12008,7 @@ function bindRpc(port, handlers) {
11950
12008
  }
11951
12009
 
11952
12010
  // ../ui/dist/mount.js
12011
+ var DEFAULT_AGENT_NAME = "Rover";
11953
12012
  var DEFAULT_MASCOT_MP4 = "https://www.rtrvr.ai/rover/mascot.mp4";
11954
12013
  var DEFAULT_MASCOT_WEBM = "https://www.rtrvr.ai/rover/mascot.webm";
11955
12014
  var EXPAND_THRESHOLD_OUTPUT = 280;
@@ -11999,6 +12058,22 @@ function deriveTraceKey(event) {
11999
12058
  function sanitizeText(text) {
12000
12059
  return String(text || "").trim();
12001
12060
  }
12061
+ function resolveAgentName(input) {
12062
+ const normalized = String(input || "").trim();
12063
+ if (!normalized)
12064
+ return DEFAULT_AGENT_NAME;
12065
+ return normalized.slice(0, 64);
12066
+ }
12067
+ function deriveAgentInitial(name) {
12068
+ const normalized = String(name || "").trim();
12069
+ if (!normalized)
12070
+ return "R";
12071
+ return normalized[0].toUpperCase();
12072
+ }
12073
+ function deriveLauncherToken(name) {
12074
+ const compact = String(name || "").replace(/[^a-zA-Z0-9]/g, "").slice(0, 3).toUpperCase();
12075
+ return compact || "RVR";
12076
+ }
12002
12077
  function parseStageFromTitle(title) {
12003
12078
  const clean = sanitizeText(title);
12004
12079
  const match = /^(Analyze|Route|Execute|Verify|Complete):\s*(.*)$/i.exec(clean);
@@ -12174,6 +12249,9 @@ function createExpandableRichContent(text, threshold) {
12174
12249
  return wrapper;
12175
12250
  }
12176
12251
  function mountWidget(opts) {
12252
+ const agentName = resolveAgentName(opts.agent?.name);
12253
+ const agentInitial = deriveAgentInitial(agentName);
12254
+ const launcherToken = deriveLauncherToken(agentName);
12177
12255
  const host = document.createElement("div");
12178
12256
  host.id = "rover-widget-root";
12179
12257
  document.documentElement.appendChild(host);
@@ -13220,7 +13298,7 @@ function mountWidget(opts) {
13220
13298
  wrapper.dataset.mood = "idle";
13221
13299
  const launcher = document.createElement("button");
13222
13300
  launcher.className = "launcher";
13223
- launcher.setAttribute("aria-label", "Open Rover assistant");
13301
+ launcher.setAttribute("aria-label", `Open ${agentName} assistant`);
13224
13302
  const mascotDisabled = opts.mascot?.disabled === true;
13225
13303
  let launcherVideo = null;
13226
13304
  if (!mascotDisabled) {
@@ -13242,7 +13320,7 @@ function mountWidget(opts) {
13242
13320
  }
13243
13321
  const launcherFallback = document.createElement("span");
13244
13322
  launcherFallback.className = "launcherFallback";
13245
- launcherFallback.textContent = "RVR";
13323
+ launcherFallback.textContent = launcherToken;
13246
13324
  const launcherShine = document.createElement("div");
13247
13325
  launcherShine.className = "launcherShine";
13248
13326
  launcher.appendChild(launcherFallback);
@@ -13260,13 +13338,13 @@ function mountWidget(opts) {
13260
13338
  }
13261
13339
  const avatarFallback = document.createElement("span");
13262
13340
  avatarFallback.className = "avatarFallback";
13263
- avatarFallback.textContent = "R";
13341
+ avatarFallback.textContent = agentInitial;
13264
13342
  avatar.appendChild(avatarFallback);
13265
13343
  const meta = document.createElement("div");
13266
13344
  meta.className = "meta";
13267
13345
  const titleEl = document.createElement("div");
13268
13346
  titleEl.className = "title";
13269
- titleEl.textContent = "Rover";
13347
+ titleEl.textContent = agentName;
13270
13348
  const statusEl = document.createElement("div");
13271
13349
  statusEl.className = "status";
13272
13350
  const statusDot = document.createElement("span");
@@ -13399,7 +13477,7 @@ function mountWidget(opts) {
13399
13477
  composer.className = "composer";
13400
13478
  const composerTextarea = document.createElement("textarea");
13401
13479
  composerTextarea.rows = 1;
13402
- composerTextarea.placeholder = "Ask Rover to act on this website...";
13480
+ composerTextarea.placeholder = `Ask ${agentName} to act on this website...`;
13403
13481
  composer.appendChild(composerTextarea);
13404
13482
  const sendButton = document.createElement("button");
13405
13483
  sendButton.type = "submit";
@@ -13707,7 +13785,7 @@ function mountWidget(opts) {
13707
13785
  if (mode === "controller") {
13708
13786
  modeBadge.textContent = "active";
13709
13787
  menuTakeControl.style.display = "none";
13710
- inputEl.placeholder = "Ask Rover to act on this website...";
13788
+ inputEl.placeholder = `Ask ${agentName} to act on this website...`;
13711
13789
  } else {
13712
13790
  modeBadge.textContent = "observer";
13713
13791
  if (executionMeta?.canTakeControl !== false) {
@@ -13720,7 +13798,7 @@ function mountWidget(opts) {
13720
13798
  } else if (canComposeInObserver) {
13721
13799
  inputEl.placeholder = "Send to take control and run here.";
13722
13800
  } else if (executionMeta?.activeLogicalTabId && executionMeta?.localLogicalTabId && executionMeta.activeLogicalTabId !== executionMeta.localLogicalTabId) {
13723
- inputEl.placeholder = `Observing: Rover is acting in tab #${executionMeta.activeLogicalTabId}`;
13801
+ inputEl.placeholder = `Observing: ${agentName} is acting in tab #${executionMeta.activeLogicalTabId}`;
13724
13802
  } else {
13725
13803
  inputEl.placeholder = "Observer mode. Take control to run actions here.";
13726
13804
  }
@@ -13920,6 +13998,9 @@ function mountWidget(opts) {
13920
13998
  var SHARED_VERSION = 2;
13921
13999
  var SHARED_KEY_PREFIX = "rover:shared:";
13922
14000
  var SHARED_CHANNEL_PREFIX = "rover:channel:";
14001
+ var STALE_DETACHED_EXTERNAL_TAB_MS = 2 * 6e4;
14002
+ var STALE_DETACHED_TAB_MS = 10 * 6e4;
14003
+ var STALE_RUNTIME_TAB_MS = 45e3;
13923
14004
  function now() {
13924
14005
  return Date.now();
13925
14006
  }
@@ -14125,6 +14206,36 @@ var SessionCoordinator = class _SessionCoordinator {
14125
14206
  lastNotifiedRole;
14126
14207
  roleChangeTimer = null;
14127
14208
  static ROLE_CHANGE_DEBOUNCE_MS = 200;
14209
+ pruneDetachedTabs(draft, options) {
14210
+ const dropRuntimeDetached = !!options?.dropRuntimeDetached;
14211
+ const dropAllDetachedExternal = !!options?.dropAllDetachedExternal;
14212
+ const nowMs2 = now();
14213
+ const before = draft.tabs.length;
14214
+ draft.tabs = draft.tabs.filter((tab) => {
14215
+ if (tab.runtimeId) {
14216
+ if (tab.runtimeId === this.runtimeId)
14217
+ return true;
14218
+ return nowMs2 - Math.max(tab.updatedAt || 0, tab.openedAt || 0) <= STALE_RUNTIME_TAB_MS;
14219
+ }
14220
+ if (dropRuntimeDetached && tab.openerRuntimeId === this.runtimeId) {
14221
+ return false;
14222
+ }
14223
+ if (tab.external) {
14224
+ if (dropAllDetachedExternal)
14225
+ return false;
14226
+ return nowMs2 - Math.max(tab.updatedAt || 0, tab.openedAt || 0) <= STALE_DETACHED_EXTERNAL_TAB_MS;
14227
+ }
14228
+ return nowMs2 - Math.max(tab.updatedAt || 0, tab.openedAt || 0) <= STALE_DETACHED_TAB_MS;
14229
+ });
14230
+ if (before !== draft.tabs.length) {
14231
+ if (draft.activeLogicalTabId && !draft.tabs.some((tab) => tab.logicalTabId === draft.activeLogicalTabId)) {
14232
+ draft.activeLogicalTabId = this.localLogicalTabId || draft.tabs[0]?.logicalTabId;
14233
+ }
14234
+ if (draft.nextLogicalTabId <= (draft.tabs.at(-1)?.logicalTabId ?? 0)) {
14235
+ draft.nextLogicalTabId = draft.tabs.reduce((max, tab) => Math.max(max, tab.logicalTabId), 0) + 1;
14236
+ }
14237
+ }
14238
+ }
14128
14239
  constructor(options) {
14129
14240
  this.siteId = options.siteId;
14130
14241
  this.sessionId = options.sessionId;
@@ -14144,6 +14255,9 @@ var SessionCoordinator = class _SessionCoordinator {
14144
14255
  if (this.started)
14145
14256
  return;
14146
14257
  this.started = true;
14258
+ this.mutate("local", (draft) => {
14259
+ this.pruneDetachedTabs(draft, { dropRuntimeDetached: true, dropAllDetachedExternal: true });
14260
+ });
14147
14261
  this.registerCurrentTab(window.location.href, document.title || void 0);
14148
14262
  this.claimLease(false);
14149
14263
  if (!this.state.task) {
@@ -14278,10 +14392,27 @@ var SessionCoordinator = class _SessionCoordinator {
14278
14392
  }
14279
14393
  hydrateExternalState(raw) {
14280
14394
  const incoming = sanitizeSharedState(raw, this.siteId, this.sessionId);
14395
+ this.pruneDetachedTabs(incoming, { dropRuntimeDetached: true, dropAllDetachedExternal: true });
14281
14396
  const beforeSeq = this.state.seq;
14282
14397
  const beforeUpdatedAt = this.state.updatedAt;
14283
14398
  this.applyIncomingState(incoming);
14284
- const changed = this.state.seq !== beforeSeq || this.state.updatedAt !== beforeUpdatedAt;
14399
+ let changed = this.state.seq !== beforeSeq || this.state.updatedAt !== beforeUpdatedAt;
14400
+ if (!changed) {
14401
+ const hasDetachedCandidates = this.state.tabs.some((tab) => {
14402
+ if (tab.runtimeId)
14403
+ return false;
14404
+ if (tab.external)
14405
+ return true;
14406
+ return tab.openerRuntimeId === this.runtimeId;
14407
+ });
14408
+ if (hasDetachedCandidates) {
14409
+ const beforeTabCount = this.state.tabs.length;
14410
+ this.mutate("local", (draft) => {
14411
+ this.pruneDetachedTabs(draft, { dropRuntimeDetached: true, dropAllDetachedExternal: true });
14412
+ });
14413
+ changed = this.state.tabs.length !== beforeTabCount;
14414
+ }
14415
+ }
14285
14416
  if (!changed)
14286
14417
  return false;
14287
14418
  this.persistState();
@@ -14291,7 +14422,23 @@ var SessionCoordinator = class _SessionCoordinator {
14291
14422
  return true;
14292
14423
  }
14293
14424
  listTabs() {
14294
- return [...this.state.tabs];
14425
+ const nowMs2 = now();
14426
+ return this.state.tabs.filter((tab) => {
14427
+ if (tab.runtimeId) {
14428
+ if (tab.runtimeId === this.runtimeId)
14429
+ return true;
14430
+ return nowMs2 - Math.max(tab.updatedAt || 0, tab.openedAt || 0) <= STALE_RUNTIME_TAB_MS;
14431
+ }
14432
+ if (tab.external) {
14433
+ return nowMs2 - Math.max(tab.updatedAt || 0, tab.openedAt || 0) <= STALE_DETACHED_EXTERNAL_TAB_MS;
14434
+ }
14435
+ return nowMs2 - Math.max(tab.updatedAt || 0, tab.openedAt || 0) <= STALE_DETACHED_TAB_MS;
14436
+ });
14437
+ }
14438
+ pruneTabs(options) {
14439
+ this.mutate("local", (draft) => {
14440
+ this.pruneDetachedTabs(draft, options);
14441
+ });
14295
14442
  }
14296
14443
  startNewTask(task) {
14297
14444
  const startedAt = Number(task.startedAt) || now();
@@ -14312,6 +14459,7 @@ var SessionCoordinator = class _SessionCoordinator {
14312
14459
  draft.uiStatus = void 0;
14313
14460
  draft.activeRun = void 0;
14314
14461
  draft.workerContext = void 0;
14462
+ this.pruneDetachedTabs(draft, { dropAllDetachedExternal: true });
14315
14463
  });
14316
14464
  return nextTask;
14317
14465
  }
@@ -14426,9 +14574,6 @@ var SessionCoordinator = class _SessionCoordinator {
14426
14574
  this.mutate("local", (draft) => {
14427
14575
  if (!activeRun) {
14428
14576
  draft.activeRun = void 0;
14429
- if (draft.task && draft.task.status === "running") {
14430
- draft.task.status = "completed";
14431
- }
14432
14577
  return;
14433
14578
  }
14434
14579
  draft.activeRun = {
@@ -14781,6 +14926,7 @@ var SessionCoordinator = class _SessionCoordinator {
14781
14926
  if (draft.workflowLock?.runtimeId === this.runtimeId) {
14782
14927
  draft.workflowLock.expiresAt = now() + this.leaseMs * 5;
14783
14928
  }
14929
+ this.pruneDetachedTabs(draft);
14784
14930
  });
14785
14931
  this.notifyRoleChange();
14786
14932
  }
@@ -14872,6 +15018,7 @@ var SessionCoordinator = class _SessionCoordinator {
14872
15018
  return;
14873
15019
  if (incoming.seq === this.state.seq && incoming.updatedAt <= this.state.updatedAt)
14874
15020
  return;
15021
+ this.pruneDetachedTabs(incoming);
14875
15022
  this.state = incoming;
14876
15023
  this.syncLocalLogicalTabId();
14877
15024
  this.onStateChange?.(this.state, "remote");
@@ -15238,9 +15385,8 @@ var MAX_AUTO_RESUME_AGE_MS = 15 * 6e4;
15238
15385
  var MAX_AUTO_RESUME_ATTEMPTS = 12;
15239
15386
  var VISITOR_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
15240
15387
  var CHECKPOINT_PAYLOAD_VERSION = 1;
15241
- var DEFAULT_TASK_INACTIVITY_MS = 5 * 6e4;
15242
- var DEFAULT_SEMANTIC_SIMILARITY_THRESHOLD = 0.18;
15243
- var MAX_RECENT_USER_MESSAGES_FOR_TASK_HEURISTIC = 3;
15388
+ var ACTIVE_PENDING_RUN_GRACE_MS = 3e3;
15389
+ var STALE_PENDING_RUN_MS = 9e4;
15244
15390
  var instance = null;
15245
15391
  var bridge = null;
15246
15392
  var worker = null;
@@ -15482,6 +15628,72 @@ function safeSerialize(value) {
15482
15628
  }
15483
15629
  }
15484
15630
  }
15631
+ function resolveEmbeddedDomainLabel(allowedDomains) {
15632
+ const raw = String((allowedDomains || [])[0] || "").trim();
15633
+ if (!raw)
15634
+ return "the configured domain";
15635
+ return raw.replace(/^\*?\./, "").replace(/^=/, "");
15636
+ }
15637
+ function buildInaccessibleTabPageData(cfg, tab, reason = "tab_not_accessible") {
15638
+ const logicalTabId = Number(tab?.logicalTabId) || void 0;
15639
+ const url = tab?.url || "";
15640
+ const title = tab?.title || (tab?.external ? "External Tab (Inaccessible)" : "Inactive Tab");
15641
+ const embeddedDomain = resolveEmbeddedDomainLabel(cfg.allowedDomains);
15642
+ const normalizedReason = String(reason || "").trim();
15643
+ const reasonLine = normalizedReason ? ` Reason: ${normalizedReason}.` : "";
15644
+ const content = tab?.external ? `Rover is embedded inside ${embeddedDomain}. This tab is outside direct DOM access and only virtual tab context is available.${reasonLine}` : `This tab is currently not attached to an active Rover runtime. Switch to a live tab or reopen it.${reasonLine}`;
15645
+ return {
15646
+ url,
15647
+ title,
15648
+ contentType: "text/html",
15649
+ content,
15650
+ metadata: {
15651
+ inaccessible: true,
15652
+ external: !!tab?.external,
15653
+ accessMode: tab?.external ? "external_placeholder" : "inactive_tab",
15654
+ reason,
15655
+ logicalTabId
15656
+ }
15657
+ };
15658
+ }
15659
+ function buildTabAccessToolError(cfg, tab, reason = "tab_not_accessible") {
15660
+ const logicalTabId = Number(tab?.logicalTabId) || 0;
15661
+ const blockedUrl = tab?.url || "";
15662
+ const embeddedDomain = resolveEmbeddedDomainLabel(cfg.allowedDomains);
15663
+ const message = tab?.external ? `Tab ${logicalTabId} is outside Rover's embedded domain scope (${embeddedDomain}) and cannot be controlled directly.` : `Tab ${logicalTabId} is not attached to an active Rover runtime.`;
15664
+ const code = tab?.external ? "DOMAIN_SCOPE_BLOCKED" : "TAB_NOT_ACCESSIBLE";
15665
+ return {
15666
+ success: false,
15667
+ error: message,
15668
+ allowFallback: true,
15669
+ output: {
15670
+ success: false,
15671
+ error: {
15672
+ code,
15673
+ message,
15674
+ missing: [],
15675
+ next_action: tab?.external ? "Use open_new_tab for external context or continue on an in-scope tab." : "Switch to an active tab and retry.",
15676
+ retryable: false
15677
+ },
15678
+ blocked_url: blockedUrl || void 0,
15679
+ logical_tab_id: logicalTabId || void 0,
15680
+ external: !!tab?.external,
15681
+ policy_action: tab?.external ? cfg.externalNavigationPolicy || "open_new_tab_notice" : void 0,
15682
+ reason
15683
+ },
15684
+ errorDetails: {
15685
+ code,
15686
+ message,
15687
+ retryable: false,
15688
+ details: {
15689
+ logicalTabId,
15690
+ blockedUrl,
15691
+ external: !!tab?.external,
15692
+ reason
15693
+ }
15694
+ }
15695
+ };
15696
+ }
15485
15697
  function normalizeStatusStage(input) {
15486
15698
  if (input === "analyze" || input === "route" || input === "execute" || input === "verify" || input === "complete") {
15487
15699
  return input;
@@ -15557,14 +15769,26 @@ function shouldIgnoreRunScopedWorkerMessage(msg) {
15557
15769
  return false;
15558
15770
  }
15559
15771
  if (type === "run_completed") {
15560
- if (!pendingRunId)
15772
+ if (!pendingRunId) {
15773
+ const sharedRunId = sessionCoordinator?.getState()?.activeRun?.runId;
15774
+ if (sharedRunId && sharedRunId === messageRunId)
15775
+ return false;
15561
15776
  return true;
15777
+ }
15562
15778
  return pendingRunId !== messageRunId;
15563
15779
  }
15564
15780
  if (!pendingRunId)
15565
15781
  return true;
15566
15782
  return pendingRunId !== messageRunId;
15567
15783
  }
15784
+ function normalizeRunCompletionState(msg) {
15785
+ if (!msg || typeof msg !== "object") {
15786
+ return { taskComplete: false, needsUserInput: false };
15787
+ }
15788
+ const needsUserInput = msg.needsUserInput === true;
15789
+ const taskComplete = msg.taskComplete === true && !needsUserInput;
15790
+ return { taskComplete, needsUserInput };
15791
+ }
15568
15792
  function getLatestAssistantText(runId) {
15569
15793
  if (runId && latestAssistantByRunId.has(runId)) {
15570
15794
  return latestAssistantByRunId.get(runId);
@@ -15828,6 +16052,33 @@ function markTaskActivity(role, timestamp = Date.now()) {
15828
16052
  task.endedAt = void 0;
15829
16053
  }
15830
16054
  }
16055
+ function markTaskRunning(reason = "worker_task_active", timestamp = Date.now()) {
16056
+ if (!runtimeState)
16057
+ return;
16058
+ const task = ensureActiveTask(reason);
16059
+ if (!task)
16060
+ return;
16061
+ task.status = "running";
16062
+ task.endedAt = void 0;
16063
+ task.boundaryReason = reason;
16064
+ if (!task.lastUserAt && !task.lastAssistantAt) {
16065
+ task.lastAssistantAt = timestamp;
16066
+ }
16067
+ sessionCoordinator?.syncTask({ ...task }, runtimeState.taskEpoch);
16068
+ persistRuntimeState();
16069
+ }
16070
+ function markTaskCompleted(reason = "worker_task_complete", timestamp = Date.now()) {
16071
+ if (!runtimeState)
16072
+ return;
16073
+ const task = ensureActiveTask(reason);
16074
+ if (!task)
16075
+ return;
16076
+ task.status = "completed";
16077
+ task.endedAt = timestamp;
16078
+ task.boundaryReason = reason;
16079
+ sessionCoordinator?.syncTask({ ...task }, runtimeState.taskEpoch);
16080
+ persistRuntimeState();
16081
+ }
15831
16082
  function hideTaskSuggestion() {
15832
16083
  pendingTaskSuggestion = null;
15833
16084
  ui?.setTaskSuggestion({ visible: false });
@@ -15837,53 +16088,37 @@ function clearTaskUiState() {
15837
16088
  ui?.clearTimeline();
15838
16089
  hideTaskSuggestion();
15839
16090
  }
15840
- function collectRecentUserTextsForHeuristic() {
15841
- if (!runtimeState)
15842
- return [];
15843
- const userMessages = runtimeState.uiMessages.filter((message) => message.role === "user");
15844
- return userMessages.slice(-MAX_RECENT_USER_MESSAGES_FOR_TASK_HEURISTIC).map((message) => message.text.trim()).filter(Boolean);
15845
- }
15846
- function tokenizeForSimilarity(input) {
15847
- return new Set(String(input || "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 2));
15848
- }
15849
- function computeSemanticOverlap(a, b) {
15850
- const ta = tokenizeForSimilarity(a);
15851
- const tb = tokenizeForSimilarity(b);
15852
- if (!ta.size || !tb.size)
15853
- return 0;
15854
- let intersection = 0;
15855
- for (const token of ta) {
15856
- if (tb.has(token))
15857
- intersection += 1;
16091
+ function isPendingRunLikelyActive() {
16092
+ const pending = runtimeState?.pendingRun;
16093
+ if (!pending)
16094
+ return false;
16095
+ if (!sessionCoordinator)
16096
+ return true;
16097
+ const sharedActiveRun = sessionCoordinator?.getState()?.activeRun;
16098
+ if (sharedActiveRun?.runId && sharedActiveRun.runId === pending.id) {
16099
+ return true;
16100
+ }
16101
+ if (sharedActiveRun?.runtimeId && sharedActiveRun.runtimeId !== runtimeId) {
16102
+ return true;
15858
16103
  }
15859
- return intersection / Math.max(ta.size, tb.size);
16104
+ const ageMs = Date.now() - Number(pending.startedAt || 0);
16105
+ return ageMs >= 0 && ageMs <= ACTIVE_PENDING_RUN_GRACE_MS;
15860
16106
  }
15861
- function shouldSuggestResetForPrompt(text) {
15862
- if (!runtimeState || !currentConfig)
15863
- return { suggest: false };
15864
- const suggestEnabled = currentConfig.taskContext?.suggestReset !== false;
15865
- if (!suggestEnabled)
15866
- return { suggest: false };
15867
- if (runtimeState.pendingRun)
15868
- return { suggest: false };
15869
- const task = ensureActiveTask("implicit");
15870
- if (!task || task.status === "ended")
15871
- return { suggest: false };
15872
- const inactivityMs = Math.max(6e4, Number(currentConfig.taskContext?.inactivityMs) || DEFAULT_TASK_INACTIVITY_MS);
15873
- const now2 = Date.now();
15874
- const latestActivity = Math.max(task.lastUserAt || 0, task.lastAssistantAt || 0, task.startedAt || 0);
15875
- if (!latestActivity || now2 - latestActivity < inactivityMs)
15876
- return { suggest: false };
15877
- const recent = collectRecentUserTextsForHeuristic();
15878
- if (!recent.length)
15879
- return { suggest: true, reason: "inactivity_only" };
15880
- const baseline = recent.join(" ");
15881
- const overlap = computeSemanticOverlap(baseline, text);
15882
- const threshold = Math.max(0.05, Math.min(0.9, Number(currentConfig.taskContext?.semanticSimilarityThreshold) || DEFAULT_SEMANTIC_SIMILARITY_THRESHOLD));
15883
- if (overlap < threshold) {
15884
- return { suggest: true, reason: `low_similarity_${overlap.toFixed(2)}` };
15885
- }
15886
- return { suggest: false };
16107
+ function maybeClearStalePendingRun() {
16108
+ if (!runtimeState?.pendingRun)
16109
+ return;
16110
+ if (isPendingRunLikelyActive())
16111
+ return;
16112
+ if (!sessionCoordinator)
16113
+ return;
16114
+ const pending = runtimeState.pendingRun;
16115
+ const ageMs = Date.now() - Number(pending.startedAt || 0);
16116
+ if (!Number.isFinite(ageMs) || ageMs < STALE_PENDING_RUN_MS)
16117
+ return;
16118
+ setPendingRun(void 0);
16119
+ sessionCoordinator?.clearActiveRunRuntimeId(pending.id);
16120
+ sessionCoordinator?.releaseWorkflowLock(pending.id);
16121
+ sessionCoordinator?.setActiveRun(void 0);
15887
16122
  }
15888
16123
  function appendUiMessage(role, text, persist = true, options) {
15889
16124
  const clean = truncateText2(String(text || ""));
@@ -16068,28 +16303,16 @@ function dispatchUserPrompt(text, options) {
16068
16303
  const trimmed = String(text || "").trim();
16069
16304
  if (!trimmed)
16070
16305
  return;
16071
- if (options?.startNewTask) {
16072
- newTask({ reason: options.reason || "suggested_new_task", clearUi: true });
16073
- } else if (!options?.bypassSuggestion) {
16074
- const decision = shouldSuggestResetForPrompt(trimmed);
16075
- if (decision.suggest) {
16076
- pendingTaskSuggestion = {
16077
- text: trimmed,
16078
- reason: decision.reason || "heuristic",
16079
- createdAt: Date.now()
16080
- };
16081
- ui?.setTaskSuggestion({
16082
- visible: true,
16083
- text: "This looks like a different request. Start a new task or continue this one?",
16084
- primaryLabel: "Start new",
16085
- secondaryLabel: "Continue"
16086
- });
16087
- emit("task_suggested_reset", {
16088
- text: trimmed,
16089
- reason: pendingTaskSuggestion.reason
16090
- });
16091
- return;
16092
- }
16306
+ maybeClearStalePendingRun();
16307
+ const activeTaskStatus = runtimeState?.activeTask?.status;
16308
+ const shouldStartFreshTask = !!options?.startNewTask || activeTaskStatus === "completed" || activeTaskStatus === "ended";
16309
+ sessionCoordinator?.pruneTabs({
16310
+ dropRuntimeDetached: true,
16311
+ dropAllDetachedExternal: shouldStartFreshTask
16312
+ });
16313
+ if (shouldStartFreshTask) {
16314
+ const autoReason = options?.reason || (activeTaskStatus === "completed" ? "auto_after_task_complete" : "auto_after_task_end");
16315
+ newTask({ reason: autoReason, clearUi: true });
16093
16316
  }
16094
16317
  hideTaskSuggestion();
16095
16318
  postRun(trimmed, { appendUserMessage: true, resume: false, autoResume: true });
@@ -16727,6 +16950,7 @@ function handleWorkerMessage(msg) {
16727
16950
  }
16728
16951
  sessionCoordinator?.setActiveRun(void 0);
16729
16952
  if (!msg.ok && msg.error) {
16953
+ markTaskRunning("worker_run_failed");
16730
16954
  setUiStatus(`Task failed: ${String(msg.error)}`);
16731
16955
  latestAssistantByRunId.delete(String(msg.runId || ""));
16732
16956
  appendTimelineEvent({
@@ -16736,8 +16960,23 @@ function handleWorkerMessage(msg) {
16736
16960
  status: "error"
16737
16961
  });
16738
16962
  } else if (msg.ok) {
16739
- setUiStatus("Execution completed");
16740
- finalizeSuccessfulRunTimeline(typeof msg.runId === "string" ? msg.runId : void 0);
16963
+ const completionState = normalizeRunCompletionState(msg);
16964
+ const taskComplete = completionState.taskComplete;
16965
+ const needsUserInput = completionState.needsUserInput;
16966
+ if (taskComplete) {
16967
+ markTaskCompleted("worker_task_complete");
16968
+ setUiStatus("Task completed");
16969
+ finalizeSuccessfulRunTimeline(typeof msg.runId === "string" ? msg.runId : void 0);
16970
+ } else {
16971
+ markTaskRunning(needsUserInput ? "worker_waiting_for_input" : "worker_continuation");
16972
+ setUiStatus(needsUserInput ? "Need more input to continue" : "Execution finished. Continue when ready.");
16973
+ appendTimelineEvent({
16974
+ kind: "status",
16975
+ title: needsUserInput ? "Waiting for your input" : "Continuation available",
16976
+ detail: needsUserInput ? "Planner requested more information before marking the task complete." : "Task is still active and will continue with your next message.",
16977
+ status: "info"
16978
+ });
16979
+ }
16741
16980
  if (typeof msg.runId === "string" && msg.runId) {
16742
16981
  latestAssistantByRunId.delete(msg.runId);
16743
16982
  }
@@ -16815,26 +17054,34 @@ function createRuntime(cfg) {
16815
17054
  bindRpc(channel.port1, {
16816
17055
  getSnapshot: () => bridge.getSnapshot(),
16817
17056
  getPageData: async (params) => {
16818
- const tabId = params?.tabId;
17057
+ const runtimeCfg = currentConfig || cfg;
17058
+ const tabId = Number(params?.tabId);
16819
17059
  const localTabId = sessionCoordinator?.getLocalLogicalTabId();
16820
- if (!tabId || tabId === localTabId || !sessionCoordinator) {
17060
+ if (!Number.isFinite(tabId) || tabId <= 0 || tabId === localTabId || !sessionCoordinator) {
16821
17061
  return bridge.getPageData(params);
16822
17062
  }
16823
17063
  const tabs = sessionCoordinator.listTabs();
16824
17064
  const targetTab = tabs.find((t) => t.logicalTabId === tabId);
16825
- if (!targetTab || targetTab.runtimeId === runtimeId) {
17065
+ if (!targetTab) {
17066
+ return buildInaccessibleTabPageData(runtimeCfg, { logicalTabId: tabId, external: true }, "target_tab_missing");
17067
+ }
17068
+ if (targetTab.runtimeId === runtimeId) {
16826
17069
  return bridge.getPageData(params);
16827
17070
  }
17071
+ if (targetTab.external && runtimeCfg.externalNavigationPolicy !== "allow") {
17072
+ return buildInaccessibleTabPageData(runtimeCfg, targetTab, "external_domain_inaccessible");
17073
+ }
16828
17074
  if (!targetTab.runtimeId || !sessionCoordinator.isTabAlive(tabId)) {
16829
- return bridge.getPageData(params);
17075
+ return buildInaccessibleTabPageData(runtimeCfg, targetTab, "target_tab_inactive");
16830
17076
  }
16831
17077
  try {
16832
17078
  return await sessionCoordinator.sendCrossTabRpc(targetTab.runtimeId, "getPageData", params, 15e3);
16833
17079
  } catch {
16834
- return bridge.getPageData(params);
17080
+ return buildInaccessibleTabPageData(runtimeCfg, targetTab, "cross_tab_rpc_failed");
16835
17081
  }
16836
17082
  },
16837
17083
  executeTool: async (params) => {
17084
+ const runtimeCfg = currentConfig || cfg;
16838
17085
  const activeTabId = sessionCoordinator?.getActiveLogicalTabId();
16839
17086
  const localTabId = sessionCoordinator?.getLocalLogicalTabId();
16840
17087
  if (!activeTabId || activeTabId === localTabId || !sessionCoordinator) {
@@ -16842,16 +17089,22 @@ function createRuntime(cfg) {
16842
17089
  }
16843
17090
  const tabs = sessionCoordinator.listTabs();
16844
17091
  const targetTab = tabs.find((t) => t.logicalTabId === activeTabId);
16845
- if (!targetTab?.runtimeId || targetTab.runtimeId === runtimeId) {
16846
- return bridge.executeTool(params.call, params.payload);
17092
+ if (!targetTab) {
17093
+ return buildTabAccessToolError(runtimeCfg, { logicalTabId: activeTabId, external: true }, "target_tab_missing");
17094
+ }
17095
+ if (targetTab.external && runtimeCfg.externalNavigationPolicy !== "allow") {
17096
+ return buildTabAccessToolError(runtimeCfg, targetTab, "external_tab_action_blocked");
16847
17097
  }
16848
- if (!sessionCoordinator.isTabAlive(activeTabId)) {
17098
+ if (targetTab.runtimeId === runtimeId) {
16849
17099
  return bridge.executeTool(params.call, params.payload);
16850
17100
  }
17101
+ if (!targetTab.runtimeId || !sessionCoordinator.isTabAlive(activeTabId)) {
17102
+ return buildTabAccessToolError(runtimeCfg, targetTab, "target_tab_inactive");
17103
+ }
16851
17104
  try {
16852
17105
  return await sessionCoordinator.sendCrossTabRpc(targetTab.runtimeId, "executeTool", params, 2e4);
16853
17106
  } catch {
16854
- return bridge.executeTool(params.call, params.payload);
17107
+ return buildTabAccessToolError(runtimeCfg, targetTab, "cross_tab_execute_failed");
16855
17108
  }
16856
17109
  },
16857
17110
  executeClientTool: (params) => bridge.executeClientTool(params.name, params.args),
@@ -16938,6 +17191,9 @@ function createRuntime(cfg) {
16938
17191
  },
16939
17192
  showTaskControls: cfg.ui?.showTaskControls !== false,
16940
17193
  muted: cfg.ui?.muted,
17194
+ agent: {
17195
+ name: cfg.ui?.agent?.name
17196
+ },
16941
17197
  mascot: {
16942
17198
  disabled: cfg.ui?.mascot?.disabled,
16943
17199
  mp4Url: cfg.ui?.mascot?.mp4Url,
@@ -17052,9 +17308,7 @@ function boot(cfg) {
17052
17308
  plannerOnActError: cfg.taskRouting?.plannerOnActError
17053
17309
  },
17054
17310
  taskContext: {
17055
- inactivityMs: cfg.taskContext?.inactivityMs ?? DEFAULT_TASK_INACTIVITY_MS,
17056
- suggestReset: cfg.taskContext?.suggestReset ?? true,
17057
- semanticSimilarityThreshold: cfg.taskContext?.semanticSimilarityThreshold ?? DEFAULT_SEMANTIC_SIMILARITY_THRESHOLD
17311
+ ...cfg.taskContext
17058
17312
  }
17059
17313
  };
17060
17314
  runtimeState.sessionId = currentConfig.sessionId;
@@ -17115,17 +17369,29 @@ function update(cfg) {
17115
17369
  ...cfg.taskRouting
17116
17370
  },
17117
17371
  taskContext: {
17118
- inactivityMs: cfg.taskContext?.inactivityMs ?? currentConfig.taskContext?.inactivityMs ?? DEFAULT_TASK_INACTIVITY_MS,
17119
- suggestReset: cfg.taskContext?.suggestReset ?? currentConfig.taskContext?.suggestReset ?? true,
17120
- semanticSimilarityThreshold: cfg.taskContext?.semanticSimilarityThreshold ?? currentConfig.taskContext?.semanticSimilarityThreshold ?? DEFAULT_SEMANTIC_SIMILARITY_THRESHOLD
17372
+ ...currentConfig.taskContext,
17373
+ ...cfg.taskContext
17121
17374
  },
17122
17375
  ui: {
17123
17376
  ...currentConfig.ui,
17124
17377
  ...cfg.ui,
17378
+ agent: {
17379
+ ...currentConfig.ui?.agent,
17380
+ ...cfg.ui?.agent
17381
+ },
17125
17382
  panel: {
17126
17383
  ...currentConfig.ui?.panel,
17127
17384
  ...cfg.ui?.panel
17128
17385
  }
17386
+ },
17387
+ tools: {
17388
+ ...currentConfig.tools,
17389
+ ...cfg.tools,
17390
+ client: cfg.tools?.client ?? currentConfig.tools?.client,
17391
+ web: {
17392
+ ...currentConfig.tools?.web,
17393
+ ...cfg.tools?.web
17394
+ }
17129
17395
  }
17130
17396
  };
17131
17397
  resolvedVisitorId = resolveVisitorId(currentConfig);