@veolab/discoverylab 1.4.4 → 1.6.5

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 (33) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +70 -211
  4. package/assets/applab-bundle-icon.png +0 -0
  5. package/assets/icons/icons8-claude-150.png +0 -0
  6. package/assets/icons/icons8-claude-500.png +0 -0
  7. package/dist/{chunk-CUBQRT5L.js → chunk-JAA53ES7.js} +111 -2
  8. package/dist/{chunk-HB3YPWF3.js → chunk-Q7Q3A2ZI.js} +301 -10
  9. package/dist/{chunk-XKX6NBHF.js → chunk-TWRWARU4.js} +52 -2
  10. package/dist/{chunk-2UUMLAVR.js → chunk-V6RREMYD.js} +332 -38
  11. package/dist/cli.js +164 -28
  12. package/dist/export/infographic-template.html +254 -0
  13. package/dist/import-W2JEW254.js +180 -0
  14. package/dist/index.d.ts +30 -6
  15. package/dist/index.html +473 -11
  16. package/dist/index.js +5 -5
  17. package/dist/infographic-GQAHEOAA.js +183 -0
  18. package/dist/mcpb/node_modules/@anthropic-ai/sdk/src/lib/.keep +4 -0
  19. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/better_sqlite3.node.d +1 -0
  20. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/better_sqlite3/src/better_sqlite3.o.d +133 -0
  21. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/deps/locate_sqlite3.stamp.d +1 -0
  22. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/sqlite3/gen/sqlite3/sqlite3.o.d +4 -0
  23. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/test_extension/deps/test_extension.o.d +7 -0
  24. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/sqlite3.a.d +1 -0
  25. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/test_extension.node.d +1 -0
  26. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/ba23eeee118cd63e16015df367567cb043fed872.intermediate.d +1 -0
  27. package/dist/{server-QFNKZCOJ.js → server-C2NZM2RV.js} +1 -1
  28. package/dist/{server-OVOACIOJ.js → server-WN6DCCUA.js} +1 -1
  29. package/dist/{setup-6JJYKKBS.js → setup-SMN7FJNZ.js} +5 -2
  30. package/dist/{tools-Q7OZO732.js → tools-VXU3JEQP.js} +6 -4
  31. package/doc/esvp-protocol.md +116 -0
  32. package/package.json +9 -3
  33. package/skills/knowledge-brain/SKILL.md +44 -43
package/dist/index.d.ts CHANGED
@@ -13,16 +13,35 @@ interface MCPTool {
13
13
  name: string;
14
14
  description: string;
15
15
  inputSchema: z.ZodType<any>;
16
+ _meta?: Record<string, any>;
16
17
  handler: (params: any) => Promise<MCPToolResult>;
17
18
  }
19
+ interface MCPToolContent {
20
+ type: 'text' | 'image';
21
+ text?: string;
22
+ data?: string;
23
+ mimeType?: string;
24
+ }
18
25
  interface MCPToolResult {
19
- content: Array<{
20
- type: 'text' | 'image';
21
- text?: string;
22
- data?: string;
23
- mimeType?: string;
24
- }>;
26
+ content: MCPToolContent[];
25
27
  isError?: boolean;
28
+ structuredContent?: Record<string, any>;
29
+ _meta?: Record<string, any>;
30
+ }
31
+ interface MCPResourceContents {
32
+ uri: string;
33
+ mimeType: string;
34
+ text?: string;
35
+ blob?: string;
36
+ _meta?: Record<string, any>;
37
+ }
38
+ interface MCPResource {
39
+ uri: string;
40
+ name: string;
41
+ title?: string;
42
+ description?: string;
43
+ mimeType: string;
44
+ contents?: MCPResourceContents[];
26
45
  }
27
46
  interface MCPRequest {
28
47
  jsonrpc: '2.0';
@@ -42,12 +61,17 @@ interface MCPResponse {
42
61
  }
43
62
  declare class MCPServer {
44
63
  private tools;
64
+ private resources;
45
65
  private serverInfo;
46
66
  registerTool(tool: MCPTool): void;
47
67
  registerTools(tools: MCPTool[]): void;
68
+ registerResource(resource: MCPResource): void;
69
+ upsertResourceContents(uri: string, resource: Omit<MCPResource, 'uri'>): void;
48
70
  handleRequest(request: MCPRequest): Promise<MCPResponse>;
49
71
  private handleInitialize;
50
72
  private handleToolsList;
73
+ private handleResourcesList;
74
+ private handleResourcesRead;
51
75
  private handleToolCall;
52
76
  private zodToJsonSchema;
53
77
  runStdio(): Promise<void>;
package/dist/index.html CHANGED
@@ -806,6 +806,10 @@
806
806
  opacity: 1;
807
807
  }
808
808
 
809
+ .project-card.select-mode .project-claude-action {
810
+ display: none;
811
+ }
812
+
809
813
  .project-card.select-mode.checked .checkbox-circle {
810
814
  opacity: 0;
811
815
  }
@@ -920,6 +924,44 @@
920
924
  object-fit: cover;
921
925
  }
922
926
 
927
+ .project-claude-action {
928
+ position: absolute;
929
+ top: 8px;
930
+ right: 8px;
931
+ display: inline-flex;
932
+ align-items: center;
933
+ justify-content: center;
934
+ width: 32px;
935
+ height: 32px;
936
+ border-radius: 999px;
937
+ border: 1px solid rgba(255, 255, 255, 0.18);
938
+ background: rgba(11, 16, 24, 0.78);
939
+ color: #f5f0e6;
940
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.22);
941
+ z-index: 2;
942
+ cursor: pointer;
943
+ transition: transform 0.15s ease, border-color 0.15s ease, background 0.15s ease;
944
+ backdrop-filter: blur(10px);
945
+ }
946
+
947
+ .project-claude-action.secondary {
948
+ background: rgba(18, 24, 34, 0.6);
949
+ border-color: rgba(255, 255, 255, 0.1);
950
+ color: var(--text-secondary);
951
+ }
952
+
953
+ .project-claude-action:hover {
954
+ transform: translateY(-1px);
955
+ border-color: rgba(245, 240, 230, 0.5);
956
+ background: rgba(17, 24, 32, 0.92);
957
+ }
958
+
959
+ .project-claude-action svg,
960
+ .project-claude-action img {
961
+ width: 16px;
962
+ height: 16px;
963
+ }
964
+
923
965
  .project-info {
924
966
  padding: 12px;
925
967
  }
@@ -987,6 +1029,84 @@
987
1029
  font-weight: 700;
988
1030
  }
989
1031
 
1032
+ .claude-integration-row {
1033
+ display: flex;
1034
+ align-items: center;
1035
+ justify-content: space-between;
1036
+ gap: 12px;
1037
+ margin-top: 10px;
1038
+ padding-top: 10px;
1039
+ border-top: 1px solid var(--border);
1040
+ }
1041
+
1042
+ .claude-integration-meta {
1043
+ min-width: 0;
1044
+ }
1045
+
1046
+ .claude-integration-title {
1047
+ font-size: 11px;
1048
+ color: var(--text-muted);
1049
+ font-weight: 600;
1050
+ text-transform: uppercase;
1051
+ letter-spacing: 0.4px;
1052
+ margin-bottom: 4px;
1053
+ }
1054
+
1055
+ .claude-integration-copy {
1056
+ font-size: 12px;
1057
+ color: var(--text-secondary);
1058
+ line-height: 1.45;
1059
+ }
1060
+
1061
+ .claude-integration-actions {
1062
+ display: flex;
1063
+ align-items: center;
1064
+ gap: 8px;
1065
+ flex-shrink: 0;
1066
+ }
1067
+
1068
+ .claude-action-btn {
1069
+ display: inline-flex;
1070
+ align-items: center;
1071
+ gap: 8px;
1072
+ border: 1px solid rgba(236, 229, 215, 0.18);
1073
+ background: linear-gradient(135deg, rgba(34, 25, 18, 0.96), rgba(17, 12, 9, 0.96));
1074
+ color: #f5f0e6;
1075
+ padding: 9px 12px;
1076
+ border-radius: 10px;
1077
+ cursor: pointer;
1078
+ transition: transform 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
1079
+ font-size: 12px;
1080
+ font-weight: 600;
1081
+ box-shadow: 0 14px 28px rgba(0, 0, 0, 0.18);
1082
+ }
1083
+
1084
+ .claude-action-btn:hover {
1085
+ transform: translateY(-1px);
1086
+ border-color: rgba(245, 240, 230, 0.34);
1087
+ }
1088
+
1089
+ .claude-action-btn.secondary {
1090
+ background: var(--bg-tertiary);
1091
+ color: var(--text-secondary);
1092
+ box-shadow: none;
1093
+ border-color: var(--border);
1094
+ }
1095
+
1096
+ .claude-action-btn:disabled {
1097
+ opacity: 0.56;
1098
+ cursor: default;
1099
+ transform: none;
1100
+ }
1101
+
1102
+ .claude-action-btn svg,
1103
+ .claude-action-btn img {
1104
+ width: 14px;
1105
+ height: 14px;
1106
+ flex-shrink: 0;
1107
+ object-fit: contain;
1108
+ }
1109
+
990
1110
  .project-meta {
991
1111
  font-size: 11px;
992
1112
  color: var(--text-muted);
@@ -6795,7 +6915,7 @@
6795
6915
  <span>App Intelligence</span>
6796
6916
  </div>
6797
6917
  <div class="welcome-footnote">
6798
- Built with the open <a href="https://esvp.dev" target="_blank" rel="noreferrer">ESVP protocol</a> by Entropy Lab for reproducible mobile sessions, replay, and network-aware validation.
6918
+ Built with the open ESVP protocol by Entropy Lab for reproducible mobile sessions, replay, and network-aware validation.
6799
6919
  </div>
6800
6920
  </div>
6801
6921
  </div>
@@ -8109,6 +8229,265 @@
8109
8229
  }
8110
8230
  }
8111
8231
 
8232
+ let claudeDesktopStatus = {
8233
+ ready: false,
8234
+ appDetected: false,
8235
+ launcherSupported: false,
8236
+ mcpConfigured: false,
8237
+ serverName: null,
8238
+ installCommand: 'npx -y @veolab/discoverylab@latest install --target desktop',
8239
+ message: 'Checking Claude Desktop...',
8240
+ checking: false,
8241
+ error: null,
8242
+ lastCheckedAt: null
8243
+ };
8244
+
8245
+ function getClaudeIconSvg() {
8246
+ return `
8247
+ <img src="/assets/icons/icons8-claude-150.png" alt="" aria-hidden="true" />
8248
+ `;
8249
+ }
8250
+
8251
+ function projectHasRenderableThumb(project) {
8252
+ if (!project) return false;
8253
+ if (project.thumbnailPath) return true;
8254
+ if (!project.videoPath) return false;
8255
+
8256
+ const ext = String(project.videoPath).toLowerCase().split('.').pop();
8257
+ return ['png', 'jpg', 'jpeg', 'gif', 'webp', 'mp4', 'mov', 'webm'].includes(ext);
8258
+ }
8259
+
8260
+ function isProjectReadyForClaude(project) {
8261
+ if (!project) return false;
8262
+ if (isProjectAnalyzing(project.status || '')) return false;
8263
+ return Boolean(
8264
+ (project.aiSummary && project.aiSummary.trim()) ||
8265
+ (project.ocrText && project.ocrText.trim()) ||
8266
+ (Array.isArray(project.frames) && project.frames.length > 0) ||
8267
+ project.frameCount > 0
8268
+ );
8269
+ }
8270
+
8271
+ function buildClaudeDesktopPrompt(project) {
8272
+ const projectId = project?.id || '';
8273
+ const projectName = project?.name || 'this DiscoveryLab project';
8274
+ return [
8275
+ `Use DiscoveryLab local tools to open project "${projectName}" (projectId: "${projectId}") in an interactive flow canvas.`,
8276
+ `Call dlab.knowledge.open with projectId "${projectId}".`,
8277
+ 'After opening the canvas, explain the flow, key screens, and important risks or opportunities.',
8278
+ ].join(' ');
8279
+ }
8280
+
8281
+ function renderClaudeProjectQuickAction(project) {
8282
+ if (!isProjectReadyForClaude(project) || !claudeDesktopStatus.launcherSupported || !projectHasRenderableThumb(project)) {
8283
+ return '';
8284
+ }
8285
+
8286
+ const setupNeeded = claudeDesktopStatus.appDetected && !claudeDesktopStatus.mcpConfigured;
8287
+ const ready = claudeDesktopStatus.ready;
8288
+ const promptTitle = ready
8289
+ ? `Copy prompt and open ${project.name} in Claude Desktop`
8290
+ : setupNeeded
8291
+ ? `Copy the ${project.name} prompt and open Claude Desktop`
8292
+ : `Copy the ${project.name} prompt and check Claude Desktop`;
8293
+ const buttonClass = ready ? 'project-claude-action' : 'project-claude-action secondary';
8294
+
8295
+ return `
8296
+ <button
8297
+ class="${buttonClass}"
8298
+ type="button"
8299
+ data-project-id="${escapeAttr(project.id)}"
8300
+ title="${escapeAttr(promptTitle)}"
8301
+ aria-label="${escapeAttr(promptTitle)}"
8302
+ onclick="event.stopPropagation(); openProjectInClaudeById(this.dataset.projectId);"
8303
+ >
8304
+ ${getClaudeIconSvg()}
8305
+ </button>
8306
+ `;
8307
+ }
8308
+
8309
+ function renderClaudeProjectLaunchRow(project) {
8310
+ const projectReady = isProjectReadyForClaude(project);
8311
+ const statusCopy = !projectReady
8312
+ ? 'Finish analysis or capture enough frames before opening this project in Claude Desktop.'
8313
+ : claudeDesktopStatus.error
8314
+ ? claudeDesktopStatus.error
8315
+ : claudeDesktopStatus.message || 'Claude Desktop status unavailable.';
8316
+ const ready = claudeDesktopStatus.ready && projectReady;
8317
+ const setupNeeded = claudeDesktopStatus.appDetected && !claudeDesktopStatus.mcpConfigured;
8318
+ const buttonLabel = ready
8319
+ ? 'Open in Claude Desktop'
8320
+ : !projectReady
8321
+ ? 'Analyze first'
8322
+ : setupNeeded
8323
+ ? 'Copy prompt'
8324
+ : 'Reload';
8325
+ const buttonClass = ready ? 'claude-action-btn' : 'claude-action-btn secondary';
8326
+ const action = ready
8327
+ ? 'openCurrentProjectInClaude()'
8328
+ : !projectReady
8329
+ ? ''
8330
+ : setupNeeded
8331
+ ? 'copyCurrentProjectClaudePrompt()'
8332
+ : 'refreshClaudeDesktopStatus()';
8333
+
8334
+ return `
8335
+ <div class="claude-integration-row">
8336
+ <div class="claude-integration-meta">
8337
+ <div class="claude-integration-title">Claude Desktop</div>
8338
+ <div class="claude-integration-copy">${escapeHtml(statusCopy)}</div>
8339
+ </div>
8340
+ <div class="claude-integration-actions">
8341
+ <button class="${buttonClass}" type="button" ${action ? `onclick="${action}"` : 'disabled'}>
8342
+ ${getClaudeIconSvg()}
8343
+ <span>${escapeHtml(buttonLabel)}</span>
8344
+ </button>
8345
+ </div>
8346
+ </div>
8347
+ `;
8348
+ }
8349
+
8350
+ function rerenderClaudeDesktopAffordances() {
8351
+ if (currentView === 'projects') {
8352
+ renderProjects();
8353
+ } else if (currentView === 'analysis' && currentProject) {
8354
+ updateAnalysisTab();
8355
+ } else if (currentView === 'export' && currentProject) {
8356
+ updateExportTab();
8357
+ }
8358
+ }
8359
+
8360
+ async function refreshClaudeDesktopStatus(options = {}) {
8361
+ const { silent = false } = options;
8362
+ claudeDesktopStatus.checking = true;
8363
+
8364
+ try {
8365
+ const response = await fetch('/api/integrations/claude-desktop/status');
8366
+ const data = await response.json();
8367
+ if (data?.error) {
8368
+ throw new Error(data.error);
8369
+ }
8370
+
8371
+ claudeDesktopStatus = {
8372
+ ...claudeDesktopStatus,
8373
+ ready: !!data.ready,
8374
+ appDetected: !!data.appDetected,
8375
+ launcherSupported: !!data.launcherSupported,
8376
+ mcpConfigured: !!data.mcpConfigured,
8377
+ serverName: data.serverName || null,
8378
+ installCommand: data.installCommand || claudeDesktopStatus.installCommand,
8379
+ message: data.message || 'Claude Desktop status loaded.',
8380
+ checking: false,
8381
+ error: null,
8382
+ lastCheckedAt: new Date().toISOString()
8383
+ };
8384
+ } catch (error) {
8385
+ claudeDesktopStatus = {
8386
+ ...claudeDesktopStatus,
8387
+ checking: false,
8388
+ error: error instanceof Error ? error.message : 'Failed to check Claude Desktop status',
8389
+ message: 'Could not check Claude Desktop status.'
8390
+ };
8391
+ if (!silent) {
8392
+ showToast('Failed to check Claude Desktop status', 'error');
8393
+ }
8394
+ }
8395
+
8396
+ rerenderClaudeDesktopAffordances();
8397
+ }
8398
+
8399
+ function copyClaudeDesktopInstallCommand() {
8400
+ void copyToClipboard(
8401
+ claudeDesktopStatus.installCommand || 'npx -y @veolab/discoverylab@latest install --target desktop',
8402
+ { silent: true }
8403
+ ).then((copied) => {
8404
+ showToast(copied ? 'Claude Desktop install command copied' : 'Failed to copy install command', copied ? 'success' : 'error');
8405
+ });
8406
+ }
8407
+
8408
+ async function copyClaudeProjectPrompt(project) {
8409
+ if (!project) return false;
8410
+
8411
+ const copied = await copyToClipboard(buildClaudeDesktopPrompt(project), { silent: true });
8412
+ if (!copied) {
8413
+ showToast('Failed to copy Claude Desktop prompt', 'error');
8414
+ return false;
8415
+ }
8416
+
8417
+ return true;
8418
+ }
8419
+
8420
+ async function copyCurrentProjectClaudePrompt() {
8421
+ if (!currentProject?.id) {
8422
+ showToast('Select a project first', 'warning');
8423
+ return;
8424
+ }
8425
+
8426
+ const copied = await copyClaudeProjectPrompt(currentProject);
8427
+ if (!copied) return;
8428
+
8429
+ showToast('Claude Desktop prompt copied', 'success');
8430
+ }
8431
+
8432
+ async function launchClaudeDesktop() {
8433
+ const response = await fetch('/api/integrations/claude-desktop/launch', { method: 'POST' });
8434
+ const data = await response.json();
8435
+ if (!response.ok || data?.success === false) {
8436
+ throw new Error(data?.error || 'Failed to open Claude Desktop');
8437
+ }
8438
+ }
8439
+
8440
+ async function copyPromptAndLaunchClaude(project) {
8441
+ if (!project) return;
8442
+
8443
+ const copied = await copyClaudeProjectPrompt(project);
8444
+ if (!copied) {
8445
+ return;
8446
+ }
8447
+
8448
+ try {
8449
+ await launchClaudeDesktop();
8450
+ showToast('Prompt copied and Claude Desktop opened', 'success');
8451
+ } catch (error) {
8452
+ showToast(error instanceof Error ? error.message : 'Prompt copied, but Claude Desktop could not be opened', 'warning');
8453
+ }
8454
+ }
8455
+
8456
+ async function openProjectInClaudeById(projectId) {
8457
+ const project = projects.find((item) => item.id === projectId) || (currentProject?.id === projectId ? currentProject : null);
8458
+ if (!project) {
8459
+ showToast('Project not found', 'error');
8460
+ return;
8461
+ }
8462
+
8463
+ if (!claudeDesktopStatus.ready) {
8464
+ if (claudeDesktopStatus.appDetected && !claudeDesktopStatus.mcpConfigured) {
8465
+ const copied = await copyClaudeProjectPrompt(project);
8466
+ if (copied) {
8467
+ showToast('Project prompt copied. Enable the DiscoveryLab extension in Claude Desktop to open it there.', 'warning');
8468
+ }
8469
+ return;
8470
+ }
8471
+
8472
+ const copied = await copyClaudeProjectPrompt(project);
8473
+ if (copied) {
8474
+ showToast(claudeDesktopStatus.message || 'Project prompt copied. Claude Desktop is not ready yet.', 'warning');
8475
+ }
8476
+ return;
8477
+ }
8478
+
8479
+ await copyPromptAndLaunchClaude(project);
8480
+ }
8481
+
8482
+ async function openCurrentProjectInClaude() {
8483
+ if (!currentProject?.id) {
8484
+ showToast('Select a project first', 'warning');
8485
+ return;
8486
+ }
8487
+
8488
+ await openProjectInClaudeById(currentProject.id);
8489
+ }
8490
+
8112
8491
  let jiraMcpStatus = {
8113
8492
  available: false,
8114
8493
  configured: false,
@@ -11288,6 +11667,7 @@
11288
11667
  <div class="processing-text">${processingText}</div>
11289
11668
  </div>
11290
11669
  <div class="project-thumb">
11670
+ ${renderClaudeProjectQuickAction(p)}
11291
11671
  ${getProjectThumb(p)}
11292
11672
  </div>
11293
11673
  <div class="project-info">
@@ -12418,6 +12798,7 @@
12418
12798
  <button class="btn btn-primary btn-small" onclick="retryProjectAnalysis()" id="retryProjectAnalysisBtn">Retry Analysis</button>
12419
12799
  </div>
12420
12800
  ` : ''}
12801
+ ${renderClaudeProjectLaunchRow(currentProject)}
12421
12802
  <div class="project-integration-row">
12422
12803
  <div class="project-integration-label">Jira MCP</div>
12423
12804
  <div class="project-integration-status">
@@ -12864,6 +13245,23 @@
12864
13245
 
12865
13246
  if (favoriteTemplate && currentTemplateProps?.templatesAllowed !== false) {
12866
13247
  switchTemplate(favoriteTemplate);
13248
+ } else if (currentTemplateProps?.templatesAllowed !== false) {
13249
+ // Auto-select cached template if available (no re-render needed)
13250
+ const templates = templateStatus.templates || [];
13251
+ for (const t of templates) {
13252
+ try {
13253
+ const checkRes = await fetch('/api/templates/render', {
13254
+ method: 'POST',
13255
+ headers: { 'Content-Type': 'application/json' },
13256
+ body: JSON.stringify({ projectId: currentProject?.id, templateId: t.id }),
13257
+ });
13258
+ const checkData = await checkRes.json();
13259
+ if (checkData.cached) {
13260
+ switchTemplate(t.id);
13261
+ break;
13262
+ }
13263
+ } catch { /* skip */ }
13264
+ }
12867
13265
  }
12868
13266
  } catch (e) {
12869
13267
  console.log('[Templates] Init error:', e);
@@ -12983,7 +13381,7 @@
12983
13381
  }
12984
13382
  }
12985
13383
 
12986
- async function switchTemplate(templateId) {
13384
+ async function switchTemplate(templateId, force = false) {
12987
13385
  // Skip if already on this template
12988
13386
  if (templateId === activeTemplate) return;
12989
13387
  if (templateId !== 'raw' && currentTemplateProps?.templatesAllowed === false) {
@@ -13028,7 +13426,7 @@
13028
13426
  const res = await fetch('/api/templates/render', {
13029
13427
  method: 'POST',
13030
13428
  headers: { 'Content-Type': 'application/json' },
13031
- body: JSON.stringify({ projectId: currentProject.id, templateId }),
13429
+ body: JSON.stringify({ projectId: currentProject.id, templateId, force }),
13032
13430
  });
13033
13431
  const data = await res.json();
13034
13432
 
@@ -13038,6 +13436,20 @@
13038
13436
  return;
13039
13437
  }
13040
13438
 
13439
+ // Cached render - show immediately without polling
13440
+ if (data.cached && data.previewUrl) {
13441
+ const skeleton = document.getElementById('templateSkeleton');
13442
+ const templateContainer = document.getElementById('templatePreviewContainer');
13443
+ const templateVideo = document.getElementById('templateVideo');
13444
+ if (skeleton) skeleton.style.display = 'none';
13445
+ if (templateVideo) {
13446
+ templateVideo.src = data.previewUrl + '&t=' + Date.now();
13447
+ templateVideo.load();
13448
+ }
13449
+ if (templateContainer) templateContainer.style.display = '';
13450
+ return;
13451
+ }
13452
+
13041
13453
  if (data.jobId) {
13042
13454
  pollRenderJob(data.jobId, templateId);
13043
13455
  }
@@ -13490,11 +13902,11 @@
13490
13902
  deviceMockup: data.content?.deviceMockup || deviceMockup,
13491
13903
  };
13492
13904
 
13493
- // Force re-render
13905
+ // Force re-render after content edit
13494
13906
  if (activeTemplate !== 'raw') {
13495
13907
  const prevTemplate = activeTemplate;
13496
13908
  activeTemplate = '__force_rerender__';
13497
- switchTemplate(prevTemplate);
13909
+ switchTemplate(prevTemplate, true);
13498
13910
  }
13499
13911
  } catch {
13500
13912
  showToast('Failed to save content', 'error');
@@ -15571,9 +15983,34 @@
15571
15983
  showToast(`Copied "${command}" - paste in Claude Code`, 'success', 5000);
15572
15984
  }
15573
15985
 
15574
- function copyToClipboard(text) {
15575
- navigator.clipboard.writeText(text);
15576
- showToast('Copied to clipboard', 'success');
15986
+ async function copyToClipboard(text, options = {}) {
15987
+ const { silent = false } = options;
15988
+
15989
+ try {
15990
+ if (navigator.clipboard?.writeText) {
15991
+ await navigator.clipboard.writeText(text);
15992
+ } else {
15993
+ const textarea = document.createElement('textarea');
15994
+ textarea.value = text;
15995
+ textarea.setAttribute('readonly', 'true');
15996
+ textarea.style.position = 'absolute';
15997
+ textarea.style.left = '-9999px';
15998
+ document.body.appendChild(textarea);
15999
+ textarea.select();
16000
+ document.execCommand('copy');
16001
+ textarea.remove();
16002
+ }
16003
+
16004
+ if (!silent) {
16005
+ showToast('Copied to clipboard', 'success');
16006
+ }
16007
+ return true;
16008
+ } catch (error) {
16009
+ if (!silent) {
16010
+ showToast('Failed to copy to clipboard', 'error');
16011
+ }
16012
+ return false;
16013
+ }
15577
16014
  }
15578
16015
 
15579
16016
  async function openMaestroStudio() {
@@ -15868,6 +16305,7 @@
15868
16305
  if (el && data.version) el.textContent = 'v' + data.version;
15869
16306
  }).catch(() => {});
15870
16307
  void loadProxySafetySettings().catch(() => null);
16308
+ void refreshClaudeDesktopStatus({ silent: true });
15871
16309
 
15872
16310
  // Auto-check dependencies on first load — open Settings if critical deps missing
15873
16311
  fetch('/api/setup/status').then(r => r.json()).then(data => {
@@ -21984,8 +22422,8 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
21984
22422
  jpg: 'Exports the current capture as JPEG images.',
21985
22423
  mp4: 'Exports the current capture as an MP4 video.',
21986
22424
  gif: 'Exports the current capture as an animated GIF.',
21987
- applab: 'Packages everything App Lab has locally for this project: media, recording session, test script, network trace, OCR, app intelligence, Task Hub data, and prior export artifacts like grids.',
21988
- esvp: 'Packages the full local project plus the attached ESVP session snapshot when available, so you can share trace, scripts, media, OCR, and app intelligence together.',
22425
+ applab: 'Packages a Claude-friendly App Lab bundle: analyzed frames, thumbnail, OCR, app intelligence, network trace, ESVP snapshot, and Task Hub metadata without heavy renders or long recordings.',
22426
+ esvp: 'Packages a lightweight ESVP-ready bundle with analyzed frames, OCR, app intelligence, network trace, and the attached ESVP snapshot when available.',
21989
22427
  };
21990
22428
 
21991
22429
  help.textContent = descriptions[select.value] || '';
@@ -22043,13 +22481,37 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
22043
22481
  <div id="exportFormatHelp" style="margin-top: 8px; font-size: 12px; color: var(--text-muted); line-height: 1.5;"></div>
22044
22482
  </div>
22045
22483
 
22046
- <button class="btn btn-primary" onclick="exportProject()">Export</button>
22484
+ <div style="display: flex; gap: 8px;">
22485
+ <button class="btn btn-primary" onclick="exportProject()">Export</button>
22486
+ <button class="btn btn-secondary" onclick="exportInfographicHtml()" style="font-size: 11px;">Infographic HTML</button>
22487
+ </div>
22047
22488
  </div>
22048
22489
  `;
22049
22490
 
22050
22491
  updateExportFormatHelp();
22051
22492
  }
22052
22493
 
22494
+ async function exportInfographicHtml() {
22495
+ if (!currentProject) return;
22496
+ showToast('Generating infographic...', 'info');
22497
+ try {
22498
+ const resp = await fetch('/api/export/infographic', {
22499
+ method: 'POST',
22500
+ headers: { 'Content-Type': 'application/json' },
22501
+ body: JSON.stringify({ projectId: currentProject.id, open: true }),
22502
+ });
22503
+ const data = await resp.json();
22504
+ if (data.success) {
22505
+ const sizeKb = ((data.size || 0) / 1024).toFixed(1);
22506
+ showToast(`Infographic exported (${sizeKb}KB, ${data.frameCount} frames)`, 'success');
22507
+ } else {
22508
+ showToast(data.error || 'Export failed', 'error');
22509
+ }
22510
+ } catch (err) {
22511
+ showToast('Export failed', 'error');
22512
+ }
22513
+ }
22514
+
22053
22515
  async function exportProject() {
22054
22516
  if (!currentProject) return;
22055
22517
 
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  startServer,
4
4
  stopServer
5
- } from "./chunk-2UUMLAVR.js";
5
+ } from "./chunk-V6RREMYD.js";
6
6
  import {
7
7
  analyzeTools,
8
8
  canvasTools,
@@ -16,15 +16,15 @@ import {
16
16
  templateTools,
17
17
  testingTools,
18
18
  uiTools
19
- } from "./chunk-HB3YPWF3.js";
19
+ } from "./chunk-Q7Q3A2ZI.js";
20
+ import "./chunk-34GGYFXX.js";
20
21
  import "./chunk-PMCXEA7J.js";
21
22
  import {
22
23
  setupTools
23
- } from "./chunk-CUBQRT5L.js";
24
+ } from "./chunk-JAA53ES7.js";
24
25
  import {
25
26
  mcpServer
26
- } from "./chunk-XKX6NBHF.js";
27
- import "./chunk-34GGYFXX.js";
27
+ } from "./chunk-TWRWARU4.js";
28
28
  import "./chunk-6GK5K6CS.js";
29
29
  import "./chunk-7R5YNOXE.js";
30
30
  import "./chunk-3ERJNXYM.js";