@bibobonking/opensol 0.0.0-stage → 3.0.2
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/server/README.md +75 -0
- package/server/daemon.mjs +1082 -0
- package/server/lib/checkpoints.mjs +124 -0
- package/server/lib/checkpoints.test.mjs +110 -0
- package/server/lib/deploy.mjs +436 -0
- package/server/lib/http.mjs +237 -0
- package/server/lib/plugins.mjs +321 -0
- package/server/lib/plugins.test.mjs +140 -0
- package/server/lib/providers.mjs +594 -0
- package/server/lib/store.mjs +491 -0
- package/server/lib/tools.mjs +990 -0
- package/server/opensol.mjs +202 -0
- package/server/package.json +42 -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,504 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Check,
|
|
4
|
+
Download,
|
|
5
|
+
FileText,
|
|
6
|
+
Folder,
|
|
7
|
+
LoaderCircle,
|
|
8
|
+
MoreHorizontal,
|
|
9
|
+
Package,
|
|
10
|
+
PanelLeftClose,
|
|
11
|
+
Pencil,
|
|
12
|
+
Plus,
|
|
13
|
+
Rocket,
|
|
14
|
+
Search,
|
|
15
|
+
Settings,
|
|
16
|
+
Sparkles,
|
|
17
|
+
Trash2,
|
|
18
|
+
X,
|
|
19
|
+
} from 'lucide-react'
|
|
20
|
+
import { useChats } from '../store/chats'
|
|
21
|
+
import { hasCredentials, useSettings } from '../store/settings'
|
|
22
|
+
import { useRuntime, useUi } from '../store/ui'
|
|
23
|
+
import { useWorkspace } from '../store/workspace'
|
|
24
|
+
import { stopRun } from '../agent'
|
|
25
|
+
import { getProviderName } from '../data/providers'
|
|
26
|
+
import { cn, downloadText, formatRelativeTime, groupChatsByDate } from '../lib/utils'
|
|
27
|
+
import { LogoLockup } from './Logo'
|
|
28
|
+
|
|
29
|
+
function shortTime(timestamp: number) {
|
|
30
|
+
return new Date(timestamp).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ChatMenu {
|
|
34
|
+
id: string
|
|
35
|
+
x: number
|
|
36
|
+
y: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function Sidebar() {
|
|
40
|
+
const sessions = useChats((state) => state.sessions)
|
|
41
|
+
const activeId = useChats((state) => state.activeId)
|
|
42
|
+
const setActive = useChats((state) => state.setActive)
|
|
43
|
+
const newChat = useChats((state) => state.newChat)
|
|
44
|
+
const renameSession = useChats((state) => state.renameSession)
|
|
45
|
+
const deleteSession = useChats((state) => state.deleteSession)
|
|
46
|
+
const settings = useSettings()
|
|
47
|
+
const workspace = useWorkspace((state) => state.info)
|
|
48
|
+
const addWorkspace = useWorkspace((state) => state.addWorkspace)
|
|
49
|
+
const sidebarCollapsed = useUi((state) => state.sidebarCollapsed)
|
|
50
|
+
const toggleSidebar = useUi((state) => state.toggleSidebar)
|
|
51
|
+
const mobileOpen = useUi((state) => state.mobileSidebarOpen)
|
|
52
|
+
const setMobileOpen = useUi((state) => state.setMobileSidebarOpen)
|
|
53
|
+
const openSettings = useUi((state) => state.openSettings)
|
|
54
|
+
const setWorkspaceOpen = useUi((state) => state.setWorkspaceOpen)
|
|
55
|
+
const setDeployOpen = useUi((state) => state.setDeployOpen)
|
|
56
|
+
const askConfirm = useUi((state) => state.askConfirm)
|
|
57
|
+
const notify = useUi((state) => state.notify)
|
|
58
|
+
const focusComposer = useUi((state) => state.focusComposer)
|
|
59
|
+
const streaming = useRuntime((state) => state.streaming)
|
|
60
|
+
|
|
61
|
+
const [query, setQuery] = useState('')
|
|
62
|
+
const [menu, setMenu] = useState<ChatMenu | null>(null)
|
|
63
|
+
const [editingId, setEditingId] = useState<string | null>(null)
|
|
64
|
+
const [draftTitle, setDraftTitle] = useState('')
|
|
65
|
+
const [switching, setSwitching] = useState('')
|
|
66
|
+
|
|
67
|
+
const filtered = useMemo(() => {
|
|
68
|
+
const needle = query.trim().toLowerCase()
|
|
69
|
+
if (!needle) return sessions
|
|
70
|
+
return sessions.filter((session) => session.title.toLowerCase().includes(needle))
|
|
71
|
+
}, [sessions, query])
|
|
72
|
+
const groups = useMemo(() => groupChatsByDate(filtered), [filtered])
|
|
73
|
+
const workspaces = useMemo(
|
|
74
|
+
() => [...workspace.workspaces].sort((a, b) => b.lastOpened - a.lastOpened),
|
|
75
|
+
[workspace.workspaces],
|
|
76
|
+
)
|
|
77
|
+
const connected = hasCredentials(settings)
|
|
78
|
+
const profileName =
|
|
79
|
+
(settings.daemon?.home ?? '').split(/[\\/]/).filter(Boolean).pop() || workspace.name || 'Local'
|
|
80
|
+
|
|
81
|
+
const startNewChat = () => {
|
|
82
|
+
newChat()
|
|
83
|
+
setMobileOpen(false)
|
|
84
|
+
setMenu(null)
|
|
85
|
+
focusComposer()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const openChat = (id: string) => {
|
|
89
|
+
setActive(id)
|
|
90
|
+
setMobileOpen(false)
|
|
91
|
+
setMenu(null)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const switchWorkspace = async (path: string) => {
|
|
95
|
+
if (path === workspace.root) {
|
|
96
|
+
setWorkspaceOpen(true)
|
|
97
|
+
setMobileOpen(false)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
setSwitching(path)
|
|
101
|
+
try {
|
|
102
|
+
await addWorkspace(path)
|
|
103
|
+
setMobileOpen(false)
|
|
104
|
+
} catch (error) {
|
|
105
|
+
notify(error instanceof Error ? error.message : 'Could not open that folder.', 'error')
|
|
106
|
+
} finally {
|
|
107
|
+
setSwitching('')
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const beginRename = (id: string, title: string) => {
|
|
112
|
+
setEditingId(id)
|
|
113
|
+
setDraftTitle(title)
|
|
114
|
+
setMenu(null)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const commitRename = () => {
|
|
118
|
+
if (!editingId) return
|
|
119
|
+
if (draftTitle.trim()) renameSession(editingId, draftTitle)
|
|
120
|
+
setEditingId(null)
|
|
121
|
+
setDraftTitle('')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const removeChat = async (id: string, title: string) => {
|
|
125
|
+
setMenu(null)
|
|
126
|
+
const confirmed = await askConfirm({
|
|
127
|
+
title: 'Delete this conversation?',
|
|
128
|
+
body: `“${title}” and its messages will be removed from this computer. This cannot be undone.`,
|
|
129
|
+
confirmLabel: 'Delete conversation',
|
|
130
|
+
tone: 'danger',
|
|
131
|
+
})
|
|
132
|
+
if (!confirmed) return
|
|
133
|
+
stopRun(id)
|
|
134
|
+
deleteSession(id)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const exportMarkdown = (id: string) => {
|
|
138
|
+
const session = sessions.find((item) => item.id === id)
|
|
139
|
+
if (!session) return
|
|
140
|
+
setMenu(null)
|
|
141
|
+
const body = session.messages
|
|
142
|
+
.flatMap((message) => {
|
|
143
|
+
if (message.role === 'user') return [`## You\n\n${message.content}`]
|
|
144
|
+
if (message.role === 'assistant' && message.content.trim()) return [`## OpenSol\n\n${message.content.trim()}`]
|
|
145
|
+
return []
|
|
146
|
+
})
|
|
147
|
+
.join('\n\n')
|
|
148
|
+
downloadText(
|
|
149
|
+
`opensol-${session.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40) || 'conversation'}.md`,
|
|
150
|
+
`# ${session.title}\n\n${body}\n`,
|
|
151
|
+
'text/markdown',
|
|
152
|
+
)
|
|
153
|
+
notify('Conversation exported as Markdown.', 'success')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const exportChat = (id: string) => {
|
|
157
|
+
const session = sessions.find((item) => item.id === id)
|
|
158
|
+
if (!session) return
|
|
159
|
+
setMenu(null)
|
|
160
|
+
downloadText(
|
|
161
|
+
`opensol-${session.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40) || 'conversation'}.json`,
|
|
162
|
+
JSON.stringify({ app: 'OpenSol', exportedAt: new Date().toISOString(), session }, null, 2),
|
|
163
|
+
'application/json',
|
|
164
|
+
)
|
|
165
|
+
notify('Conversation exported.', 'success')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<aside
|
|
170
|
+
className={cn(
|
|
171
|
+
'fixed inset-y-0 left-0 z-50 flex h-dvh w-[264px] shrink-0 flex-col bg-canvas shadow-float transition-transform duration-300 ease-out lg:static lg:z-auto lg:shadow-none',
|
|
172
|
+
mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
|
|
173
|
+
sidebarCollapsed && 'lg:hidden',
|
|
174
|
+
)}
|
|
175
|
+
>
|
|
176
|
+
<div className="flex h-12 items-center gap-1 pl-3 pr-2">
|
|
177
|
+
<LogoLockup />
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
className="icon-button ml-auto hidden lg:inline-flex"
|
|
181
|
+
onClick={toggleSidebar}
|
|
182
|
+
title="Collapse sidebar (Ctrl B)"
|
|
183
|
+
aria-label="Collapse sidebar"
|
|
184
|
+
>
|
|
185
|
+
<PanelLeftClose size={16} strokeWidth={1.7} />
|
|
186
|
+
</button>
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
className="icon-button ml-auto lg:hidden"
|
|
190
|
+
onClick={() => setMobileOpen(false)}
|
|
191
|
+
aria-label="Close menu"
|
|
192
|
+
>
|
|
193
|
+
<X size={17} />
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<nav className="space-y-0.5 px-2 pt-1">
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
onClick={startNewChat}
|
|
201
|
+
className="group/new mb-2 flex h-9 w-full items-center gap-2 rounded-lg border border-line bg-panel px-2.5 text-left text-[13px] font-medium text-ink shadow-pill transition-all duration-150 hover:border-accent/40 hover:shadow-composer active:scale-[0.985]"
|
|
202
|
+
>
|
|
203
|
+
<Plus size={15} strokeWidth={2} className="text-accent transition-transform duration-300 group-hover/new:rotate-90" />
|
|
204
|
+
New chat
|
|
205
|
+
</button>
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
onClick={() => {
|
|
209
|
+
setMobileOpen(false)
|
|
210
|
+
openSettings('skills')
|
|
211
|
+
}}
|
|
212
|
+
className="nav-item"
|
|
213
|
+
>
|
|
214
|
+
<Sparkles size={15} strokeWidth={1.7} className="shrink-0 text-faint" />
|
|
215
|
+
Skills
|
|
216
|
+
</button>
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
onClick={() => {
|
|
220
|
+
setMobileOpen(false)
|
|
221
|
+
openSettings('plugins')
|
|
222
|
+
}}
|
|
223
|
+
className="nav-item"
|
|
224
|
+
>
|
|
225
|
+
<Package size={15} strokeWidth={1.7} className="shrink-0 text-faint" />
|
|
226
|
+
Plugins
|
|
227
|
+
</button>
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
onClick={() => {
|
|
231
|
+
setMobileOpen(false)
|
|
232
|
+
setDeployOpen(true)
|
|
233
|
+
}}
|
|
234
|
+
className="nav-item"
|
|
235
|
+
>
|
|
236
|
+
<Rocket size={15} strokeWidth={1.7} className="shrink-0 text-faint" />
|
|
237
|
+
Deploy
|
|
238
|
+
</button>
|
|
239
|
+
<div className="relative">
|
|
240
|
+
<Search
|
|
241
|
+
size={15}
|
|
242
|
+
strokeWidth={1.7}
|
|
243
|
+
className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-faint"
|
|
244
|
+
/>
|
|
245
|
+
<input
|
|
246
|
+
value={query}
|
|
247
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
248
|
+
placeholder="Search"
|
|
249
|
+
className="mt-1 h-8 w-full rounded-lg border border-transparent bg-transparent pl-[34px] pr-2.5 text-[13px] text-ink outline-none transition-colors placeholder:text-muted hover:bg-raised/80 focus:border-line focus:bg-panel"
|
|
250
|
+
aria-label="Search conversations"
|
|
251
|
+
/>
|
|
252
|
+
</div>
|
|
253
|
+
</nav>
|
|
254
|
+
|
|
255
|
+
<div className="thin-scroll min-h-0 flex-1 overflow-y-auto px-2 pb-2 pt-5">
|
|
256
|
+
<div className="mb-5">
|
|
257
|
+
<div className="flex items-center justify-between px-2.5 pb-1.5">
|
|
258
|
+
<p className="eyebrow">Workspaces</p>
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
onClick={() => {
|
|
262
|
+
setMobileOpen(false)
|
|
263
|
+
setWorkspaceOpen(true)
|
|
264
|
+
}}
|
|
265
|
+
className="flex h-6 w-6 items-center justify-center rounded-md text-faint transition-colors hover:bg-raised hover:text-ink"
|
|
266
|
+
title="Open another folder"
|
|
267
|
+
aria-label="Open another folder"
|
|
268
|
+
>
|
|
269
|
+
<Plus size={13} />
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
{workspaces.length === 0 ? (
|
|
273
|
+
<button
|
|
274
|
+
type="button"
|
|
275
|
+
onClick={() => {
|
|
276
|
+
setMobileOpen(false)
|
|
277
|
+
setWorkspaceOpen(true)
|
|
278
|
+
}}
|
|
279
|
+
className="flex w-full items-center gap-2 rounded-lg border border-dashed border-line px-2.5 py-2.5 text-left text-[13px] text-muted transition-colors hover:border-faint/50 hover:text-ink"
|
|
280
|
+
>
|
|
281
|
+
<Folder size={15} strokeWidth={1.6} className="text-faint" />
|
|
282
|
+
Open a project folder
|
|
283
|
+
</button>
|
|
284
|
+
) : (
|
|
285
|
+
<div className="space-y-0.5">
|
|
286
|
+
{workspaces.map((entry) => {
|
|
287
|
+
const active = entry.path === workspace.root
|
|
288
|
+
return (
|
|
289
|
+
<button
|
|
290
|
+
key={entry.path}
|
|
291
|
+
type="button"
|
|
292
|
+
onClick={() => void switchWorkspace(entry.path)}
|
|
293
|
+
title={entry.path}
|
|
294
|
+
className={cn(
|
|
295
|
+
'flex h-8 w-full items-center gap-2.5 rounded-lg px-2.5 text-left text-[13px] transition-all duration-150',
|
|
296
|
+
active ? 'bg-raised text-ink' : 'text-muted hover:bg-raised/80 hover:text-ink',
|
|
297
|
+
)}
|
|
298
|
+
>
|
|
299
|
+
<Folder size={15} strokeWidth={1.6} className={cn('shrink-0', active ? 'text-accent' : 'text-faint')} />
|
|
300
|
+
<span className="min-w-0 flex-1 truncate">{entry.name}</span>
|
|
301
|
+
{switching === entry.path ? (
|
|
302
|
+
<LoaderCircle size={11} className="shrink-0 animate-spin text-faint" />
|
|
303
|
+
) : active ? (
|
|
304
|
+
<Check size={11} className="shrink-0 text-accent" />
|
|
305
|
+
) : (
|
|
306
|
+
<span className="shrink-0 text-[10px] tabular-nums text-faint">
|
|
307
|
+
{formatRelativeTime(entry.lastOpened)}
|
|
308
|
+
</span>
|
|
309
|
+
)}
|
|
310
|
+
</button>
|
|
311
|
+
)
|
|
312
|
+
})}
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div>
|
|
318
|
+
<div className="flex items-center justify-between px-2.5 pb-1.5">
|
|
319
|
+
<p className="eyebrow">Recents</p>
|
|
320
|
+
<span className="text-[11px] tabular-nums text-faint">{filtered.length}</span>
|
|
321
|
+
</div>
|
|
322
|
+
{groups.length === 0 && (
|
|
323
|
+
<div className="px-2 py-7 text-center">
|
|
324
|
+
<p className="text-[13px] font-medium text-muted">
|
|
325
|
+
{query ? 'No conversations match' : 'No conversations yet'}
|
|
326
|
+
</p>
|
|
327
|
+
<p className="mt-1 text-[11.5px] leading-relaxed text-faint">
|
|
328
|
+
{query ? 'Try a shorter search.' : 'Start with a task, a question or a file.'}
|
|
329
|
+
</p>
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
{groups.map((group, gi) => (
|
|
333
|
+
<div key={group.label} className="mb-3">
|
|
334
|
+
<p className="px-2.5 pb-1 pt-1 text-[11px] font-medium text-faint/80">{group.label}</p>
|
|
335
|
+
<div className="space-y-0.5">
|
|
336
|
+
{group.items.map((session, si) => (
|
|
337
|
+
<div key={session.id} className="group/item relative animate-slideleft" style={{ animationDelay: `${Math.min(gi * 40 + si * 18, 320)}ms` }}>
|
|
338
|
+
{editingId === session.id ? (
|
|
339
|
+
<input
|
|
340
|
+
value={draftTitle}
|
|
341
|
+
onChange={(event) => setDraftTitle(event.target.value)}
|
|
342
|
+
onBlur={commitRename}
|
|
343
|
+
onKeyDown={(event) => {
|
|
344
|
+
if (event.key === 'Enter') commitRename()
|
|
345
|
+
if (event.key === 'Escape') {
|
|
346
|
+
setEditingId(null)
|
|
347
|
+
setDraftTitle('')
|
|
348
|
+
}
|
|
349
|
+
}}
|
|
350
|
+
autoFocus
|
|
351
|
+
className="h-8 w-full rounded-lg border border-accent/50 bg-panel px-2.5 text-[13px] text-ink outline-none"
|
|
352
|
+
aria-label="Rename conversation"
|
|
353
|
+
/>
|
|
354
|
+
) : (
|
|
355
|
+
<>
|
|
356
|
+
<button
|
|
357
|
+
type="button"
|
|
358
|
+
onClick={() => openChat(session.id)}
|
|
359
|
+
onDoubleClick={() => beginRename(session.id, session.title)}
|
|
360
|
+
title={`${session.title} · ${formatRelativeTime(session.updatedAt)}`}
|
|
361
|
+
className={cn('sidebar-item pr-9', activeId === session.id && 'sidebar-item-active')}
|
|
362
|
+
>
|
|
363
|
+
<span className="min-w-0 flex-1 truncate">{session.title}</span>
|
|
364
|
+
{streaming[session.id] && (
|
|
365
|
+
<span
|
|
366
|
+
className="flex shrink-0 items-center gap-1 text-[10.5px] font-medium text-accent group-hover/item:hidden"
|
|
367
|
+
title="Agent is running"
|
|
368
|
+
>
|
|
369
|
+
<span className="h-1.5 w-1.5 animate-breathe rounded-full bg-accent" />
|
|
370
|
+
running
|
|
371
|
+
</span>
|
|
372
|
+
)}
|
|
373
|
+
</button>
|
|
374
|
+
<button
|
|
375
|
+
type="button"
|
|
376
|
+
onClick={(event) => {
|
|
377
|
+
event.stopPropagation()
|
|
378
|
+
if (menu?.id === session.id) {
|
|
379
|
+
setMenu(null)
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
383
|
+
const width = 176
|
|
384
|
+
const height = 138
|
|
385
|
+
const x = Math.max(8, Math.min(rect.right - width, window.innerWidth - width - 8))
|
|
386
|
+
const y =
|
|
387
|
+
rect.bottom + height + 8 > window.innerHeight
|
|
388
|
+
? Math.max(8, rect.top - height - 4)
|
|
389
|
+
: rect.bottom + 4
|
|
390
|
+
setMenu({ id: session.id, x, y })
|
|
391
|
+
}}
|
|
392
|
+
className={cn(
|
|
393
|
+
'absolute right-1 top-1/2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-md text-faint transition-all hover:bg-panel hover:text-ink',
|
|
394
|
+
menu?.id === session.id
|
|
395
|
+
? 'flex bg-panel text-ink opacity-100'
|
|
396
|
+
: 'opacity-0 group-hover/item:opacity-100 focus:opacity-100',
|
|
397
|
+
)}
|
|
398
|
+
aria-label={`Actions for ${session.title}`}
|
|
399
|
+
title="Conversation actions"
|
|
400
|
+
>
|
|
401
|
+
<MoreHorizontal size={14} />
|
|
402
|
+
</button>
|
|
403
|
+
</>
|
|
404
|
+
)}
|
|
405
|
+
</div>
|
|
406
|
+
))}
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
))}
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
<div className="p-2">
|
|
414
|
+
<div className="flex items-center gap-2.5 rounded-lg px-1.5 py-1.5 transition-colors hover:bg-raised/80">
|
|
415
|
+
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-ink text-[12px] font-semibold text-panel">
|
|
416
|
+
{profileName.charAt(0).toUpperCase()}
|
|
417
|
+
</div>
|
|
418
|
+
<button
|
|
419
|
+
type="button"
|
|
420
|
+
onClick={() => {
|
|
421
|
+
setMobileOpen(false)
|
|
422
|
+
openSettings('models')
|
|
423
|
+
}}
|
|
424
|
+
className="min-w-0 flex-1 text-left"
|
|
425
|
+
>
|
|
426
|
+
<p className="truncate text-[13px] font-medium leading-tight text-ink">{profileName}</p>
|
|
427
|
+
<p className="mt-0.5 truncate text-[11px] leading-tight text-faint">
|
|
428
|
+
{connected
|
|
429
|
+
? `${getProviderName(settings.provider)}${settings.provider === 'ollama' ? ' · local' : ' · your key'}`
|
|
430
|
+
: 'Add a provider key'}
|
|
431
|
+
</p>
|
|
432
|
+
</button>
|
|
433
|
+
<button
|
|
434
|
+
type="button"
|
|
435
|
+
className="icon-button h-8 w-8"
|
|
436
|
+
onClick={() => {
|
|
437
|
+
setMobileOpen(false)
|
|
438
|
+
openSettings('appearance')
|
|
439
|
+
}}
|
|
440
|
+
title="Preferences"
|
|
441
|
+
aria-label="Preferences"
|
|
442
|
+
>
|
|
443
|
+
<Settings size={16} strokeWidth={1.7} />
|
|
444
|
+
</button>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
{menu && (
|
|
449
|
+
<>
|
|
450
|
+
<button
|
|
451
|
+
type="button"
|
|
452
|
+
tabIndex={-1}
|
|
453
|
+
aria-label="Close conversation menu"
|
|
454
|
+
className="fixed inset-0 z-[60] cursor-default bg-transparent"
|
|
455
|
+
onClick={() => setMenu(null)}
|
|
456
|
+
/>
|
|
457
|
+
<div
|
|
458
|
+
className="surface-lift fixed z-[70] w-[184px] overflow-hidden rounded-xl border border-line p-1 shadow-float"
|
|
459
|
+
style={{ left: menu.x, top: menu.y }}
|
|
460
|
+
>
|
|
461
|
+
{(() => {
|
|
462
|
+
const session = sessions.find((item) => item.id === menu.id)
|
|
463
|
+
if (!session) return null
|
|
464
|
+
return (
|
|
465
|
+
<>
|
|
466
|
+
<button
|
|
467
|
+
type="button"
|
|
468
|
+
onClick={() => beginRename(session.id, session.title)}
|
|
469
|
+
className="flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-[11.5px] text-muted hover:bg-raised hover:text-ink"
|
|
470
|
+
>
|
|
471
|
+
<Pencil size={12} /> Rename
|
|
472
|
+
</button>
|
|
473
|
+
<button
|
|
474
|
+
type="button"
|
|
475
|
+
onClick={() => exportChat(session.id)}
|
|
476
|
+
className="flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-[11.5px] text-muted hover:bg-raised hover:text-ink"
|
|
477
|
+
>
|
|
478
|
+
<Download size={12} /> Export JSON
|
|
479
|
+
</button>
|
|
480
|
+
<button
|
|
481
|
+
type="button"
|
|
482
|
+
onClick={() => exportMarkdown(session.id)}
|
|
483
|
+
className="flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-[11.5px] text-muted hover:bg-raised hover:text-ink"
|
|
484
|
+
>
|
|
485
|
+
<FileText size={12} /> Export Markdown
|
|
486
|
+
</button>
|
|
487
|
+
<div className="my-1 border-t border-line" />
|
|
488
|
+
<button
|
|
489
|
+
type="button"
|
|
490
|
+
onClick={() => void removeChat(session.id, session.title)}
|
|
491
|
+
className="flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-[11.5px] text-danger hover:bg-danger/[0.06]"
|
|
492
|
+
>
|
|
493
|
+
<Trash2 size={12} /> Delete
|
|
494
|
+
</button>
|
|
495
|
+
<p className="px-2 pb-1 pt-1.5 text-[9px] text-faint">Updated {shortTime(session.updatedAt)}</p>
|
|
496
|
+
</>
|
|
497
|
+
)
|
|
498
|
+
})()}
|
|
499
|
+
</div>
|
|
500
|
+
</>
|
|
501
|
+
)}
|
|
502
|
+
</aside>
|
|
503
|
+
)
|
|
504
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { AlertCircle, RefreshCw } from 'lucide-react'
|
|
3
|
+
import { BrandFooter } from './Logo'
|
|
4
|
+
import { cn } from '../lib/utils'
|
|
5
|
+
|
|
6
|
+
const STATUS = ['Waking the local daemon', 'Unlocking your keys', 'Loading workspace', 'Almost there']
|
|
7
|
+
|
|
8
|
+
const RAYS_PLUS = ['M32 7v8', 'M32 49v8', 'M7 32h8', 'M49 32h8']
|
|
9
|
+
const RAYS_DIAG = ['M14.3 14.3l5.5 5.5', 'M44.2 44.2l5.5 5.5', 'M14.3 49.7l5.5-5.5', 'M44.2 19.8l5.5-5.5']
|
|
10
|
+
|
|
11
|
+
function SunDraw({ size = 64 }: { size?: number }) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="relative" style={{ width: size * 1.7, height: size * 1.7 }}>
|
|
14
|
+
<span className="absolute inset-0 m-auto animate-ping rounded-full bg-accent/20" style={{ width: size * 0.7, height: size * 0.7 }} />
|
|
15
|
+
<span
|
|
16
|
+
className="absolute inset-0 m-auto rounded-full bg-accent/10 blur-2xl"
|
|
17
|
+
style={{ width: size * 1.4, height: size * 1.4 }}
|
|
18
|
+
/>
|
|
19
|
+
<svg viewBox="0 0 64 64" width={size} height={size} fill="none" className="splash-sun absolute inset-0 m-auto text-accent">
|
|
20
|
+
<circle className="core" cx="32" cy="32" r="12" fill="currentColor" />
|
|
21
|
+
<g className="rays" stroke="currentColor" strokeWidth="4.4" strokeLinecap="round">
|
|
22
|
+
{RAYS_PLUS.map((d, i) => (
|
|
23
|
+
<path key={d} d={d} className="ray" pathLength={8} style={{ animationDelay: `${0.35 + i * 0.07}s` }} />
|
|
24
|
+
))}
|
|
25
|
+
{RAYS_DIAG.map((d, i) => (
|
|
26
|
+
<path key={d} d={d} className="ray" pathLength={8} opacity={0.55} style={{ animationDelay: `${0.65 + i * 0.07}s` }} />
|
|
27
|
+
))}
|
|
28
|
+
</g>
|
|
29
|
+
</svg>
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Splash({
|
|
35
|
+
phase,
|
|
36
|
+
error,
|
|
37
|
+
leaving,
|
|
38
|
+
onRetry,
|
|
39
|
+
}: {
|
|
40
|
+
phase: 'idle' | 'loading' | 'ready' | 'error'
|
|
41
|
+
error: string
|
|
42
|
+
leaving: boolean
|
|
43
|
+
onRetry: () => void
|
|
44
|
+
}) {
|
|
45
|
+
const [step, setStep] = useState(0)
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (phase === 'error') return
|
|
48
|
+
const timer = window.setInterval(() => setStep((value) => Math.min(STATUS.length - 1, value + 1)), 650)
|
|
49
|
+
return () => window.clearInterval(timer)
|
|
50
|
+
}, [phase])
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className={cn('splash', leaving && 'is-leaving')} role="status" aria-live="polite">
|
|
54
|
+
<div className="hero-wash pointer-events-none absolute inset-0" aria-hidden="true" />
|
|
55
|
+
<div className="relative flex w-full max-w-[400px] flex-col items-center px-6 text-center">
|
|
56
|
+
<SunDraw />
|
|
57
|
+
<h1 className="splash-word -mt-1 text-[26px] font-semibold leading-none tracking-[-0.04em] text-ink" aria-label="OpenSol">
|
|
58
|
+
{'OpenSol'.split('').map((char, index) => (
|
|
59
|
+
<span key={index} style={{ animationDelay: `${0.5 + index * 0.05}s` }} className={index >= 4 ? 'text-accent' : undefined}>
|
|
60
|
+
{char}
|
|
61
|
+
</span>
|
|
62
|
+
))}
|
|
63
|
+
</h1>
|
|
64
|
+
<p className="mt-2 animate-fade text-[11px] font-medium uppercase tracking-[0.22em] text-faint [animation-delay:1s]">
|
|
65
|
+
by OpenSoft
|
|
66
|
+
</p>
|
|
67
|
+
|
|
68
|
+
{phase === 'error' ? (
|
|
69
|
+
<div className="mt-8 w-full animate-rise">
|
|
70
|
+
<div className="w-full rounded-xl border border-danger/25 bg-danger/[0.05] px-4 py-3 text-left">
|
|
71
|
+
<p className="flex items-center gap-1.5 text-[12.5px] font-medium text-ink">
|
|
72
|
+
<AlertCircle size={14} className="text-danger" />
|
|
73
|
+
The local daemon did not answer
|
|
74
|
+
</p>
|
|
75
|
+
<p className="mt-1.5 whitespace-pre-wrap break-words text-[11.5px] leading-relaxed text-muted">{error}</p>
|
|
76
|
+
</div>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={onRetry}
|
|
80
|
+
className="mt-4 inline-flex h-9 items-center gap-1.5 rounded-lg bg-ink px-4 text-[12.5px] font-medium text-panel transition-all hover:opacity-85 active:scale-95"
|
|
81
|
+
>
|
|
82
|
+
<RefreshCw size={13} />
|
|
83
|
+
Try again
|
|
84
|
+
</button>
|
|
85
|
+
<p className="mt-3 text-[11px] leading-relaxed text-faint">
|
|
86
|
+
Start OpenSol with <code className="rounded bg-raised px-1 py-0.5 font-mono text-[10.5px]">npm start</code> in the
|
|
87
|
+
OpenSol folder, then reload this page.
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
) : (
|
|
91
|
+
<div className="mt-9 flex animate-fade flex-col items-center gap-3 [animation-delay:0.8s]">
|
|
92
|
+
<div className="loading-bar h-[2px] w-32 rounded-full" />
|
|
93
|
+
<p key={step} className="animate-rise text-[12px] text-muted">
|
|
94
|
+
{STATUS[step]}…
|
|
95
|
+
</p>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
<div className="mt-12 animate-fade [animation-delay:1.2s]">
|
|
99
|
+
<BrandFooter />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|