@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.
- package/dist/UPNG-ShUlaTDh.mjs +5074 -0
- package/dist/__vite-browser-external-2447137e-Bopa5BFR.mjs +4 -0
- package/dist/_banner-A70_y2Vi.mjs +43 -0
- package/dist/ansi-0VXlUmNn.mjs +16397 -0
- package/dist/apng-B0gRaDVT.mjs +3 -0
- package/dist/apng-BTRDTfDW.mjs +68 -0
- package/dist/apps/aichat/index.mjs +1298 -0
- package/dist/apps/app-todo.mjs +138 -0
- package/dist/apps/async-data.mjs +203 -0
- package/dist/apps/cli-wizard.mjs +338 -0
- package/dist/apps/clipboard.mjs +197 -0
- package/dist/apps/components.mjs +863 -0
- package/dist/apps/data-explorer.mjs +482 -0
- package/dist/apps/dev-tools.mjs +396 -0
- package/dist/apps/explorer.mjs +697 -0
- package/dist/apps/gallery.mjs +765 -0
- package/dist/apps/inline-bench.mjs +115 -0
- package/dist/apps/kanban.mjs +279 -0
- package/dist/apps/layout-ref.mjs +186 -0
- package/dist/apps/outline.mjs +202 -0
- package/dist/apps/paste-demo.mjs +188 -0
- package/dist/apps/scroll.mjs +85 -0
- package/dist/apps/search-filter.mjs +286 -0
- package/dist/apps/selection.mjs +354 -0
- package/dist/apps/spatial-focus-demo.mjs +387 -0
- package/dist/apps/task-list.mjs +257 -0
- package/dist/apps/terminal-caps-demo.mjs +314 -0
- package/dist/apps/terminal.mjs +871 -0
- package/dist/apps/text-selection-demo.mjs +253 -0
- package/dist/apps/textarea.mjs +177 -0
- package/dist/apps/theme.mjs +660 -0
- package/dist/apps/transform.mjs +214 -0
- package/dist/apps/virtual-10k.mjs +421 -0
- package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
- package/dist/backends-Dj-11kZF.mjs +1179 -0
- package/dist/backends-U3QwStfO.mjs +3 -0
- package/dist/{cli.mjs → bin/cli.mjs} +15 -19
- package/dist/chunk-BSw8zbkd.mjs +37 -0
- package/dist/components/counter.mjs +47 -0
- package/dist/components/hello.mjs +30 -0
- package/dist/components/progress-bar.mjs +58 -0
- package/dist/components/select-list.mjs +84 -0
- package/dist/components/spinner.mjs +56 -0
- package/dist/components/text-input.mjs +61 -0
- package/dist/components/virtual-list.mjs +50 -0
- package/dist/flexily-zero-adapter-ByVzLTFP.mjs +3374 -0
- package/dist/gif-B6NGH5gs.mjs +3 -0
- package/dist/gif-CfkOF-iG.mjs +71 -0
- package/dist/gifenc-BI4ihP_T.mjs +728 -0
- package/dist/key-mapping-5oYQdAQE.mjs +3 -0
- package/dist/key-mapping-D4LR1go6.mjs +130 -0
- package/dist/layout/dashboard.mjs +1203 -0
- package/dist/layout/live-resize.mjs +302 -0
- package/dist/layout/overflow.mjs +69 -0
- package/dist/layout/text-layout.mjs +334 -0
- package/dist/node-nsrAOjH4.mjs +1083 -0
- package/dist/plugins-CT0DdV_E.mjs +3056 -0
- package/dist/resvg-js-Cnk2o49d.mjs +201 -0
- package/dist/src-9ZhfQyzD.mjs +814 -0
- package/dist/src-CUUOuRH6.mjs +5322 -0
- package/dist/src-jO3Zuzjj.mjs +23538 -0
- package/dist/usingCtx-CsEf0xO3.mjs +57 -0
- package/dist/yoga-adapter-BSQHuMV9.mjs +237 -0
- package/package.json +21 -14
- package/_banner.tsx +0 -60
- package/apps/aichat/components.tsx +0 -469
- package/apps/aichat/index.tsx +0 -220
- package/apps/aichat/script.ts +0 -460
- package/apps/aichat/state.ts +0 -325
- package/apps/aichat/types.ts +0 -19
- package/apps/app-todo.tsx +0 -201
- package/apps/async-data.tsx +0 -196
- package/apps/cli-wizard.tsx +0 -332
- package/apps/clipboard.tsx +0 -183
- package/apps/components.tsx +0 -658
- package/apps/data-explorer.tsx +0 -490
- package/apps/dev-tools.tsx +0 -395
- package/apps/explorer.tsx +0 -731
- package/apps/gallery.tsx +0 -653
- package/apps/inline-bench.tsx +0 -138
- package/apps/kanban.tsx +0 -265
- package/apps/layout-ref.tsx +0 -173
- package/apps/outline.tsx +0 -160
- package/apps/panes/index.tsx +0 -203
- package/apps/paste-demo.tsx +0 -185
- package/apps/scroll.tsx +0 -80
- package/apps/search-filter.tsx +0 -240
- package/apps/selection.tsx +0 -346
- package/apps/spatial-focus-demo.tsx +0 -372
- package/apps/task-list.tsx +0 -271
- package/apps/terminal-caps-demo.tsx +0 -317
- package/apps/terminal.tsx +0 -784
- package/apps/text-selection-demo.tsx +0 -193
- package/apps/textarea.tsx +0 -155
- package/apps/theme.tsx +0 -515
- package/apps/transform.tsx +0 -229
- package/apps/virtual-10k.tsx +0 -405
- package/apps/vterm-demo/index.tsx +0 -216
- package/components/counter.tsx +0 -49
- package/components/hello.tsx +0 -38
- package/components/progress-bar.tsx +0 -52
- package/components/select-list.tsx +0 -54
- package/components/spinner.tsx +0 -44
- package/components/text-input.tsx +0 -61
- package/components/virtual-list.tsx +0 -56
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs.map +0 -1
- package/layout/dashboard.tsx +0 -953
- package/layout/live-resize.tsx +0 -282
- package/layout/overflow.tsx +0 -51
- package/layout/text-layout.tsx +0 -283
|
@@ -1,469 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* UI components for the AI coding agent demo.
|
|
3
|
-
*
|
|
4
|
-
* Components: ExchangeItem, StatusBar, DemoFooter (public)
|
|
5
|
-
* Internal: LinkifiedLine, ThinkingBlock, ToolCallBlock, StreamingText
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { JSX } from "react"
|
|
9
|
-
import React, { useState, useEffect, useCallback, useRef } from "react"
|
|
10
|
-
import { Box, Text, Link, Spinner, TextInput, useTerminalFocused } from "silvery"
|
|
11
|
-
import type { Exchange, ToolCall } from "./types.js"
|
|
12
|
-
import { TOOL_COLORS, URL_RE, RANDOM_USER_COMMANDS, CONTEXT_WINDOW } from "./script.js"
|
|
13
|
-
import type { StreamPhase } from "./state.js"
|
|
14
|
-
import { formatTokens, formatCost, computeCumulativeTokens } from "./state.js"
|
|
15
|
-
|
|
16
|
-
// ============================================================================
|
|
17
|
-
// Footer Control — simplified interface for parent to trigger submit
|
|
18
|
-
// ============================================================================
|
|
19
|
-
|
|
20
|
-
export interface FooterControl {
|
|
21
|
-
submit: () => void
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ============================================================================
|
|
25
|
-
// Internal Helpers
|
|
26
|
-
// ============================================================================
|
|
27
|
-
|
|
28
|
-
/** Split content into a short title (first sentence) and the remaining body.
|
|
29
|
-
* Title must be ≤40 chars to fit on the header line with metadata. */
|
|
30
|
-
function splitTitleBody(content: string): { title: string; body: string } {
|
|
31
|
-
const match = content.match(/^(.+?[.!?])\s+(.+)$/s)
|
|
32
|
-
if (match && match[1]!.length <= 40) return { title: match[1]!, body: match[2]! }
|
|
33
|
-
// No sentence break or sentence too long — short content goes entirely to title
|
|
34
|
-
if (content.length <= 40) return { title: content, body: "" }
|
|
35
|
-
return { title: "", body: content }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ============================================================================
|
|
39
|
-
// Internal Components
|
|
40
|
-
// ============================================================================
|
|
41
|
-
|
|
42
|
-
/** Render a line with auto-linked URLs. */
|
|
43
|
-
function LinkifiedLine({ text, dim, color }: { text: string; dim?: boolean; color?: string }) {
|
|
44
|
-
const parts: JSX.Element[] = []
|
|
45
|
-
let lastIndex = 0
|
|
46
|
-
let match: RegExpExecArray | null
|
|
47
|
-
|
|
48
|
-
URL_RE.lastIndex = 0
|
|
49
|
-
while ((match = URL_RE.exec(text)) !== null) {
|
|
50
|
-
if (match.index > lastIndex) {
|
|
51
|
-
parts.push(
|
|
52
|
-
<Text key={`t${lastIndex}`} dim={dim} color={color}>
|
|
53
|
-
{text.slice(lastIndex, match.index)}
|
|
54
|
-
</Text>,
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
const url = match[0]
|
|
58
|
-
parts.push(
|
|
59
|
-
<Link key={`l${match.index}`} href={url} dim={dim}>
|
|
60
|
-
{url}
|
|
61
|
-
</Link>,
|
|
62
|
-
)
|
|
63
|
-
lastIndex = match.index + url.length
|
|
64
|
-
}
|
|
65
|
-
if (lastIndex < text.length) {
|
|
66
|
-
parts.push(
|
|
67
|
-
<Text key={`t${lastIndex}`} dim={dim} color={color}>
|
|
68
|
-
{text.slice(lastIndex)}
|
|
69
|
-
</Text>,
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
if (parts.length === 0) {
|
|
73
|
-
return (
|
|
74
|
-
<Text dim={dim} color={color}>
|
|
75
|
-
{text}
|
|
76
|
-
</Text>
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
return <Text>{parts}</Text>
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Thinking block — shows thinking text preview in the body. */
|
|
83
|
-
function ThinkingBlock({ text, done }: { text: string; done: boolean }) {
|
|
84
|
-
if (done)
|
|
85
|
-
return (
|
|
86
|
-
<Text color="$muted" italic>
|
|
87
|
-
{"▸ thought"}
|
|
88
|
-
</Text>
|
|
89
|
-
)
|
|
90
|
-
return (
|
|
91
|
-
<Text color="$muted" wrap="truncate" italic>
|
|
92
|
-
{text}
|
|
93
|
-
</Text>
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Tool call with lifecycle: spinner -> output -> checkmark. */
|
|
98
|
-
function ToolCallBlock({ call, phase }: { call: ToolCall; phase: "pending" | "running" | "done" }) {
|
|
99
|
-
const color = TOOL_COLORS[call.tool] ?? "$muted"
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<Box flexDirection="column" marginTop={0}>
|
|
103
|
-
<Text>
|
|
104
|
-
{phase === "running" ? (
|
|
105
|
-
<>
|
|
106
|
-
<Spinner type="dots" />{" "}
|
|
107
|
-
</>
|
|
108
|
-
) : phase === "done" ? (
|
|
109
|
-
<Text color="$success">{"✓ "}</Text>
|
|
110
|
-
) : (
|
|
111
|
-
<Text color="$muted">{"○ "}</Text>
|
|
112
|
-
)}
|
|
113
|
-
<Text color={color} bold>
|
|
114
|
-
{call.tool}
|
|
115
|
-
</Text>{" "}
|
|
116
|
-
{call.tool === "Bash" || call.tool === "Grep" || call.tool === "Glob" ? (
|
|
117
|
-
<Text color="$muted">{call.args}</Text>
|
|
118
|
-
) : (
|
|
119
|
-
<Link href={`file://${call.args}`}>{call.args}</Link>
|
|
120
|
-
)}
|
|
121
|
-
</Text>
|
|
122
|
-
{phase === "done" && (
|
|
123
|
-
<Box flexDirection="column" paddingLeft={2}>
|
|
124
|
-
{call.output.map((line, i) => {
|
|
125
|
-
if (line.startsWith("+")) return <LinkifiedLine key={i} text={line} color="$success" />
|
|
126
|
-
if (line.startsWith("-")) return <LinkifiedLine key={i} text={line} color="$error" />
|
|
127
|
-
return <LinkifiedLine key={i} text={line} />
|
|
128
|
-
})}
|
|
129
|
-
</Box>
|
|
130
|
-
)}
|
|
131
|
-
</Box>
|
|
132
|
-
)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Streaming text — reveals content word by word. */
|
|
136
|
-
function StreamingText({
|
|
137
|
-
fullText,
|
|
138
|
-
revealFraction,
|
|
139
|
-
showCursor,
|
|
140
|
-
}: {
|
|
141
|
-
fullText: string
|
|
142
|
-
revealFraction: number
|
|
143
|
-
showCursor: boolean
|
|
144
|
-
}) {
|
|
145
|
-
if (revealFraction >= 1) {
|
|
146
|
-
return <Text>{fullText}</Text>
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const words = fullText.split(/(\s+)/)
|
|
150
|
-
const totalWords = words.filter((w) => w.trim()).length
|
|
151
|
-
const revealWords = Math.ceil(totalWords * revealFraction)
|
|
152
|
-
|
|
153
|
-
let wordCount = 0
|
|
154
|
-
let revealedText = ""
|
|
155
|
-
for (const word of words) {
|
|
156
|
-
if (word.trim()) {
|
|
157
|
-
wordCount++
|
|
158
|
-
if (wordCount > revealWords) break
|
|
159
|
-
}
|
|
160
|
-
revealedText += word
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return (
|
|
164
|
-
<Text>
|
|
165
|
-
{revealedText}
|
|
166
|
-
{showCursor && <Text color="$primary">{"▌"}</Text>}
|
|
167
|
-
</Text>
|
|
168
|
-
)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// ============================================================================
|
|
172
|
-
// Exchange Item — live rendering with streaming, spinners, scrollback freeze
|
|
173
|
-
// ============================================================================
|
|
174
|
-
|
|
175
|
-
export function ExchangeItem({
|
|
176
|
-
exchange,
|
|
177
|
-
streamPhase,
|
|
178
|
-
revealFraction,
|
|
179
|
-
pulse,
|
|
180
|
-
isLatest,
|
|
181
|
-
isFirstInGroup,
|
|
182
|
-
isLastInGroup,
|
|
183
|
-
}: {
|
|
184
|
-
exchange: Exchange
|
|
185
|
-
streamPhase: StreamPhase
|
|
186
|
-
revealFraction: number
|
|
187
|
-
pulse: boolean
|
|
188
|
-
isLatest: boolean
|
|
189
|
-
isFirstInGroup: boolean
|
|
190
|
-
isLastInGroup: boolean
|
|
191
|
-
}) {
|
|
192
|
-
if (exchange.role === "system") {
|
|
193
|
-
return (
|
|
194
|
-
<Box flexDirection="column">
|
|
195
|
-
<Text> </Text>
|
|
196
|
-
<Text bold>AI Chat</Text>
|
|
197
|
-
<Text> </Text>
|
|
198
|
-
<Text color="$muted">{exchange.content}</Text>
|
|
199
|
-
<Text> </Text>
|
|
200
|
-
</Box>
|
|
201
|
-
)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const isUser = exchange.role === "user"
|
|
205
|
-
|
|
206
|
-
if (isUser) {
|
|
207
|
-
return (
|
|
208
|
-
<Box paddingX={1} flexDirection="row" backgroundColor="$surface-bg">
|
|
209
|
-
<Text bold color="$focusring">
|
|
210
|
-
{"❯"}{" "}
|
|
211
|
-
</Text>
|
|
212
|
-
<Box flexShrink={1}>
|
|
213
|
-
<Text>{exchange.content}</Text>
|
|
214
|
-
</Box>
|
|
215
|
-
</Box>
|
|
216
|
-
)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const phase = isLatest ? streamPhase : "done"
|
|
220
|
-
const fraction = isLatest ? revealFraction : 1
|
|
221
|
-
|
|
222
|
-
const toolCalls = exchange.toolCalls ?? []
|
|
223
|
-
const toolRevealCount = phase === "tools" || phase === "done" ? toolCalls.length : 0
|
|
224
|
-
const hasOperations = toolCalls.length > 0 || !!exchange.thinking
|
|
225
|
-
|
|
226
|
-
// Metadata: token count + thought indicator
|
|
227
|
-
const metaParts: string[] = []
|
|
228
|
-
if (exchange.tokens && phase === "done") metaParts.push(`${formatTokens(exchange.tokens.output)} tokens`)
|
|
229
|
-
if (exchange.thinking && (phase === "done" || phase === "streaming")) metaParts.push("thought for 1s")
|
|
230
|
-
const metaStr = metaParts.length > 0 ? ` (${metaParts.join(" · ")})` : ""
|
|
231
|
-
|
|
232
|
-
// Split content into title (first sentence) and body (rest)
|
|
233
|
-
const { title, body } = splitTitleBody(exchange.content)
|
|
234
|
-
|
|
235
|
-
const bulletColor = hasOperations ? "$success" : "$muted"
|
|
236
|
-
const contentText = title ? body : exchange.content
|
|
237
|
-
|
|
238
|
-
return (
|
|
239
|
-
<Box flexDirection="column">
|
|
240
|
-
<Text>
|
|
241
|
-
<Text bold color={bulletColor} dimColor={hasOperations && !pulse && phase !== "done"}>
|
|
242
|
-
{"●"}
|
|
243
|
-
</Text>
|
|
244
|
-
{phase === "thinking" ? (
|
|
245
|
-
<Text color="$muted" italic>
|
|
246
|
-
{" "}
|
|
247
|
-
<Spinner type="dots" /> thinking
|
|
248
|
-
</Text>
|
|
249
|
-
) : (
|
|
250
|
-
<>
|
|
251
|
-
{title && <Text> {title}</Text>}
|
|
252
|
-
<Text color="$muted">{metaStr}</Text>
|
|
253
|
-
</>
|
|
254
|
-
)}
|
|
255
|
-
</Text>
|
|
256
|
-
|
|
257
|
-
<Box
|
|
258
|
-
flexDirection="column"
|
|
259
|
-
borderStyle="bold"
|
|
260
|
-
borderColor="$border"
|
|
261
|
-
borderLeft
|
|
262
|
-
borderRight={false}
|
|
263
|
-
borderTop={false}
|
|
264
|
-
borderBottom={false}
|
|
265
|
-
paddingLeft={1}
|
|
266
|
-
>
|
|
267
|
-
{exchange.thinking && (phase === "thinking" || phase === "streaming") && (
|
|
268
|
-
<ThinkingBlock text={exchange.thinking} done={phase !== "thinking"} />
|
|
269
|
-
)}
|
|
270
|
-
|
|
271
|
-
{(phase === "streaming" || phase === "tools" || phase === "done") && contentText && (
|
|
272
|
-
<StreamingText
|
|
273
|
-
fullText={contentText}
|
|
274
|
-
revealFraction={phase === "streaming" ? fraction : 1}
|
|
275
|
-
showCursor={phase === "streaming" && fraction < 1}
|
|
276
|
-
/>
|
|
277
|
-
)}
|
|
278
|
-
|
|
279
|
-
{toolRevealCount > 0 && (
|
|
280
|
-
<Box flexDirection="column">
|
|
281
|
-
{toolCalls.map((call, i) => (
|
|
282
|
-
<ToolCallBlock
|
|
283
|
-
key={i}
|
|
284
|
-
call={call}
|
|
285
|
-
phase={phase === "done" ? "done" : i < toolRevealCount - 1 ? "done" : "running"}
|
|
286
|
-
/>
|
|
287
|
-
))}
|
|
288
|
-
</Box>
|
|
289
|
-
)}
|
|
290
|
-
</Box>
|
|
291
|
-
</Box>
|
|
292
|
-
)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// ============================================================================
|
|
296
|
-
// Status Bar — single compact row
|
|
297
|
-
// ============================================================================
|
|
298
|
-
|
|
299
|
-
export function StatusBar({
|
|
300
|
-
exchanges,
|
|
301
|
-
compacting,
|
|
302
|
-
done,
|
|
303
|
-
elapsed,
|
|
304
|
-
contextBaseline = 0,
|
|
305
|
-
ctrlDPending = false,
|
|
306
|
-
}: {
|
|
307
|
-
exchanges: Exchange[]
|
|
308
|
-
compacting: boolean
|
|
309
|
-
done: boolean
|
|
310
|
-
elapsed: number
|
|
311
|
-
contextBaseline?: number
|
|
312
|
-
ctrlDPending?: boolean
|
|
313
|
-
}) {
|
|
314
|
-
const cumulative = computeCumulativeTokens(exchanges)
|
|
315
|
-
const cost = formatCost(cumulative.input, cumulative.output)
|
|
316
|
-
const minutes = Math.floor(elapsed / 60)
|
|
317
|
-
const seconds = elapsed % 60
|
|
318
|
-
const elapsedStr = `${minutes}:${seconds.toString().padStart(2, "0")}`
|
|
319
|
-
|
|
320
|
-
const CTX_W = 20
|
|
321
|
-
const effectiveContext = Math.max(0, cumulative.currentContext - contextBaseline)
|
|
322
|
-
const ctxFrac = effectiveContext / CONTEXT_WINDOW
|
|
323
|
-
const ctxFilled = Math.round(Math.min(ctxFrac, 1) * CTX_W)
|
|
324
|
-
const ctxPct = Math.round(ctxFrac * 100)
|
|
325
|
-
const ctxColor = ctxPct > 100 ? "$error" : ctxPct > 80 ? "$warning" : "$primary"
|
|
326
|
-
const ctxBar = "█".repeat(ctxFilled) + "░".repeat(CTX_W - ctxFilled)
|
|
327
|
-
|
|
328
|
-
const keys = ctrlDPending ? "Ctrl-D again to exit" : compacting ? "compacting..." : "esc quit"
|
|
329
|
-
|
|
330
|
-
return (
|
|
331
|
-
<Box flexDirection="row" justifyContent="space-between" width="100%">
|
|
332
|
-
<Text color="$muted" wrap="truncate">
|
|
333
|
-
{elapsedStr}
|
|
334
|
-
{" "}
|
|
335
|
-
{keys}
|
|
336
|
-
</Text>
|
|
337
|
-
<Text color={ctxPct > 80 ? ctxColor : "$muted"} wrap="truncate">
|
|
338
|
-
ctx {ctxBar} {ctxPct}%{" "}
|
|
339
|
-
{cost}
|
|
340
|
-
</Text>
|
|
341
|
-
</Box>
|
|
342
|
-
)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// ============================================================================
|
|
346
|
-
// Footer — owns inputText state so typing doesn't re-render the parent
|
|
347
|
-
// ============================================================================
|
|
348
|
-
|
|
349
|
-
const AUTO_SUBMIT_DELAY = 10_000
|
|
350
|
-
|
|
351
|
-
export function DemoFooter({
|
|
352
|
-
controlRef,
|
|
353
|
-
onSubmit,
|
|
354
|
-
streamPhase,
|
|
355
|
-
done,
|
|
356
|
-
compacting,
|
|
357
|
-
exchanges,
|
|
358
|
-
contextBaseline = 0,
|
|
359
|
-
ctrlDPending = false,
|
|
360
|
-
nextMessage = "",
|
|
361
|
-
autoTypingText = null,
|
|
362
|
-
}: {
|
|
363
|
-
controlRef: React.RefObject<FooterControl>
|
|
364
|
-
onSubmit: (text: string) => void
|
|
365
|
-
streamPhase: StreamPhase
|
|
366
|
-
done: boolean
|
|
367
|
-
compacting: boolean
|
|
368
|
-
exchanges: Exchange[]
|
|
369
|
-
contextBaseline?: number
|
|
370
|
-
ctrlDPending?: boolean
|
|
371
|
-
nextMessage?: string
|
|
372
|
-
autoTypingText?: string | null
|
|
373
|
-
}) {
|
|
374
|
-
const terminalFocused = useTerminalFocused()
|
|
375
|
-
const [inputText, setInputText] = useState("")
|
|
376
|
-
const inputTextRef = useRef(inputText)
|
|
377
|
-
inputTextRef.current = inputText
|
|
378
|
-
|
|
379
|
-
const startRef = useRef(Date.now())
|
|
380
|
-
const [elapsed, setElapsed] = useState(0)
|
|
381
|
-
useEffect(() => {
|
|
382
|
-
const timer = setInterval(() => setElapsed(Math.floor((Date.now() - startRef.current) / 1000)), 1000)
|
|
383
|
-
return () => clearInterval(timer)
|
|
384
|
-
}, [])
|
|
385
|
-
|
|
386
|
-
const [randomIdx, setRandomIdx] = useState(() => Math.floor(Math.random() * RANDOM_USER_COMMANDS.length))
|
|
387
|
-
const randomPlaceholder = RANDOM_USER_COMMANDS[randomIdx % RANDOM_USER_COMMANDS.length]!
|
|
388
|
-
const effectiveMessage = nextMessage || randomPlaceholder
|
|
389
|
-
const placeholder = !terminalFocused
|
|
390
|
-
? "Click to focus"
|
|
391
|
-
: ctrlDPending
|
|
392
|
-
? "Press Ctrl-D again to exit"
|
|
393
|
-
: effectiveMessage
|
|
394
|
-
|
|
395
|
-
const handleSubmit = useCallback(
|
|
396
|
-
(text: string) => {
|
|
397
|
-
if (!text.trim() && effectiveMessage) {
|
|
398
|
-
onSubmit(effectiveMessage)
|
|
399
|
-
} else {
|
|
400
|
-
onSubmit(text)
|
|
401
|
-
}
|
|
402
|
-
setInputText("")
|
|
403
|
-
setRandomIdx((i) => i + 1)
|
|
404
|
-
},
|
|
405
|
-
[onSubmit, effectiveMessage],
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
// Expose submit() to parent — replaces the old getText/setText/getPlaceholder pattern
|
|
409
|
-
controlRef.current = {
|
|
410
|
-
submit: () => handleSubmit(inputTextRef.current),
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Auto-submit: if idle for AUTO_SUBMIT_DELAY, submit the placeholder message
|
|
414
|
-
const autoSubmitRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
415
|
-
useEffect(() => {
|
|
416
|
-
if (autoSubmitRef.current) clearTimeout(autoSubmitRef.current)
|
|
417
|
-
if (
|
|
418
|
-
done ||
|
|
419
|
-
compacting ||
|
|
420
|
-
streamPhase !== "done" ||
|
|
421
|
-
!effectiveMessage ||
|
|
422
|
-
inputText ||
|
|
423
|
-
autoTypingText ||
|
|
424
|
-
!terminalFocused
|
|
425
|
-
)
|
|
426
|
-
return
|
|
427
|
-
autoSubmitRef.current = setTimeout(() => onSubmit(effectiveMessage), AUTO_SUBMIT_DELAY)
|
|
428
|
-
return () => {
|
|
429
|
-
if (autoSubmitRef.current) clearTimeout(autoSubmitRef.current)
|
|
430
|
-
}
|
|
431
|
-
}, [done, compacting, streamPhase, effectiveMessage, inputText, autoTypingText, onSubmit])
|
|
432
|
-
|
|
433
|
-
const displayText = autoTypingText ?? inputText
|
|
434
|
-
|
|
435
|
-
return (
|
|
436
|
-
<Box flexDirection="column" width="100%">
|
|
437
|
-
<Text> </Text>
|
|
438
|
-
<Box
|
|
439
|
-
flexDirection="row"
|
|
440
|
-
borderStyle="round"
|
|
441
|
-
borderColor={!done && terminalFocused ? "$focusborder" : "$inputborder"}
|
|
442
|
-
paddingX={1}
|
|
443
|
-
>
|
|
444
|
-
<Text bold color="$focusring">
|
|
445
|
-
{"❯"}{" "}
|
|
446
|
-
</Text>
|
|
447
|
-
<Box flexShrink={1} flexGrow={1}>
|
|
448
|
-
<TextInput
|
|
449
|
-
value={displayText}
|
|
450
|
-
onChange={autoTypingText ? () => {} : setInputText}
|
|
451
|
-
onSubmit={handleSubmit}
|
|
452
|
-
placeholder={placeholder}
|
|
453
|
-
isActive={!done && !autoTypingText && terminalFocused}
|
|
454
|
-
/>
|
|
455
|
-
</Box>
|
|
456
|
-
</Box>
|
|
457
|
-
<Box paddingX={2} width="100%">
|
|
458
|
-
<StatusBar
|
|
459
|
-
exchanges={exchanges}
|
|
460
|
-
compacting={compacting}
|
|
461
|
-
done={done}
|
|
462
|
-
elapsed={elapsed}
|
|
463
|
-
contextBaseline={contextBaseline}
|
|
464
|
-
ctrlDPending={ctrlDPending}
|
|
465
|
-
/>
|
|
466
|
-
</Box>
|
|
467
|
-
</Box>
|
|
468
|
-
)
|
|
469
|
-
}
|
package/apps/aichat/index.tsx
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AI Chat — Coding Agent Demo
|
|
3
|
-
*
|
|
4
|
-
* Showcases ListView with streaming, tool calls, context tracking.
|
|
5
|
-
* TEA state machine drives all animation; ListView caches completed
|
|
6
|
-
* exchanges while live content stays in the React tree.
|
|
7
|
-
*
|
|
8
|
-
* Flags: --auto (auto-advance) --fast (skip animation) --stress (200 exchanges)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import React, { useCallback, useEffect, useRef, useMemo } from "react"
|
|
12
|
-
import { Box, Text, Spinner, ListView, useTea, useWindowSize } from "silvery"
|
|
13
|
-
import type { ListItemMeta } from "silvery"
|
|
14
|
-
import { run, useInput, useExit, type Key } from "silvery/runtime"
|
|
15
|
-
import type { ExampleMeta } from "../../_banner.js"
|
|
16
|
-
import type { ScriptEntry } from "./types.js"
|
|
17
|
-
import { SCRIPT, generateStressScript, CONTEXT_WINDOW } from "./script.js"
|
|
18
|
-
import {
|
|
19
|
-
INIT_STATE,
|
|
20
|
-
createDemoUpdate,
|
|
21
|
-
computeCumulativeTokens,
|
|
22
|
-
getNextMessage,
|
|
23
|
-
type DemoState,
|
|
24
|
-
type DemoMsg,
|
|
25
|
-
} from "./state.js"
|
|
26
|
-
import { ExchangeItem, DemoFooter } from "./components.js"
|
|
27
|
-
import type { FooterControl } from "./components.js"
|
|
28
|
-
|
|
29
|
-
// Re-export for test consumers
|
|
30
|
-
export { SCRIPT, generateStressScript, CONTEXT_WINDOW } from "./script.js"
|
|
31
|
-
export type { ScriptEntry } from "./types.js"
|
|
32
|
-
export type { Exchange, ToolCall } from "./types.js"
|
|
33
|
-
|
|
34
|
-
export const meta: ExampleMeta = {
|
|
35
|
-
name: "AI Coding Agent",
|
|
36
|
-
description: "Coding agent showcase — ListView, streaming, context tracking",
|
|
37
|
-
demo: true,
|
|
38
|
-
features: ["ListView", "cache", "inline mode", "streaming", "OSC 8 links"],
|
|
39
|
-
// TODO: Add OSC 133 marker support to ListView (km-silvery.listview-markers)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ============================================================================
|
|
43
|
-
// AIChat — TEA state machine + ListView
|
|
44
|
-
// ============================================================================
|
|
45
|
-
|
|
46
|
-
export function AIChat({
|
|
47
|
-
script,
|
|
48
|
-
autoStart,
|
|
49
|
-
fastMode,
|
|
50
|
-
}: {
|
|
51
|
-
script: ScriptEntry[]
|
|
52
|
-
autoStart: boolean
|
|
53
|
-
fastMode: boolean
|
|
54
|
-
}) {
|
|
55
|
-
const exit = useExit()
|
|
56
|
-
const { rows: termRows } = useWindowSize()
|
|
57
|
-
const update = useMemo(() => createDemoUpdate(script, fastMode, autoStart), [script, fastMode, autoStart])
|
|
58
|
-
const [state, send] = useTea(INIT_STATE, update)
|
|
59
|
-
const footerControlRef = useRef<FooterControl>({ submit: () => {} })
|
|
60
|
-
|
|
61
|
-
useEffect(() => send({ type: "mount" }), [send])
|
|
62
|
-
useAutoCompact(state, send)
|
|
63
|
-
useAutoExit(autoStart, state.done, exit)
|
|
64
|
-
useKeyBindings(state, send, footerControlRef)
|
|
65
|
-
|
|
66
|
-
const renderExchange = useCallback(
|
|
67
|
-
(exchange: (typeof state.exchanges)[number], index: number, _meta: ListItemMeta) => {
|
|
68
|
-
const isLatest = index === state.exchanges.length - 1
|
|
69
|
-
return (
|
|
70
|
-
<Box flexDirection="column">
|
|
71
|
-
{index > 0 && <Text> </Text>}
|
|
72
|
-
{state.compacting && isLatest && <CompactingOverlay />}
|
|
73
|
-
{state.done && autoStart && isLatest && <SessionComplete />}
|
|
74
|
-
<ExchangeItem
|
|
75
|
-
exchange={exchange}
|
|
76
|
-
streamPhase={state.streamPhase}
|
|
77
|
-
revealFraction={state.revealFraction}
|
|
78
|
-
pulse={state.pulse}
|
|
79
|
-
isLatest={isLatest}
|
|
80
|
-
isFirstInGroup={exchange.role !== (index > 0 ? state.exchanges[index - 1]!.role : null)}
|
|
81
|
-
isLastInGroup={
|
|
82
|
-
exchange.role !== (index < state.exchanges.length - 1 ? state.exchanges[index + 1]!.role : null)
|
|
83
|
-
}
|
|
84
|
-
/>
|
|
85
|
-
</Box>
|
|
86
|
-
)
|
|
87
|
-
},
|
|
88
|
-
[state, autoStart],
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<Box flexDirection="column" paddingX={1}>
|
|
93
|
-
<ListView
|
|
94
|
-
items={state.exchanges}
|
|
95
|
-
getKey={(ex) => ex.id}
|
|
96
|
-
height={termRows}
|
|
97
|
-
estimateHeight={6}
|
|
98
|
-
renderItem={renderExchange}
|
|
99
|
-
scrollTo={state.exchanges.length - 1}
|
|
100
|
-
cache={{
|
|
101
|
-
mode: "virtual",
|
|
102
|
-
isCacheable: (_ex, index) => index < state.exchanges.length - 1,
|
|
103
|
-
}}
|
|
104
|
-
listFooter={
|
|
105
|
-
<DemoFooter
|
|
106
|
-
controlRef={footerControlRef}
|
|
107
|
-
onSubmit={(text) => send({ type: "submit", text })}
|
|
108
|
-
streamPhase={state.streamPhase}
|
|
109
|
-
done={state.done}
|
|
110
|
-
compacting={state.compacting}
|
|
111
|
-
exchanges={state.exchanges}
|
|
112
|
-
contextBaseline={state.contextBaseline}
|
|
113
|
-
ctrlDPending={state.ctrlDPending}
|
|
114
|
-
nextMessage={getNextMessage(state, script, autoStart)}
|
|
115
|
-
autoTypingText={state.autoTyping ? state.autoTyping.full.slice(0, state.autoTyping.revealed) : null}
|
|
116
|
-
/>
|
|
117
|
-
}
|
|
118
|
-
/>
|
|
119
|
-
</Box>
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ============================================================================
|
|
124
|
-
// Main
|
|
125
|
-
// ============================================================================
|
|
126
|
-
|
|
127
|
-
export async function main() {
|
|
128
|
-
const args = process.argv.slice(2)
|
|
129
|
-
const script = args.includes("--stress") ? generateStressScript() : SCRIPT
|
|
130
|
-
const mode = args.includes("--inline") ? "inline" : "fullscreen"
|
|
131
|
-
|
|
132
|
-
using handle = await run(
|
|
133
|
-
<AIChat script={script} autoStart={args.includes("--auto")} fastMode={args.includes("--fast")} />,
|
|
134
|
-
{ mode: mode as "inline" | "fullscreen", focusReporting: true },
|
|
135
|
-
)
|
|
136
|
-
await handle.waitUntilExit()
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (import.meta.main) {
|
|
140
|
-
await main()
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ============================================================================
|
|
144
|
-
// Hooks
|
|
145
|
-
// ============================================================================
|
|
146
|
-
|
|
147
|
-
function useAutoCompact(state: DemoState, send: (msg: DemoMsg) => void) {
|
|
148
|
-
useEffect(() => {
|
|
149
|
-
if (state.done || state.compacting) return
|
|
150
|
-
const cumulative = computeCumulativeTokens(state.exchanges)
|
|
151
|
-
const effective = Math.max(0, cumulative.currentContext - state.contextBaseline)
|
|
152
|
-
if (effective >= CONTEXT_WINDOW * 0.95) send({ type: "compact" })
|
|
153
|
-
}, [state.exchanges, state.done, state.compacting, state.contextBaseline, send])
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function useAutoExit(autoStart: boolean, done: boolean, exit: () => void) {
|
|
157
|
-
useEffect(() => {
|
|
158
|
-
if (!autoStart || !done) return
|
|
159
|
-
const timer = setTimeout(exit, 1000)
|
|
160
|
-
return () => clearTimeout(timer)
|
|
161
|
-
}, [autoStart, done, exit])
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function useKeyBindings(
|
|
165
|
-
state: DemoState,
|
|
166
|
-
send: (msg: DemoMsg) => void,
|
|
167
|
-
footerControlRef: React.RefObject<FooterControl>,
|
|
168
|
-
) {
|
|
169
|
-
const lastCtrlDRef = useRef(0)
|
|
170
|
-
|
|
171
|
-
useInput((input: string, key: Key) => {
|
|
172
|
-
if (key.escape) return "exit"
|
|
173
|
-
if (key.ctrl && input === "d") {
|
|
174
|
-
const now = Date.now()
|
|
175
|
-
if (now - lastCtrlDRef.current < 500) return "exit"
|
|
176
|
-
lastCtrlDRef.current = now
|
|
177
|
-
send({ type: "setCtrlDPending", pending: true })
|
|
178
|
-
return
|
|
179
|
-
}
|
|
180
|
-
if (lastCtrlDRef.current > 0) {
|
|
181
|
-
lastCtrlDRef.current = 0
|
|
182
|
-
send({ type: "setCtrlDPending", pending: false })
|
|
183
|
-
}
|
|
184
|
-
if (key.tab) {
|
|
185
|
-
if (state.done || state.compacting) return
|
|
186
|
-
footerControlRef.current.submit()
|
|
187
|
-
return
|
|
188
|
-
}
|
|
189
|
-
if (key.ctrl && input === "l") {
|
|
190
|
-
send({ type: "compact" })
|
|
191
|
-
}
|
|
192
|
-
})
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ============================================================================
|
|
196
|
-
// Inline UI fragments
|
|
197
|
-
// ============================================================================
|
|
198
|
-
|
|
199
|
-
function CompactingOverlay() {
|
|
200
|
-
return (
|
|
201
|
-
<Box flexDirection="column" borderStyle="round" borderColor="$warning" paddingX={1} overflow="hidden">
|
|
202
|
-
<Text color="$warning" bold>
|
|
203
|
-
<Spinner type="arc" /> Compacting context
|
|
204
|
-
</Text>
|
|
205
|
-
<Text> </Text>
|
|
206
|
-
<Text color="$muted">Freezing exchanges into terminal scrollback. Scroll up to review.</Text>
|
|
207
|
-
</Box>
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function SessionComplete() {
|
|
212
|
-
return (
|
|
213
|
-
<Box flexDirection="column" borderStyle="round" borderColor="$success" paddingX={1}>
|
|
214
|
-
<Text color="$success" bold>
|
|
215
|
-
{"✓"} Session complete
|
|
216
|
-
</Text>
|
|
217
|
-
<Text color="$muted">Scroll up to review — colors, borders, and hyperlinks preserved in scrollback.</Text>
|
|
218
|
-
</Box>
|
|
219
|
-
)
|
|
220
|
-
}
|