@bibobonking/opensol 3.0.3 → 3.1.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 +60 -8
- package/CHANGELOG.md +21 -0
- package/README.md +2 -2
- 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-CeN54A2N.css +1 -0
- package/dist/assets/index-fhIO9a7_.js +318 -0
- package/dist/index.html +4 -4
- package/index.html +2 -2
- package/package.json +2 -1
- package/server/README.md +2 -2
- package/server/daemon.mjs +138 -2
- package/server/lib/mcp-oauth.mjs +219 -0
- package/server/lib/mcp-oauth.test.mjs +175 -0
- package/server/lib/mcp.mjs +899 -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/App.tsx +2 -0
- package/src/agent/tools.ts +6 -5
- package/src/components/CommandPalette.tsx +14 -1
- package/src/components/Composer.tsx +22 -38
- package/src/components/DiffView.tsx +98 -0
- package/src/components/Logo.tsx +1 -1
- package/src/components/McpPanel.tsx +687 -0
- package/src/components/MessageItem.tsx +62 -13
- package/src/components/ModelDeck.tsx +661 -0
- package/src/components/SessionsTab.tsx +206 -0
- package/src/components/SettingsModal.tsx +146 -71
- package/src/components/Sidebar.tsx +79 -24
- package/src/components/Splash.tsx +1 -1
- 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/data/providers.ts +1 -1
- package/src/index.css +97 -1
- package/src/lib/daemon.ts +38 -0
- package/src/lib/diff.ts +62 -0
- package/src/lib/mcp-config.test.mjs +47 -0
- package/src/lib/mcp-config.ts +203 -0
- package/src/lib/permissions.ts +6 -6
- package/src/store/chats.ts +9 -1
- package/src/store/mcp.ts +94 -0
- package/src/store/settings.ts +10 -7
- package/src/store/ui.ts +17 -0
- package/src/store/workspace.ts +26 -0
- package/src/types.ts +63 -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 { useMcp } from '../store/mcp'
|
|
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: 'Model Context Protocol servers now have their own panel, with OAuth sign-in and local, HTTP and SSE connections.' },
|
|
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>
|
|
@@ -1775,10 +1792,10 @@ function AboutTab() {
|
|
|
1775
1792
|
<h3 className="font-display text-[20px] font-semibold tracking-[-0.03em] text-ink">OpenSol</h3>
|
|
1776
1793
|
<span className="text-[10px] text-faint">v{daemon?.version ?? '1.0.0'}</span>
|
|
1777
1794
|
</div>
|
|
1778
|
-
<p className="mt-0.5 text-[10.5px] font-medium text-muted">
|
|
1795
|
+
<p className="mt-0.5 text-[10.5px] font-medium text-muted">An OpenSoftX product</p>
|
|
1779
1796
|
<p className="mt-2 max-w-[520px] text-[10.5px] leading-relaxed text-muted">
|
|
1780
1797
|
A local-first coding agent with a real workspace, shell, git, web research, subagents and deploy flows.
|
|
1781
|
-
No OpenSol account and no
|
|
1798
|
+
No OpenSol account and no OpenSoftX server. Your keys never leave this machine.
|
|
1782
1799
|
</p>
|
|
1783
1800
|
</div>
|
|
1784
1801
|
</div>
|
|
@@ -1933,6 +1950,44 @@ function AboutTab() {
|
|
|
1933
1950
|
)
|
|
1934
1951
|
}
|
|
1935
1952
|
|
|
1953
|
+
function McpLauncher() {
|
|
1954
|
+
const servers = useMcp((state) => state.servers)
|
|
1955
|
+
const loaded = useMcp((state) => state.loaded)
|
|
1956
|
+
const load = useMcp((state) => state.load)
|
|
1957
|
+
const setMcpOpen = useUi((state) => state.setMcpOpen)
|
|
1958
|
+
useEffect(() => {
|
|
1959
|
+
void load()
|
|
1960
|
+
}, [load])
|
|
1961
|
+
const connected = servers.filter((server) => server.status === 'connected').length
|
|
1962
|
+
const waiting = servers.filter((server) => server.status === 'needs_auth').length
|
|
1963
|
+
return (
|
|
1964
|
+
<div className="space-y-5 px-5 py-6 sm:px-8">
|
|
1965
|
+
<div className="settings-card flex flex-wrap items-center gap-4">
|
|
1966
|
+
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-accent-soft text-accent">
|
|
1967
|
+
<Link2 size={18} />
|
|
1968
|
+
</span>
|
|
1969
|
+
<div className="min-w-0 flex-1">
|
|
1970
|
+
<p className="settings-label">MCP panel</p>
|
|
1971
|
+
<p className="settings-hint">
|
|
1972
|
+
{!loaded
|
|
1973
|
+
? 'Loading…'
|
|
1974
|
+
: servers.length === 0
|
|
1975
|
+
? 'No servers yet. Add a local command or a remote URL, with OAuth where the service asks for it.'
|
|
1976
|
+
: `${connected} of ${servers.length} server${servers.length === 1 ? '' : 's'} connected${waiting ? ` · ${waiting} waiting for sign-in` : ''}.`}
|
|
1977
|
+
</p>
|
|
1978
|
+
</div>
|
|
1979
|
+
<button
|
|
1980
|
+
type="button"
|
|
1981
|
+
onClick={() => setMcpOpen(true)}
|
|
1982
|
+
className="inline-flex h-9 shrink-0 items-center gap-1.5 rounded-full bg-ink px-4 text-[12px] font-medium text-panel transition-transform active:scale-95"
|
|
1983
|
+
>
|
|
1984
|
+
Open MCP panel
|
|
1985
|
+
</button>
|
|
1986
|
+
</div>
|
|
1987
|
+
</div>
|
|
1988
|
+
)
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1936
1991
|
export function SettingsModal() {
|
|
1937
1992
|
const open = useUi((state) => state.settingsOpen)
|
|
1938
1993
|
const tab = useUi((state) => state.settingsTab)
|
|
@@ -1941,13 +1996,15 @@ export function SettingsModal() {
|
|
|
1941
1996
|
const notify = useUi((state) => state.notify)
|
|
1942
1997
|
const daemon = useSettings((state) => state.daemon)
|
|
1943
1998
|
const contentRef = useRef<HTMLDivElement>(null)
|
|
1999
|
+
const activeIndex = Math.max(0, TABS.findIndex((item) => item.id === tab))
|
|
2000
|
+
const current = TABS[activeIndex]
|
|
1944
2001
|
|
|
1945
2002
|
useEffect(() => {
|
|
1946
2003
|
if (open) contentRef.current?.scrollTo({ top: 0 })
|
|
1947
2004
|
}, [open, tab])
|
|
1948
2005
|
|
|
1949
2006
|
const copyConfig = async () => {
|
|
1950
|
-
const path = daemon?.dataDir ?? '~/.
|
|
2007
|
+
const path = daemon?.dataDir ?? '~/.opensol'
|
|
1951
2008
|
try {
|
|
1952
2009
|
await navigator.clipboard.writeText(path)
|
|
1953
2010
|
notify(`Copied the data folder path: ${path}`, 'success')
|
|
@@ -1961,85 +2018,103 @@ export function SettingsModal() {
|
|
|
1961
2018
|
open={open}
|
|
1962
2019
|
onClose={close}
|
|
1963
2020
|
labelledBy="settings-title"
|
|
1964
|
-
className="h-[
|
|
2021
|
+
className="settings-shell h-[94dvh] p-0 sm:h-[min(88dvh,780px)] sm:w-[min(1040px,96vw)] sm:!rounded-[28px]"
|
|
1965
2022
|
>
|
|
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>
|
|
2023
|
+
<div className="flex min-h-0 flex-1 flex-col sm:flex-row">
|
|
2024
|
+
<aside className="relative hidden w-[232px] shrink-0 flex-col bg-canvas sm:flex">
|
|
2025
|
+
<div className="settings-glow pointer-events-none absolute inset-x-0 top-0 h-48" />
|
|
2026
|
+
<div className="relative px-6 pb-4 pt-6">
|
|
2027
|
+
<p className="font-mono text-[10px] uppercase tracking-[0.24em] text-faint">OpenSol · v3.1.0</p>
|
|
2028
|
+
<h2 id="settings-title" className="mt-1.5 font-display text-[26px] font-semibold leading-none tracking-[-0.035em] text-ink">
|
|
2029
|
+
Settings
|
|
2030
|
+
</h2>
|
|
1984
2031
|
</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) => (
|
|
2032
|
+
<nav className="relative flex-1 overflow-y-auto px-3 pb-3">
|
|
2033
|
+
<span
|
|
2034
|
+
aria-hidden
|
|
2035
|
+
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"
|
|
2036
|
+
style={{ transform: `translateY(${activeIndex * 42}px)` }}
|
|
2037
|
+
/>
|
|
2038
|
+
{TABS.map((item, index) => {
|
|
2039
|
+
const Icon = item.icon
|
|
2040
|
+
const active = tab === item.id
|
|
2041
|
+
return (
|
|
2022
2042
|
<button
|
|
2023
2043
|
key={item.id}
|
|
2024
2044
|
type="button"
|
|
2025
2045
|
onClick={() => openSettings(item.id)}
|
|
2046
|
+
title={item.description}
|
|
2026
2047
|
className={cn(
|
|
2027
|
-
'
|
|
2028
|
-
|
|
2048
|
+
'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',
|
|
2049
|
+
active ? 'font-semibold text-ink' : 'text-muted hover:text-ink',
|
|
2029
2050
|
)}
|
|
2030
2051
|
>
|
|
2052
|
+
<span className={cn('w-4 font-mono text-[9.5px] tabular-nums transition-colors', active ? 'text-accent' : 'text-faint')}>
|
|
2053
|
+
{String(index + 1).padStart(2, '0')}
|
|
2054
|
+
</span>
|
|
2055
|
+
<Icon size={14} className={cn('transition-transform duration-300 group-hover/tab:scale-110', active ? 'text-accent' : 'text-faint')} />
|
|
2031
2056
|
{item.label}
|
|
2032
2057
|
</button>
|
|
2033
|
-
)
|
|
2058
|
+
)
|
|
2059
|
+
})}
|
|
2060
|
+
</nav>
|
|
2061
|
+
<div className="relative px-6 py-4">
|
|
2062
|
+
<p className="flex items-center gap-1.5 text-[10px] text-faint">
|
|
2063
|
+
<LockKeyhole size={10} /> Keys encrypted on this machine
|
|
2064
|
+
</p>
|
|
2065
|
+
</div>
|
|
2066
|
+
</aside>
|
|
2067
|
+
|
|
2068
|
+
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col bg-panel sm:border-l sm:border-line">
|
|
2069
|
+
<div className="thin-scroll flex shrink-0 gap-1.5 overflow-x-auto border-b border-line px-3 py-2.5 sm:hidden">
|
|
2070
|
+
{TABS.map((item) => (
|
|
2071
|
+
<button
|
|
2072
|
+
key={item.id}
|
|
2073
|
+
type="button"
|
|
2074
|
+
onClick={() => openSettings(item.id)}
|
|
2075
|
+
className={cn(
|
|
2076
|
+
'shrink-0 rounded-full px-3 py-1.5 text-[11px] font-medium transition-all duration-200',
|
|
2077
|
+
tab === item.id ? 'bg-ink text-panel' : 'bg-raised text-muted',
|
|
2078
|
+
)}
|
|
2079
|
+
>
|
|
2080
|
+
{item.label}
|
|
2081
|
+
</button>
|
|
2082
|
+
))}
|
|
2083
|
+
</div>
|
|
2084
|
+
|
|
2085
|
+
<header className="flex shrink-0 items-start justify-between gap-4 px-5 pb-3 pt-5 sm:px-8 sm:pt-7">
|
|
2086
|
+
<div key={tab} className="settings-title-in min-w-0">
|
|
2087
|
+
<p className="font-mono text-[10px] uppercase tracking-[0.22em] text-accent">
|
|
2088
|
+
{String(activeIndex + 1).padStart(2, '0')} / {String(TABS.length).padStart(2, '0')}
|
|
2089
|
+
</p>
|
|
2090
|
+
<h3 className="mt-1 font-display text-[28px] font-semibold leading-none tracking-[-0.035em] text-ink">{current.label}</h3>
|
|
2091
|
+
<p className="mt-2 max-w-[560px] text-[12.5px] leading-relaxed text-muted">{current.description}</p>
|
|
2092
|
+
</div>
|
|
2093
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
2094
|
+
<button
|
|
2095
|
+
type="button"
|
|
2096
|
+
onClick={() => void copyConfig()}
|
|
2097
|
+
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"
|
|
2098
|
+
title={daemon?.dataDir ?? '~/.opensol'}
|
|
2099
|
+
>
|
|
2100
|
+
<HardDrive size={12} />
|
|
2101
|
+
Data folder
|
|
2102
|
+
</button>
|
|
2103
|
+
<button type="button" className="icon-button h-8 w-8 rounded-full" onClick={close} aria-label="Close settings">
|
|
2104
|
+
<X size={16} />
|
|
2105
|
+
</button>
|
|
2034
2106
|
</div>
|
|
2107
|
+
</header>
|
|
2035
2108
|
|
|
2036
|
-
|
|
2109
|
+
<div ref={contentRef} className="thin-scroll min-h-0 flex-1 overflow-y-auto pb-6">
|
|
2110
|
+
<div key={tab} className="settings-enter">
|
|
2037
2111
|
{tab === 'general' && <GeneralTab />}
|
|
2038
2112
|
{tab === 'models' && <ModelsTab />}
|
|
2039
2113
|
{tab === 'tools' && <ToolsTab />}
|
|
2040
2114
|
{tab === 'search' && <SearchTab />}
|
|
2041
2115
|
{tab === 'skills' && <SkillsTab />}
|
|
2042
2116
|
{tab === 'plugins' && <PluginsTab />}
|
|
2117
|
+
{tab === 'mcp' && <McpLauncher />}
|
|
2043
2118
|
{tab === 'deploy' && <DeployTab />}
|
|
2044
2119
|
{tab === 'appearance' && <AppearanceTab />}
|
|
2045
2120
|
{tab === 'about' && <AboutTab />}
|