@silvery/examples 0.17.3 → 0.17.5

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 (111) hide show
  1. package/dist/UPNG-ShUlaTDh.mjs +5074 -0
  2. package/dist/__vite-browser-external-2447137e-Bopa5BFR.mjs +4 -0
  3. package/dist/_banner-A70_y2Vi.mjs +43 -0
  4. package/dist/ansi-0VXlUmNn.mjs +16397 -0
  5. package/dist/apng-B0gRaDVT.mjs +3 -0
  6. package/dist/apng-BTRDTfDW.mjs +68 -0
  7. package/dist/apps/aichat/index.mjs +1298 -0
  8. package/dist/apps/app-todo.mjs +138 -0
  9. package/dist/apps/async-data.mjs +203 -0
  10. package/dist/apps/cli-wizard.mjs +338 -0
  11. package/dist/apps/clipboard.mjs +197 -0
  12. package/dist/apps/components.mjs +863 -0
  13. package/dist/apps/data-explorer.mjs +482 -0
  14. package/dist/apps/dev-tools.mjs +396 -0
  15. package/dist/apps/explorer.mjs +697 -0
  16. package/dist/apps/gallery.mjs +765 -0
  17. package/dist/apps/inline-bench.mjs +115 -0
  18. package/dist/apps/kanban.mjs +279 -0
  19. package/dist/apps/layout-ref.mjs +186 -0
  20. package/dist/apps/outline.mjs +202 -0
  21. package/dist/apps/paste-demo.mjs +188 -0
  22. package/dist/apps/scroll.mjs +85 -0
  23. package/dist/apps/search-filter.mjs +286 -0
  24. package/dist/apps/selection.mjs +354 -0
  25. package/dist/apps/spatial-focus-demo.mjs +387 -0
  26. package/dist/apps/task-list.mjs +257 -0
  27. package/dist/apps/terminal-caps-demo.mjs +314 -0
  28. package/dist/apps/terminal.mjs +871 -0
  29. package/dist/apps/text-selection-demo.mjs +253 -0
  30. package/dist/apps/textarea.mjs +177 -0
  31. package/dist/apps/theme.mjs +660 -0
  32. package/dist/apps/transform.mjs +214 -0
  33. package/dist/apps/virtual-10k.mjs +421 -0
  34. package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
  35. package/dist/backends-Dj-11kZF.mjs +1179 -0
  36. package/dist/backends-U3QwStfO.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 +47 -0
  40. package/dist/components/hello.mjs +30 -0
  41. package/dist/components/progress-bar.mjs +58 -0
  42. package/dist/components/select-list.mjs +84 -0
  43. package/dist/components/spinner.mjs +56 -0
  44. package/dist/components/text-input.mjs +61 -0
  45. package/dist/components/virtual-list.mjs +50 -0
  46. package/dist/flexily-zero-adapter-ByVzLTFP.mjs +3374 -0
  47. package/dist/gif-B6NGH5gs.mjs +3 -0
  48. package/dist/gif-CfkOF-iG.mjs +71 -0
  49. package/dist/gifenc-BI4ihP_T.mjs +728 -0
  50. package/dist/key-mapping-5oYQdAQE.mjs +3 -0
  51. package/dist/key-mapping-D4LR1go6.mjs +130 -0
  52. package/dist/layout/dashboard.mjs +1203 -0
  53. package/dist/layout/live-resize.mjs +302 -0
  54. package/dist/layout/overflow.mjs +69 -0
  55. package/dist/layout/text-layout.mjs +334 -0
  56. package/dist/node-nsrAOjH4.mjs +1083 -0
  57. package/dist/plugins-CT0DdV_E.mjs +3056 -0
  58. package/dist/resvg-js-Cnk2o49d.mjs +201 -0
  59. package/dist/src-9ZhfQyzD.mjs +814 -0
  60. package/dist/src-CUUOuRH6.mjs +5322 -0
  61. package/dist/src-jO3Zuzjj.mjs +23538 -0
  62. package/dist/usingCtx-CsEf0xO3.mjs +57 -0
  63. package/dist/yoga-adapter-BSQHuMV9.mjs +237 -0
  64. package/package.json +21 -14
  65. package/_banner.tsx +0 -60
  66. package/apps/aichat/components.tsx +0 -469
  67. package/apps/aichat/index.tsx +0 -220
  68. package/apps/aichat/script.ts +0 -460
  69. package/apps/aichat/state.ts +0 -325
  70. package/apps/aichat/types.ts +0 -19
  71. package/apps/app-todo.tsx +0 -201
  72. package/apps/async-data.tsx +0 -196
  73. package/apps/cli-wizard.tsx +0 -332
  74. package/apps/clipboard.tsx +0 -183
  75. package/apps/components.tsx +0 -658
  76. package/apps/data-explorer.tsx +0 -490
  77. package/apps/dev-tools.tsx +0 -395
  78. package/apps/explorer.tsx +0 -731
  79. package/apps/gallery.tsx +0 -653
  80. package/apps/inline-bench.tsx +0 -138
  81. package/apps/kanban.tsx +0 -265
  82. package/apps/layout-ref.tsx +0 -173
  83. package/apps/outline.tsx +0 -160
  84. package/apps/panes/index.tsx +0 -203
  85. package/apps/paste-demo.tsx +0 -185
  86. package/apps/scroll.tsx +0 -80
  87. package/apps/search-filter.tsx +0 -240
  88. package/apps/selection.tsx +0 -346
  89. package/apps/spatial-focus-demo.tsx +0 -372
  90. package/apps/task-list.tsx +0 -271
  91. package/apps/terminal-caps-demo.tsx +0 -317
  92. package/apps/terminal.tsx +0 -784
  93. package/apps/text-selection-demo.tsx +0 -193
  94. package/apps/textarea.tsx +0 -155
  95. package/apps/theme.tsx +0 -515
  96. package/apps/transform.tsx +0 -229
  97. package/apps/virtual-10k.tsx +0 -405
  98. package/apps/vterm-demo/index.tsx +0 -216
  99. package/components/counter.tsx +0 -49
  100. package/components/hello.tsx +0 -38
  101. package/components/progress-bar.tsx +0 -52
  102. package/components/select-list.tsx +0 -54
  103. package/components/spinner.tsx +0 -44
  104. package/components/text-input.tsx +0 -61
  105. package/components/virtual-list.tsx +0 -56
  106. package/dist/cli.d.mts +0 -1
  107. package/dist/cli.mjs.map +0 -1
  108. package/layout/dashboard.tsx +0 -953
  109. package/layout/live-resize.tsx +0 -282
  110. package/layout/overflow.tsx +0 -51
  111. package/layout/text-layout.tsx +0 -283
@@ -0,0 +1,1298 @@
1
+ import { t as _usingCtx } from "../../usingCtx-CsEf0xO3.mjs";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { Box, Link, ListView, Spinner, Text, TextInput, fx, useTea, useTerminalFocused, useWindowSize } from "silvery";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
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
+ /** Split content into a short title (first sentence) and the remaining body.
764
+ * Title must be ≤40 chars to fit on the header line with metadata. */
765
+ function splitTitleBody(content) {
766
+ const match = content.match(/^(.+?[.!?])\s+(.+)$/s);
767
+ if (match && match[1].length <= 40) return {
768
+ title: match[1],
769
+ body: match[2]
770
+ };
771
+ if (content.length <= 40) return {
772
+ title: content,
773
+ body: ""
774
+ };
775
+ return {
776
+ title: "",
777
+ body: content
778
+ };
779
+ }
780
+ /** Render a line with auto-linked URLs. */
781
+ function LinkifiedLine({ text, dim, color }) {
782
+ const parts = [];
783
+ let lastIndex = 0;
784
+ let match;
785
+ URL_RE.lastIndex = 0;
786
+ while ((match = URL_RE.exec(text)) !== null) {
787
+ if (match.index > lastIndex) parts.push(/* @__PURE__ */ jsx(Text, {
788
+ dim,
789
+ color,
790
+ children: text.slice(lastIndex, match.index)
791
+ }, `t${lastIndex}`));
792
+ const url = match[0];
793
+ parts.push(/* @__PURE__ */ jsx(Link, {
794
+ href: url,
795
+ dim,
796
+ children: url
797
+ }, `l${match.index}`));
798
+ lastIndex = match.index + url.length;
799
+ }
800
+ if (lastIndex < text.length) parts.push(/* @__PURE__ */ jsx(Text, {
801
+ dim,
802
+ color,
803
+ children: text.slice(lastIndex)
804
+ }, `t${lastIndex}`));
805
+ if (parts.length === 0) return /* @__PURE__ */ jsx(Text, {
806
+ dim,
807
+ color,
808
+ children: text
809
+ });
810
+ return /* @__PURE__ */ jsx(Text, { children: parts });
811
+ }
812
+ /** Thinking block — shows thinking text preview in the body. */
813
+ function ThinkingBlock({ text, done }) {
814
+ if (done) return /* @__PURE__ */ jsx(Text, {
815
+ color: "$muted",
816
+ italic: true,
817
+ children: "▸ thought"
818
+ });
819
+ return /* @__PURE__ */ jsx(Text, {
820
+ color: "$muted",
821
+ wrap: "truncate",
822
+ italic: true,
823
+ children: text
824
+ });
825
+ }
826
+ /** Tool call with lifecycle: spinner -> output -> checkmark. */
827
+ function ToolCallBlock({ call, phase }) {
828
+ const color = TOOL_COLORS[call.tool] ?? "$muted";
829
+ return /* @__PURE__ */ jsxs(Box, {
830
+ flexDirection: "column",
831
+ marginTop: 0,
832
+ children: [/* @__PURE__ */ jsxs(Text, { children: [
833
+ phase === "running" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { type: "dots" }), " "] }) : phase === "done" ? /* @__PURE__ */ jsx(Text, {
834
+ color: "$success",
835
+ children: "✓ "
836
+ }) : /* @__PURE__ */ jsx(Text, {
837
+ color: "$muted",
838
+ children: "○ "
839
+ }),
840
+ /* @__PURE__ */ jsx(Text, {
841
+ color,
842
+ bold: true,
843
+ children: call.tool
844
+ }),
845
+ " ",
846
+ call.tool === "Bash" || call.tool === "Grep" || call.tool === "Glob" ? /* @__PURE__ */ jsx(Text, {
847
+ color: "$muted",
848
+ children: call.args
849
+ }) : /* @__PURE__ */ jsx(Link, {
850
+ href: `file://${call.args}`,
851
+ children: call.args
852
+ })
853
+ ] }), phase === "done" && /* @__PURE__ */ jsx(Box, {
854
+ flexDirection: "column",
855
+ paddingLeft: 2,
856
+ children: call.output.map((line, i) => {
857
+ if (line.startsWith("+")) return /* @__PURE__ */ jsx(LinkifiedLine, {
858
+ text: line,
859
+ color: "$success"
860
+ }, i);
861
+ if (line.startsWith("-")) return /* @__PURE__ */ jsx(LinkifiedLine, {
862
+ text: line,
863
+ color: "$error"
864
+ }, i);
865
+ return /* @__PURE__ */ jsx(LinkifiedLine, { text: line }, i);
866
+ })
867
+ })]
868
+ });
869
+ }
870
+ /** Streaming text — reveals content word by word. */
871
+ function StreamingText({ fullText, revealFraction, showCursor }) {
872
+ if (revealFraction >= 1) return /* @__PURE__ */ jsx(Text, { children: fullText });
873
+ const words = fullText.split(/(\s+)/);
874
+ const totalWords = words.filter((w) => w.trim()).length;
875
+ const revealWords = Math.ceil(totalWords * revealFraction);
876
+ let wordCount = 0;
877
+ let revealedText = "";
878
+ for (const word of words) {
879
+ if (word.trim()) {
880
+ wordCount++;
881
+ if (wordCount > revealWords) break;
882
+ }
883
+ revealedText += word;
884
+ }
885
+ return /* @__PURE__ */ jsxs(Text, { children: [revealedText, showCursor && /* @__PURE__ */ jsx(Text, {
886
+ color: "$primary",
887
+ children: "▌"
888
+ })] });
889
+ }
890
+ function ExchangeItem({ exchange, streamPhase, revealFraction, pulse, isLatest, isFirstInGroup, isLastInGroup }) {
891
+ if (exchange.role === "system") return /* @__PURE__ */ jsxs(Box, {
892
+ flexDirection: "column",
893
+ children: [
894
+ /* @__PURE__ */ jsx(Text, { children: " " }),
895
+ /* @__PURE__ */ jsx(Text, {
896
+ bold: true,
897
+ children: "AI Chat"
898
+ }),
899
+ /* @__PURE__ */ jsx(Text, { children: " " }),
900
+ /* @__PURE__ */ jsx(Text, {
901
+ color: "$muted",
902
+ children: exchange.content
903
+ }),
904
+ /* @__PURE__ */ jsx(Text, { children: " " })
905
+ ]
906
+ });
907
+ if (exchange.role === "user") return /* @__PURE__ */ jsxs(Box, {
908
+ paddingX: 1,
909
+ flexDirection: "row",
910
+ backgroundColor: "$surface-bg",
911
+ children: [/* @__PURE__ */ jsxs(Text, {
912
+ bold: true,
913
+ color: "$focusring",
914
+ children: ["❯", " "]
915
+ }), /* @__PURE__ */ jsx(Box, {
916
+ flexShrink: 1,
917
+ children: /* @__PURE__ */ jsx(Text, { children: exchange.content })
918
+ })]
919
+ });
920
+ const phase = isLatest ? streamPhase : "done";
921
+ const fraction = isLatest ? revealFraction : 1;
922
+ const toolCalls = exchange.toolCalls ?? [];
923
+ const toolRevealCount = phase === "tools" || phase === "done" ? toolCalls.length : 0;
924
+ const hasOperations = toolCalls.length > 0 || !!exchange.thinking;
925
+ const metaParts = [];
926
+ if (exchange.tokens && phase === "done") metaParts.push(`${formatTokens(exchange.tokens.output)} tokens`);
927
+ if (exchange.thinking && (phase === "done" || phase === "streaming")) metaParts.push("thought for 1s");
928
+ const metaStr = metaParts.length > 0 ? ` (${metaParts.join(" · ")})` : "";
929
+ const { title, body } = splitTitleBody(exchange.content);
930
+ const bulletColor = hasOperations ? "$success" : "$muted";
931
+ const contentText = title ? body : exchange.content;
932
+ return /* @__PURE__ */ jsxs(Box, {
933
+ flexDirection: "column",
934
+ children: [/* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
935
+ bold: true,
936
+ color: bulletColor,
937
+ dimColor: hasOperations && !pulse && phase !== "done",
938
+ children: "●"
939
+ }), phase === "thinking" ? /* @__PURE__ */ jsxs(Text, {
940
+ color: "$muted",
941
+ italic: true,
942
+ children: [
943
+ " ",
944
+ /* @__PURE__ */ jsx(Spinner, { type: "dots" }),
945
+ " thinking"
946
+ ]
947
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [title && /* @__PURE__ */ jsxs(Text, { children: [" ", title] }), /* @__PURE__ */ jsx(Text, {
948
+ color: "$muted",
949
+ children: metaStr
950
+ })] })] }), /* @__PURE__ */ jsxs(Box, {
951
+ flexDirection: "column",
952
+ borderStyle: "bold",
953
+ borderColor: "$border",
954
+ borderLeft: true,
955
+ borderRight: false,
956
+ borderTop: false,
957
+ borderBottom: false,
958
+ paddingLeft: 1,
959
+ children: [
960
+ exchange.thinking && (phase === "thinking" || phase === "streaming") && /* @__PURE__ */ jsx(ThinkingBlock, {
961
+ text: exchange.thinking,
962
+ done: phase !== "thinking"
963
+ }),
964
+ (phase === "streaming" || phase === "tools" || phase === "done") && contentText && /* @__PURE__ */ jsx(StreamingText, {
965
+ fullText: contentText,
966
+ revealFraction: phase === "streaming" ? fraction : 1,
967
+ showCursor: phase === "streaming" && fraction < 1
968
+ }),
969
+ toolRevealCount > 0 && /* @__PURE__ */ jsx(Box, {
970
+ flexDirection: "column",
971
+ children: toolCalls.map((call, i) => /* @__PURE__ */ jsx(ToolCallBlock, {
972
+ call,
973
+ phase: phase === "done" ? "done" : i < toolRevealCount - 1 ? "done" : "running"
974
+ }, i))
975
+ })
976
+ ]
977
+ })]
978
+ });
979
+ }
980
+ function StatusBar({ exchanges, compacting, done, elapsed, contextBaseline = 0, ctrlDPending = false }) {
981
+ const cumulative = computeCumulativeTokens(exchanges);
982
+ const cost = formatCost(cumulative.input, cumulative.output);
983
+ const elapsedStr = `${Math.floor(elapsed / 60)}:${(elapsed % 60).toString().padStart(2, "0")}`;
984
+ const CTX_W = 20;
985
+ const ctxFrac = Math.max(0, cumulative.currentContext - contextBaseline) / CONTEXT_WINDOW;
986
+ const ctxFilled = Math.round(Math.min(ctxFrac, 1) * CTX_W);
987
+ const ctxPct = Math.round(ctxFrac * 100);
988
+ const ctxColor = ctxPct > 100 ? "$error" : ctxPct > 80 ? "$warning" : "$primary";
989
+ const ctxBar = "█".repeat(ctxFilled) + "░".repeat(CTX_W - ctxFilled);
990
+ return /* @__PURE__ */ jsxs(Box, {
991
+ flexDirection: "row",
992
+ justifyContent: "space-between",
993
+ width: "100%",
994
+ children: [/* @__PURE__ */ jsxs(Text, {
995
+ color: "$muted",
996
+ wrap: "truncate",
997
+ children: [
998
+ elapsedStr,
999
+ " ",
1000
+ ctrlDPending ? "Ctrl-D again to exit" : compacting ? "compacting..." : "esc quit"
1001
+ ]
1002
+ }), /* @__PURE__ */ jsxs(Text, {
1003
+ color: ctxPct > 80 ? ctxColor : "$muted",
1004
+ wrap: "truncate",
1005
+ children: [
1006
+ "ctx ",
1007
+ ctxBar,
1008
+ " ",
1009
+ ctxPct,
1010
+ "%",
1011
+ " ",
1012
+ cost
1013
+ ]
1014
+ })]
1015
+ });
1016
+ }
1017
+ const AUTO_SUBMIT_DELAY = 1e4;
1018
+ function DemoFooter({ controlRef, onSubmit, streamPhase, done, compacting, exchanges, contextBaseline = 0, ctrlDPending = false, nextMessage = "", autoTypingText = null }) {
1019
+ const terminalFocused = useTerminalFocused();
1020
+ const [inputText, setInputText] = useState("");
1021
+ const inputTextRef = useRef(inputText);
1022
+ inputTextRef.current = inputText;
1023
+ const startRef = useRef(Date.now());
1024
+ const [elapsed, setElapsed] = useState(0);
1025
+ useEffect(() => {
1026
+ const timer = setInterval(() => setElapsed(Math.floor((Date.now() - startRef.current) / 1e3)), 1e3);
1027
+ return () => clearInterval(timer);
1028
+ }, []);
1029
+ const [randomIdx, setRandomIdx] = useState(() => Math.floor(Math.random() * RANDOM_USER_COMMANDS.length));
1030
+ const randomPlaceholder = RANDOM_USER_COMMANDS[randomIdx % RANDOM_USER_COMMANDS.length];
1031
+ const effectiveMessage = nextMessage || randomPlaceholder;
1032
+ const placeholder = !terminalFocused ? "Click to focus" : ctrlDPending ? "Press Ctrl-D again to exit" : effectiveMessage;
1033
+ const handleSubmit = useCallback((text) => {
1034
+ if (!text.trim() && effectiveMessage) onSubmit(effectiveMessage);
1035
+ else onSubmit(text);
1036
+ setInputText("");
1037
+ setRandomIdx((i) => i + 1);
1038
+ }, [onSubmit, effectiveMessage]);
1039
+ controlRef.current = { submit: () => handleSubmit(inputTextRef.current) };
1040
+ const autoSubmitRef = useRef(null);
1041
+ useEffect(() => {
1042
+ if (autoSubmitRef.current) clearTimeout(autoSubmitRef.current);
1043
+ if (done || compacting || streamPhase !== "done" || !effectiveMessage || inputText || autoTypingText || !terminalFocused) return;
1044
+ autoSubmitRef.current = setTimeout(() => onSubmit(effectiveMessage), AUTO_SUBMIT_DELAY);
1045
+ return () => {
1046
+ if (autoSubmitRef.current) clearTimeout(autoSubmitRef.current);
1047
+ };
1048
+ }, [
1049
+ done,
1050
+ compacting,
1051
+ streamPhase,
1052
+ effectiveMessage,
1053
+ inputText,
1054
+ autoTypingText,
1055
+ onSubmit
1056
+ ]);
1057
+ return /* @__PURE__ */ jsxs(Box, {
1058
+ flexDirection: "column",
1059
+ width: "100%",
1060
+ children: [
1061
+ /* @__PURE__ */ jsx(Text, { children: " " }),
1062
+ /* @__PURE__ */ jsxs(Box, {
1063
+ flexDirection: "row",
1064
+ borderStyle: "round",
1065
+ borderColor: !done && terminalFocused ? "$focusborder" : "$inputborder",
1066
+ paddingX: 1,
1067
+ children: [/* @__PURE__ */ jsxs(Text, {
1068
+ bold: true,
1069
+ color: "$focusring",
1070
+ children: ["❯", " "]
1071
+ }), /* @__PURE__ */ jsx(Box, {
1072
+ flexShrink: 1,
1073
+ flexGrow: 1,
1074
+ children: /* @__PURE__ */ jsx(TextInput, {
1075
+ value: autoTypingText ?? inputText,
1076
+ onChange: autoTypingText ? () => {} : setInputText,
1077
+ onSubmit: handleSubmit,
1078
+ placeholder,
1079
+ isActive: !done && !autoTypingText && terminalFocused
1080
+ })
1081
+ })]
1082
+ }),
1083
+ /* @__PURE__ */ jsx(Box, {
1084
+ paddingX: 2,
1085
+ width: "100%",
1086
+ children: /* @__PURE__ */ jsx(StatusBar, {
1087
+ exchanges,
1088
+ compacting,
1089
+ done,
1090
+ elapsed,
1091
+ contextBaseline,
1092
+ ctrlDPending
1093
+ })
1094
+ })
1095
+ ]
1096
+ });
1097
+ }
1098
+ //#endregion
1099
+ //#region apps/aichat/index.tsx
1100
+ /**
1101
+ * AI Chat — Coding Agent Demo
1102
+ *
1103
+ * Showcases ListView with streaming, tool calls, context tracking.
1104
+ * TEA state machine drives all animation; ListView caches completed
1105
+ * exchanges while live content stays in the React tree.
1106
+ *
1107
+ * Flags: --auto (auto-advance) --fast (skip animation) --stress (200 exchanges)
1108
+ */
1109
+ const meta = {
1110
+ name: "AI Coding Agent",
1111
+ description: "Coding agent showcase — ListView, streaming, context tracking",
1112
+ demo: true,
1113
+ features: [
1114
+ "ListView",
1115
+ "cache",
1116
+ "inline mode",
1117
+ "streaming",
1118
+ "OSC 8 links"
1119
+ ]
1120
+ };
1121
+ function AIChat({ script, autoStart, fastMode }) {
1122
+ const exit = useExit();
1123
+ const { rows: termRows } = useWindowSize();
1124
+ const [state, send] = useTea(INIT_STATE, useMemo(() => createDemoUpdate(script, fastMode, autoStart), [
1125
+ script,
1126
+ fastMode,
1127
+ autoStart
1128
+ ]));
1129
+ const footerControlRef = useRef({ submit: () => {} });
1130
+ useEffect(() => send({ type: "mount" }), [send]);
1131
+ useAutoCompact(state, send);
1132
+ useAutoExit(autoStart, state.done, exit);
1133
+ useKeyBindings(state, send, footerControlRef);
1134
+ const renderExchange = useCallback((exchange, index, _meta) => {
1135
+ const isLatest = index === state.exchanges.length - 1;
1136
+ return /* @__PURE__ */ jsxs(Box, {
1137
+ flexDirection: "column",
1138
+ children: [
1139
+ index > 0 && /* @__PURE__ */ jsx(Text, { children: " " }),
1140
+ state.compacting && isLatest && /* @__PURE__ */ jsx(CompactingOverlay, {}),
1141
+ state.done && autoStart && isLatest && /* @__PURE__ */ jsx(SessionComplete, {}),
1142
+ /* @__PURE__ */ jsx(ExchangeItem, {
1143
+ exchange,
1144
+ streamPhase: state.streamPhase,
1145
+ revealFraction: state.revealFraction,
1146
+ pulse: state.pulse,
1147
+ isLatest,
1148
+ isFirstInGroup: exchange.role !== (index > 0 ? state.exchanges[index - 1].role : null),
1149
+ isLastInGroup: exchange.role !== (index < state.exchanges.length - 1 ? state.exchanges[index + 1].role : null)
1150
+ })
1151
+ ]
1152
+ });
1153
+ }, [state, autoStart]);
1154
+ return /* @__PURE__ */ jsx(Box, {
1155
+ flexDirection: "column",
1156
+ paddingX: 1,
1157
+ children: /* @__PURE__ */ jsx(ListView, {
1158
+ items: state.exchanges,
1159
+ getKey: (ex) => ex.id,
1160
+ height: termRows,
1161
+ estimateHeight: 6,
1162
+ renderItem: renderExchange,
1163
+ scrollTo: state.exchanges.length - 1,
1164
+ cache: {
1165
+ mode: "virtual",
1166
+ isCacheable: (_ex, index) => index < state.exchanges.length - 1
1167
+ },
1168
+ listFooter: /* @__PURE__ */ jsx(DemoFooter, {
1169
+ controlRef: footerControlRef,
1170
+ onSubmit: (text) => send({
1171
+ type: "submit",
1172
+ text
1173
+ }),
1174
+ streamPhase: state.streamPhase,
1175
+ done: state.done,
1176
+ compacting: state.compacting,
1177
+ exchanges: state.exchanges,
1178
+ contextBaseline: state.contextBaseline,
1179
+ ctrlDPending: state.ctrlDPending,
1180
+ nextMessage: getNextMessage(state, script, autoStart),
1181
+ autoTypingText: state.autoTyping ? state.autoTyping.full.slice(0, state.autoTyping.revealed) : null
1182
+ })
1183
+ })
1184
+ });
1185
+ }
1186
+ async function main() {
1187
+ try {
1188
+ var _usingCtx$1 = _usingCtx();
1189
+ const args = process.argv.slice(2);
1190
+ const script = args.includes("--stress") ? generateStressScript() : SCRIPT;
1191
+ const mode = args.includes("--inline") ? "inline" : "fullscreen";
1192
+ await _usingCtx$1.u(await run(/* @__PURE__ */ jsx(AIChat, {
1193
+ script,
1194
+ autoStart: args.includes("--auto"),
1195
+ fastMode: args.includes("--fast")
1196
+ }), {
1197
+ mode,
1198
+ focusReporting: true
1199
+ })).waitUntilExit();
1200
+ } catch (_) {
1201
+ _usingCtx$1.e = _;
1202
+ } finally {
1203
+ _usingCtx$1.d();
1204
+ }
1205
+ }
1206
+ if (import.meta.main) await main();
1207
+ function useAutoCompact(state, send) {
1208
+ useEffect(() => {
1209
+ if (state.done || state.compacting) return;
1210
+ const cumulative = computeCumulativeTokens(state.exchanges);
1211
+ if (Math.max(0, cumulative.currentContext - state.contextBaseline) >= 2e5 * .95) send({ type: "compact" });
1212
+ }, [
1213
+ state.exchanges,
1214
+ state.done,
1215
+ state.compacting,
1216
+ state.contextBaseline,
1217
+ send
1218
+ ]);
1219
+ }
1220
+ function useAutoExit(autoStart, done, exit) {
1221
+ useEffect(() => {
1222
+ if (!autoStart || !done) return;
1223
+ const timer = setTimeout(exit, 1e3);
1224
+ return () => clearTimeout(timer);
1225
+ }, [
1226
+ autoStart,
1227
+ done,
1228
+ exit
1229
+ ]);
1230
+ }
1231
+ function useKeyBindings(state, send, footerControlRef) {
1232
+ const lastCtrlDRef = useRef(0);
1233
+ useInput$1((input, key) => {
1234
+ if (key.escape) return "exit";
1235
+ if (key.ctrl && input === "d") {
1236
+ const now = Date.now();
1237
+ if (now - lastCtrlDRef.current < 500) return "exit";
1238
+ lastCtrlDRef.current = now;
1239
+ send({
1240
+ type: "setCtrlDPending",
1241
+ pending: true
1242
+ });
1243
+ return;
1244
+ }
1245
+ if (lastCtrlDRef.current > 0) {
1246
+ lastCtrlDRef.current = 0;
1247
+ send({
1248
+ type: "setCtrlDPending",
1249
+ pending: false
1250
+ });
1251
+ }
1252
+ if (key.tab) {
1253
+ if (state.done || state.compacting) return;
1254
+ footerControlRef.current.submit();
1255
+ return;
1256
+ }
1257
+ if (key.ctrl && input === "l") send({ type: "compact" });
1258
+ });
1259
+ }
1260
+ function CompactingOverlay() {
1261
+ return /* @__PURE__ */ jsxs(Box, {
1262
+ flexDirection: "column",
1263
+ borderStyle: "round",
1264
+ borderColor: "$warning",
1265
+ paddingX: 1,
1266
+ overflow: "hidden",
1267
+ children: [
1268
+ /* @__PURE__ */ jsxs(Text, {
1269
+ color: "$warning",
1270
+ bold: true,
1271
+ children: [/* @__PURE__ */ jsx(Spinner, { type: "arc" }), " Compacting context"]
1272
+ }),
1273
+ /* @__PURE__ */ jsx(Text, { children: " " }),
1274
+ /* @__PURE__ */ jsx(Text, {
1275
+ color: "$muted",
1276
+ children: "Freezing exchanges into terminal scrollback. Scroll up to review."
1277
+ })
1278
+ ]
1279
+ });
1280
+ }
1281
+ function SessionComplete() {
1282
+ return /* @__PURE__ */ jsxs(Box, {
1283
+ flexDirection: "column",
1284
+ borderStyle: "round",
1285
+ borderColor: "$success",
1286
+ paddingX: 1,
1287
+ children: [/* @__PURE__ */ jsxs(Text, {
1288
+ color: "$success",
1289
+ bold: true,
1290
+ children: ["✓", " Session complete"]
1291
+ }), /* @__PURE__ */ jsx(Text, {
1292
+ color: "$muted",
1293
+ children: "Scroll up to review — colors, borders, and hyperlinks preserved in scrollback."
1294
+ })]
1295
+ });
1296
+ }
1297
+ //#endregion
1298
+ export { AIChat, CONTEXT_WINDOW, SCRIPT, generateStressScript, main, meta };