horizon-code 0.1.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/assets/python/highlights.scm +137 -0
  2. package/assets/python/tree-sitter-python.wasm +0 -0
  3. package/bin/horizon.js +2 -0
  4. package/package.json +40 -0
  5. package/src/ai/client.ts +369 -0
  6. package/src/ai/system-prompt.ts +86 -0
  7. package/src/app.ts +1454 -0
  8. package/src/chat/messages.ts +48 -0
  9. package/src/chat/renderer.ts +243 -0
  10. package/src/chat/types.ts +18 -0
  11. package/src/components/code-panel.ts +329 -0
  12. package/src/components/footer.ts +72 -0
  13. package/src/components/hooks-panel.ts +224 -0
  14. package/src/components/input-bar.ts +193 -0
  15. package/src/components/mode-bar.ts +245 -0
  16. package/src/components/session-panel.ts +294 -0
  17. package/src/components/settings-panel.ts +372 -0
  18. package/src/components/splash.ts +156 -0
  19. package/src/components/strategy-panel.ts +489 -0
  20. package/src/components/tab-bar.ts +112 -0
  21. package/src/components/tutorial-panel.ts +680 -0
  22. package/src/components/widgets/progress-bar.ts +38 -0
  23. package/src/components/widgets/sparkline.ts +57 -0
  24. package/src/hooks/executor.ts +109 -0
  25. package/src/index.ts +22 -0
  26. package/src/keys/handler.ts +198 -0
  27. package/src/platform/auth.ts +36 -0
  28. package/src/platform/client.ts +159 -0
  29. package/src/platform/config.ts +121 -0
  30. package/src/platform/session-sync.ts +158 -0
  31. package/src/platform/supabase.ts +376 -0
  32. package/src/platform/sync.ts +149 -0
  33. package/src/platform/tiers.ts +103 -0
  34. package/src/platform/tools.ts +163 -0
  35. package/src/platform/types.ts +86 -0
  36. package/src/platform/usage.ts +224 -0
  37. package/src/research/apis.ts +367 -0
  38. package/src/research/tools.ts +205 -0
  39. package/src/research/widgets.ts +523 -0
  40. package/src/state/store.ts +256 -0
  41. package/src/state/types.ts +109 -0
  42. package/src/strategy/ascii-chart.ts +74 -0
  43. package/src/strategy/code-stream.ts +146 -0
  44. package/src/strategy/dashboard.ts +140 -0
  45. package/src/strategy/persistence.ts +82 -0
  46. package/src/strategy/prompts.ts +626 -0
  47. package/src/strategy/sandbox.ts +137 -0
  48. package/src/strategy/tools.ts +764 -0
  49. package/src/strategy/validator.ts +216 -0
  50. package/src/strategy/widgets.ts +270 -0
  51. package/src/syntax/setup.ts +54 -0
  52. package/src/theme/colors.ts +107 -0
  53. package/src/theme/icons.ts +27 -0
  54. package/src/util/hyperlink.ts +21 -0
@@ -0,0 +1,680 @@
1
+ // Tutorial panel — split-view panel with tabbed guide content
2
+ // Rendered as markdown with syntax highlighting
3
+
4
+ import {
5
+ BoxRenderable,
6
+ TextRenderable,
7
+ ScrollBoxRenderable,
8
+ MarkdownRenderable,
9
+ SyntaxStyle,
10
+ type CliRenderer,
11
+ } from "@opentui/core";
12
+ import { COLORS } from "../theme/colors.ts";
13
+
14
+ const syntaxStyle = SyntaxStyle.create();
15
+
16
+ type TutorialTab = "start" | "overview" | "research" | "strategy" | "portfolio" | "settings" | "commands" | "tips";
17
+
18
+ const TAB_LIST: { id: TutorialTab; label: string }[] = [
19
+ { id: "start", label: "Start" },
20
+ { id: "overview", label: "Modes" },
21
+ { id: "research", label: "Research" },
22
+ { id: "strategy", label: "Strategy" },
23
+ { id: "portfolio", label: "Portfolio" },
24
+ { id: "settings", label: "Config" },
25
+ { id: "commands", label: "Cmds" },
26
+ { id: "tips", label: "Tips" },
27
+ ];
28
+
29
+ const CONTENT: Record<TutorialTab, string> = {
30
+ start: `# First Steps
31
+
32
+ Welcome to Horizon. Here's how to get started in 5 minutes.
33
+
34
+ ---
35
+
36
+ ## Step 1: Research a market
37
+
38
+ You're in **Research mode** by default. Just ask:
39
+
40
+ > "What prediction markets are there about bitcoin?"
41
+
42
+ The AI searches **live Polymarket data** and shows results with
43
+ prices, volume, and liquidity. Try:
44
+
45
+ - *"Show me the order book for that first one"*
46
+ - *"What do whales think about this market?"*
47
+ - *"Calculate the EV if I think it's 60% likely"*
48
+
49
+ ---
50
+
51
+ ## Step 2: Build a strategy
52
+
53
+ Press **Ctrl+R** to switch to **Strategy mode**. Then:
54
+
55
+ > "Build me a market making strategy for this market"
56
+
57
+ The AI researches the market, writes Python code using the
58
+ **Horizon SDK**, validates it, and opens the **code panel** (Ctrl+G)
59
+ showing your strategy with syntax highlighting.
60
+
61
+ Iterate: *"Make the spread tighter"* or *"Add inventory skew"*
62
+
63
+ ---
64
+
65
+ ## Step 3: Backtest it
66
+
67
+ > "Backtest this strategy"
68
+
69
+ Runs \`hz.backtest()\` with real SDK metrics — Sharpe ratio,
70
+ drawdown, win rate, profit factor, equity curve.
71
+
72
+ ---
73
+
74
+ ## Step 4: Deploy
75
+
76
+ > "Deploy it in paper mode"
77
+
78
+ Your strategy goes live on the Horizon cloud in paper trading mode.
79
+ Switch to **Portfolio mode** (Ctrl+R) to monitor it.
80
+
81
+ ---
82
+
83
+ ## Step 5: Customize
84
+
85
+ - **/settings** — intelligence level, themes, sound, auto-compact
86
+ - **/hooks** — run custom bash commands before/after strategy actions
87
+ - **/env add ALPACA_KEY sk-xxx** — store encrypted API keys for SDK integrations
88
+
89
+ ---
90
+
91
+ ## Keyboard cheat sheet
92
+
93
+ | Key | What it does |
94
+ |-----|-------------|
95
+ | **Ctrl+R** | Switch mode (research/strategy/portfolio) |
96
+ | **Ctrl+N** | New chat |
97
+ | **Ctrl+L / Ctrl+H** | Switch between open chats |
98
+ | **Ctrl+W** | Close current chat |
99
+ | **Ctrl+E** | Open chat sidebar |
100
+ | **Ctrl+D** | Open bots panel |
101
+ | **Ctrl+G** | Toggle code panel |
102
+ | **Esc** | Stop generation / close panels |
103
+
104
+ ---
105
+
106
+ ## Plans & limits
107
+
108
+ | | **Free** | **Pro** | **Ultra** |
109
+ |---|---------|---------|-----------|
110
+ | Model | Fast | Fast + Standard + Pro | All + Ultra |
111
+ | Tokens/5h | 50K | 500K | 5M |
112
+ | Sentiment | - | Yes | Yes |
113
+ | Knowledge | - | Yes | Yes |
114
+
115
+ Upgrade at **mathematicalcompany.com/pricing**
116
+ `,
117
+
118
+ overview: `# Horizon
119
+
120
+ Horizon is a terminal-native AI trading assistant for **prediction markets**.
121
+ Research, build strategies, and manage your portfolio — all from the terminal.
122
+
123
+ ---
124
+
125
+ ## Quick Start
126
+
127
+ 1. **Research** — just ask: *"What's happening with Iran?"*
128
+ 2. **Strategy** — switch with **Ctrl+R**: *"Build me a market maker"*
129
+ 3. **Portfolio** — switch again: *"Show my deployments"*
130
+
131
+ The AI has **live tools** — it searches real markets, writes real code,
132
+ backtests with real metrics. Never answers from memory.
133
+
134
+ ---
135
+
136
+ ## Three Modes
137
+
138
+ Switch with **Ctrl+R** or \`/mode\`
139
+
140
+ **Research** — 13 live data tools. Search markets, order books,
141
+ whale activity, sentiment, EV calculator, cross-platform comparison.
142
+
143
+ **Strategy** — Full coding partner. Writes Python with the Horizon SDK,
144
+ validates, backtests with \`hz.backtest()\`, runs locally in sandbox,
145
+ deploys to the cloud. Code panel shows syntax-highlighted source.
146
+
147
+ **Portfolio** — Monitor live deployments. P&L, positions, orders,
148
+ drawdown. Start, stop, restart from the bots panel.
149
+
150
+ ---
151
+
152
+ ## Layout
153
+
154
+ | Area | Key | What it does |
155
+ |------|-----|-------------|
156
+ | **Chat** | — | Left side, talk to the AI |
157
+ | **Code panel** | Ctrl+G | Right side, strategy code + logs |
158
+ | **Sessions** | Ctrl+E | Left overlay, manage chats |
159
+ | **Bots** | Ctrl+D | Right overlay, monitor strategies |
160
+ | **Tutorial** | /tutorial | This guide |
161
+ | **Settings** | /settings | Configuration |
162
+
163
+ ---
164
+
165
+ ## Authentication
166
+
167
+ \`/login\` opens your browser — sign in with **Google** or email.
168
+ Sessions, strategies, and settings sync across devices.
169
+ `,
170
+
171
+ research: `# Research Mode
172
+
173
+ The AI has 13 tools that fetch **live data** from Polymarket, Kalshi,
174
+ and the web. It never answers from memory — always calls tools.
175
+
176
+ ## Market Discovery
177
+
178
+ Ask about any topic and the AI searches 200+ active markets:
179
+
180
+ - "What's happening with Iran?"
181
+ - "Show me crypto markets"
182
+ - "Any sports betting markets?"
183
+
184
+ ## Deep Analysis
185
+
186
+ Once you find a market, dig deeper:
187
+
188
+ - **Event detail** — sub-markets, prices, spreads, volume, sparkline
189
+ - **Order book** — bid/ask depth with visual bars
190
+ - **Price history** — sparkline charts at any interval (1h to max)
191
+ - **Whale tracker** — large trades, smart money flow direction
192
+ - **Sentiment** — news analysis with bull/bear gauge
193
+ - **Volatility** — realized vol, regime detection (high/normal/low)
194
+
195
+ ## Quantitative Tools
196
+
197
+ - **EV Calculator** — expected value, edge, Kelly criterion sizing
198
+ - **Probability** — joint and conditional probabilities between markets
199
+ - **Cross-platform** — compare Polymarket vs Kalshi prices for arbitrage
200
+
201
+ ## Tool Chaining
202
+
203
+ The AI remembers market slugs from previous results. Say "tell me more
204
+ about the first one" and it uses the slug without re-searching.
205
+
206
+ ## Widgets
207
+
208
+ Tool results render as rich terminal widgets — not raw text. Order
209
+ books show depth bars, sparklines show price trends, sentiment shows
210
+ a gauge. The AI adds 1-2 sentences of insight after each widget.
211
+ `,
212
+
213
+ strategy: `# Strategy Mode
214
+
215
+ Build trading strategies with the Horizon SDK. The AI is a coding
216
+ partner — it researches markets, writes code, validates, backtests,
217
+ runs locally, and deploys to the cloud.
218
+
219
+ ## Workflow
220
+
221
+ 1. **Describe your idea** — "build a market maker for bitcoin"
222
+ 2. **AI researches** — checks real spreads, volume, liquidity
223
+ 3. **AI proposes code** — complete Python with hz.run()
224
+ 4. **Code panel opens** — shows the code with syntax highlighting
225
+ 5. **Iterate** — "make the spread tighter", "add a stop loss"
226
+ 6. **Backtest** — "backtest it" runs real hz.backtest()
227
+ 7. **Run locally** — "run it" executes in paper mode sandbox
228
+ 8. **Deploy** — "deploy it" pushes to Horizon cloud
229
+
230
+ ## Code Panel (Ctrl+G)
231
+
232
+ Three tabs:
233
+ - **Code** — strategy source with Python highlighting
234
+ - **Logs** — stdout/stderr from run_strategy
235
+ - **Dashboard** — HTML source from spawn_dashboard
236
+
237
+ Switch tabs with Ctrl+1, Ctrl+2, Ctrl+3.
238
+
239
+ ## Tools
240
+
241
+ Strategy code is generated by writing \`\`\`python code fences.
242
+ Code streams into the panel in real-time as the AI writes it.
243
+
244
+ | Tool | Purpose |
245
+ |------|---------|
246
+ | edit_strategy | Find-and-replace for small changes |
247
+ | validate_strategy | Check against SDK sandbox rules |
248
+ | backtest_strategy | Run hz.backtest() with real metrics |
249
+ | run_strategy | Execute locally in paper mode |
250
+ | read_logs | Check process output |
251
+ | save_strategy | Push to Horizon platform |
252
+ | lookup_sdk_docs | Fetch advanced SDK docs |
253
+ | polymarket_data | Search real markets |
254
+ | run_command | Shell commands (pip install, etc.) |
255
+ | spawn_dashboard | Serve custom HTML dashboard |
256
+
257
+ ## The Code
258
+
259
+ Strategies are single Python files using the Horizon SDK:
260
+
261
+ \`\`\`python
262
+ import horizon as hz
263
+
264
+ def fair_value(ctx):
265
+ feed = ctx.feeds.get("mid")
266
+ if not feed or feed.is_stale(30):
267
+ return None
268
+ return feed.price
269
+
270
+ def quoter(ctx, fair):
271
+ if fair is None:
272
+ return []
273
+ return hz.quotes(fair, spread=0.06, size=5)
274
+
275
+ hz.run(
276
+ name="MyStrategy",
277
+ exchange=hz.Polymarket(),
278
+ markets=["market-slug"],
279
+ feeds={"mid": hz.PolymarketBook("market-slug")},
280
+ pipeline=[fair_value, quoter],
281
+ risk=hz.Risk(max_position=100),
282
+ mode="paper",
283
+ )
284
+ \`\`\`
285
+
286
+ ## File Persistence
287
+
288
+ Strategies auto-save to ~/.horizon/strategies/ on every change.
289
+ Use \`/strategies\` to list, \`/load <name>\` to load.
290
+ `,
291
+
292
+ portfolio: `# Portfolio Mode
293
+
294
+ Monitor and manage your live deployments.
295
+
296
+ ## Bots Panel (Ctrl+D)
297
+
298
+ Shows all your strategies with status:
299
+ - Running strategies show a spinner
300
+ - Stopped strategies show a dash
301
+ - Error strategies show an x
302
+
303
+ Navigate with arrow keys, Enter to expand details.
304
+
305
+ ## Actions
306
+
307
+ When a strategy is expanded:
308
+ - **r** — restart a stopped strategy (redeploys with same settings)
309
+ - **p** — pause a running strategy
310
+ - **k** — kill a running strategy
311
+
312
+ ## Metrics
313
+
314
+ When metrics are available for running strategies:
315
+
316
+ | Metric | Meaning |
317
+ |--------|---------|
318
+ | Total P&L | Net profit/loss |
319
+ | Realized P&L | Closed position profits |
320
+ | Unrealized P&L | Open position mark-to-market |
321
+ | Win Rate | Percentage of profitable trades |
322
+ | Max Drawdown | Largest peak-to-trough decline |
323
+ | Sharpe Ratio | Risk-adjusted return (>1 is good, >2 is great) |
324
+ | Sortino Ratio | Like Sharpe but only penalizes downside |
325
+ | Profit Factor | Gross profit / gross loss (>1.5 is good) |
326
+ | Expectancy | Average profit per trade |
327
+
328
+ ## AI Tools
329
+
330
+ In portfolio mode, the AI has these tools:
331
+ - **list_strategies** — see all strategies
332
+ - **get_strategy** — full detail with code
333
+ - **list_deployments** — deployment history
334
+ - **get_metrics** — live P&L, positions, orders
335
+ - **get_logs** — recent bot output
336
+ - **deploy_strategy** — deploy to paper or live
337
+ - **stop_strategy** — stop a deployment
338
+ `,
339
+
340
+ settings: `# Settings & Configuration
341
+
342
+ Open with **/settings**. Navigate with arrow keys, left/right to change.
343
+
344
+ ---
345
+
346
+ ## Intelligence
347
+
348
+ Controls which AI model powers your chat:
349
+
350
+ | Level | Model | Budget multiplier |
351
+ |-------|-------|-------------------|
352
+ | **Fast** | Hunter Alpha | 0.25x (cheapest) |
353
+ | **Standard** | Claude Haiku 4.5 | 1x |
354
+ | **Pro** | Claude Sonnet 4.6 | 2x (Pro plan+) |
355
+ | **Ultra** | Claude Opus 4.6 | 4x (Ultra plan only) |
356
+
357
+ Higher intelligence = smarter responses but drains your token
358
+ budget faster. Fast is great for simple queries.
359
+
360
+ ---
361
+
362
+ ## Response Style
363
+
364
+ - **Short** — 1-2 sentences max. Terse, no elaboration.
365
+ - **Normal** — balanced (default)
366
+ - **Verbose** — detailed explanations with reasoning
367
+
368
+ Does NOT affect code generation in strategy mode.
369
+
370
+ ---
371
+
372
+ ## Hooks
373
+
374
+ Open with **/hooks**. Custom bash commands that run at strategy
375
+ lifecycle points. Each hook shows output in chat.
376
+
377
+ | Event | When it runs |
378
+ |-------|-------------|
379
+ | before_generate | Before AI writes strategy code |
380
+ | after_generate | After code is proposed |
381
+ | before_validate | Before validation check |
382
+ | before_backtest | Before running backtest |
383
+ | before_deploy | Before deploying to cloud |
384
+ | after_deploy | After deploy succeeds |
385
+
386
+ **Adding a hook:** press \`a\`, select event, type command, enter.
387
+ **Example:** \`before_deploy\` -> \`ruff check strategy.py\`
388
+
389
+ ---
390
+
391
+ ## Encrypted API Keys
392
+
393
+ Store keys for SDK integrations (Alpaca, Unusual Whales, etc.):
394
+
395
+ \`/env add ALPACA_KEY sk-your-key-here\`
396
+ \`/env list\` — see stored keys
397
+ \`/env remove ALPACA_KEY\` — delete a key
398
+
399
+ Keys are encrypted with **AES-256-GCM** using a key derived from
400
+ your Horizon API key. Stored in ~/.horizon/config.json.
401
+
402
+ ---
403
+
404
+ ## Safety Controls
405
+
406
+ **Pay As You Go** — off by default. When on, allows extra charges
407
+ if your token budget runs out. When off, hard stop.
408
+
409
+ **Live Trading** — off by default. Must be enabled to deploy live
410
+ strategies. Paper mode only until you turn this on.
411
+
412
+ ---
413
+
414
+ ## Themes
415
+
416
+ Four themes: **Dark** (default), **Midnight**, **Nord**, **Solarized**.
417
+ Restart to fully apply.
418
+
419
+ ---
420
+
421
+ ## Other Settings
422
+
423
+ - **Auto Focus** — focus input after sending a message
424
+ - **Smart Compact** — auto-compact context when it gets full
425
+ - **Compact Warning** — threshold % to warn about context usage
426
+ - **Show Tool Calls** — show/hide tool call indicators in chat
427
+ - **Sound** — terminal bell on generation complete
428
+ `,
429
+
430
+ commands: `# Commands
431
+
432
+ Type \`/\` to see autocomplete suggestions as you type.
433
+
434
+ ---
435
+
436
+ ## Chat
437
+
438
+ | Command | Action |
439
+ |---------|--------|
440
+ | /clear | Clear current chat messages |
441
+ | /compact | Force context compaction |
442
+ | /context | Show context window usage |
443
+
444
+ ## Auth
445
+
446
+ | Command | Action |
447
+ |---------|--------|
448
+ | /login | Browser auth (Google) |
449
+ | /login email pass | Email/password login |
450
+ | /logout | Sign out |
451
+ | /whoami | Show current user |
452
+
453
+ ## Panels
454
+
455
+ | Command | Action |
456
+ |---------|--------|
457
+ | /settings | Open settings panel |
458
+ | /hooks | Manage lifecycle hooks |
459
+ | /tutorial | This guide |
460
+ | /code | Toggle code panel |
461
+ | /home | Back to splash screen |
462
+
463
+ ## Mode
464
+
465
+ | Command | Action |
466
+ |---------|--------|
467
+ | /mode | Cycle research/strategy/portfolio |
468
+ | /mode research | Switch to research |
469
+ | /mode strategy | Switch to strategy |
470
+ | /usage | Toggle metrics display |
471
+ | /privacy | Toggle privacy mode |
472
+
473
+ ## Strategy
474
+
475
+ | Command | Action |
476
+ |---------|--------|
477
+ | /strategies | List saved strategies on disk |
478
+ | /load name | Load a saved strategy |
479
+
480
+ ## Environment
481
+
482
+ | Command | Action |
483
+ |---------|--------|
484
+ | /env list | Show stored encrypted keys |
485
+ | /env add NAME VALUE | Store an encrypted API key |
486
+ | /env remove NAME | Delete a stored key |
487
+ | /tutorial | This guide |
488
+
489
+ ## Context
490
+
491
+ | Command | Action |
492
+ |---------|--------|
493
+ | /compact | Force context compaction |
494
+ | /context | Show context window usage |
495
+
496
+ ## System
497
+
498
+ | Command | Action |
499
+ |---------|--------|
500
+ | /help | List all commands |
501
+ | /quit | Exit Horizon |
502
+
503
+ ## Keyboard Shortcuts
504
+
505
+ | Key | Action |
506
+ |-----|--------|
507
+ | Ctrl+R | Cycle mode |
508
+ | Ctrl+E | Toggle sessions panel |
509
+ | Ctrl+D | Toggle bots panel |
510
+ | Ctrl+G | Toggle code panel |
511
+ | Ctrl+T | Toggle metrics display |
512
+ | Ctrl+P | Toggle privacy mode |
513
+ | Ctrl+1/2/3 | Code panel tabs |
514
+ | Esc | Stop generation / close panel |
515
+ | PageUp/Down | Scroll chat |
516
+ `,
517
+
518
+ tips: `# Tips
519
+
520
+ ---
521
+
522
+ ## Research
523
+
524
+ - **Be specific** — *"order book for btc-100k"* beats *"show me bitcoin"*
525
+ - **Chain requests** — *"analyze the first one"* reuses the slug
526
+ - **EV before betting** — calculates Kelly sizing from your edge
527
+ - **Cross-platform** — *"compare bitcoin Polymarket vs Kalshi"* for arb
528
+
529
+ ---
530
+
531
+ ## Strategy
532
+
533
+ - **Let it research first** — the AI checks real spreads before coding
534
+ - **Paper mode default** — always starts safe. Enable Live in /settings
535
+ - **Backtest before deploy** — real \`hz.backtest()\` metrics
536
+ - **Small edits** — the AI uses find/replace for tweaks, full rewrite for rewrites
537
+ - **Ctrl+G** — code panel shows validation status (green = valid)
538
+
539
+ ---
540
+
541
+ ## Performance
542
+
543
+ - **Ctrl+T** — toggle metrics bar (context %, budget, tokens)
544
+ - **Smart compact** — enabled by default, prompts before compacting
545
+ - **/compact** — force compaction when context is high
546
+ - **Queue messages** — send while generating, they process in order
547
+ - **Esc** — interrupt generation. Queue pauses with /resume
548
+
549
+ ---
550
+
551
+ ## Security
552
+
553
+ - **Sandboxed execution** — strategy code runs in temp dirs, no secrets
554
+ - **Server-side enforcement** — budget, rate limits, model access checked server-side
555
+ - **Encrypted env vars** — \`/env add\` stores keys with AES-256-GCM
556
+ - **Paper mode guard** — /settings → Live Trading must be ON for live deploys
557
+ - **Config permissions** — ~/.horizon/config.json is 0600 (owner only)
558
+ `,
559
+ };
560
+
561
+ export class TutorialPanel {
562
+ readonly container: BoxRenderable;
563
+ private tabBar: BoxRenderable;
564
+ private tabTexts: Map<TutorialTab, TextRenderable> = new Map();
565
+ private contentScroll: ScrollBoxRenderable;
566
+ private contentMd: MarkdownRenderable;
567
+ private _visible = false;
568
+ private _activeTab: TutorialTab = "start";
569
+
570
+ constructor(private renderer: CliRenderer) {
571
+ this.container = new BoxRenderable(renderer, {
572
+ id: "tutorial-panel",
573
+ width: "50%",
574
+ height: "100%",
575
+ flexDirection: "column",
576
+ borderColor: COLORS.borderDim,
577
+ });
578
+ this.container.visible = false;
579
+
580
+ // Tab bar
581
+ this.tabBar = new BoxRenderable(renderer, {
582
+ id: "tutorial-tabs",
583
+ height: 1,
584
+ width: "100%",
585
+ flexDirection: "row",
586
+ alignItems: "center",
587
+ paddingLeft: 1,
588
+ paddingRight: 1,
589
+ backgroundColor: COLORS.bgDarker,
590
+ flexShrink: 0,
591
+ });
592
+
593
+ for (const tab of TAB_LIST) {
594
+ const text = new TextRenderable(renderer, {
595
+ id: `tutorial-tab-${tab.id}`,
596
+ content: ` ${tab.label} `,
597
+ fg: COLORS.textMuted,
598
+ });
599
+ this.tabTexts.set(tab.id, text);
600
+ this.tabBar.add(text);
601
+ }
602
+ this.container.add(this.tabBar);
603
+
604
+ // Content area
605
+ this.contentScroll = new ScrollBoxRenderable(renderer, {
606
+ id: "tutorial-scroll",
607
+ flexGrow: 1,
608
+ paddingLeft: 2,
609
+ paddingRight: 2,
610
+ paddingTop: 1,
611
+ stickyScroll: false,
612
+ });
613
+ this.contentMd = new MarkdownRenderable(renderer, {
614
+ id: "tutorial-md",
615
+ content: CONTENT.start,
616
+ syntaxStyle,
617
+ });
618
+ this.contentScroll.add(this.contentMd);
619
+ this.container.add(this.contentScroll);
620
+
621
+ // Footer
622
+ this.container.add(new TextRenderable(renderer, {
623
+ id: "tutorial-footer",
624
+ content: " < > switch tab | /tutorial to close",
625
+ fg: COLORS.borderDim,
626
+ }));
627
+
628
+ this.updateTabBar();
629
+ }
630
+
631
+ get visible(): boolean { return this._visible; }
632
+
633
+ show(): void {
634
+ this._visible = true;
635
+ this.container.visible = true;
636
+ this.renderer.requestRender();
637
+ }
638
+
639
+ hide(): void {
640
+ this._visible = false;
641
+ this.container.visible = false;
642
+ this.renderer.requestRender();
643
+ }
644
+
645
+ toggle(): void {
646
+ if (this._visible) this.hide();
647
+ else this.show();
648
+ }
649
+
650
+ setTab(tab: TutorialTab): void {
651
+ this._activeTab = tab;
652
+ this.contentMd.content = CONTENT[tab];
653
+ this.updateTabBar();
654
+ this.renderer.requestRender();
655
+ }
656
+
657
+ nextTab(): void {
658
+ const idx = TAB_LIST.findIndex((t) => t.id === this._activeTab);
659
+ this.setTab(TAB_LIST[(idx + 1) % TAB_LIST.length]!.id);
660
+ }
661
+
662
+ prevTab(): void {
663
+ const idx = TAB_LIST.findIndex((t) => t.id === this._activeTab);
664
+ this.setTab(TAB_LIST[(idx - 1 + TAB_LIST.length) % TAB_LIST.length]!.id);
665
+ }
666
+
667
+ private updateTabBar(): void {
668
+ for (const [id, text] of this.tabTexts) {
669
+ if (id === this._activeTab) {
670
+ text.fg = "#212121";
671
+ text.bg = COLORS.secondary;
672
+ text.content = ` ${TAB_LIST.find((t) => t.id === id)!.label} `;
673
+ } else {
674
+ text.fg = COLORS.textMuted;
675
+ text.bg = undefined;
676
+ text.content = ` ${TAB_LIST.find((t) => t.id === id)!.label} `;
677
+ }
678
+ }
679
+ }
680
+ }
@@ -0,0 +1,38 @@
1
+ import { TextRenderable, type CliRenderer } from "@opentui/core";
2
+ import { COLORS } from "../../theme/colors.ts";
3
+ import { ICONS } from "../../theme/icons.ts";
4
+
5
+ export class ProgressBar {
6
+ private text: TextRenderable;
7
+
8
+ constructor(
9
+ renderer: CliRenderer,
10
+ id: string,
11
+ private width: number = 20,
12
+ ) {
13
+ this.text = new TextRenderable(renderer, {
14
+ id,
15
+ content: "",
16
+ fg: COLORS.textMuted,
17
+ });
18
+ }
19
+
20
+ get renderable(): TextRenderable {
21
+ return this.text;
22
+ }
23
+
24
+ setValue(pct: number, label?: string): void {
25
+ const clamped = Math.max(0, Math.min(100, pct));
26
+ const filled = Math.round((clamped / 100) * this.width);
27
+ const empty = this.width - filled;
28
+
29
+ const bar = ICONS.barFull.repeat(filled) + ICONS.barEmpty.repeat(empty);
30
+ const suffix = label ?? `${clamped.toFixed(0)}%`;
31
+
32
+ this.text.content = `${bar} ${suffix}`;
33
+
34
+ if (clamped <= 60) this.text.fg = COLORS.success;
35
+ else if (clamped <= 80) this.text.fg = COLORS.warning;
36
+ else this.text.fg = COLORS.error;
37
+ }
38
+ }