mirai-cli 0.1.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.
Files changed (46) hide show
  1. package/bin/config.ts +99 -0
  2. package/bin/mirai.js +17 -0
  3. package/bin/mirai.ts +4 -0
  4. package/bin/provider.ts +149 -0
  5. package/bin/router.ts +134 -0
  6. package/dist/mirai.mjs +28316 -0
  7. package/package.json +29 -0
  8. package/src/app/index.tsx +274 -0
  9. package/src/components/chat.tsx +254 -0
  10. package/src/components/dialog/help-dialog.tsx +101 -0
  11. package/src/components/dialog/index.ts +3 -0
  12. package/src/components/dialog/provider.tsx +96 -0
  13. package/src/components/header/index.tsx +78 -0
  14. package/src/components/input/command-palette.tsx +129 -0
  15. package/src/components/input/commands.ts +46 -0
  16. package/src/components/input/index.tsx +284 -0
  17. package/src/components/matrix-rain/index.tsx +122 -0
  18. package/src/components/permission-modal.tsx +66 -0
  19. package/src/components/scroll-bar/index.tsx +56 -0
  20. package/src/components/status-bar/index.tsx +43 -0
  21. package/src/components/tool-result.tsx +11 -0
  22. package/src/hooks/use-chat.ts +208 -0
  23. package/src/hooks/use-mouse.tsx +121 -0
  24. package/src/hooks/use-permission.ts +35 -0
  25. package/src/hooks/use-runtime.ts +99 -0
  26. package/src/hooks/use-scroll-bar-drag.ts +115 -0
  27. package/src/hooks/use-scroll.ts +70 -0
  28. package/src/index.ts +39 -0
  29. package/src/renderers/builtins/BashResult.tsx +65 -0
  30. package/src/renderers/builtins/EditFileResult.tsx +69 -0
  31. package/src/renderers/builtins/GenericToolResult.tsx +39 -0
  32. package/src/renderers/builtins/GlobSearchResult.tsx +40 -0
  33. package/src/renderers/builtins/GrepSearchResult.tsx +49 -0
  34. package/src/renderers/builtins/ReadFileResult.tsx +54 -0
  35. package/src/renderers/builtins/WriteFileResult.tsx +24 -0
  36. package/src/renderers/constants.ts +7 -0
  37. package/src/renderers/register-builtins.ts +27 -0
  38. package/src/renderers/registry.ts +37 -0
  39. package/src/renderers/status.ts +22 -0
  40. package/src/renderers/utils.ts +70 -0
  41. package/src/services/hit-test.ts +49 -0
  42. package/src/services/mouse-input.ts +237 -0
  43. package/src/services/scroll-registry.ts +64 -0
  44. package/src/services/tui-permission-provider.ts +35 -0
  45. package/src/theme.ts +38 -0
  46. package/tsconfig.json +27 -0
@@ -0,0 +1,96 @@
1
+ import { Box } from "ink";
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useState,
6
+ useCallback,
7
+ useEffect,
8
+ type ReactNode,
9
+ type MutableRefObject,
10
+ } from "react";
11
+
12
+ let nextId = 0;
13
+
14
+ interface DialogItem {
15
+ id: string;
16
+ node: ReactNode;
17
+ }
18
+
19
+ interface DialogCtx {
20
+ show: (render: () => ReactNode) => void;
21
+ push: (render: () => ReactNode) => void;
22
+ close: () => void;
23
+ isActive: boolean;
24
+ }
25
+
26
+ export interface DialogBridge {
27
+ show: (render: () => ReactNode) => void;
28
+ isActive: boolean;
29
+ }
30
+
31
+ const DialogContext = createContext<DialogCtx>({
32
+ show: () => {},
33
+ push: () => {},
34
+ close: () => {},
35
+ isActive: false,
36
+ });
37
+
38
+ export function DialogProvider({
39
+ children,
40
+ dialogRef,
41
+ }: {
42
+ children: ReactNode;
43
+ dialogRef?: MutableRefObject<DialogBridge>;
44
+ }) {
45
+ const [stack, setStack] = useState<DialogItem[]>([]);
46
+
47
+ const show = useCallback((render: () => ReactNode) => {
48
+ const id = `dialog-${++nextId}`;
49
+ setStack([{ id, node: render() }]);
50
+ }, []);
51
+
52
+ const push = useCallback((render: () => ReactNode) => {
53
+ const id = `dialog-${++nextId}`;
54
+ setStack((prev) => [...prev, { id, node: render() }]);
55
+ }, []);
56
+
57
+ const close = useCallback(() => {
58
+ setStack((prev) => prev.slice(0, -1));
59
+ }, []);
60
+
61
+ const ctx: DialogCtx = {
62
+ show,
63
+ push,
64
+ close,
65
+ isActive: stack.length > 0,
66
+ };
67
+
68
+ // Sync state into external ref (so parent components can read show/isActive)
69
+ useEffect(() => {
70
+ if (dialogRef) {
71
+ dialogRef.current = { show, isActive: stack.length > 0 };
72
+ }
73
+ });
74
+
75
+ return (
76
+ <DialogContext.Provider value={ctx}>
77
+ {children}
78
+ {stack.map((item, i) => (
79
+ <Box
80
+ key={item.id}
81
+ position="absolute"
82
+ top={0}
83
+ left={0}
84
+ width="100%"
85
+ height="100%"
86
+ >
87
+ {item.node}
88
+ </Box>
89
+ ))}
90
+ </DialogContext.Provider>
91
+ );
92
+ }
93
+
94
+ export function useDialog(): DialogCtx {
95
+ return useContext(DialogContext);
96
+ }
@@ -0,0 +1,78 @@
1
+ import { Box, Text } from "ink";
2
+ import { Badge } from "@inkjs/ui";
3
+ import { memo } from "react";
4
+ import { default as Spinner } from "ink-spinner";
5
+ import BigText from "ink-big-text";
6
+ import Gradient from "ink-gradient";
7
+ import { NEON_COLORS } from "@mirai/core/constants";
8
+ import { toolRegistry } from "@mirai/tools";
9
+ import { theme } from "../../theme.js";
10
+
11
+ const MODEL = process.env.OLLAMA_MODEL || "qwen3.5:4b";
12
+ const DIR = process.cwd();
13
+ const TOOLS = toolRegistry.list();
14
+
15
+ function Header() {
16
+ return (
17
+ <Box flexDirection="column" paddingX={0} paddingY={0}>
18
+ <Box flexDirection="row" justifyContent="center">
19
+ <Gradient
20
+ colors={[
21
+ { color: "#7aa2f7", pos: 0 }, // xanh dương
22
+ { color: "#bb9af7", pos: 0.4 }, // tím
23
+ { color: "#e0af68", pos: 0.7 }, // cam
24
+ { color: "#9ece6a", pos: 1 },
25
+ ]}
26
+ >
27
+ <BigText text="mirai" letterSpacing={0} />
28
+ </Gradient>
29
+ </Box>
30
+ <Box
31
+ borderStyle="round"
32
+ borderColor={theme.bg.surface}
33
+ backgroundColor={theme.bg.alt}
34
+ paddingX={2}
35
+ paddingY={1}
36
+ width="100%"
37
+ flexDirection="row"
38
+ >
39
+ <Box flexDirection="column" marginRight={2}>
40
+ <Box>
41
+ <Badge color={theme.info.primary}>
42
+ <Text backgroundColor={theme.info.primary}>MODEL</Text>
43
+ </Badge>
44
+ <Text> </Text>
45
+ <Text color={theme.role.user}>{MODEL}</Text>
46
+ </Box>
47
+ <Box marginTop={1}>
48
+ <Badge color={theme.info.primary}>
49
+ <Text backgroundColor={theme.info.primary}>DIR</Text>
50
+ </Badge>
51
+ <Text> </Text>
52
+ <Text color={theme.text.dim}>{DIR}</Text>
53
+ </Box>
54
+ <Box marginTop={1}>
55
+ <Badge color={theme.warning.primary}>
56
+ <Text backgroundColor={theme.warning.primary}>TOOLS</Text>
57
+ </Badge>
58
+ <Text> </Text>
59
+ <Text color={theme.text.dim}>▶ {TOOLS.length} available</Text>
60
+ </Box>
61
+ <Box flexDirection="column" marginTop={1}>
62
+ {TOOLS.map((t) => (
63
+ <Text key={t.name} color={theme.text.primary}>
64
+ {" "}
65
+ ○ {t.name}
66
+ </Text>
67
+ ))}
68
+ </Box>
69
+ </Box>
70
+ <Box flexGrow={1} />
71
+ <Box>
72
+ <Spinner type="dots" />
73
+ </Box>
74
+ </Box>
75
+ </Box>
76
+ );
77
+ }
78
+ export default memo(Header);
@@ -0,0 +1,129 @@
1
+ import { Box, Text, useCursor } from "ink";
2
+ import { useRef, useCallback, useEffect } from "react";
3
+ import { useOnMouseClick } from "../../hooks/use-mouse.js";
4
+ import { scrollRegistry } from "../../services/scroll-registry.js";
5
+ import type { CommandDef } from "./commands.js";
6
+ import { ScrollView } from "ink-scroll-view";
7
+ import { theme } from "../../theme.js";
8
+ import { NEON_COLORS } from "@mirai/core/constants";
9
+
10
+ interface Props {
11
+ commands: CommandDef[];
12
+ query: string;
13
+ selectedIndex: number;
14
+ onSelect?: (cmd: CommandDef) => void;
15
+ }
16
+
17
+ function PaletteItem({
18
+ cmd,
19
+ isSelected,
20
+ onSelect,
21
+ }: {
22
+ cmd: CommandDef;
23
+ isSelected: boolean;
24
+ onSelect?: (cmd: CommandDef) => void;
25
+ }) {
26
+ const ref = useRef<any>(null);
27
+ const handleClick = useCallback(() => onSelect?.(cmd), [cmd, onSelect]);
28
+ useOnMouseClick(ref, handleClick);
29
+
30
+ return (
31
+ <Box ref={ref}>
32
+ <Text bold={isSelected} color={isSelected ? theme.info.primary : undefined}>
33
+ {isSelected ? "▸ " : " "}
34
+ {cmd.label}
35
+ </Text>
36
+ <Text dimColor> {cmd.description}</Text>
37
+ </Box>
38
+ );
39
+ }
40
+
41
+ export default function CommandPalette({
42
+ commands,
43
+ query,
44
+ selectedIndex,
45
+ onSelect,
46
+ }: Props) {
47
+ const { setCursorPosition } = useCursor();
48
+ const paletteScrollRef = useRef<any>(null);
49
+ const containerRef = useRef<any>(null);
50
+ const filtered = commands.filter((c) =>
51
+ c.command.toLowerCase().startsWith("/" + query.toLowerCase()),
52
+ );
53
+
54
+ useEffect(() => {
55
+ setCursorPosition({ x: 2, y: filtered.length - selectedIndex });
56
+ }, [selectedIndex, filtered.length, setCursorPosition]);
57
+
58
+ useEffect(() => {
59
+ const unreg = scrollRegistry.register({
60
+ id: "command-palette",
61
+ zIndex: 1,
62
+ getRect: () => {
63
+ if (!containerRef.current) return null;
64
+ const node = (containerRef.current as any).yogaNode;
65
+ if (!node) return null;
66
+ return {
67
+ left: node.getComputedLeft?.() ?? 0,
68
+ top: node.getComputedTop?.() ?? 0,
69
+ width: node.getComputedWidth?.() ?? 0,
70
+ height: node.getComputedHeight?.() ?? 0,
71
+ };
72
+ },
73
+ onScroll: (delta) => {
74
+ const ref = paletteScrollRef.current;
75
+ if (!ref) return false;
76
+ const offset = ref.getScrollOffset();
77
+ const content = ref.getContentHeight();
78
+ const viewport = ref.getViewportHeight();
79
+ if (!content || content <= viewport) return false;
80
+ const steps = -delta * 3;
81
+ const clamped =
82
+ steps > 0
83
+ ? Math.min(steps, content - viewport - offset)
84
+ : Math.max(steps, -offset);
85
+ if (clamped !== 0) {
86
+ ref.scrollBy(clamped);
87
+ return false;
88
+ }
89
+ return true;
90
+ },
91
+ });
92
+ return unreg;
93
+ }, []);
94
+
95
+ if (filtered.length === 0) return null;
96
+
97
+ return (
98
+ <Box
99
+ ref={containerRef}
100
+ position="absolute"
101
+ bottom="100%"
102
+ left={0}
103
+ right={0}
104
+ paddingTop={1}
105
+ borderStyle={"bold"}
106
+ borderColor={theme.role.user}
107
+ borderTop={false}
108
+ borderLeft={true}
109
+ borderRight={true}
110
+ borderBottom={false}
111
+ flexDirection="row"
112
+ backgroundColor={theme.bg.surface}
113
+ maxHeight={10}
114
+ >
115
+ <Box flexGrow={1}>
116
+ <ScrollView ref={paletteScrollRef}>
117
+ {filtered.map((cmd, i) => (
118
+ <PaletteItem
119
+ key={cmd.command}
120
+ cmd={cmd}
121
+ isSelected={i === selectedIndex}
122
+ onSelect={onSelect}
123
+ />
124
+ ))}
125
+ </ScrollView>
126
+ </Box>
127
+ </Box>
128
+ );
129
+ }
@@ -0,0 +1,46 @@
1
+ import { PermissionMode } from "@mirai/permission";
2
+
3
+ export type Command =
4
+ | { type: "mode.set"; value: PermissionMode }
5
+ | { type: "system.clear" }
6
+ | { type: "help.show" };
7
+
8
+ export interface CommandDef {
9
+ command: string;
10
+ label: string;
11
+ description: string;
12
+ execute: () => Command;
13
+ }
14
+
15
+ export const BUILTIN_COMMANDS: CommandDef[] = [
16
+ {
17
+ command: "/mode readonly",
18
+ label: "/mode readonly",
19
+ description: "READONLY — deny all writes/exec",
20
+ execute: () => ({ type: "mode.set", value: PermissionMode.READONLY }),
21
+ },
22
+ {
23
+ command: "/mode workspace_write",
24
+ label: "/mode workspace_write",
25
+ description: "WORKSPACE_WRITE — allow writes, ask exec",
26
+ execute: () => ({ type: "mode.set", value: PermissionMode.WORKSPACE_WRITE }),
27
+ },
28
+ {
29
+ command: "/mode full_access",
30
+ label: "/mode full_access",
31
+ description: "FULL_ACCESS — allow everything",
32
+ execute: () => ({ type: "mode.set", value: PermissionMode.FULL_ACCESS }),
33
+ },
34
+ {
35
+ command: "/clear",
36
+ label: "/clear",
37
+ description: "Clear chat history",
38
+ execute: () => ({ type: "system.clear" }),
39
+ },
40
+ {
41
+ command: "/help",
42
+ label: "/help",
43
+ description: "Show available commands",
44
+ execute: () => ({ type: "help.show" }),
45
+ },
46
+ ];
@@ -0,0 +1,284 @@
1
+ import { Box, Text, useInput } from "ink";
2
+ import { useState, useRef, useEffect } from "react";
3
+ import { theme } from "../../theme.js";
4
+ import { NEON_COLORS } from "@mirai/core/constants";
5
+ import Spinner from "ink-spinner";
6
+ import type { Command } from "./commands.js";
7
+ import { BUILTIN_COMMANDS } from "./commands.js";
8
+ import CommandPalette from "./command-palette.js";
9
+ import { useDialog } from "../dialog/index.js";
10
+
11
+ // ─── Gradient: 3 màu (bỏ xanh lá #00ff87) ────────────────
12
+ const GRAD = NEON_COLORS.slice(0, 3);
13
+
14
+ function gradAt(t: number): string {
15
+ const stops = [
16
+ { color: GRAD[0], pos: 0 },
17
+ { color: GRAD[1], pos: 0.5 },
18
+ { color: GRAD[2], pos: 1 },
19
+ ];
20
+ for (let i = 0; i < stops.length - 1; i++) {
21
+ const a = stops[i],
22
+ b = stops[i + 1];
23
+ if (t >= a.pos && t <= b.pos) {
24
+ const p = (t - a.pos) / (b.pos - a.pos);
25
+ const [r1, g1, b1] = [
26
+ parseInt(a.color.slice(1, 3), 16),
27
+ parseInt(a.color.slice(3, 5), 16),
28
+ parseInt(a.color.slice(5, 7), 16),
29
+ ];
30
+ const [r2, g2, b2] = [
31
+ parseInt(b.color.slice(1, 3), 16),
32
+ parseInt(b.color.slice(3, 5), 16),
33
+ parseInt(b.color.slice(5, 7), 16),
34
+ ];
35
+ const lerp = (a: number, b: number, t: number) =>
36
+ Math.round(a + (b - a) * t);
37
+ return `#${lerp(r1, r2, p).toString(16).padStart(2, "0")}${lerp(g1, g2, p).toString(16).padStart(2, "0")}${lerp(b1, b2, p).toString(16).padStart(2, "0")}`;
38
+ }
39
+ }
40
+ return stops[stops.length - 1].color;
41
+ }
42
+
43
+ // ─── Matrix idle animation ─────────────────────────────────
44
+ const KATA = "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン゙゚";
45
+ const KATA_ARR = [...KATA];
46
+ const BLINK = "█▓░▒";
47
+ const BLINK_ARR = [...BLINK];
48
+
49
+ function randChar(): string {
50
+ return KATA_ARR[Math.floor(Math.random() * KATA_ARR.length)];
51
+ }
52
+
53
+ function flicker(s: string): string {
54
+ return [...s]
55
+ .map((ch) =>
56
+ Math.random() < 0.25
57
+ ? BLINK_ARR[Math.floor(Math.random() * BLINK_ARR.length)]
58
+ : ch,
59
+ )
60
+ .join("");
61
+ }
62
+
63
+ interface Props {
64
+ onSubmit: (value: string) => void;
65
+ onCommand: (cmd: Command) => void;
66
+ disabled: boolean;
67
+ onToggleMatrix: () => void;
68
+ }
69
+
70
+ export default function Input({
71
+ onSubmit,
72
+ onCommand,
73
+ disabled,
74
+ onToggleMatrix,
75
+ }: Props) {
76
+ const [isIdle, setIsIdle] = useState(false);
77
+ const [value, setValue] = useState("");
78
+ const [displayText, setDisplayText] = useState("");
79
+ const [selectedIndex, setSelectedIndex] = useState(0);
80
+ const [colorIdx, setColorIdx] = useState(0);
81
+ const lastActive = useRef(Date.now());
82
+ const dialog = useDialog();
83
+
84
+ // Derived palette state
85
+ const isSlashMode = value.startsWith("/");
86
+ const paletteQuery = isSlashMode ? value.slice(1) : "";
87
+ const filtered = isSlashMode
88
+ ? BUILTIN_COMMANDS.filter((c) =>
89
+ c.command.toLowerCase().startsWith("/" + paletteQuery.toLowerCase()),
90
+ )
91
+ : [];
92
+ const filteredRef = useRef(filtered);
93
+ filteredRef.current = filtered;
94
+ const showPalette = isSlashMode && filtered.length > 0 && !disabled;
95
+
96
+ // Reset selection when filter changes
97
+ useEffect(() => {
98
+ setSelectedIndex(0);
99
+ }, [paletteQuery]);
100
+
101
+ // Neon border + prompt cycle
102
+ useEffect(() => {
103
+ const t = setInterval(
104
+ () => setColorIdx((i) => (i + 1) % NEON_COLORS.length),
105
+ 500,
106
+ );
107
+ return () => clearInterval(t);
108
+ }, []);
109
+
110
+ // Idle detection (3s since last keypress, only when not disabled)
111
+ useEffect(() => {
112
+ if (disabled) {
113
+ setIsIdle(false);
114
+ return;
115
+ }
116
+ const check = () => {
117
+ if (Date.now() - lastActive.current >= 3000) setIsIdle(true);
118
+ };
119
+ const t = setInterval(check, 200);
120
+ return () => clearInterval(t);
121
+ }, [disabled]);
122
+
123
+ // ── Matrix animation chain (setTimeout-driven) ──
124
+ useEffect(() => {
125
+ if (!isIdle) {
126
+ setDisplayText("");
127
+ return;
128
+ }
129
+
130
+ let alive = true;
131
+ let len = 0;
132
+ let baseStr = "";
133
+ const maxLen = 10 + Math.floor(Math.random() * 11);
134
+ const ADD_MS = 120,
135
+ REMOVE_MS = 120,
136
+ PAUSE_MS = 500;
137
+
138
+ const flickerTimer = setInterval(() => {
139
+ if (!alive || !baseStr) return;
140
+ setDisplayText(flicker(baseStr));
141
+ }, 50);
142
+
143
+ function add() {
144
+ if (!alive) return;
145
+ len++;
146
+ baseStr = Array.from({ length: len }, () => randChar()).join("");
147
+ if (len < maxLen) setTimeout(add, ADD_MS);
148
+ else setTimeout(remove, PAUSE_MS);
149
+ }
150
+
151
+ function remove() {
152
+ if (!alive) return;
153
+ len--;
154
+ if (len > 0) {
155
+ baseStr = Array.from({ length: len }, () => randChar()).join("");
156
+ setTimeout(remove, REMOVE_MS);
157
+ } else {
158
+ baseStr = "";
159
+ setDisplayText("");
160
+ setTimeout(add, 3000);
161
+ }
162
+ }
163
+
164
+ setTimeout(add, 300);
165
+ return () => {
166
+ alive = false;
167
+ clearInterval(flickerTimer);
168
+ };
169
+ }, [isIdle]);
170
+
171
+ // ── Input handler ──
172
+ useInput((input, key) => {
173
+ if (dialog.isActive) return;
174
+ if (input.includes("[<")) return;
175
+ lastActive.current = Date.now();
176
+
177
+ // ── Palette navigation (when visible) ──
178
+ if (showPalette) {
179
+ if (key.upArrow) {
180
+ setSelectedIndex((i) =>
181
+ i <= 0 ? filteredRef.current.length - 1 : i - 1,
182
+ );
183
+ return;
184
+ }
185
+ if (key.downArrow) {
186
+ setSelectedIndex((i) =>
187
+ i >= filteredRef.current.length - 1 ? 0 : i + 1,
188
+ );
189
+ return;
190
+ }
191
+ if (key.return) {
192
+ const cmd = filtered[selectedIndex];
193
+ if (cmd) {
194
+ onCommand(cmd.execute());
195
+ setValue("");
196
+ setSelectedIndex(0);
197
+ return;
198
+ }
199
+ }
200
+ if (key.escape) {
201
+ setValue("");
202
+ setSelectedIndex(0);
203
+ return;
204
+ }
205
+ }
206
+
207
+ // ── Submit ──
208
+ if (key.return) {
209
+ onSubmit(value);
210
+ setValue("");
211
+ return;
212
+ }
213
+
214
+ // ── Idle wake ──
215
+ if (isIdle && !disabled) {
216
+ if (input.length > 0 && input.charCodeAt(0) >= 32) {
217
+ setValue((v) => v + input);
218
+ } else if (key.backspace) {
219
+ setValue((v) => v.slice(0, -1));
220
+ }
221
+ setIsIdle(false);
222
+ return;
223
+ }
224
+
225
+ // ── Toggle matrix ──
226
+ if (key.meta && input === "m") {
227
+ onToggleMatrix();
228
+ return;
229
+ }
230
+
231
+ // ── Regular input ──
232
+ if (key.backspace) {
233
+ setValue((v) => v.slice(0, -1));
234
+ return;
235
+ }
236
+ setValue((v) => v + input);
237
+ });
238
+
239
+ return (
240
+ <Box position="relative" maxHeight={10}>
241
+ {showPalette && (
242
+ <CommandPalette
243
+ commands={BUILTIN_COMMANDS}
244
+ query={paletteQuery}
245
+ selectedIndex={selectedIndex}
246
+ onSelect={(cmd) => {
247
+ onCommand(cmd.execute());
248
+ setValue("");
249
+ setSelectedIndex(0);
250
+ }}
251
+ />
252
+ )}
253
+ <Box
254
+ paddingY={1}
255
+ paddingX={1}
256
+ backgroundColor={theme.bg.surface}
257
+ borderStyle={"bold"}
258
+ borderTop={false}
259
+ borderBottom={false}
260
+ borderColor={theme.role.user}
261
+ width={"100%"}
262
+ >
263
+ <Box>
264
+ <Text bold color={theme.role.user}>
265
+ {disabled ? <Spinner type="dots" /> : "❯"}
266
+ </Text>
267
+ <Box marginLeft={1} flexGrow={1} minWidth={0}>
268
+ {isIdle && displayText ? (
269
+ <Box>
270
+ {[...displayText].map((ch, i) => (
271
+ <Text key={i} color={theme.text.primary}>
272
+ {ch}
273
+ </Text>
274
+ ))}
275
+ </Box>
276
+ ) : (
277
+ <Text>{value ? value : "type a message..."}</Text>
278
+ )}
279
+ </Box>
280
+ </Box>
281
+ </Box>
282
+ </Box>
283
+ );
284
+ }