agl 23.0.0 → 24.0.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.
Files changed (35) hide show
  1. package/changelog.md +11 -0
  2. package/dist_serve/bundle.js +811 -808
  3. package/dist_serve/bundle.js.map +1 -0
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/classes.authstore.js +2 -2
  6. package/dist_ts/classes.browserresourcehost.d.ts +1 -0
  7. package/dist_ts/classes.browserresourcehost.js +7 -1
  8. package/dist_ts/classes.controller.d.ts +35 -5
  9. package/dist_ts/classes.controller.js +994 -229
  10. package/dist_ts/classes.validation.d.ts +9 -1
  11. package/dist_ts/classes.validation.js +62 -7
  12. package/dist_ts_interfaces/browsertransport.d.ts +72 -0
  13. package/dist_ts_interfaces/browsertransport.js +47 -0
  14. package/dist_ts_interfaces/index.d.ts +1 -0
  15. package/dist_ts_interfaces/index.js +2 -1
  16. package/dist_ts_interfaces/interfaces.d.ts +10 -6
  17. package/dist_ts_interfaces/interfaces.js +2 -3
  18. package/dist_ts_interfaces/requests.d.ts +20 -21
  19. package/package.json +5 -5
  20. package/readme.md +7 -3
  21. package/ts/00_commitinfo_data.ts +1 -1
  22. package/ts/classes.authstore.ts +1 -1
  23. package/ts/classes.browserresourcehost.ts +7 -0
  24. package/ts/classes.controller.ts +1367 -279
  25. package/ts/classes.validation.ts +77 -6
  26. package/ts_interfaces/browsertransport.ts +126 -0
  27. package/ts_interfaces/index.ts +1 -0
  28. package/ts_interfaces/interfaces.ts +11 -11
  29. package/ts_interfaces/requests.ts +23 -30
  30. package/ts_web/00_commitinfo_data.ts +1 -1
  31. package/ts_web/classes.browserviewtransport.ts +532 -0
  32. package/ts_web/classes.controllersocketclient.ts +6 -17
  33. package/ts_web/elements.harnesscontrollerapp.ts +450 -115
  34. package/ts_web/index.ts +1 -0
  35. package/ts_web/plugins.ts +2 -0
@@ -1,10 +1,8 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import * as interfaces from '../ts_interfaces/index.js';
3
3
  import { commitinfo } from './00_commitinfo_data.js';
4
- import {
5
- ControllerSocketClient,
6
- type TControllerBrowserViewEventMessage,
7
- } from './classes.controllersocketclient.js';
4
+ import { ControllerSocketClient } from './classes.controllersocketclient.js';
5
+ import { ControllerBrowserViewTransportClient } from './classes.browserviewtransport.js';
8
6
  import {
9
7
  SessionDraftSync,
10
8
  type ISessionDraftSyncState,
@@ -459,6 +457,7 @@ const layoutRequestTimeoutMs = 10_000;
459
457
  const providerLoginPollTimeoutMs = 15 * 60 * 1000;
460
458
  const providerRefreshPollTimeoutMs = 5 * 60 * 1000;
461
459
  const providerOpenCodeSwitchTimeoutMs = 6 * 60 * 1000;
460
+ const browserRendererLifecycleTimeoutMs = 1_000;
462
461
  const maximumSlashSuggestions = 12;
463
462
  const maximumWorkspaceNoticeCharacters = 500;
464
463
 
@@ -621,6 +620,25 @@ const errorMessage = (errorArg: unknown): string => {
621
620
  return String(errorArg || 'The operation failed.');
622
621
  };
623
622
 
623
+ const waitForBrowserRendererLifecycle = async (
624
+ operationArg: Promise<void>,
625
+ actionArg: string,
626
+ ): Promise<void> => {
627
+ let timeout: ReturnType<typeof setTimeout> | undefined;
628
+ try {
629
+ await Promise.race([
630
+ operationArg,
631
+ new Promise<never>((_resolve, reject) => {
632
+ timeout = setTimeout(() => {
633
+ reject(new Error(`Browser renderer ${actionArg} timed out.`));
634
+ }, browserRendererLifecycleTimeoutMs);
635
+ }),
636
+ ]);
637
+ } finally {
638
+ if (timeout) clearTimeout(timeout);
639
+ }
640
+ };
641
+
624
642
  const formatPercentage = (valueArg: number | null | undefined): string => (
625
643
  typeof valueArg === 'number' && Number.isFinite(valueArg)
626
644
  ? `${Math.round(valueArg)}%`
@@ -716,14 +734,27 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
716
734
  private browserNavigationRequestId = 0;
717
735
  private browserViewOpenAbortController: AbortController | undefined;
718
736
  private browserNavigationAbortController: AbortController | undefined;
737
+ private browserViewRecoveryAbortController: AbortController | undefined;
719
738
  private browserViewId = '';
720
739
  private browserViewConnectionGeneration = -1;
721
- private browserViewPendingClose: { viewId: string; connectionGeneration: number } | undefined;
740
+ private browserViewStreamGeneration = -1;
741
+ private browserViewActivated = false;
742
+ private browserViewPendingClose: {
743
+ viewId: string;
744
+ streamGeneration: number;
745
+ connectionGeneration: number;
746
+ attempts: number;
747
+ } | undefined;
748
+ private browserViewCloseTask: Promise<void> | undefined;
749
+ private browserViewCloseRetryTimer: ReturnType<typeof setTimeout> | undefined;
722
750
  private browserViewResourceId = '';
723
751
  private browserViewAttachmentRevision = -1;
724
- private browserViewState: interfaces.IControllerBrowserViewState | undefined;
725
752
  private readonly browserViewListeners = new Set<plugins.TLiveBrowserEventListener>();
753
+ private browserViewTransport?: ControllerBrowserViewTransportClient;
754
+ private browserViewTransportRecoveryTask?: Promise<void>;
726
755
  private browserRenderer?: plugins.LiveBrowserCanvasRenderer;
756
+ private browserAddressDraft = '';
757
+ private browserAddressEditing = false;
727
758
  private suggestRequestId = 0;
728
759
  private suggestDebounce: ReturnType<typeof setTimeout> | undefined;
729
760
  private controllerStatus: interfaces.IControllerStatus | undefined;
@@ -937,8 +968,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
937
968
  @plugins.deesElement.state()
938
969
  accessor staleBundleVersion = '';
939
970
 
940
- @plugins.deesElement.state()
941
- accessor systemMetrics: interfaces.IControllerSystemMetrics | undefined;
971
+ public systemMetrics: interfaces.IControllerSystemMetrics | undefined;
942
972
 
943
973
  @plugins.deesElement.state()
944
974
  accessor highCpuWarning = false;
@@ -1015,6 +1045,9 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
1015
1045
  @plugins.deesElement.state()
1016
1046
  accessor browserViewLoading = false;
1017
1047
 
1048
+ @plugins.deesElement.state()
1049
+ accessor browserViewState: interfaces.IControllerBrowserViewState | undefined;
1050
+
1018
1051
  @plugins.deesElement.state()
1019
1052
  accessor selectedTerminalId: TBrowserTerminalId | undefined;
1020
1053
 
@@ -1036,7 +1069,6 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
1036
1069
  onStatus: (statusArg) => this.handleConnectionStatus(statusArg),
1037
1070
  onControllerEvent: (eventArg) => this.handleControllerEvent(eventArg),
1038
1071
  onTerminalOutput: (outputArg) => this.handleTerminalOutput(outputArg),
1039
- onBrowserViewEvent: (eventArg) => this.handleBrowserViewEvent(eventArg),
1040
1072
  onTransportClose: (closeCodeArg, reasonArg) => {
1041
1073
  this.recordTransportClose(closeCodeArg, reasonArg);
1042
1074
  console.error(`Controller transport closed (code ${closeCodeArg}${reasonArg ? `: ${reasonArg}` : ''}).`);
@@ -1045,6 +1077,13 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
1045
1077
  ? 'The connection was closed because a response exceeded the transport frame limit. Reconnecting…'
1046
1078
  : `The connection was closed by the transport (code ${closeCodeArg}${reasonArg ? `: ${reasonArg}` : ''}). Reconnecting…`;
1047
1079
  },
1080
+ onReconnectExhausted: (attemptsArg, endpointArg) => {
1081
+ if (!this.componentConnected) return;
1082
+ this.connectionError = `Automatic reconnection stopped after ${attemptsArg} attempts to ${endpointArg}.`;
1083
+ void this.socketClient.stop().finally(() => {
1084
+ if (this.componentConnected) this.connectionStatus = 'disconnected';
1085
+ });
1086
+ },
1048
1087
  });
1049
1088
  this.sessionDraftSync = new SessionDraftSync({
1050
1089
  transport: {
@@ -2074,6 +2113,8 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2074
2113
 
2075
2114
  this.connectionStatus = statusArg;
2076
2115
  this.connectionGeneration += 1;
2116
+ if (this.browserViewCloseRetryTimer) clearTimeout(this.browserViewCloseRetryTimer);
2117
+ this.browserViewCloseRetryTimer = undefined;
2077
2118
  if (this.browserViewPendingClose?.connectionGeneration !== this.connectionGeneration) {
2078
2119
  this.browserViewPendingClose = undefined;
2079
2120
  }
@@ -2092,6 +2133,8 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2092
2133
 
2093
2134
  private invalidateConnectionState(): void {
2094
2135
  this.connectionGeneration += 1;
2136
+ if (this.browserViewCloseRetryTimer) clearTimeout(this.browserViewCloseRetryTimer);
2137
+ this.browserViewCloseRetryTimer = undefined;
2095
2138
  if (this.browserViewPendingClose?.connectionGeneration !== this.connectionGeneration) {
2096
2139
  this.browserViewPendingClose = undefined;
2097
2140
  }
@@ -2609,12 +2652,54 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2609
2652
 
2610
2653
  this.lastCpuSampleAt = sampledAt;
2611
2654
  this.systemMetrics = metricsArg;
2655
+ this.updateSystemMetricsDom();
2612
2656
  }
2613
2657
 
2614
2658
  private resetHighCpuWarning(): void {
2615
2659
  this.highCpuStartedAt = undefined;
2616
2660
  this.lastCpuSampleAt = undefined;
2617
2661
  this.highCpuWarning = false;
2662
+ this.updateSystemMetricsDom();
2663
+ }
2664
+
2665
+ private updateSystemMetricsDom(): void {
2666
+ const metrics = this.systemMetrics;
2667
+ const memoryPercent = metrics?.memoryUsedBytes !== null
2668
+ && metrics?.memoryUsedBytes !== undefined
2669
+ && metrics.memoryTotalBytes !== null
2670
+ && metrics.memoryTotalBytes !== undefined
2671
+ && metrics.memoryTotalBytes > 0
2672
+ ? (metrics.memoryUsedBytes / metrics.memoryTotalBytes) * 100
2673
+ : null;
2674
+ const cpu = this.shadowRoot?.querySelector<HTMLElement>('.cpuMetric');
2675
+ const memory = this.shadowRoot?.querySelector<HTMLElement>('.memoryMetric');
2676
+ const network = this.shadowRoot?.querySelector<HTMLElement>('.networkMetric');
2677
+ const cpuValue = cpu?.querySelector<HTMLElement>('.metricValue');
2678
+ const memoryValue = memory?.querySelector<HTMLElement>('.metricValue');
2679
+ const networkReceive = network?.querySelector<HTMLElement>('.networkReceive');
2680
+ const networkTransmit = network?.querySelector<HTMLElement>('.networkTransmit');
2681
+ if (cpu) {
2682
+ cpu.classList.toggle('highCpuWarning', this.highCpuWarning);
2683
+ cpu.title = this.highCpuWarning
2684
+ ? 'Whole-system CPU usage has remained above 50% for at least 10 seconds'
2685
+ : 'Whole-system CPU usage';
2686
+ }
2687
+ if (cpuValue) cpuValue.textContent = formatPercentage(metrics?.cpuUsagePercent);
2688
+ if (memory) {
2689
+ memory.title = metrics?.memoryUsedBytes !== null
2690
+ && metrics?.memoryUsedBytes !== undefined
2691
+ && metrics.memoryTotalBytes !== null
2692
+ && metrics.memoryTotalBytes !== undefined
2693
+ ? `${formatBytes(metrics.memoryUsedBytes)} used of ${formatBytes(metrics.memoryTotalBytes)}`
2694
+ : 'Memory usage unavailable';
2695
+ }
2696
+ if (memoryValue) memoryValue.textContent = formatPercentage(memoryPercent);
2697
+ if (networkReceive) {
2698
+ networkReceive.textContent = `in ${formatByteRate(metrics?.networkReceiveBytesPerSecond)}`;
2699
+ }
2700
+ if (networkTransmit) {
2701
+ networkTransmit.textContent = `out ${formatByteRate(metrics?.networkTransmitBytesPerSecond)}`;
2702
+ }
2618
2703
  }
2619
2704
 
2620
2705
  private readonly showLegalDisclosure = (): void => {
@@ -5400,6 +5485,18 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
5400
5485
  const resourceId = eventArg.detail?.resource?.id;
5401
5486
  const resource = this.resources.find((candidate) => candidate.id === resourceId);
5402
5487
  if (!resource || resource.lifecycle !== 'active') return;
5488
+ if (
5489
+ resource.kind === 'browser'
5490
+ && this.selectedResourceId === resource.id
5491
+ && (
5492
+ this.browserViewLoading
5493
+ || (
5494
+ this.browserViewResourceId === resource.id
5495
+ && this.browserViewAttachmentRevision === resource.attachment.revision
5496
+ && this.browserViewId !== ''
5497
+ )
5498
+ )
5499
+ ) return;
5403
5500
  this.invalidateSlashCatalog();
5404
5501
  this.selectedResourceId = resource.id;
5405
5502
  if (resource.kind === 'browser') {
@@ -5435,6 +5532,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
5435
5532
  this.browserViewLoading = true;
5436
5533
  this.workspaceError = '';
5437
5534
  let openedViewId = '';
5535
+ let openedStreamGeneration = -1;
5438
5536
  try {
5439
5537
  const response = await this.socketClient.fire<interfaces.IReq_ControllerBrowserViewOpen>(
5440
5538
  'controller.browser.view.open',
@@ -5446,6 +5544,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
5446
5544
  { maxRetries: 0, abortSignal: openAbortController.signal },
5447
5545
  );
5448
5546
  openedViewId = response.viewId;
5547
+ openedStreamGeneration = response.streamGeneration;
5449
5548
  if (
5450
5549
  generation !== this.browserViewGeneration
5451
5550
  || connectionGeneration !== this.connectionGeneration
@@ -5453,7 +5552,11 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
5453
5552
  || response.resourceId !== resourceArg.id
5454
5553
  || response.attachmentRevision !== resourceArg.attachment.revision
5455
5554
  ) {
5456
- void this.requestBrowserViewClose(response.viewId, connectionGeneration);
5555
+ void this.requestBrowserViewClose(
5556
+ response.viewId,
5557
+ response.streamGeneration,
5558
+ connectionGeneration,
5559
+ );
5457
5560
  return;
5458
5561
  }
5459
5562
  if (
@@ -5462,29 +5565,49 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
5462
5565
  ) this.browserViewPendingClose = undefined;
5463
5566
  this.browserViewId = response.viewId;
5464
5567
  this.browserViewConnectionGeneration = connectionGeneration;
5568
+ this.browserViewStreamGeneration = response.streamGeneration;
5569
+ this.browserViewActivated = false;
5465
5570
  this.browserViewResourceId = response.resourceId;
5466
5571
  this.browserViewAttachmentRevision = response.attachmentRevision;
5467
- this.browserViewState = response.state;
5572
+ this.applyBrowserViewState(response.state);
5573
+ const transport = this.createBrowserViewTransportClient(
5574
+ response.streamGeneration,
5575
+ response.events,
5576
+ response.operations,
5577
+ generation,
5578
+ );
5579
+ this.browserViewTransport = transport;
5580
+ const transportStart = transport.start();
5468
5581
  this.browserViewLoading = false;
5469
5582
  this.requestUpdate();
5470
- await this.updateComplete;
5583
+ await Promise.all([this.updateComplete, transportStart]);
5471
5584
  const rendererReady = await this.mountBrowserRenderer(generation);
5472
5585
  if (
5473
5586
  !rendererReady
5474
5587
  || generation !== this.browserViewGeneration
5475
5588
  || connectionGeneration !== this.connectionGeneration
5476
5589
  || response.viewId !== this.browserViewId
5590
+ || response.streamGeneration !== this.browserViewStreamGeneration
5477
5591
  ) return;
5478
5592
  await this.socketClient.fire<interfaces.IReq_ControllerBrowserViewActivate>(
5479
5593
  'controller.browser.view.activate',
5480
- { viewId: response.viewId },
5594
+ { viewId: response.viewId, streamGeneration: response.streamGeneration },
5481
5595
  { maxRetries: 0, abortSignal: openAbortController.signal },
5482
5596
  );
5597
+ if (
5598
+ generation === this.browserViewGeneration
5599
+ && response.viewId === this.browserViewId
5600
+ && response.streamGeneration === this.browserViewStreamGeneration
5601
+ ) this.browserViewActivated = true;
5483
5602
  } catch (errorArg) {
5484
5603
  if (openedViewId && openedViewId === this.browserViewId) {
5485
5604
  await this.closeBrowserView(false);
5486
5605
  } else if (openedViewId && this.socketClient.isConnected) {
5487
- await this.requestBrowserViewClose(openedViewId, connectionGeneration);
5606
+ await this.requestBrowserViewClose(
5607
+ openedViewId,
5608
+ openedStreamGeneration,
5609
+ connectionGeneration,
5610
+ );
5488
5611
  }
5489
5612
  if (
5490
5613
  generation === this.browserViewGeneration
@@ -5507,39 +5630,80 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
5507
5630
  this.browserViewOpenAbortController = undefined;
5508
5631
  this.browserNavigationAbortController?.abort(new Error('Browser view changed.'));
5509
5632
  this.browserNavigationAbortController = undefined;
5633
+ this.browserViewRecoveryAbortController?.abort(new Error('Browser view changed.'));
5634
+ this.browserViewRecoveryAbortController = undefined;
5510
5635
  const renderer = this.browserRenderer;
5511
- if (renderer) {
5512
- await renderer.stop().then(
5513
- () => {
5514
- if (this.browserRenderer === renderer) this.browserRenderer = undefined;
5515
- },
5516
- (errorArg) => {
5517
- this.workspaceError = `Browser renderer cleanup remains pending: ${errorMessage(errorArg)}`;
5518
- },
5519
- );
5520
- }
5636
+ const transport = this.browserViewTransport;
5521
5637
  const viewId = this.browserViewId;
5638
+ const viewStreamGeneration = this.browserViewStreamGeneration;
5522
5639
  const viewConnectionGeneration = this.browserViewConnectionGeneration;
5523
5640
  this.browserViewId = '';
5524
5641
  this.browserViewConnectionGeneration = -1;
5642
+ this.browserViewStreamGeneration = -1;
5643
+ this.browserViewActivated = false;
5525
5644
  this.browserViewResourceId = '';
5526
5645
  this.browserViewAttachmentRevision = -1;
5527
5646
  this.browserViewState = undefined;
5647
+ this.browserAddressDraft = '';
5648
+ this.browserAddressEditing = false;
5528
5649
  this.browserViewLoading = false;
5529
5650
  this.browserViewListeners.clear();
5530
- if (viewId && viewConnectionGeneration === this.connectionGeneration) {
5531
- await this.requestBrowserViewClose(viewId, viewConnectionGeneration);
5532
- } else {
5533
- await this.flushPendingBrowserViewClose();
5651
+ const remoteCloseTask = (
5652
+ viewId
5653
+ && viewStreamGeneration > 0
5654
+ && viewConnectionGeneration === this.connectionGeneration
5655
+ )
5656
+ ? this.requestBrowserViewClose(
5657
+ viewId,
5658
+ viewStreamGeneration,
5659
+ viewConnectionGeneration,
5660
+ )
5661
+ : this.flushPendingBrowserViewClose();
5662
+ const rendererStopTask = renderer
5663
+ ? this.stopBrowserRenderer(renderer)
5664
+ : Promise.resolve();
5665
+ const transportStopTask = transport
5666
+ ? transport.stop(new Error('Browser view changed.')).then(() => {
5667
+ if (this.browserViewTransport === transport) this.browserViewTransport = undefined;
5668
+ })
5669
+ : Promise.resolve();
5670
+ await Promise.all([remoteCloseTask, rendererStopTask, transportStopTask]);
5671
+ }
5672
+
5673
+ private async stopBrowserRenderer(rendererArg: plugins.LiveBrowserCanvasRenderer): Promise<void> {
5674
+ let stopTask: Promise<void>;
5675
+ try {
5676
+ stopTask = rendererArg.stop();
5677
+ } catch (errorArg) {
5678
+ if (this.browserRenderer === rendererArg) {
5679
+ this.workspaceError = `Browser renderer cleanup remains pending: ${errorMessage(errorArg)}`;
5680
+ }
5681
+ return;
5682
+ }
5683
+ void stopTask.then(() => {
5684
+ if (this.browserRenderer === rendererArg) this.browserRenderer = undefined;
5685
+ }, () => undefined);
5686
+ try {
5687
+ await waitForBrowserRendererLifecycle(stopTask, 'stop');
5688
+ } catch (errorArg) {
5689
+ if (this.browserRenderer === rendererArg) {
5690
+ this.workspaceError = `Browser renderer cleanup remains pending: ${errorMessage(errorArg)}`;
5691
+ }
5534
5692
  }
5535
5693
  }
5536
5694
 
5537
5695
  private async requestBrowserViewClose(
5538
5696
  viewIdArg: string,
5697
+ streamGenerationArg: number,
5539
5698
  connectionGenerationArg: number,
5540
5699
  ): Promise<void> {
5541
5700
  if (connectionGenerationArg !== this.connectionGeneration) return;
5542
- const pendingClose = { viewId: viewIdArg, connectionGeneration: connectionGenerationArg };
5701
+ const pendingClose = {
5702
+ viewId: viewIdArg,
5703
+ streamGeneration: streamGenerationArg,
5704
+ connectionGeneration: connectionGenerationArg,
5705
+ attempts: 0,
5706
+ };
5543
5707
  this.browserViewPendingClose = pendingClose;
5544
5708
  await this.flushPendingBrowserViewClose();
5545
5709
  }
@@ -5551,70 +5715,190 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
5551
5715
  || pendingClose.connectionGeneration !== this.connectionGeneration
5552
5716
  || !this.socketClient.isConnected
5553
5717
  ) return;
5554
- try {
5555
- await this.socketClient.fire<interfaces.IReq_ControllerBrowserViewClose>(
5556
- 'controller.browser.view.close',
5557
- { viewId: pendingClose.viewId },
5558
- { maxRetries: 0 },
5559
- );
5560
- } catch {
5718
+ if (this.browserViewCloseTask) return this.browserViewCloseTask;
5719
+ pendingClose.attempts += 1;
5720
+ const closeTask = this.socketClient.fire<interfaces.IReq_ControllerBrowserViewClose>(
5721
+ 'controller.browser.view.close',
5722
+ {
5723
+ viewId: pendingClose.viewId,
5724
+ expectedStreamGeneration: pendingClose.streamGeneration,
5725
+ },
5726
+ { maxRetries: 0 },
5727
+ ).then(() => {
5728
+ if (this.browserViewPendingClose === pendingClose) {
5729
+ this.browserViewPendingClose = undefined;
5730
+ }
5731
+ }).catch(() => {
5732
+ if (
5733
+ this.browserViewPendingClose === pendingClose
5734
+ && pendingClose.attempts >= 3
5735
+ ) {
5736
+ this.browserViewPendingClose = undefined;
5737
+ this.workspaceError = 'The browser view close request failed after three attempts.';
5738
+ return;
5739
+ }
5740
+ if (
5741
+ this.browserViewPendingClose !== pendingClose
5742
+ || pendingClose.connectionGeneration !== this.connectionGeneration
5743
+ ) return;
5744
+ if (this.browserViewCloseRetryTimer) clearTimeout(this.browserViewCloseRetryTimer);
5745
+ this.browserViewCloseRetryTimer = setTimeout(() => {
5746
+ this.browserViewCloseRetryTimer = undefined;
5747
+ void this.flushPendingBrowserViewClose();
5748
+ }, 250 * pendingClose.attempts);
5749
+ }).finally(() => {
5750
+ if (this.browserViewCloseTask === closeTask) this.browserViewCloseTask = undefined;
5751
+ if (
5752
+ this.browserViewPendingClose
5753
+ && this.browserViewPendingClose !== pendingClose
5754
+ ) void this.flushPendingBrowserViewClose();
5755
+ });
5756
+ this.browserViewCloseTask = closeTask;
5757
+ return closeTask;
5758
+ }
5759
+
5760
+ private createBrowserViewTransportClient(
5761
+ streamGenerationArg: number,
5762
+ eventsArg: plugins.typedrequestInterfaces.TVirtualStream<'receive'>,
5763
+ operationsArg: plugins.typedrequestInterfaces.TVirtualStream<'send'>,
5764
+ viewGenerationArg: number,
5765
+ ): ControllerBrowserViewTransportClient {
5766
+ return new ControllerBrowserViewTransportClient({
5767
+ streamGeneration: streamGenerationArg,
5768
+ events: eventsArg,
5769
+ operations: operationsArg,
5770
+ onEvent: (eventArg) => {
5771
+ if (
5772
+ viewGenerationArg !== this.browserViewGeneration
5773
+ || streamGenerationArg !== this.browserViewStreamGeneration
5774
+ ) return;
5775
+ this.handleBrowserViewEvent(eventArg);
5776
+ },
5777
+ onFailure: (errorArg, recoverableArg) => {
5778
+ this.handleBrowserTransportFailure(
5779
+ viewGenerationArg,
5780
+ streamGenerationArg,
5781
+ errorArg,
5782
+ recoverableArg,
5783
+ );
5784
+ },
5785
+ });
5786
+ }
5787
+
5788
+ private handleBrowserViewEvent(eventArg: interfaces.TControllerBrowserViewEvent): void {
5789
+ if (eventArg.type === 'state') this.applyBrowserViewState(eventArg.state);
5790
+ if (eventArg.type === 'error' && eventArg.error.fatal) this.workspaceError = eventArg.error.message;
5791
+ for (const listener of this.browserViewListeners) listener(eventArg);
5792
+ }
5793
+
5794
+ private applyBrowserViewState(stateArg: interfaces.IControllerBrowserViewState): boolean {
5795
+ if (this.browserViewState && stateArg.revision <= this.browserViewState.revision) return false;
5796
+ this.browserViewState = stateArg;
5797
+ if (!this.browserAddressEditing) {
5798
+ this.browserAddressDraft = stateArg.tabs.find((tabArg) => tabArg.active)?.url ?? '';
5799
+ }
5800
+ return true;
5801
+ }
5802
+
5803
+ private handleBrowserTransportFailure(
5804
+ viewGenerationArg: number,
5805
+ streamGenerationArg: number,
5806
+ errorArg: unknown,
5807
+ recoverableArg: boolean,
5808
+ ): void {
5809
+ if (
5810
+ viewGenerationArg !== this.browserViewGeneration
5811
+ || streamGenerationArg !== this.browserViewStreamGeneration
5812
+ || !this.browserViewId
5813
+ ) return;
5814
+ if (!recoverableArg || !this.browserViewActivated || !this.browserRenderer) {
5815
+ this.workspaceError = `Browser transport failed: ${errorMessage(errorArg)}`;
5816
+ void this.closeBrowserView();
5561
5817
  return;
5562
5818
  }
5563
- if (this.browserViewPendingClose === pendingClose) this.browserViewPendingClose = undefined;
5819
+ if (this.browserViewTransportRecoveryTask) return;
5820
+ const recovery = this.recoverBrowserViewTransport(
5821
+ viewGenerationArg,
5822
+ streamGenerationArg,
5823
+ errorArg,
5824
+ ).finally(() => {
5825
+ if (this.browserViewTransportRecoveryTask === recovery) {
5826
+ this.browserViewTransportRecoveryTask = undefined;
5827
+ }
5828
+ });
5829
+ this.browserViewTransportRecoveryTask = recovery;
5830
+ void recovery.catch(() => undefined);
5564
5831
  }
5565
5832
 
5566
- private async handleBrowserViewEvent(
5567
- messageArg: TControllerBrowserViewEventMessage,
5833
+ private async recoverBrowserViewTransport(
5834
+ viewGenerationArg: number,
5835
+ streamGenerationArg: number,
5836
+ reasonArg: unknown,
5568
5837
  ): Promise<void> {
5569
- const isCurrent = (): boolean => (
5570
- messageArg.viewId !== this.browserViewId
5571
- ? false
5572
- : messageArg.resourceId === this.browserViewResourceId
5573
- && messageArg.attachmentRevision === this.browserViewAttachmentRevision
5574
- );
5575
- if (messageArg.event.type === 'frame') {
5576
- const { dataStream, ...frame } = messageArg.event.frame;
5577
- const reject = async (reasonArg: unknown): Promise<void> => {
5578
- await dataStream.reject(reasonArg).catch(async () => {
5579
- await dataStream.abort(reasonArg).catch(() => undefined);
5580
- });
5581
- };
5582
- if (!isCurrent()) {
5583
- await reject(new Error('The browser frame belongs to a stale view.'));
5584
- return;
5838
+ const viewId = this.browserViewId;
5839
+ const connectionGeneration = this.connectionGeneration;
5840
+ const oldTransport = this.browserViewTransport;
5841
+ const renderer = this.browserRenderer;
5842
+ if (!viewId || !oldTransport || !renderer) return;
5843
+ const recoveryAbortController = new AbortController();
5844
+ this.browserViewRecoveryAbortController = recoveryAbortController;
5845
+ this.browserViewActivated = false;
5846
+ try {
5847
+ await waitForBrowserRendererLifecycle(renderer.suspend(), 'suspension');
5848
+ await oldTransport.stop(reasonArg);
5849
+ const response = await this.socketClient.fire<interfaces.IReq_ControllerBrowserViewRecover>(
5850
+ 'controller.browser.view.recover',
5851
+ { viewId, expectedStreamGeneration: streamGenerationArg },
5852
+ { maxRetries: 0, abortSignal: recoveryAbortController.signal },
5853
+ );
5854
+ if (
5855
+ viewGenerationArg !== this.browserViewGeneration
5856
+ || connectionGeneration !== this.connectionGeneration
5857
+ || viewId !== this.browserViewId
5858
+ || streamGenerationArg !== this.browserViewStreamGeneration
5859
+ || response.viewId !== viewId
5860
+ || response.resourceId !== this.browserViewResourceId
5861
+ || response.attachmentRevision !== this.browserViewAttachmentRevision
5862
+ || response.streamGeneration !== streamGenerationArg + 1
5863
+ ) throw new Error('The recovered browser stream generation is stale.');
5864
+ const transport = this.createBrowserViewTransportClient(
5865
+ response.streamGeneration,
5866
+ response.events,
5867
+ response.operations,
5868
+ viewGenerationArg,
5869
+ );
5870
+ this.browserViewTransport = transport;
5871
+ this.browserViewStreamGeneration = response.streamGeneration;
5872
+ this.applyBrowserViewState(response.state);
5873
+ await transport.start();
5874
+ await waitForBrowserRendererLifecycle(renderer.resume(), 'resume');
5875
+ await this.socketClient.fire<interfaces.IReq_ControllerBrowserViewActivate>(
5876
+ 'controller.browser.view.activate',
5877
+ { viewId, streamGeneration: response.streamGeneration },
5878
+ { maxRetries: 0, abortSignal: recoveryAbortController.signal },
5879
+ );
5880
+ if (
5881
+ viewGenerationArg === this.browserViewGeneration
5882
+ && viewId === this.browserViewId
5883
+ && response.streamGeneration === this.browserViewStreamGeneration
5884
+ ) {
5885
+ this.browserViewActivated = true;
5886
+ this.workspaceError = '';
5585
5887
  }
5586
- let data: Uint8Array;
5587
- try {
5588
- const firstChunk = await dataStream.receive();
5589
- if (firstChunk === undefined) throw new Error('The browser frame stream was empty.');
5590
- if (await dataStream.receive() !== undefined) {
5591
- throw new Error('The browser frame stream contained multiple chunks.');
5592
- }
5593
- if (
5594
- dataStream.contentType !== frame.mimeType
5595
- || dataStream.integrity?.algorithm !== 'sha256'
5596
- || dataStream.integrity.byteLength !== firstChunk.byteLength
5597
- ) throw new Error('The browser frame stream metadata is invalid.');
5598
- if (!isCurrent()) {
5599
- await reject(new Error('The browser frame became stale during transfer.'));
5600
- return;
5601
- }
5602
- data = firstChunk;
5603
- await dataStream.accept();
5604
- } catch (errorArg) {
5605
- await reject(errorArg);
5606
- throw errorArg;
5888
+ } catch (errorArg) {
5889
+ if (
5890
+ viewGenerationArg === this.browserViewGeneration
5891
+ && connectionGeneration === this.connectionGeneration
5892
+ && viewId === this.browserViewId
5893
+ ) {
5894
+ this.workspaceError = `Browser transport recovery failed: ${errorMessage(errorArg)}`;
5895
+ await this.closeBrowserView();
5896
+ }
5897
+ } finally {
5898
+ if (this.browserViewRecoveryAbortController === recoveryAbortController) {
5899
+ this.browserViewRecoveryAbortController = undefined;
5607
5900
  }
5608
- if (!isCurrent()) return;
5609
- const event = { type: 'frame' as const, frame: { ...frame, data } };
5610
- for (const listener of this.browserViewListeners) listener(event);
5611
- return;
5612
5901
  }
5613
- if (!isCurrent()) return;
5614
- const event = messageArg.event;
5615
- if (event.type === 'state') this.browserViewState = event.state;
5616
- if (event.type === 'error' && event.error.fatal) this.workspaceError = event.error.message;
5617
- for (const listener of this.browserViewListeners) listener(event);
5618
5902
  }
5619
5903
 
5620
5904
  private async browserViewOperation(
@@ -5623,14 +5907,19 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
5623
5907
  abortSignalArg?: AbortSignal,
5624
5908
  ): Promise<interfaces.IControllerBrowserViewState> {
5625
5909
  const viewId = this.browserViewId;
5626
- if (!viewId) throw new Error('The browser view is unavailable.');
5627
- const response = await this.socketClient.fire<interfaces.IReq_ControllerBrowserViewOperate>(
5628
- 'controller.browser.view.operate',
5629
- { viewId, operation: operationArg },
5630
- { maxRetries: 0, abortSignal: abortSignalArg },
5631
- );
5632
- if (publishStateArg && viewId === this.browserViewId) this.browserViewState = response.state;
5633
- return response.state;
5910
+ const transport = this.browserViewTransport;
5911
+ if (!viewId || !transport) throw new Error('The browser view is unavailable.');
5912
+ const result = await transport.operate(operationArg, abortSignalArg);
5913
+ if (
5914
+ result.status === 'succeeded'
5915
+ && result.state
5916
+ && viewId === this.browserViewId
5917
+ ) {
5918
+ if (!publishStateArg) return result.state;
5919
+ this.applyBrowserViewState(result.state);
5920
+ }
5921
+ if (!this.browserViewState) throw new Error('The browser view state is unavailable.');
5922
+ return this.browserViewState;
5634
5923
  }
5635
5924
 
5636
5925
  private async navigateBrowserAddress(formArg: HTMLFormElement): Promise<void> {
@@ -5643,6 +5932,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
5643
5932
  const input = formArg.querySelector<HTMLInputElement>('input');
5644
5933
  try {
5645
5934
  const url = controllerNormalizeBrowserAddress(input?.value ?? '');
5935
+ this.browserAddressDraft = input?.value ?? '';
5646
5936
  this.workspaceError = '';
5647
5937
  const state = await this.browserViewOperation(
5648
5938
  { type: 'navigate', url },
@@ -5653,7 +5943,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
5653
5943
  requestId === this.browserNavigationRequestId
5654
5944
  && viewGeneration === this.browserViewGeneration
5655
5945
  && viewId === this.browserViewId
5656
- ) this.browserViewState = state;
5946
+ ) this.applyBrowserViewState(state);
5657
5947
  } catch (errorArg) {
5658
5948
  if (
5659
5949
  requestId === this.browserNavigationRequestId
@@ -5678,6 +5968,23 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
5678
5968
  if (form) void this.navigateBrowserAddress(form);
5679
5969
  };
5680
5970
 
5971
+ private readonly handleBrowserAddressFocus = (eventArg: Event): void => {
5972
+ const input = eventArg.currentTarget;
5973
+ if (!(input instanceof HTMLInputElement)) return;
5974
+ this.browserAddressEditing = true;
5975
+ this.browserAddressDraft = input.value;
5976
+ };
5977
+
5978
+ private readonly handleBrowserAddressInput = (eventArg: Event): void => {
5979
+ const input = eventArg.currentTarget;
5980
+ if (input instanceof HTMLInputElement) this.browserAddressDraft = input.value;
5981
+ };
5982
+
5983
+ private readonly handleBrowserAddressBlur = (): void => {
5984
+ this.browserAddressEditing = false;
5985
+ this.requestUpdate();
5986
+ };
5987
+
5681
5988
  private browserCanvasClient(): plugins.ILiveBrowserCanvasClient {
5682
5989
  return {
5683
5990
  getState: () => {
@@ -5689,12 +5996,13 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
5689
5996
  return () => this.browserViewListeners.delete(listenerArg);
5690
5997
  },
5691
5998
  acknowledgeFrame: async (acknowledgement, options) => {
5692
- const response = await this.socketClient.fire<interfaces.IReq_ControllerBrowserViewOperate>(
5693
- 'controller.browser.view.operate',
5694
- { viewId: this.browserViewId, operation: { type: 'acknowledgeFrame', acknowledgement } },
5695
- { maxRetries: 0, abortSignal: options.signal },
5999
+ const transport = this.browserViewTransport;
6000
+ if (!transport) throw new Error('The browser view is unavailable.');
6001
+ const result = await transport.operate(
6002
+ { type: 'acknowledgeFrame', acknowledgement },
6003
+ options.signal,
5696
6004
  );
5697
- return { accepted: response.accepted };
6005
+ return { accepted: result.status === 'succeeded' && result.accepted === true };
5698
6006
  },
5699
6007
  setViewport: async (viewport, options) => {
5700
6008
  await this.browserViewOperation({ type: 'setViewport', viewport }, true, options.signal);
@@ -5725,14 +6033,30 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
5725
6033
  focusTarget: surface,
5726
6034
  resizeTarget: surface,
5727
6035
  onError: (errorArg) => {
5728
- if (generationArg === this.browserViewGeneration) this.workspaceError = errorArg.message;
6036
+ if (generationArg !== this.browserViewGeneration) return;
6037
+ this.workspaceError = errorArg.message;
6038
+ if (
6039
+ errorArg.code === 'frame_acknowledgement_failed'
6040
+ || errorArg.code === 'frame_render_failed'
6041
+ ) {
6042
+ this.handleBrowserTransportFailure(
6043
+ generationArg,
6044
+ this.browserViewStreamGeneration,
6045
+ errorArg,
6046
+ true,
6047
+ );
6048
+ }
5729
6049
  },
5730
6050
  });
5731
6051
  this.browserRenderer = renderer;
5732
- await renderer.start();
6052
+ await this.startBrowserRenderer(renderer);
5733
6053
  return generationArg === this.browserViewGeneration && this.browserRenderer === renderer;
5734
6054
  }
5735
6055
 
6056
+ private startBrowserRenderer(rendererArg: plugins.LiveBrowserCanvasRenderer): Promise<void> {
6057
+ return waitForBrowserRendererLifecycle(rendererArg.start(), 'start');
6058
+ }
6059
+
5736
6060
  private readonly handleResourceAssociationRequest = (
5737
6061
  eventArg: CustomEvent<plugins.deesCatalog.THarnessResourceAssociationRequestDetail>,
5738
6062
  ): void => {
@@ -7518,7 +7842,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
7518
7842
  ) return;
7519
7843
  this.resources = Array.isArray(response.resources) ? response.resources : [];
7520
7844
  this.resourcesLoadedForProjectId = projectId;
7521
- const viewedBrowserResource = this.resources.find((resource) => (
7845
+ const viewedBrowserResource = this.resources.find((resource): resource is interfaces.IControllerBrowserResource => (
7522
7846
  resource.id === this.browserViewResourceId && resource.kind === 'browser'
7523
7847
  ));
7524
7848
  if (
@@ -7528,7 +7852,22 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
7528
7852
  || viewedBrowserResource.lifecycle !== 'active'
7529
7853
  || viewedBrowserResource.attachment.revision !== this.browserViewAttachmentRevision
7530
7854
  )
7531
- ) void this.closeBrowserView();
7855
+ ) {
7856
+ const reopen = Boolean(
7857
+ viewedBrowserResource
7858
+ && viewedBrowserResource.lifecycle === 'active'
7859
+ && viewedBrowserResource.browserRuntimeState === 'available'
7860
+ && this.selectedResourceId === viewedBrowserResource.id,
7861
+ );
7862
+ await this.closeBrowserView();
7863
+ if (
7864
+ reopen
7865
+ && viewedBrowserResource
7866
+ && this.selectedResourceId === viewedBrowserResource.id
7867
+ && this.selectedProjectId === projectId
7868
+ && this.isCurrentConnection(generation)
7869
+ ) await this.openBrowserView(viewedBrowserResource);
7870
+ }
7532
7871
  if (
7533
7872
  this.selectedResourceId
7534
7873
  && !this.resources.some((resource) => resource.id === this.selectedResourceId)
@@ -11864,9 +12203,12 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
11864
12203
  <dees-icon icon="lucide:Globe2" iconSize="16"></dees-icon>
11865
12204
  <input
11866
12205
  aria-label="Browser address"
11867
- .value=${activeTab?.url ?? ''}
12206
+ .value=${this.browserAddressEditing ? this.browserAddressDraft : activeTab?.url ?? ''}
11868
12207
  placeholder="https://example.com"
11869
12208
  ?disabled=${!this.browserViewId}
12209
+ @focus=${this.handleBrowserAddressFocus}
12210
+ @input=${this.handleBrowserAddressInput}
12211
+ @blur=${this.handleBrowserAddressBlur}
11870
12212
  />
11871
12213
  <dees-button
11872
12214
  type="highlighted"
@@ -12559,13 +12901,6 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
12559
12901
  ? (this.authenticated ? 'authenticated' : 'connected')
12560
12902
  : pending ? 'connecting' : 'offline';
12561
12903
  const metrics = this.systemMetrics;
12562
- const memoryPercent = metrics?.memoryUsedBytes !== null
12563
- && metrics?.memoryUsedBytes !== undefined
12564
- && metrics.memoryTotalBytes !== null
12565
- && metrics.memoryTotalBytes !== undefined
12566
- && metrics.memoryTotalBytes > 0
12567
- ? (metrics.memoryUsedBytes / metrics.memoryTotalBytes) * 100
12568
- : null;
12569
12904
  const memoryTitle = metrics?.memoryUsedBytes !== null
12570
12905
  && metrics?.memoryUsedBytes !== undefined
12571
12906
  && metrics.memoryTotalBytes !== null
@@ -12589,19 +12924,19 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
12589
12924
  : 'Whole-system CPU usage'}
12590
12925
  >
12591
12926
  <span class="metricLabel">CPU</span>
12592
- <span>${formatPercentage(metrics?.cpuUsagePercent)}</span>
12927
+ <span class="metricValue">--</span>
12593
12928
  </span>
12594
12929
  <span class="statusMetric memoryMetric" title=${memoryTitle}>
12595
12930
  <span class="metricLabel">RAM</span>
12596
- <span>${formatPercentage(memoryPercent)}</span>
12931
+ <span class="metricValue">--</span>
12597
12932
  </span>
12598
12933
  <span
12599
12934
  class="statusMetric networkMetric"
12600
12935
  title="Aggregate traffic across all system network interfaces"
12601
12936
  >
12602
12937
  <span class="metricLabel">NET</span>
12603
- <span>in ${formatByteRate(metrics?.networkReceiveBytesPerSecond)}</span>
12604
- <span>out ${formatByteRate(metrics?.networkTransmitBytesPerSecond)}</span>
12938
+ <span class="networkReceive">in --</span>
12939
+ <span class="networkTransmit">out --</span>
12605
12940
  </span>
12606
12941
  </div>
12607
12942
  `