tjs-lang 0.2.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 (91) hide show
  1. package/CONTEXT.md +594 -0
  2. package/LICENSE +190 -0
  3. package/README.md +220 -0
  4. package/bin/benchmarks.ts +351 -0
  5. package/bin/dev.ts +205 -0
  6. package/bin/docs.js +170 -0
  7. package/bin/install-cursor.sh +71 -0
  8. package/bin/install-vscode.sh +71 -0
  9. package/bin/select-local-models.d.ts +1 -0
  10. package/bin/select-local-models.js +28 -0
  11. package/bin/select-local-models.ts +31 -0
  12. package/demo/autocomplete.test.ts +232 -0
  13. package/demo/docs.json +186 -0
  14. package/demo/examples.test.ts +598 -0
  15. package/demo/index.html +91 -0
  16. package/demo/src/autocomplete.ts +482 -0
  17. package/demo/src/capabilities.ts +859 -0
  18. package/demo/src/demo-nav.ts +2097 -0
  19. package/demo/src/examples.test.ts +161 -0
  20. package/demo/src/examples.ts +476 -0
  21. package/demo/src/imports.test.ts +196 -0
  22. package/demo/src/imports.ts +421 -0
  23. package/demo/src/index.ts +639 -0
  24. package/demo/src/module-store.ts +635 -0
  25. package/demo/src/module-sw.ts +132 -0
  26. package/demo/src/playground.ts +949 -0
  27. package/demo/src/service-host.ts +389 -0
  28. package/demo/src/settings.ts +440 -0
  29. package/demo/src/style.ts +280 -0
  30. package/demo/src/tjs-playground.ts +1605 -0
  31. package/demo/src/ts-examples.ts +478 -0
  32. package/demo/src/ts-playground.ts +1092 -0
  33. package/demo/static/favicon.svg +30 -0
  34. package/demo/static/photo-1.jpg +0 -0
  35. package/demo/static/photo-2.jpg +0 -0
  36. package/demo/static/texts/ai-history.txt +9 -0
  37. package/demo/static/texts/coffee-origins.txt +9 -0
  38. package/demo/static/texts/renewable-energy.txt +9 -0
  39. package/dist/index.js +256 -0
  40. package/dist/index.js.map +37 -0
  41. package/dist/tjs-batteries.js +4 -0
  42. package/dist/tjs-batteries.js.map +15 -0
  43. package/dist/tjs-full.js +256 -0
  44. package/dist/tjs-full.js.map +37 -0
  45. package/dist/tjs-transpiler.js +220 -0
  46. package/dist/tjs-transpiler.js.map +21 -0
  47. package/dist/tjs-vm.js +4 -0
  48. package/dist/tjs-vm.js.map +14 -0
  49. package/docs/CNAME +1 -0
  50. package/docs/favicon.svg +30 -0
  51. package/docs/index.html +91 -0
  52. package/docs/index.js +10468 -0
  53. package/docs/index.js.map +92 -0
  54. package/docs/photo-1.jpg +0 -0
  55. package/docs/photo-1.webp +0 -0
  56. package/docs/photo-2.jpg +0 -0
  57. package/docs/photo-2.webp +0 -0
  58. package/docs/texts/ai-history.txt +9 -0
  59. package/docs/texts/coffee-origins.txt +9 -0
  60. package/docs/texts/renewable-energy.txt +9 -0
  61. package/docs/tjs-lang.svg +31 -0
  62. package/docs/tosijs-agent.svg +31 -0
  63. package/editors/README.md +325 -0
  64. package/editors/ace/ajs-mode.js +328 -0
  65. package/editors/ace/ajs-mode.ts +269 -0
  66. package/editors/ajs-syntax.ts +212 -0
  67. package/editors/build-grammars.ts +510 -0
  68. package/editors/codemirror/ajs-language.js +287 -0
  69. package/editors/codemirror/ajs-language.ts +1447 -0
  70. package/editors/codemirror/autocomplete.test.ts +531 -0
  71. package/editors/codemirror/component.ts +404 -0
  72. package/editors/monaco/ajs-monarch.js +243 -0
  73. package/editors/monaco/ajs-monarch.ts +225 -0
  74. package/editors/tjs-syntax.ts +115 -0
  75. package/editors/vscode/language-configuration.json +37 -0
  76. package/editors/vscode/package.json +65 -0
  77. package/editors/vscode/syntaxes/ajs-injection.tmLanguage.json +107 -0
  78. package/editors/vscode/syntaxes/ajs.tmLanguage.json +252 -0
  79. package/editors/vscode/syntaxes/tjs.tmLanguage.json +333 -0
  80. package/package.json +83 -0
  81. package/src/cli/commands/check.ts +41 -0
  82. package/src/cli/commands/convert.ts +133 -0
  83. package/src/cli/commands/emit.ts +260 -0
  84. package/src/cli/commands/run.ts +68 -0
  85. package/src/cli/commands/test.ts +194 -0
  86. package/src/cli/commands/types.ts +20 -0
  87. package/src/cli/create-app.ts +236 -0
  88. package/src/cli/playground.ts +250 -0
  89. package/src/cli/tjs.ts +166 -0
  90. package/src/cli/tjsx.ts +160 -0
  91. package/tjs-lang.svg +31 -0
@@ -0,0 +1,949 @@
1
+ /*
2
+ * playground.ts - Interactive AsyncJS playground component
3
+ */
4
+
5
+ import { elements, Component, PartsMap } from 'tosijs'
6
+
7
+ import { icons } from 'tosijs-ui'
8
+
9
+ import { EditorView, basicSetup } from 'codemirror'
10
+ import { EditorState, Compartment } from '@codemirror/state'
11
+ import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'
12
+ import { oneDark } from '@codemirror/theme-one-dark'
13
+ import { ajsEditorExtension } from '../../editors/codemirror/ajs-language'
14
+
15
+ import { examples, type Example } from './examples'
16
+ import {
17
+ AgentVM,
18
+ transpile,
19
+ type TranspileResult,
20
+ coreAtoms,
21
+ batteryAtoms,
22
+ } from '../../src'
23
+ import { getStoreCapabilityDefault } from '../../src/batteries'
24
+ import {
25
+ buildLLMCapability,
26
+ buildLLMBattery,
27
+ getSettings,
28
+ type LLMProvider,
29
+ } from './capabilities'
30
+
31
+ // Default LM Studio URL
32
+ const DEFAULT_LM_STUDIO_URL = 'http://localhost:1234/v1'
33
+
34
+ // Initialize default LM Studio URL on HTTP if not already set
35
+ function initLLMDefaults() {
36
+ const existingUrl = localStorage.getItem('customLlmUrl')
37
+ const hasExistingUrl = existingUrl && existingUrl.trim() !== ''
38
+
39
+ // On HTTP, default to LM Studio URL if nothing is configured
40
+ if (!hasExistingUrl && window.location.protocol === 'http:') {
41
+ localStorage.setItem('customLlmUrl', DEFAULT_LM_STUDIO_URL)
42
+ console.log('🤖 Defaulting to LM Studio endpoint:', DEFAULT_LM_STUDIO_URL)
43
+ }
44
+ }
45
+
46
+ // Set defaults on module load
47
+ initLLMDefaults()
48
+
49
+ const { div, button, span, select, option, optgroup, input } = elements
50
+
51
+ // localStorage key for custom examples
52
+ const STORAGE_KEY = 'agent-playground-examples'
53
+
54
+ // Default code for new examples
55
+ const NEW_EXAMPLE_CODE = `// Write your AsyncJS code here
56
+ function myFunction({ name = 'World' }) {
57
+ let message = template({ tmpl: 'Hello, {{name}}!', vars: { name } })
58
+ return { message }
59
+ }`
60
+
61
+ // Load custom examples from localStorage
62
+ function loadCustomExamples(): Example[] {
63
+ try {
64
+ const stored = localStorage.getItem(STORAGE_KEY)
65
+ return stored ? JSON.parse(stored) : []
66
+ } catch {
67
+ return []
68
+ }
69
+ }
70
+
71
+ // Save custom examples to localStorage
72
+ function saveCustomExamples(customExamples: Example[]): void {
73
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(customExamples))
74
+ }
75
+
76
+ // Parts interface for typed access
77
+ interface PlaygroundParts extends PartsMap {
78
+ editorContainer: HTMLElement
79
+ resultContainer: HTMLElement
80
+ statusBar: HTMLElement
81
+ tabResult: HTMLButtonElement
82
+ tabAst: HTMLButtonElement
83
+ tabTrace: HTMLButtonElement
84
+ copyBtn: HTMLButtonElement
85
+ runBtn: HTMLButtonElement
86
+ newBtn: HTMLButtonElement
87
+ saveBtn: HTMLButtonElement
88
+ deleteBtn: HTMLButtonElement
89
+ exampleSelect: HTMLSelectElement
90
+ }
91
+
92
+ export class Playground extends Component<PlaygroundParts> {
93
+ private editor: EditorView | null = null
94
+ private themeCompartment = new Compartment()
95
+ private darkModeObserver: MutationObserver | null = null
96
+ private vm = new AgentVM({ ...coreAtoms, ...batteryAtoms })
97
+ private currentTab = 'result'
98
+ private lastResult: any = null
99
+ private lastAst: TranspileResult | null = null
100
+ private lastError: string | null = null
101
+ private isRunning = false
102
+ private customExamples: Example[] = loadCustomExamples()
103
+ private currentExampleIndex: number = -1 // -1 = new/unsaved
104
+
105
+ // Use Shadow DOM styles (static styleSpec)
106
+ // CSS variables for theming
107
+ static styleSpec = {
108
+ ':host': {
109
+ display: 'block',
110
+ height: '100%',
111
+ position: 'relative',
112
+ // Light mode defaults
113
+ '--pg-bg': '#f3f4f6',
114
+ '--pg-border': '#e5e7eb',
115
+ '--pg-text': '#1f2937',
116
+ '--pg-brand': '#3d4a6b',
117
+ '--pg-success': '#16a34a',
118
+ '--pg-error': '#dc2626',
119
+ },
120
+
121
+ // Dark mode - detect from parent
122
+ ':host-context(.darkmode)': {
123
+ '--pg-bg': '#1f2937',
124
+ '--pg-border': '#374151',
125
+ '--pg-text': '#f3f4f6',
126
+ '--pg-brand': '#818cf8',
127
+ '--pg-success': '#4ade80',
128
+ '--pg-error': '#f87171',
129
+ },
130
+
131
+ '.playground': {
132
+ display: 'flex',
133
+ flexDirection: 'column',
134
+ height: '100%',
135
+ gap: '10px',
136
+ color: 'var(--pg-text)',
137
+ },
138
+
139
+ '.playground-toolbar': {
140
+ display: 'flex',
141
+ alignItems: 'center',
142
+ gap: '10px',
143
+ padding: '10px',
144
+ background: 'var(--pg-bg)',
145
+ borderRadius: '6px',
146
+ flexWrap: 'wrap',
147
+ },
148
+
149
+ '.run-btn': {
150
+ display: 'flex',
151
+ alignItems: 'center',
152
+ gap: '4px',
153
+ padding: '6px 12px',
154
+ background: 'var(--pg-brand)',
155
+ color: 'white',
156
+ border: 'none',
157
+ borderRadius: '6px',
158
+ cursor: 'pointer',
159
+ fontWeight: '500',
160
+ },
161
+
162
+ '.iconic': {
163
+ background: 'none',
164
+ border: 'none',
165
+ cursor: 'pointer',
166
+ padding: '5px',
167
+ borderRadius: '6px',
168
+ color: 'var(--pg-text)',
169
+ display: 'flex',
170
+ alignItems: 'center',
171
+ justifyContent: 'center',
172
+ transition: 'background 0.15s',
173
+ },
174
+
175
+ '.iconic:hover': {
176
+ background: 'rgba(99, 102, 241, 0.15)',
177
+ },
178
+
179
+ '.playground-main': {
180
+ display: 'flex',
181
+ flex: '1 1 auto',
182
+ gap: '10px',
183
+ minHeight: 0,
184
+ },
185
+
186
+ '.playground-editor': {
187
+ flex: '1 1 50%',
188
+ minWidth: '300px',
189
+ minHeight: '300px',
190
+ border: '1px solid var(--pg-border)',
191
+ borderRadius: '6px',
192
+ overflow: 'hidden',
193
+ position: 'relative',
194
+ background: 'var(--pg-bg)',
195
+ },
196
+
197
+ // Critical CodeMirror styles
198
+ '.playground-editor .cm-editor': {
199
+ height: '100%',
200
+ position: 'absolute',
201
+ top: 0,
202
+ left: 0,
203
+ right: 0,
204
+ bottom: 0,
205
+ },
206
+
207
+ '.playground-editor .cm-scroller': {
208
+ outline: 'none',
209
+ fontFamily: "Menlo, Monaco, Consolas, 'Courier New', monospace",
210
+ },
211
+
212
+ '.playground-output': {
213
+ flex: '1 1 50%',
214
+ display: 'flex',
215
+ flexDirection: 'column',
216
+ minWidth: '300px',
217
+ border: '1px solid var(--pg-border)',
218
+ borderRadius: '6px',
219
+ overflow: 'hidden',
220
+ background: 'var(--pg-bg)',
221
+ },
222
+
223
+ '.playground-tabs': {
224
+ display: 'flex',
225
+ background: 'var(--pg-bg)',
226
+ borderBottom: '1px solid var(--pg-border)',
227
+ alignItems: 'center',
228
+ },
229
+
230
+ '.playground-tabs .elastic': {
231
+ flex: '1 1 auto',
232
+ },
233
+
234
+ '.copy-btn': {
235
+ padding: '4px 8px',
236
+ marginRight: '4px',
237
+ border: 'none',
238
+ background: 'transparent',
239
+ cursor: 'pointer',
240
+ color: 'var(--pg-text)',
241
+ opacity: 0.6,
242
+ transition: 'opacity 0.15s',
243
+ },
244
+
245
+ '.copy-btn:hover': {
246
+ opacity: 1,
247
+ },
248
+
249
+ '.copy-btn.copied': {
250
+ color: 'var(--pg-success)',
251
+ },
252
+
253
+ '.playground-tab': {
254
+ padding: '5px 10px',
255
+ border: 'none',
256
+ background: 'transparent',
257
+ cursor: 'pointer',
258
+ color: 'var(--pg-text)',
259
+ opacity: 0.7,
260
+ transition: 'opacity 0.15s, background 0.15s',
261
+ },
262
+
263
+ '.playground-tab:hover': {
264
+ opacity: 1,
265
+ background: 'rgba(99, 102, 241, 0.1)',
266
+ },
267
+
268
+ '.playground-tab.active': {
269
+ opacity: 1,
270
+ borderBottom: '2px solid var(--pg-brand)',
271
+ marginBottom: '-1px',
272
+ },
273
+
274
+ '.playground-result': {
275
+ flex: '1 1 auto',
276
+ overflow: 'auto',
277
+ padding: '10px',
278
+ fontFamily: "'SF Mono', Monaco, 'Cascadia Code', Consolas, monospace",
279
+ fontSize: '13px',
280
+ whiteSpace: 'pre-wrap',
281
+ wordBreak: 'break-word',
282
+ color: 'var(--pg-text)',
283
+ background: 'var(--pg-bg)',
284
+ },
285
+
286
+ '.playground-result.error': {
287
+ color: 'var(--pg-error)',
288
+ },
289
+
290
+ '.playground-result.success': {
291
+ color: 'var(--pg-success)',
292
+ },
293
+
294
+ '.loading-spinner': {
295
+ display: 'inline-block',
296
+ width: '14px',
297
+ height: '14px',
298
+ border: '2px solid var(--pg-border)',
299
+ borderTopColor: 'var(--pg-brand)',
300
+ borderRadius: '50%',
301
+ animation: 'spin 0.8s linear infinite',
302
+ verticalAlign: 'middle',
303
+ marginRight: '8px',
304
+ },
305
+
306
+ '@keyframes spin': {
307
+ to: { transform: 'rotate(360deg)' },
308
+ },
309
+
310
+ '.playground-status': {
311
+ padding: '5px 10px',
312
+ background: 'var(--pg-bg)',
313
+ borderTop: '1px solid var(--pg-border)',
314
+ fontSize: '12px',
315
+ opacity: 0.7,
316
+ },
317
+
318
+ // Select dropdown styling
319
+ select: {
320
+ padding: '4px 8px',
321
+ borderRadius: '4px',
322
+ border: '1px solid var(--pg-border)',
323
+ background: 'var(--pg-bg)',
324
+ color: 'var(--pg-text)',
325
+ },
326
+
327
+ '@media (max-width: 768px)': {
328
+ '.playground-main': {
329
+ flexDirection: 'column',
330
+ },
331
+ '.playground-editor, .playground-output': {
332
+ flex: '1 1 auto',
333
+ minHeight: '250px',
334
+ },
335
+ },
336
+ }
337
+
338
+ content = () => [
339
+ div(
340
+ { class: 'playground' },
341
+ // Toolbar
342
+ div(
343
+ { class: 'playground-toolbar' },
344
+ button(
345
+ {
346
+ part: 'runBtn',
347
+ class: 'run-btn',
348
+ },
349
+ icons.play({ size: 16 }),
350
+ 'Run'
351
+ ),
352
+
353
+ button(
354
+ {
355
+ part: 'newBtn',
356
+ class: 'iconic',
357
+ title: 'New',
358
+ },
359
+ icons.plus()
360
+ ),
361
+
362
+ button(
363
+ {
364
+ part: 'saveBtn',
365
+ class: 'iconic',
366
+ title: 'Save Example',
367
+ },
368
+ icons.save()
369
+ ),
370
+
371
+ button(
372
+ {
373
+ part: 'deleteBtn',
374
+ class: 'iconic',
375
+ title: 'Delete Custom Example',
376
+ style: { display: 'none' },
377
+ },
378
+ icons.trash()
379
+ ),
380
+
381
+ span({ style: { flex: '1' } }),
382
+
383
+ select(
384
+ {
385
+ part: 'exampleSelect',
386
+ style: { padding: '4px 8px', borderRadius: '4px' },
387
+ },
388
+ option({ value: 'new' }, '-- New --')
389
+ )
390
+ ),
391
+
392
+ // Main area
393
+ div(
394
+ { class: 'playground-main' },
395
+ div({ part: 'editorContainer', class: 'playground-editor' }),
396
+
397
+ div(
398
+ { class: 'playground-output' },
399
+ div(
400
+ { class: 'playground-tabs' },
401
+ button(
402
+ { part: 'tabResult', class: 'playground-tab active' },
403
+ 'Result'
404
+ ),
405
+ button({ part: 'tabAst', class: 'playground-tab' }, 'AST'),
406
+ button({ part: 'tabTrace', class: 'playground-tab' }, 'Trace'),
407
+ span({ class: 'elastic' }),
408
+ button(
409
+ {
410
+ part: 'copyBtn',
411
+ class: 'copy-btn',
412
+ title: 'Copy to clipboard',
413
+ },
414
+ icons.copy({ size: 16 })
415
+ )
416
+ ),
417
+ div(
418
+ { part: 'resultContainer', class: 'playground-result' },
419
+ '// Run code to see results'
420
+ ),
421
+ div({ part: 'statusBar', class: 'playground-status' }, 'Ready')
422
+ )
423
+ )
424
+ ),
425
+ ]
426
+
427
+ connectedCallback() {
428
+ super.connectedCallback()
429
+
430
+ // Bind event handlers manually for Shadow DOM
431
+ this.parts.runBtn.addEventListener('click', this.runCode)
432
+ this.parts.newBtn.addEventListener('click', this.newExample)
433
+ this.parts.saveBtn.addEventListener('click', this.saveExample)
434
+ this.parts.deleteBtn.addEventListener('click', this.deleteExample)
435
+ this.parts.exampleSelect.addEventListener('change', this.loadExample)
436
+ this.parts.tabResult.addEventListener('click', () =>
437
+ this.switchTab('result')
438
+ )
439
+ this.parts.tabAst.addEventListener('click', () => this.switchTab('ast'))
440
+ this.parts.tabTrace.addEventListener('click', () => this.switchTab('trace'))
441
+ this.parts.copyBtn.addEventListener('click', this.copyOutput)
442
+
443
+ // Listen for hash changes
444
+ window.addEventListener('hashchange', this.handleHashChange)
445
+
446
+ // Populate the example select BEFORE initEditor (so select value can be set)
447
+ this.rebuildExampleSelect()
448
+
449
+ // Initialize CodeMirror after hydration
450
+ this.initEditor()
451
+ }
452
+
453
+ disconnectedCallback() {
454
+ window.removeEventListener('hashchange', this.handleHashChange)
455
+ this.darkModeObserver?.disconnect()
456
+ }
457
+
458
+ // Get all examples (built-in + custom)
459
+ getAllExamples(): Example[] {
460
+ return [...examples, ...this.customExamples]
461
+ }
462
+
463
+ // Rebuild the example select dropdown
464
+ rebuildExampleSelect() {
465
+ const sel = this.parts.exampleSelect
466
+ sel.innerHTML = ''
467
+
468
+ // New option
469
+ sel.appendChild(option({ value: 'new' }, '-- New --'))
470
+
471
+ // Built-in examples
472
+ const builtInGroup = optgroup({ label: 'Built-in Examples' })
473
+ examples.forEach((ex, i) => {
474
+ builtInGroup.appendChild(
475
+ option(
476
+ { value: `builtin:${i}` },
477
+ ex.requiresApi ? `${ex.name} 🔑` : ex.name
478
+ )
479
+ )
480
+ })
481
+ sel.appendChild(builtInGroup)
482
+
483
+ // Custom examples (if any)
484
+ if (this.customExamples.length > 0) {
485
+ const customGroup = optgroup({ label: 'My Examples' })
486
+ this.customExamples.forEach((ex, i) => {
487
+ customGroup.appendChild(option({ value: `custom:${i}` }, ex.name))
488
+ })
489
+ sel.appendChild(customGroup)
490
+ }
491
+ }
492
+
493
+ // Get example index from hash (e.g., #example=2 or #example=Hello%20World)
494
+ getExampleFromHash(): { type: 'new' | 'builtin' | 'custom'; index: number } {
495
+ const hash = window.location.hash.slice(1)
496
+ const params = new URLSearchParams(hash)
497
+ const value = params.get('example')
498
+ if (value === null || value === 'new') return { type: 'new', index: -1 }
499
+
500
+ // Try matching by name in built-in examples
501
+ const decodedName = decodeURIComponent(value).toLowerCase()
502
+ const builtinIdx = examples.findIndex(
503
+ (ex) => ex.name.toLowerCase() === decodedName
504
+ )
505
+ if (builtinIdx >= 0) return { type: 'builtin', index: builtinIdx }
506
+
507
+ // Try matching in custom examples
508
+ const customIdx = this.customExamples.findIndex(
509
+ (ex) => ex.name.toLowerCase() === decodedName
510
+ )
511
+ if (customIdx >= 0) return { type: 'custom', index: customIdx }
512
+
513
+ return { type: 'builtin', index: 0 }
514
+ }
515
+
516
+ // Update hash when example changes
517
+ setHashForExample(type: 'new' | 'builtin' | 'custom', idx: number) {
518
+ if (type === 'new') {
519
+ history.replaceState(null, '', '#example=new')
520
+ return
521
+ }
522
+ const allExamples = type === 'builtin' ? examples : this.customExamples
523
+ const example = allExamples[idx]
524
+ if (example) {
525
+ const hash = `example=${encodeURIComponent(example.name)}`
526
+ history.replaceState(null, '', `#${hash}`)
527
+ }
528
+ }
529
+
530
+ handleHashChange = () => {
531
+ const { type, index } = this.getExampleFromHash()
532
+ this.loadExampleByTypeAndIndex(type, index)
533
+ }
534
+
535
+ private isDarkMode(): boolean {
536
+ return document.body.classList.contains('darkmode')
537
+ }
538
+
539
+ private getThemeExtension() {
540
+ return this.isDarkMode() ? oneDark : []
541
+ }
542
+
543
+ private updateEditorTheme() {
544
+ if (!this.editor) return
545
+ this.editor.dispatch({
546
+ effects: this.themeCompartment.reconfigure(this.getThemeExtension()),
547
+ })
548
+ }
549
+
550
+ initEditor() {
551
+ const container = this.parts.editorContainer
552
+ if (!container) return
553
+
554
+ const extensions = [
555
+ basicSetup,
556
+ syntaxHighlighting(defaultHighlightStyle),
557
+ ajsEditorExtension(),
558
+ this.themeCompartment.of(this.getThemeExtension()),
559
+ ]
560
+
561
+ // Get initial example from hash or default to first built-in
562
+ const { type, index } = this.getExampleFromHash()
563
+ let startDoc = NEW_EXAMPLE_CODE
564
+ if (type === 'builtin' && index >= 0) {
565
+ startDoc = examples[index]?.code || startDoc
566
+ } else if (type === 'custom' && index >= 0) {
567
+ startDoc = this.customExamples[index]?.code || startDoc
568
+ }
569
+
570
+ this.editor = new EditorView({
571
+ state: EditorState.create({
572
+ doc: startDoc,
573
+ extensions,
574
+ }),
575
+ parent: container,
576
+ })
577
+
578
+ // Watch for dark mode changes on body
579
+ this.darkModeObserver = new MutationObserver((mutations) => {
580
+ for (const mutation of mutations) {
581
+ if (mutation.attributeName === 'class') {
582
+ this.updateEditorTheme()
583
+ }
584
+ }
585
+ })
586
+ this.darkModeObserver.observe(document.body, { attributes: true })
587
+
588
+ // Update current example tracking
589
+ this.currentExampleIndex = index
590
+ this.updateDeleteButtonVisibility(type, index)
591
+
592
+ // Update select to match the loaded example
593
+ if (type === 'new') {
594
+ this.parts.exampleSelect.value = 'new'
595
+ } else if (type === 'builtin') {
596
+ this.parts.exampleSelect.value = `builtin:${index}`
597
+ } else if (type === 'custom') {
598
+ this.parts.exampleSelect.value = `custom:${index}`
599
+ }
600
+
601
+ // Set hash if not already set
602
+ if (!window.location.hash.includes('example=')) {
603
+ this.setHashForExample(type, index)
604
+ }
605
+ }
606
+
607
+ loadExampleByTypeAndIndex(type: 'new' | 'builtin' | 'custom', idx: number) {
608
+ if (!this.editor) return
609
+
610
+ let code = NEW_EXAMPLE_CODE
611
+ if (type === 'builtin' && idx >= 0 && idx < examples.length) {
612
+ code = examples[idx].code
613
+ } else if (
614
+ type === 'custom' &&
615
+ idx >= 0 &&
616
+ idx < this.customExamples.length
617
+ ) {
618
+ code = this.customExamples[idx].code
619
+ }
620
+
621
+ this.editor.dispatch({
622
+ changes: {
623
+ from: 0,
624
+ to: this.editor.state.doc.length,
625
+ insert: code,
626
+ },
627
+ })
628
+
629
+ this.currentExampleIndex = idx
630
+ this.updateDeleteButtonVisibility(type, idx)
631
+ this.setHashForExample(type, idx)
632
+
633
+ // Update select to match the loaded example
634
+ if (type === 'new') {
635
+ this.parts.exampleSelect.value = 'new'
636
+ } else if (type === 'builtin') {
637
+ this.parts.exampleSelect.value = `builtin:${idx}`
638
+ } else if (type === 'custom') {
639
+ this.parts.exampleSelect.value = `custom:${idx}`
640
+ }
641
+
642
+ // Clear results
643
+ this.parts.resultContainer.textContent = '// Run code to see results'
644
+ this.parts.statusBar.textContent = 'Ready'
645
+ }
646
+
647
+ updateDeleteButtonVisibility(
648
+ type: 'new' | 'builtin' | 'custom',
649
+ idx: number
650
+ ) {
651
+ // Only show delete button for custom examples
652
+ const showDelete = type === 'custom' && idx >= 0
653
+ this.parts.deleteBtn.style.display = showDelete ? '' : 'none'
654
+ }
655
+
656
+ loadExample = (e: Event) => {
657
+ const value = (e.target as HTMLSelectElement).value
658
+ if (value === 'new') {
659
+ this.loadExampleByTypeAndIndex('new', -1)
660
+ return
661
+ }
662
+
663
+ const [type, idxStr] = value.split(':')
664
+ const idx = parseInt(idxStr, 10)
665
+ if (type === 'builtin' || type === 'custom') {
666
+ this.loadExampleByTypeAndIndex(type, idx)
667
+ }
668
+ }
669
+
670
+ newExample = () => {
671
+ if (this.editor) {
672
+ this.editor.dispatch({
673
+ changes: {
674
+ from: 0,
675
+ to: this.editor.state.doc.length,
676
+ insert: NEW_EXAMPLE_CODE,
677
+ },
678
+ })
679
+ }
680
+ this.currentExampleIndex = -1
681
+ this.parts.exampleSelect.value = 'new'
682
+ this.updateDeleteButtonVisibility('new', -1)
683
+ this.setHashForExample('new', -1)
684
+ this.parts.resultContainer.textContent = '// Run code to see results'
685
+ this.parts.statusBar.textContent = 'Ready'
686
+ }
687
+
688
+ saveExample = () => {
689
+ if (!this.editor) return
690
+
691
+ const code = this.editor.state.doc.toString()
692
+ if (!code.trim()) {
693
+ alert('Cannot save empty example')
694
+ return
695
+ }
696
+
697
+ // Try to extract function name from code
698
+ const funcMatch = code.match(/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/)
699
+ const defaultName = funcMatch ? funcMatch[1] : 'My Example'
700
+
701
+ const name = prompt('Enter a name for this example:', defaultName)
702
+ if (!name) return
703
+
704
+ // Check for duplicate names
705
+ const existingIdx = this.customExamples.findIndex(
706
+ (ex) => ex.name.toLowerCase() === name.toLowerCase()
707
+ )
708
+
709
+ if (existingIdx >= 0) {
710
+ if (
711
+ !confirm(`An example named "${name}" already exists. Overwrite it?`)
712
+ ) {
713
+ return
714
+ }
715
+ this.customExamples[existingIdx] = { name, description: '', code }
716
+ } else {
717
+ this.customExamples.push({ name, description: '', code })
718
+ }
719
+
720
+ saveCustomExamples(this.customExamples)
721
+ this.rebuildExampleSelect()
722
+
723
+ // Select the saved example
724
+ const newIdx =
725
+ existingIdx >= 0 ? existingIdx : this.customExamples.length - 1
726
+ this.currentExampleIndex = newIdx
727
+ this.parts.exampleSelect.value = `custom:${newIdx}`
728
+ this.updateDeleteButtonVisibility('custom', newIdx)
729
+ this.setHashForExample('custom', newIdx)
730
+
731
+ this.parts.statusBar.textContent = `Saved "${name}"`
732
+ }
733
+
734
+ deleteExample = () => {
735
+ const value = this.parts.exampleSelect.value
736
+ if (!value.startsWith('custom:')) return
737
+
738
+ const idx = parseInt(value.split(':')[1], 10)
739
+ if (idx < 0 || idx >= this.customExamples.length) return
740
+
741
+ const name = this.customExamples[idx].name
742
+ if (!confirm(`Delete "${name}"?`)) return
743
+
744
+ this.customExamples.splice(idx, 1)
745
+ saveCustomExamples(this.customExamples)
746
+ this.rebuildExampleSelect()
747
+
748
+ // Go to new
749
+ this.newExample()
750
+ this.parts.statusBar.textContent = `Deleted "${name}"`
751
+ }
752
+
753
+ copyOutput = async () => {
754
+ const text = this.parts.resultContainer.textContent || ''
755
+ try {
756
+ await navigator.clipboard.writeText(text)
757
+ this.parts.copyBtn.classList.add('copied')
758
+ setTimeout(() => {
759
+ this.parts.copyBtn.classList.remove('copied')
760
+ }, 1500)
761
+ } catch (e) {
762
+ console.error('Failed to copy:', e)
763
+ }
764
+ }
765
+
766
+ switchTab(tab: string) {
767
+ this.currentTab = tab
768
+ this.parts.tabResult.classList.toggle('active', tab === 'result')
769
+ this.parts.tabAst.classList.toggle('active', tab === 'ast')
770
+ this.parts.tabTrace.classList.toggle('active', tab === 'trace')
771
+ this.updateOutput()
772
+ }
773
+
774
+ updateOutput() {
775
+ const container = this.parts.resultContainer
776
+ container.className = 'playground-result'
777
+
778
+ if (this.lastError) {
779
+ container.className += ' error'
780
+ container.textContent = this.lastError
781
+ return
782
+ }
783
+
784
+ switch (this.currentTab) {
785
+ case 'ast':
786
+ container.textContent = this.lastAst
787
+ ? JSON.stringify(this.lastAst.ast, null, 2)
788
+ : '// Run code to see AST'
789
+ break
790
+
791
+ case 'trace':
792
+ container.textContent = this.lastResult?.trace
793
+ ? this.lastResult.trace
794
+ .map((t: any) => `${t.op}: ${JSON.stringify(t.result)}`)
795
+ .join('\n')
796
+ : '// Run code with tracing enabled to see execution trace'
797
+ break
798
+
799
+ case 'result':
800
+ default:
801
+ if (this.lastResult) {
802
+ container.className += this.lastResult.error ? ' error' : ' success'
803
+ const output = this.lastResult.error || this.lastResult.result
804
+ container.textContent =
805
+ typeof output === 'string'
806
+ ? output
807
+ : JSON.stringify(output, null, 2)
808
+ } else {
809
+ container.textContent = '// Run code to see results'
810
+ }
811
+ break
812
+ }
813
+ }
814
+
815
+ runCode = async () => {
816
+ console.log('runCode called', {
817
+ isRunning: this.isRunning,
818
+ editor: this.editor,
819
+ })
820
+ if (this.isRunning || !this.editor) return
821
+
822
+ this.isRunning = true
823
+ this.lastError = null
824
+ this.lastAst = null
825
+ this.lastResult = null
826
+
827
+ const code = this.editor.state.doc.toString()
828
+ const startTime = performance.now()
829
+
830
+ // Show loading state
831
+ this.parts.resultContainer.className = 'playground-result'
832
+ this.parts.resultContainer.innerHTML =
833
+ '<span class="loading-spinner"></span> Running...'
834
+ this.parts.statusBar.textContent = 'Transpiling...'
835
+
836
+ // Update elapsed time while running
837
+ const updateTimer = setInterval(() => {
838
+ const elapsed = ((performance.now() - startTime) / 1000).toFixed(1)
839
+ this.parts.statusBar.textContent = `Running... ${elapsed}s`
840
+ }, 100)
841
+
842
+ try {
843
+ const transpileResult = transpile(code)
844
+ this.lastAst = transpileResult
845
+
846
+ this.parts.statusBar.textContent = 'Running...'
847
+
848
+ // Build args from signature defaults
849
+ const args: Record<string, any> = {}
850
+ if (transpileResult.signature?.parameters) {
851
+ for (const [key, param] of Object.entries(
852
+ transpileResult.signature.parameters
853
+ )) {
854
+ if ('default' in param) {
855
+ args[key] = param.default
856
+ }
857
+ }
858
+ }
859
+
860
+ // Build capabilities from settings
861
+ const settings = getSettings()
862
+ const llmCapability = buildLLMCapability(settings)
863
+ const llmBattery = buildLLMBattery(settings)
864
+
865
+ const noLLMError = () => {
866
+ const isHttps = window.location.protocol === 'https:'
867
+ if (isHttps) {
868
+ throw new Error(
869
+ 'No LLM configured. Go to Settings (⋮) > API Keys to add an OpenAI or Anthropic API key. Note: Local LLM endpoints require HTTP.'
870
+ )
871
+ } else {
872
+ throw new Error(
873
+ 'No LLM configured. Go to Settings (⋮) > API Keys to add an OpenAI key, Anthropic key, or LM Studio endpoint (default: http://localhost:1234/v1).'
874
+ )
875
+ }
876
+ }
877
+
878
+ const result = await this.vm.run(transpileResult.ast, args, {
879
+ trace: true,
880
+ fuel: 10000,
881
+ capabilities: {
882
+ fetch: async (url: string, options?: any) => {
883
+ const response = await fetch(url, options)
884
+
885
+ // Handle dataUrl response type for vision/image fetching
886
+ if (options?.responseType === 'dataUrl') {
887
+ const buffer = await response.arrayBuffer()
888
+ const bytes = new Uint8Array(buffer)
889
+ let binary = ''
890
+ for (let i = 0; i < bytes.length; i++) {
891
+ binary += String.fromCharCode(bytes[i])
892
+ }
893
+ const base64 = btoa(binary)
894
+ const ct =
895
+ response.headers.get('content-type') ||
896
+ 'application/octet-stream'
897
+ return `data:${ct};base64,${base64}`
898
+ }
899
+
900
+ const contentType = response.headers.get('content-type')
901
+ if (contentType && contentType.includes('application/json')) {
902
+ return response.json()
903
+ }
904
+ return response.text()
905
+ },
906
+ store: getStoreCapabilityDefault(),
907
+ llm: {
908
+ predict: async (prompt: string, options?: any) => {
909
+ if (!llmCapability) noLLMError()
910
+ return llmCapability!.predict(prompt, options)
911
+ },
912
+ },
913
+ llmBattery: llmBattery || {
914
+ predict: () => noLLMError(),
915
+ embed: () => noLLMError(),
916
+ },
917
+ code: {
918
+ transpile: (source: string) => transpile(source).ast,
919
+ },
920
+ },
921
+ })
922
+
923
+ this.lastResult = result
924
+ clearInterval(updateTimer)
925
+ const elapsed = ((performance.now() - startTime) / 1000).toFixed(2)
926
+ this.parts.statusBar.textContent = `Done in ${elapsed}s (${result.fuelUsed.toFixed(
927
+ 1
928
+ )} fuel)`
929
+ } catch (e: any) {
930
+ clearInterval(updateTimer)
931
+ const elapsed = ((performance.now() - startTime) / 1000).toFixed(2)
932
+ this.lastError = e.message || String(e)
933
+ this.parts.statusBar.textContent = `Error after ${elapsed}s`
934
+ }
935
+
936
+ this.updateOutput()
937
+ this.isRunning = false
938
+ }
939
+
940
+ render() {
941
+ super.render()
942
+ }
943
+ }
944
+
945
+ // Register component with Shadow DOM (uses static styleSpec)
946
+ console.log('Registering agent-playground component')
947
+ export const playground = Playground.elementCreator({
948
+ tag: 'agent-playground',
949
+ })