@bibobonking/opensol 3.0.2 → 3.1.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.
- package/.github/workflows/build.yml +60 -8
- package/CHANGELOG.md +18 -0
- package/README.md +15 -12
- package/desktop/README.md +39 -0
- package/desktop/loading/index.html +36 -0
- package/desktop/src-tauri/Cargo.lock +4301 -0
- package/desktop/src-tauri/Cargo.toml +23 -0
- package/desktop/src-tauri/binaries/.gitkeep +0 -0
- package/desktop/src-tauri/build.rs +3 -0
- package/desktop/src-tauri/capabilities/default.json +7 -0
- package/desktop/src-tauri/icons/128x128.png +0 -0
- package/desktop/src-tauri/icons/128x128@2x.png +0 -0
- package/desktop/src-tauri/icons/32x32.png +0 -0
- package/desktop/src-tauri/icons/64x64.png +0 -0
- package/desktop/src-tauri/icons/Square107x107Logo.png +0 -0
- package/desktop/src-tauri/icons/Square142x142Logo.png +0 -0
- package/desktop/src-tauri/icons/Square150x150Logo.png +0 -0
- package/desktop/src-tauri/icons/Square284x284Logo.png +0 -0
- package/desktop/src-tauri/icons/Square30x30Logo.png +0 -0
- package/desktop/src-tauri/icons/Square310x310Logo.png +0 -0
- package/desktop/src-tauri/icons/Square44x44Logo.png +0 -0
- package/desktop/src-tauri/icons/Square71x71Logo.png +0 -0
- package/desktop/src-tauri/icons/Square89x89Logo.png +0 -0
- package/desktop/src-tauri/icons/StoreLogo.png +0 -0
- package/desktop/src-tauri/icons/icon.icns +0 -0
- package/desktop/src-tauri/icons/icon.ico +0 -0
- package/desktop/src-tauri/icons/icon.png +0 -0
- package/desktop/src-tauri/src/lib.rs +210 -0
- package/desktop/src-tauri/src/main.rs +6 -0
- package/desktop/src-tauri/tauri.conf.json +48 -0
- package/dist/assets/index-DdI1_iPD.css +1 -0
- package/dist/assets/index-egG5Bgfo.js +312 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/server/daemon.mjs +64 -2
- package/server/lib/mcp.mjs +532 -0
- package/server/lib/mcp.test.mjs +46 -0
- package/server/lib/store.mjs +22 -0
- package/server/lib/tools.mjs +59 -1
- package/src/agent/tools.ts +6 -5
- package/src/components/Composer.tsx +22 -38
- package/src/components/DiffView.tsx +98 -0
- package/src/components/McpTab.tsx +337 -0
- package/src/components/MessageItem.tsx +62 -13
- package/src/components/ModelDeck.tsx +652 -0
- package/src/components/SessionsTab.tsx +206 -0
- package/src/components/SettingsModal.tsx +106 -69
- package/src/components/Sidebar.tsx +78 -24
- package/src/components/ToolCallCard.tsx +29 -7
- package/src/components/WorkspacePanel.tsx +16 -1
- package/src/core/kernel.ts +2 -0
- package/src/core/mcp-tools.ts +63 -0
- package/src/core/plugins/tools/fs.ts +24 -3
- package/src/core/services/agent-loop.ts +3 -1
- package/src/core/services/approval.ts +2 -1
- package/src/core/services/titles.ts +135 -0
- package/src/core/services/tools.ts +11 -8
- package/src/core/types.ts +4 -0
- package/src/index.css +97 -1
- package/src/lib/daemon.ts +29 -0
- package/src/lib/diff.ts +62 -0
- package/src/lib/permissions.ts +6 -6
- package/src/store/chats.ts +9 -1
- package/src/store/mcp.ts +67 -0
- package/src/store/settings.ts +9 -6
- package/src/store/ui.ts +13 -0
- package/src/store/workspace.ts +26 -0
- package/src/types.ts +51 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/assets/index-BmN7pBji.css +0 -1
- package/dist/assets/index-DgNDjkOz.js +0 -301
- package/src/components/ModelPicker.tsx +0 -487
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react'
|
|
2
|
+
import { Check, MessagesSquare, Pencil, Plus, Sparkles, Trash2 } from 'lucide-react'
|
|
3
|
+
import { useChats } from '../store/chats'
|
|
4
|
+
import { useRuntime, useUi } from '../store/ui'
|
|
5
|
+
import { useWorkspace } from '../store/workspace'
|
|
6
|
+
import { stopRun } from '../agent'
|
|
7
|
+
import { kernel } from '../core/kernel'
|
|
8
|
+
import { cn, formatRelativeTime } from '../lib/utils'
|
|
9
|
+
import type { ChatSession } from '../types'
|
|
10
|
+
|
|
11
|
+
function messageCount(session: ChatSession) {
|
|
12
|
+
return session.messages.filter((message) => message.role === 'user').length
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function editedFiles(session: ChatSession) {
|
|
16
|
+
const files = new Set<string>()
|
|
17
|
+
for (const message of session.messages) {
|
|
18
|
+
if (message.role !== 'tool' || message.status !== 'done') continue
|
|
19
|
+
if (!['write_file', 'edit_file', 'rewrite_file'].includes(message.call.name)) continue
|
|
20
|
+
const path = message.call.args.path
|
|
21
|
+
if (typeof path === 'string') files.add(path)
|
|
22
|
+
}
|
|
23
|
+
return files.size
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Sessions that belong to the open workspace, with quick rename / AI title / delete. */
|
|
27
|
+
export function SessionsTab() {
|
|
28
|
+
const sessions = useChats((state) => state.sessions)
|
|
29
|
+
const activeId = useChats((state) => state.activeId)
|
|
30
|
+
const setActive = useChats((state) => state.setActive)
|
|
31
|
+
const newChat = useChats((state) => state.newChat)
|
|
32
|
+
const renameSession = useChats((state) => state.renameSession)
|
|
33
|
+
const deleteSession = useChats((state) => state.deleteSession)
|
|
34
|
+
const root = useWorkspace((state) => state.info.root)
|
|
35
|
+
const streaming = useRuntime((state) => state.streaming)
|
|
36
|
+
const titling = useRuntime((state) => state.titling)
|
|
37
|
+
const askConfirm = useUi((state) => state.askConfirm)
|
|
38
|
+
const notify = useUi((state) => state.notify)
|
|
39
|
+
const focusComposer = useUi((state) => state.focusComposer)
|
|
40
|
+
|
|
41
|
+
const [scope, setScope] = useState<'workspace' | 'all'>('workspace')
|
|
42
|
+
const [editingId, setEditingId] = useState<string | null>(null)
|
|
43
|
+
const [draft, setDraft] = useState('')
|
|
44
|
+
|
|
45
|
+
const shown = useMemo(() => {
|
|
46
|
+
const list = scope === 'all' ? sessions : sessions.filter((session) => session.workspace === root)
|
|
47
|
+
return [...list].sort((a, b) => b.updatedAt - a.updatedAt)
|
|
48
|
+
}, [sessions, scope, root])
|
|
49
|
+
const untagged = sessions.filter((session) => !session.workspace).length
|
|
50
|
+
|
|
51
|
+
const commit = () => {
|
|
52
|
+
if (editingId && draft.trim()) renameSession(editingId, draft)
|
|
53
|
+
setEditingId(null)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const remove = async (session: ChatSession) => {
|
|
57
|
+
const confirmed = await askConfirm({
|
|
58
|
+
title: 'Delete this session?',
|
|
59
|
+
body: `“${session.title}” and its messages will be removed from this computer. This cannot be undone.`,
|
|
60
|
+
confirmLabel: 'Delete session',
|
|
61
|
+
tone: 'danger',
|
|
62
|
+
})
|
|
63
|
+
if (!confirmed) return
|
|
64
|
+
stopRun(session.id)
|
|
65
|
+
deleteSession(session.id)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const aiName = async (id: string) => {
|
|
69
|
+
const title = await kernel.titles?.generate(id, { force: true })
|
|
70
|
+
if (!title) notify('The model could not suggest a title right now.', 'error')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
75
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-line px-3 py-2.5">
|
|
76
|
+
<div className="relative flex rounded-full bg-raised p-0.5 text-[10.5px] font-medium">
|
|
77
|
+
{(['workspace', 'all'] as const).map((item) => (
|
|
78
|
+
<button
|
|
79
|
+
key={item}
|
|
80
|
+
type="button"
|
|
81
|
+
onClick={() => setScope(item)}
|
|
82
|
+
className={cn(
|
|
83
|
+
'relative z-10 rounded-full px-2.5 py-1 transition-colors',
|
|
84
|
+
scope === item ? 'bg-panel text-ink shadow-pill' : 'text-muted hover:text-ink',
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
{item === 'workspace' ? 'This workspace' : `All · ${sessions.length}`}
|
|
88
|
+
</button>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
onClick={() => {
|
|
94
|
+
newChat()
|
|
95
|
+
focusComposer()
|
|
96
|
+
}}
|
|
97
|
+
className="ml-auto inline-flex h-7 items-center gap-1 rounded-full bg-ink px-2.5 text-[10.5px] font-medium text-panel transition-transform active:scale-95"
|
|
98
|
+
>
|
|
99
|
+
<Plus size={12} /> New
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div className="thin-scroll min-h-0 flex-1 overflow-y-auto p-2">
|
|
104
|
+
{shown.length === 0 ? (
|
|
105
|
+
<div className="px-4 py-10 text-center">
|
|
106
|
+
<MessagesSquare size={22} className="mx-auto text-faint" />
|
|
107
|
+
<p className="mt-2 text-[12px] font-medium text-ink">No sessions here yet</p>
|
|
108
|
+
<p className="mt-1 text-[10.5px] leading-relaxed text-faint">
|
|
109
|
+
{scope === 'workspace' && untagged > 0
|
|
110
|
+
? `${untagged} older session${untagged === 1 ? '' : 's'} were started before workspaces were tracked. Switch to “All” to see them.`
|
|
111
|
+
: 'Start a conversation and it shows up here.'}
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
) : (
|
|
115
|
+
shown.map((session, index) => {
|
|
116
|
+
const running = Boolean(streaming[session.id])
|
|
117
|
+
const edits = editedFiles(session)
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
key={session.id}
|
|
121
|
+
style={{ animationDelay: `${Math.min(index * 30, 300)}ms` }}
|
|
122
|
+
className={cn(
|
|
123
|
+
'session-card group/session animate-slideleft mb-1.5 rounded-xl border px-3 py-2.5 transition-all duration-200',
|
|
124
|
+
activeId === session.id ? 'border-accent/40 bg-accent-soft/60' : 'border-transparent hover:border-line hover:bg-raised/60',
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
{editingId === session.id ? (
|
|
128
|
+
<input
|
|
129
|
+
autoFocus
|
|
130
|
+
value={draft}
|
|
131
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
132
|
+
onBlur={commit}
|
|
133
|
+
onKeyDown={(event) => {
|
|
134
|
+
if (event.key === 'Enter') commit()
|
|
135
|
+
if (event.key === 'Escape') setEditingId(null)
|
|
136
|
+
}}
|
|
137
|
+
className="h-7 w-full rounded-md border border-accent/50 bg-panel px-2 text-[12px] text-ink outline-none"
|
|
138
|
+
aria-label="Rename session"
|
|
139
|
+
/>
|
|
140
|
+
) : (
|
|
141
|
+
<button type="button" onClick={() => setActive(session.id)} className="block w-full text-left">
|
|
142
|
+
<span className="flex items-center gap-2">
|
|
143
|
+
{activeId === session.id && <Check size={12} className="shrink-0 text-accent" />}
|
|
144
|
+
<span className={cn('min-w-0 flex-1 truncate text-[12.5px] font-medium text-ink', titling[session.id] && 'text-shimmer')}>
|
|
145
|
+
{session.title}
|
|
146
|
+
</span>
|
|
147
|
+
{running && (
|
|
148
|
+
<span className="flex shrink-0 items-center gap-1 text-[10px] font-medium text-accent">
|
|
149
|
+
<span className="h-1.5 w-1.5 animate-breathe rounded-full bg-accent" /> running
|
|
150
|
+
</span>
|
|
151
|
+
)}
|
|
152
|
+
</span>
|
|
153
|
+
<span className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[10px] text-faint">
|
|
154
|
+
<span>{formatRelativeTime(session.updatedAt)}</span>
|
|
155
|
+
<span>· {messageCount(session)} turn{messageCount(session) === 1 ? '' : 's'}</span>
|
|
156
|
+
{edits > 0 && <span>· {edits} file{edits === 1 ? '' : 's'} edited</span>}
|
|
157
|
+
{session.model && <span className="max-w-[120px] truncate font-mono">· {session.model}</span>}
|
|
158
|
+
{session.titleSource === 'ai' && (
|
|
159
|
+
<span className="inline-flex items-center gap-0.5 text-accent/80">
|
|
160
|
+
<Sparkles size={9} /> AI title
|
|
161
|
+
</span>
|
|
162
|
+
)}
|
|
163
|
+
</span>
|
|
164
|
+
</button>
|
|
165
|
+
)}
|
|
166
|
+
<div className="mt-1.5 flex items-center gap-0.5 opacity-0 transition-opacity group-hover/session:opacity-100 focus-within:opacity-100">
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
className="icon-button h-6 w-6"
|
|
170
|
+
onClick={() => {
|
|
171
|
+
setEditingId(session.id)
|
|
172
|
+
setDraft(session.title)
|
|
173
|
+
}}
|
|
174
|
+
title="Rename"
|
|
175
|
+
aria-label="Rename session"
|
|
176
|
+
>
|
|
177
|
+
<Pencil size={11} />
|
|
178
|
+
</button>
|
|
179
|
+
<button
|
|
180
|
+
type="button"
|
|
181
|
+
className="icon-button h-6 w-6"
|
|
182
|
+
onClick={() => void aiName(session.id)}
|
|
183
|
+
disabled={Boolean(titling[session.id]) || session.messages.length === 0}
|
|
184
|
+
title="Let the model name this session"
|
|
185
|
+
aria-label="Name session with AI"
|
|
186
|
+
>
|
|
187
|
+
<Sparkles size={11} />
|
|
188
|
+
</button>
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
className="icon-button h-6 w-6 hover:!text-danger"
|
|
192
|
+
onClick={() => void remove(session)}
|
|
193
|
+
title="Delete"
|
|
194
|
+
aria-label="Delete session"
|
|
195
|
+
>
|
|
196
|
+
<Trash2 size={11} />
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
)
|
|
201
|
+
})
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
Wrench,
|
|
40
40
|
X,
|
|
41
41
|
} from 'lucide-react'
|
|
42
|
-
import type { DeployProviderId, Effort, ModelInfo, ProviderId, SearchProvider, Skill,
|
|
42
|
+
import type { DeployProviderId, Effort, ModelInfo, ProviderId, SearchProvider, Skill, BuiltinToolName } from '../types'
|
|
43
43
|
import type { LucideIcon } from 'lucide-react'
|
|
44
44
|
import { EFFORT_LABELS, PROVIDERS, PROVIDER_MAP } from '../data/providers'
|
|
45
45
|
import { STARTER_SKILL } from '../data/skills'
|
|
@@ -61,6 +61,7 @@ import { PERMISSION_MODES, permissionMode, permissionTools } from '../lib/permis
|
|
|
61
61
|
import { cn, formatBytes, readFileAsText } from '../lib/utils'
|
|
62
62
|
import { LogoMark } from './Logo'
|
|
63
63
|
import { PluginManager } from './PluginManager'
|
|
64
|
+
import { McpTab } from './McpTab'
|
|
64
65
|
import { pluginRuntime } from '../plugins'
|
|
65
66
|
import { Modal, Segmented, Spinner, Switch } from './ui'
|
|
66
67
|
|
|
@@ -71,6 +72,7 @@ const TABS = [
|
|
|
71
72
|
{ id: 'search', label: 'Search', icon: Globe2, description: 'Give the agent current web results with your own key.' },
|
|
72
73
|
{ id: 'skills', label: 'Skills', icon: Sparkles, description: 'Instructions added to every request when enabled.' },
|
|
73
74
|
{ id: 'plugins', label: 'Plugins', icon: Package, description: 'Install local plugin packages, review what they ask for, and run them once you trust the code.' },
|
|
75
|
+
{ id: 'mcp', label: 'MCP', icon: Link2, description: 'Connect Model Context Protocol servers and choose which of their tools the agent may call.' },
|
|
74
76
|
{ id: 'deploy', label: 'Deploy', icon: Rocket, description: 'Hosting credentials and command-line tools.' },
|
|
75
77
|
{ id: 'appearance', label: 'Appearance', icon: Palette, description: 'Theme and interface preferences.' },
|
|
76
78
|
{ id: 'about', label: 'About', icon: Info, description: 'Local storage, history and reset controls.' },
|
|
@@ -100,15 +102,18 @@ function SettingsSection({
|
|
|
100
102
|
action?: ReactNode
|
|
101
103
|
}) {
|
|
102
104
|
return (
|
|
103
|
-
<section className="
|
|
105
|
+
<section className="settings-card mx-5 my-4 sm:mx-8">
|
|
104
106
|
<div className="flex items-start justify-between gap-3">
|
|
105
107
|
<div className="min-w-0">
|
|
106
|
-
<h3 className="font-display text-[14px] font-semibold tracking-[-0.01em] text-ink">
|
|
107
|
-
|
|
108
|
+
<h3 className="flex items-center gap-2 font-display text-[14px] font-semibold tracking-[-0.01em] text-ink">
|
|
109
|
+
<span className="h-3.5 w-[3px] rounded-full bg-accent" />
|
|
110
|
+
{title}
|
|
111
|
+
</h3>
|
|
112
|
+
{description && <p className="mt-1.5 max-w-[620px] text-[11.5px] leading-relaxed text-muted">{description}</p>}
|
|
108
113
|
</div>
|
|
109
114
|
{action && <div className="shrink-0">{action}</div>}
|
|
110
115
|
</div>
|
|
111
|
-
<div className="mt-
|
|
116
|
+
<div className="mt-4">{children}</div>
|
|
112
117
|
</section>
|
|
113
118
|
)
|
|
114
119
|
}
|
|
@@ -636,7 +641,7 @@ function ModelsTab() {
|
|
|
636
641
|
)
|
|
637
642
|
}
|
|
638
643
|
|
|
639
|
-
const TOOL_ORDER: Array<{ title: string; tools:
|
|
644
|
+
const TOOL_ORDER: Array<{ title: string; tools: BuiltinToolName[] }> = [
|
|
640
645
|
{
|
|
641
646
|
title: 'Workspace and shell',
|
|
642
647
|
tools: ['read_file', 'list_files', 'grep', 'write_file', 'edit_file', 'rewrite_file', 'shell', 'git'],
|
|
@@ -645,7 +650,7 @@ const TOOL_ORDER: Array<{ title: string; tools: ToolName[] }> = [
|
|
|
645
650
|
{ title: 'Delegation', tools: ['subagent'] },
|
|
646
651
|
]
|
|
647
652
|
|
|
648
|
-
const TOOL_ICONS: Record<
|
|
653
|
+
const TOOL_ICONS: Record<BuiltinToolName, LucideIcon> = {
|
|
649
654
|
shell: SquareTerminal,
|
|
650
655
|
read_file: FileText,
|
|
651
656
|
list_files: FolderTree,
|
|
@@ -659,7 +664,7 @@ const TOOL_ICONS: Record<ToolName, LucideIcon> = {
|
|
|
659
664
|
subagent: Bot,
|
|
660
665
|
}
|
|
661
666
|
|
|
662
|
-
const TOOL_COPY: Record<
|
|
667
|
+
const TOOL_COPY: Record<BuiltinToolName, { label: string; description: string }> = {
|
|
663
668
|
shell: { label: 'Shell', description: 'Run commands in the workspace root. Plan mode allows read-only commands only.' },
|
|
664
669
|
read_file: { label: 'Read file', description: 'Inspect a file on disk before changing it.' },
|
|
665
670
|
list_files: { label: 'List files', description: 'Explore the real workspace tree.' },
|
|
@@ -1604,6 +1609,18 @@ function GeneralTab() {
|
|
|
1604
1609
|
</select>
|
|
1605
1610
|
</div>
|
|
1606
1611
|
|
|
1612
|
+
<div className="settings-row">
|
|
1613
|
+
<div className="min-w-0">
|
|
1614
|
+
<p className="settings-label">Let AI name sessions</p>
|
|
1615
|
+
<p className="settings-hint">After the first answer, the model writes a short title for the session. Names you type yourself are never replaced.</p>
|
|
1616
|
+
</div>
|
|
1617
|
+
<Switch
|
|
1618
|
+
checked={settings.aiSessionTitles}
|
|
1619
|
+
onChange={(aiSessionTitles) => update({ aiSessionTitles })}
|
|
1620
|
+
label="Let AI name sessions"
|
|
1621
|
+
/>
|
|
1622
|
+
</div>
|
|
1623
|
+
|
|
1607
1624
|
<div className="settings-row">
|
|
1608
1625
|
<div className="min-w-0">
|
|
1609
1626
|
<p className="settings-label">Show token estimates</p>
|
|
@@ -1941,13 +1958,15 @@ export function SettingsModal() {
|
|
|
1941
1958
|
const notify = useUi((state) => state.notify)
|
|
1942
1959
|
const daemon = useSettings((state) => state.daemon)
|
|
1943
1960
|
const contentRef = useRef<HTMLDivElement>(null)
|
|
1961
|
+
const activeIndex = Math.max(0, TABS.findIndex((item) => item.id === tab))
|
|
1962
|
+
const current = TABS[activeIndex]
|
|
1944
1963
|
|
|
1945
1964
|
useEffect(() => {
|
|
1946
1965
|
if (open) contentRef.current?.scrollTo({ top: 0 })
|
|
1947
1966
|
}, [open, tab])
|
|
1948
1967
|
|
|
1949
1968
|
const copyConfig = async () => {
|
|
1950
|
-
const path = daemon?.dataDir ?? '~/.
|
|
1969
|
+
const path = daemon?.dataDir ?? '~/.opensol'
|
|
1951
1970
|
try {
|
|
1952
1971
|
await navigator.clipboard.writeText(path)
|
|
1953
1972
|
notify(`Copied the data folder path: ${path}`, 'success')
|
|
@@ -1961,85 +1980,103 @@ export function SettingsModal() {
|
|
|
1961
1980
|
open={open}
|
|
1962
1981
|
onClose={close}
|
|
1963
1982
|
labelledBy="settings-title"
|
|
1964
|
-
className="h-[
|
|
1983
|
+
className="settings-shell h-[94dvh] p-0 sm:h-[min(88dvh,780px)] sm:w-[min(1040px,96vw)] sm:!rounded-[28px]"
|
|
1965
1984
|
>
|
|
1966
|
-
<div className="flex min-h-0 flex-1 flex-col">
|
|
1967
|
-
<
|
|
1968
|
-
<
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
onClick={() => void copyConfig()}
|
|
1975
|
-
className="hidden h-8 items-center gap-1.5 rounded-lg border border-line bg-panel px-2.5 text-[11px] font-medium text-ink transition-colors hover:bg-raised sm:inline-flex"
|
|
1976
|
-
title={daemon?.dataDir ?? '~/.maxcode'}
|
|
1977
|
-
>
|
|
1978
|
-
<HardDrive size={12} />
|
|
1979
|
-
Data folder
|
|
1980
|
-
</button>
|
|
1981
|
-
<button type="button" className="icon-button" onClick={close} aria-label="Close settings">
|
|
1982
|
-
<X size={16} />
|
|
1983
|
-
</button>
|
|
1985
|
+
<div className="flex min-h-0 flex-1 flex-col sm:flex-row">
|
|
1986
|
+
<aside className="relative hidden w-[232px] shrink-0 flex-col bg-canvas sm:flex">
|
|
1987
|
+
<div className="settings-glow pointer-events-none absolute inset-x-0 top-0 h-48" />
|
|
1988
|
+
<div className="relative px-6 pb-4 pt-6">
|
|
1989
|
+
<p className="font-mono text-[10px] uppercase tracking-[0.24em] text-faint">OpenSol · v3.1.0</p>
|
|
1990
|
+
<h2 id="settings-title" className="mt-1.5 font-display text-[26px] font-semibold leading-none tracking-[-0.035em] text-ink">
|
|
1991
|
+
Settings
|
|
1992
|
+
</h2>
|
|
1984
1993
|
</div>
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
key={item.id}
|
|
1996
|
-
type="button"
|
|
1997
|
-
onClick={() => openSettings(item.id)}
|
|
1998
|
-
title={item.description}
|
|
1999
|
-
className={cn(
|
|
2000
|
-
'flex h-9 w-full items-center gap-2.5 rounded-lg px-2.5 text-left text-[12.5px] transition-colors',
|
|
2001
|
-
active ? 'bg-raised font-medium text-ink' : 'text-muted hover:bg-raised/60 hover:text-ink',
|
|
2002
|
-
)}
|
|
2003
|
-
>
|
|
2004
|
-
<Icon size={14} className={cn(active ? 'text-ink' : 'text-faint')} />
|
|
2005
|
-
{item.label}
|
|
2006
|
-
</button>
|
|
2007
|
-
)
|
|
2008
|
-
})}
|
|
2009
|
-
</nav>
|
|
2010
|
-
<div className="border-t border-line p-3">
|
|
2011
|
-
<p className="flex items-center gap-1.5 text-[9px] text-faint">
|
|
2012
|
-
<LockKeyhole size={10} />
|
|
2013
|
-
Keys encrypted on this machine
|
|
2014
|
-
</p>
|
|
2015
|
-
<p className="mt-1 text-[9px] text-faint">OpenSol 1.0 · OpenSoft</p>
|
|
2016
|
-
</div>
|
|
2017
|
-
</aside>
|
|
2018
|
-
|
|
2019
|
-
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
2020
|
-
<div className="thin-scroll flex shrink-0 gap-1 overflow-x-auto border-b border-line px-3 py-2 sm:hidden">
|
|
2021
|
-
{TABS.map((item) => (
|
|
1994
|
+
<nav className="relative flex-1 overflow-y-auto px-3 pb-3">
|
|
1995
|
+
<span
|
|
1996
|
+
aria-hidden
|
|
1997
|
+
className="settings-pill absolute left-3 right-3 top-0 h-10 rounded-2xl bg-panel shadow-pill ring-1 ring-line transition-transform duration-500 ease-spring"
|
|
1998
|
+
style={{ transform: `translateY(${activeIndex * 42}px)` }}
|
|
1999
|
+
/>
|
|
2000
|
+
{TABS.map((item, index) => {
|
|
2001
|
+
const Icon = item.icon
|
|
2002
|
+
const active = tab === item.id
|
|
2003
|
+
return (
|
|
2022
2004
|
<button
|
|
2023
2005
|
key={item.id}
|
|
2024
2006
|
type="button"
|
|
2025
2007
|
onClick={() => openSettings(item.id)}
|
|
2008
|
+
title={item.description}
|
|
2026
2009
|
className={cn(
|
|
2027
|
-
'
|
|
2028
|
-
|
|
2010
|
+
'group/tab relative mb-[2px] flex h-10 w-full items-center gap-3 rounded-2xl px-3 text-left text-[12.5px] transition-colors duration-200',
|
|
2011
|
+
active ? 'font-semibold text-ink' : 'text-muted hover:text-ink',
|
|
2029
2012
|
)}
|
|
2030
2013
|
>
|
|
2014
|
+
<span className={cn('w-4 font-mono text-[9.5px] tabular-nums transition-colors', active ? 'text-accent' : 'text-faint')}>
|
|
2015
|
+
{String(index + 1).padStart(2, '0')}
|
|
2016
|
+
</span>
|
|
2017
|
+
<Icon size={14} className={cn('transition-transform duration-300 group-hover/tab:scale-110', active ? 'text-accent' : 'text-faint')} />
|
|
2031
2018
|
{item.label}
|
|
2032
2019
|
</button>
|
|
2033
|
-
)
|
|
2020
|
+
)
|
|
2021
|
+
})}
|
|
2022
|
+
</nav>
|
|
2023
|
+
<div className="relative px-6 py-4">
|
|
2024
|
+
<p className="flex items-center gap-1.5 text-[10px] text-faint">
|
|
2025
|
+
<LockKeyhole size={10} /> Keys encrypted on this machine
|
|
2026
|
+
</p>
|
|
2027
|
+
</div>
|
|
2028
|
+
</aside>
|
|
2029
|
+
|
|
2030
|
+
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col bg-panel sm:border-l sm:border-line">
|
|
2031
|
+
<div className="thin-scroll flex shrink-0 gap-1.5 overflow-x-auto border-b border-line px-3 py-2.5 sm:hidden">
|
|
2032
|
+
{TABS.map((item) => (
|
|
2033
|
+
<button
|
|
2034
|
+
key={item.id}
|
|
2035
|
+
type="button"
|
|
2036
|
+
onClick={() => openSettings(item.id)}
|
|
2037
|
+
className={cn(
|
|
2038
|
+
'shrink-0 rounded-full px-3 py-1.5 text-[11px] font-medium transition-all duration-200',
|
|
2039
|
+
tab === item.id ? 'bg-ink text-panel' : 'bg-raised text-muted',
|
|
2040
|
+
)}
|
|
2041
|
+
>
|
|
2042
|
+
{item.label}
|
|
2043
|
+
</button>
|
|
2044
|
+
))}
|
|
2045
|
+
</div>
|
|
2046
|
+
|
|
2047
|
+
<header className="flex shrink-0 items-start justify-between gap-4 px-5 pb-3 pt-5 sm:px-8 sm:pt-7">
|
|
2048
|
+
<div key={tab} className="settings-title-in min-w-0">
|
|
2049
|
+
<p className="font-mono text-[10px] uppercase tracking-[0.22em] text-accent">
|
|
2050
|
+
{String(activeIndex + 1).padStart(2, '0')} / {String(TABS.length).padStart(2, '0')}
|
|
2051
|
+
</p>
|
|
2052
|
+
<h3 className="mt-1 font-display text-[28px] font-semibold leading-none tracking-[-0.035em] text-ink">{current.label}</h3>
|
|
2053
|
+
<p className="mt-2 max-w-[560px] text-[12.5px] leading-relaxed text-muted">{current.description}</p>
|
|
2054
|
+
</div>
|
|
2055
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
2056
|
+
<button
|
|
2057
|
+
type="button"
|
|
2058
|
+
onClick={() => void copyConfig()}
|
|
2059
|
+
className="hidden h-8 items-center gap-1.5 rounded-full border border-line bg-panel px-3 text-[11px] font-medium text-ink transition-all hover:bg-raised active:scale-95 sm:inline-flex"
|
|
2060
|
+
title={daemon?.dataDir ?? '~/.opensol'}
|
|
2061
|
+
>
|
|
2062
|
+
<HardDrive size={12} />
|
|
2063
|
+
Data folder
|
|
2064
|
+
</button>
|
|
2065
|
+
<button type="button" className="icon-button h-8 w-8 rounded-full" onClick={close} aria-label="Close settings">
|
|
2066
|
+
<X size={16} />
|
|
2067
|
+
</button>
|
|
2034
2068
|
</div>
|
|
2069
|
+
</header>
|
|
2035
2070
|
|
|
2036
|
-
|
|
2071
|
+
<div ref={contentRef} className="thin-scroll min-h-0 flex-1 overflow-y-auto pb-6">
|
|
2072
|
+
<div key={tab} className="settings-enter">
|
|
2037
2073
|
{tab === 'general' && <GeneralTab />}
|
|
2038
2074
|
{tab === 'models' && <ModelsTab />}
|
|
2039
2075
|
{tab === 'tools' && <ToolsTab />}
|
|
2040
2076
|
{tab === 'search' && <SearchTab />}
|
|
2041
2077
|
{tab === 'skills' && <SkillsTab />}
|
|
2042
2078
|
{tab === 'plugins' && <PluginsTab />}
|
|
2079
|
+
{tab === 'mcp' && <McpTab />}
|
|
2043
2080
|
{tab === 'deploy' && <DeployTab />}
|
|
2044
2081
|
{tab === 'appearance' && <AppearanceTab />}
|
|
2045
2082
|
{tab === 'about' && <AboutTab />}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
Plus,
|
|
13
13
|
Rocket,
|
|
14
14
|
Search,
|
|
15
|
+
Plug,
|
|
15
16
|
Settings,
|
|
16
17
|
Sparkles,
|
|
17
18
|
Trash2,
|
|
@@ -22,6 +23,7 @@ import { hasCredentials, useSettings } from '../store/settings'
|
|
|
22
23
|
import { useRuntime, useUi } from '../store/ui'
|
|
23
24
|
import { useWorkspace } from '../store/workspace'
|
|
24
25
|
import { stopRun } from '../agent'
|
|
26
|
+
import { kernel } from '../core/kernel'
|
|
25
27
|
import { getProviderName } from '../data/providers'
|
|
26
28
|
import { cn, downloadText, formatRelativeTime, groupChatsByDate } from '../lib/utils'
|
|
27
29
|
import { LogoLockup } from './Logo'
|
|
@@ -46,6 +48,7 @@ export function Sidebar() {
|
|
|
46
48
|
const settings = useSettings()
|
|
47
49
|
const workspace = useWorkspace((state) => state.info)
|
|
48
50
|
const addWorkspace = useWorkspace((state) => state.addWorkspace)
|
|
51
|
+
const removeWorkspace = useWorkspace((state) => state.removeWorkspace)
|
|
49
52
|
const sidebarCollapsed = useUi((state) => state.sidebarCollapsed)
|
|
50
53
|
const toggleSidebar = useUi((state) => state.toggleSidebar)
|
|
51
54
|
const mobileOpen = useUi((state) => state.mobileSidebarOpen)
|
|
@@ -57,6 +60,7 @@ export function Sidebar() {
|
|
|
57
60
|
const notify = useUi((state) => state.notify)
|
|
58
61
|
const focusComposer = useUi((state) => state.focusComposer)
|
|
59
62
|
const streaming = useRuntime((state) => state.streaming)
|
|
63
|
+
const titling = useRuntime((state) => state.titling)
|
|
60
64
|
|
|
61
65
|
const [query, setQuery] = useState('')
|
|
62
66
|
const [menu, setMenu] = useState<ChatMenu | null>(null)
|
|
@@ -108,6 +112,28 @@ export function Sidebar() {
|
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
const deleteWorkspace = async (path: string, name: string) => {
|
|
116
|
+
const confirmed = await askConfirm({
|
|
117
|
+
title: 'Remove this workspace?',
|
|
118
|
+
body: `“${name}” is removed from OpenSol's list. The folder and its files stay on your disk; conversations started there are kept.`,
|
|
119
|
+
confirmLabel: 'Remove workspace',
|
|
120
|
+
tone: 'danger',
|
|
121
|
+
})
|
|
122
|
+
if (!confirmed) return
|
|
123
|
+
try {
|
|
124
|
+
await removeWorkspace(path)
|
|
125
|
+
notify(`Removed ${name} from your workspaces.`, 'success')
|
|
126
|
+
} catch (error) {
|
|
127
|
+
notify(error instanceof Error ? error.message : 'Could not remove that workspace.', 'error')
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const aiRename = async (id: string) => {
|
|
132
|
+
setMenu(null)
|
|
133
|
+
const title = await kernel.titles?.generate(id, { force: true })
|
|
134
|
+
if (!title) notify('The model could not suggest a title right now.', 'error')
|
|
135
|
+
}
|
|
136
|
+
|
|
111
137
|
const beginRename = (id: string, title: string) => {
|
|
112
138
|
setEditingId(id)
|
|
113
139
|
setDraftTitle(title)
|
|
@@ -225,6 +251,17 @@ export function Sidebar() {
|
|
|
225
251
|
<Package size={15} strokeWidth={1.7} className="shrink-0 text-faint" />
|
|
226
252
|
Plugins
|
|
227
253
|
</button>
|
|
254
|
+
<button
|
|
255
|
+
type="button"
|
|
256
|
+
onClick={() => {
|
|
257
|
+
setMobileOpen(false)
|
|
258
|
+
openSettings('mcp')
|
|
259
|
+
}}
|
|
260
|
+
className="nav-item"
|
|
261
|
+
>
|
|
262
|
+
<Plug size={15} strokeWidth={1.7} className="shrink-0 text-faint" />
|
|
263
|
+
MCP
|
|
264
|
+
</button>
|
|
228
265
|
<button
|
|
229
266
|
type="button"
|
|
230
267
|
onClick={() => {
|
|
@@ -286,28 +323,38 @@ export function Sidebar() {
|
|
|
286
323
|
{workspaces.map((entry) => {
|
|
287
324
|
const active = entry.path === workspace.root
|
|
288
325
|
return (
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
326
|
+
<div key={entry.path} className="group/ws relative">
|
|
327
|
+
<button
|
|
328
|
+
type="button"
|
|
329
|
+
onClick={() => void switchWorkspace(entry.path)}
|
|
330
|
+
title={entry.path}
|
|
331
|
+
className={cn(
|
|
332
|
+
'flex h-8 w-full items-center gap-2.5 rounded-lg px-2.5 pr-8 text-left text-[13px] transition-all duration-150 active:scale-[0.985]',
|
|
333
|
+
active ? 'bg-raised text-ink' : 'text-muted hover:bg-raised/80 hover:text-ink',
|
|
334
|
+
)}
|
|
335
|
+
>
|
|
336
|
+
<Folder size={15} strokeWidth={1.6} className={cn('shrink-0 transition-transform duration-200 group-hover/ws:-rotate-6 group-hover/ws:scale-110', active ? 'text-accent' : 'text-faint')} />
|
|
337
|
+
<span className="min-w-0 flex-1 truncate">{entry.name}</span>
|
|
338
|
+
{switching === entry.path ? (
|
|
339
|
+
<LoaderCircle size={11} className="shrink-0 animate-spin text-faint" />
|
|
340
|
+
) : active ? (
|
|
341
|
+
<Check size={11} className="shrink-0 text-accent" />
|
|
342
|
+
) : (
|
|
343
|
+
<span className="shrink-0 text-[10px] tabular-nums text-faint transition-opacity group-hover/ws:opacity-0">
|
|
344
|
+
{formatRelativeTime(entry.lastOpened)}
|
|
345
|
+
</span>
|
|
346
|
+
)}
|
|
347
|
+
</button>
|
|
348
|
+
<button
|
|
349
|
+
type="button"
|
|
350
|
+
onClick={() => void deleteWorkspace(entry.path, entry.name)}
|
|
351
|
+
className="absolute right-1 top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-faint opacity-0 transition-all hover:bg-danger/10 hover:text-danger focus:opacity-100 group-hover/ws:opacity-100"
|
|
352
|
+
title="Remove workspace from the list"
|
|
353
|
+
aria-label={`Remove workspace ${entry.name}`}
|
|
354
|
+
>
|
|
355
|
+
<Trash2 size={12} />
|
|
356
|
+
</button>
|
|
357
|
+
</div>
|
|
311
358
|
)
|
|
312
359
|
})}
|
|
313
360
|
</div>
|
|
@@ -360,7 +407,7 @@ export function Sidebar() {
|
|
|
360
407
|
title={`${session.title} · ${formatRelativeTime(session.updatedAt)}`}
|
|
361
408
|
className={cn('sidebar-item pr-9', activeId === session.id && 'sidebar-item-active')}
|
|
362
409
|
>
|
|
363
|
-
<span className=
|
|
410
|
+
<span className={cn('min-w-0 flex-1 truncate', titling[session.id] && 'text-shimmer')}>{session.title}</span>
|
|
364
411
|
{streaming[session.id] && (
|
|
365
412
|
<span
|
|
366
413
|
className="flex shrink-0 items-center gap-1 text-[10.5px] font-medium text-accent group-hover/item:hidden"
|
|
@@ -381,7 +428,7 @@ export function Sidebar() {
|
|
|
381
428
|
}
|
|
382
429
|
const rect = event.currentTarget.getBoundingClientRect()
|
|
383
430
|
const width = 176
|
|
384
|
-
const height =
|
|
431
|
+
const height = 166
|
|
385
432
|
const x = Math.max(8, Math.min(rect.right - width, window.innerWidth - width - 8))
|
|
386
433
|
const y =
|
|
387
434
|
rect.bottom + height + 8 > window.innerHeight
|
|
@@ -470,6 +517,13 @@ export function Sidebar() {
|
|
|
470
517
|
>
|
|
471
518
|
<Pencil size={12} /> Rename
|
|
472
519
|
</button>
|
|
520
|
+
<button
|
|
521
|
+
type="button"
|
|
522
|
+
onClick={() => void aiRename(session.id)}
|
|
523
|
+
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"
|
|
524
|
+
>
|
|
525
|
+
<Sparkles size={12} /> Name with AI
|
|
526
|
+
</button>
|
|
473
527
|
<button
|
|
474
528
|
type="button"
|
|
475
529
|
onClick={() => exportChat(session.id)}
|