@standardagents/cli 0.10.1-next.bbd142a → 0.11.0-next.ab7e1ea

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.
@@ -0,0 +1,222 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback } from 'react'
4
+ import ReactMarkdown from 'react-markdown'
5
+ import ShikiHighlighter, { isInlineCode } from 'react-shiki'
6
+ import type { Components } from 'react-markdown'
7
+ import type { Element } from 'hast'
8
+
9
+ // Copy icon component
10
+ function CopyIcon({ className }: { className?: string }) {
11
+ return (
12
+ <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
13
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
14
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
15
+ </svg>
16
+ )
17
+ }
18
+
19
+ function CheckIcon({ className }: { className?: string }) {
20
+ return (
21
+ <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
22
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
23
+ </svg>
24
+ )
25
+ }
26
+
27
+ // Language aliases for common variations
28
+ const langAlias: Record<string, string> = {
29
+ js: 'javascript',
30
+ ts: 'typescript',
31
+ tsx: 'tsx',
32
+ jsx: 'jsx',
33
+ sh: 'bash',
34
+ shell: 'bash',
35
+ zsh: 'bash',
36
+ yml: 'yaml',
37
+ py: 'python',
38
+ rb: 'ruby',
39
+ rs: 'rust',
40
+ go: 'go',
41
+ md: 'markdown',
42
+ mdx: 'mdx',
43
+ vue: 'vue',
44
+ svelte: 'svelte',
45
+ dockerfile: 'docker',
46
+ tf: 'hcl',
47
+ terraform: 'hcl',
48
+ cs: 'csharp',
49
+ 'c#': 'csharp',
50
+ 'c++': 'cpp',
51
+ kt: 'kotlin',
52
+ objc: 'objective-c',
53
+ 'objective-c': 'objc',
54
+ pl: 'perl',
55
+ hs: 'haskell',
56
+ ex: 'elixir',
57
+ erl: 'erlang',
58
+ fs: 'fsharp',
59
+ 'f#': 'fsharp',
60
+ ps1: 'powershell',
61
+ psm1: 'powershell',
62
+ }
63
+
64
+ interface CodeBlockProps {
65
+ className?: string
66
+ children?: React.ReactNode
67
+ node?: Element
68
+ }
69
+
70
+ function CodeBlock({ className, children, node, ...props }: CodeBlockProps) {
71
+ const [copied, setCopied] = useState(false)
72
+ const code = String(children).trim()
73
+ const match = className?.match(/language-(\w+)/)
74
+ let language = match ? match[1] : 'text'
75
+
76
+ // Apply language aliases
77
+ if (language in langAlias) {
78
+ language = langAlias[language]
79
+ }
80
+
81
+ const isInline = node ? isInlineCode(node) : !code.includes('\n')
82
+
83
+ const handleCopy = useCallback(async () => {
84
+ await navigator.clipboard.writeText(code)
85
+ setCopied(true)
86
+ setTimeout(() => setCopied(false), 2000)
87
+ }, [code])
88
+
89
+ if (isInline) {
90
+ return (
91
+ <code
92
+ className="px-1.5 py-0.5 rounded bg-[var(--code-bg)] text-[var(--text-primary)] text-sm font-mono"
93
+ {...props}
94
+ >
95
+ {children}
96
+ </code>
97
+ )
98
+ }
99
+
100
+ return (
101
+ <div className="relative group my-4 rounded-lg overflow-hidden bg-[var(--code-bg)]">
102
+ <button
103
+ onClick={handleCopy}
104
+ className="absolute top-2 right-2 px-2 py-1 rounded-md flex items-center gap-1 text-xs text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] transition-all z-10"
105
+ title={copied ? 'Copied!' : 'Copy code'}
106
+ >
107
+ {copied ? (
108
+ <>
109
+ <CheckIcon className="w-3 h-3 text-emerald-500" />
110
+ <span className="text-emerald-500">Copied</span>
111
+ </>
112
+ ) : (
113
+ <>
114
+ <CopyIcon className="w-3 h-3" />
115
+ <span>Copy</span>
116
+ </>
117
+ )}
118
+ </button>
119
+ <ShikiHighlighter
120
+ language={language}
121
+ theme={{
122
+ light: 'github-light',
123
+ dark: 'github-dark',
124
+ }}
125
+ defaultColor="light-dark()"
126
+ langAlias={langAlias}
127
+ showLanguage={false}
128
+ addDefaultStyles={false}
129
+ className="text-sm p-4"
130
+ {...props}
131
+ >
132
+ {code}
133
+ </ShikiHighlighter>
134
+ </div>
135
+ )
136
+ }
137
+
138
+ // Custom components for react-markdown
139
+ const components: Components = {
140
+ code: CodeBlock as Components['code'],
141
+ // Style other markdown elements
142
+ p: ({ children }) => (
143
+ <p className="mb-4 last:mb-0">{children}</p>
144
+ ),
145
+ ul: ({ children }) => (
146
+ <ul className="list-disc list-inside mb-4 space-y-1">{children}</ul>
147
+ ),
148
+ ol: ({ children }) => (
149
+ <ol className="list-decimal list-inside mb-4 space-y-1">{children}</ol>
150
+ ),
151
+ li: ({ children }) => (
152
+ <li className="text-[var(--text-primary)]">{children}</li>
153
+ ),
154
+ h1: ({ children }) => (
155
+ <h1 className="text-2xl font-bold mb-4 mt-6 first:mt-0 text-[var(--text-primary)]">{children}</h1>
156
+ ),
157
+ h2: ({ children }) => (
158
+ <h2 className="text-xl font-bold mb-3 mt-5 first:mt-0 text-[var(--text-primary)]">{children}</h2>
159
+ ),
160
+ h3: ({ children }) => (
161
+ <h3 className="text-lg font-bold mb-2 mt-4 first:mt-0 text-[var(--text-primary)]">{children}</h3>
162
+ ),
163
+ h4: ({ children }) => (
164
+ <h4 className="text-base font-bold mb-2 mt-3 first:mt-0 text-[var(--text-primary)]">{children}</h4>
165
+ ),
166
+ blockquote: ({ children }) => (
167
+ <blockquote className="border-l-4 border-[var(--border-secondary)] pl-4 my-4 text-[var(--text-secondary)] italic">
168
+ {children}
169
+ </blockquote>
170
+ ),
171
+ a: ({ href, children }) => (
172
+ <a
173
+ href={href}
174
+ target="_blank"
175
+ rel="noopener noreferrer"
176
+ className="text-blue-500 hover:text-blue-400 underline"
177
+ >
178
+ {children}
179
+ </a>
180
+ ),
181
+ strong: ({ children }) => (
182
+ <strong className="font-semibold text-[var(--text-primary)]">{children}</strong>
183
+ ),
184
+ em: ({ children }) => (
185
+ <em className="italic">{children}</em>
186
+ ),
187
+ hr: () => (
188
+ <hr className="my-6 border-[var(--border-primary)]" />
189
+ ),
190
+ table: ({ children }) => (
191
+ <div className="overflow-x-auto my-4">
192
+ <table className="min-w-full border-collapse border border-[var(--border-primary)]">
193
+ {children}
194
+ </table>
195
+ </div>
196
+ ),
197
+ th: ({ children }) => (
198
+ <th className="border border-[var(--border-primary)] px-3 py-2 bg-[var(--bg-tertiary)] text-left font-semibold">
199
+ {children}
200
+ </th>
201
+ ),
202
+ td: ({ children }) => (
203
+ <td className="border border-[var(--border-primary)] px-3 py-2">
204
+ {children}
205
+ </td>
206
+ ),
207
+ }
208
+
209
+ interface MarkdownProps {
210
+ children: string
211
+ className?: string
212
+ }
213
+
214
+ export function Markdown({ children, className }: MarkdownProps) {
215
+ return (
216
+ <div className={className}>
217
+ <ReactMarkdown components={components}>
218
+ {children}
219
+ </ReactMarkdown>
220
+ </div>
221
+ )
222
+ }
@@ -0,0 +1,197 @@
1
+ 'use client'
2
+
3
+ import { useState, useRef, useEffect, useCallback } from 'react'
4
+ import { useThread } from '@standardagents/react'
5
+
6
+ export function MessageInput() {
7
+ const [input, setInput] = useState('')
8
+ const [sending, setSending] = useState(false)
9
+ const {
10
+ loading,
11
+ sendMessage,
12
+ stopExecution,
13
+ addAttachment,
14
+ attachments,
15
+ removeAttachment,
16
+ getPreviewUrl,
17
+ } = useThread()
18
+ const textareaRef = useRef<HTMLTextAreaElement>(null)
19
+ const fileInputRef = useRef<HTMLInputElement>(null)
20
+
21
+ const handleSubmit = useCallback(async (e: React.FormEvent) => {
22
+ e.preventDefault()
23
+ if ((!input.trim() && attachments.length === 0) || loading || sending) return
24
+
25
+ const message = input
26
+ setInput('')
27
+ setSending(true)
28
+
29
+ try {
30
+ // SDK's sendMessage automatically includes queued attachments and clears them
31
+ await sendMessage({ role: 'user', content: message })
32
+ } catch (error) {
33
+ console.error('Failed to send message:', error)
34
+ setInput(message)
35
+ } finally {
36
+ setSending(false)
37
+ }
38
+ }, [input, attachments.length, loading, sending, sendMessage])
39
+
40
+ const handleFileSelect = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
41
+ const files = e.target.files
42
+ if (!files || files.length === 0) return
43
+
44
+ // SDK handles base64 encoding, dimensions, and preview URLs
45
+ for (const file of Array.from(files)) {
46
+ await addAttachment(file)
47
+ }
48
+
49
+ // Reset file input and refocus textarea
50
+ if (fileInputRef.current) {
51
+ fileInputRef.current.value = ''
52
+ }
53
+ textareaRef.current?.focus()
54
+ }, [addAttachment])
55
+
56
+ // Auto-resize textarea
57
+ useEffect(() => {
58
+ const textarea = textareaRef.current
59
+ if (textarea) {
60
+ textarea.style.height = 'auto'
61
+ const newHeight = Math.min(Math.max(textarea.scrollHeight, 24), 200)
62
+ textarea.style.height = newHeight + 'px'
63
+ }
64
+ }, [input])
65
+
66
+ // Focus textarea when not loading
67
+ useEffect(() => {
68
+ if (!loading && !sending) {
69
+ textareaRef.current?.focus()
70
+ }
71
+ }, [loading, sending])
72
+
73
+ const isDisabled = loading || sending
74
+ const canSend = (input.trim() || attachments.length > 0) && !isDisabled
75
+
76
+ return (
77
+ <div className="bg-[var(--bg-tertiary)]/80 backdrop-blur-md rounded-2xl border border-[var(--border-primary)] shadow-lg shadow-black/10 overflow-hidden">
78
+ {/* Attachment drawer - extends from top of input */}
79
+ {attachments.length > 0 && (
80
+ <div className="px-3 pt-3 pb-2 border-b border-[var(--border-secondary)]">
81
+ <div className="flex flex-wrap gap-2">
82
+ {attachments.map((attachment) => {
83
+ // Try multiple ways to get preview URL
84
+ const previewUrl = getPreviewUrl(attachment) || (attachment as any).localPreviewUrl || (attachment as any).previewUrl
85
+ const isImage = attachment.mimeType?.startsWith('image/') || (attachment as any).type?.startsWith('image/')
86
+ return (
87
+ <div
88
+ key={attachment.id}
89
+ className="relative group"
90
+ >
91
+ {isImage && previewUrl ? (
92
+ <img
93
+ src={previewUrl}
94
+ alt={attachment.name}
95
+ className="h-16 w-auto rounded-lg object-cover border border-[var(--border-secondary)]"
96
+ />
97
+ ) : (
98
+ <div className="h-16 px-3 flex items-center gap-2 rounded-lg bg-[var(--bg-hover)] border border-[var(--border-secondary)]">
99
+ <svg className="w-4 h-4 text-[var(--text-secondary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
100
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
101
+ </svg>
102
+ <span className="text-xs text-[var(--text-secondary)] max-w-[100px] truncate">{attachment.name}</span>
103
+ </div>
104
+ )}
105
+ <button
106
+ type="button"
107
+ onClick={() => removeAttachment(attachment.id)}
108
+ className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-[var(--bg-active)] hover:bg-[var(--bg-hover)] rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
109
+ >
110
+ <svg className="w-3 h-3 text-[var(--text-primary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
111
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
112
+ </svg>
113
+ </button>
114
+ </div>
115
+ )
116
+ })}
117
+ </div>
118
+ </div>
119
+ )}
120
+
121
+ {/* Input row */}
122
+ <form onSubmit={handleSubmit}>
123
+ <div className="flex items-center gap-2 px-2 py-1.5">
124
+ {/* Attachment button */}
125
+ <input
126
+ ref={fileInputRef}
127
+ type="file"
128
+ multiple
129
+ accept="image/*,.pdf,.txt,.md,.json,.csv"
130
+ onChange={handleFileSelect}
131
+ className="hidden"
132
+ />
133
+ <button
134
+ type="button"
135
+ onClick={() => fileInputRef.current?.click()}
136
+ disabled={isDisabled}
137
+ className="p-2 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
138
+ title="Attach files"
139
+ >
140
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
141
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1.5 1.5 0 002.112 2.13" />
142
+ </svg>
143
+ </button>
144
+
145
+ {/* Textarea */}
146
+ <textarea
147
+ ref={textareaRef}
148
+ value={input}
149
+ onChange={(e) => setInput(e.target.value)}
150
+ onKeyDown={(e) => {
151
+ if (e.key === 'Enter' && !e.shiftKey) {
152
+ e.preventDefault()
153
+ handleSubmit(e)
154
+ }
155
+ }}
156
+ placeholder="Message..."
157
+ rows={1}
158
+ disabled={isDisabled}
159
+ className="flex-1 bg-transparent resize-none text-[var(--text-primary)] placeholder-[var(--text-tertiary)] focus:outline-none disabled:opacity-50 text-[15px] leading-normal py-2.5 max-h-[200px] overflow-y-auto scrollbar-hide"
160
+ style={{ scrollbarWidth: 'none' }}
161
+ />
162
+
163
+ {/* Stop / Send button */}
164
+ {loading ? (
165
+ <button
166
+ type="button"
167
+ onClick={stopExecution}
168
+ className="p-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] rounded-xl transition-colors shrink-0"
169
+ title="Stop generating"
170
+ >
171
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
172
+ <rect x="6" y="6" width="12" height="12" rx="2" />
173
+ </svg>
174
+ </button>
175
+ ) : (
176
+ <button
177
+ type="submit"
178
+ disabled={!canSend}
179
+ className="p-2 rounded-xl bg-[var(--text-primary)] text-[var(--bg-primary)] hover:opacity-90 disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-150 shrink-0"
180
+ >
181
+ {sending ? (
182
+ <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
183
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
184
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
185
+ </svg>
186
+ ) : (
187
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
188
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" />
189
+ </svg>
190
+ )}
191
+ </button>
192
+ )}
193
+ </div>
194
+ </form>
195
+ </div>
196
+ )
197
+ }