@agenthippo-ai/agenthippo 0.1.1 → 0.2.3-dataplane
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/out/agentpack/index.js +2 -1
- package/out/agentpack/paths.js +23 -0
- package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
- package/out/analytics/dashboard/webview/panel.js +290 -80
- package/out/analytics/sessionEvents.js +21 -9
- package/out/auth/brokerClient.js +1 -1
- package/out/auth/onboardingPrompt.js +112 -0
- package/out/auth/provider.js +1 -1
- package/out/auth-broker/config.js +8 -3
- package/out/auth-broker/server.js +36 -0
- package/out/cli/commands/ask.js +3 -0
- package/out/cli/commands/chat.js +3 -0
- package/out/cli/commands/deploy.js +49 -10
- package/out/cli/commands/gateway.js +54 -0
- package/out/cli/commands/idp.js +85 -0
- package/out/cli/commands/serve.js +315 -55
- package/out/cli/credential-context.js +42 -80
- package/out/cli/data-plane/broker.js +78 -0
- package/out/cli/data-plane/connector-host.js +121 -0
- package/out/cli/data-plane/egress.js +189 -0
- package/out/cli/data-plane/gateway.js +195 -0
- package/out/cli/data-plane/index.js +137 -0
- package/out/cli/data-plane/registry.js +80 -0
- package/out/cli/deploy/blueprints/registry.js +84 -0
- package/out/cli/deploy/layers/identity.js +91 -0
- package/out/cli/deploy-init.js +1105 -335
- package/out/cli/idp-init.js +303 -0
- package/out/cli/index.js +37 -7
- package/out/cli/pack-loader.js +37 -2
- package/out/cli/runtime.js +10 -3
- package/out/cli/serve-hardening.js +60 -1
- package/out/cli/state-sync.js +16 -15
- package/out/engines/claude.js +26 -5
- package/out/engines/codex.js +24 -5
- package/out/engines/custom.js +46 -12
- package/out/extension.js +2 -0
- package/out/gateway/outboundSend.js +6 -6
- package/out/headless/index.js +24 -9
- package/out/live-probe/connector.mjs +8 -0
- package/out/live-probe/env-report.mjs +8 -0
- package/out/mcp.js +85 -67
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +14 -0
- package/out/shared/models.js +1 -1
- package/out/shared/providers.js +2 -2
- package/out/test/analytics.test.js +42 -17
- package/out/test/auth-broker.test.js +53 -0
- package/out/test/blueprint-registry.test.js +105 -0
- package/out/test/credential-context.test.js +23 -28
- package/out/test/custom-env-layering.test.js +151 -0
- package/out/test/data-plane-engine-wiring.test.js +250 -0
- package/out/test/data-plane.test.js +244 -0
- package/out/test/deploy-init.test.js +255 -4
- package/out/test/egress-broker.test.js +172 -0
- package/out/test/fleetDisplayUtils.test.js +4 -0
- package/out/test/idp-init.test.js +151 -0
- package/out/test/mcp-obo.test.js +52 -27
- package/out/test/mcp.test.js +43 -3
- package/out/test/model-routing.test.js +133 -0
- package/out/test/serve-gateway-turn.test.js +267 -0
- package/out/test/serve-hardening.test.js +80 -0
- package/out/test/serveClient.test.js +32 -0
- package/out/test/state-sync-peer.test.js +18 -5
- package/out/utils.js +27 -11
- package/package.json +1 -1
package/out/agentpack/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Copyright (c) AgentHippo.ai. All rights reserved.
|
|
4
4
|
*--------------------------------------------------------------------------------------------*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.checkGitHubUpdate = exports.installFromGitHub = exports.validateForPublish = exports.publishToGitHub = exports.getGitHubUser = exports.isGitHubAuthenticated = exports.getGitHubToken = exports.formatGitHubSource = exports.parseGitHubSource = exports.resetDefaultAgent = exports.duplicateAgent = exports.updateFromUserLibrary = exports.checkForUpdate = exports.readAgentSource = exports.promoteToUserLibrary = exports.hydrateFromUserLibrary = exports.isAgentInUserLibrary = exports.isWorkspaceAgentAvailable = exports.listUserLibraryAgents = exports.updateManifestName = exports.parseAgentManifest = exports.loadAgentManifest = exports.stringifyYaml = exports.parseYaml = exports.AgentPackManager = exports.getUserAgentCurrentPath = exports.getUserAgentVersionDir = exports.getUserAgentRoot = exports.getUserAgentsDir = exports.getAgentideHome = exports.parseAgentIdFromPath = exports.getSessionsDir = exports.getAgentMcpDir = exports.getAgentRulesDir = exports.getAgentSkillsDir = exports.getAgentContextPath = exports.getAgentAgentsMdPath = exports.getAgentManifestPath = exports.isDefaultAgent = exports.resolveRuntimePaths = exports.resolveAgentRuntimePaths = exports.getAgentRoot = exports.getAgentsDir = exports.getAgentideDir = exports.AGENT_CONSTANTS = void 0;
|
|
6
|
+
exports.checkGitHubUpdate = exports.installFromGitHub = exports.validateForPublish = exports.publishToGitHub = exports.getGitHubUser = exports.isGitHubAuthenticated = exports.getGitHubToken = exports.formatGitHubSource = exports.parseGitHubSource = exports.resetDefaultAgent = exports.duplicateAgent = exports.updateFromUserLibrary = exports.checkForUpdate = exports.readAgentSource = exports.promoteToUserLibrary = exports.hydrateFromUserLibrary = exports.isAgentInUserLibrary = exports.isWorkspaceAgentAvailable = exports.listUserLibraryAgents = exports.updateManifestName = exports.parseAgentManifest = exports.loadAgentManifest = exports.stringifyYaml = exports.parseYaml = exports.AgentPackManager = exports.getUserAgentCurrentPath = exports.getUserAgentVersionDir = exports.getUserAgentRoot = exports.getUserAgentsDir = exports.getAgentideHome = exports.parseAgentIdFromPath = exports.getSessionsDir = exports.getAgentMcpDir = exports.getAgentRulesDir = exports.getAgentSkillsDir = exports.getAgentContextPath = exports.getAgentAgentsMdPath = exports.getAgentManifestPath = exports.isDefaultAgent = exports.userScopedEngineHome = exports.resolveRuntimePaths = exports.resolveAgentRuntimePaths = exports.getAgentRoot = exports.getAgentsDir = exports.getAgentideDir = exports.AGENT_CONSTANTS = void 0;
|
|
7
7
|
// Path utilities
|
|
8
8
|
var paths_1 = require("./paths");
|
|
9
9
|
Object.defineProperty(exports, "AGENT_CONSTANTS", { enumerable: true, get: function () { return paths_1.AGENT_CONSTANTS; } });
|
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "getAgentsDir", { enumerable: true, get: function
|
|
|
12
12
|
Object.defineProperty(exports, "getAgentRoot", { enumerable: true, get: function () { return paths_1.getAgentRoot; } });
|
|
13
13
|
Object.defineProperty(exports, "resolveAgentRuntimePaths", { enumerable: true, get: function () { return paths_1.resolveAgentRuntimePaths; } });
|
|
14
14
|
Object.defineProperty(exports, "resolveRuntimePaths", { enumerable: true, get: function () { return paths_1.resolveRuntimePaths; } });
|
|
15
|
+
Object.defineProperty(exports, "userScopedEngineHome", { enumerable: true, get: function () { return paths_1.userScopedEngineHome; } });
|
|
15
16
|
Object.defineProperty(exports, "isDefaultAgent", { enumerable: true, get: function () { return paths_1.isDefaultAgent; } });
|
|
16
17
|
Object.defineProperty(exports, "getAgentManifestPath", { enumerable: true, get: function () { return paths_1.getAgentManifestPath; } });
|
|
17
18
|
Object.defineProperty(exports, "getAgentAgentsMdPath", { enumerable: true, get: function () { return paths_1.getAgentAgentsMdPath; } });
|
package/out/agentpack/paths.js
CHANGED
|
@@ -51,6 +51,7 @@ exports.getAgentsDir = getAgentsDir;
|
|
|
51
51
|
exports.getAgentRoot = getAgentRoot;
|
|
52
52
|
exports.resolveAgentRuntimePaths = resolveAgentRuntimePaths;
|
|
53
53
|
exports.resolveRuntimePaths = resolveRuntimePaths;
|
|
54
|
+
exports.userScopedEngineHome = userScopedEngineHome;
|
|
54
55
|
exports.isDefaultAgent = isDefaultAgent;
|
|
55
56
|
exports.getAgentManifestPath = getAgentManifestPath;
|
|
56
57
|
exports.getAgentAgentsMdPath = getAgentAgentsMdPath;
|
|
@@ -70,6 +71,7 @@ exports.generateAgentYaml = generateAgentYaml;
|
|
|
70
71
|
exports.generateAgentsMd = generateAgentsMd;
|
|
71
72
|
const path = __importStar(require("path"));
|
|
72
73
|
const os = __importStar(require("os"));
|
|
74
|
+
const crypto = __importStar(require("crypto"));
|
|
73
75
|
const branding_1 = require("../branding");
|
|
74
76
|
/**
|
|
75
77
|
* Constants for agent directory structure.
|
|
@@ -211,6 +213,27 @@ function resolveRuntimePaths(workspaceRoot, storageUri, agentId, agentVersion) {
|
|
|
211
213
|
const effectiveAgentId = agentId ?? exports.AGENT_CONSTANTS.DEFAULT_AGENT_ID;
|
|
212
214
|
return resolveAgentRuntimePaths(workspaceRoot, storageUri, effectiveAgentId, agentVersion);
|
|
213
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Key an engine's SDK state dir (CODEX_HOME / claudeConfigDir) by the verified request
|
|
218
|
+
* user in multi-tenant serve/headless deployments (secure-obo-design §8 Phase 0).
|
|
219
|
+
*
|
|
220
|
+
* The base dir is per-agent, not per-user — under concurrent turns of different users the
|
|
221
|
+
* codex engine rewrites the shared config.toml per turn, so one user's engine can launch
|
|
222
|
+
* pointed at another user's per-turn credential state. Scoping the home per verified user
|
|
223
|
+
* makes any remaining config race same-user (same credential ⇒ harmless).
|
|
224
|
+
*
|
|
225
|
+
* Returns the base dir unchanged when no verified user is in scope: the IDE and
|
|
226
|
+
* single-tenant CLI paths have no `runWithCredential` frame and must be unaffected.
|
|
227
|
+
* The user id is hashed — it is an email/subject, never safe as a raw path segment.
|
|
228
|
+
*/
|
|
229
|
+
function userScopedEngineHome(baseHome, verifiedUserId) {
|
|
230
|
+
const uid = verifiedUserId?.trim();
|
|
231
|
+
if (!uid) {
|
|
232
|
+
return baseHome;
|
|
233
|
+
}
|
|
234
|
+
const hash = crypto.createHash('sha256').update(uid).digest('hex').slice(0, 16);
|
|
235
|
+
return path.join(baseHome, 'users', hash);
|
|
236
|
+
}
|
|
214
237
|
/**
|
|
215
238
|
* Check if an agent is the default workspace agent.
|
|
216
239
|
*/
|
|
@@ -74,6 +74,9 @@ function formatFleetModelLabel(model, engine) {
|
|
|
74
74
|
}
|
|
75
75
|
return '—';
|
|
76
76
|
}
|
|
77
|
+
if (raw === 'ah-auto' || raw.endsWith('::ah-auto')) {
|
|
78
|
+
return 'Auto';
|
|
79
|
+
}
|
|
77
80
|
if (raw.startsWith('agentide-cursor::')) {
|
|
78
81
|
const tail = raw.slice('agentide-cursor::'.length).trim() || 'auto';
|
|
79
82
|
if (tail === 'auto') {
|
|
@@ -17,7 +17,7 @@ const panelApi = window.panelApi;
|
|
|
17
17
|
|
|
18
18
|
// Seed/version marker for deciding whether a user's locally seeded panel.js
|
|
19
19
|
// is compatible with the extension runtime (e.g. message forwarding fixes).
|
|
20
|
-
const AGENTIDE_PANEL_JS_VERSION = '2026.
|
|
20
|
+
const AGENTIDE_PANEL_JS_VERSION = '2026.07.21';
|
|
21
21
|
try { window.__AGENTIDE_PANEL_JS_VERSION__ = AGENTIDE_PANEL_JS_VERSION; } catch { /* no-op */ }
|
|
22
22
|
|
|
23
23
|
// -----------------------------------------------------------------------------
|
|
@@ -44,7 +44,7 @@ function createDefaultPanelState() {
|
|
|
44
44
|
return { version: 1, charts: {}, summaryHidden: false };
|
|
45
45
|
}
|
|
46
46
|
|
|
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 }} */
|
|
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, needsGalleryRender: boolean, pendingForceFreshHtml: boolean, refreshingViewId: string | null }} */
|
|
48
48
|
const state = {
|
|
49
49
|
artifacts: [],
|
|
50
50
|
galleryIgnorePatterns: [],
|
|
@@ -64,8 +64,64 @@ const state = {
|
|
|
64
64
|
fleetAutoRefreshIntervalSeconds: 30,
|
|
65
65
|
fleetRefreshVersion: 0,
|
|
66
66
|
surfaceMode: 'spotlight',
|
|
67
|
+
// While a refresh is in flight, multiple artifactsUpdated bursts can arrive as scripts/
|
|
68
|
+
// watchers write files incrementally. Rendering on each one tears down and rebuilds the
|
|
69
|
+
// whole gallery (see renderGallery), which is visibly flickery. Defer to one render when
|
|
70
|
+
// the refresh actually ends (refreshFinished/refreshFailed/refreshNoData).
|
|
71
|
+
needsGalleryRender: false,
|
|
72
|
+
pendingForceFreshHtml: false,
|
|
73
|
+
// View id the in-flight refresh belongs to. Termination messages carry the viewId captured
|
|
74
|
+
// at refresh start, so after a mid-refresh view switch they no longer match activeViewId —
|
|
75
|
+
// match against this instead so isRefreshing cannot get stuck true.
|
|
76
|
+
refreshingViewId: null,
|
|
67
77
|
};
|
|
68
78
|
|
|
79
|
+
/** Render the gallery now if a render was deferred mid-refresh. */
|
|
80
|
+
function flushPendingGalleryRender() {
|
|
81
|
+
if (!state.needsGalleryRender) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
state.needsGalleryRender = false;
|
|
85
|
+
const forceFreshHtml = state.pendingForceFreshHtml;
|
|
86
|
+
state.pendingForceFreshHtml = false;
|
|
87
|
+
renderGallery(state.artifacts, { forceFreshHtml });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Single entry point for gallery renders triggered by host messages. While a refresh is in
|
|
92
|
+
* flight, defer to one render at refresh termination instead of tearing down and rebuilding
|
|
93
|
+
* the gallery on every artifactsUpdated/manifestUpdated/disabledChartsUpdated burst.
|
|
94
|
+
* @param {{ forceFreshHtml?: boolean }} [options]
|
|
95
|
+
*/
|
|
96
|
+
function requestGalleryRender(options) {
|
|
97
|
+
if (state.isRefreshing) {
|
|
98
|
+
state.needsGalleryRender = true;
|
|
99
|
+
if (options?.forceFreshHtml) {
|
|
100
|
+
state.pendingForceFreshHtml = true;
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
renderGallery(state.artifacts, { forceFreshHtml: Boolean(options?.forceFreshHtml) });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The active view changed while a refresh may be in flight: that refresh belongs to the view
|
|
109
|
+
* being left, so stop tracking it. The abandoned refresh's termination message is dropped by
|
|
110
|
+
* the viewId gates, and without this reset a deferred render would block the new view's first
|
|
111
|
+
* paint until that refresh ends (or forever, if no matching termination arrives).
|
|
112
|
+
*/
|
|
113
|
+
function resetRefreshTrackingForViewChange() {
|
|
114
|
+
if (!state.isRefreshing && !state.needsGalleryRender && !state.pendingForceFreshHtml) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
state.isRefreshing = false;
|
|
118
|
+
state.refreshingViewId = null;
|
|
119
|
+
state.needsGalleryRender = false;
|
|
120
|
+
state.pendingForceFreshHtml = false;
|
|
121
|
+
setRefreshButtonState(false);
|
|
122
|
+
setPerChartRefreshing(false);
|
|
123
|
+
}
|
|
124
|
+
|
|
69
125
|
function applySurfaceMode(mode) {
|
|
70
126
|
const team = mode === 'team';
|
|
71
127
|
state.surfaceMode = team ? 'team' : 'spotlight';
|
|
@@ -362,6 +418,7 @@ function switchToView(nextViewId) {
|
|
|
362
418
|
return;
|
|
363
419
|
}
|
|
364
420
|
state.activeViewId = nextViewId;
|
|
421
|
+
resetRefreshTrackingForViewChange();
|
|
365
422
|
state.panelStateLoaded = false;
|
|
366
423
|
state.panelState = createDefaultPanelState();
|
|
367
424
|
const viewSelect = /** @type {HTMLSelectElement | null} */ (document.getElementById('view-select'));
|
|
@@ -849,6 +906,9 @@ function toggleChartHiddenInPanelState(filename) {
|
|
|
849
906
|
// Summary Cards
|
|
850
907
|
// -----------------------------------------------------------------------------
|
|
851
908
|
|
|
909
|
+
/** Last rendered summary-cards payload, to skip no-op rebuilds of the summary strip. */
|
|
910
|
+
let lastSummaryCardsSignature = '';
|
|
911
|
+
|
|
852
912
|
/**
|
|
853
913
|
* Render summary cards in the header
|
|
854
914
|
* @param {Array<{label: string, value: string, icon: string}>} cards
|
|
@@ -859,10 +919,19 @@ function renderSummaryCards(cards) {
|
|
|
859
919
|
return;
|
|
860
920
|
}
|
|
861
921
|
if (!cards || cards.length === 0) {
|
|
922
|
+
lastSummaryCardsSignature = '';
|
|
862
923
|
renderSummaryPlaceholders();
|
|
863
924
|
return;
|
|
864
925
|
}
|
|
865
926
|
|
|
927
|
+
// The host posts summaryCards more than once per refresh; skip the rebuild (and its
|
|
928
|
+
// visible flash) when the payload is identical to what is already rendered.
|
|
929
|
+
const signature = JSON.stringify(cards);
|
|
930
|
+
if (signature === lastSummaryCardsSignature && !container.classList.contains('loading')) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
lastSummaryCardsSignature = signature;
|
|
934
|
+
|
|
866
935
|
container.style.display = 'grid';
|
|
867
936
|
container.classList.remove('loading');
|
|
868
937
|
container.innerHTML = cards.map(card => `
|
|
@@ -987,7 +1056,6 @@ function renderGallery(artifacts, options) {
|
|
|
987
1056
|
}
|
|
988
1057
|
return new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime();
|
|
989
1058
|
});
|
|
990
|
-
const interactiveChartKeys = chartArtifacts.map((artifact) => normalizeFilename(String(artifact.filename || '')));
|
|
991
1059
|
const nonChartArtifacts = visibleArtifacts
|
|
992
1060
|
.filter((artifact) => artifact.type !== 'chart')
|
|
993
1061
|
.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime());
|
|
@@ -1085,7 +1153,7 @@ function renderGallery(artifacts, options) {
|
|
|
1085
1153
|
|
|
1086
1154
|
showGalleryState('content');
|
|
1087
1155
|
|
|
1088
|
-
const
|
|
1156
|
+
const buildCardHtml = (artifact) => {
|
|
1089
1157
|
const timeAgo = formatTimeAgo(new Date(artifact.modifiedAt));
|
|
1090
1158
|
const sizeFormatted = formatSize(artifact.size);
|
|
1091
1159
|
const isChart = artifact.type === 'chart';
|
|
@@ -1175,101 +1243,214 @@ function renderGallery(artifacts, options) {
|
|
|
1175
1243
|
${resizeHandle}
|
|
1176
1244
|
</div>
|
|
1177
1245
|
`;
|
|
1178
|
-
}
|
|
1246
|
+
};
|
|
1247
|
+
// Keyed card reuse: diff incoming artifacts against existing cards by localPath and only
|
|
1248
|
+
// rebuild what changed. Rebuilding a card destroys its iframe/img and re-fetches content —
|
|
1249
|
+
// that teardown is the visible blink — so unchanged cards must not be touched at all.
|
|
1250
|
+
/** @type {Map<string, HTMLElement>} */
|
|
1251
|
+
const existingCards = new Map();
|
|
1252
|
+
Array.from(container.children).forEach((childEl) => {
|
|
1253
|
+
const child = /** @type {HTMLElement} */ (childEl);
|
|
1254
|
+
const childPath = child.dataset.path || '';
|
|
1255
|
+
const reusableAsCard =
|
|
1256
|
+
child.classList.contains('artifact-card')
|
|
1257
|
+
&& !child.classList.contains('fleet-dashboard-full') // fleet layout card: different structure
|
|
1258
|
+
&& !child.classList.contains('deleting')
|
|
1259
|
+
&& childPath
|
|
1260
|
+
&& !existingCards.has(childPath);
|
|
1261
|
+
if (!reusableAsCard) {
|
|
1262
|
+
child.remove();
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
existingCards.set(childPath, child);
|
|
1266
|
+
});
|
|
1179
1267
|
|
|
1180
|
-
container.innerHTML = html;
|
|
1181
1268
|
/** @type {Array<object>} */
|
|
1182
1269
|
const htmlArtifactsNeedingFetch = [];
|
|
1183
1270
|
/** @type {Array<object>} */
|
|
1184
1271
|
const markdownArtifactsNeedingFetch = [];
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
const
|
|
1272
|
+
/** @type {HTMLElement[]} */
|
|
1273
|
+
const orderedCards = [];
|
|
1274
|
+
for (const artifact of orderedArtifacts) {
|
|
1275
|
+
const path = String(artifact.localPath || '');
|
|
1276
|
+
const modifiedAt = String(artifact.modifiedAt || '');
|
|
1277
|
+
const isVisual = artifact.type === 'chart' || artifact.type === 'image';
|
|
1278
|
+
const chartState = isVisual ? artifact._panel : undefined;
|
|
1279
|
+
const signature = computeCardRenderSignature(artifact);
|
|
1280
|
+
const forceHtmlReload = forceFreshHtml && artifact.format === 'html';
|
|
1281
|
+
const existing = existingCards.get(path);
|
|
1282
|
+
existingCards.delete(path);
|
|
1283
|
+
|
|
1284
|
+
if (existing && existing.dataset.cardSignature === signature && !forceHtmlReload) {
|
|
1285
|
+
// Unchanged: leave the DOM (and any live iframe) alone. Expanded/height are kept
|
|
1286
|
+
// out of the signature and applied in place so toggling them never reloads content.
|
|
1287
|
+
if (isVisual) {
|
|
1288
|
+
existing.classList.toggle('expanded', chartState?.expanded === true);
|
|
1289
|
+
const contentEl = /** @type {HTMLElement | null} */ (existing.querySelector('.artifact-content'));
|
|
1290
|
+
if (contentEl && chartState?.height) {
|
|
1291
|
+
contentEl.style.height = `${chartState.height}px`;
|
|
1292
|
+
contentEl.style.minHeight = `${chartState.height}px`;
|
|
1293
|
+
contentEl.style.overflow = 'auto';
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
const timeEl = existing.querySelector('.artifact-time');
|
|
1297
|
+
if (timeEl) {
|
|
1298
|
+
timeEl.textContent = formatTimeAgo(new Date(artifact.modifiedAt));
|
|
1299
|
+
}
|
|
1300
|
+
orderedCards.push(existing);
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
const template = document.createElement('template');
|
|
1305
|
+
template.innerHTML = buildCardHtml(artifact).trim();
|
|
1306
|
+
const card = /** @type {HTMLElement | null} */ (template.content.firstElementChild);
|
|
1307
|
+
if (!card) {
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
card.dataset.cardSignature = signature;
|
|
1189
1311
|
const htmlHost = /** @type {HTMLElement | null} */ (card.querySelector('.html-preview'));
|
|
1190
|
-
if (htmlHost
|
|
1191
|
-
const reusable =
|
|
1312
|
+
if (htmlHost) {
|
|
1313
|
+
const reusable = forceHtmlReload ? undefined : reusableHtmlPreviews.get(`${path}::${modifiedAt}`);
|
|
1192
1314
|
if (reusable) {
|
|
1193
1315
|
htmlHost.classList.add('ready');
|
|
1194
1316
|
htmlHost.innerHTML = '';
|
|
1195
1317
|
htmlHost.appendChild(reusable);
|
|
1196
|
-
} else {
|
|
1197
|
-
|
|
1198
|
-
if (htmlArtifact) {
|
|
1199
|
-
htmlArtifactsNeedingFetch.push(htmlArtifact);
|
|
1200
|
-
}
|
|
1318
|
+
} else if (artifact.format === 'html') {
|
|
1319
|
+
htmlArtifactsNeedingFetch.push(artifact);
|
|
1201
1320
|
}
|
|
1202
1321
|
}
|
|
1322
|
+
if (card.querySelector('.markdown-preview')) {
|
|
1323
|
+
markdownArtifactsNeedingFetch.push(artifact);
|
|
1324
|
+
}
|
|
1325
|
+
attachArtifactCardHandlers(card);
|
|
1326
|
+
if (existing) {
|
|
1327
|
+
existing.replaceWith(card);
|
|
1328
|
+
}
|
|
1329
|
+
orderedCards.push(card);
|
|
1330
|
+
}
|
|
1203
1331
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1332
|
+
// Cards whose artifacts disappeared.
|
|
1333
|
+
existingCards.forEach((card) => card.remove());
|
|
1334
|
+
|
|
1335
|
+
// Minimal-move reorder: re-inserting a node reloads its iframe, so only move cards that
|
|
1336
|
+
// are actually out of position.
|
|
1337
|
+
let cursor = container.firstElementChild;
|
|
1338
|
+
for (const card of orderedCards) {
|
|
1339
|
+
if (card === cursor) {
|
|
1340
|
+
cursor = cursor.nextElementSibling;
|
|
1341
|
+
continue;
|
|
1210
1342
|
}
|
|
1211
|
-
|
|
1343
|
+
container.insertBefore(card, cursor);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1212
1346
|
requestHtmlPreviewContents(htmlArtifactsNeedingFetch);
|
|
1213
1347
|
requestMarkdownPreviewContents(markdownArtifactsNeedingFetch);
|
|
1214
1348
|
|
|
1215
|
-
//
|
|
1216
|
-
|
|
1217
|
-
|
|
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';
|
|
1349
|
+
// Markdown previews are loaded via host message passing (artifactContent)
|
|
1350
|
+
// to avoid fetch restrictions for local report files.
|
|
1351
|
+
}
|
|
1222
1352
|
|
|
1223
|
-
|
|
1353
|
+
/**
|
|
1354
|
+
* Per-card render signature: every input that affects a card's DOM except expanded/height,
|
|
1355
|
+
* which renderGallery applies in place so toggling them never rebuilds the card.
|
|
1356
|
+
* @param {object} artifact
|
|
1357
|
+
* @returns {string}
|
|
1358
|
+
*/
|
|
1359
|
+
function computeCardRenderSignature(artifact) {
|
|
1360
|
+
const isVisual = artifact.type === 'chart' || artifact.type === 'image';
|
|
1361
|
+
const chartState = isVisual ? artifact._panel : undefined;
|
|
1362
|
+
const manifestEntry = state.manifest?.artifacts?.find(e => e.filename === artifact.filename);
|
|
1363
|
+
return [
|
|
1364
|
+
String(artifact.localPath || ''),
|
|
1365
|
+
String(artifact.filename || ''),
|
|
1366
|
+
String(artifact.modifiedAt || ''),
|
|
1367
|
+
String(artifact.size || ''),
|
|
1368
|
+
String(artifact.type || ''),
|
|
1369
|
+
String(artifact.format || ''),
|
|
1370
|
+
String(artifact.generator || ''),
|
|
1371
|
+
String(manifestEntry?.title || ''),
|
|
1372
|
+
String(manifestEntry?.source || ''),
|
|
1373
|
+
chartState?.hidden === true ? '1' : '0',
|
|
1374
|
+
Number.isFinite(chartState?.order) ? String(chartState.order) : '',
|
|
1375
|
+
].join('|');
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/**
|
|
1379
|
+
* Chart/image ordering keys derived from current DOM order — computed at click time so
|
|
1380
|
+
* handlers on long-lived cards never act on a stale snapshot.
|
|
1381
|
+
* @returns {string[]}
|
|
1382
|
+
*/
|
|
1383
|
+
function getInteractiveChartKeysFromDom() {
|
|
1384
|
+
const container = document.getElementById('gallery-content');
|
|
1385
|
+
if (!container) {
|
|
1386
|
+
return [];
|
|
1387
|
+
}
|
|
1388
|
+
return Array.from(container.querySelectorAll('.artifact-card'))
|
|
1389
|
+
.filter((cardEl) => {
|
|
1390
|
+
const card = /** @type {HTMLElement} */ (cardEl);
|
|
1391
|
+
return card.dataset.type === 'chart' || card.dataset.type === 'image';
|
|
1392
|
+
})
|
|
1393
|
+
.map((cardEl) => normalizeFilename(String(/** @type {HTMLElement} */ (cardEl).dataset.filename || '')))
|
|
1394
|
+
.filter(Boolean);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/**
|
|
1398
|
+
* Wire interactive behaviors for one gallery card (open/delete, layout buttons, expand on
|
|
1399
|
+
* click, resize handle). Called once per created card; reused cards keep their listeners.
|
|
1400
|
+
* @param {HTMLElement} card
|
|
1401
|
+
*/
|
|
1402
|
+
function attachArtifactCardHandlers(card) {
|
|
1403
|
+
const path = card.dataset.path;
|
|
1404
|
+
const filename = card.dataset.filename;
|
|
1405
|
+
const isChart = card.dataset.type === 'chart';
|
|
1406
|
+
const isVisual = card.dataset.type === 'chart' || card.dataset.type === 'image';
|
|
1407
|
+
|
|
1408
|
+
card.querySelector('.open-btn')?.addEventListener('click', (e) => {
|
|
1409
|
+
e.stopPropagation();
|
|
1410
|
+
panelApi.postMessage({ type: 'openArtifact', path });
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
card.querySelector('.delete-btn')?.addEventListener('click', (e) => {
|
|
1414
|
+
e.stopPropagation();
|
|
1415
|
+
panelApi.postMessage({ type: 'deleteArtifact', path });
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
if (isChart && filename) {
|
|
1419
|
+
card.querySelector('.move-up-btn')?.addEventListener('click', (e) => {
|
|
1224
1420
|
e.stopPropagation();
|
|
1225
|
-
|
|
1421
|
+
moveChartInPanelState(filename, 'up', getInteractiveChartKeysFromDom());
|
|
1422
|
+
renderGallery(state.artifacts);
|
|
1226
1423
|
});
|
|
1227
1424
|
|
|
1228
|
-
card.querySelector('.
|
|
1425
|
+
card.querySelector('.move-down-btn')?.addEventListener('click', (e) => {
|
|
1229
1426
|
e.stopPropagation();
|
|
1230
|
-
|
|
1427
|
+
moveChartInPanelState(filename, 'down', getInteractiveChartKeysFromDom());
|
|
1428
|
+
renderGallery(state.artifacts);
|
|
1231
1429
|
});
|
|
1232
1430
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
-
}
|
|
1431
|
+
card.querySelector('.toggle-enabled-btn')?.addEventListener('click', (e) => {
|
|
1432
|
+
e.stopPropagation();
|
|
1433
|
+
toggleChartHiddenInPanelState(filename);
|
|
1434
|
+
renderGallery(state.artifacts);
|
|
1264
1435
|
});
|
|
1436
|
+
}
|
|
1265
1437
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1438
|
+
// Click on card to expand/collapse
|
|
1439
|
+
card.addEventListener('click', (event) => {
|
|
1440
|
+
const target = /** @type {HTMLElement | null} */ (event.target instanceof HTMLElement ? event.target : null);
|
|
1441
|
+
if (target?.closest('button,.resize-handle')) {
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
const nextExpanded = !card.classList.contains('expanded');
|
|
1445
|
+
card.classList.toggle('expanded', nextExpanded);
|
|
1446
|
+
if (isVisual && filename) {
|
|
1447
|
+
setChartExpanded(filename, nextExpanded);
|
|
1268
1448
|
}
|
|
1269
1449
|
});
|
|
1270
1450
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1451
|
+
if (filename) {
|
|
1452
|
+
attachResizeHandle(card, filename);
|
|
1453
|
+
}
|
|
1273
1454
|
}
|
|
1274
1455
|
|
|
1275
1456
|
/**
|
|
@@ -2044,12 +2225,18 @@ panelApi.onMessage(msg => {
|
|
|
2044
2225
|
state.artifacts = msg.artifacts;
|
|
2045
2226
|
state.galleryIgnorePatterns = nextGalleryIgnore;
|
|
2046
2227
|
// View selection is owned by viewsLoaded; do not let artifact batches revert the dropdown.
|
|
2228
|
+
const prevActiveViewId = state.activeViewId;
|
|
2047
2229
|
if (
|
|
2048
2230
|
nextActiveViewId === state.activeViewId
|
|
2049
2231
|
|| !state.views.some((view) => view.id === state.activeViewId)
|
|
2050
2232
|
) {
|
|
2051
2233
|
state.activeViewId = nextActiveViewId;
|
|
2052
2234
|
}
|
|
2235
|
+
if (state.activeViewId !== prevActiveViewId) {
|
|
2236
|
+
// Host-initiated view change (e.g. openWithView): any in-flight refresh belongs
|
|
2237
|
+
// to the previous view — render now instead of deferring behind it.
|
|
2238
|
+
resetRefreshTrackingForViewChange();
|
|
2239
|
+
}
|
|
2053
2240
|
state.fleetRefreshVersion = nextFleetRefreshVersion;
|
|
2054
2241
|
applyFleetAutoRefresh();
|
|
2055
2242
|
if (exportOnlyCatalogChange) {
|
|
@@ -2059,10 +2246,11 @@ panelApi.onMessage(msg => {
|
|
|
2059
2246
|
// Fleet JSON can change without agent-fleet.html mtime changing if the host
|
|
2060
2247
|
// did not re-run the widget script; bumping fleetRefreshVersion means data is
|
|
2061
2248
|
// fresher — force a preview reload so the iframe is not reused with stale HTML.
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2249
|
+
if (state.isRefreshing) {
|
|
2250
|
+
// Mid-refresh burst: keep spinners on; requestGalleryRender defers the rebuild.
|
|
2251
|
+
setPerChartRefreshing(true);
|
|
2252
|
+
}
|
|
2253
|
+
requestGalleryRender({ forceFreshHtml: hasFleetVersionUpdate });
|
|
2066
2254
|
break;
|
|
2067
2255
|
}
|
|
2068
2256
|
|
|
@@ -2093,7 +2281,7 @@ panelApi.onMessage(msg => {
|
|
|
2093
2281
|
state.manifest = msg.manifest;
|
|
2094
2282
|
// Re-render gallery with new manifest data
|
|
2095
2283
|
if (state.artifacts.length > 0) {
|
|
2096
|
-
|
|
2284
|
+
requestGalleryRender();
|
|
2097
2285
|
}
|
|
2098
2286
|
break;
|
|
2099
2287
|
|
|
@@ -2137,6 +2325,10 @@ panelApi.onMessage(msg => {
|
|
|
2137
2325
|
case 'generatingStarted':
|
|
2138
2326
|
setRefreshButtonState(true);
|
|
2139
2327
|
state.isRefreshing = false;
|
|
2328
|
+
state.refreshingViewId = null;
|
|
2329
|
+
// Drop any deferred render — a later flush would stomp the generating state.
|
|
2330
|
+
state.needsGalleryRender = false;
|
|
2331
|
+
state.pendingForceFreshHtml = false;
|
|
2140
2332
|
setPerChartRefreshing(false);
|
|
2141
2333
|
showGeneratingState();
|
|
2142
2334
|
break;
|
|
@@ -2144,6 +2336,9 @@ panelApi.onMessage(msg => {
|
|
|
2144
2336
|
case 'generatingCancelled':
|
|
2145
2337
|
setRefreshButtonState(false);
|
|
2146
2338
|
state.isRefreshing = false;
|
|
2339
|
+
state.refreshingViewId = null;
|
|
2340
|
+
state.needsGalleryRender = false;
|
|
2341
|
+
state.pendingForceFreshHtml = false;
|
|
2147
2342
|
setPerChartRefreshing(false);
|
|
2148
2343
|
// Go back to empty or content state
|
|
2149
2344
|
if (state.artifacts.length > 0) {
|
|
@@ -2156,6 +2351,9 @@ panelApi.onMessage(msg => {
|
|
|
2156
2351
|
case 'generatingFailed':
|
|
2157
2352
|
setRefreshButtonState(false);
|
|
2158
2353
|
state.isRefreshing = false;
|
|
2354
|
+
state.refreshingViewId = null;
|
|
2355
|
+
state.needsGalleryRender = false;
|
|
2356
|
+
state.pendingForceFreshHtml = false;
|
|
2159
2357
|
setPerChartRefreshing(false);
|
|
2160
2358
|
showFailedState(msg.error);
|
|
2161
2359
|
break;
|
|
@@ -2164,15 +2362,18 @@ panelApi.onMessage(msg => {
|
|
|
2164
2362
|
if (!msg.viewId || msg.viewId === state.activeViewId) {
|
|
2165
2363
|
setRefreshButtonState(true);
|
|
2166
2364
|
state.isRefreshing = true;
|
|
2365
|
+
state.refreshingViewId = typeof msg.viewId === 'string' && msg.viewId ? msg.viewId : null;
|
|
2167
2366
|
setPerChartRefreshing(true);
|
|
2168
2367
|
}
|
|
2169
2368
|
break;
|
|
2170
2369
|
|
|
2171
2370
|
case 'refreshFinished':
|
|
2172
|
-
if (!msg.viewId || msg.viewId === state.activeViewId) {
|
|
2371
|
+
if (!msg.viewId || msg.viewId === state.activeViewId || msg.viewId === state.refreshingViewId) {
|
|
2173
2372
|
setRefreshButtonState(false);
|
|
2174
2373
|
state.isRefreshing = false;
|
|
2374
|
+
state.refreshingViewId = null;
|
|
2175
2375
|
setPerChartRefreshing(false);
|
|
2376
|
+
flushPendingGalleryRender();
|
|
2176
2377
|
if (activeViewPreservesFleetIframe()) {
|
|
2177
2378
|
document.querySelectorAll('.html-preview iframe').forEach((frameEl) => {
|
|
2178
2379
|
const iframe = /** @type {HTMLIFrameElement} */ (frameEl);
|
|
@@ -2186,29 +2387,38 @@ panelApi.onMessage(msg => {
|
|
|
2186
2387
|
}
|
|
2187
2388
|
break;
|
|
2188
2389
|
|
|
2189
|
-
case 'viewsLoaded':
|
|
2390
|
+
case 'viewsLoaded': {
|
|
2190
2391
|
if (msg.surfaceMode === 'team' || msg.surfaceMode === 'spotlight') {
|
|
2191
2392
|
applySurfaceMode(msg.surfaceMode);
|
|
2192
2393
|
}
|
|
2394
|
+
const prevViewsLoadedViewId = state.activeViewId;
|
|
2193
2395
|
state.views = Array.isArray(msg.views) ? msg.views : [];
|
|
2194
2396
|
state.activeViewId = typeof msg.activeViewId === 'string' ? msg.activeViewId : state.activeViewId;
|
|
2397
|
+
if (state.activeViewId !== prevViewsLoadedViewId) {
|
|
2398
|
+
resetRefreshTrackingForViewChange();
|
|
2399
|
+
}
|
|
2195
2400
|
applyViewOptions();
|
|
2196
2401
|
applyFleetAutoRefresh();
|
|
2197
2402
|
break;
|
|
2403
|
+
}
|
|
2198
2404
|
|
|
2199
2405
|
case 'refreshNoData':
|
|
2200
|
-
if (!msg.viewId || msg.viewId === state.activeViewId) {
|
|
2406
|
+
if (!msg.viewId || msg.viewId === state.activeViewId || msg.viewId === state.refreshingViewId) {
|
|
2201
2407
|
setRefreshButtonState(false);
|
|
2202
2408
|
state.isRefreshing = false;
|
|
2409
|
+
state.refreshingViewId = null;
|
|
2203
2410
|
setPerChartRefreshing(false);
|
|
2411
|
+
flushPendingGalleryRender();
|
|
2204
2412
|
}
|
|
2205
2413
|
break;
|
|
2206
2414
|
|
|
2207
2415
|
case 'refreshFailed':
|
|
2208
|
-
if (!msg.viewId || msg.viewId === state.activeViewId) {
|
|
2416
|
+
if (!msg.viewId || msg.viewId === state.activeViewId || msg.viewId === state.refreshingViewId) {
|
|
2209
2417
|
setRefreshButtonState(false);
|
|
2210
2418
|
state.isRefreshing = false;
|
|
2419
|
+
state.refreshingViewId = null;
|
|
2211
2420
|
setPerChartRefreshing(false);
|
|
2421
|
+
flushPendingGalleryRender();
|
|
2212
2422
|
if (state.artifacts.length === 0) {
|
|
2213
2423
|
showFailedState(msg.error);
|
|
2214
2424
|
}
|
|
@@ -2256,7 +2466,7 @@ panelApi.onMessage(msg => {
|
|
|
2256
2466
|
.map((name) => String(name || '').trim().toLowerCase())
|
|
2257
2467
|
.filter(Boolean),
|
|
2258
2468
|
);
|
|
2259
|
-
|
|
2469
|
+
requestGalleryRender();
|
|
2260
2470
|
break;
|
|
2261
2471
|
|
|
2262
2472
|
case 'dataStale':
|