create-theokit 1.2.4 → 1.2.6
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,5 +1,5 @@
|
|
|
1
1
|
import { Box, Text, useApp, useInput } from 'ink'
|
|
2
|
-
import { type ReactElement } from 'react'
|
|
2
|
+
import { useEffect, useState, type ReactElement } from 'react'
|
|
3
3
|
import {
|
|
4
4
|
AgentStreaming,
|
|
5
5
|
AppStatusBar,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
WelcomeBanner,
|
|
11
11
|
} from '@theokit/tui'
|
|
12
12
|
import { uiMessagesToChatThread } from '@theokit/tui/ai-sdk'
|
|
13
|
+
import type { UIMessage } from 'ai'
|
|
13
14
|
import { InProcessTransport, useAgent } from 'theokit/client'
|
|
14
15
|
import { streamAgentTurnInProcess } from 'theokit/server/agent'
|
|
15
16
|
|
|
@@ -19,17 +20,31 @@ import * as chatAgent from '../agents/chat.js'
|
|
|
19
20
|
const MODEL = 'openai/gpt-4o-mini'
|
|
20
21
|
const CWD = process.cwd()
|
|
21
22
|
|
|
23
|
+
/** The agent's opening line — so the conversation starts warm instead of empty (like a coding-agent CLI). */
|
|
24
|
+
const GREETING: UIMessage = {
|
|
25
|
+
id: 'greeting',
|
|
26
|
+
role: 'assistant',
|
|
27
|
+
parts: [
|
|
28
|
+
{
|
|
29
|
+
type: 'text',
|
|
30
|
+
text: "Hi — I'm your TheoKit agent. Ask me anything and I'll stream a reply. Type below and press Enter · /clear resets · esc cancels.",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
/** Resolve the provider key from the environment (OpenRouter / Anthropic / OpenAI). */
|
|
23
36
|
const apiKey = (): string =>
|
|
24
37
|
process.env.OPENROUTER_API_KEY ?? process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY ?? ''
|
|
25
38
|
|
|
26
39
|
/**
|
|
27
40
|
* M46 — the terminal surface, composed from `@theokit/tui` the way a Claude Code / OpenCode / Codex CLI is:
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
41
|
+
* `WelcomeBanner` header, scrolling `<ChatThread>`, live `<AgentStreaming>`, bordered `<ChatComposer>`, and a
|
|
42
|
+
* persistent `<AppStatusBar>` footer. Driven by the unified `useAgent` hook (M41).
|
|
43
|
+
*
|
|
44
|
+
* `useAgent` opens a FRESH stream per send — its `messages` hold only the CURRENT turn (and the SDK assigns
|
|
45
|
+
* them no stable id), so we OWN the transcript: `history` accumulates every finished turn with our own unique
|
|
46
|
+
* ids (`u-N` / `a-N`), and the in-flight reply is shown live until it commits. This keeps the order correct,
|
|
47
|
+
* the history complete, and every id unique (ChatThread rejects duplicates).
|
|
33
48
|
*/
|
|
34
49
|
const transport = new InProcessTransport({
|
|
35
50
|
run: (input) => streamAgentTurnInProcess(chatAgent, apiKey(), input),
|
|
@@ -41,6 +56,32 @@ export function App(): ReactElement {
|
|
|
41
56
|
const elapsed = useTurnElapsed(streaming)
|
|
42
57
|
const { exit } = useApp()
|
|
43
58
|
|
|
59
|
+
const [history, setHistory] = useState<UIMessage[]>([GREETING])
|
|
60
|
+
|
|
61
|
+
const users = history.filter((m) => m.role === 'user').length
|
|
62
|
+
const replies = history.filter((m) => m.role === 'assistant' && m.id !== 'greeting').length
|
|
63
|
+
// A sent prompt is still awaiting its committed reply — the current turn is "in flight".
|
|
64
|
+
const pending = users > replies
|
|
65
|
+
|
|
66
|
+
// Merge the in-flight turn's parts into ONE assistant message with our own unique id (the SDK's ids are
|
|
67
|
+
// empty, which would collide in the thread). `suffix` distinguishes the live copy from the committed one.
|
|
68
|
+
const inflightReply = (suffix: string): UIMessage => ({
|
|
69
|
+
id: `a-${String(replies)}${suffix}`,
|
|
70
|
+
role: 'assistant',
|
|
71
|
+
parts: agent.messages.flatMap((m) => m.parts),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// Commit the finished reply into history exactly once (the next send resets `agent.messages`).
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!streaming && pending && agent.messages.length > 0) {
|
|
77
|
+
setHistory((h) => [...h, inflightReply('')])
|
|
78
|
+
}
|
|
79
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
|
+
}, [streaming, pending, agent.messages, replies])
|
|
81
|
+
|
|
82
|
+
// Transcript = committed history + the in-flight reply (shown until it commits — no flicker, no double).
|
|
83
|
+
const thread = pending && agent.messages.length > 0 ? [...history, inflightReply('-live')] : history
|
|
84
|
+
|
|
44
85
|
// Esc cancels a running turn, or quits when idle (the Claude Code affordance).
|
|
45
86
|
useInput((_input, key) => {
|
|
46
87
|
if (!key.escape) return
|
|
@@ -52,9 +93,14 @@ export function App(): ReactElement {
|
|
|
52
93
|
const trimmed = text.trim()
|
|
53
94
|
if (trimmed.length === 0) return
|
|
54
95
|
if (trimmed === '/clear') {
|
|
96
|
+
setHistory([GREETING])
|
|
55
97
|
agent.reset()
|
|
56
98
|
return
|
|
57
99
|
}
|
|
100
|
+
setHistory((h) => [
|
|
101
|
+
...h,
|
|
102
|
+
{ id: `u-${String(h.length)}`, role: 'user', parts: [{ type: 'text', text: trimmed }] },
|
|
103
|
+
])
|
|
58
104
|
agent.send({ message: trimmed })
|
|
59
105
|
}
|
|
60
106
|
|
|
@@ -69,7 +115,7 @@ export function App(): ReactElement {
|
|
|
69
115
|
<Text dimColor>cwd: {CWD}</Text>
|
|
70
116
|
</WelcomeBanner>
|
|
71
117
|
|
|
72
|
-
<ChatThread messages={uiMessagesToChatThread(
|
|
118
|
+
<ChatThread messages={uiMessagesToChatThread(thread)} />
|
|
73
119
|
|
|
74
120
|
{streaming ? <AgentStreaming thought="Thinking…" elapsedSeconds={elapsed} showCancelHint /> : null}
|
|
75
121
|
{agent.error ? <Text color="red">⚠ {agent.error.message}</Text> : null}
|