@kidsinai/kids-client 0.0.21 → 0.0.22
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@kidsinai/kids-client",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.22",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Own-client TUI for Kids OpenCode — talks to local `opencode serve` via @opencode-ai/sdk v2 with kid-warm rendering, mission progress, permission dialog, and stderr-tail audit pipeline.",
|
|
7
7
|
"license": "MIT",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"ink-spinner": "^5.0.0",
|
|
36
36
|
"ink-text-input": "^6.0.0",
|
|
37
37
|
"react": "^18.3.1",
|
|
38
|
-
"@kidsinai/kids-opencode-plugin": "^0.0.
|
|
38
|
+
"@kidsinai/kids-opencode-plugin": "^0.0.22"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@opencode-ai/sdk": "^1.14.51",
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
import { Box, Static, Text } from "ink"
|
|
3
3
|
import { getTheme } from "../theme.ts"
|
|
4
|
+
import { MessageBody } from "./MessageBody.tsx"
|
|
4
5
|
import type { ChatMessage } from "../../../core/store.ts"
|
|
5
6
|
|
|
6
7
|
interface ChatStreamProps {
|
|
7
8
|
messages: ChatMessage[]
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
/** Agent text gets markdown/code rendering; kid + system stay plain. */
|
|
12
|
+
function MessageText({ m }: { m: ChatMessage }): React.ReactElement {
|
|
11
13
|
const theme = getTheme()
|
|
14
|
+
const color = colorFor(m.actor, theme)
|
|
15
|
+
if (m.actor === "agent") return <MessageBody text={m.text || " "} color={color} />
|
|
16
|
+
return <Text color={color}>{m.text || " "}</Text>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ChatStream({ messages }: ChatStreamProps): React.ReactElement {
|
|
12
20
|
if (messages.length === 0) return <Box />
|
|
13
21
|
// Settled messages go into <Static> so Ink doesn't re-render the entire
|
|
14
22
|
// history every delta. The active streaming message (if any) renders
|
|
@@ -22,7 +30,7 @@ export function ChatStream({ messages }: ChatStreamProps): React.ReactElement {
|
|
|
22
30
|
<Box key={m.id} flexDirection="row" marginBottom={1}>
|
|
23
31
|
<ActorBadge actor={m.actor} />
|
|
24
32
|
<Box flexDirection="column" flexGrow={1}>
|
|
25
|
-
<
|
|
33
|
+
<MessageText m={m} />
|
|
26
34
|
</Box>
|
|
27
35
|
</Box>
|
|
28
36
|
)}
|
|
@@ -31,7 +39,7 @@ export function ChatStream({ messages }: ChatStreamProps): React.ReactElement {
|
|
|
31
39
|
<Box flexDirection="row" marginBottom={1}>
|
|
32
40
|
<ActorBadge actor={active.actor} />
|
|
33
41
|
<Box flexDirection="column" flexGrow={1}>
|
|
34
|
-
<
|
|
42
|
+
<MessageText m={active} />
|
|
35
43
|
</Box>
|
|
36
44
|
</Box>
|
|
37
45
|
)}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight markdown-ish renderer for the AI's chat messages — no heavy dep.
|
|
3
|
+
* Handles the things a kid actually sees from a coding mentor:
|
|
4
|
+
* - fenced ```code``` blocks (incl. an unterminated one while streaming)
|
|
5
|
+
* - headings (#, ##), bullet lists (-, *), and inline **bold** / `code`
|
|
6
|
+
* Everything else renders as plain wrapped text. Kid/system messages stay plain
|
|
7
|
+
* (they don't author markdown); only agent text is enriched.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from "react"
|
|
11
|
+
import { Box, Text } from "ink"
|
|
12
|
+
import { getTheme } from "../theme.ts"
|
|
13
|
+
|
|
14
|
+
type Theme = ReturnType<typeof getTheme>
|
|
15
|
+
|
|
16
|
+
export interface Block {
|
|
17
|
+
type: "code" | "text"
|
|
18
|
+
/** code language label (may be ""); unused for text blocks. */
|
|
19
|
+
lang?: string
|
|
20
|
+
content: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Split message text into fenced-code vs prose blocks. */
|
|
24
|
+
export function splitBlocks(text: string): Block[] {
|
|
25
|
+
const lines = text.split("\n")
|
|
26
|
+
const blocks: Block[] = []
|
|
27
|
+
let mode: "text" | "code" = "text"
|
|
28
|
+
let buf: string[] = []
|
|
29
|
+
let lang = ""
|
|
30
|
+
const flush = (type: "code" | "text"): void => {
|
|
31
|
+
if (buf.length === 0 && type === "text") return
|
|
32
|
+
blocks.push({ type, lang: type === "code" ? lang : undefined, content: buf.join("\n") })
|
|
33
|
+
buf = []
|
|
34
|
+
}
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const fence = line.match(/^```(\w*)\s*$/)
|
|
37
|
+
if (fence) {
|
|
38
|
+
if (mode === "text") { flush("text"); mode = "code"; lang = fence[1] ?? "" }
|
|
39
|
+
else { flush("code"); mode = "text"; lang = "" }
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
buf.push(line)
|
|
43
|
+
}
|
|
44
|
+
// Unterminated fence while streaming → keep what we have as a code block.
|
|
45
|
+
flush(mode)
|
|
46
|
+
return blocks
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Parse inline **bold** and `code` into Ink <Text> spans. */
|
|
50
|
+
export function renderInline(line: string, theme: Theme, keyPrefix: string): React.ReactNode[] {
|
|
51
|
+
const out: React.ReactNode[] = []
|
|
52
|
+
const re = /(\*\*([^*]+)\*\*|`([^`]+)`)/g
|
|
53
|
+
let last = 0
|
|
54
|
+
let m: RegExpExecArray | null
|
|
55
|
+
let i = 0
|
|
56
|
+
while ((m = re.exec(line)) !== null) {
|
|
57
|
+
if (m.index > last) out.push(<Text key={`${keyPrefix}-t${i}`}>{line.slice(last, m.index)}</Text>)
|
|
58
|
+
if (m[2] !== undefined) out.push(<Text key={`${keyPrefix}-b${i}`} bold>{m[2]}</Text>)
|
|
59
|
+
else if (m[3] !== undefined) out.push(<Text key={`${keyPrefix}-c${i}`} color={theme.accent}>{m[3]}</Text>)
|
|
60
|
+
last = m.index + m[0].length
|
|
61
|
+
i++
|
|
62
|
+
}
|
|
63
|
+
if (last < line.length) out.push(<Text key={`${keyPrefix}-t${i}`}>{line.slice(last)}</Text>)
|
|
64
|
+
if (out.length === 0) out.push(<Text key={`${keyPrefix}-empty`}> </Text>)
|
|
65
|
+
return out
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function MessageBody({ text, color }: { text: string; color: string }): React.ReactElement {
|
|
69
|
+
const theme = getTheme()
|
|
70
|
+
const blocks = splitBlocks(text)
|
|
71
|
+
return (
|
|
72
|
+
<Box flexDirection="column">
|
|
73
|
+
{blocks.map((b, bi) => {
|
|
74
|
+
if (b.type === "code") {
|
|
75
|
+
return (
|
|
76
|
+
<Box key={`code-${bi}`} flexDirection="column" borderStyle="round" borderColor={theme.fgDim} paddingX={1}>
|
|
77
|
+
{b.lang ? <Text color={theme.fgDim} dimColor>{b.lang}</Text> : null}
|
|
78
|
+
{(b.content || " ").split("\n").map((l, li) => (
|
|
79
|
+
<Text key={li} color={theme.success}>{l || " "}</Text>
|
|
80
|
+
))}
|
|
81
|
+
</Box>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
return (
|
|
85
|
+
<Box key={`text-${bi}`} flexDirection="column">
|
|
86
|
+
{b.content.split("\n").map((line, li) => {
|
|
87
|
+
const key = `${bi}-${li}`
|
|
88
|
+
const heading = line.match(/^(#{1,3})\s+(.*)$/)
|
|
89
|
+
if (heading) return <Text key={key} color={theme.accent} bold>{heading[2]}</Text>
|
|
90
|
+
const bullet = line.match(/^\s*[-*]\s+(.*)$/)
|
|
91
|
+
if (bullet) return <Text key={key} color={color}>{"• "}{renderInline(bullet[1] ?? "", theme, key)}</Text>
|
|
92
|
+
return <Text key={key} color={color}>{renderInline(line, theme, key)}</Text>
|
|
93
|
+
})}
|
|
94
|
+
</Box>
|
|
95
|
+
)
|
|
96
|
+
})}
|
|
97
|
+
</Box>
|
|
98
|
+
)
|
|
99
|
+
}
|