shokupan 0.10.4 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/{analyzer-CKLGLFtx.cjs → analyzer-BAhvpNY_.cjs} +2 -7
  2. package/dist/{analyzer-CKLGLFtx.cjs.map → analyzer-BAhvpNY_.cjs.map} +1 -1
  3. package/dist/{analyzer-BqIe1p0R.js → analyzer-CnKnQ5KV.js} +3 -8
  4. package/dist/{analyzer-BqIe1p0R.js.map → analyzer-CnKnQ5KV.js.map} +1 -1
  5. package/dist/{analyzer.impl-D9Yi1Hax.cjs → analyzer.impl-CfpMu4-g.cjs} +586 -40
  6. package/dist/analyzer.impl-CfpMu4-g.cjs.map +1 -0
  7. package/dist/{analyzer.impl-CV6W1Eq7.js → analyzer.impl-DCiqlXI5.js} +586 -40
  8. package/dist/analyzer.impl-DCiqlXI5.js.map +1 -0
  9. package/dist/cli.cjs +206 -18
  10. package/dist/cli.cjs.map +1 -1
  11. package/dist/cli.js +206 -18
  12. package/dist/cli.js.map +1 -1
  13. package/dist/context.d.ts +6 -1
  14. package/dist/index.cjs +2405 -1008
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.js +2402 -1006
  17. package/dist/index.js.map +1 -1
  18. package/dist/plugins/application/api-explorer/static/explorer-client.mjs +423 -30
  19. package/dist/plugins/application/api-explorer/static/style.css +351 -10
  20. package/dist/plugins/application/api-explorer/static/theme.css +7 -2
  21. package/dist/plugins/application/asyncapi/generator.d.ts +4 -0
  22. package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +154 -22
  23. package/dist/plugins/application/asyncapi/static/style.css +24 -8
  24. package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +107 -0
  25. package/dist/plugins/application/dashboard/metrics-collector.d.ts +38 -2
  26. package/dist/plugins/application/dashboard/plugin.d.ts +44 -1
  27. package/dist/plugins/application/dashboard/static/charts.js +127 -62
  28. package/dist/plugins/application/dashboard/static/client.js +160 -0
  29. package/dist/plugins/application/dashboard/static/graph.mjs +167 -56
  30. package/dist/plugins/application/dashboard/static/reactflow.css +20 -10
  31. package/dist/plugins/application/dashboard/static/registry.js +112 -8
  32. package/dist/plugins/application/dashboard/static/requests.js +868 -58
  33. package/dist/plugins/application/dashboard/static/styles.css +186 -14
  34. package/dist/plugins/application/dashboard/static/tabs.js +44 -9
  35. package/dist/plugins/application/dashboard/static/theme.css +7 -2
  36. package/dist/plugins/application/openapi/analyzer.impl.d.ts +61 -1
  37. package/dist/plugins/application/openapi/openapi.d.ts +3 -0
  38. package/dist/plugins/application/shared/ast-utils.d.ts +7 -0
  39. package/dist/router.d.ts +55 -16
  40. package/dist/shokupan.d.ts +7 -2
  41. package/dist/util/adapter/adapters.d.ts +19 -0
  42. package/dist/util/adapter/filesystem.d.ts +20 -0
  43. package/dist/util/controller-scanner.d.ts +4 -0
  44. package/dist/util/cpu-monitor.d.ts +2 -0
  45. package/dist/util/middleware-tracker.d.ts +10 -0
  46. package/dist/util/types.d.ts +37 -0
  47. package/package.json +5 -5
  48. package/dist/analyzer.impl-CV6W1Eq7.js.map +0 -1
  49. package/dist/analyzer.impl-D9Yi1Hax.cjs.map +0 -1
  50. package/dist/http-server-BEMPIs33.cjs +0 -85
  51. package/dist/http-server-BEMPIs33.cjs.map +0 -1
  52. package/dist/http-server-CCeagTyU.js +0 -68
  53. package/dist/http-server-CCeagTyU.js.map +0 -1
  54. package/dist/plugins/application/dashboard/static/poll.js +0 -146
@@ -40,6 +40,16 @@ function handleHashNavigation() {
40
40
  return;
41
41
  }
42
42
 
43
+ // Check if it's a middleware view
44
+ if (hash.startsWith('middleware-')) {
45
+ const middlewareId = hash.replace('middleware-', '');
46
+ const middleware = explorerData.middlewareRegistry?.[middlewareId];
47
+ if (middleware) {
48
+ renderMiddlewareView(middleware, container);
49
+ return;
50
+ }
51
+ }
52
+
43
53
  // Find route
44
54
  const route = explorerData.routes.find(r => r.op.operationId === hash);
45
55
  if (route) {
@@ -60,19 +70,202 @@ function renderInfoSection(info) {
60
70
  `;
61
71
  }
62
72
 
73
+ function renderMiddlewareView(middleware, container) {
74
+ const html = `
75
+ <div class="middleware-detail-view">
76
+ <div class="middleware-header">
77
+ <h1>${middleware.name}</h1>
78
+ <div class="middleware-meta">
79
+ ${middleware.scope ? `<span class="badge">${middleware.scope}</span>` : ''}
80
+ ${middleware.file ? `
81
+ <a href="vscode://file/${middleware.file}:${middleware.startLine || 1}" class="doc-source-link">
82
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
83
+ <polyline points="16 18 22 12 16 6"></polyline>
84
+ <polyline points="8 6 2 12 8 18"></polyline>
85
+ </svg>
86
+ ${middleware.file.split('/').pop()}:${middleware.startLine || 1}
87
+ </a>
88
+ ` : ''}
89
+ </div>
90
+ </div>
91
+
92
+ ${middleware.responseTypes || middleware.headers ? `
93
+ <div class="middleware-capabilities">
94
+ ${middleware.responseTypes ? `
95
+ <div class="capability-section">
96
+ <h3>Response Types</h3>
97
+ <div class="response-list">
98
+ ${Object.entries(middleware.responseTypes).map(([code, resp]) => `
99
+ <div class="response-item">
100
+ <code class="status-code">${code}</code>
101
+ <span>${resp.description || 'Response'}</span>
102
+ </div>
103
+ `).join('')}
104
+ </div>
105
+ </div>
106
+ ` : ''}
107
+ ${middleware.headers && middleware.headers.length > 0 ? `
108
+ <div class="capability-section">
109
+ <h3>Headers</h3>
110
+ <div class="header-list">
111
+ ${middleware.headers.map(h => `<code class="header-name">${h}</code>`).join('')}
112
+ </div>
113
+ </div>
114
+ ` : ''}
115
+ </div>
116
+ ` : ''}
117
+
118
+ ${middleware.file ? `
119
+ <div class="source-section">
120
+ <h3>Source Code</h3>
121
+ <div id="monaco-middleware-source" class="monaco-container" style="height: 400px;"></div>
122
+ </div>
123
+ ` : ''}
124
+
125
+ ${middleware.usedBy && middleware.usedBy.length > 0 ? `
126
+ <div class="usage-section">
127
+ <h3>Used By (${middleware.usedBy.length} routes)</h3>
128
+ <div class="table-container">
129
+ <table class="data-table">
130
+ <thead>
131
+ <tr>
132
+ <th>Method</th>
133
+ <th>Path</th>
134
+ <th>Description</th>
135
+ </tr>
136
+ </thead>
137
+ <tbody>
138
+ ${middleware.usedBy.map(routePath => {
139
+ const route = explorerData.routes.find(r => r.path === routePath);
140
+ if (route) {
141
+ return `
142
+ <tr>
143
+ <td class="col-method"><span class="badge badge-${route.method.toUpperCase()}">${route.method.toUpperCase()}</span></td>
144
+ <td class="col-path"><a href="#${route.op.operationId}" class="route-link">${routePath}</a></td>
145
+ <td class="col-desc">${route.op.summary || route.op.description || '-'}</td>
146
+ </tr>
147
+ `;
148
+ }
149
+ return `
150
+ <tr>
151
+ <td colspan="3">${routePath} (Route definition not found)</td>
152
+ </tr>
153
+ `;
154
+ }).join('')}
155
+ </tbody>
156
+ </table>
157
+ </div>
158
+ </div>
159
+ ` : ''}
160
+ </div>
161
+ `;
162
+
163
+ container.innerHTML = html;
164
+
165
+ // Initialize Monaco for source code
166
+ if (middleware.file && typeof require !== 'undefined') {
167
+ require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
168
+ require(['vs/editor/editor.main'], function () {
169
+ const monacoContainer = document.getElementById('monaco-middleware-source');
170
+ if (monacoContainer) {
171
+ if (currentEditors.source) currentEditors.source.dispose();
172
+
173
+ currentEditors.source = monaco.editor.create(monacoContainer, {
174
+ value: '// Loading source...',
175
+ language: 'typescript',
176
+ theme: 'vs-dark',
177
+ minimap: { enabled: false },
178
+ lineNumbers: 'on',
179
+ readOnly: true,
180
+ scrollBeyondLastLine: false,
181
+ automaticLayout: true,
182
+ glyphMargin: false,
183
+ folding: false,
184
+ lineNumbersMinChars: 3,
185
+ fontSize: 13,
186
+ fontFamily: 'JetBrains Mono',
187
+ renderLineHighlight: 'none'
188
+ });
189
+
190
+ // Load source
191
+ fetch(`_source?file=${encodeURIComponent(middleware.file)}`)
192
+ .then(res => {
193
+ if (!res.ok) throw new Error(res.statusText);
194
+ return res.text();
195
+ })
196
+ .then(text => {
197
+ if (currentEditors.source) {
198
+ currentEditors.source.setValue(text);
199
+
200
+ // Highlight the middleware function
201
+ if (middleware.startLine) {
202
+ const endLine = middleware.endLine || middleware.startLine;
203
+ const decorations = [{
204
+ range: new monaco.Range(middleware.startLine, 1, endLine, 1),
205
+ options: {
206
+ isWholeLine: true,
207
+ className: 'closure-highlight'
208
+ }
209
+ }];
210
+ currentEditors.source.deltaDecorations([], decorations);
211
+ currentEditors.source.revealLineInCenter(middleware.startLine);
212
+ }
213
+ }
214
+ })
215
+ .catch(err => {
216
+ if (currentEditors.source) {
217
+ currentEditors.source.setValue(`// Failed to load source: ${err.message}`);
218
+ }
219
+ });
220
+ }
221
+ });
222
+ }
223
+ }
224
+
63
225
  // Helper to recursively render schema properties
64
- function renderSchema(schema, depth = 0) {
226
+ function renderSchema(schema, depth = 0, isResponse = false) {
65
227
  if (!schema) return '';
66
228
 
67
229
  const indent = depth * 16;
68
230
  const type = schema.type || 'any';
69
231
  const required = schema.required || [];
70
232
 
233
+ // Handle oneOf (multiple possible schemas)
234
+ if (schema.oneOf) {
235
+ return `
236
+ <div style="margin-left: ${indent}px;">
237
+ <div style="font-weight: 500; color: var(--text-primary); margin-bottom: 8px;">
238
+ <span style="color: var(--text-secondary); font-size: 0.85rem;">One of the following:</span>
239
+ </div>
240
+ ${schema.oneOf.map((subSchema, idx) => `
241
+ <div style="border-left: 3px solid #4caf50; padding-left: 12px; margin-bottom: 12px;">
242
+ <div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 4px;">Option ${idx + 1}:</div>
243
+ ${renderSchema(subSchema, 0, isResponse)}
244
+ </div>
245
+ `).join('')}
246
+ </div>
247
+ `;
248
+ }
249
+
71
250
  if (type === 'object' && schema.properties) {
72
251
  const props = Object.entries(schema.properties).map(([key, prop]) => {
73
252
  const isRequired = required.includes(key);
74
- const propType = prop.type || 'any';
75
- const hasNested = (propType === 'object' && prop.properties) || (propType === 'array' && prop.items);
253
+ const isUnknown = prop['x-unknown'] === true;
254
+ const propType = isUnknown ? 'unknown' : (prop.type || 'any');
255
+ const hasNested = (prop.type === 'object' && prop.properties) || (prop.type === 'array' && prop.items);
256
+
257
+ // For responses, show "optional" for non-required fields
258
+ // For requests, show "required" for required fields
259
+ let badgeHtml = '';
260
+ if (isResponse) {
261
+ if (!isRequired) {
262
+ badgeHtml = '<div class="property-optional" style="margin-left: auto; font-size: 0.75rem; color: #9e9e9e; text-transform: uppercase; font-style: italic;">optional</div>';
263
+ }
264
+ } else {
265
+ if (isRequired) {
266
+ badgeHtml = '<div class="property-required" style="margin-left: auto; font-size: 0.75rem; color: #f44336; text-transform: uppercase;">required</div>';
267
+ }
268
+ }
76
269
 
77
270
  return `
78
271
  <div style="margin-left: ${indent}px;">
@@ -80,11 +273,12 @@ function renderSchema(schema, depth = 0) {
80
273
  <div class="property-name" style="font-family: monospace; font-weight: 500; color: var(--text-primary);">${key}</div>
81
274
  <span class="property-detail" style="color: var(--text-secondary); font-size: 0.85rem;">
82
275
  <span class="property-detail-value">${propType}</span>
276
+ ${isUnknown ? '<span class="unknown-marker" title="Type could not be determined statically" style="color: #ff9800; margin-left: 4px;">⚠️</span>' : ''}
83
277
  </span>
84
- ${isRequired ? '<div class="property-required" style="margin-left: auto; font-size: 0.75rem; color: #f44336; text-transform: uppercase;">required</div>' : ''}
278
+ ${badgeHtml}
85
279
  </div>
86
280
  ${prop.description ? `<div style="color: var(--text-secondary); font-size: 0.85rem; margin-left: 0; margin-top: -4px; margin-bottom: 4px;">${prop.description}</div>` : ''}
87
- ${hasNested ? renderSchema(propType === 'array' ? prop.items : prop, depth + 1) : ''}
281
+ ${hasNested ? renderSchema(propType === 'array' ? prop.items : prop, depth + 1, isResponse) : ''}
88
282
  </div>
89
283
  `;
90
284
  }).join('');
@@ -95,12 +289,18 @@ function renderSchema(schema, depth = 0) {
95
289
  <div style="font-family: monospace; font-size: 0.85rem; color: var(--text-secondary);">
96
290
  [array items]
97
291
  </div>
98
- ${renderSchema(schema.items, depth + 1)}
292
+ ${renderSchema(schema.items, depth + 1, isResponse)}
99
293
  </div>
100
294
  `;
101
295
  }
102
296
 
103
- return '';
297
+ return `
298
+ <div style="margin-left: ${indent}px; padding: 4px 0;">
299
+ <span class="property-detail-value" style="color: var(--text-secondary); font-family: monospace;">${type}</span>
300
+ ${schema.format ? `<span style="color: var(--text-secondary); font-size: 0.85rem; margin-left: 6px;">(${schema.format})</span>` : ''}
301
+ ${schema.description ? `<div style="color: var(--text-secondary); font-size: 0.85rem; margin-top: 4px;">${schema.description}</div>` : ''}
302
+ </div>
303
+ `;
104
304
  }
105
305
 
106
306
  // Helper to highlight path operators
@@ -168,11 +368,38 @@ function renderRequestView(route, container) {
168
368
  <div class="info-header">
169
369
  <h2 class="info-title">${summary}</h2>
170
370
  <div class="info-meta">
371
+ ${op['x-shokupan-builtin'] ? `
372
+ <div class="meta-row">
373
+ <span class="builtin-badge" title="This endpoint is provided by a built-in plugin">
374
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
375
+ <rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
376
+ <path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
377
+ <line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
378
+ </svg>
379
+ Built-in Plugin ${op['x-shokupan-plugin-name'] ? `(${op['x-shokupan-plugin-name']})` : ''}
380
+ </span>
381
+ </div>
382
+ ` : ''}
171
383
  ${op.tags ? `<div class="meta-row"><strong>Tags:</strong> ${op.tags.map(t => `<span class="badge">${t}</span>`).join('')}</div>` : ''}
172
384
  </div>
173
385
  </div>
174
386
 
175
387
  ${op.description ? `<div class="markdown-content" style="margin:16px 0;">${parseMarkdown(op.description)}</div>` : ''}
388
+
389
+ ${op['x-source-info']?.isRuntime ? `
390
+ <div class="alert alert-warning" style="margin: 16px 0; padding: 12px; background: rgba(255, 152, 0, 0.1); border-left: 3px solid #ff9800; border-radius: 4px;">
391
+ <strong style="color: #ff9800;">⚠️ Warning:</strong> This route's path could not be statically determined.
392
+ <div style="margin-top: 4px; font-size: 0.9em; opacity: 0.8;">
393
+ The path depends on runtime variables (e.g., process.env). Static analysis features like type inference may be limited.
394
+ </div>
395
+ </div>
396
+ ` : ''}
397
+
398
+ ${op['x-warning'] ? `
399
+ <div class="alert alert-warning" style="margin: 16px 0; padding: 12px; background: rgba(255, 152, 0, 0.1); border-left: 3px solid #ff9800; border-radius: 4px;">
400
+ <strong style="color: #ff9800;">⚠️ Warning:</strong> ${op['x-warning-reason'] || 'This operation could not be fully analyzed statically'}
401
+ </div>
402
+ ` : ''}
176
403
 
177
404
  ${op.tags && op.tags.length > 0 ? `
178
405
  <div class="hierarchy-section" style="margin:16px 0;">
@@ -259,7 +486,7 @@ function renderRequestView(route, container) {
259
486
  ` : ''}
260
487
  ${schema ? `
261
488
  <div style="margin-top: 8px; background: var(--bg-primary); padding: 8px; border-radius: 4px;">
262
- ${renderSchema(schema)}
489
+ ${renderSchema(schema, 0, true)}
263
490
  </div>
264
491
  ` : ''}
265
492
  </div>
@@ -272,13 +499,18 @@ function renderRequestView(route, container) {
272
499
  ${source ? `
273
500
  <div class="source-section">
274
501
  <h3 style="margin-bottom:8px; font-size:1.1rem; color:var(--text-primary);">Source Code</h3>
275
- <div class="source-header" style="justify-content: flex-start; margin-bottom: 8px;">
502
+ <div class="source-header" style="justify-content: space-between; margin-bottom: 8px; align-items: center;">
276
503
  <a href="vscode://file/${source.file}:${source.line}" class="doc-source-link" title="${source.file}:${source.line}">
277
504
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px">
278
505
  <polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline>
279
506
  </svg>
280
507
  ${source.file.split('/').pop()}:${source.line}
281
508
  </a>
509
+ <button class="btn icon-btn" id="btn-source-fullscreen" title="Toggle Fullscreen">
510
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
511
+ <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>
512
+ </svg>
513
+ </button>
282
514
  </div>
283
515
  <div id="monaco-source-viewer" class="monaco-container"></div>
284
516
  </div>
@@ -421,6 +653,48 @@ function renderRequestView(route, container) {
421
653
  // Setup initial remove buttons
422
654
  container.querySelectorAll('.remove-header').forEach(setupRemoveHeaderBtn);
423
655
 
656
+ // Source viewer fullscreen toggle
657
+ const fullscreenBtn = document.getElementById('btn-source-fullscreen');
658
+ if (fullscreenBtn) {
659
+ const updateFullscreenIcon = (isFullscreen) => {
660
+ const svg = fullscreenBtn.querySelector('svg');
661
+ if (isFullscreen) {
662
+ // Exit fullscreen icon (minimize)
663
+ svg.innerHTML = '<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"></path>';
664
+ } else {
665
+ // Enter fullscreen icon (maximize)
666
+ svg.innerHTML = '<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>';
667
+ }
668
+ };
669
+
670
+ fullscreenBtn.addEventListener('click', () => {
671
+ const sourceSection = container.querySelector('.source-section');
672
+ if (sourceSection) {
673
+ const isFullscreen = sourceSection.classList.toggle('fullscreen');
674
+ updateFullscreenIcon(isFullscreen);
675
+
676
+ // Update Monaco layout after transition
677
+ setTimeout(() => {
678
+ if (currentEditors.source) currentEditors.source.layout();
679
+ }, 300);
680
+ }
681
+ });
682
+
683
+ // ESC key to exit fullscreen
684
+ document.addEventListener('keydown', (e) => {
685
+ if (e.key === 'Escape') {
686
+ const sourceSection = container.querySelector('.source-section');
687
+ if (sourceSection && sourceSection.classList.contains('fullscreen')) {
688
+ sourceSection.classList.remove('fullscreen');
689
+ updateFullscreenIcon(false);
690
+ setTimeout(() => {
691
+ if (currentEditors.source) currentEditors.source.layout();
692
+ }, 300);
693
+ }
694
+ }
695
+ });
696
+ }
697
+
424
698
  // Populate Request Body if example exists
425
699
  if (hasBody && currentEditors.request) {
426
700
  currentEditors.request.setValue('{\n \n}');
@@ -429,6 +703,132 @@ function renderRequestView(route, container) {
429
703
  setupPanelResizer(container);
430
704
  }
431
705
 
706
+ const STORAGE_PREFIX = 'shokupan:explorer:';
707
+
708
+ function saveState(key, value) {
709
+ try {
710
+ localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(value));
711
+ } catch (e) {
712
+ console.warn('Failed to save state', e);
713
+ }
714
+ }
715
+
716
+ function getState(key, defaultValue) {
717
+ try {
718
+ const item = localStorage.getItem(STORAGE_PREFIX + key);
719
+ return item ? JSON.parse(item) : defaultValue;
720
+ } catch (e) {
721
+ return defaultValue;
722
+ }
723
+ }
724
+
725
+ function setupSidebar() {
726
+ const sidebar = document.querySelector('.sidebar');
727
+ const content = document.querySelector('.content');
728
+ if (!sidebar) return;
729
+
730
+ // Restore state
731
+ const savedWidth = getState('sidebar_width', 300);
732
+ const isCollapsed = getState('sidebar_collapsed', false);
733
+
734
+ if (savedWidth) sidebar.style.width = `${savedWidth}px`;
735
+ if (isCollapsed) {
736
+ sidebar.classList.add('collapsed');
737
+ content.classList.add('no-sidebar');
738
+ }
739
+
740
+ // 1. Toggle Sidebar Logic
741
+ const toggleBtn = document.querySelector('.toggle-sidebar');
742
+ const expandBtn = document.querySelector('.sidebar-collapse-trigger');
743
+
744
+ const toggleSidebar = (collapse) => {
745
+ if (collapse) {
746
+ sidebar.classList.add('collapsed');
747
+ content.classList.add('no-sidebar');
748
+ saveState('sidebar_collapsed', true);
749
+ } else {
750
+ sidebar.classList.remove('collapsed');
751
+ content.classList.remove('no-sidebar');
752
+ saveState('sidebar_collapsed', false);
753
+ }
754
+ // Trigger resize for editors
755
+ setTimeout(() => {
756
+ Object.values(currentEditors).forEach(editor => editor && editor.layout());
757
+ }, 300);
758
+ };
759
+
760
+ if (toggleBtn) {
761
+ toggleBtn.addEventListener('click', () => toggleSidebar(true));
762
+ }
763
+
764
+ if (expandBtn) {
765
+ expandBtn.addEventListener('click', () => toggleSidebar(false));
766
+ }
767
+
768
+ // 2. Resize Logic
769
+ setupSidebarResizer(sidebar);
770
+
771
+ // 3. Collapsible Groups (top-level)
772
+ document.querySelectorAll('.nav-group-title').forEach(title => {
773
+ title.addEventListener('click', (e) => {
774
+ const group = e.currentTarget.parentElement;
775
+ group.classList.toggle('collapsed');
776
+ });
777
+ });
778
+
779
+ // 4. Collapsible Subgroups (nested)
780
+ document.querySelectorAll('.nav-subgroup-title').forEach(title => {
781
+ title.addEventListener('click', (e) => {
782
+ e.stopPropagation(); // Prevent bubbling to parent group
783
+ const subgroup = e.currentTarget.parentElement;
784
+ subgroup.classList.toggle('collapsed');
785
+ });
786
+ });
787
+ }
788
+
789
+ function setupSidebarResizer(sidebar) {
790
+ const resizer = sidebar.querySelector('.resize-handle');
791
+ if (!resizer) return;
792
+
793
+ let isResizing = false;
794
+ let startX, startWidth;
795
+
796
+ resizer.addEventListener('mousedown', (e) => {
797
+ isResizing = true;
798
+ startX = e.clientX;
799
+ startWidth = sidebar.getBoundingClientRect().width;
800
+
801
+ sidebar.classList.add('resizing'); // Disable transition
802
+ document.body.style.cursor = 'col-resize';
803
+ document.body.style.userSelect = 'none';
804
+ e.preventDefault();
805
+ });
806
+
807
+ document.addEventListener('mousemove', (e) => {
808
+ if (!isResizing) return;
809
+
810
+ const newWidth = startWidth + (e.clientX - startX);
811
+ // Clean constraints: min 150px, max 800px
812
+ const clamped = Math.max(150, Math.min(800, newWidth));
813
+
814
+ sidebar.style.width = `${clamped}px`;
815
+ });
816
+
817
+ document.addEventListener('mouseup', () => {
818
+ if (isResizing) {
819
+ isResizing = false;
820
+ sidebar.classList.remove('resizing');
821
+ document.body.style.cursor = '';
822
+ document.body.style.userSelect = '';
823
+
824
+ saveState('sidebar_width', sidebar.getBoundingClientRect().width);
825
+
826
+ // Layout editors
827
+ Object.values(currentEditors).forEach(editor => editor && editor.layout());
828
+ }
829
+ });
830
+ }
831
+
432
832
  function setupPanelResizer(container) {
433
833
  const resizer = container.querySelector('.panel-resizer');
434
834
  const topPanel = container.querySelector('.request-panel');
@@ -436,6 +836,10 @@ function setupPanelResizer(container) {
436
836
 
437
837
  if (!resizer || !topPanel || !bottomPanel) return;
438
838
 
839
+ // Restore state
840
+ const savedSplit = getState('panel_split', 50); // Default 50%
841
+ topPanel.style.flex = `0 0 ${savedSplit}%`;
842
+
439
843
  let isResizing = false;
440
844
 
441
845
  resizer.addEventListener('mousedown', (e) => {
@@ -470,6 +874,13 @@ function setupPanelResizer(container) {
470
874
  document.body.style.cursor = '';
471
875
  document.body.style.userSelect = '';
472
876
 
877
+ // Save state
878
+ const currentFlex = topPanel.style.flex;
879
+ const match = currentFlex.match(/([\d.]+)%/);
880
+ if (match) {
881
+ saveState('panel_split', parseFloat(match[1]));
882
+ }
883
+
473
884
  // Trigger monaco layout as size changed
474
885
  if (currentEditors.request) currentEditors.request.layout();
475
886
  if (currentEditors.response) currentEditors.response.layout();
@@ -562,11 +973,12 @@ function initMonaco() {
562
973
  const endLine = highlights[1] || startLine;
563
974
 
564
975
  if (startLine > 0) {
976
+ const highlightClass = sourceInfo.isRuntime ? 'closure-highlight-dynamic' : 'closure-highlight';
565
977
  decorations.push({
566
978
  range: new monaco.Range(startLine, 1, endLine, 1),
567
979
  options: {
568
980
  isWholeLine: true,
569
- className: 'closure-highlight'
981
+ className: highlightClass
570
982
  }
571
983
  });
572
984
  currentEditors.source.revealLineInCenter(startLine);
@@ -581,6 +993,7 @@ function initMonaco() {
581
993
  if (h.type === 'emit') className = 'emit-highlight';
582
994
  else if (h.type === 'return-success') className = 'success-line-highlight';
583
995
  else if (h.type === 'return-warning') className = 'warning-line-highlight';
996
+ else if (h.type === 'dynamic-path') className = 'closure-highlight-dynamic';
584
997
  // Fallback for older 'return' type if any mixed
585
998
  else if (h.type === 'return') className = 'warning-line-highlight';
586
999
 
@@ -857,24 +1270,4 @@ function parseMarkdown(text) {
857
1270
  return marked.parse(text, { renderer });
858
1271
  }
859
1272
 
860
- function setupSidebar() {
861
- const sidebar = document.querySelector('.sidebar');
862
- if (!sidebar) return;
863
-
864
- // Collapsible Groups (top-level)
865
- document.querySelectorAll('.nav-group-title').forEach(title => {
866
- title.addEventListener('click', (e) => {
867
- const group = e.currentTarget.parentElement;
868
- group.classList.toggle('collapsed');
869
- });
870
- });
871
1273
 
872
- // Collapsible Subgroups (nested)
873
- document.querySelectorAll('.nav-subgroup-title').forEach(title => {
874
- title.addEventListener('click', (e) => {
875
- e.stopPropagation(); // Prevent bubbling to parent group
876
- const subgroup = e.currentTarget.parentElement;
877
- subgroup.classList.toggle('collapsed');
878
- });
879
- });
880
- }