@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.
Files changed (83) hide show
  1. package/.github/workflows/build.yml +60 -8
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +2 -2
  4. package/desktop/README.md +39 -0
  5. package/desktop/loading/index.html +36 -0
  6. package/desktop/src-tauri/Cargo.lock +4301 -0
  7. package/desktop/src-tauri/Cargo.toml +23 -0
  8. package/desktop/src-tauri/binaries/.gitkeep +0 -0
  9. package/desktop/src-tauri/build.rs +3 -0
  10. package/desktop/src-tauri/capabilities/default.json +7 -0
  11. package/desktop/src-tauri/icons/128x128.png +0 -0
  12. package/desktop/src-tauri/icons/128x128@2x.png +0 -0
  13. package/desktop/src-tauri/icons/32x32.png +0 -0
  14. package/desktop/src-tauri/icons/64x64.png +0 -0
  15. package/desktop/src-tauri/icons/Square107x107Logo.png +0 -0
  16. package/desktop/src-tauri/icons/Square142x142Logo.png +0 -0
  17. package/desktop/src-tauri/icons/Square150x150Logo.png +0 -0
  18. package/desktop/src-tauri/icons/Square284x284Logo.png +0 -0
  19. package/desktop/src-tauri/icons/Square30x30Logo.png +0 -0
  20. package/desktop/src-tauri/icons/Square310x310Logo.png +0 -0
  21. package/desktop/src-tauri/icons/Square44x44Logo.png +0 -0
  22. package/desktop/src-tauri/icons/Square71x71Logo.png +0 -0
  23. package/desktop/src-tauri/icons/Square89x89Logo.png +0 -0
  24. package/desktop/src-tauri/icons/StoreLogo.png +0 -0
  25. package/desktop/src-tauri/icons/icon.icns +0 -0
  26. package/desktop/src-tauri/icons/icon.ico +0 -0
  27. package/desktop/src-tauri/icons/icon.png +0 -0
  28. package/desktop/src-tauri/src/lib.rs +210 -0
  29. package/desktop/src-tauri/src/main.rs +6 -0
  30. package/desktop/src-tauri/tauri.conf.json +48 -0
  31. package/dist/assets/index-CeN54A2N.css +1 -0
  32. package/dist/assets/index-fhIO9a7_.js +318 -0
  33. package/dist/index.html +4 -4
  34. package/index.html +2 -2
  35. package/package.json +2 -1
  36. package/server/README.md +2 -2
  37. package/server/daemon.mjs +138 -2
  38. package/server/lib/mcp-oauth.mjs +219 -0
  39. package/server/lib/mcp-oauth.test.mjs +175 -0
  40. package/server/lib/mcp.mjs +899 -0
  41. package/server/lib/mcp.test.mjs +46 -0
  42. package/server/lib/store.mjs +22 -0
  43. package/server/lib/tools.mjs +59 -1
  44. package/src/App.tsx +2 -0
  45. package/src/agent/tools.ts +6 -5
  46. package/src/components/CommandPalette.tsx +14 -1
  47. package/src/components/Composer.tsx +22 -38
  48. package/src/components/DiffView.tsx +98 -0
  49. package/src/components/Logo.tsx +1 -1
  50. package/src/components/McpPanel.tsx +687 -0
  51. package/src/components/MessageItem.tsx +62 -13
  52. package/src/components/ModelDeck.tsx +661 -0
  53. package/src/components/SessionsTab.tsx +206 -0
  54. package/src/components/SettingsModal.tsx +146 -71
  55. package/src/components/Sidebar.tsx +79 -24
  56. package/src/components/Splash.tsx +1 -1
  57. package/src/components/ToolCallCard.tsx +29 -7
  58. package/src/components/WorkspacePanel.tsx +16 -1
  59. package/src/core/kernel.ts +2 -0
  60. package/src/core/mcp-tools.ts +63 -0
  61. package/src/core/plugins/tools/fs.ts +24 -3
  62. package/src/core/services/agent-loop.ts +3 -1
  63. package/src/core/services/approval.ts +2 -1
  64. package/src/core/services/titles.ts +135 -0
  65. package/src/core/services/tools.ts +11 -8
  66. package/src/core/types.ts +4 -0
  67. package/src/data/providers.ts +1 -1
  68. package/src/index.css +97 -1
  69. package/src/lib/daemon.ts +38 -0
  70. package/src/lib/diff.ts +62 -0
  71. package/src/lib/mcp-config.test.mjs +47 -0
  72. package/src/lib/mcp-config.ts +203 -0
  73. package/src/lib/permissions.ts +6 -6
  74. package/src/store/chats.ts +9 -1
  75. package/src/store/mcp.ts +94 -0
  76. package/src/store/settings.ts +10 -7
  77. package/src/store/ui.ts +17 -0
  78. package/src/store/workspace.ts +26 -0
  79. package/src/types.ts +63 -2
  80. package/tsconfig.tsbuildinfo +1 -1
  81. package/dist/assets/index-BmN7pBji.css +0 -1
  82. package/dist/assets/index-DgNDjkOz.js +0 -301
  83. 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, ToolName } from '../types'
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="border-b border-line px-5 py-5 last:border-b-0 sm:px-7">
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">{title}</h3>
107
- {description && <p className="mt-1 max-w-[620px] text-[11px] leading-relaxed text-muted">{description}</p>}
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-3.5">{children}</div>
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: ToolName[] }> = [
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<ToolName, LucideIcon> = {
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<ToolName, { label: string; description: string }> = {
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">A OpenSoft product</p>
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 OpenSoft server. Your keys never leave this machine.
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 ?? '~/.maxcode'
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-[92dvh] p-0 sm:h-[min(88dvh,780px)] sm:w-[min(980px,94vw)]"
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
- <div className="flex h-14 shrink-0 items-center justify-between gap-3 border-b border-line px-4 sm:px-5">
1968
- <h2 id="settings-title" className="text-[16px] font-semibold tracking-[-0.02em] text-ink">
1969
- Settings
1970
- </h2>
1971
- <div className="flex shrink-0 items-center gap-1.5">
1972
- <button
1973
- type="button"
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
- </div>
1986
-
1987
- <div className="flex min-h-0 flex-1">
1988
- <aside className="hidden w-[200px] shrink-0 flex-col border-r border-line bg-canvas sm:flex">
1989
- <nav className="flex-1 space-y-0.5 overflow-y-auto p-2">
1990
- {TABS.map((item) => {
1991
- const Icon = item.icon
1992
- const active = tab === item.id
1993
- return (
1994
- <button
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
- 'shrink-0 rounded-full px-2.5 py-1 text-[10.5px] font-medium transition-colors',
2028
- tab === item.id ? 'bg-ink text-panel' : 'text-muted hover:bg-raised',
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
- <div ref={contentRef} className="thin-scroll min-h-0 flex-1 overflow-y-auto">
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 />}