@silvery/examples 0.17.3 → 0.17.4

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 (112) hide show
  1. package/dist/UPNG-Cy7ViL8f.mjs +5074 -0
  2. package/dist/__vite-browser-external-2447137e-BML7CYau.mjs +4 -0
  3. package/dist/_banner-DLPxCqVy.mjs +44 -0
  4. package/dist/ansi-CCE2pVS0.mjs +16397 -0
  5. package/dist/apng-HhhBjRGt.mjs +68 -0
  6. package/dist/apng-mwUQbTTF.mjs +3 -0
  7. package/dist/apps/aichat/index.mjs +1299 -0
  8. package/dist/apps/app-todo.mjs +139 -0
  9. package/dist/apps/async-data.mjs +204 -0
  10. package/dist/apps/cli-wizard.mjs +339 -0
  11. package/dist/apps/clipboard.mjs +198 -0
  12. package/dist/apps/components.mjs +864 -0
  13. package/dist/apps/data-explorer.mjs +483 -0
  14. package/dist/apps/dev-tools.mjs +397 -0
  15. package/dist/apps/explorer.mjs +698 -0
  16. package/dist/apps/gallery.mjs +766 -0
  17. package/dist/apps/inline-bench.mjs +115 -0
  18. package/dist/apps/kanban.mjs +280 -0
  19. package/dist/apps/layout-ref.mjs +187 -0
  20. package/dist/apps/outline.mjs +203 -0
  21. package/dist/apps/paste-demo.mjs +189 -0
  22. package/dist/apps/scroll.mjs +86 -0
  23. package/dist/apps/search-filter.mjs +287 -0
  24. package/dist/apps/selection.mjs +355 -0
  25. package/dist/apps/spatial-focus-demo.mjs +388 -0
  26. package/dist/apps/task-list.mjs +258 -0
  27. package/dist/apps/terminal-caps-demo.mjs +315 -0
  28. package/dist/apps/terminal.mjs +872 -0
  29. package/dist/apps/text-selection-demo.mjs +254 -0
  30. package/dist/apps/textarea.mjs +178 -0
  31. package/dist/apps/theme.mjs +661 -0
  32. package/dist/apps/transform.mjs +215 -0
  33. package/dist/apps/virtual-10k.mjs +422 -0
  34. package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
  35. package/dist/backends-Bahh9mKN.mjs +1179 -0
  36. package/dist/backends-CCtCDQ94.mjs +3 -0
  37. package/dist/{cli.mjs → bin/cli.mjs} +15 -19
  38. package/dist/chunk-BSw8zbkd.mjs +37 -0
  39. package/dist/components/counter.mjs +48 -0
  40. package/dist/components/hello.mjs +31 -0
  41. package/dist/components/progress-bar.mjs +59 -0
  42. package/dist/components/select-list.mjs +85 -0
  43. package/dist/components/spinner.mjs +57 -0
  44. package/dist/components/text-input.mjs +62 -0
  45. package/dist/components/virtual-list.mjs +51 -0
  46. package/dist/flexily-zero-adapter-UB-ra8fR.mjs +3374 -0
  47. package/dist/gif-BZaqPPVX.mjs +3 -0
  48. package/dist/gif-BtnXuxLF.mjs +71 -0
  49. package/dist/gifenc-CLRW41dk.mjs +728 -0
  50. package/dist/jsx-runtime-dMs_8fNu.mjs +241 -0
  51. package/dist/key-mapping-5oYQdAQE.mjs +3 -0
  52. package/dist/key-mapping-D4LR1go6.mjs +130 -0
  53. package/dist/layout/dashboard.mjs +1204 -0
  54. package/dist/layout/live-resize.mjs +303 -0
  55. package/dist/layout/overflow.mjs +70 -0
  56. package/dist/layout/text-layout.mjs +335 -0
  57. package/dist/node-NuJ94BWl.mjs +1083 -0
  58. package/dist/plugins-D1KtkT4a.mjs +3057 -0
  59. package/dist/resvg-js-C_8Wps1F.mjs +201 -0
  60. package/dist/src-BTEVGpd9.mjs +23538 -0
  61. package/dist/src-CUUOuRH6.mjs +5322 -0
  62. package/dist/src-CzfRafCQ.mjs +814 -0
  63. package/dist/usingCtx-CsEf0xO3.mjs +57 -0
  64. package/dist/yoga-adapter-BVtQ5OJR.mjs +237 -0
  65. package/package.json +18 -13
  66. package/_banner.tsx +0 -60
  67. package/apps/aichat/components.tsx +0 -469
  68. package/apps/aichat/index.tsx +0 -220
  69. package/apps/aichat/script.ts +0 -460
  70. package/apps/aichat/state.ts +0 -325
  71. package/apps/aichat/types.ts +0 -19
  72. package/apps/app-todo.tsx +0 -201
  73. package/apps/async-data.tsx +0 -196
  74. package/apps/cli-wizard.tsx +0 -332
  75. package/apps/clipboard.tsx +0 -183
  76. package/apps/components.tsx +0 -658
  77. package/apps/data-explorer.tsx +0 -490
  78. package/apps/dev-tools.tsx +0 -395
  79. package/apps/explorer.tsx +0 -731
  80. package/apps/gallery.tsx +0 -653
  81. package/apps/inline-bench.tsx +0 -138
  82. package/apps/kanban.tsx +0 -265
  83. package/apps/layout-ref.tsx +0 -173
  84. package/apps/outline.tsx +0 -160
  85. package/apps/panes/index.tsx +0 -203
  86. package/apps/paste-demo.tsx +0 -185
  87. package/apps/scroll.tsx +0 -80
  88. package/apps/search-filter.tsx +0 -240
  89. package/apps/selection.tsx +0 -346
  90. package/apps/spatial-focus-demo.tsx +0 -372
  91. package/apps/task-list.tsx +0 -271
  92. package/apps/terminal-caps-demo.tsx +0 -317
  93. package/apps/terminal.tsx +0 -784
  94. package/apps/text-selection-demo.tsx +0 -193
  95. package/apps/textarea.tsx +0 -155
  96. package/apps/theme.tsx +0 -515
  97. package/apps/transform.tsx +0 -229
  98. package/apps/virtual-10k.tsx +0 -405
  99. package/apps/vterm-demo/index.tsx +0 -216
  100. package/components/counter.tsx +0 -49
  101. package/components/hello.tsx +0 -38
  102. package/components/progress-bar.tsx +0 -52
  103. package/components/select-list.tsx +0 -54
  104. package/components/spinner.tsx +0 -44
  105. package/components/text-input.tsx +0 -61
  106. package/components/virtual-list.tsx +0 -56
  107. package/dist/cli.d.mts +0 -1
  108. package/dist/cli.mjs.map +0 -1
  109. package/layout/dashboard.tsx +0 -953
  110. package/layout/live-resize.tsx +0 -282
  111. package/layout/overflow.tsx +0 -51
  112. package/layout/text-layout.tsx +0 -283
@@ -0,0 +1,1299 @@
1
+ import { t as _usingCtx } from "../../usingCtx-CsEf0xO3.mjs";
2
+ import { t as require_jsx_runtime } from "../../jsx-runtime-dMs_8fNu.mjs";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { Box, Link, ListView, Spinner, Text, TextInput, fx, useTea, useTerminalFocused, useWindowSize } from "silvery";
5
+ import { run, useExit, useInput as useInput$1 } from "silvery/runtime";
6
+ //#region apps/aichat/script.ts
7
+ const CONTEXT_WINDOW = 2e5;
8
+ const TOOL_COLORS = {
9
+ Read: "$info",
10
+ Edit: "$warning",
11
+ Bash: "$error",
12
+ Write: "$accent",
13
+ Glob: "$muted",
14
+ Grep: "$success"
15
+ };
16
+ /** Random user commands for Tab-to-inject feature. */
17
+ const RANDOM_USER_COMMANDS = [
18
+ "Can you add unit tests for the auth module?",
19
+ "Refactor the database queries to use prepared statements.",
20
+ "Add TypeScript strict mode and fix any errors.",
21
+ "Set up CI/CD with GitHub Actions.",
22
+ "The search feature is slow — can you optimize it?",
23
+ "Add dark mode support to the UI.",
24
+ "We need input validation on the registration form.",
25
+ "Create a migration script for the new schema.",
26
+ "Add WebSocket support for real-time updates.",
27
+ "The CSV export is broken — dates are wrong."
28
+ ];
29
+ /** Random agent responses for Tab-injected turns. */
30
+ const RANDOM_AGENT_RESPONSES = [{
31
+ role: "agent",
32
+ thinking: "Let me analyze the codebase to understand the current structure.",
33
+ content: "I'll look at the relevant files and make the changes.",
34
+ toolCalls: [{
35
+ tool: "Read",
36
+ args: "src/index.ts",
37
+ output: ["export function main() { /* ... */ }"]
38
+ }, {
39
+ tool: "Edit",
40
+ args: "src/index.ts",
41
+ output: ["+ // Updated implementation"]
42
+ }],
43
+ tokens: {
44
+ input: 12400,
45
+ output: 890
46
+ }
47
+ }, {
48
+ role: "agent",
49
+ content: "Done! I've made the changes and verified everything works.",
50
+ tokens: {
51
+ input: 15200,
52
+ output: 340
53
+ }
54
+ }];
55
+ /** Regex matching https/http URLs in output text. */
56
+ const URL_RE = /https?:\/\/[^\s)]+/g;
57
+ const SCRIPT = [
58
+ {
59
+ role: "user",
60
+ content: "Fix the login bug in auth.ts — expired tokens throw instead of refreshing.",
61
+ tokens: {
62
+ input: 84,
63
+ output: 0
64
+ }
65
+ },
66
+ {
67
+ role: "agent",
68
+ thinking: "The user reports expired tokens throw instead of refreshing. This is likely in the token validation flow. I should read auth.ts to see the current expiry check logic. The bug is probably comparing jwt.exp (seconds) with Date.now() (milliseconds).",
69
+ content: "Let me look at the auth module.",
70
+ toolCalls: [{
71
+ tool: "Read",
72
+ args: "src/auth.ts",
73
+ output: [
74
+ "export async function login(token: string) {",
75
+ " const decoded = jwt.decode(token)",
76
+ " if (decoded.exp < Date.now()) {",
77
+ " throw new Error(\"Token expired\")",
78
+ " }",
79
+ " return decoded.user",
80
+ "}"
81
+ ]
82
+ }],
83
+ tokens: {
84
+ input: 3694,
85
+ output: 624
86
+ }
87
+ },
88
+ {
89
+ role: "agent",
90
+ thinking: "Found it — decoded.exp is in seconds (Unix timestamp) but Date.now() returns milliseconds. Every token appears expired because exp (e.g. 1700000000) is always less than Date.now() (e.g. 1700000000000). I need to divide Date.now() by 1000, and change the throw to a refresh call.",
91
+ content: "Found it. The expiry check compares seconds (jwt.exp) to milliseconds (Date.now()). Fixing now.",
92
+ toolCalls: [{
93
+ tool: "Edit",
94
+ args: "src/auth.ts",
95
+ output: [
96
+ " const decoded = jwt.decode(token)",
97
+ "- if (decoded.exp < Date.now()) {",
98
+ "- throw new Error(\"Token expired\")",
99
+ "+ if (decoded.exp < Date.now() / 1000) {",
100
+ "+ return refreshToken(token)",
101
+ " }"
102
+ ]
103
+ }],
104
+ tokens: {
105
+ input: 4312,
106
+ output: 574
107
+ }
108
+ },
109
+ {
110
+ role: "agent",
111
+ content: "Running tests to verify the fix.",
112
+ toolCalls: [{
113
+ tool: "Bash",
114
+ args: "bun test src/auth.test.ts",
115
+ output: [
116
+ "auth.test.ts",
117
+ " ✓ login with valid token (2ms)",
118
+ " ✓ login with expired token refreshes (3ms)",
119
+ " ✓ login with invalid token rejects (1ms)",
120
+ "",
121
+ "3 passed | 0 failed"
122
+ ]
123
+ }],
124
+ tokens: {
125
+ input: 4978,
126
+ output: 312
127
+ }
128
+ },
129
+ {
130
+ role: "agent",
131
+ content: "Fixed! The bug was comparing jwt.exp (seconds since epoch) with Date.now() (milliseconds). Expired tokens now trigger a refresh instead of throwing.",
132
+ tokens: {
133
+ input: 5202,
134
+ output: 178
135
+ }
136
+ },
137
+ {
138
+ role: "user",
139
+ content: "Nice. Can you also add rate limiting to the login endpoint?",
140
+ tokens: {
141
+ input: 116,
142
+ output: 0
143
+ }
144
+ },
145
+ {
146
+ role: "agent",
147
+ thinking: "The user wants rate limiting on login. I should check if there's existing rate limiting infrastructure before building from scratch. Let me search for rate-limit patterns in the codebase.",
148
+ content: "I'll check what rate limiting infrastructure exists.",
149
+ toolCalls: [{
150
+ tool: "Grep",
151
+ args: "rateLimit|rate-limit",
152
+ output: [
153
+ "src/middleware/rate-limit.ts",
154
+ "src/config.ts",
155
+ "See https://docs.example.com/api/rate-limiting for API docs"
156
+ ]
157
+ }, {
158
+ tool: "Read",
159
+ args: "src/middleware/rate-limit.ts",
160
+ output: [
161
+ "import { RateLimiter } from '@lib/rate-limit'",
162
+ "",
163
+ "export function createLimiter(opts: { window: number; max: number }) {",
164
+ " return new RateLimiter(opts)",
165
+ "}"
166
+ ]
167
+ }],
168
+ tokens: {
169
+ input: 6490,
170
+ output: 846
171
+ }
172
+ },
173
+ {
174
+ role: "agent",
175
+ thinking: "Good — there's already a RateLimiter abstraction. I just need to create an instance with appropriate settings (5 attempts per 60s window seems reasonable for login) and wire it into the auth route as middleware.",
176
+ content: "Good, there's already a rate limiter. I'll wire it into the auth route.",
177
+ toolCalls: [{
178
+ tool: "Edit",
179
+ args: "src/routes/auth.ts",
180
+ output: [
181
+ "+ import { createLimiter } from '../middleware/rate-limit'",
182
+ "+",
183
+ "+ const loginLimiter = createLimiter({ window: 60_000, max: 5 })",
184
+ "+",
185
+ " router.post('/login', async (req, res) => {",
186
+ "+ const limited = await loginLimiter.check(req.ip)",
187
+ "+ if (limited) return res.status(429).json({ error: 'Too many attempts' })",
188
+ " const { token } = req.body"
189
+ ]
190
+ }],
191
+ tokens: {
192
+ input: 7782,
193
+ output: 756
194
+ }
195
+ },
196
+ {
197
+ role: "agent",
198
+ content: "Running the full test suite.",
199
+ toolCalls: [{
200
+ tool: "Bash",
201
+ args: "bun test",
202
+ output: [
203
+ "auth.test.ts ........... 3 passed",
204
+ "rate-limit.test.ts ..... 5 passed",
205
+ "routes/auth.test.ts .... 7 passed",
206
+ "",
207
+ "15 passed | 0 failed | 3 files"
208
+ ]
209
+ }],
210
+ tokens: {
211
+ input: 8204,
212
+ output: 334
213
+ }
214
+ },
215
+ {
216
+ role: "agent",
217
+ content: "Rate limiting added: 5 attempts per minute per IP on the login endpoint. All 15 tests pass.",
218
+ tokens: {
219
+ input: 8468,
220
+ output: 156
221
+ }
222
+ },
223
+ {
224
+ role: "user",
225
+ content: "Now add i18n support for error messages. We need 日本語 (Japanese) and Deutsch (German). 🌍",
226
+ tokens: {
227
+ input: 146,
228
+ output: 0
229
+ }
230
+ },
231
+ {
232
+ role: "agent",
233
+ thinking: "i18n for error messages — I need to create translation JSON files for ja and de, then build a simple t() function that resolves message keys by locale. I'll also need to update the error responses in auth.ts to use t() with the request's locale.",
234
+ content: "I'll create the translation files and update the error handling.",
235
+ toolCalls: [{
236
+ tool: "Write",
237
+ args: "src/i18n/ja.json",
238
+ output: [
239
+ "{",
240
+ " \"token_expired\": \"トークンの有効期限が切れました 🔧\",",
241
+ " \"rate_limited\": \"リクエストが多すぎます。後でもう一度お試しください\",",
242
+ " \"invalid_token\": \"無効なトークンです 🐛\",",
243
+ " \"login_success\": \"ログイン成功 ✅\"",
244
+ "}"
245
+ ]
246
+ }, {
247
+ tool: "Write",
248
+ args: "src/i18n/de.json",
249
+ output: [
250
+ "{",
251
+ " \"token_expired\": \"Token abgelaufen\",",
252
+ " \"rate_limited\": \"Zu viele Anfragen. Bitte versuchen Sie es später\",",
253
+ " \"invalid_token\": \"Ungültiges Token\",",
254
+ " \"login_success\": \"Anmeldung erfolgreich\"",
255
+ "}"
256
+ ]
257
+ }],
258
+ tokens: {
259
+ input: 17868,
260
+ output: 1134
261
+ }
262
+ },
263
+ {
264
+ role: "agent",
265
+ thinking: "Now I need the i18n loader module that imports the locale files and exports a t() function. The function should look up the key in the requested locale, falling back to English defaults. Then I'll wire it into the auth error responses.",
266
+ content: "Now I'll create the i18n loader and wire it into the error responses.",
267
+ toolCalls: [{
268
+ tool: "Write",
269
+ args: "src/i18n/index.ts",
270
+ output: [
271
+ "import ja from './ja.json'",
272
+ "import de from './de.json'",
273
+ "",
274
+ "const locales = { ja, de, en: { /* defaults */ } }",
275
+ "",
276
+ "export function t(key: string, lang = 'en'): string {",
277
+ " return locales[lang]?.[key] ?? locales.en[key] ?? key",
278
+ "}"
279
+ ]
280
+ }, {
281
+ tool: "Edit",
282
+ args: "src/auth.ts",
283
+ output: [
284
+ "+ import { t } from './i18n'",
285
+ "",
286
+ "- return res.status(429).json({ error: 'Too many attempts' })",
287
+ "+ return res.status(429).json({ error: t('rate_limited', req.lang) })"
288
+ ]
289
+ }],
290
+ tokens: {
291
+ input: 19752,
292
+ output: 890
293
+ }
294
+ },
295
+ {
296
+ role: "agent",
297
+ content: "Running tests with locale coverage.",
298
+ toolCalls: [{
299
+ tool: "Bash",
300
+ args: "bun test src/i18n/",
301
+ output: [
302
+ "i18n.test.ts",
303
+ " ✓ English fallback for unknown key",
304
+ " ✓ Japanese: トークンの有効期限が切れました 🔧",
305
+ " ✓ German: Token abgelaufen",
306
+ " ✓ Emoji preserved in translations 🐛✅",
307
+ "",
308
+ "4 passed | 0 failed"
309
+ ]
310
+ }],
311
+ tokens: {
312
+ input: 20468,
313
+ output: 356
314
+ }
315
+ },
316
+ {
317
+ role: "agent",
318
+ content: "i18n support added with Japanese (日本語) and German (Deutsch) translations. Error messages are now locale-aware. 🌍✅",
319
+ tokens: {
320
+ input: 20802,
321
+ output: 134
322
+ }
323
+ },
324
+ {
325
+ role: "user",
326
+ content: "Add a health check endpoint at /health that returns the service version.",
327
+ tokens: {
328
+ input: 104,
329
+ output: 0
330
+ }
331
+ },
332
+ {
333
+ role: "agent",
334
+ thinking: "Simple task — read the version from package.json and expose it on GET /health alongside uptime. Quick implementation.",
335
+ content: "Simple addition — reading package.json for the version, then adding the route.",
336
+ toolCalls: [{
337
+ tool: "Read",
338
+ args: "package.json",
339
+ output: [
340
+ "{",
341
+ " \"name\": \"auth-service\",",
342
+ " \"version\": \"2.4.1\",",
343
+ " ...",
344
+ "}"
345
+ ]
346
+ }, {
347
+ tool: "Edit",
348
+ args: "src/routes/health.ts",
349
+ output: [
350
+ "+ import { version } from '../../package.json'",
351
+ "+",
352
+ "+ router.get('/health', (req, res) => {",
353
+ "+ res.json({ status: 'ok', version, uptime: process.uptime() })",
354
+ "+ })"
355
+ ]
356
+ }],
357
+ tokens: {
358
+ input: 21578,
359
+ output: 468
360
+ }
361
+ },
362
+ {
363
+ role: "agent",
364
+ content: "Running final tests.",
365
+ toolCalls: [{
366
+ tool: "Bash",
367
+ args: "bun test",
368
+ output: [
369
+ "auth.test.ts ........... 3 passed",
370
+ "rate-limit.test.ts ..... 5 passed",
371
+ "routes/auth.test.ts .... 7 passed",
372
+ "routes/health.test.ts .. 2 passed",
373
+ "i18n.test.ts ........... 4 passed",
374
+ "",
375
+ "21 passed | 0 failed | 5 files"
376
+ ]
377
+ }],
378
+ tokens: {
379
+ input: 22046,
380
+ output: 290
381
+ }
382
+ },
383
+ {
384
+ role: "agent",
385
+ content: "All done! Summary of changes:\n• Fixed token expiry bug (seconds vs milliseconds)\n• Added rate limiting (5 req/min per IP)\n• Added i18n support (日本語 + Deutsch) 🌍\n• Added /health endpoint (v2.4.1)\n\nAll 21 tests pass. Ready to commit?",
386
+ tokens: {
387
+ input: 22468,
388
+ output: 224
389
+ }
390
+ }
391
+ ];
392
+ function generateStressScript() {
393
+ const exchanges = [];
394
+ const tools = [
395
+ "Read",
396
+ "Edit",
397
+ "Bash",
398
+ "Write",
399
+ "Grep",
400
+ "Glob"
401
+ ];
402
+ const files = [
403
+ "src/auth.ts",
404
+ "src/db.ts",
405
+ "src/routes/api.ts",
406
+ "src/middleware/cors.ts",
407
+ "src/utils/crypto.ts",
408
+ "src/config.ts",
409
+ "tests/integration.test.ts",
410
+ "src/i18n/日本語.json"
411
+ ];
412
+ let cumulativeInput = 4e3;
413
+ for (let i = 0; i < 200; i++) {
414
+ if (i % 5 === 0) {
415
+ const prompts = [
416
+ `Fix bug #${100 + i} in ${files[i % files.length]}`,
417
+ `Add feature: ${[
418
+ "caching",
419
+ "logging",
420
+ "retry",
421
+ "batching",
422
+ "バリデーション"
423
+ ][i % 5]}`,
424
+ `Refactor ${files[i % files.length]} — it's too complex 🔧`,
425
+ `Why is test #${i} failing? 🐛`,
426
+ `Add 日本語 translations for module ${i}`
427
+ ];
428
+ exchanges.push({
429
+ role: "user",
430
+ content: prompts[Math.floor(i / 5) % prompts.length],
431
+ tokens: {
432
+ input: 40 + i % 30,
433
+ output: 0
434
+ }
435
+ });
436
+ } else if (i % 5 === 4) exchanges.push({
437
+ role: "agent",
438
+ content: `Done with batch ${Math.floor(i / 5) + 1}. ${3 + i % 7} tests pass. ✅`,
439
+ tokens: {
440
+ input: cumulativeInput,
441
+ output: 45 + i % 60
442
+ }
443
+ });
444
+ else {
445
+ const tool = tools[i % tools.length];
446
+ const file = files[i % files.length];
447
+ cumulativeInput += 200 + i % 300;
448
+ exchanges.push({
449
+ role: "agent",
450
+ thinking: i % 3 === 0 ? `Analyzing ${file} for the reported issue...` : void 0,
451
+ content: `Working on ${file}...`,
452
+ toolCalls: [{
453
+ tool,
454
+ args: tool === "Bash" ? `bun test ${file.replace("src/", "tests/")}` : file,
455
+ output: [
456
+ `// ${tool} output for ${file}`,
457
+ `line ${i * 10 + 1}: processing...`,
458
+ tool === "Edit" ? `- old code at line ${i}` : ` existing line ${i}`,
459
+ tool === "Edit" ? `+ new code at line ${i}` : ` result: ok`,
460
+ i % 10 === 0 ? `✓ テスト合格 🎉` : `✓ done`
461
+ ]
462
+ }],
463
+ tokens: {
464
+ input: cumulativeInput,
465
+ output: 120 + i % 200
466
+ }
467
+ });
468
+ }
469
+ if (i === 80 || i === 160) exchanges.push({
470
+ role: "system",
471
+ content: `📦 Compaction #${i === 80 ? 1 : 2}: context cleared. Scrollback preserved above.`
472
+ });
473
+ }
474
+ return exchanges;
475
+ }
476
+ const INIT_STATE = {
477
+ exchanges: [{
478
+ id: 0,
479
+ role: "system",
480
+ content: [
481
+ "Coding agent simulation showcasing ListView:",
482
+ " • ListView — unified virtualized list with cache",
483
+ " • Cache mode — completed exchanges cached for performance",
484
+ " • OSC 8 hyperlinks — clickable file paths and URLs",
485
+ " • $token theme colors — semantic color tokens"
486
+ ].join("\n")
487
+ }],
488
+ scriptIdx: 0,
489
+ streamPhase: "done",
490
+ revealFraction: 1,
491
+ done: false,
492
+ compacting: false,
493
+ pulse: false,
494
+ ctrlDPending: false,
495
+ contextBaseline: 0,
496
+ offScript: false,
497
+ nextId: 1,
498
+ autoTyping: null
499
+ };
500
+ function formatTokens(n) {
501
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
502
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
503
+ return String(n);
504
+ }
505
+ function formatCost(inputTokens, outputTokens) {
506
+ const cost = (inputTokens * 15 + outputTokens * 75) / 1e6;
507
+ if (cost < .01) return `$${cost.toFixed(4)}`;
508
+ return `$${cost.toFixed(2)}`;
509
+ }
510
+ /**
511
+ * Compute token stats for display and compaction.
512
+ *
513
+ * Token values in the script are CUMULATIVE — each exchange's `input` represents
514
+ * the total context consumed at that point. So:
515
+ * - `currentContext`: the LAST exchange's input tokens (= current context window usage)
516
+ * - `totalCost`: sum of all (input + output) for cost calculation (each API call costs)
517
+ */
518
+ function computeCumulativeTokens(exchanges) {
519
+ let input = 0;
520
+ let output = 0;
521
+ let currentContext = 0;
522
+ for (const ex of exchanges) if (ex.tokens) {
523
+ input += ex.tokens.input;
524
+ output += ex.tokens.output;
525
+ if (ex.tokens.input > currentContext) currentContext = ex.tokens.input;
526
+ }
527
+ return {
528
+ input,
529
+ output,
530
+ currentContext
531
+ };
532
+ }
533
+ /** Next scripted user message for footer placeholder. */
534
+ function getNextMessage(state, script, autoMode) {
535
+ if (autoMode || state.done || state.offScript || state.streamPhase !== "done" || state.exchanges.length === 0) return "";
536
+ const entry = script[state.scriptIdx];
537
+ return entry?.role === "user" ? entry.content : "";
538
+ }
539
+ function createDemoUpdate(script, fastMode, autoMode) {
540
+ function addExchange(state, entry) {
541
+ const exchange = {
542
+ ...entry,
543
+ id: state.nextId
544
+ };
545
+ return {
546
+ ...state,
547
+ exchanges: [...state.exchanges, exchange],
548
+ nextId: state.nextId + 1
549
+ };
550
+ }
551
+ function startStreaming(state, entry) {
552
+ const s = addExchange(state, entry);
553
+ if (entry.role !== "agent" || fastMode) return [{
554
+ ...s,
555
+ streamPhase: "done",
556
+ revealFraction: 1
557
+ }, []];
558
+ if (entry.thinking) return [{
559
+ ...s,
560
+ streamPhase: "thinking",
561
+ revealFraction: 0
562
+ }, [fx.delay(1200, { type: "endThinking" })]];
563
+ return [{
564
+ ...s,
565
+ streamPhase: "streaming",
566
+ revealFraction: 0
567
+ }, [fx.interval(50, { type: "streamTick" }, "reveal")]];
568
+ }
569
+ function autoAdvanceEffects(state) {
570
+ if (state.done || state.compacting || state.streamPhase !== "done") return [];
571
+ const next = script[state.scriptIdx];
572
+ if (!next) return autoMode ? [fx.delay(0, { type: "autoAdvance" })] : [];
573
+ if (autoMode || next.role !== "user") return [fx.delay(fastMode ? 100 : 400, { type: "autoAdvance" })];
574
+ return [];
575
+ }
576
+ function doAdvance(state, extraEffects = []) {
577
+ if (state.done || state.compacting || state.streamPhase !== "done") return state;
578
+ if (state.scriptIdx >= script.length) return autoMode ? {
579
+ ...state,
580
+ done: true
581
+ } : state;
582
+ const entry = script[state.scriptIdx];
583
+ let s = {
584
+ ...state,
585
+ scriptIdx: state.scriptIdx + 1
586
+ };
587
+ const effects = [...extraEffects];
588
+ let streamFx;
589
+ [s, streamFx] = startStreaming(s, entry);
590
+ effects.push(...streamFx);
591
+ if (fastMode) {
592
+ while (s.scriptIdx < script.length && script[s.scriptIdx].role !== "user") {
593
+ [s, streamFx] = startStreaming({
594
+ ...s,
595
+ scriptIdx: s.scriptIdx + 1
596
+ }, script[s.scriptIdx]);
597
+ effects.push(...streamFx);
598
+ }
599
+ effects.push(...autoAdvanceEffects(s));
600
+ } else if (entry.role === "user") {
601
+ if (s.scriptIdx < script.length && script[s.scriptIdx].role === "agent") {
602
+ [s, streamFx] = startStreaming({
603
+ ...s,
604
+ scriptIdx: s.scriptIdx + 1
605
+ }, script[s.scriptIdx]);
606
+ effects.push(...streamFx);
607
+ }
608
+ }
609
+ return [s, effects];
610
+ }
611
+ return function update(state, msg) {
612
+ switch (msg.type) {
613
+ case "mount": return doAdvance(state, [fx.interval(400, { type: "pulse" }, "pulse")]);
614
+ case "advance":
615
+ case "autoAdvance":
616
+ if (autoMode && !fastMode && state.streamPhase === "done" && !state.done && !state.compacting) {
617
+ const next = script[state.scriptIdx];
618
+ if (next?.role === "user") return [{
619
+ ...state,
620
+ autoTyping: {
621
+ full: next.content,
622
+ revealed: 0
623
+ }
624
+ }, [fx.interval(30, { type: "typingTick" }, "typing")]];
625
+ }
626
+ if (autoMode && state.scriptIdx >= script.length && state.streamPhase === "done") return {
627
+ ...state,
628
+ done: true
629
+ };
630
+ return doAdvance(state);
631
+ case "typingTick": {
632
+ if (!state.autoTyping) return state;
633
+ const next = state.autoTyping.revealed + 1;
634
+ if (next >= state.autoTyping.full.length) return [{
635
+ ...state,
636
+ autoTyping: {
637
+ ...state.autoTyping,
638
+ revealed: state.autoTyping.full.length
639
+ }
640
+ }, [fx.cancel("typing"), fx.delay(300, { type: "autoTypingDone" })]];
641
+ return {
642
+ ...state,
643
+ autoTyping: {
644
+ ...state.autoTyping,
645
+ revealed: next
646
+ }
647
+ };
648
+ }
649
+ case "autoTypingDone": return doAdvance({
650
+ ...state,
651
+ autoTyping: null
652
+ });
653
+ case "endThinking": return [{
654
+ ...state,
655
+ streamPhase: "streaming",
656
+ revealFraction: 0
657
+ }, [fx.interval(50, { type: "streamTick" }, "reveal")]];
658
+ case "streamTick": {
659
+ const last = state.exchanges[state.exchanges.length - 1];
660
+ const rate = last?.thinking ? .08 : .12;
661
+ const frac = Math.min(state.revealFraction + rate, 1);
662
+ if (frac < 1) return {
663
+ ...state,
664
+ revealFraction: frac
665
+ };
666
+ const tools = last?.toolCalls ?? [];
667
+ if (tools.length > 0) return [{
668
+ ...state,
669
+ streamPhase: "tools",
670
+ revealFraction: 1
671
+ }, [fx.cancel("reveal"), fx.delay(600 * tools.length, { type: "endTools" })]];
672
+ const s = {
673
+ ...state,
674
+ streamPhase: "done",
675
+ revealFraction: 1
676
+ };
677
+ return [s, [fx.cancel("reveal"), ...autoAdvanceEffects(s)]];
678
+ }
679
+ case "endTools": {
680
+ const s = {
681
+ ...state,
682
+ streamPhase: "done"
683
+ };
684
+ return [s, autoAdvanceEffects(s)];
685
+ }
686
+ case "submit": {
687
+ const base = state.streamPhase !== "done" ? {
688
+ ...state,
689
+ streamPhase: "done",
690
+ revealFraction: 1,
691
+ autoTyping: null
692
+ } : state.autoTyping ? {
693
+ ...state,
694
+ autoTyping: null
695
+ } : state;
696
+ const cancelEffects = state.streamPhase !== "done" ? [fx.cancel("reveal"), fx.cancel("typing")] : [fx.cancel("typing")];
697
+ if (!msg.text.trim()) return [base, cancelEffects];
698
+ if (base.done) return base;
699
+ const s = addExchange(base, {
700
+ role: "user",
701
+ content: msg.text,
702
+ tokens: {
703
+ input: msg.text.length * 4,
704
+ output: 0
705
+ }
706
+ });
707
+ if (s.scriptIdx < script.length) {
708
+ let nextIdx = s.scriptIdx;
709
+ while (nextIdx < script.length && script[nextIdx].role === "user") nextIdx++;
710
+ return [{
711
+ ...s,
712
+ scriptIdx: nextIdx
713
+ }, [...cancelEffects, fx.delay(150, { type: "autoAdvance" })]];
714
+ }
715
+ return [{
716
+ ...s,
717
+ offScript: true
718
+ }, [...cancelEffects, fx.delay(150, { type: "respondRandom" })]];
719
+ }
720
+ case "respondRandom": {
721
+ const resp = RANDOM_AGENT_RESPONSES[Math.floor(Math.random() * RANDOM_AGENT_RESPONSES.length)];
722
+ const [s, effects] = startStreaming(state, resp);
723
+ return [{
724
+ ...s,
725
+ offScript: true
726
+ }, effects];
727
+ }
728
+ case "compact": {
729
+ if (state.done || state.compacting) return state;
730
+ const cumulative = computeCumulativeTokens(state.exchanges);
731
+ return [{
732
+ ...state,
733
+ streamPhase: "done",
734
+ revealFraction: 1,
735
+ compacting: true,
736
+ contextBaseline: cumulative.currentContext,
737
+ exchanges: state.exchanges,
738
+ autoTyping: null
739
+ }, [
740
+ fx.cancel("reveal"),
741
+ fx.cancel("typing"),
742
+ fx.delay(fastMode ? 300 : 3e3, { type: "compactDone" })
743
+ ]];
744
+ }
745
+ case "compactDone": return doAdvance({
746
+ ...state,
747
+ compacting: false
748
+ });
749
+ case "pulse": return {
750
+ ...state,
751
+ pulse: !state.pulse
752
+ };
753
+ case "setCtrlDPending": return {
754
+ ...state,
755
+ ctrlDPending: msg.pending
756
+ };
757
+ default: return state;
758
+ }
759
+ };
760
+ }
761
+ //#endregion
762
+ //#region apps/aichat/components.tsx
763
+ var import_jsx_runtime = require_jsx_runtime();
764
+ /** Split content into a short title (first sentence) and the remaining body.
765
+ * Title must be ≤40 chars to fit on the header line with metadata. */
766
+ function splitTitleBody(content) {
767
+ const match = content.match(/^(.+?[.!?])\s+(.+)$/s);
768
+ if (match && match[1].length <= 40) return {
769
+ title: match[1],
770
+ body: match[2]
771
+ };
772
+ if (content.length <= 40) return {
773
+ title: content,
774
+ body: ""
775
+ };
776
+ return {
777
+ title: "",
778
+ body: content
779
+ };
780
+ }
781
+ /** Render a line with auto-linked URLs. */
782
+ function LinkifiedLine({ text, dim, color }) {
783
+ const parts = [];
784
+ let lastIndex = 0;
785
+ let match;
786
+ URL_RE.lastIndex = 0;
787
+ while ((match = URL_RE.exec(text)) !== null) {
788
+ if (match.index > lastIndex) parts.push(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
789
+ dim,
790
+ color,
791
+ children: text.slice(lastIndex, match.index)
792
+ }, `t${lastIndex}`));
793
+ const url = match[0];
794
+ parts.push(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Link, {
795
+ href: url,
796
+ dim,
797
+ children: url
798
+ }, `l${match.index}`));
799
+ lastIndex = match.index + url.length;
800
+ }
801
+ if (lastIndex < text.length) parts.push(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
802
+ dim,
803
+ color,
804
+ children: text.slice(lastIndex)
805
+ }, `t${lastIndex}`));
806
+ if (parts.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
807
+ dim,
808
+ color,
809
+ children: text
810
+ });
811
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: parts });
812
+ }
813
+ /** Thinking block — shows thinking text preview in the body. */
814
+ function ThinkingBlock({ text, done }) {
815
+ if (done) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
816
+ color: "$muted",
817
+ italic: true,
818
+ children: "▸ thought"
819
+ });
820
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
821
+ color: "$muted",
822
+ wrap: "truncate",
823
+ italic: true,
824
+ children: text
825
+ });
826
+ }
827
+ /** Tool call with lifecycle: spinner -> output -> checkmark. */
828
+ function ToolCallBlock({ call, phase }) {
829
+ const color = TOOL_COLORS[call.tool] ?? "$muted";
830
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
831
+ flexDirection: "column",
832
+ marginTop: 0,
833
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [
834
+ phase === "running" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, { type: "dots" }), " "] }) : phase === "done" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
835
+ color: "$success",
836
+ children: "✓ "
837
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
838
+ color: "$muted",
839
+ children: "○ "
840
+ }),
841
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
842
+ color,
843
+ bold: true,
844
+ children: call.tool
845
+ }),
846
+ " ",
847
+ call.tool === "Bash" || call.tool === "Grep" || call.tool === "Glob" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
848
+ color: "$muted",
849
+ children: call.args
850
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Link, {
851
+ href: `file://${call.args}`,
852
+ children: call.args
853
+ })
854
+ ] }), phase === "done" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
855
+ flexDirection: "column",
856
+ paddingLeft: 2,
857
+ children: call.output.map((line, i) => {
858
+ if (line.startsWith("+")) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkifiedLine, {
859
+ text: line,
860
+ color: "$success"
861
+ }, i);
862
+ if (line.startsWith("-")) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkifiedLine, {
863
+ text: line,
864
+ color: "$error"
865
+ }, i);
866
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkifiedLine, { text: line }, i);
867
+ })
868
+ })]
869
+ });
870
+ }
871
+ /** Streaming text — reveals content word by word. */
872
+ function StreamingText({ fullText, revealFraction, showCursor }) {
873
+ if (revealFraction >= 1) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: fullText });
874
+ const words = fullText.split(/(\s+)/);
875
+ const totalWords = words.filter((w) => w.trim()).length;
876
+ const revealWords = Math.ceil(totalWords * revealFraction);
877
+ let wordCount = 0;
878
+ let revealedText = "";
879
+ for (const word of words) {
880
+ if (word.trim()) {
881
+ wordCount++;
882
+ if (wordCount > revealWords) break;
883
+ }
884
+ revealedText += word;
885
+ }
886
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [revealedText, showCursor && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
887
+ color: "$primary",
888
+ children: "▌"
889
+ })] });
890
+ }
891
+ function ExchangeItem({ exchange, streamPhase, revealFraction, pulse, isLatest, isFirstInGroup, isLastInGroup }) {
892
+ if (exchange.role === "system") return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
893
+ flexDirection: "column",
894
+ children: [
895
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: " " }),
896
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
897
+ bold: true,
898
+ children: "AI Chat"
899
+ }),
900
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: " " }),
901
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
902
+ color: "$muted",
903
+ children: exchange.content
904
+ }),
905
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: " " })
906
+ ]
907
+ });
908
+ if (exchange.role === "user") return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
909
+ paddingX: 1,
910
+ flexDirection: "row",
911
+ backgroundColor: "$surface-bg",
912
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
913
+ bold: true,
914
+ color: "$focusring",
915
+ children: ["❯", " "]
916
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
917
+ flexShrink: 1,
918
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: exchange.content })
919
+ })]
920
+ });
921
+ const phase = isLatest ? streamPhase : "done";
922
+ const fraction = isLatest ? revealFraction : 1;
923
+ const toolCalls = exchange.toolCalls ?? [];
924
+ const toolRevealCount = phase === "tools" || phase === "done" ? toolCalls.length : 0;
925
+ const hasOperations = toolCalls.length > 0 || !!exchange.thinking;
926
+ const metaParts = [];
927
+ if (exchange.tokens && phase === "done") metaParts.push(`${formatTokens(exchange.tokens.output)} tokens`);
928
+ if (exchange.thinking && (phase === "done" || phase === "streaming")) metaParts.push("thought for 1s");
929
+ const metaStr = metaParts.length > 0 ? ` (${metaParts.join(" · ")})` : "";
930
+ const { title, body } = splitTitleBody(exchange.content);
931
+ const bulletColor = hasOperations ? "$success" : "$muted";
932
+ const contentText = title ? body : exchange.content;
933
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
934
+ flexDirection: "column",
935
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
936
+ bold: true,
937
+ color: bulletColor,
938
+ dimColor: hasOperations && !pulse && phase !== "done",
939
+ children: "●"
940
+ }), phase === "thinking" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
941
+ color: "$muted",
942
+ italic: true,
943
+ children: [
944
+ " ",
945
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, { type: "dots" }),
946
+ " thinking"
947
+ ]
948
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [title && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [" ", title] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
949
+ color: "$muted",
950
+ children: metaStr
951
+ })] })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
952
+ flexDirection: "column",
953
+ borderStyle: "bold",
954
+ borderColor: "$border",
955
+ borderLeft: true,
956
+ borderRight: false,
957
+ borderTop: false,
958
+ borderBottom: false,
959
+ paddingLeft: 1,
960
+ children: [
961
+ exchange.thinking && (phase === "thinking" || phase === "streaming") && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThinkingBlock, {
962
+ text: exchange.thinking,
963
+ done: phase !== "thinking"
964
+ }),
965
+ (phase === "streaming" || phase === "tools" || phase === "done") && contentText && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StreamingText, {
966
+ fullText: contentText,
967
+ revealFraction: phase === "streaming" ? fraction : 1,
968
+ showCursor: phase === "streaming" && fraction < 1
969
+ }),
970
+ toolRevealCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
971
+ flexDirection: "column",
972
+ children: toolCalls.map((call, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolCallBlock, {
973
+ call,
974
+ phase: phase === "done" ? "done" : i < toolRevealCount - 1 ? "done" : "running"
975
+ }, i))
976
+ })
977
+ ]
978
+ })]
979
+ });
980
+ }
981
+ function StatusBar({ exchanges, compacting, done, elapsed, contextBaseline = 0, ctrlDPending = false }) {
982
+ const cumulative = computeCumulativeTokens(exchanges);
983
+ const cost = formatCost(cumulative.input, cumulative.output);
984
+ const elapsedStr = `${Math.floor(elapsed / 60)}:${(elapsed % 60).toString().padStart(2, "0")}`;
985
+ const CTX_W = 20;
986
+ const ctxFrac = Math.max(0, cumulative.currentContext - contextBaseline) / CONTEXT_WINDOW;
987
+ const ctxFilled = Math.round(Math.min(ctxFrac, 1) * CTX_W);
988
+ const ctxPct = Math.round(ctxFrac * 100);
989
+ const ctxColor = ctxPct > 100 ? "$error" : ctxPct > 80 ? "$warning" : "$primary";
990
+ const ctxBar = "█".repeat(ctxFilled) + "░".repeat(CTX_W - ctxFilled);
991
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
992
+ flexDirection: "row",
993
+ justifyContent: "space-between",
994
+ width: "100%",
995
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
996
+ color: "$muted",
997
+ wrap: "truncate",
998
+ children: [
999
+ elapsedStr,
1000
+ " ",
1001
+ ctrlDPending ? "Ctrl-D again to exit" : compacting ? "compacting..." : "esc quit"
1002
+ ]
1003
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1004
+ color: ctxPct > 80 ? ctxColor : "$muted",
1005
+ wrap: "truncate",
1006
+ children: [
1007
+ "ctx ",
1008
+ ctxBar,
1009
+ " ",
1010
+ ctxPct,
1011
+ "%",
1012
+ " ",
1013
+ cost
1014
+ ]
1015
+ })]
1016
+ });
1017
+ }
1018
+ const AUTO_SUBMIT_DELAY = 1e4;
1019
+ function DemoFooter({ controlRef, onSubmit, streamPhase, done, compacting, exchanges, contextBaseline = 0, ctrlDPending = false, nextMessage = "", autoTypingText = null }) {
1020
+ const terminalFocused = useTerminalFocused();
1021
+ const [inputText, setInputText] = useState("");
1022
+ const inputTextRef = useRef(inputText);
1023
+ inputTextRef.current = inputText;
1024
+ const startRef = useRef(Date.now());
1025
+ const [elapsed, setElapsed] = useState(0);
1026
+ useEffect(() => {
1027
+ const timer = setInterval(() => setElapsed(Math.floor((Date.now() - startRef.current) / 1e3)), 1e3);
1028
+ return () => clearInterval(timer);
1029
+ }, []);
1030
+ const [randomIdx, setRandomIdx] = useState(() => Math.floor(Math.random() * RANDOM_USER_COMMANDS.length));
1031
+ const randomPlaceholder = RANDOM_USER_COMMANDS[randomIdx % RANDOM_USER_COMMANDS.length];
1032
+ const effectiveMessage = nextMessage || randomPlaceholder;
1033
+ const placeholder = !terminalFocused ? "Click to focus" : ctrlDPending ? "Press Ctrl-D again to exit" : effectiveMessage;
1034
+ const handleSubmit = useCallback((text) => {
1035
+ if (!text.trim() && effectiveMessage) onSubmit(effectiveMessage);
1036
+ else onSubmit(text);
1037
+ setInputText("");
1038
+ setRandomIdx((i) => i + 1);
1039
+ }, [onSubmit, effectiveMessage]);
1040
+ controlRef.current = { submit: () => handleSubmit(inputTextRef.current) };
1041
+ const autoSubmitRef = useRef(null);
1042
+ useEffect(() => {
1043
+ if (autoSubmitRef.current) clearTimeout(autoSubmitRef.current);
1044
+ if (done || compacting || streamPhase !== "done" || !effectiveMessage || inputText || autoTypingText || !terminalFocused) return;
1045
+ autoSubmitRef.current = setTimeout(() => onSubmit(effectiveMessage), AUTO_SUBMIT_DELAY);
1046
+ return () => {
1047
+ if (autoSubmitRef.current) clearTimeout(autoSubmitRef.current);
1048
+ };
1049
+ }, [
1050
+ done,
1051
+ compacting,
1052
+ streamPhase,
1053
+ effectiveMessage,
1054
+ inputText,
1055
+ autoTypingText,
1056
+ onSubmit
1057
+ ]);
1058
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1059
+ flexDirection: "column",
1060
+ width: "100%",
1061
+ children: [
1062
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: " " }),
1063
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1064
+ flexDirection: "row",
1065
+ borderStyle: "round",
1066
+ borderColor: !done && terminalFocused ? "$focusborder" : "$inputborder",
1067
+ paddingX: 1,
1068
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1069
+ bold: true,
1070
+ color: "$focusring",
1071
+ children: ["❯", " "]
1072
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1073
+ flexShrink: 1,
1074
+ flexGrow: 1,
1075
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextInput, {
1076
+ value: autoTypingText ?? inputText,
1077
+ onChange: autoTypingText ? () => {} : setInputText,
1078
+ onSubmit: handleSubmit,
1079
+ placeholder,
1080
+ isActive: !done && !autoTypingText && terminalFocused
1081
+ })
1082
+ })]
1083
+ }),
1084
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1085
+ paddingX: 2,
1086
+ width: "100%",
1087
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusBar, {
1088
+ exchanges,
1089
+ compacting,
1090
+ done,
1091
+ elapsed,
1092
+ contextBaseline,
1093
+ ctrlDPending
1094
+ })
1095
+ })
1096
+ ]
1097
+ });
1098
+ }
1099
+ //#endregion
1100
+ //#region apps/aichat/index.tsx
1101
+ /**
1102
+ * AI Chat — Coding Agent Demo
1103
+ *
1104
+ * Showcases ListView with streaming, tool calls, context tracking.
1105
+ * TEA state machine drives all animation; ListView caches completed
1106
+ * exchanges while live content stays in the React tree.
1107
+ *
1108
+ * Flags: --auto (auto-advance) --fast (skip animation) --stress (200 exchanges)
1109
+ */
1110
+ const meta = {
1111
+ name: "AI Coding Agent",
1112
+ description: "Coding agent showcase — ListView, streaming, context tracking",
1113
+ demo: true,
1114
+ features: [
1115
+ "ListView",
1116
+ "cache",
1117
+ "inline mode",
1118
+ "streaming",
1119
+ "OSC 8 links"
1120
+ ]
1121
+ };
1122
+ function AIChat({ script, autoStart, fastMode }) {
1123
+ const exit = useExit();
1124
+ const { rows: termRows } = useWindowSize();
1125
+ const [state, send] = useTea(INIT_STATE, useMemo(() => createDemoUpdate(script, fastMode, autoStart), [
1126
+ script,
1127
+ fastMode,
1128
+ autoStart
1129
+ ]));
1130
+ const footerControlRef = useRef({ submit: () => {} });
1131
+ useEffect(() => send({ type: "mount" }), [send]);
1132
+ useAutoCompact(state, send);
1133
+ useAutoExit(autoStart, state.done, exit);
1134
+ useKeyBindings(state, send, footerControlRef);
1135
+ const renderExchange = useCallback((exchange, index, _meta) => {
1136
+ const isLatest = index === state.exchanges.length - 1;
1137
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1138
+ flexDirection: "column",
1139
+ children: [
1140
+ index > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: " " }),
1141
+ state.compacting && isLatest && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CompactingOverlay, {}),
1142
+ state.done && autoStart && isLatest && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SessionComplete, {}),
1143
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExchangeItem, {
1144
+ exchange,
1145
+ streamPhase: state.streamPhase,
1146
+ revealFraction: state.revealFraction,
1147
+ pulse: state.pulse,
1148
+ isLatest,
1149
+ isFirstInGroup: exchange.role !== (index > 0 ? state.exchanges[index - 1].role : null),
1150
+ isLastInGroup: exchange.role !== (index < state.exchanges.length - 1 ? state.exchanges[index + 1].role : null)
1151
+ })
1152
+ ]
1153
+ });
1154
+ }, [state, autoStart]);
1155
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box, {
1156
+ flexDirection: "column",
1157
+ paddingX: 1,
1158
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ListView, {
1159
+ items: state.exchanges,
1160
+ getKey: (ex) => ex.id,
1161
+ height: termRows,
1162
+ estimateHeight: 6,
1163
+ renderItem: renderExchange,
1164
+ scrollTo: state.exchanges.length - 1,
1165
+ cache: {
1166
+ mode: "virtual",
1167
+ isCacheable: (_ex, index) => index < state.exchanges.length - 1
1168
+ },
1169
+ listFooter: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DemoFooter, {
1170
+ controlRef: footerControlRef,
1171
+ onSubmit: (text) => send({
1172
+ type: "submit",
1173
+ text
1174
+ }),
1175
+ streamPhase: state.streamPhase,
1176
+ done: state.done,
1177
+ compacting: state.compacting,
1178
+ exchanges: state.exchanges,
1179
+ contextBaseline: state.contextBaseline,
1180
+ ctrlDPending: state.ctrlDPending,
1181
+ nextMessage: getNextMessage(state, script, autoStart),
1182
+ autoTypingText: state.autoTyping ? state.autoTyping.full.slice(0, state.autoTyping.revealed) : null
1183
+ })
1184
+ })
1185
+ });
1186
+ }
1187
+ async function main() {
1188
+ try {
1189
+ var _usingCtx$1 = _usingCtx();
1190
+ const args = process.argv.slice(2);
1191
+ const script = args.includes("--stress") ? generateStressScript() : SCRIPT;
1192
+ const mode = args.includes("--inline") ? "inline" : "fullscreen";
1193
+ await _usingCtx$1.u(await run(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AIChat, {
1194
+ script,
1195
+ autoStart: args.includes("--auto"),
1196
+ fastMode: args.includes("--fast")
1197
+ }), {
1198
+ mode,
1199
+ focusReporting: true
1200
+ })).waitUntilExit();
1201
+ } catch (_) {
1202
+ _usingCtx$1.e = _;
1203
+ } finally {
1204
+ _usingCtx$1.d();
1205
+ }
1206
+ }
1207
+ if (import.meta.main) await main();
1208
+ function useAutoCompact(state, send) {
1209
+ useEffect(() => {
1210
+ if (state.done || state.compacting) return;
1211
+ const cumulative = computeCumulativeTokens(state.exchanges);
1212
+ if (Math.max(0, cumulative.currentContext - state.contextBaseline) >= 2e5 * .95) send({ type: "compact" });
1213
+ }, [
1214
+ state.exchanges,
1215
+ state.done,
1216
+ state.compacting,
1217
+ state.contextBaseline,
1218
+ send
1219
+ ]);
1220
+ }
1221
+ function useAutoExit(autoStart, done, exit) {
1222
+ useEffect(() => {
1223
+ if (!autoStart || !done) return;
1224
+ const timer = setTimeout(exit, 1e3);
1225
+ return () => clearTimeout(timer);
1226
+ }, [
1227
+ autoStart,
1228
+ done,
1229
+ exit
1230
+ ]);
1231
+ }
1232
+ function useKeyBindings(state, send, footerControlRef) {
1233
+ const lastCtrlDRef = useRef(0);
1234
+ useInput$1((input, key) => {
1235
+ if (key.escape) return "exit";
1236
+ if (key.ctrl && input === "d") {
1237
+ const now = Date.now();
1238
+ if (now - lastCtrlDRef.current < 500) return "exit";
1239
+ lastCtrlDRef.current = now;
1240
+ send({
1241
+ type: "setCtrlDPending",
1242
+ pending: true
1243
+ });
1244
+ return;
1245
+ }
1246
+ if (lastCtrlDRef.current > 0) {
1247
+ lastCtrlDRef.current = 0;
1248
+ send({
1249
+ type: "setCtrlDPending",
1250
+ pending: false
1251
+ });
1252
+ }
1253
+ if (key.tab) {
1254
+ if (state.done || state.compacting) return;
1255
+ footerControlRef.current.submit();
1256
+ return;
1257
+ }
1258
+ if (key.ctrl && input === "l") send({ type: "compact" });
1259
+ });
1260
+ }
1261
+ function CompactingOverlay() {
1262
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1263
+ flexDirection: "column",
1264
+ borderStyle: "round",
1265
+ borderColor: "$warning",
1266
+ paddingX: 1,
1267
+ overflow: "hidden",
1268
+ children: [
1269
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1270
+ color: "$warning",
1271
+ bold: true,
1272
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, { type: "arc" }), " Compacting context"]
1273
+ }),
1274
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: " " }),
1275
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1276
+ color: "$muted",
1277
+ children: "Freezing exchanges into terminal scrollback. Scroll up to review."
1278
+ })
1279
+ ]
1280
+ });
1281
+ }
1282
+ function SessionComplete() {
1283
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box, {
1284
+ flexDirection: "column",
1285
+ borderStyle: "round",
1286
+ borderColor: "$success",
1287
+ paddingX: 1,
1288
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
1289
+ color: "$success",
1290
+ bold: true,
1291
+ children: ["✓", " Session complete"]
1292
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
1293
+ color: "$muted",
1294
+ children: "Scroll up to review — colors, borders, and hyperlinks preserved in scrollback."
1295
+ })]
1296
+ });
1297
+ }
1298
+ //#endregion
1299
+ export { AIChat, CONTEXT_WINDOW, SCRIPT, generateStressScript, main, meta };