@suchitraswain/nightcode-cli 1.0.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 (51) hide show
  1. package/bin/nightcode.cjs +10 -0
  2. package/bin/nightcode.ts +5 -0
  3. package/package.json +50 -0
  4. package/src/bootstrap-env.ts +33 -0
  5. package/src/components/border.tsx +18 -0
  6. package/src/components/command-menu/commands.tsx +147 -0
  7. package/src/components/command-menu/filter-commands.ts +8 -0
  8. package/src/components/command-menu/index.tsx +74 -0
  9. package/src/components/command-menu/types.ts +20 -0
  10. package/src/components/command-menu/use-command-menu.ts +113 -0
  11. package/src/components/dialog-search-list.tsx +127 -0
  12. package/src/components/dialogs/agents-dialog.tsx +47 -0
  13. package/src/components/dialogs/index.tsx +4 -0
  14. package/src/components/dialogs/models-dialog.tsx +41 -0
  15. package/src/components/dialogs/sessions-dialog.tsx +94 -0
  16. package/src/components/dialogs/theme-dialog.tsx +58 -0
  17. package/src/components/header.tsx +10 -0
  18. package/src/components/input-bar.tsx +611 -0
  19. package/src/components/messages/bot-message.tsx +160 -0
  20. package/src/components/messages/error-message.tsx +36 -0
  21. package/src/components/messages/index.tsx +3 -0
  22. package/src/components/messages/user-message.tsx +36 -0
  23. package/src/components/session-shell.tsx +65 -0
  24. package/src/components/spinner.tsx +14 -0
  25. package/src/components/status-bar.tsx +23 -0
  26. package/src/hooks/use-chat.ts +107 -0
  27. package/src/hosted-config.ts +6 -0
  28. package/src/index.tsx +29 -0
  29. package/src/layouts/root-layout.tsx +25 -0
  30. package/src/layouts/themed-root.tsx +21 -0
  31. package/src/lib/api-client.ts +25 -0
  32. package/src/lib/auth.ts +38 -0
  33. package/src/lib/http-errors.ts +18 -0
  34. package/src/lib/local-tools.ts +170 -0
  35. package/src/lib/oauth.ts +166 -0
  36. package/src/lib/upgrade.ts +27 -0
  37. package/src/providers/dialog/index.tsx +123 -0
  38. package/src/providers/dialog/types.ts +6 -0
  39. package/src/providers/keyboard-layer/index.tsx +98 -0
  40. package/src/providers/prompt-config/index.tsx +52 -0
  41. package/src/providers/theme/index.tsx +75 -0
  42. package/src/providers/toast/index.tsx +118 -0
  43. package/src/providers/toast/types.ts +9 -0
  44. package/src/screens/home.tsx +39 -0
  45. package/src/screens/new-session.tsx +82 -0
  46. package/src/screens/session.tsx +171 -0
  47. package/src/theme.ts +568 -0
  48. package/vendor/shared/api-types.ts +11 -0
  49. package/vendor/shared/index.ts +20 -0
  50. package/vendor/shared/models.ts +72 -0
  51. package/vendor/shared/schemas.ts +87 -0
@@ -0,0 +1,611 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { isAbsolute, relative, resolve } from "node:path";
3
+
4
+ import { useRef, useState, useCallback, useEffect, type RefObject } from "react";
5
+
6
+ import { TextAttributes } from "@opentui/core";
7
+
8
+ import type { TextareaRenderable, ScrollBoxRenderable } from "@opentui/core";
9
+ import { useKeyboard, useRenderer } from "@opentui/react";
10
+ import type { KeyBinding } from "@opentui/core";
11
+ import { useNavigate } from "react-router";
12
+ import { EmptyBorder } from "./border";
13
+ import { StatusBar } from "./status-bar";
14
+ import { CommandMenu } from "./command-menu";
15
+ import type { Command } from "./command-menu/types";
16
+ import { useCommandMenu } from "./command-menu/use-command-menu";
17
+ import { useToast } from "../providers/toast";
18
+ import { useKeyboardLayer } from "../providers/keyboard-layer";
19
+ import { useDialog } from "../providers/dialog";
20
+ import { useTheme } from "../providers/theme";
21
+ import { usePromptConfig } from "../providers/prompt-config";
22
+ import { Mode } from "@nightcode/shared";
23
+
24
+ const MAX_VISIBLE_MENTIONS = 8;
25
+ const CURRENT_DIRECTORY = process.cwd();
26
+ const MAX_FALLBACK_MENTION_CANDIDATES = 32;
27
+ const MENTION_QUERY_CHARACTER = /[A-Za-z0-9._/-]/;
28
+ const RECURSIVE_MENTION_IGNORED_DIRECTORIES = new Set(["node_modules"]);
29
+
30
+ type MentionMatch = {
31
+ start: number;
32
+ end: number;
33
+ query: string;
34
+ };
35
+
36
+ type MentionCandidate = {
37
+ path: string;
38
+ kind: "file" | "directory";
39
+ };
40
+
41
+ function isWithinCurrentDirectory(targetPath: string) {
42
+ const relativePath = relative(CURRENT_DIRECTORY, targetPath);
43
+ return relativePath === ""
44
+ || (!relativePath.startsWith("..")
45
+ && !isAbsolute(relativePath));
46
+ }
47
+
48
+ function isMentionQueryCharacter(character: string) {
49
+ return MENTION_QUERY_CHARACTER.test(character);
50
+ }
51
+
52
+ function findActiveMention(text: string, cursorOffset: number): MentionMatch | null {
53
+ const safeOffset = Math.max(0, Math.min(cursorOffset, text.length));
54
+
55
+ let start = safeOffset;
56
+ while (start > 0 && !/\s/.test(text[start - 1]!)) {
57
+ start -= 1;
58
+ }
59
+
60
+ let end = safeOffset;
61
+ while (end < text.length && !/\s/.test(text[end]!)) {
62
+ end += 1;
63
+ }
64
+
65
+ const token = text.slice(start, end);
66
+ const relativeCursor = safeOffset - start;
67
+ const mentionStart = token.lastIndexOf("@", relativeCursor);
68
+
69
+ if (mentionStart === -1) {
70
+ return null;
71
+ }
72
+
73
+ const previousCharacter = token[mentionStart - 1];
74
+ if (previousCharacter && isMentionQueryCharacter(previousCharacter)) {
75
+ return null;
76
+ }
77
+
78
+ let mentionEnd = mentionStart + 1;
79
+ while (mentionEnd < token.length && isMentionQueryCharacter(token[mentionEnd]!)) {
80
+ mentionEnd += 1;
81
+ }
82
+
83
+ if (relativeCursor < mentionStart || relativeCursor > mentionEnd) {
84
+ return null;
85
+ }
86
+
87
+ return {
88
+ start: start + mentionStart,
89
+ end: start + mentionEnd,
90
+ query: token.slice(mentionStart + 1, mentionEnd),
91
+ };
92
+ }
93
+
94
+ async function getMentionCandidates(query: string): Promise<MentionCandidate[]> {
95
+ const normalizedQuery = query.startsWith("./") ? query.slice(2) : query;
96
+ if (normalizedQuery.startsWith("/")) {
97
+ return [];
98
+ }
99
+
100
+ const hasTrailingSlash = normalizedQuery.endsWith("/");
101
+ const lastSlashIndex = hasTrailingSlash
102
+ ? normalizedQuery.length - 1
103
+ : normalizedQuery.lastIndexOf("/");
104
+
105
+ const directoryPart = hasTrailingSlash
106
+ ? normalizedQuery.slice(0, -1)
107
+ : lastSlashIndex === -1
108
+ ? ""
109
+ : normalizedQuery.slice(0, lastSlashIndex);
110
+
111
+ const namePrefix = hasTrailingSlash
112
+ ? ""
113
+ : lastSlashIndex === -1
114
+ ? normalizedQuery
115
+ : normalizedQuery.slice(lastSlashIndex + 1);
116
+
117
+ const absoluteDirectory = resolve(CURRENT_DIRECTORY, directoryPart || ".");
118
+ if (!isWithinCurrentDirectory(absoluteDirectory)) {
119
+ return [];
120
+ }
121
+
122
+ try {
123
+ const entries = await readdir(absoluteDirectory, { withFileTypes: true });
124
+ const lowercasePrefix = namePrefix.toLowerCase();
125
+ const showHiddenEntries = namePrefix.startsWith(".");
126
+
127
+ const directMatches = entries
128
+ .filter((entry) => showHiddenEntries || !entry.name.startsWith("."))
129
+ .filter((entry) => {
130
+ return lowercasePrefix === "" || entry.name.toLowerCase().startsWith(lowercasePrefix);
131
+ })
132
+ .sort((left, right) => {
133
+ if (left.isDirectory() !== right.isDirectory()) {
134
+ return left.isDirectory() ? -1 : 1;
135
+ }
136
+ return left.name.localeCompare(right.name);
137
+ })
138
+ .map((entry) => {
139
+ const path = directoryPart ? `${directoryPart}/${entry.name}` : entry.name;
140
+ const kind: MentionCandidate["kind"] = entry.isDirectory() ? "directory" : "file";
141
+ return {
142
+ path: kind === "directory" ? `${path}/` : path,
143
+ kind,
144
+ };
145
+ });
146
+
147
+ if (directMatches.length > 0 || directoryPart !== "" || namePrefix === "") {
148
+ return directMatches;
149
+ }
150
+
151
+ const fallbackMatches: MentionCandidate[] = [];
152
+ const visit = async (
153
+ absoluteDirectory: string,
154
+ directoryPart: string
155
+ ): Promise<void> => {
156
+ const entries = await readdir(absoluteDirectory, { withFileTypes: true });
157
+
158
+ for (const entry of entries) {
159
+ if (!showHiddenEntries && entry.name.startsWith(".")) {
160
+ continue;
161
+ }
162
+
163
+ if (
164
+ entry.isDirectory()
165
+ && RECURSIVE_MENTION_IGNORED_DIRECTORIES.has(entry.name)
166
+ ) {
167
+ continue;
168
+ }
169
+
170
+ const path = directoryPart ? `${directoryPart}/${entry.name}` : entry.name;
171
+ const kind: MentionCandidate["kind"] =
172
+ entry.isDirectory() ? "directory" : "file";
173
+
174
+ if (entry.name.toLowerCase().startsWith(lowercasePrefix)) {
175
+ fallbackMatches.push({
176
+ path: kind === "directory" ? `${path}/` : path,
177
+ kind,
178
+ });
179
+ if (fallbackMatches.length >= MAX_FALLBACK_MENTION_CANDIDATES) {
180
+ return;
181
+ }
182
+ }
183
+
184
+ if (entry.isDirectory()) {
185
+ await visit(resolve(absoluteDirectory, entry.name), path);
186
+ if (fallbackMatches.length >= MAX_FALLBACK_MENTION_CANDIDATES) {
187
+ return;
188
+ }
189
+ }
190
+ }
191
+ };
192
+
193
+ await visit(CURRENT_DIRECTORY, "");
194
+ return fallbackMatches.sort((left, right) => left.path.localeCompare(right.path));
195
+ } catch {
196
+ return [];
197
+ }
198
+ }
199
+
200
+ type FileMentionMenuProps = {
201
+ candidates: MentionCandidate[];
202
+ selectedIndex: number;
203
+ scrollRef: RefObject<ScrollBoxRenderable | null>;
204
+ onSelect: (index: number) => void;
205
+ onExecute: (index: number) => void;
206
+ };
207
+
208
+ function FileMentionMenu({
209
+ candidates,
210
+ selectedIndex,
211
+ scrollRef,
212
+ onSelect,
213
+ onExecute,
214
+ }: FileMentionMenuProps) {
215
+ const { colors } = useTheme();
216
+ const visibleHeight = Math.min(candidates.length, MAX_VISIBLE_MENTIONS);
217
+
218
+ if (candidates.length === 0) {
219
+ return (
220
+ <box paddingX={1}>
221
+ <text attributes={TextAttributes.DIM}>No matching files or folders</text>
222
+ </box>
223
+ );
224
+ }
225
+
226
+ return (
227
+ <scrollbox ref={scrollRef} height={visibleHeight}>
228
+ {candidates.map((candidate, index) => {
229
+ const isSelected = index === selectedIndex;
230
+
231
+ return (
232
+ <box
233
+ key={candidate.path}
234
+ flexDirection="row"
235
+ paddingX={1}
236
+ height={1}
237
+ overflow="hidden"
238
+ backgroundColor={isSelected ? colors.selection : undefined}
239
+ onMouseMove={() => onSelect(index)}
240
+ onMouseDown={() => onExecute(index)}
241
+ >
242
+ <box flexGrow={1} flexShrink={1} overflow="hidden">
243
+ <text selectable={false} fg={isSelected ? "black" : "white"}>
244
+ {candidate.path}
245
+ </text>
246
+ </box>
247
+
248
+ <box width={8} alignItems="flex-end" flexShrink={0}>
249
+ <text selectable={false} fg={isSelected ? "black" : "gray"}>
250
+ {candidate.kind === "directory" ? "Folder" : "File"}
251
+ </text>
252
+ </box>
253
+ </box>
254
+ );
255
+ })}
256
+ </scrollbox>
257
+ );
258
+ };
259
+
260
+ type Props = {
261
+ onSubmit: (text: string) => void;
262
+ disabled?: boolean;
263
+ };
264
+
265
+ export const TEXTAREA_KEY_BINDINGS: KeyBinding[] = [
266
+ { name: "return", action: "submit" },
267
+ { name: "enter", action: "submit" },
268
+ { name: "return", shift: true, action: "newline" },
269
+ { name: "enter", shift: true, action: "newline" },
270
+ ];
271
+
272
+ export function InputBar({ onSubmit, disabled = false }: Props) {
273
+ const { mode, toggleMode, setMode, setModel } = usePromptConfig();
274
+ const textareaRef = useRef<TextareaRenderable>(null);
275
+ const onSubmitRef = useRef<() => void>(() => {});
276
+ const activeMentionRef = useRef<MentionMatch | null>(null);
277
+ const mentionScrollRef = useRef<ScrollBoxRenderable>(null);
278
+
279
+ const renderer = useRenderer();
280
+ const navigate = useNavigate();
281
+ const toast = useToast();
282
+ const dialog = useDialog();
283
+ const { colors } = useTheme();
284
+ const { isTopLayer, push, pop, setResponder } = useKeyboardLayer();
285
+
286
+ const [activeMention, setActiveMention] = useState<MentionMatch | null>(null);
287
+ const [mentionCandidates, setMentionCandidates] = useState<MentionCandidate[]>([]);
288
+ const [mentionSelectedIndex, setMentionSelectedIndex] = useState(0);
289
+
290
+ const {
291
+ showCommandMenu,
292
+ commandQuery,
293
+ selectedIndex,
294
+ scrollRef,
295
+ handleContentChange,
296
+ resolveCommand,
297
+ setSelectedIndex,
298
+ } = useCommandMenu();
299
+
300
+ const showMentionMenu = activeMention !== null;
301
+
302
+ const closeMentionMenu = useCallback(() => {
303
+ activeMentionRef.current = null;
304
+ setActiveMention(null);
305
+ setMentionCandidates([]);
306
+ pop("mention");
307
+ }, [pop]);
308
+
309
+ const syncMentionMenu = useCallback((text: string, cursorOffset: number) => {
310
+ const nextMention = findActiveMention(text, cursorOffset);
311
+ const previousMention = activeMentionRef.current;
312
+ const mentionChanged =
313
+ previousMention?.start !== nextMention?.start ||
314
+ previousMention?.end !== nextMention?.end ||
315
+ previousMention?.query !== nextMention?.query;
316
+
317
+ if (!nextMention) {
318
+ if (previousMention) {
319
+ closeMentionMenu();
320
+ }
321
+ return;
322
+ }
323
+
324
+ activeMentionRef.current = nextMention;
325
+ setActiveMention(nextMention);
326
+ push("mention", () => {
327
+ closeMentionMenu();
328
+ return true;
329
+ });
330
+
331
+ if (mentionChanged) {
332
+ setMentionSelectedIndex(0);
333
+ mentionScrollRef.current?.scrollTo(0);
334
+ }
335
+ }, [closeMentionMenu, push]);
336
+
337
+ const handleTextareaContentChange = useCallback(() => {
338
+ const textarea = textareaRef.current;
339
+ if (!textarea) return;
340
+
341
+ const text = textarea.plainText;
342
+
343
+ handleContentChange(textarea.plainText);
344
+ syncMentionMenu(text, textarea.cursorOffset);
345
+ }, [handleContentChange, syncMentionMenu]);
346
+
347
+ const handleSubmit = useCallback(() => {
348
+ if (disabled) return;
349
+
350
+ const textarea = textareaRef.current;
351
+ if (!textarea) return;
352
+
353
+ const text = textarea.plainText.trim();
354
+ if (text.length === 0) return;
355
+
356
+ onSubmit(text);
357
+ textarea.setText("");
358
+ }, [disabled, onSubmit])
359
+
360
+ const handleMentionExecute = useCallback((index: number) => {
361
+ const textarea = textareaRef.current;
362
+ const mention = activeMentionRef.current;
363
+ const candidate = mentionCandidates[index];
364
+
365
+ if (!textarea || !mention || !candidate) return;
366
+
367
+ const insertion = candidate.kind === "directory"
368
+ ? candidate.path
369
+ : `${candidate.path} `;
370
+
371
+ const nextText = `${textarea.plainText.slice(0, mention.start)}@${insertion}${textarea.plainText.slice(mention.end)}`;
372
+
373
+ textarea.replaceText(nextText);
374
+ textarea.cursorOffset = mention.start + insertion.length + 1;
375
+ syncMentionMenu(nextText, textarea.cursorOffset);
376
+ }, [mentionCandidates, syncMentionMenu]);
377
+
378
+ const handleTextareaCursorChange = useCallback(() => {
379
+ const textarea = textareaRef.current;
380
+ if (!textarea) return;
381
+
382
+ syncMentionMenu(textarea.plainText, textarea.cursorOffset);
383
+ }, [syncMentionMenu]);
384
+
385
+ const handleCommand = useCallback((
386
+ command: Command | undefined
387
+ ) => {
388
+ const textarea = textareaRef.current;
389
+ if (!textarea || !command) return;
390
+
391
+ textarea.setText("");
392
+
393
+ if (command.action) {
394
+ command.action({
395
+ exit: () => renderer.destroy(),
396
+ toast,
397
+ dialog,
398
+ navigate,
399
+ mode,
400
+ setMode,
401
+ setModel,
402
+ });
403
+ } else {
404
+ textarea.insertText(command.value + " ");
405
+ }
406
+ }, [renderer, toast, dialog, navigate, mode, setMode, setModel]);
407
+
408
+ const handleCommandExecute = useCallback(
409
+ (index: number) => {
410
+ const command = resolveCommand(index);
411
+ handleCommand(command);
412
+ },
413
+ [resolveCommand, handleCommand],
414
+ );
415
+
416
+ // Keep the file picker in sync with the current @mention token.
417
+ useEffect(() => {
418
+ if (!activeMention) {
419
+ setMentionCandidates([]);
420
+ return;
421
+ }
422
+
423
+ let ignore = false;
424
+ const loadCandidates = async () => {
425
+ const nextCandidates = await getMentionCandidates(activeMention.query);
426
+ if (ignore) return;
427
+
428
+ setMentionCandidates(nextCandidates);
429
+ setMentionSelectedIndex((currentIndex) => {
430
+ if (nextCandidates.length === 0) {
431
+ return 0;
432
+ }
433
+ return Math.min(currentIndex, nextCandidates.length - 1);
434
+ });
435
+ };
436
+
437
+ void loadCandidates();
438
+
439
+ return () => {
440
+ ignore = true;
441
+ };
442
+ }, [activeMention]);
443
+
444
+ // Wire up textarea submit handler once so it always reads the latest state.
445
+ useEffect(() => {
446
+ const textarea = textareaRef.current;
447
+ if (!textarea) return;
448
+
449
+ textarea.onSubmit = () => {
450
+ onSubmitRef.current();
451
+ };
452
+ }, []);
453
+
454
+ onSubmitRef.current = () => {
455
+ if (disabled) return;
456
+
457
+ if (showCommandMenu) {
458
+ const command = resolveCommand(selectedIndex);
459
+ handleCommand(command);
460
+ return;
461
+ }
462
+
463
+ if (showMentionMenu) {
464
+ const candidate = mentionCandidates[mentionSelectedIndex];
465
+ if (candidate) {
466
+ handleMentionExecute(mentionSelectedIndex);
467
+ return;
468
+ }
469
+ }
470
+
471
+ handleSubmit();
472
+ };
473
+
474
+ useKeyboard((key) => {
475
+ if (disabled) return;
476
+ if (!isTopLayer("base")) return;
477
+ if (key.name === "tab") {
478
+ key.preventDefault();
479
+ toggleMode();
480
+ }
481
+ });
482
+
483
+ // Register the base layer responder for ctrl+c dismissal
484
+ useEffect(() => {
485
+ setResponder("base", () => {
486
+ if (disabled) return false;
487
+
488
+ const textarea = textareaRef.current;
489
+ if (textarea && textarea.plainText.length > 0) {
490
+ textarea.setText("");
491
+ return true;
492
+ }
493
+ return false;
494
+ });
495
+
496
+ return () => setResponder("base", null);
497
+ }, [disabled, setResponder]);
498
+
499
+ useKeyboard((key) => {
500
+ if (disabled) return;
501
+ if (!showMentionMenu || !isTopLayer("mention")) return;
502
+
503
+ if (key.name === "escape") {
504
+ key.preventDefault();
505
+ closeMentionMenu();
506
+ } else if (key.name === "up") {
507
+ key.preventDefault();
508
+ setMentionSelectedIndex((currentIndex) => {
509
+ const nextIndex = Math.max(0, currentIndex - 1);
510
+ const scrollbox = mentionScrollRef.current;
511
+ if (scrollbox && nextIndex < scrollbox.scrollTop) {
512
+ scrollbox.scrollTo(nextIndex);
513
+ }
514
+ return nextIndex;
515
+ });
516
+ } else if (key.name === "down") {
517
+ key.preventDefault();
518
+ setMentionSelectedIndex((currentIndex) => {
519
+ if (mentionCandidates.length === 0) {
520
+ return 0;
521
+ }
522
+
523
+ const nextIndex = Math.min(mentionCandidates.length - 1, currentIndex + 1);
524
+ const scrollbox = mentionScrollRef.current;
525
+
526
+ if (scrollbox) {
527
+ const viewportHeight = scrollbox.viewport.height;
528
+ const visibleEnd = scrollbox.scrollTop + viewportHeight - 1;
529
+ if (nextIndex > visibleEnd) {
530
+ scrollbox.scrollTo(nextIndex - viewportHeight + 1);
531
+ }
532
+ }
533
+
534
+ return nextIndex;
535
+ });
536
+ }
537
+ });
538
+
539
+ return (
540
+ <box width="100%" alignItems="center">
541
+ <box
542
+ border={["left"]}
543
+ borderColor={mode === Mode.BUILD ? colors.primary : colors.planMode}
544
+ customBorderChars={{
545
+ ...EmptyBorder,
546
+ vertical: "┃",
547
+ bottomLeft: "╹",
548
+ }}
549
+ width="100%"
550
+ >
551
+ <box
552
+ position="relative"
553
+ justifyContent="center"
554
+ paddingX={2}
555
+ paddingY={1}
556
+ backgroundColor={colors.surface}
557
+ width="100%"
558
+ gap={1}
559
+ >
560
+ {showCommandMenu && (
561
+ <box
562
+ position="absolute"
563
+ bottom="100%"
564
+ left={0}
565
+ width="100%"
566
+ backgroundColor={colors.surface}
567
+ zIndex={10}
568
+ >
569
+ <CommandMenu
570
+ query={commandQuery}
571
+ selectedIndex={selectedIndex}
572
+ scrollRef={scrollRef}
573
+ onSelect={setSelectedIndex}
574
+ onExecute={handleCommandExecute}
575
+ />
576
+ </box>
577
+ )}
578
+ {!showCommandMenu && showMentionMenu && (
579
+ <box
580
+ position="absolute"
581
+ bottom="100%"
582
+ left={0}
583
+ width="100%"
584
+ backgroundColor={colors.surface}
585
+ zIndex={10}
586
+ >
587
+ <FileMentionMenu
588
+ candidates={mentionCandidates}
589
+ selectedIndex={mentionSelectedIndex}
590
+ scrollRef={mentionScrollRef}
591
+ onSelect={setMentionSelectedIndex}
592
+ onExecute={handleMentionExecute}
593
+ />
594
+ </box>
595
+ )}
596
+ <textarea
597
+ ref={textareaRef}
598
+ focused={
599
+ !disabled &&
600
+ (isTopLayer("base") || isTopLayer("command") || isTopLayer("mention"))
601
+ }
602
+ keyBindings={TEXTAREA_KEY_BINDINGS}
603
+ onContentChange={handleTextareaContentChange}
604
+ placeholder={`Ask anything... "Fix a bug in the database"`}
605
+ />
606
+ <StatusBar />
607
+ </box>
608
+ </box>
609
+ </box>
610
+ );
611
+ };