claude-ws 0.1.1 → 0.1.3

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.
@@ -1,7 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Folder, FolderOpen, File } from 'lucide-react';
4
- import { cn } from '@/lib/utils';
3
+ import { FileExtensionIcon } from '@/components/ui/file-extension-icon';
5
4
 
6
5
  interface FileIconProps {
7
6
  name: string;
@@ -11,101 +10,16 @@ interface FileIconProps {
11
10
  size?: 'sm' | 'md';
12
11
  }
13
12
 
14
- // Icon config: [label, color]
15
- const EXT_ICONS: Record<string, [string, string]> = {
16
- js: ['JS', 'text-yellow-500'],
17
- jsx: ['JS', 'text-yellow-400'],
18
- mjs: ['JS', 'text-yellow-500'],
19
- cjs: ['JS', 'text-yellow-500'],
20
- ts: ['TS', 'text-blue-500'],
21
- tsx: ['TS', 'text-blue-400'],
22
- json: ['{ }', 'text-amber-500'],
23
- yaml: ['Y', 'text-rose-400'],
24
- yml: ['Y', 'text-rose-400'],
25
- css: ['#', 'text-purple-500'],
26
- scss: ['S#', 'text-pink-500'],
27
- html: ['<>', 'text-orange-500'],
28
- xml: ['<>', 'text-orange-400'],
29
- svg: ['◇', 'text-amber-400'],
30
- md: ['MD', 'text-sky-500'],
31
- mdx: ['MD', 'text-sky-400'],
32
- py: ['PY', 'text-green-500'],
33
- go: ['GO', 'text-cyan-500'],
34
- rs: ['RS', 'text-orange-600'],
35
- sh: ['$', 'text-slate-400'],
36
- sql: ['Q', 'text-cyan-500'],
37
- png: ['◫', 'text-emerald-500'],
38
- jpg: ['◫', 'text-emerald-500'],
39
- jpeg: ['◫', 'text-emerald-500'],
40
- gif: ['◫', 'text-emerald-500'],
41
- webp: ['◫', 'text-emerald-500'],
42
- ico: ['◫', 'text-emerald-500'],
43
- txt: ['T', 'text-muted-foreground'],
44
- log: ['≡', 'text-muted-foreground'],
45
- env: ['⚙', 'text-yellow-600'],
46
- lock: ['🔒', 'text-slate-500'],
47
- db: ['DB', 'text-cyan-600'],
48
- sqlite: ['DB', 'text-cyan-600'],
49
- sqlite3: ['DB', 'text-cyan-600'],
50
- };
51
-
52
- // Special files
53
- const FILE_ICONS: Record<string, [string, string]> = {
54
- 'package.json': ['{ }', 'text-green-500'],
55
- 'tsconfig.json': ['TS', 'text-blue-500'],
56
- 'next.config.ts': ['N', 'text-slate-400'],
57
- 'next.config.js': ['N', 'text-slate-400'],
58
- 'next.config.mjs': ['N', 'text-slate-400'],
59
- 'tailwind.config.ts': ['TW', 'text-cyan-400'],
60
- 'tailwind.config.js': ['TW', 'text-cyan-400'],
61
- '.gitignore': ['G', 'text-orange-500'],
62
- '.npmignore': ['N', 'text-red-500'],
63
- '.env': ['⚙', 'text-yellow-600'],
64
- '.env.local': ['⚙', 'text-yellow-600'],
65
- 'README.md': ['i', 'text-sky-500'],
66
- 'LICENSE': ['§', 'text-amber-500'],
67
- 'LICENSE.md': ['§', 'text-amber-500'],
68
- 'LICENSE.txt': ['§', 'text-amber-500'],
69
- 'Dockerfile': ['🐳', 'text-sky-500'],
70
- };
71
-
72
- function getExt(name: string): string {
73
- const i = name.lastIndexOf('.');
74
- return i > 0 ? name.slice(i + 1).toLowerCase() : '';
75
- }
76
-
77
13
  export function FileIcon({ name, type, isExpanded, className, size = 'md' }: FileIconProps) {
78
- const isSmall = size === 'sm';
79
- const iconSize = isSmall ? 'size-3' : 'size-4';
80
- const textSize = isSmall ? 'text-[7px] w-3' : 'text-[10px] w-4';
81
-
82
- // Folders
83
- if (type === 'directory') {
84
- const Icon = isExpanded ? FolderOpen : Folder;
85
- return <Icon className={cn(iconSize, 'text-amber-500', className)} />;
86
- }
87
-
88
- // Special files first
89
- const special = FILE_ICONS[name];
90
- if (special) {
91
- return (
92
- <span className={cn(textSize, 'font-bold text-center', special[1], className)}>
93
- {special[0]}
94
- </span>
95
- );
96
- }
97
-
98
- // By extension
99
- const ext = getExt(name);
100
- const config = EXT_ICONS[ext];
101
- if (config) {
102
- return (
103
- <span className={cn(textSize, 'font-bold text-center', config[1], className)}>
104
- {config[0]}
105
- </span>
106
- );
107
- }
108
-
109
- // Default
110
- return <File className={cn(iconSize, 'text-muted-foreground', className)} />;
14
+ const iconSize = size === 'sm' ? 12 : 16;
15
+
16
+ return (
17
+ <FileExtensionIcon
18
+ name={name}
19
+ type={type}
20
+ isExpanded={isExpanded}
21
+ size={iconSize}
22
+ className={className}
23
+ />
24
+ );
111
25
  }
@@ -5,7 +5,7 @@ import { Loader2, FileText } from 'lucide-react';
5
5
  import { ScrollArea } from '@/components/ui/scroll-area';
6
6
  import { MessageBlock } from '@/components/claude/message-block';
7
7
  import { ToolUseBlock } from '@/components/claude/tool-use-block';
8
- import { RunningDots } from '@/components/ui/running-dots';
8
+ import { RunningDots, useRandomStatusVerb } from '@/components/ui/running-dots';
9
9
  import { cn } from '@/lib/utils';
10
10
  import type { ClaudeOutput, ClaudeContentBlock, AttemptFile, PendingFile } from '@/types';
11
11
 
@@ -125,9 +125,19 @@ export function ConversationView({
125
125
  const [historicalTurns, setHistoricalTurns] = useState<ConversationTurn[]>([]);
126
126
  const [isLoading, setIsLoading] = useState(true);
127
127
  const [lastIsRunning, setLastIsRunning] = useState(isRunning);
128
+ const statusVerb = useRandomStatusVerb();
129
+ // Track if user is manually scrolling (to pause auto-scroll)
130
+ const userScrollingRef = useRef(false);
131
+ const userScrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
128
132
 
129
133
  // Check if user is near bottom of scroll area (within threshold)
130
134
  const isNearBottom = () => {
135
+ const detachedContainer = scrollAreaRef.current?.closest('[data-detached-scroll-container]');
136
+ if (detachedContainer) {
137
+ const threshold = 150;
138
+ return detachedContainer.scrollHeight - detachedContainer.scrollTop - detachedContainer.clientHeight < threshold;
139
+ }
140
+
131
141
  const viewport = scrollAreaRef.current?.querySelector('[data-slot="scroll-area-viewport"]');
132
142
  if (!viewport) return true;
133
143
  const threshold = 150; // pixels from bottom
@@ -137,21 +147,66 @@ export function ConversationView({
137
147
  // Scroll to bottom of scroll area viewport (only if user is near bottom)
138
148
  const scrollToBottomIfNear = () => {
139
149
  if (isNearBottom()) {
140
- const viewport = scrollAreaRef.current?.querySelector('[data-slot="scroll-area-viewport"]');
141
- if (viewport) {
142
- viewport.scrollTop = viewport.scrollHeight;
150
+ const detachedContainer = scrollAreaRef.current?.closest('[data-detached-scroll-container]');
151
+ if (detachedContainer) {
152
+ detachedContainer.scrollTop = detachedContainer.scrollHeight;
153
+ } else {
154
+ const viewport = scrollAreaRef.current?.querySelector('[data-slot="scroll-area-viewport"]');
155
+ if (viewport) {
156
+ viewport.scrollTop = viewport.scrollHeight;
157
+ }
143
158
  }
144
159
  }
145
160
  };
146
161
 
147
162
  // Force scroll to bottom (bypasses isNearBottom check)
148
163
  const scrollToBottom = () => {
149
- const viewport = scrollAreaRef.current?.querySelector('[data-slot="scroll-area-viewport"]');
150
- if (viewport) {
151
- viewport.scrollTop = viewport.scrollHeight;
164
+ const detachedContainer = scrollAreaRef.current?.closest('[data-detached-scroll-container]');
165
+ if (detachedContainer) {
166
+ detachedContainer.scrollTop = detachedContainer.scrollHeight;
167
+ } else {
168
+ const viewport = scrollAreaRef.current?.querySelector('[data-slot="scroll-area-viewport"]');
169
+ if (viewport) {
170
+ viewport.scrollTop = viewport.scrollHeight;
171
+ }
152
172
  }
153
173
  };
154
174
 
175
+ // Scroll to bottom with retry for better reliability (especially in detached mode)
176
+ const scrollToBottomWithRetry = (attempts = 3) => {
177
+ const attemptScroll = (remainingAttempts: number) => {
178
+ // Check if we're in a detached window
179
+ const detachedContainer = scrollAreaRef.current?.closest('[data-detached-scroll-container]');
180
+
181
+ if (detachedContainer) {
182
+ // In detached mode, scroll the detached container
183
+ detachedContainer.scrollTop = detachedContainer.scrollHeight;
184
+ requestAnimationFrame(() => {
185
+ const isAtBottom = detachedContainer.scrollHeight - detachedContainer.scrollTop - detachedContainer.clientHeight < 10;
186
+ if (!isAtBottom && remainingAttempts > 0) {
187
+ setTimeout(() => attemptScroll(remainingAttempts - 1), 100);
188
+ }
189
+ });
190
+ } else {
191
+ // Normal mode, scroll the ScrollArea viewport
192
+ const viewport = scrollAreaRef.current?.querySelector('[data-slot="scroll-area-viewport"]');
193
+ if (viewport && viewport.scrollHeight > 0) {
194
+ viewport.scrollTop = viewport.scrollHeight;
195
+ requestAnimationFrame(() => {
196
+ const isAtBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight < 10;
197
+ if (!isAtBottom && remainingAttempts > 0) {
198
+ setTimeout(() => attemptScroll(remainingAttempts - 1), 100);
199
+ }
200
+ });
201
+ } else if (remainingAttempts > 0) {
202
+ // Viewport not ready, retry
203
+ setTimeout(() => attemptScroll(remainingAttempts - 1), 100);
204
+ }
205
+ }
206
+ };
207
+ attemptScroll(attempts);
208
+ };
209
+
155
210
  // Load historical conversation
156
211
  const loadHistory = async () => {
157
212
  try {
@@ -174,9 +229,9 @@ export function ConversationView({
174
229
 
175
230
  // Auto-scroll to bottom when switching to a new task (after history loads)
176
231
  useEffect(() => {
177
- if (!isLoading && historicalTurns.length > 0) {
178
- // Small delay to ensure DOM is updated
179
- setTimeout(scrollToBottom, 100);
232
+ if (!isLoading) {
233
+ // Use retry logic for better reliability in detached mode
234
+ scrollToBottomWithRetry(5);
180
235
  }
181
236
  }, [taskId, isLoading]);
182
237
 
@@ -188,11 +243,73 @@ export function ConversationView({
188
243
  setLastIsRunning(isRunning);
189
244
  }, [isRunning, lastIsRunning]);
190
245
 
191
- // Auto-scroll to bottom on new messages (only if user is near bottom)
246
+ // Detect user scroll to pause auto-scroll
192
247
  useEffect(() => {
193
- scrollToBottomIfNear();
248
+ const getScrollContainer = () => {
249
+ const detachedContainer = scrollAreaRef.current?.closest('[data-detached-scroll-container]');
250
+ if (detachedContainer) return detachedContainer;
251
+ return scrollAreaRef.current?.querySelector('[data-slot="scroll-area-viewport"]');
252
+ };
253
+
254
+ const handleScroll = () => {
255
+ // Mark user as scrolling
256
+ userScrollingRef.current = true;
257
+
258
+ // Clear previous timeout
259
+ if (userScrollTimeoutRef.current) {
260
+ clearTimeout(userScrollTimeoutRef.current);
261
+ }
262
+
263
+ // Reset after user stops scrolling AND is near bottom
264
+ userScrollTimeoutRef.current = setTimeout(() => {
265
+ if (isNearBottom()) {
266
+ userScrollingRef.current = false;
267
+ }
268
+ }, 150);
269
+ };
270
+
271
+ const container = getScrollContainer();
272
+ if (container) {
273
+ container.addEventListener('scroll', handleScroll, { passive: true });
274
+ }
275
+
276
+ return () => {
277
+ if (container) {
278
+ container.removeEventListener('scroll', handleScroll);
279
+ }
280
+ if (userScrollTimeoutRef.current) {
281
+ clearTimeout(userScrollTimeoutRef.current);
282
+ }
283
+ };
284
+ }, [isLoading]);
285
+
286
+ // Auto-scroll to bottom on new messages (only if user is near bottom and not manually scrolling)
287
+ useEffect(() => {
288
+ if (!userScrollingRef.current) {
289
+ scrollToBottomIfNear();
290
+ }
194
291
  }, [currentMessages, historicalTurns, isRunning]);
195
292
 
293
+ // Continuous auto-scroll during streaming (respects user scroll intent)
294
+ // Uses requestAnimationFrame to smoothly scroll as content appears
295
+ useEffect(() => {
296
+ if (!isRunning) return;
297
+
298
+ let rafId: number;
299
+
300
+ const autoScroll = () => {
301
+ // Only auto-scroll if user is not manually scrolling
302
+ if (!userScrollingRef.current) {
303
+ scrollToBottomIfNear();
304
+ }
305
+ rafId = requestAnimationFrame(autoScroll);
306
+ };
307
+
308
+ rafId = requestAnimationFrame(autoScroll);
309
+
310
+ return () => cancelAnimationFrame(rafId);
311
+ }, [isRunning]);
312
+
196
313
  const renderContentBlock = (
197
314
  block: ClaudeContentBlock,
198
315
  index: number,
@@ -243,7 +360,7 @@ export function ConversationView({
243
360
  const blocks = output.message.content;
244
361
 
245
362
  return (
246
- <div key={(output as any)._msgId || index} className="space-y-1 max-w-full overflow-hidden">
363
+ <div key={(output as any)._msgId || index} className="space-y-1 w-full max-w-full overflow-hidden">
247
364
  {blocks.map((block, blockIndex) =>
248
365
  renderContentBlock(block, blockIndex, lastToolUseId, toolResultsMap, isStreaming)
249
366
  )}
@@ -305,7 +422,7 @@ export function ConversationView({
305
422
 
306
423
  // User prompt - simple muted box with file thumbnails
307
424
  const renderUserTurn = (turn: ConversationTurn) => (
308
- <div key={`user-${turn.attemptId}`} className="bg-muted/40 rounded-lg px-4 py-3 text-[15px] leading-relaxed break-words space-y-3">
425
+ <div key={`user-${turn.attemptId}`} className="bg-muted/40 rounded-lg px-4 py-3 text-[15px] leading-relaxed break-words space-y-3 w-full max-w-full overflow-hidden">
309
426
  <div>{turn.prompt}</div>
310
427
  {turn.files && turn.files.length > 0 && (
311
428
  <div className="flex flex-wrap gap-2 pt-1">
@@ -349,7 +466,7 @@ export function ConversationView({
349
466
 
350
467
  // Assistant response - clean text flow
351
468
  const renderAssistantTurn = (turn: ConversationTurn) => (
352
- <div key={`assistant-${turn.attemptId}`} className="space-y-4 max-w-full overflow-hidden">
469
+ <div key={`assistant-${turn.attemptId}`} className="space-y-4 w-full max-w-full overflow-hidden">
353
470
  {turn.messages.map((msg, idx) => renderMessage(msg, idx, false, turn.messages))}
354
471
  </div>
355
472
  );
@@ -390,8 +507,8 @@ export function ConversationView({
390
507
  : historicalTurns;
391
508
 
392
509
  return (
393
- <ScrollArea ref={scrollAreaRef} className={cn('h-full', className)}>
394
- <div className="space-y-6 p-4 pb-24 max-w-full overflow-hidden">
510
+ <ScrollArea ref={scrollAreaRef} className={cn('h-full w-full max-w-full overflow-x-hidden', className)}>
511
+ <div className="space-y-6 p-4 pb-24 w-full max-w-full overflow-x-hidden box-border">
395
512
  {/* Historical turns */}
396
513
  {filteredHistoricalTurns.map(renderTurn)}
397
514
 
@@ -401,7 +518,7 @@ export function ConversationView({
401
518
  <>
402
519
  {/* User prompt if not in history */}
403
520
  {!filteredHistoricalTurns.some(t => t.attemptId === currentAttemptId && t.type === 'user') && currentPrompt && (
404
- <div className="bg-muted/40 rounded-lg px-4 py-3 text-[15px] leading-relaxed break-words space-y-3">
521
+ <div className="bg-muted/40 rounded-lg px-4 py-3 text-[15px] leading-relaxed break-words space-y-3 w-full max-w-full overflow-hidden">
405
522
  <div>{currentPrompt}</div>
406
523
  {currentFiles && currentFiles.length > 0 && (
407
524
  <div className="flex flex-wrap gap-2 pt-1">
@@ -437,7 +554,7 @@ export function ConversationView({
437
554
  </div>
438
555
  )}
439
556
  {/* Streaming response */}
440
- <div className="space-y-4 max-w-full overflow-hidden">
557
+ <div className="space-y-4 w-full max-w-full overflow-hidden">
441
558
  {currentMessages.map((msg, idx) => renderMessage(msg, idx, true, currentMessages))}
442
559
  </div>
443
560
  </>
@@ -447,8 +564,8 @@ export function ConversationView({
447
564
  {isRunning && !hasVisibleContent(currentMessages) &&
448
565
  !filteredHistoricalTurns.some(t => t.attemptId === currentAttemptId && t.type === 'assistant') && (
449
566
  <div className="flex items-center gap-2 text-muted-foreground text-sm py-1">
450
- <RunningDots className="text-primary" />
451
- <span className="font-mono text-[14px]">Thinking...</span>
567
+ <RunningDots />
568
+ <span className="font-mono text-[14px]" style={{ color: '#b9664a' }}>{statusVerb}...</span>
452
569
  </div>
453
570
  )}
454
571
  </div>
@@ -1,49 +1,81 @@
1
1
  'use client';
2
2
 
3
- import {
4
- FileText,
5
- FileCode,
6
- FileJson,
7
- FileImage,
8
- File,
9
- } from 'lucide-react';
10
- import { cn } from '@/lib/utils';
3
+ import { FileExtensionIcon } from '@/components/ui/file-extension-icon';
4
+
5
+ // MIME type to extension mapping
6
+ const MIME_TO_EXT: Record<string, string> = {
7
+ 'image/png': 'png',
8
+ 'image/jpeg': 'jpg',
9
+ 'image/jpg': 'jpg',
10
+ 'image/gif': 'gif',
11
+ 'image/webp': 'webp',
12
+ 'image/svg+xml': 'svg',
13
+ 'image/x-icon': 'ico',
14
+ 'image/bmp': 'bmp',
15
+ 'application/json': 'json',
16
+ 'application/pdf': 'pdf',
17
+ 'application/xml': 'xml',
18
+ 'application/zip': 'zip',
19
+ 'application/gzip': 'gz',
20
+ 'application/x-tar': 'tar',
21
+ 'application/x-rar-compressed': 'rar',
22
+ 'application/x-7z-compressed': '7z',
23
+ 'text/plain': 'txt',
24
+ 'text/html': 'html',
25
+ 'text/css': 'css',
26
+ 'text/javascript': 'js',
27
+ 'text/typescript': 'ts',
28
+ 'text/markdown': 'md',
29
+ 'text/xml': 'xml',
30
+ 'text/csv': 'csv',
31
+ 'application/javascript': 'js',
32
+ 'application/typescript': 'ts',
33
+ 'application/x-typescript': 'ts',
34
+ };
11
35
 
12
36
  interface FileIconProps {
13
37
  mimeType: string;
14
38
  className?: string;
39
+ /** File name for better extension detection */
40
+ fileName?: string;
15
41
  }
16
42
 
17
- // Map MIME types to Lucide icons
18
- export function FileIcon({ mimeType, className }: FileIconProps) {
19
- const iconClass = cn('size-6', className);
43
+ function getExtensionFromMime(mimeType: string): string {
44
+ // Direct mapping
45
+ if (mimeType in MIME_TO_EXT) {
46
+ return MIME_TO_EXT[mimeType];
47
+ }
20
48
 
49
+ // Fallback patterns
21
50
  if (mimeType.startsWith('image/')) {
22
- return <FileImage className={iconClass} />;
51
+ return mimeType.split('/')[1] || 'image';
23
52
  }
24
-
25
- if (mimeType === 'application/json') {
26
- return <FileJson className={iconClass} />;
53
+ if (mimeType.startsWith('video/')) {
54
+ return mimeType.split('/')[1] || 'video';
27
55
  }
28
-
29
- if (
30
- mimeType.includes('typescript') ||
31
- mimeType.includes('javascript') ||
32
- mimeType === 'text/css' ||
33
- mimeType === 'text/html' ||
34
- mimeType === 'text/xml' ||
35
- mimeType === 'application/xml'
36
- ) {
37
- return <FileCode className={iconClass} />;
56
+ if (mimeType.startsWith('audio/')) {
57
+ return mimeType.split('/')[1] || 'audio';
38
58
  }
39
-
40
- if (
41
- mimeType === 'text/plain' ||
42
- mimeType === 'text/markdown' ||
43
- mimeType === 'application/pdf'
44
- ) {
45
- return <FileText className={iconClass} />;
59
+ if (mimeType.includes('typescript')) {
60
+ return 'ts';
46
61
  }
62
+ if (mimeType.includes('javascript')) {
63
+ return 'js';
64
+ }
65
+
66
+ return 'txt';
67
+ }
68
+
69
+ export function FileIcon({ mimeType, className, fileName }: FileIconProps) {
70
+ // Use fileName if provided, otherwise derive from mimeType
71
+ const name = fileName || `file.${getExtensionFromMime(mimeType)}`;
47
72
 
48
- return <File className={iconClass} />;
73
+ return (
74
+ <FileExtensionIcon
75
+ name={name}
76
+ type="file"
77
+ size={24}
78
+ className={className}
79
+ />
80
+ );
49
81
  }
@@ -18,6 +18,7 @@ import { X } from 'lucide-react';
18
18
 
19
19
  export interface PromptInputRef {
20
20
  submit: () => void;
21
+ focus: () => void;
21
22
  }
22
23
 
23
24
  interface PromptInputProps {
@@ -365,21 +366,24 @@ export const PromptInput = forwardRef<PromptInputRef, PromptInputProps>(({
365
366
  fileInputRef.current?.click();
366
367
  };
367
368
 
368
- // Expose submit function to parent via ref
369
+ // Expose submit and focus functions to parent via ref
369
370
  useImperativeHandle(ref, () => ({
370
371
  submit: () => {
371
372
  if (!prompt.trim() || disabled) return;
372
373
  handleSubmit({ preventDefault: () => { } } as FormEvent);
373
374
  },
375
+ focus: () => {
376
+ textareaRef.current?.focus();
377
+ },
374
378
  }));
375
379
 
376
380
  return (
377
381
  <FileDropZone
378
382
  onFilesSelected={handleFilesSelected}
379
383
  disabled={disabled}
380
- className={cn('relative flex flex-col', className)}
384
+ className={cn('relative flex flex-col overflow-x-hidden', className)}
381
385
  >
382
- <form onSubmit={handleSubmit} className="flex flex-col gap-2 w-full min-w-0">
386
+ <form onSubmit={handleSubmit} className="flex flex-col gap-2 w-full min-w-0 overflow-x-hidden">
383
387
  {/* Command Selector */}
384
388
  <CommandSelector
385
389
  isOpen={showCommands}
@@ -422,7 +426,7 @@ export const PromptInput = forwardRef<PromptInputRef, PromptInputProps>(({
422
426
  )}
423
427
 
424
428
  {/* Input area */}
425
- <div className="w-full min-w-0">
429
+ <div className="w-full min-w-0 max-w-full">
426
430
  {/* File Mention Dropdown */}
427
431
  <FileMentionDropdown
428
432
  query={fileMentionQuery}
@@ -433,10 +437,10 @@ export const PromptInput = forwardRef<PromptInputRef, PromptInputProps>(({
433
437
 
434
438
  {/* Textarea and buttons as a single block */}
435
439
  <div className={cn(
436
- 'rounded-md border overflow-hidden bg-background',
440
+ 'rounded-md border overflow-hidden bg-background w-full max-w-full',
437
441
  selectedCommand ? 'border-primary' : 'border-input'
438
442
  )}>
439
- <div className="relative">
443
+ <div className="relative w-full max-w-full">
440
444
  <Textarea
441
445
  ref={textareaRef}
442
446
  value={prompt}
@@ -451,9 +455,10 @@ export const PromptInput = forwardRef<PromptInputRef, PromptInputProps>(({
451
455
  placeholder={placeholder}
452
456
  disabled={disabled}
453
457
  className={cn(
454
- 'min-h-10 max-h-48 resize-none w-full break-words overflow-y-auto border-0 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0',
458
+ 'min-h-10 max-h-48 resize-none w-full break-words overflow-y-auto border-0 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 text-base',
455
459
  selectedCommand && 'border-primary'
456
460
  )}
461
+ style={{ fontSize: '16px' }}
457
462
  />
458
463
  {selectedCommand && (
459
464
  <div className="absolute top-2 right-2">