create-theokit 1.21.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
import { homedir } from 'node:os'
|
|
2
2
|
|
|
3
3
|
import { Box, Text, useApp, useInput } from 'ink'
|
|
4
|
-
import { type ReactElement, useState } from 'react'
|
|
4
|
+
import { type ReactElement, useEffect, useState } from 'react'
|
|
5
5
|
import {
|
|
6
6
|
AgentStreaming,
|
|
7
7
|
AgentTimeline,
|
|
8
8
|
ChatComposer,
|
|
9
|
+
ContextWindowBar,
|
|
10
|
+
CostMeter,
|
|
9
11
|
DEFAULT_COMPOSER_SHORTCUTS,
|
|
10
12
|
findPendingApproval,
|
|
11
13
|
InkInputProvider,
|
|
12
14
|
KeyboardHelp,
|
|
13
15
|
messagesToAgentEvents,
|
|
16
|
+
MultiStepProgress,
|
|
14
17
|
Notice,
|
|
15
18
|
PermissionPrompt,
|
|
19
|
+
PlanApproval,
|
|
20
|
+
ProgressActivity,
|
|
21
|
+
ProgressBar,
|
|
22
|
+
QuestionPrompt,
|
|
16
23
|
readTurnUsage,
|
|
24
|
+
SelectList,
|
|
25
|
+
type SelectListItem,
|
|
26
|
+
Stack,
|
|
17
27
|
StatusFooter,
|
|
18
28
|
TheoTUIProvider,
|
|
29
|
+
Toast,
|
|
30
|
+
type TodoItem,
|
|
31
|
+
type TokenCategory,
|
|
32
|
+
TokenUsageChart,
|
|
19
33
|
useTurnElapsed,
|
|
20
34
|
type UIMessageLike,
|
|
21
35
|
} from '@theokit/tui'
|
|
@@ -56,11 +70,40 @@ const GREETING: UIMessageLike = {
|
|
|
56
70
|
const apiKey = (): string =>
|
|
57
71
|
process.env.OPENROUTER_API_KEY ?? process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY ?? ''
|
|
58
72
|
|
|
73
|
+
// ── Slash-command demo data ────────────────────────────────────────────────────────────────────────
|
|
74
|
+
// The interactive `@theokit/tui` surfaces (`/plan`, `/ask`, `/select`, `/progress`) run live IN the app,
|
|
75
|
+
// triggered from the composer, with real handlers — not a separate gallery. Delete the ones you don't need.
|
|
76
|
+
const DEMO_PLAN = [
|
|
77
|
+
'## Proposed plan',
|
|
78
|
+
'',
|
|
79
|
+
'1. Scaffold the `reports` route',
|
|
80
|
+
'2. Add the `generateReport` server action',
|
|
81
|
+
'3. Stream results into the dashboard',
|
|
82
|
+
].join('\n')
|
|
83
|
+
|
|
84
|
+
const DEMO_ASK_OPTIONS: readonly SelectListItem[] = [
|
|
85
|
+
{ value: 'python', label: 'Python (FastAPI)', description: 'polyglot service via --backend python' },
|
|
86
|
+
{ value: 'node', label: 'Node (Hono)', description: 'fetch-handler service' },
|
|
87
|
+
{ value: 'none', label: 'TypeScript only', description: 'no external backend' },
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
const DEMO_SELECT_ITEMS: readonly SelectListItem[] = [
|
|
91
|
+
{ value: 'auth', label: 'Auth', description: 'sessions + requireAuth' },
|
|
92
|
+
{ value: 'db', label: 'Database', description: 'SQLite by default' },
|
|
93
|
+
{ value: 'ws', label: 'WebSocket', description: 'realtime channel' },
|
|
94
|
+
{ value: 'deploy', label: 'Deploy', description: 'TheoCloud target' },
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
const DEMO_STEP_LABELS = ['Plan', 'Generate', 'Validate', 'Ship'] as const
|
|
98
|
+
|
|
99
|
+
/** The composer mode — `chat` is the default; the others swap the composer for a live demo surface. */
|
|
100
|
+
type Mode = 'chat' | 'plan' | 'ask' | 'select' | 'progress'
|
|
101
|
+
|
|
59
102
|
/**
|
|
60
103
|
* The terminal surface, composed from `@theokit/tui` the way a Claude Code / OpenCode / Codex CLI is:
|
|
61
104
|
* `WelcomeBanner` header, a scrolling `<AgentTimeline>` (assistant turns rendered as Markdown + fenced code,
|
|
62
105
|
* tool calls as collapsible cards, thinking rows — the Claude-Code render), a live `<AgentStreaming>` spinner,
|
|
63
|
-
* a bordered `<ChatComposer>`, and a persistent `<
|
|
106
|
+
* a bordered `<ChatComposer>`, and a persistent `<StatusFooter>` footer. Driven by the unified `useAgent`
|
|
64
107
|
* hook (M41).
|
|
65
108
|
*
|
|
66
109
|
* The conversation comes from `useAgent().thread` (M46), projected to the timeline's `AgentEvent[]` by the
|
|
@@ -152,15 +195,33 @@ export function App(): ReactElement {
|
|
|
152
195
|
// a resolved state (it is keyed by its own id, distinct from the tool call), so we remember settled ids
|
|
153
196
|
// to hide the prompt once answered instead of re-showing it forever.
|
|
154
197
|
const [settledApprovals, setSettledApprovals] = useState<readonly string[]>([])
|
|
198
|
+
// The composer mode + the slash-command demo state (a plan/ask/select/progress surface swaps the composer).
|
|
199
|
+
const [mode, setMode] = useState<Mode>('chat')
|
|
200
|
+
const [progressStep, setProgressStep] = useState(0)
|
|
201
|
+
// `/usage` toggles a token-usage panel; a Toast surfaces transient outcomes (a demo answered, a task done).
|
|
202
|
+
const [showUsage, setShowUsage] = useState(false)
|
|
203
|
+
const [toast, setToast] = useState<{ message: string; variant: 'info' | 'success' | 'error' } | null>(
|
|
204
|
+
null,
|
|
205
|
+
)
|
|
155
206
|
|
|
156
207
|
// The store owns the conversation (M46) — prepend the warm greeting and project to timeline events.
|
|
157
208
|
const events = messagesToAgentEvents([GREETING, ...agent.thread])
|
|
158
209
|
|
|
159
210
|
// The last turn's usage (readTurnUsage reads the totals the agent stream rides on each assistant
|
|
160
|
-
// message's metadata) drives the streaming token count
|
|
161
|
-
// the first turn
|
|
211
|
+
// message's metadata) drives the streaming token count, the footer's context usage, AND the `/usage`
|
|
212
|
+
// observability panel (ContextWindowBar / TokenUsageChart / CostMeter). Undefined until the first turn.
|
|
162
213
|
const lastUsage = agent.thread.map(readTurnUsage).filter((u) => u !== undefined).at(-1)
|
|
163
214
|
|
|
215
|
+
// Only PRESENT categories render in the chart — build the map from whatever the turn actually reported.
|
|
216
|
+
const usageChart: Partial<Record<TokenCategory, number>> = lastUsage
|
|
217
|
+
? {
|
|
218
|
+
input: lastUsage.inputTokens,
|
|
219
|
+
output: lastUsage.outputTokens,
|
|
220
|
+
...(lastUsage.cacheReadTokens !== undefined ? { cached: lastUsage.cacheReadTokens } : {}),
|
|
221
|
+
...(lastUsage.reasoningTokens !== undefined ? { reasoning: lastUsage.reasoningTokens } : {}),
|
|
222
|
+
}
|
|
223
|
+
: {}
|
|
224
|
+
|
|
164
225
|
// Human-in-the-loop: a gated tool (`send_notification`) pauses the run awaiting a decision. When one is
|
|
165
226
|
// pending (and not already settled) we show the approval prompt IN PLACE OF the composer and settle it.
|
|
166
227
|
const rawApproval = findPendingApproval(agent.thread)
|
|
@@ -172,13 +233,32 @@ export function App(): ReactElement {
|
|
|
172
233
|
void agent.approve(approvalId, { approved })
|
|
173
234
|
}
|
|
174
235
|
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
|
|
236
|
+
// `/progress` runs a live multi-step task: advance one step every 700ms; when it finishes, toast + return
|
|
237
|
+
// to chat. This is the only mode driven by a timer — the interactive modes complete on the user's input.
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (mode !== 'progress') return
|
|
240
|
+
if (progressStep >= DEMO_STEP_LABELS.length) {
|
|
241
|
+
const done = setTimeout(() => {
|
|
242
|
+
setMode('chat')
|
|
243
|
+
setToast({ message: 'Task complete', variant: 'success' })
|
|
244
|
+
}, 600)
|
|
245
|
+
return () => clearTimeout(done)
|
|
246
|
+
}
|
|
247
|
+
const tick = setTimeout(() => setProgressStep((s) => s + 1), 700)
|
|
248
|
+
return () => clearTimeout(tick)
|
|
249
|
+
}, [mode, progressStep])
|
|
250
|
+
|
|
251
|
+
const inDemoInput = mode === 'plan' || mode === 'ask' || mode === 'select'
|
|
252
|
+
|
|
253
|
+
// Global keys. A gated tool (`PermissionPrompt`) or an interactive demo (`PlanApproval`/`QuestionPrompt`/
|
|
254
|
+
// `SelectList`) OWNS the keys while active — stay out of its way. Otherwise: Esc exits the progress demo /
|
|
255
|
+
// closes the usage panel / closes help / cancels a running turn; Ctrl+C cancels a turn, else arms → quits.
|
|
178
256
|
useInput((input, key) => {
|
|
179
|
-
if (pendingApproval) return
|
|
257
|
+
if (pendingApproval || inDemoInput) return
|
|
180
258
|
if (key.escape) {
|
|
181
|
-
if (
|
|
259
|
+
if (mode === 'progress') setMode('chat')
|
|
260
|
+
else if (showUsage) setShowUsage(false)
|
|
261
|
+
else if (showHelp) setShowHelp(false)
|
|
182
262
|
else if (streaming) agent.abort()
|
|
183
263
|
return
|
|
184
264
|
}
|
|
@@ -197,43 +277,84 @@ export function App(): ReactElement {
|
|
|
197
277
|
const handleSubmit = (text: string): void => {
|
|
198
278
|
const trimmed = text.trim()
|
|
199
279
|
if (trimmed.length === 0) return
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
280
|
+
switch (trimmed) {
|
|
281
|
+
case '/clear':
|
|
282
|
+
agent.reset()
|
|
283
|
+
return
|
|
284
|
+
case '/help':
|
|
285
|
+
setShowHelp((h) => !h)
|
|
286
|
+
return
|
|
287
|
+
case '/usage':
|
|
288
|
+
setShowUsage((u) => !u)
|
|
289
|
+
return
|
|
290
|
+
case '/plan':
|
|
291
|
+
setMode('plan')
|
|
292
|
+
return
|
|
293
|
+
case '/ask':
|
|
294
|
+
setMode('ask')
|
|
295
|
+
return
|
|
296
|
+
case '/select':
|
|
297
|
+
setMode('select')
|
|
298
|
+
return
|
|
299
|
+
case '/progress':
|
|
300
|
+
setProgressStep(0)
|
|
301
|
+
setMode('progress')
|
|
302
|
+
return
|
|
303
|
+
default:
|
|
304
|
+
agent.send({ message: trimmed })
|
|
207
305
|
}
|
|
208
|
-
agent.send({ message: trimmed })
|
|
209
306
|
}
|
|
210
307
|
|
|
308
|
+
// The `/progress` lanes: steps before the cursor are done, the cursor is active, the rest are pending.
|
|
309
|
+
const progressSteps: readonly TodoItem[] = DEMO_STEP_LABELS.map((label, i) => ({
|
|
310
|
+
id: label,
|
|
311
|
+
label,
|
|
312
|
+
status: i < progressStep ? 'done' : i === progressStep ? 'active' : 'pending',
|
|
313
|
+
}))
|
|
314
|
+
const progressPercent = Math.min(100, Math.round((progressStep / DEMO_STEP_LABELS.length) * 100))
|
|
315
|
+
|
|
211
316
|
return (
|
|
212
317
|
<TheoTUIProvider theme={THEME}>
|
|
213
|
-
{/* InkInputProvider bridges Ink's stdin to @theokit/tui's interactive surfaces (PermissionPrompt
|
|
214
|
-
|
|
318
|
+
{/* InkInputProvider bridges Ink's stdin to @theokit/tui's interactive surfaces (PermissionPrompt +
|
|
319
|
+
PlanApproval / QuestionPrompt / SelectList) so they receive keys under plain Ink — the ChatComposer
|
|
320
|
+
keeps Ink's own hooks. `Stack` gives the top-level sections the Claude-Code one-line cadence. */}
|
|
215
321
|
<InkInputProvider>
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
<AgentTimeline events={events} />
|
|
220
|
-
|
|
221
|
-
{streaming ? (
|
|
222
|
-
<AgentStreaming
|
|
223
|
-
phrases={THINKING_PHRASES}
|
|
224
|
-
shimmer
|
|
225
|
-
elapsedSeconds={elapsed}
|
|
226
|
-
tokens={lastUsage?.totalTokens}
|
|
227
|
-
tokenDirection="down"
|
|
228
|
-
showCancelHint
|
|
229
|
-
/>
|
|
230
|
-
) : null}
|
|
231
|
-
{agent.error ? <Notice variant="error">{agent.error.message}</Notice> : null}
|
|
322
|
+
<Stack gap={1}>
|
|
323
|
+
<Banner />
|
|
232
324
|
|
|
233
|
-
|
|
325
|
+
<AgentTimeline events={events} />
|
|
234
326
|
|
|
235
|
-
|
|
236
|
-
|
|
327
|
+
{streaming ? (
|
|
328
|
+
<AgentStreaming
|
|
329
|
+
phrases={THINKING_PHRASES}
|
|
330
|
+
shimmer
|
|
331
|
+
elapsedSeconds={elapsed}
|
|
332
|
+
tokens={lastUsage?.totalTokens}
|
|
333
|
+
tokenDirection="down"
|
|
334
|
+
showCancelHint
|
|
335
|
+
/>
|
|
336
|
+
) : null}
|
|
337
|
+
{agent.error ? <Notice variant="error">{agent.error.message}</Notice> : null}
|
|
338
|
+
|
|
339
|
+
{/* `/usage` — the observability panel, from the last turn's real usage. ContextWindowBar shows the
|
|
340
|
+
context fill, TokenUsageChart the per-category breakdown, CostMeter the session cost (if reported). */}
|
|
341
|
+
{showUsage && lastUsage ? (
|
|
342
|
+
<Box flexDirection="column">
|
|
343
|
+
<ContextWindowBar usedTokens={lastUsage.inputTokens} limitTokens={AGENT.contextWindow} />
|
|
344
|
+
<TokenUsageChart usage={usageChart} />
|
|
345
|
+
{lastUsage.cost !== undefined ? <CostMeter costUsd={lastUsage.cost} /> : null}
|
|
346
|
+
</Box>
|
|
347
|
+
) : null}
|
|
348
|
+
|
|
349
|
+
{/* A transient outcome banner (a demo answered, a task finished). Auto-dismisses after 5s. */}
|
|
350
|
+
{toast ? (
|
|
351
|
+
<Toast message={toast.message} variant={toast.variant} onDismiss={() => setToast(null)} />
|
|
352
|
+
) : null}
|
|
353
|
+
|
|
354
|
+
{showHelp ? <KeyboardHelp shortcuts={DEFAULT_COMPOSER_SHORTCUTS} /> : null}
|
|
355
|
+
|
|
356
|
+
{/* The input area: a gated tool's approval card, an interactive demo surface (/plan /ask /select
|
|
357
|
+
/progress), or the composer. Each demo completes back to chat (Enter) or cancels (Esc). */}
|
|
237
358
|
{pendingApproval ? (
|
|
238
359
|
<PermissionPrompt
|
|
239
360
|
toolType="Tool call"
|
|
@@ -250,6 +371,56 @@ export function App(): ReactElement {
|
|
|
250
371
|
settleApproval(pendingApproval.approvalId, decision === 'yes')
|
|
251
372
|
}}
|
|
252
373
|
/>
|
|
374
|
+
) : mode === 'plan' ? (
|
|
375
|
+
<PlanApproval
|
|
376
|
+
plan={DEMO_PLAN}
|
|
377
|
+
onDecision={(d) => {
|
|
378
|
+
setMode('chat')
|
|
379
|
+
setToast({
|
|
380
|
+
message:
|
|
381
|
+
d.kind === 'approve'
|
|
382
|
+
? 'Plan approved'
|
|
383
|
+
: `Revision requested${d.feedback ? `: ${d.feedback}` : ''}`,
|
|
384
|
+
variant: 'success',
|
|
385
|
+
})
|
|
386
|
+
}}
|
|
387
|
+
/>
|
|
388
|
+
) : mode === 'ask' ? (
|
|
389
|
+
<QuestionPrompt
|
|
390
|
+
header="Backend"
|
|
391
|
+
question="Which backend should the app ship next to?"
|
|
392
|
+
options={DEMO_ASK_OPTIONS}
|
|
393
|
+
allowFreeText
|
|
394
|
+
onAnswer={(a) => {
|
|
395
|
+
setMode('chat')
|
|
396
|
+
setToast({
|
|
397
|
+
message: `Answered: ${a.values.join(', ')}${a.text ? ` (${a.text})` : ''}`,
|
|
398
|
+
variant: 'info',
|
|
399
|
+
})
|
|
400
|
+
}}
|
|
401
|
+
/>
|
|
402
|
+
) : mode === 'select' ? (
|
|
403
|
+
<SelectList
|
|
404
|
+
items={DEMO_SELECT_ITEMS}
|
|
405
|
+
multi
|
|
406
|
+
onSubmit={(values) => {
|
|
407
|
+
setMode('chat')
|
|
408
|
+
setToast({ message: `Selected: ${values.join(', ') || '(none)'}`, variant: 'info' })
|
|
409
|
+
}}
|
|
410
|
+
/>
|
|
411
|
+
) : mode === 'progress' ? (
|
|
412
|
+
<Box flexDirection="column">
|
|
413
|
+
<MultiStepProgress steps={progressSteps} current={progressStep} groupLabel="Demo task" />
|
|
414
|
+
<ProgressActivity
|
|
415
|
+
label="Working…"
|
|
416
|
+
percent={progressPercent}
|
|
417
|
+
elapsedSeconds={elapsed}
|
|
418
|
+
tokens={lastUsage?.totalTokens}
|
|
419
|
+
tokenDirection="up"
|
|
420
|
+
/>
|
|
421
|
+
<ProgressBar percent={progressPercent} />
|
|
422
|
+
<Text dimColor>esc to exit</Text>
|
|
423
|
+
</Box>
|
|
253
424
|
) : (
|
|
254
425
|
<ChatComposer
|
|
255
426
|
placeholder={PLACEHOLDER}
|
|
@@ -258,27 +429,31 @@ export function App(): ReactElement {
|
|
|
258
429
|
commands={[
|
|
259
430
|
{ name: 'clear', description: 'clear the conversation' },
|
|
260
431
|
{ name: 'help', description: 'toggle the keyboard shortcuts panel' },
|
|
432
|
+
{ name: 'usage', description: 'toggle the token-usage panel' },
|
|
433
|
+
{ name: 'plan', description: 'demo: plan approval card' },
|
|
434
|
+
{ name: 'ask', description: 'demo: question prompt' },
|
|
435
|
+
{ name: 'select', description: 'demo: multi-select list' },
|
|
436
|
+
{ name: 'progress', description: 'demo: multi-step progress' },
|
|
261
437
|
]}
|
|
262
438
|
onHelpToggle={() => setShowHelp((h) => !h)}
|
|
263
439
|
onSubmit={handleSubmit}
|
|
264
440
|
/>
|
|
265
441
|
)}
|
|
266
|
-
</Box>
|
|
267
442
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
</
|
|
443
|
+
{/* Claude Code's two-line footer: top-left the model, top-right the context usage; bottom the
|
|
444
|
+
shortcuts hint. `StatusFooter` justifies the top row to the terminal edges. */}
|
|
445
|
+
<StatusFooter
|
|
446
|
+
left={<Text>{MODEL}</Text>}
|
|
447
|
+
right={
|
|
448
|
+
lastUsage ? (
|
|
449
|
+
<Text>
|
|
450
|
+
{fmtK(lastUsage.inputTokens)}/{fmtK(AGENT.contextWindow)} context
|
|
451
|
+
</Text>
|
|
452
|
+
) : undefined
|
|
453
|
+
}
|
|
454
|
+
hint="? for shortcuts"
|
|
455
|
+
/>
|
|
456
|
+
</Stack>
|
|
282
457
|
</InkInputProvider>
|
|
283
458
|
</TheoTUIProvider>
|
|
284
459
|
)
|