@bibobonking/opensol 0.0.0-stage → 3.0.1
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/.github/workflows/build.yml +25 -0
- package/README.md +74 -2
- package/dist/assets/index-BmN7pBji.css +1 -0
- package/dist/assets/index-DgNDjkOz.js +301 -0
- package/dist/favicon.svg +15 -0
- package/dist/index.html +55 -0
- package/dist/preview-host.html +33 -0
- package/index.html +54 -0
- package/package.json +43 -4
- package/postcss.config.js +6 -0
- package/public/favicon.svg +15 -0
- package/public/preview-host.html +33 -0
- package/shared/plugin-builder.test.mjs +43 -0
- package/shared/plugin-package.d.mts +34 -0
- package/shared/plugin-package.mjs +197 -0
- package/shared/plugin-package.test.mjs +94 -0
- package/src/App.tsx +152 -0
- package/src/agent/adapters/anthropic.ts +233 -0
- package/src/agent/adapters/gemini.ts +225 -0
- package/src/agent/adapters/openai-responses.ts +215 -0
- package/src/agent/adapters/openai.ts +200 -0
- package/src/agent/adapters/shared.ts +96 -0
- package/src/agent/approval.ts +20 -0
- package/src/agent/compact.ts +234 -0
- package/src/agent/index.ts +36 -0
- package/src/agent/model.ts +20 -0
- package/src/agent/prompt.ts +107 -0
- package/src/agent/queue.test.mjs +111 -0
- package/src/agent/queue.ts +84 -0
- package/src/agent/subagent.ts +109 -0
- package/src/agent/tools.ts +541 -0
- package/src/components/ChatView.tsx +403 -0
- package/src/components/CommandPalette.tsx +416 -0
- package/src/components/Composer.tsx +678 -0
- package/src/components/ContextBar.tsx +58 -0
- package/src/components/DeployModal.tsx +512 -0
- package/src/components/ErrorBoundary.tsx +75 -0
- package/src/components/HomeDashboard.tsx +189 -0
- package/src/components/Logo.tsx +41 -0
- package/src/components/Markdown.tsx +232 -0
- package/src/components/MessageItem.tsx +655 -0
- package/src/components/ModelPicker.tsx +487 -0
- package/src/components/PixleSol.tsx +243 -0
- package/src/components/PluginBuilder.tsx +285 -0
- package/src/components/PluginManager.tsx +341 -0
- package/src/components/PreviewPane.tsx +166 -0
- package/src/components/SettingsModal.tsx +2052 -0
- package/src/components/Sidebar.tsx +504 -0
- package/src/components/Splash.tsx +104 -0
- package/src/components/ToolCallCard.tsx +177 -0
- package/src/components/TopBar.tsx +182 -0
- package/src/components/TrajectoryView.tsx +178 -0
- package/src/components/WorkspacePanel.tsx +710 -0
- package/src/components/ui.tsx +311 -0
- package/src/core/kernel.ts +45 -0
- package/src/core/plugins/tools/fs.ts +219 -0
- package/src/core/plugins/tools/git.ts +83 -0
- package/src/core/plugins/tools/grep.ts +98 -0
- package/src/core/plugins/tools/shell.ts +88 -0
- package/src/core/plugins/tools/subagent.ts +36 -0
- package/src/core/plugins/tools/web.ts +97 -0
- package/src/core/services/agent-loop.ts +417 -0
- package/src/core/services/approval.ts +66 -0
- package/src/core/services/compaction.ts +45 -0
- package/src/core/services/llm.ts +32 -0
- package/src/core/services/sessions.ts +47 -0
- package/src/core/services/subagents.ts +15 -0
- package/src/core/services/system-prompt.ts +177 -0
- package/src/core/services/tools.ts +135 -0
- package/src/core/services/ui.ts +109 -0
- package/src/core/types.ts +139 -0
- package/src/data/providers.ts +125 -0
- package/src/data/skills.ts +68 -0
- package/src/index.css +776 -0
- package/src/lib/boot.ts +94 -0
- package/src/lib/context.ts +75 -0
- package/src/lib/conversation.test.mjs +64 -0
- package/src/lib/conversation.ts +59 -0
- package/src/lib/daemon.ts +475 -0
- package/src/lib/hooks.ts +33 -0
- package/src/lib/permissions.ts +60 -0
- package/src/lib/pixlesol.test.mjs +51 -0
- package/src/lib/pixlesol.ts +55 -0
- package/src/lib/plugin-folder.test.mjs +145 -0
- package/src/lib/plugin-folder.ts +96 -0
- package/src/lib/reasoning.ts +87 -0
- package/src/lib/sse.ts +75 -0
- package/src/lib/themes.test.mjs +72 -0
- package/src/lib/themes.ts +52 -0
- package/src/lib/usage.test.mjs +72 -0
- package/src/lib/usage.ts +155 -0
- package/src/lib/utils.ts +73 -0
- package/src/main.tsx +16 -0
- package/src/plugins/api.test.mjs +77 -0
- package/src/plugins/api.ts +113 -0
- package/src/plugins/bridge.d.mts +1 -0
- package/src/plugins/bridge.mjs +26 -0
- package/src/plugins/bridge.test.mjs +17 -0
- package/src/plugins/index.ts +52 -0
- package/src/plugins/preview.test.mjs +18 -0
- package/src/plugins/preview.ts +34 -0
- package/src/plugins/runtime.ts +194 -0
- package/src/plugins/types.ts +40 -0
- package/src/store/chats.ts +278 -0
- package/src/store/models.ts +120 -0
- package/src/store/settings.ts +445 -0
- package/src/store/ui.ts +132 -0
- package/src/store/workspace.ts +299 -0
- package/src/types.ts +337 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +83 -0
- package/tsconfig.json +22 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vite.config.ts +19 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { ArrowUp, Bug, FlaskConical, FolderSearch, Gamepad2, Globe2, KeyRound, LayoutTemplate, ListChecks, Sparkles } from 'lucide-react'
|
|
3
|
+
import type { Attachment } from '../types'
|
|
4
|
+
import { createId, useChats } from '../store/chats'
|
|
5
|
+
import { activeModelId, hasCredentials, useSettings } from '../store/settings'
|
|
6
|
+
import { useActiveModel } from '../store/models'
|
|
7
|
+
import { useRuntime, useUi } from '../store/ui'
|
|
8
|
+
import { useWorkspace } from '../store/workspace'
|
|
9
|
+
import { getProviderName } from '../data/providers'
|
|
10
|
+
import { stopRun, runAgent } from '../agent'
|
|
11
|
+
import { getContextUsage } from '../lib/context'
|
|
12
|
+
import { pixleMood } from '../lib/pixlesol'
|
|
13
|
+
import { formatTokens } from '../lib/usage'
|
|
14
|
+
import { groupConversationTurns } from '../lib/conversation'
|
|
15
|
+
import { TopBar, type ChatViewMode } from './TopBar'
|
|
16
|
+
import { Composer } from './Composer'
|
|
17
|
+
import { TurnBlock } from './MessageItem'
|
|
18
|
+
import { HomeDashboard, HomeGreeting } from './HomeDashboard'
|
|
19
|
+
import { TrajectoryView } from './TrajectoryView'
|
|
20
|
+
|
|
21
|
+
const STARTERS = [
|
|
22
|
+
{
|
|
23
|
+
icon: Gamepad2,
|
|
24
|
+
label: 'Build a game',
|
|
25
|
+
prompt:
|
|
26
|
+
'Build a small browser game as one self-contained index.html (canvas, no build step). Make it playable with keyboard and touch, with a score and a restart button. Put the whole file in a single html code block so I can preview it.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
icon: LayoutTemplate,
|
|
30
|
+
label: 'Design a landing page',
|
|
31
|
+
prompt:
|
|
32
|
+
'Design a polished, responsive landing page for a small indie game studio. Choose a distinctive typographic style, keep it to one file, and put everything in a single html code block so I can preview it.',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
icon: Bug,
|
|
36
|
+
label: 'Trace a bug',
|
|
37
|
+
prompt:
|
|
38
|
+
'Inspect this workspace for a real bug. Explain the root cause, make the smallest correct fix, and run the project checks.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
icon: FolderSearch,
|
|
42
|
+
label: 'Audit this folder',
|
|
43
|
+
prompt:
|
|
44
|
+
'Audit this workspace. Read the key files first, then list the three most important improvements with file references and a short plan.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
icon: FlaskConical,
|
|
48
|
+
label: 'Write tests',
|
|
49
|
+
prompt:
|
|
50
|
+
'Find the least-tested, highest-risk module in this workspace, write focused tests for it, run them, and fix anything they reveal.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
icon: Globe2,
|
|
54
|
+
label: 'Research with sources',
|
|
55
|
+
prompt:
|
|
56
|
+
'Search the web for the current release notes of the main framework in this project, then summarise what matters and cite the pages you read.',
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
export function ChatView() {
|
|
61
|
+
const sessions = useChats((state) => state.sessions)
|
|
62
|
+
const activeId = useChats((state) => state.activeId)
|
|
63
|
+
const session = sessions.find((item) => item.id === activeId) ?? null
|
|
64
|
+
const streaming = useRuntime((state) => (activeId ? Boolean(state.streaming[activeId]) : false))
|
|
65
|
+
const settings = useSettings()
|
|
66
|
+
const setMode = useSettings((state) => state.setMode)
|
|
67
|
+
const update = useSettings((state) => state.update)
|
|
68
|
+
const notify = useUi((state) => state.notify)
|
|
69
|
+
const openSettings = useUi((state) => state.openSettings)
|
|
70
|
+
const focusComposer = useUi((state) => state.focusComposer)
|
|
71
|
+
const setWorkspaceOpen = useUi((state) => state.setWorkspaceOpen)
|
|
72
|
+
const setDeployOpen = useUi((state) => state.setDeployOpen)
|
|
73
|
+
const askConfirm = useUi((state) => state.askConfirm)
|
|
74
|
+
const workspace = useWorkspace((state) => state.info)
|
|
75
|
+
const git = useWorkspace((state) => state.git)
|
|
76
|
+
const activeModel = useActiveModel()
|
|
77
|
+
const [draft, setDraft] = useState('')
|
|
78
|
+
const [view, setView] = useState<ChatViewMode>('chat')
|
|
79
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
80
|
+
const [atBottom, setAtBottom] = useState(true)
|
|
81
|
+
const lastSessionRef = useRef<string | null>(activeId)
|
|
82
|
+
const lastMessageIdRef = useRef<string | null>(null)
|
|
83
|
+
|
|
84
|
+
const messages = session?.messages ?? []
|
|
85
|
+
const lastMessage = messages[messages.length - 1]
|
|
86
|
+
const lastAssistantId = [...messages].reverse().find((message) => message.role === 'assistant')?.id
|
|
87
|
+
const connected = hasCredentials(settings)
|
|
88
|
+
const selectedModel = activeModelId(settings)
|
|
89
|
+
const activeSkills = settings.skills.filter((skill) => skill.enabled).length
|
|
90
|
+
const turns = groupConversationTurns(messages)
|
|
91
|
+
const context = getContextUsage(messages, activeModel)
|
|
92
|
+
const mood = pixleMood(messages, streaming)
|
|
93
|
+
|
|
94
|
+
const scrollToBottom = (behavior: ScrollBehavior = 'auto') => {
|
|
95
|
+
const node = scrollRef.current
|
|
96
|
+
if (!node) return
|
|
97
|
+
node.scrollTo({ top: node.scrollHeight, behavior })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (lastSessionRef.current !== activeId) {
|
|
102
|
+
lastSessionRef.current = activeId
|
|
103
|
+
setAtBottom(true)
|
|
104
|
+
setView('chat')
|
|
105
|
+
requestAnimationFrame(() => scrollToBottom('auto'))
|
|
106
|
+
const current = useChats.getState().sessions.find((item) => item.id === activeId)
|
|
107
|
+
if (current?.mode) setMode(current.mode)
|
|
108
|
+
}
|
|
109
|
+
}, [activeId, setMode])
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!lastMessage) return
|
|
113
|
+
if (lastMessage.id !== lastMessageIdRef.current) {
|
|
114
|
+
lastMessageIdRef.current = lastMessage.id
|
|
115
|
+
if (lastMessage.role === 'user') {
|
|
116
|
+
setAtBottom(true)
|
|
117
|
+
requestAnimationFrame(() => scrollToBottom('auto'))
|
|
118
|
+
} else if (atBottom) {
|
|
119
|
+
requestAnimationFrame(() => scrollToBottom('auto'))
|
|
120
|
+
}
|
|
121
|
+
} else if (atBottom) {
|
|
122
|
+
scrollToBottom('auto')
|
|
123
|
+
}
|
|
124
|
+
}, [lastMessage, atBottom])
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!atBottom) return
|
|
128
|
+
const timer = window.setTimeout(() => scrollToBottom('auto'), 40)
|
|
129
|
+
return () => window.clearTimeout(timer)
|
|
130
|
+
}, [messages, atBottom])
|
|
131
|
+
|
|
132
|
+
const send = async (text: string, attachments: Attachment[]): Promise<boolean> => {
|
|
133
|
+
const current = useSettings.getState()
|
|
134
|
+
if (!hasCredentials(current)) {
|
|
135
|
+
notify(
|
|
136
|
+
current.provider === 'custom'
|
|
137
|
+
? 'Add a base URL for your custom endpoint in Settings → Models.'
|
|
138
|
+
: `Add an API key for ${getProviderName(current.provider)} in Settings → Models.`,
|
|
139
|
+
'error',
|
|
140
|
+
)
|
|
141
|
+
openSettings('models')
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
const modelId = activeModelId(current)
|
|
145
|
+
if (!modelId) {
|
|
146
|
+
notify('Choose a model before sending a message.', 'error')
|
|
147
|
+
openSettings('models')
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let sessionId = activeId
|
|
152
|
+
if (!sessionId || !useChats.getState().sessions.some((item) => item.id === sessionId)) {
|
|
153
|
+
sessionId = useChats.getState().createSession(current.provider, modelId, current.mode)
|
|
154
|
+
}
|
|
155
|
+
const content = text.trim()
|
|
156
|
+
const userMessageId = createId('msg')
|
|
157
|
+
useChats.getState().addMessage(sessionId, {
|
|
158
|
+
id: userMessageId,
|
|
159
|
+
role: 'user',
|
|
160
|
+
content,
|
|
161
|
+
attachments: attachments.length > 0 ? attachments : undefined,
|
|
162
|
+
createdAt: Date.now(),
|
|
163
|
+
})
|
|
164
|
+
useChats.getState().updateSession(sessionId, { mode: current.mode })
|
|
165
|
+
setDraft('')
|
|
166
|
+
setAtBottom(true)
|
|
167
|
+
runAgent(sessionId, userMessageId)
|
|
168
|
+
return true
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const useStarter = (prompt: string) => {
|
|
172
|
+
setDraft(prompt)
|
|
173
|
+
focusComposer()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const handleCommand = async (command: string, args: string): Promise<boolean> => {
|
|
177
|
+
switch (command) {
|
|
178
|
+
case 'search': {
|
|
179
|
+
if (!args.trim()) {
|
|
180
|
+
setDraft('/search ')
|
|
181
|
+
notify('Type what you want OpenSol to research.', 'info')
|
|
182
|
+
focusComposer()
|
|
183
|
+
return false
|
|
184
|
+
}
|
|
185
|
+
update({ webSearchEnabled: true })
|
|
186
|
+
return send(
|
|
187
|
+
`Use the web to research this: ${args.trim()}\n\nRead the most useful sources, cite the URLs you actually opened, and answer for this workspace.`,
|
|
188
|
+
[],
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
case 'plan': {
|
|
192
|
+
setMode('plan')
|
|
193
|
+
if (!args.trim()) {
|
|
194
|
+
notify('Plan mode is on: investigation only until you switch to Build.', 'info')
|
|
195
|
+
focusComposer()
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
return send(args.trim(), [])
|
|
199
|
+
}
|
|
200
|
+
case 'build': {
|
|
201
|
+
setMode('build')
|
|
202
|
+
if (!args.trim()) {
|
|
203
|
+
notify('Build mode is on: OpenSol can edit files and run commands.', 'info')
|
|
204
|
+
focusComposer()
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
return send(args.trim(), [])
|
|
208
|
+
}
|
|
209
|
+
case 'new': {
|
|
210
|
+
useChats.getState().newChat()
|
|
211
|
+
focusComposer()
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
case 'workspace': {
|
|
215
|
+
setWorkspaceOpen(true)
|
|
216
|
+
return true
|
|
217
|
+
}
|
|
218
|
+
case 'deploy': {
|
|
219
|
+
setDeployOpen(true)
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
case 'settings': {
|
|
223
|
+
openSettings('models')
|
|
224
|
+
return true
|
|
225
|
+
}
|
|
226
|
+
case 'clear': {
|
|
227
|
+
const current = useChats.getState().sessions.find((item) => item.id === activeId)
|
|
228
|
+
if (!current || current.messages.length === 0) return true
|
|
229
|
+
const confirmed = await askConfirm({
|
|
230
|
+
title: 'Clear this conversation?',
|
|
231
|
+
body: 'The messages in this conversation will be removed from your local history. The workspace files are not touched.',
|
|
232
|
+
confirmLabel: 'Clear conversation',
|
|
233
|
+
tone: 'danger',
|
|
234
|
+
})
|
|
235
|
+
if (!confirmed) return true
|
|
236
|
+
stopRun(current.id)
|
|
237
|
+
useChats.getState().updateSession(current.id, { messages: [] })
|
|
238
|
+
useChats.getState().updateSession(current.id, { title: 'New conversation' })
|
|
239
|
+
notify('Conversation cleared.', 'success')
|
|
240
|
+
return true
|
|
241
|
+
}
|
|
242
|
+
default:
|
|
243
|
+
return false
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div className="flex min-h-0 min-w-0 flex-1 flex-col bg-panel">
|
|
249
|
+
<TopBar session={session} view={view} onViewChange={setView} />
|
|
250
|
+
|
|
251
|
+
{!session || messages.length === 0 ? (
|
|
252
|
+
<div className="thin-scroll relative min-h-0 flex-1 overflow-y-auto">
|
|
253
|
+
<div className="hero-wash pointer-events-none absolute inset-0" aria-hidden="true" />
|
|
254
|
+
<div className="bg-dots pointer-events-none absolute inset-0 opacity-70" aria-hidden="true" />
|
|
255
|
+
<div className="relative mx-auto flex min-h-full w-full max-w-[720px] flex-col justify-center px-4 py-10 sm:px-6">
|
|
256
|
+
<div className="animate-blurin">
|
|
257
|
+
<HomeGreeting />
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div className="mt-8 w-full animate-rise [animation-delay:120ms]">
|
|
261
|
+
{(!connected || !selectedModel) && (
|
|
262
|
+
<button
|
|
263
|
+
type="button"
|
|
264
|
+
onClick={() => openSettings('models')}
|
|
265
|
+
className="mb-3 flex w-full items-center gap-2 rounded-xl border border-accent/30 bg-accent-soft px-4 py-2.5 text-left text-[12.5px] font-medium text-accent transition-all hover:border-accent/50"
|
|
266
|
+
>
|
|
267
|
+
<KeyRound size={14} className="shrink-0" />
|
|
268
|
+
<span className="flex-1">{connected ? 'Choose a model to start' : 'Connect a provider to start'}</span>
|
|
269
|
+
<span className="text-[11px] font-normal opacity-70">Open settings →</span>
|
|
270
|
+
</button>
|
|
271
|
+
)}
|
|
272
|
+
<Composer
|
|
273
|
+
value={draft}
|
|
274
|
+
onChange={setDraft}
|
|
275
|
+
onSend={send}
|
|
276
|
+
onStop={() => undefined}
|
|
277
|
+
onCommand={handleCommand}
|
|
278
|
+
streaming={false}
|
|
279
|
+
hints
|
|
280
|
+
autoFocus
|
|
281
|
+
placeholder={connected ? 'How can I help you today?' : 'Add a provider key to start'}
|
|
282
|
+
pixleMood={connected ? 'idle' : 'sleepy'}
|
|
283
|
+
/>
|
|
284
|
+
<div className="stagger mt-5 grid grid-cols-2 gap-2 sm:grid-cols-3">
|
|
285
|
+
{STARTERS.slice(0, 6).map((starter) => (
|
|
286
|
+
<button
|
|
287
|
+
key={starter.label}
|
|
288
|
+
type="button"
|
|
289
|
+
onClick={() => useStarter(starter.prompt)}
|
|
290
|
+
className="card-lift group/starter flex items-center gap-2.5 rounded-xl border border-line bg-panel px-3 py-2.5 text-left text-[12.5px] font-medium text-muted hover:text-ink"
|
|
291
|
+
>
|
|
292
|
+
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-raised text-faint transition-colors duration-200 group-hover/starter:bg-accent-soft group-hover/starter:text-accent">
|
|
293
|
+
<starter.icon size={14} strokeWidth={1.8} />
|
|
294
|
+
</span>
|
|
295
|
+
<span className="min-w-0 truncate">{starter.label}</span>
|
|
296
|
+
</button>
|
|
297
|
+
))}
|
|
298
|
+
</div>
|
|
299
|
+
<div className="mt-2.5 flex items-center justify-center gap-3 px-1 text-[11px] text-faint">
|
|
300
|
+
<span className="truncate">
|
|
301
|
+
{workspace.available ? workspace.name : 'No folder open'}
|
|
302
|
+
{workspace.available && git.available && git.branch ? ` · ${git.branch}` : ''}
|
|
303
|
+
</span>
|
|
304
|
+
<span aria-hidden="true">·</span>
|
|
305
|
+
<span className="shrink-0">
|
|
306
|
+
{activeSkills} skill{activeSkills === 1 ? '' : 's'} ·{' '}
|
|
307
|
+
{!connected ? 'Provider needed' : !selectedModel ? 'Model needed' : 'Ready'}
|
|
308
|
+
</span>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<div className="mt-10 animate-rise [animation-delay:420ms]">
|
|
313
|
+
<HomeDashboard />
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
) : (
|
|
318
|
+
<>
|
|
319
|
+
<div className="relative min-h-0 flex-1">
|
|
320
|
+
{view === 'trajectory' ? (
|
|
321
|
+
<TrajectoryView session={session} />
|
|
322
|
+
) : (
|
|
323
|
+
<>
|
|
324
|
+
<div
|
|
325
|
+
ref={scrollRef}
|
|
326
|
+
onScroll={(event) => {
|
|
327
|
+
const node = event.currentTarget
|
|
328
|
+
const distance = node.scrollHeight - node.scrollTop - node.clientHeight
|
|
329
|
+
setAtBottom(distance < 90)
|
|
330
|
+
}}
|
|
331
|
+
className="thin-scroll h-full overflow-y-auto"
|
|
332
|
+
>
|
|
333
|
+
<div className="mx-auto w-full max-w-[760px] px-4 pb-12 pt-8 sm:px-6 sm:pt-10">
|
|
334
|
+
{turns.map((turn, index) => (
|
|
335
|
+
<div key={turn.id} className={index === 0 ? 'animate-rise' : 'mt-10 animate-rise'}>
|
|
336
|
+
<TurnBlock
|
|
337
|
+
turn={turn}
|
|
338
|
+
sessionId={session.id}
|
|
339
|
+
streaming={streaming}
|
|
340
|
+
lastAssistantId={lastAssistantId}
|
|
341
|
+
activeTurn={streaming && index === turns.length - 1}
|
|
342
|
+
/>
|
|
343
|
+
</div>
|
|
344
|
+
))}
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
{!atBottom && (
|
|
349
|
+
<button
|
|
350
|
+
type="button"
|
|
351
|
+
onClick={() => {
|
|
352
|
+
setAtBottom(true)
|
|
353
|
+
scrollToBottom('smooth')
|
|
354
|
+
}}
|
|
355
|
+
className="surface-lift absolute bottom-4 left-1/2 -translate-x-1/2 animate-scalein rounded-full border border-line px-3.5 py-1.5 text-[11.5px] font-medium text-muted shadow-composer transition-colors hover:text-ink"
|
|
356
|
+
>
|
|
357
|
+
<span className="inline-flex items-center gap-1.5">
|
|
358
|
+
<ArrowUp size={11} />
|
|
359
|
+
Jump to latest
|
|
360
|
+
</span>
|
|
361
|
+
</button>
|
|
362
|
+
)}
|
|
363
|
+
</>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
<div className="shrink-0 bg-panel px-3 pb-3 pt-1 sm:px-6 sm:pb-4">
|
|
368
|
+
<div className="mx-auto w-full max-w-[760px]">
|
|
369
|
+
<Composer
|
|
370
|
+
value={draft}
|
|
371
|
+
onChange={setDraft}
|
|
372
|
+
onSend={send}
|
|
373
|
+
onStop={() => session && stopRun(session.id)}
|
|
374
|
+
onCommand={handleCommand}
|
|
375
|
+
streaming={streaming}
|
|
376
|
+
placeholder={connected ? 'How can I help you today?' : 'Add a provider key to start'}
|
|
377
|
+
contextPct={context.pct}
|
|
378
|
+
contextTitle={`Context: ~${formatTokens(context.used)} / ${formatTokens(context.limit)} tokens`}
|
|
379
|
+
pixleMood={mood}
|
|
380
|
+
/>
|
|
381
|
+
<div className="mt-2 flex items-center justify-between gap-3 px-2 text-[11px] text-faint">
|
|
382
|
+
<span className="flex items-center gap-1">
|
|
383
|
+
{settings.mode === 'plan' ? <ListChecks size={10} /> : <Sparkles size={10} />}
|
|
384
|
+
{settings.mode === 'plan'
|
|
385
|
+
? 'Plan mode · read-only tools'
|
|
386
|
+
: activeSkills > 0
|
|
387
|
+
? `${activeSkills} skill${activeSkills === 1 ? '' : 's'} loaded`
|
|
388
|
+
: 'Build mode · full tools'}
|
|
389
|
+
</span>
|
|
390
|
+
<span className="hidden sm:inline">
|
|
391
|
+
{workspace.available ? `Working in ${workspace.name}` : 'No workspace open'}
|
|
392
|
+
</span>
|
|
393
|
+
<span className="sm:hidden">
|
|
394
|
+
{settings.tools.shell.approval === 'ask' ? 'Shell asks first' : 'Shell auto-runs'}
|
|
395
|
+
</span>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</>
|
|
400
|
+
)}
|
|
401
|
+
</div>
|
|
402
|
+
)
|
|
403
|
+
}
|