shokupan 0.10.5 → 0.12.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 (73) hide show
  1. package/README.md +46 -1815
  2. package/dist/{analyzer-BqIe1p0R.js → analyzer-BkNQHWj4.js} +3 -8
  3. package/dist/{analyzer-BqIe1p0R.js.map → analyzer-BkNQHWj4.js.map} +1 -1
  4. package/dist/{analyzer-CKLGLFtx.cjs → analyzer-DM-OlRq8.cjs} +2 -7
  5. package/dist/{analyzer-CKLGLFtx.cjs.map → analyzer-DM-OlRq8.cjs.map} +1 -1
  6. package/dist/{analyzer.impl-D9Yi1Hax.cjs → analyzer.impl-CVJ8zfGQ.cjs} +596 -42
  7. package/dist/analyzer.impl-CVJ8zfGQ.cjs.map +1 -0
  8. package/dist/{analyzer.impl-CV6W1Eq7.js → analyzer.impl-CsA1bS_s.js} +596 -42
  9. package/dist/analyzer.impl-CsA1bS_s.js.map +1 -0
  10. package/dist/cli.cjs +206 -18
  11. package/dist/cli.cjs.map +1 -1
  12. package/dist/cli.js +206 -18
  13. package/dist/cli.js.map +1 -1
  14. package/dist/context.d.ts +46 -9
  15. package/dist/index.cjs +3239 -1173
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +3236 -1171
  18. package/dist/index.js.map +1 -1
  19. package/dist/plugins/application/api-explorer/static/explorer-client.mjs +375 -29
  20. package/dist/plugins/application/api-explorer/static/style.css +327 -8
  21. package/dist/plugins/application/api-explorer/static/theme.css +11 -2
  22. package/dist/plugins/application/asyncapi/generator.d.ts +4 -0
  23. package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +154 -22
  24. package/dist/plugins/application/asyncapi/static/style.css +24 -8
  25. package/dist/plugins/application/auth.d.ts +5 -0
  26. package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +119 -0
  27. package/dist/plugins/application/dashboard/metrics-collector.d.ts +38 -2
  28. package/dist/plugins/application/dashboard/plugin.d.ts +53 -1
  29. package/dist/plugins/application/dashboard/static/charts.js +127 -62
  30. package/dist/plugins/application/dashboard/static/client.js +160 -0
  31. package/dist/plugins/application/dashboard/static/graph.mjs +167 -56
  32. package/dist/plugins/application/dashboard/static/reactflow.css +20 -10
  33. package/dist/plugins/application/dashboard/static/registry.js +112 -8
  34. package/dist/plugins/application/dashboard/static/requests.js +1167 -71
  35. package/dist/plugins/application/dashboard/static/styles.css +186 -14
  36. package/dist/plugins/application/dashboard/static/tabs.js +44 -9
  37. package/dist/plugins/application/dashboard/static/tabulator.css +23 -3
  38. package/dist/plugins/application/dashboard/static/theme.css +11 -2
  39. package/dist/plugins/application/mcp-server/plugin.d.ts +39 -0
  40. package/dist/plugins/application/openapi/analyzer.impl.d.ts +65 -1
  41. package/dist/plugins/application/openapi/openapi.d.ts +3 -0
  42. package/dist/plugins/application/shared/ast-utils.d.ts +7 -0
  43. package/dist/plugins/middleware/compression.d.ts +12 -2
  44. package/dist/plugins/middleware/rate-limit.d.ts +5 -0
  45. package/dist/router.d.ts +59 -19
  46. package/dist/server.d.ts +22 -0
  47. package/dist/shokupan.d.ts +31 -3
  48. package/dist/util/adapter/bun.d.ts +8 -0
  49. package/dist/util/adapter/filesystem.d.ts +20 -0
  50. package/dist/util/adapter/index.d.ts +4 -0
  51. package/dist/util/adapter/interface.d.ts +12 -0
  52. package/dist/util/adapter/node.d.ts +8 -0
  53. package/dist/util/adapter/wintercg.d.ts +5 -0
  54. package/dist/util/body-parser.d.ts +30 -0
  55. package/dist/util/controller-scanner.d.ts +4 -0
  56. package/dist/util/cpu-monitor.d.ts +2 -0
  57. package/dist/util/decorators.d.ts +20 -3
  58. package/dist/util/di.d.ts +3 -8
  59. package/dist/util/metadata.d.ts +18 -0
  60. package/dist/util/middleware-tracker.d.ts +10 -0
  61. package/dist/util/request.d.ts +1 -0
  62. package/dist/util/symbol.d.ts +1 -0
  63. package/dist/util/types.d.ts +167 -1
  64. package/package.json +7 -5
  65. package/dist/analyzer.impl-CV6W1Eq7.js.map +0 -1
  66. package/dist/analyzer.impl-D9Yi1Hax.cjs.map +0 -1
  67. package/dist/http-server-BEMPIs33.cjs +0 -85
  68. package/dist/http-server-BEMPIs33.cjs.map +0 -1
  69. package/dist/http-server-CCeagTyU.js +0 -68
  70. package/dist/http-server-CCeagTyU.js.map +0 -1
  71. package/dist/plugins/application/dashboard/static/failures.js +0 -85
  72. package/dist/plugins/application/dashboard/static/poll.js +0 -146
  73. package/dist/plugins/application/http-server.d.ts +0 -13
@@ -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>
@@ -476,6 +703,132 @@ function renderRequestView(route, container) {
476
703
  setupPanelResizer(container);
477
704
  }
478
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
+
479
832
  function setupPanelResizer(container) {
480
833
  const resizer = container.querySelector('.panel-resizer');
481
834
  const topPanel = container.querySelector('.request-panel');
@@ -483,6 +836,10 @@ function setupPanelResizer(container) {
483
836
 
484
837
  if (!resizer || !topPanel || !bottomPanel) return;
485
838
 
839
+ // Restore state
840
+ const savedSplit = getState('panel_split', 50); // Default 50%
841
+ topPanel.style.flex = `0 0 ${savedSplit}%`;
842
+
486
843
  let isResizing = false;
487
844
 
488
845
  resizer.addEventListener('mousedown', (e) => {
@@ -517,6 +874,13 @@ function setupPanelResizer(container) {
517
874
  document.body.style.cursor = '';
518
875
  document.body.style.userSelect = '';
519
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
+
520
884
  // Trigger monaco layout as size changed
521
885
  if (currentEditors.request) currentEditors.request.layout();
522
886
  if (currentEditors.response) currentEditors.response.layout();
@@ -609,11 +973,12 @@ function initMonaco() {
609
973
  const endLine = highlights[1] || startLine;
610
974
 
611
975
  if (startLine > 0) {
976
+ const highlightClass = sourceInfo.isRuntime ? 'closure-highlight-dynamic' : 'closure-highlight';
612
977
  decorations.push({
613
978
  range: new monaco.Range(startLine, 1, endLine, 1),
614
979
  options: {
615
980
  isWholeLine: true,
616
- className: 'closure-highlight'
981
+ className: highlightClass
617
982
  }
618
983
  });
619
984
  currentEditors.source.revealLineInCenter(startLine);
@@ -628,6 +993,7 @@ function initMonaco() {
628
993
  if (h.type === 'emit') className = 'emit-highlight';
629
994
  else if (h.type === 'return-success') className = 'success-line-highlight';
630
995
  else if (h.type === 'return-warning') className = 'warning-line-highlight';
996
+ else if (h.type === 'dynamic-path') className = 'closure-highlight-dynamic';
631
997
  // Fallback for older 'return' type if any mixed
632
998
  else if (h.type === 'return') className = 'warning-line-highlight';
633
999
 
@@ -904,24 +1270,4 @@ function parseMarkdown(text) {
904
1270
  return marked.parse(text, { renderer });
905
1271
  }
906
1272
 
907
- function setupSidebar() {
908
- const sidebar = document.querySelector('.sidebar');
909
- if (!sidebar) return;
910
1273
 
911
- // Collapsible Groups (top-level)
912
- document.querySelectorAll('.nav-group-title').forEach(title => {
913
- title.addEventListener('click', (e) => {
914
- const group = e.currentTarget.parentElement;
915
- group.classList.toggle('collapsed');
916
- });
917
- });
918
-
919
- // Collapsible Subgroups (nested)
920
- document.querySelectorAll('.nav-subgroup-title').forEach(title => {
921
- title.addEventListener('click', (e) => {
922
- e.stopPropagation(); // Prevent bubbling to parent group
923
- const subgroup = e.currentTarget.parentElement;
924
- subgroup.classList.toggle('collapsed');
925
- });
926
- });
927
- }