@agenthippo-ai/agenthippo 0.1.1 → 1.110.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.
Files changed (158) hide show
  1. package/out/agentpack/index.js +2 -1
  2. package/out/agentpack/paths.js +23 -0
  3. package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
  4. package/out/analytics/dashboard/provider.js +13 -2
  5. package/out/analytics/dashboard/webview/panel.js +293 -82
  6. package/out/analytics/identity.js +0 -0
  7. package/out/analytics/service.js +23 -25
  8. package/out/analytics/sessionEvents.js +109 -22
  9. package/out/auth/authBaseUrl.js +59 -0
  10. package/out/auth/bootstrapApply.js +9 -10
  11. package/out/auth/brokerClient.js +49 -15
  12. package/out/auth/onboardingPrompt.js +112 -0
  13. package/out/auth/provider.js +9 -22
  14. package/out/auth/sessionStore.js +3 -40
  15. package/out/auth/signInFlow.js +2 -1
  16. package/out/auth-broker/config.js +54 -4
  17. package/out/auth-broker/server.js +152 -20
  18. package/out/auth-broker/store.js +25 -24
  19. package/out/authority/warrant.js +206 -0
  20. package/out/branding.js +27 -1
  21. package/out/cli/commands/ask.js +3 -0
  22. package/out/cli/commands/chat.js +3 -0
  23. package/out/cli/commands/conformance.js +249 -0
  24. package/out/cli/commands/data-plane.js +67 -0
  25. package/out/cli/commands/deploy.js +50 -10
  26. package/out/cli/commands/gateway.js +58 -0
  27. package/out/cli/commands/idp.js +85 -0
  28. package/out/cli/commands/pack.js +182 -3
  29. package/out/cli/commands/sandbox.js +332 -0
  30. package/out/cli/commands/serve.js +1165 -82
  31. package/out/cli/commands/store.js +12 -4
  32. package/out/cli/credential-context.js +42 -80
  33. package/out/cli/data-plane/broker.js +78 -0
  34. package/out/cli/data-plane/connector-host.js +121 -0
  35. package/out/cli/data-plane/egress.js +189 -0
  36. package/out/cli/data-plane/gateway.js +218 -0
  37. package/out/cli/data-plane/index.js +152 -0
  38. package/out/cli/data-plane/registry.js +89 -0
  39. package/out/cli/data-plane/remote.js +106 -0
  40. package/out/cli/data-plane/sidecar.js +161 -0
  41. package/out/cli/deploy/blueprints/registry.js +84 -0
  42. package/out/cli/deploy/layers/identity.js +172 -0
  43. package/out/cli/deploy-init.js +1182 -347
  44. package/out/cli/deps.js +106 -2
  45. package/out/cli/idp-init.js +338 -0
  46. package/out/cli/index.js +192 -23
  47. package/out/cli/pack-install-ref.js +15 -0
  48. package/out/cli/pack-loader.js +45 -3
  49. package/out/cli/pack-runtime.js +61 -0
  50. package/out/cli/runtime.js +44 -3
  51. package/out/cli/serve-hardening.js +139 -3
  52. package/out/cli/state-sync.js +16 -15
  53. package/out/cli/utils.js +12 -1
  54. package/out/engines/base.js +32 -3
  55. package/out/engines/claude.js +47 -8
  56. package/out/engines/codex.js +125 -22
  57. package/out/engines/custom.js +54 -12
  58. package/out/evidence/sessionShip.js +610 -0
  59. package/out/extension.js +13 -2
  60. package/out/gateway/bridge.js +40 -2
  61. package/out/gateway/client.js +34 -1
  62. package/out/gateway/installSource.js +68 -0
  63. package/out/gateway/lifecycle.js +185 -51
  64. package/out/gateway/outboundPaths.js +4 -1
  65. package/out/gateway/outboundSend.js +6 -6
  66. package/out/gateway/secretsPreflight.js +90 -0
  67. package/out/gateway/service.js +7 -2
  68. package/out/headless/index.js +40 -9
  69. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  70. package/out/live-probe/connector.mjs +8 -0
  71. package/out/live-probe/env-report.mjs +8 -0
  72. package/out/mcp.js +115 -67
  73. package/out/pack/caps.js +271 -0
  74. package/out/pack/governor.js +309 -0
  75. package/out/pack/lease-schema.js +270 -0
  76. package/out/pack/lease.js +640 -0
  77. package/out/pack/reconcile.js +217 -0
  78. package/out/pack/signature.js +839 -0
  79. package/out/remote/handleRemoteChat.js +44 -3
  80. package/out/remote/remoteTrust.js +277 -0
  81. package/out/remote/serveClient.js +52 -5
  82. package/out/runner/host.js +131 -0
  83. package/out/runner/protocol.js +108 -0
  84. package/out/runner/remoteEngine.js +130 -0
  85. package/out/runtime/interfaces/edit-tracker.js +2 -0
  86. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  87. package/out/sandbox/containerDriver.js +570 -0
  88. package/out/sandbox/processDriver.js +315 -0
  89. package/out/sandbox/sandboxRunner.js +191 -0
  90. package/out/sandbox/support.js +301 -0
  91. package/out/sandbox/types.js +55 -0
  92. package/out/scripts/agentide-litellm.sh +67 -5
  93. package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
  94. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -118
  95. package/out/scripts/analytics/setup-analytics.ps1 +37 -13
  96. package/out/scripts/analytics/setup-analytics.sh +93 -13
  97. package/out/scripts/analytics/start-analytics.sh +86 -14
  98. package/out/scripts/gateway/setup-gateway.sh +72 -32
  99. package/out/shared/models.js +12 -111
  100. package/out/shared/providers.js +2 -2
  101. package/out/spotlight/install.js +7 -1
  102. package/out/store/index.js +41 -10
  103. package/out/sync.js +3 -1
  104. package/out/test/analytics.test.js +198 -34
  105. package/out/test/auth-baseurl.test.js +132 -0
  106. package/out/test/auth-bootstrapApply.test.js +27 -6
  107. package/out/test/auth-broker-hardening.test.js +227 -0
  108. package/out/test/auth-broker.test.js +112 -9
  109. package/out/test/blueprint-registry.test.js +105 -0
  110. package/out/test/branding.test.js +13 -0
  111. package/out/test/caps-governor.test.js +455 -0
  112. package/out/test/codex-new-file-survival.test.js +119 -0
  113. package/out/test/configuration-scopes.test.js +131 -0
  114. package/out/test/credential-context.test.js +23 -28
  115. package/out/test/custom-env-layering.test.js +151 -0
  116. package/out/test/data-plane-engine-wiring.test.js +250 -0
  117. package/out/test/data-plane.test.js +311 -0
  118. package/out/test/deploy-init.test.js +295 -4
  119. package/out/test/egress-broker.test.js +172 -0
  120. package/out/test/fleetDisplayUtils.test.js +4 -0
  121. package/out/test/gateway-client.test.js +28 -0
  122. package/out/test/gateway-install-source.test.js +111 -0
  123. package/out/test/gateway-lifecycle.test.js +1 -1
  124. package/out/test/gateway-outbound.test.js +7 -0
  125. package/out/test/gateway-secrets-preflight.test.js +127 -0
  126. package/out/test/gateway-session-routing.test.js +18 -0
  127. package/out/test/governor-hardening.test.js +217 -0
  128. package/out/test/headless-sync.test.js +22 -0
  129. package/out/test/idp-init.test.js +151 -0
  130. package/out/test/lease-signature.test.js +357 -0
  131. package/out/test/lease-trust-root.test.js +273 -0
  132. package/out/test/lease-validation.test.js +169 -0
  133. package/out/test/lease.test.js +114 -0
  134. package/out/test/mcp-obo.test.js +52 -27
  135. package/out/test/mcp.test.js +69 -3
  136. package/out/test/model-routing.test.js +133 -0
  137. package/out/test/pack-admission.test.js +216 -0
  138. package/out/test/pack-digest-parity.test.js +140 -0
  139. package/out/test/pack-reconcile.test.js +183 -0
  140. package/out/test/pack-signature-artifact.test.js +103 -0
  141. package/out/test/pack-signature.test.js +176 -0
  142. package/out/test/pack-trust-root.test.js +202 -0
  143. package/out/test/remote-trust.test.js +242 -0
  144. package/out/test/runner-protocol.test.js +249 -0
  145. package/out/test/sandbox-driver.test.js +553 -0
  146. package/out/test/sandbox-hardening.test.js +395 -0
  147. package/out/test/sandbox-runner.test.js +343 -0
  148. package/out/test/serve-gateway-turn.test.js +267 -0
  149. package/out/test/serve-hardening.test.js +125 -6
  150. package/out/test/serveClient.test.js +38 -5
  151. package/out/test/sessionShip.test.js +560 -0
  152. package/out/test/spotlightPublish.test.js +28 -0
  153. package/out/test/state-sync-peer.test.js +18 -5
  154. package/out/test/store-install-safe.test.js +69 -0
  155. package/out/test/tls-bypass.test.js +90 -0
  156. package/out/test/warrant.test.js +138 -0
  157. package/out/utils.js +73 -20
  158. package/package.json +1 -1
@@ -5,8 +5,9 @@
5
5
  * Handles artifact display, rendering, and interactions.
6
6
  * Uses panelApi (provided by runtime-bridge.js) instead of raw VS Code API.
7
7
  *
8
- * This file is seeded to .agent-hippo/analytics/panel/panel.js on first use.
9
- * Users can edit the seeded copy to customize the dashboard UI.
8
+ * This bundled file is the ONLY panel.js ever executed (see provider.getHtmlContent).
9
+ * It is not seeded to .agent-hippo/analytics/panel/ — a copy there would never run.
10
+ * Dashboard UI is customized via panel.html / panel.css, which ARE read from that directory.
10
11
  * ----------------------------------------------------------------------------- */
11
12
 
12
13
  // @ts-check
@@ -17,7 +18,7 @@ const panelApi = window.panelApi;
17
18
 
18
19
  // Seed/version marker for deciding whether a user's locally seeded panel.js
19
20
  // is compatible with the extension runtime (e.g. message forwarding fixes).
20
- const AGENTIDE_PANEL_JS_VERSION = '2026.05.30';
21
+ const AGENTIDE_PANEL_JS_VERSION = '2026.07.21';
21
22
  try { window.__AGENTIDE_PANEL_JS_VERSION__ = AGENTIDE_PANEL_JS_VERSION; } catch { /* no-op */ }
22
23
 
23
24
  // -----------------------------------------------------------------------------
@@ -44,7 +45,7 @@ function createDefaultPanelState() {
44
45
  return { version: 1, charts: {}, summaryHidden: false };
45
46
  }
46
47
 
47
- /** @type {{ artifacts: object[], galleryIgnorePatterns: string[], summaryCards: object[] | null, manifest: object | null, isRefreshing: boolean, scope: 'global'|'workspace', projectId: string, agentId: string, agentIds: string[], views: Array<{ id: string, name: string, scope: 'global'|'workspace', system: boolean, layout?: 'fleet-dashboard'|'gallery', refreshOnNavigate?: 'always'|'when-empty', preserveFleetIframe?: boolean }>, activeViewId: string, disabledCharts: Set<string>, panelState: PanelState, panelStateLoaded: boolean, lastArtifactCatalogSignature: string, fleetAutoRefreshIntervalSeconds: number, fleetRefreshVersion: number }} */
48
+ /** @type {{ artifacts: object[], galleryIgnorePatterns: string[], summaryCards: object[] | null, manifest: object | null, isRefreshing: boolean, scope: 'global'|'workspace', projectId: string, agentId: string, agentIds: string[], views: Array<{ id: string, name: string, scope: 'global'|'workspace', system: boolean, layout?: 'fleet-dashboard'|'gallery', refreshOnNavigate?: 'always'|'when-empty', preserveFleetIframe?: boolean }>, activeViewId: string, disabledCharts: Set<string>, panelState: PanelState, panelStateLoaded: boolean, lastArtifactCatalogSignature: string, fleetAutoRefreshIntervalSeconds: number, fleetRefreshVersion: number, needsGalleryRender: boolean, pendingForceFreshHtml: boolean, refreshingViewId: string | null }} */
48
49
  const state = {
49
50
  artifacts: [],
50
51
  galleryIgnorePatterns: [],
@@ -64,8 +65,64 @@ const state = {
64
65
  fleetAutoRefreshIntervalSeconds: 30,
65
66
  fleetRefreshVersion: 0,
66
67
  surfaceMode: 'spotlight',
68
+ // While a refresh is in flight, multiple artifactsUpdated bursts can arrive as scripts/
69
+ // watchers write files incrementally. Rendering on each one tears down and rebuilds the
70
+ // whole gallery (see renderGallery), which is visibly flickery. Defer to one render when
71
+ // the refresh actually ends (refreshFinished/refreshFailed/refreshNoData).
72
+ needsGalleryRender: false,
73
+ pendingForceFreshHtml: false,
74
+ // View id the in-flight refresh belongs to. Termination messages carry the viewId captured
75
+ // at refresh start, so after a mid-refresh view switch they no longer match activeViewId —
76
+ // match against this instead so isRefreshing cannot get stuck true.
77
+ refreshingViewId: null,
67
78
  };
68
79
 
80
+ /** Render the gallery now if a render was deferred mid-refresh. */
81
+ function flushPendingGalleryRender() {
82
+ if (!state.needsGalleryRender) {
83
+ return;
84
+ }
85
+ state.needsGalleryRender = false;
86
+ const forceFreshHtml = state.pendingForceFreshHtml;
87
+ state.pendingForceFreshHtml = false;
88
+ renderGallery(state.artifacts, { forceFreshHtml });
89
+ }
90
+
91
+ /**
92
+ * Single entry point for gallery renders triggered by host messages. While a refresh is in
93
+ * flight, defer to one render at refresh termination instead of tearing down and rebuilding
94
+ * the gallery on every artifactsUpdated/manifestUpdated/disabledChartsUpdated burst.
95
+ * @param {{ forceFreshHtml?: boolean }} [options]
96
+ */
97
+ function requestGalleryRender(options) {
98
+ if (state.isRefreshing) {
99
+ state.needsGalleryRender = true;
100
+ if (options?.forceFreshHtml) {
101
+ state.pendingForceFreshHtml = true;
102
+ }
103
+ return;
104
+ }
105
+ renderGallery(state.artifacts, { forceFreshHtml: Boolean(options?.forceFreshHtml) });
106
+ }
107
+
108
+ /**
109
+ * The active view changed while a refresh may be in flight: that refresh belongs to the view
110
+ * being left, so stop tracking it. The abandoned refresh's termination message is dropped by
111
+ * the viewId gates, and without this reset a deferred render would block the new view's first
112
+ * paint until that refresh ends (or forever, if no matching termination arrives).
113
+ */
114
+ function resetRefreshTrackingForViewChange() {
115
+ if (!state.isRefreshing && !state.needsGalleryRender && !state.pendingForceFreshHtml) {
116
+ return;
117
+ }
118
+ state.isRefreshing = false;
119
+ state.refreshingViewId = null;
120
+ state.needsGalleryRender = false;
121
+ state.pendingForceFreshHtml = false;
122
+ setRefreshButtonState(false);
123
+ setPerChartRefreshing(false);
124
+ }
125
+
69
126
  function applySurfaceMode(mode) {
70
127
  const team = mode === 'team';
71
128
  state.surfaceMode = team ? 'team' : 'spotlight';
@@ -362,6 +419,7 @@ function switchToView(nextViewId) {
362
419
  return;
363
420
  }
364
421
  state.activeViewId = nextViewId;
422
+ resetRefreshTrackingForViewChange();
365
423
  state.panelStateLoaded = false;
366
424
  state.panelState = createDefaultPanelState();
367
425
  const viewSelect = /** @type {HTMLSelectElement | null} */ (document.getElementById('view-select'));
@@ -849,6 +907,9 @@ function toggleChartHiddenInPanelState(filename) {
849
907
  // Summary Cards
850
908
  // -----------------------------------------------------------------------------
851
909
 
910
+ /** Last rendered summary-cards payload, to skip no-op rebuilds of the summary strip. */
911
+ let lastSummaryCardsSignature = '';
912
+
852
913
  /**
853
914
  * Render summary cards in the header
854
915
  * @param {Array<{label: string, value: string, icon: string}>} cards
@@ -859,10 +920,19 @@ function renderSummaryCards(cards) {
859
920
  return;
860
921
  }
861
922
  if (!cards || cards.length === 0) {
923
+ lastSummaryCardsSignature = '';
862
924
  renderSummaryPlaceholders();
863
925
  return;
864
926
  }
865
927
 
928
+ // The host posts summaryCards more than once per refresh; skip the rebuild (and its
929
+ // visible flash) when the payload is identical to what is already rendered.
930
+ const signature = JSON.stringify(cards);
931
+ if (signature === lastSummaryCardsSignature && !container.classList.contains('loading')) {
932
+ return;
933
+ }
934
+ lastSummaryCardsSignature = signature;
935
+
866
936
  container.style.display = 'grid';
867
937
  container.classList.remove('loading');
868
938
  container.innerHTML = cards.map(card => `
@@ -987,7 +1057,6 @@ function renderGallery(artifacts, options) {
987
1057
  }
988
1058
  return new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime();
989
1059
  });
990
- const interactiveChartKeys = chartArtifacts.map((artifact) => normalizeFilename(String(artifact.filename || '')));
991
1060
  const nonChartArtifacts = visibleArtifacts
992
1061
  .filter((artifact) => artifact.type !== 'chart')
993
1062
  .sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime());
@@ -1085,7 +1154,7 @@ function renderGallery(artifacts, options) {
1085
1154
 
1086
1155
  showGalleryState('content');
1087
1156
 
1088
- const html = orderedArtifacts.map(artifact => {
1157
+ const buildCardHtml = (artifact) => {
1089
1158
  const timeAgo = formatTimeAgo(new Date(artifact.modifiedAt));
1090
1159
  const sizeFormatted = formatSize(artifact.size);
1091
1160
  const isChart = artifact.type === 'chart';
@@ -1175,101 +1244,214 @@ function renderGallery(artifacts, options) {
1175
1244
  ${resizeHandle}
1176
1245
  </div>
1177
1246
  `;
1178
- }).join('');
1247
+ };
1248
+ // Keyed card reuse: diff incoming artifacts against existing cards by localPath and only
1249
+ // rebuild what changed. Rebuilding a card destroys its iframe/img and re-fetches content —
1250
+ // that teardown is the visible blink — so unchanged cards must not be touched at all.
1251
+ /** @type {Map<string, HTMLElement>} */
1252
+ const existingCards = new Map();
1253
+ Array.from(container.children).forEach((childEl) => {
1254
+ const child = /** @type {HTMLElement} */ (childEl);
1255
+ const childPath = child.dataset.path || '';
1256
+ const reusableAsCard =
1257
+ child.classList.contains('artifact-card')
1258
+ && !child.classList.contains('fleet-dashboard-full') // fleet layout card: different structure
1259
+ && !child.classList.contains('deleting')
1260
+ && childPath
1261
+ && !existingCards.has(childPath);
1262
+ if (!reusableAsCard) {
1263
+ child.remove();
1264
+ return;
1265
+ }
1266
+ existingCards.set(childPath, child);
1267
+ });
1179
1268
 
1180
- container.innerHTML = html;
1181
1269
  /** @type {Array<object>} */
1182
1270
  const htmlArtifactsNeedingFetch = [];
1183
1271
  /** @type {Array<object>} */
1184
1272
  const markdownArtifactsNeedingFetch = [];
1185
- container.querySelectorAll('.artifact-card').forEach((cardEl) => {
1186
- const card = /** @type {HTMLElement} */ (cardEl);
1187
- const path = card.dataset.path || '';
1188
- const modifiedAt = card.dataset.modifiedAt || '';
1273
+ /** @type {HTMLElement[]} */
1274
+ const orderedCards = [];
1275
+ for (const artifact of orderedArtifacts) {
1276
+ const path = String(artifact.localPath || '');
1277
+ const modifiedAt = String(artifact.modifiedAt || '');
1278
+ const isVisual = artifact.type === 'chart' || artifact.type === 'image';
1279
+ const chartState = isVisual ? artifact._panel : undefined;
1280
+ const signature = computeCardRenderSignature(artifact);
1281
+ const forceHtmlReload = forceFreshHtml && artifact.format === 'html';
1282
+ const existing = existingCards.get(path);
1283
+ existingCards.delete(path);
1284
+
1285
+ if (existing && existing.dataset.cardSignature === signature && !forceHtmlReload) {
1286
+ // Unchanged: leave the DOM (and any live iframe) alone. Expanded/height are kept
1287
+ // out of the signature and applied in place so toggling them never reloads content.
1288
+ if (isVisual) {
1289
+ existing.classList.toggle('expanded', chartState?.expanded === true);
1290
+ const contentEl = /** @type {HTMLElement | null} */ (existing.querySelector('.artifact-content'));
1291
+ if (contentEl && chartState?.height) {
1292
+ contentEl.style.height = `${chartState.height}px`;
1293
+ contentEl.style.minHeight = `${chartState.height}px`;
1294
+ contentEl.style.overflow = 'auto';
1295
+ }
1296
+ }
1297
+ const timeEl = existing.querySelector('.artifact-time');
1298
+ if (timeEl) {
1299
+ timeEl.textContent = formatTimeAgo(new Date(artifact.modifiedAt));
1300
+ }
1301
+ orderedCards.push(existing);
1302
+ continue;
1303
+ }
1304
+
1305
+ const template = document.createElement('template');
1306
+ template.innerHTML = buildCardHtml(artifact).trim();
1307
+ const card = /** @type {HTMLElement | null} */ (template.content.firstElementChild);
1308
+ if (!card) {
1309
+ continue;
1310
+ }
1311
+ card.dataset.cardSignature = signature;
1189
1312
  const htmlHost = /** @type {HTMLElement | null} */ (card.querySelector('.html-preview'));
1190
- if (htmlHost && path && modifiedAt) {
1191
- const reusable = forceFreshHtml ? undefined : reusableHtmlPreviews.get(`${path}::${modifiedAt}`);
1313
+ if (htmlHost) {
1314
+ const reusable = forceHtmlReload ? undefined : reusableHtmlPreviews.get(`${path}::${modifiedAt}`);
1192
1315
  if (reusable) {
1193
1316
  htmlHost.classList.add('ready');
1194
1317
  htmlHost.innerHTML = '';
1195
1318
  htmlHost.appendChild(reusable);
1196
- } else {
1197
- const htmlArtifact = orderedArtifacts.find((item) => item.localPath === path && item.format === 'html');
1198
- if (htmlArtifact) {
1199
- htmlArtifactsNeedingFetch.push(htmlArtifact);
1200
- }
1319
+ } else if (artifact.format === 'html') {
1320
+ htmlArtifactsNeedingFetch.push(artifact);
1201
1321
  }
1202
1322
  }
1323
+ if (card.querySelector('.markdown-preview')) {
1324
+ markdownArtifactsNeedingFetch.push(artifact);
1325
+ }
1326
+ attachArtifactCardHandlers(card);
1327
+ if (existing) {
1328
+ existing.replaceWith(card);
1329
+ }
1330
+ orderedCards.push(card);
1331
+ }
1203
1332
 
1204
- const markdownHost = /** @type {HTMLElement | null} */ (card.querySelector('.markdown-preview'));
1205
- if (markdownHost && path) {
1206
- const markdownArtifact = orderedArtifacts.find((item) => item.localPath === path && item.format === 'md');
1207
- if (markdownArtifact) {
1208
- markdownArtifactsNeedingFetch.push(markdownArtifact);
1209
- }
1333
+ // Cards whose artifacts disappeared.
1334
+ existingCards.forEach((card) => card.remove());
1335
+
1336
+ // Minimal-move reorder: re-inserting a node reloads its iframe, so only move cards that
1337
+ // are actually out of position.
1338
+ let cursor = container.firstElementChild;
1339
+ for (const card of orderedCards) {
1340
+ if (card === cursor) {
1341
+ cursor = cursor.nextElementSibling;
1342
+ continue;
1210
1343
  }
1211
- });
1344
+ container.insertBefore(card, cursor);
1345
+ }
1346
+
1212
1347
  requestHtmlPreviewContents(htmlArtifactsNeedingFetch);
1213
1348
  requestMarkdownPreviewContents(markdownArtifactsNeedingFetch);
1214
1349
 
1215
- // Attach event handlers
1216
- container.querySelectorAll('.artifact-card').forEach(cardEl => {
1217
- const card = /** @type {HTMLElement} */ (cardEl);
1218
- const path = card.dataset.path;
1219
- const filename = card.dataset.filename;
1220
- const isChart = card.dataset.type === 'chart';
1221
- const isVisual = card.dataset.type === 'chart' || card.dataset.type === 'image';
1350
+ // Markdown previews are loaded via host message passing (artifactContent)
1351
+ // to avoid fetch restrictions for local report files.
1352
+ }
1222
1353
 
1223
- card.querySelector('.open-btn')?.addEventListener('click', (e) => {
1354
+ /**
1355
+ * Per-card render signature: every input that affects a card's DOM except expanded/height,
1356
+ * which renderGallery applies in place so toggling them never rebuilds the card.
1357
+ * @param {object} artifact
1358
+ * @returns {string}
1359
+ */
1360
+ function computeCardRenderSignature(artifact) {
1361
+ const isVisual = artifact.type === 'chart' || artifact.type === 'image';
1362
+ const chartState = isVisual ? artifact._panel : undefined;
1363
+ const manifestEntry = state.manifest?.artifacts?.find(e => e.filename === artifact.filename);
1364
+ return [
1365
+ String(artifact.localPath || ''),
1366
+ String(artifact.filename || ''),
1367
+ String(artifact.modifiedAt || ''),
1368
+ String(artifact.size || ''),
1369
+ String(artifact.type || ''),
1370
+ String(artifact.format || ''),
1371
+ String(artifact.generator || ''),
1372
+ String(manifestEntry?.title || ''),
1373
+ String(manifestEntry?.source || ''),
1374
+ chartState?.hidden === true ? '1' : '0',
1375
+ Number.isFinite(chartState?.order) ? String(chartState.order) : '',
1376
+ ].join('|');
1377
+ }
1378
+
1379
+ /**
1380
+ * Chart/image ordering keys derived from current DOM order — computed at click time so
1381
+ * handlers on long-lived cards never act on a stale snapshot.
1382
+ * @returns {string[]}
1383
+ */
1384
+ function getInteractiveChartKeysFromDom() {
1385
+ const container = document.getElementById('gallery-content');
1386
+ if (!container) {
1387
+ return [];
1388
+ }
1389
+ return Array.from(container.querySelectorAll('.artifact-card'))
1390
+ .filter((cardEl) => {
1391
+ const card = /** @type {HTMLElement} */ (cardEl);
1392
+ return card.dataset.type === 'chart' || card.dataset.type === 'image';
1393
+ })
1394
+ .map((cardEl) => normalizeFilename(String(/** @type {HTMLElement} */ (cardEl).dataset.filename || '')))
1395
+ .filter(Boolean);
1396
+ }
1397
+
1398
+ /**
1399
+ * Wire interactive behaviors for one gallery card (open/delete, layout buttons, expand on
1400
+ * click, resize handle). Called once per created card; reused cards keep their listeners.
1401
+ * @param {HTMLElement} card
1402
+ */
1403
+ function attachArtifactCardHandlers(card) {
1404
+ const path = card.dataset.path;
1405
+ const filename = card.dataset.filename;
1406
+ const isChart = card.dataset.type === 'chart';
1407
+ const isVisual = card.dataset.type === 'chart' || card.dataset.type === 'image';
1408
+
1409
+ card.querySelector('.open-btn')?.addEventListener('click', (e) => {
1410
+ e.stopPropagation();
1411
+ panelApi.postMessage({ type: 'openArtifact', path });
1412
+ });
1413
+
1414
+ card.querySelector('.delete-btn')?.addEventListener('click', (e) => {
1415
+ e.stopPropagation();
1416
+ panelApi.postMessage({ type: 'deleteArtifact', path });
1417
+ });
1418
+
1419
+ if (isChart && filename) {
1420
+ card.querySelector('.move-up-btn')?.addEventListener('click', (e) => {
1224
1421
  e.stopPropagation();
1225
- panelApi.postMessage({ type: 'openArtifact', path });
1422
+ moveChartInPanelState(filename, 'up', getInteractiveChartKeysFromDom());
1423
+ renderGallery(state.artifacts);
1226
1424
  });
1227
1425
 
1228
- card.querySelector('.delete-btn')?.addEventListener('click', (e) => {
1426
+ card.querySelector('.move-down-btn')?.addEventListener('click', (e) => {
1229
1427
  e.stopPropagation();
1230
- panelApi.postMessage({ type: 'deleteArtifact', path });
1428
+ moveChartInPanelState(filename, 'down', getInteractiveChartKeysFromDom());
1429
+ renderGallery(state.artifacts);
1231
1430
  });
1232
1431
 
1233
- if (isChart && filename) {
1234
- card.querySelector('.move-up-btn')?.addEventListener('click', (e) => {
1235
- e.stopPropagation();
1236
- moveChartInPanelState(filename, 'up', interactiveChartKeys);
1237
- renderGallery(state.artifacts);
1238
- });
1239
-
1240
- card.querySelector('.move-down-btn')?.addEventListener('click', (e) => {
1241
- e.stopPropagation();
1242
- moveChartInPanelState(filename, 'down', interactiveChartKeys);
1243
- renderGallery(state.artifacts);
1244
- });
1245
-
1246
- card.querySelector('.toggle-enabled-btn')?.addEventListener('click', (e) => {
1247
- e.stopPropagation();
1248
- toggleChartHiddenInPanelState(filename);
1249
- renderGallery(state.artifacts);
1250
- });
1251
- }
1252
-
1253
- // Click on card to expand/collapse
1254
- card.addEventListener('click', (event) => {
1255
- const target = /** @type {HTMLElement | null} */ (event.target instanceof HTMLElement ? event.target : null);
1256
- if (target?.closest('button,.resize-handle')) {
1257
- return;
1258
- }
1259
- const nextExpanded = !card.classList.contains('expanded');
1260
- card.classList.toggle('expanded', nextExpanded);
1261
- if (isVisual && filename) {
1262
- setChartExpanded(filename, nextExpanded);
1263
- }
1432
+ card.querySelector('.toggle-enabled-btn')?.addEventListener('click', (e) => {
1433
+ e.stopPropagation();
1434
+ toggleChartHiddenInPanelState(filename);
1435
+ renderGallery(state.artifacts);
1264
1436
  });
1437
+ }
1265
1438
 
1266
- if (filename) {
1267
- attachResizeHandle(card, filename);
1439
+ // Click on card to expand/collapse
1440
+ card.addEventListener('click', (event) => {
1441
+ const target = /** @type {HTMLElement | null} */ (event.target instanceof HTMLElement ? event.target : null);
1442
+ if (target?.closest('button,.resize-handle')) {
1443
+ return;
1444
+ }
1445
+ const nextExpanded = !card.classList.contains('expanded');
1446
+ card.classList.toggle('expanded', nextExpanded);
1447
+ if (isVisual && filename) {
1448
+ setChartExpanded(filename, nextExpanded);
1268
1449
  }
1269
1450
  });
1270
1451
 
1271
- // Markdown previews are loaded via host message passing (artifactContent)
1272
- // to avoid fetch restrictions for local report files.
1452
+ if (filename) {
1453
+ attachResizeHandle(card, filename);
1454
+ }
1273
1455
  }
1274
1456
 
1275
1457
  /**
@@ -2044,12 +2226,18 @@ panelApi.onMessage(msg => {
2044
2226
  state.artifacts = msg.artifacts;
2045
2227
  state.galleryIgnorePatterns = nextGalleryIgnore;
2046
2228
  // View selection is owned by viewsLoaded; do not let artifact batches revert the dropdown.
2229
+ const prevActiveViewId = state.activeViewId;
2047
2230
  if (
2048
2231
  nextActiveViewId === state.activeViewId
2049
2232
  || !state.views.some((view) => view.id === state.activeViewId)
2050
2233
  ) {
2051
2234
  state.activeViewId = nextActiveViewId;
2052
2235
  }
2236
+ if (state.activeViewId !== prevActiveViewId) {
2237
+ // Host-initiated view change (e.g. openWithView): any in-flight refresh belongs
2238
+ // to the previous view — render now instead of deferring behind it.
2239
+ resetRefreshTrackingForViewChange();
2240
+ }
2053
2241
  state.fleetRefreshVersion = nextFleetRefreshVersion;
2054
2242
  applyFleetAutoRefresh();
2055
2243
  if (exportOnlyCatalogChange) {
@@ -2059,10 +2247,11 @@ panelApi.onMessage(msg => {
2059
2247
  // Fleet JSON can change without agent-fleet.html mtime changing if the host
2060
2248
  // did not re-run the widget script; bumping fleetRefreshVersion means data is
2061
2249
  // fresher — force a preview reload so the iframe is not reused with stale HTML.
2062
- renderGallery(state.artifacts, { forceFreshHtml: hasFleetVersionUpdate });
2063
- if (state.isRefreshing) {
2064
- setPerChartRefreshing(true);
2065
- }
2250
+ if (state.isRefreshing) {
2251
+ // Mid-refresh burst: keep spinners on; requestGalleryRender defers the rebuild.
2252
+ setPerChartRefreshing(true);
2253
+ }
2254
+ requestGalleryRender({ forceFreshHtml: hasFleetVersionUpdate });
2066
2255
  break;
2067
2256
  }
2068
2257
 
@@ -2093,7 +2282,7 @@ panelApi.onMessage(msg => {
2093
2282
  state.manifest = msg.manifest;
2094
2283
  // Re-render gallery with new manifest data
2095
2284
  if (state.artifacts.length > 0) {
2096
- renderGallery(state.artifacts);
2285
+ requestGalleryRender();
2097
2286
  }
2098
2287
  break;
2099
2288
 
@@ -2137,6 +2326,10 @@ panelApi.onMessage(msg => {
2137
2326
  case 'generatingStarted':
2138
2327
  setRefreshButtonState(true);
2139
2328
  state.isRefreshing = false;
2329
+ state.refreshingViewId = null;
2330
+ // Drop any deferred render — a later flush would stomp the generating state.
2331
+ state.needsGalleryRender = false;
2332
+ state.pendingForceFreshHtml = false;
2140
2333
  setPerChartRefreshing(false);
2141
2334
  showGeneratingState();
2142
2335
  break;
@@ -2144,6 +2337,9 @@ panelApi.onMessage(msg => {
2144
2337
  case 'generatingCancelled':
2145
2338
  setRefreshButtonState(false);
2146
2339
  state.isRefreshing = false;
2340
+ state.refreshingViewId = null;
2341
+ state.needsGalleryRender = false;
2342
+ state.pendingForceFreshHtml = false;
2147
2343
  setPerChartRefreshing(false);
2148
2344
  // Go back to empty or content state
2149
2345
  if (state.artifacts.length > 0) {
@@ -2156,6 +2352,9 @@ panelApi.onMessage(msg => {
2156
2352
  case 'generatingFailed':
2157
2353
  setRefreshButtonState(false);
2158
2354
  state.isRefreshing = false;
2355
+ state.refreshingViewId = null;
2356
+ state.needsGalleryRender = false;
2357
+ state.pendingForceFreshHtml = false;
2159
2358
  setPerChartRefreshing(false);
2160
2359
  showFailedState(msg.error);
2161
2360
  break;
@@ -2164,15 +2363,18 @@ panelApi.onMessage(msg => {
2164
2363
  if (!msg.viewId || msg.viewId === state.activeViewId) {
2165
2364
  setRefreshButtonState(true);
2166
2365
  state.isRefreshing = true;
2366
+ state.refreshingViewId = typeof msg.viewId === 'string' && msg.viewId ? msg.viewId : null;
2167
2367
  setPerChartRefreshing(true);
2168
2368
  }
2169
2369
  break;
2170
2370
 
2171
2371
  case 'refreshFinished':
2172
- if (!msg.viewId || msg.viewId === state.activeViewId) {
2372
+ if (!msg.viewId || msg.viewId === state.activeViewId || msg.viewId === state.refreshingViewId) {
2173
2373
  setRefreshButtonState(false);
2174
2374
  state.isRefreshing = false;
2375
+ state.refreshingViewId = null;
2175
2376
  setPerChartRefreshing(false);
2377
+ flushPendingGalleryRender();
2176
2378
  if (activeViewPreservesFleetIframe()) {
2177
2379
  document.querySelectorAll('.html-preview iframe').forEach((frameEl) => {
2178
2380
  const iframe = /** @type {HTMLIFrameElement} */ (frameEl);
@@ -2186,29 +2388,38 @@ panelApi.onMessage(msg => {
2186
2388
  }
2187
2389
  break;
2188
2390
 
2189
- case 'viewsLoaded':
2391
+ case 'viewsLoaded': {
2190
2392
  if (msg.surfaceMode === 'team' || msg.surfaceMode === 'spotlight') {
2191
2393
  applySurfaceMode(msg.surfaceMode);
2192
2394
  }
2395
+ const prevViewsLoadedViewId = state.activeViewId;
2193
2396
  state.views = Array.isArray(msg.views) ? msg.views : [];
2194
2397
  state.activeViewId = typeof msg.activeViewId === 'string' ? msg.activeViewId : state.activeViewId;
2398
+ if (state.activeViewId !== prevViewsLoadedViewId) {
2399
+ resetRefreshTrackingForViewChange();
2400
+ }
2195
2401
  applyViewOptions();
2196
2402
  applyFleetAutoRefresh();
2197
2403
  break;
2404
+ }
2198
2405
 
2199
2406
  case 'refreshNoData':
2200
- if (!msg.viewId || msg.viewId === state.activeViewId) {
2407
+ if (!msg.viewId || msg.viewId === state.activeViewId || msg.viewId === state.refreshingViewId) {
2201
2408
  setRefreshButtonState(false);
2202
2409
  state.isRefreshing = false;
2410
+ state.refreshingViewId = null;
2203
2411
  setPerChartRefreshing(false);
2412
+ flushPendingGalleryRender();
2204
2413
  }
2205
2414
  break;
2206
2415
 
2207
2416
  case 'refreshFailed':
2208
- if (!msg.viewId || msg.viewId === state.activeViewId) {
2417
+ if (!msg.viewId || msg.viewId === state.activeViewId || msg.viewId === state.refreshingViewId) {
2209
2418
  setRefreshButtonState(false);
2210
2419
  state.isRefreshing = false;
2420
+ state.refreshingViewId = null;
2211
2421
  setPerChartRefreshing(false);
2422
+ flushPendingGalleryRender();
2212
2423
  if (state.artifacts.length === 0) {
2213
2424
  showFailedState(msg.error);
2214
2425
  }
@@ -2256,7 +2467,7 @@ panelApi.onMessage(msg => {
2256
2467
  .map((name) => String(name || '').trim().toLowerCase())
2257
2468
  .filter(Boolean),
2258
2469
  );
2259
- renderGallery(state.artifacts);
2470
+ requestGalleryRender();
2260
2471
  break;
2261
2472
 
2262
2473
  case 'dataStale':
Binary file
@@ -800,21 +800,22 @@ class AnalyticsService {
800
800
  '-ConfigPath',
801
801
  options.configPath,
802
802
  ]);
803
+ // A missing config is not fatal: the launcher re-seeds the managed default
804
+ // (~/.agent-hippo/litellm-otel.yaml) from its bundled template before starting.
803
805
  return [
804
806
  'Write-Host "Starting LiteLLM proxy..."',
805
807
  `if (-not (Test-Path ${q(options.configPath)})) {`,
806
- ` Write-Host ${q(`[WARN] LiteLLM config not found: ${options.configPath}`)} -ForegroundColor Yellow`,
807
- '} else {',
808
- ` $env:LITELLM_CONFIG_PATH = ${q(options.configPath)}`,
809
- ' try {',
810
- ` $proc = Start-Process -FilePath ${q(getWindowsPwshOrPowerShell())} -WindowStyle Hidden -WorkingDirectory ${q(path.dirname(options.litellmScript))} -ArgumentList ${q(launcherArgumentLine)} -RedirectStandardOutput ${q(options.stdoutPath)} -RedirectStandardError ${q(options.stderrPath)} -PassThru -ErrorAction Stop`,
811
- ' Write-Host "[OK] LiteLLM launcher started (PID: $($proc.Id))"',
812
- ` Write-Host ${q(` Logs: ${options.stdoutPath}`)}`,
813
- ` Write-Host ${q(` Errors: ${options.stderrPath}`)}`,
814
- ' } catch {',
815
- ' Write-Host "[ERROR] Failed to start LiteLLM launcher." -ForegroundColor Red',
816
- ' Write-Host $_.Exception.Message -ForegroundColor Red',
817
- ' }',
808
+ ` Write-Host ${q(`[INFO] LiteLLM config missing: ${options.configPath} — launcher will re-create it from the bundled template.`)}`,
809
+ '}',
810
+ `$env:LITELLM_CONFIG_PATH = ${q(options.configPath)}`,
811
+ 'try {',
812
+ ` $proc = Start-Process -FilePath ${q(getWindowsPwshOrPowerShell())} -WindowStyle Hidden -WorkingDirectory ${q(path.dirname(options.litellmScript))} -ArgumentList ${q(launcherArgumentLine)} -RedirectStandardOutput ${q(options.stdoutPath)} -RedirectStandardError ${q(options.stderrPath)} -PassThru -ErrorAction Stop`,
813
+ ' Write-Host "[OK] LiteLLM launcher started (PID: $($proc.Id))"',
814
+ ` Write-Host ${q(` Logs: ${options.stdoutPath}`)}`,
815
+ ` Write-Host ${q(` Errors: ${options.stderrPath}`)}`,
816
+ '} catch {',
817
+ ' Write-Host "[ERROR] Failed to start LiteLLM launcher." -ForegroundColor Red',
818
+ ' Write-Host $_.Exception.Message -ForegroundColor Red',
818
819
  '}',
819
820
  ];
820
821
  }
@@ -854,17 +855,18 @@ class AnalyticsService {
854
855
  if (!options.litellmScript) {
855
856
  return ['echo "[WARN] LiteLLM launcher script not found. Skipping LiteLLM start." >&2'];
856
857
  }
858
+ // A missing config is not fatal: the launcher re-seeds the managed default
859
+ // (~/.agent-hippo/litellm-otel.yaml) from its bundled template before starting.
857
860
  return [
858
861
  'echo "Starting LiteLLM proxy..."',
859
862
  `if [ ! -f ${q(options.configPath)} ]; then`,
860
- ` echo ${q(`[WARN] LiteLLM config not found: ${options.configPath}`)} >&2`,
861
- 'else',
862
- ` export LITELLM_CONFIG_PATH=${q(options.configPath)}`,
863
- ` nohup ${q(options.litellmScript)} --config ${q(options.configPath)} >> ${q(options.stdoutPath)} 2>> ${q(options.stderrPath)} &`,
864
- ' echo "[OK] LiteLLM launcher started (PID: $!)"',
865
- ` echo ${q(` Logs: ${options.stdoutPath}`)}`,
866
- ` echo ${q(` Errors: ${options.stderrPath}`)}`,
863
+ ` echo ${q(`[INFO] LiteLLM config missing: ${options.configPath} — launcher will re-create it from the bundled template.`)}`,
867
864
  'fi',
865
+ `export LITELLM_CONFIG_PATH=${q(options.configPath)}`,
866
+ `nohup ${q(options.litellmScript)} --config ${q(options.configPath)} >> ${q(options.stdoutPath)} 2>> ${q(options.stderrPath)} &`,
867
+ 'echo "[OK] LiteLLM launcher started (PID: $!)"',
868
+ `echo ${q(` Logs: ${options.stdoutPath}`)}`,
869
+ `echo ${q(` Errors: ${options.stderrPath}`)}`,
868
870
  ];
869
871
  }
870
872
  getLifecycleTerminalIntro(action) {
@@ -893,13 +895,9 @@ class AnalyticsService {
893
895
  * Start LiteLLM in a detached background process. Logs go to .agent-hippo/analytics/litellm.{log,error.log}.
894
896
  */
895
897
  async startLiteLLMDirectly() {
898
+ // No existence check: a missing managed config is re-seeded from the bundled
899
+ // template by the launcher itself (deleting it is a "reset to defaults" gesture).
896
900
  const configPath = path.join((0, index_1.getAgentideHome)(), 'litellm-otel.yaml');
897
- try {
898
- await fs_1.promises.access(configPath);
899
- }
900
- catch {
901
- return false;
902
- }
903
901
  const isWindows = process.platform === 'win32';
904
902
  const args = isWindows
905
903
  ? ['-ConfigPath', configPath]