@bibobonking/opensol 0.0.0-stage → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build.yml +25 -0
- package/README.md +74 -2
- package/dist/assets/index-BmN7pBji.css +1 -0
- package/dist/assets/index-DgNDjkOz.js +301 -0
- package/dist/favicon.svg +15 -0
- package/dist/index.html +55 -0
- package/dist/preview-host.html +33 -0
- package/index.html +54 -0
- package/package.json +43 -4
- package/postcss.config.js +6 -0
- package/public/favicon.svg +15 -0
- package/public/preview-host.html +33 -0
- package/server/README.md +75 -0
- package/server/daemon.mjs +1082 -0
- package/server/lib/checkpoints.mjs +124 -0
- package/server/lib/checkpoints.test.mjs +110 -0
- package/server/lib/deploy.mjs +436 -0
- package/server/lib/http.mjs +237 -0
- package/server/lib/plugins.mjs +321 -0
- package/server/lib/plugins.test.mjs +140 -0
- package/server/lib/providers.mjs +594 -0
- package/server/lib/store.mjs +491 -0
- package/server/lib/tools.mjs +990 -0
- package/server/opensol.mjs +202 -0
- package/server/package.json +42 -0
- package/shared/plugin-builder.test.mjs +43 -0
- package/shared/plugin-package.d.mts +34 -0
- package/shared/plugin-package.mjs +197 -0
- package/shared/plugin-package.test.mjs +94 -0
- package/src/App.tsx +152 -0
- package/src/agent/adapters/anthropic.ts +233 -0
- package/src/agent/adapters/gemini.ts +225 -0
- package/src/agent/adapters/openai-responses.ts +215 -0
- package/src/agent/adapters/openai.ts +200 -0
- package/src/agent/adapters/shared.ts +96 -0
- package/src/agent/approval.ts +20 -0
- package/src/agent/compact.ts +234 -0
- package/src/agent/index.ts +36 -0
- package/src/agent/model.ts +20 -0
- package/src/agent/prompt.ts +107 -0
- package/src/agent/queue.test.mjs +111 -0
- package/src/agent/queue.ts +84 -0
- package/src/agent/subagent.ts +109 -0
- package/src/agent/tools.ts +541 -0
- package/src/components/ChatView.tsx +403 -0
- package/src/components/CommandPalette.tsx +416 -0
- package/src/components/Composer.tsx +678 -0
- package/src/components/ContextBar.tsx +58 -0
- package/src/components/DeployModal.tsx +512 -0
- package/src/components/ErrorBoundary.tsx +75 -0
- package/src/components/HomeDashboard.tsx +189 -0
- package/src/components/Logo.tsx +41 -0
- package/src/components/Markdown.tsx +232 -0
- package/src/components/MessageItem.tsx +655 -0
- package/src/components/ModelPicker.tsx +487 -0
- package/src/components/PixleSol.tsx +243 -0
- package/src/components/PluginBuilder.tsx +285 -0
- package/src/components/PluginManager.tsx +341 -0
- package/src/components/PreviewPane.tsx +166 -0
- package/src/components/SettingsModal.tsx +2052 -0
- package/src/components/Sidebar.tsx +504 -0
- package/src/components/Splash.tsx +104 -0
- package/src/components/ToolCallCard.tsx +177 -0
- package/src/components/TopBar.tsx +182 -0
- package/src/components/TrajectoryView.tsx +178 -0
- package/src/components/WorkspacePanel.tsx +710 -0
- package/src/components/ui.tsx +311 -0
- package/src/core/kernel.ts +45 -0
- package/src/core/plugins/tools/fs.ts +219 -0
- package/src/core/plugins/tools/git.ts +83 -0
- package/src/core/plugins/tools/grep.ts +98 -0
- package/src/core/plugins/tools/shell.ts +88 -0
- package/src/core/plugins/tools/subagent.ts +36 -0
- package/src/core/plugins/tools/web.ts +97 -0
- package/src/core/services/agent-loop.ts +417 -0
- package/src/core/services/approval.ts +66 -0
- package/src/core/services/compaction.ts +45 -0
- package/src/core/services/llm.ts +32 -0
- package/src/core/services/sessions.ts +47 -0
- package/src/core/services/subagents.ts +15 -0
- package/src/core/services/system-prompt.ts +177 -0
- package/src/core/services/tools.ts +135 -0
- package/src/core/services/ui.ts +109 -0
- package/src/core/types.ts +139 -0
- package/src/data/providers.ts +125 -0
- package/src/data/skills.ts +68 -0
- package/src/index.css +776 -0
- package/src/lib/boot.ts +94 -0
- package/src/lib/context.ts +75 -0
- package/src/lib/conversation.test.mjs +64 -0
- package/src/lib/conversation.ts +59 -0
- package/src/lib/daemon.ts +475 -0
- package/src/lib/hooks.ts +33 -0
- package/src/lib/permissions.ts +60 -0
- package/src/lib/pixlesol.test.mjs +51 -0
- package/src/lib/pixlesol.ts +55 -0
- package/src/lib/plugin-folder.test.mjs +145 -0
- package/src/lib/plugin-folder.ts +96 -0
- package/src/lib/reasoning.ts +87 -0
- package/src/lib/sse.ts +75 -0
- package/src/lib/themes.test.mjs +72 -0
- package/src/lib/themes.ts +52 -0
- package/src/lib/usage.test.mjs +72 -0
- package/src/lib/usage.ts +155 -0
- package/src/lib/utils.ts +73 -0
- package/src/main.tsx +16 -0
- package/src/plugins/api.test.mjs +77 -0
- package/src/plugins/api.ts +113 -0
- package/src/plugins/bridge.d.mts +1 -0
- package/src/plugins/bridge.mjs +26 -0
- package/src/plugins/bridge.test.mjs +17 -0
- package/src/plugins/index.ts +52 -0
- package/src/plugins/preview.test.mjs +18 -0
- package/src/plugins/preview.ts +34 -0
- package/src/plugins/runtime.ts +194 -0
- package/src/plugins/types.ts +40 -0
- package/src/store/chats.ts +278 -0
- package/src/store/models.ts +120 -0
- package/src/store/settings.ts +445 -0
- package/src/store/ui.ts +132 -0
- package/src/store/workspace.ts +299 -0
- package/src/types.ts +337 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +83 -0
- package/tsconfig.json +22 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vite.config.ts +19 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
import type {
|
|
3
|
+
AppSettings,
|
|
4
|
+
DaemonSnapshot,
|
|
5
|
+
DeployProviderId,
|
|
6
|
+
DeploySecretEntry,
|
|
7
|
+
DeployToolInfo,
|
|
8
|
+
Effort,
|
|
9
|
+
ProviderId,
|
|
10
|
+
SecretEntry,
|
|
11
|
+
SecretsSummary,
|
|
12
|
+
Skill,
|
|
13
|
+
ToolName,
|
|
14
|
+
ToolSetting,
|
|
15
|
+
} from '../types'
|
|
16
|
+
import { BUILTIN_SKILLS } from '../data/skills'
|
|
17
|
+
import { PROVIDER_MAP } from '../data/providers'
|
|
18
|
+
import {
|
|
19
|
+
patchSettings,
|
|
20
|
+
setDeployCredentials as saveDeployCredentials,
|
|
21
|
+
setProviderKey,
|
|
22
|
+
setSearchKey as saveSearchKey,
|
|
23
|
+
listProviders,
|
|
24
|
+
} from '../lib/daemon'
|
|
25
|
+
import { defaultEffort, normalizeEffort } from '../lib/reasoning'
|
|
26
|
+
import { normalizeTheme } from '../lib/themes'
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_SYSTEM_PROMPT = `You are OpenSol, a senior web engineer and coding agent built by OpenSoft.
|
|
29
|
+
|
|
30
|
+
You work directly in the user's open folder through a local daemon. The shell, file, git, search and deploy tools are real. Never describe tool output you did not receive.
|
|
31
|
+
|
|
32
|
+
Engineering standard:
|
|
33
|
+
- Read every file you change. Match the existing stack, style, naming and conventions. Never invent APIs, versions, flags, package names or paths.
|
|
34
|
+
- Make the smallest correct change. One concern per edit. No speculative abstraction, dead code, placeholder TODOs or swallowed errors.
|
|
35
|
+
- Keep public behavior stable unless the task explicitly changes it.
|
|
36
|
+
- Security: never expose secrets, never weaken auth, CSRF, CSP or validation to make something pass, and validate untrusted input at boundaries.
|
|
37
|
+
- Accessibility: semantic HTML, keyboard paths, visible focus, sufficient contrast.
|
|
38
|
+
- Performance: avoid needless work; measure before optimizing.
|
|
39
|
+
- Every bug fix gets a regression test when a test setup exists. Tests assert behavior, not implementation.
|
|
40
|
+
|
|
41
|
+
Working loop:
|
|
42
|
+
1. Inspect before editing: list files, read the relevant source, grep for call sites, check git status.
|
|
43
|
+
2. For non-trivial work, state a short plan.
|
|
44
|
+
3. Edit in small, coherent steps.
|
|
45
|
+
4. Verify with the project's real commands (typecheck, lint, tests, build) and read the output.
|
|
46
|
+
5. Report what changed, which files, and what was verified. If a check fails or cannot run, say so plainly and explain why.
|
|
47
|
+
|
|
48
|
+
Never claim a check passed without running it. Ask only when a decision is genuinely ambiguous or destructive. Answer in the user's language. Cite web sources as links, and only when you actually read them.`
|
|
49
|
+
|
|
50
|
+
const DEFAULT_TOOLS: Record<ToolName, ToolSetting> = {
|
|
51
|
+
shell: { enabled: true, approval: 'ask' },
|
|
52
|
+
read_file: { enabled: true, approval: 'auto' },
|
|
53
|
+
list_files: { enabled: true, approval: 'auto' },
|
|
54
|
+
write_file: { enabled: true, approval: 'auto' },
|
|
55
|
+
edit_file: { enabled: true, approval: 'auto' },
|
|
56
|
+
rewrite_file: { enabled: true, approval: 'auto' },
|
|
57
|
+
grep: { enabled: true, approval: 'auto' },
|
|
58
|
+
web_search: { enabled: true, approval: 'auto' },
|
|
59
|
+
read_url: { enabled: true, approval: 'auto' },
|
|
60
|
+
git: { enabled: true, approval: 'ask' },
|
|
61
|
+
subagent: { enabled: true, approval: 'ask' },
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const DEFAULTS: AppSettings = {
|
|
65
|
+
provider: 'openai',
|
|
66
|
+
model: '',
|
|
67
|
+
effort: 'medium',
|
|
68
|
+
webSearchEnabled: true,
|
|
69
|
+
temperature: 0.4,
|
|
70
|
+
maxTokens: 8192,
|
|
71
|
+
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
|
72
|
+
providers: {},
|
|
73
|
+
searchProvider: 'duckduckgo',
|
|
74
|
+
tools: DEFAULT_TOOLS,
|
|
75
|
+
skills: BUILTIN_SKILLS,
|
|
76
|
+
theme: 'system',
|
|
77
|
+
showTokenEstimate: true,
|
|
78
|
+
mode: 'build',
|
|
79
|
+
chatFontSize: 15,
|
|
80
|
+
workDetail: 'detailed',
|
|
81
|
+
autoCollapseWork: true,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const EFFORTS = new Set<Effort>(['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'])
|
|
85
|
+
|
|
86
|
+
interface SettingsStore extends AppSettings {
|
|
87
|
+
hydrated: boolean
|
|
88
|
+
daemon: DaemonSnapshot | null
|
|
89
|
+
secretStatus: SecretsSummary | null
|
|
90
|
+
providerStatus: DaemonSnapshot['providers']
|
|
91
|
+
deployTools: DaemonSnapshot['deployTools'] | null
|
|
92
|
+
deploySecrets: DaemonSnapshot['deploySecrets'] | null
|
|
93
|
+
secretsError: string
|
|
94
|
+
hydrate: (snapshot: DaemonSnapshot) => void
|
|
95
|
+
update: (patch: Partial<AppSettings>) => void
|
|
96
|
+
selectModel: (provider: ProviderId, model: string, effort?: Effort) => void
|
|
97
|
+
updateProvider: (provider: ProviderId, patch: { baseUrl?: string }) => void
|
|
98
|
+
saveProviderKey: (provider: ProviderId, apiKey: string) => Promise<void>
|
|
99
|
+
saveSearchKey: (apiKey: string) => Promise<void>
|
|
100
|
+
saveDeployCredentials: (provider: DeployProviderId, fields: Record<string, string>) => Promise<void>
|
|
101
|
+
refreshProviders: () => Promise<void>
|
|
102
|
+
updateTool: (tool: ToolName, patch: Partial<ToolSetting>) => void
|
|
103
|
+
addSkill: (skill: Skill) => void
|
|
104
|
+
updateSkill: (id: string, patch: Partial<Skill>) => void
|
|
105
|
+
removeSkill: (id: string) => void
|
|
106
|
+
setMode: (mode: AppSettings['mode']) => void
|
|
107
|
+
flush: () => Promise<void>
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function notify(message: string, tone: 'info' | 'success' | 'error' = 'error') {
|
|
111
|
+
if (typeof window === 'undefined') return
|
|
112
|
+
window.dispatchEvent(new CustomEvent('maxcode:notice', { detail: { message, tone } }))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function mergeTools(saved?: Partial<Record<ToolName, Partial<ToolSetting>>>) {
|
|
116
|
+
const merged = {} as Record<ToolName, ToolSetting>
|
|
117
|
+
for (const key of Object.keys(DEFAULT_TOOLS) as ToolName[]) {
|
|
118
|
+
merged[key] = { ...DEFAULT_TOOLS[key], ...(saved?.[key] ?? {}) }
|
|
119
|
+
}
|
|
120
|
+
return merged
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function mergeSkills(saved?: Skill[]) {
|
|
124
|
+
const byId = new Map<string, Skill>()
|
|
125
|
+
for (const skill of BUILTIN_SKILLS) byId.set(skill.id, skill)
|
|
126
|
+
for (const skill of saved ?? []) {
|
|
127
|
+
if (!skill || typeof skill.id !== 'string') continue
|
|
128
|
+
byId.set(skill.id, { ...(byId.get(skill.id) ?? {}), ...skill, builtIn: skill.builtIn ?? byId.has(skill.id) })
|
|
129
|
+
}
|
|
130
|
+
return [...byId.values()]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function upgradePrompt(value: unknown) {
|
|
134
|
+
if (typeof value !== 'string' || !value.trim()) return DEFAULT_SYSTEM_PROMPT
|
|
135
|
+
const legacy =
|
|
136
|
+
/sandboxed workspace stored in the user's browser/i.test(value) ||
|
|
137
|
+
/There is no node, npm, git or network in the shell/i.test(value) ||
|
|
138
|
+
/workspace root is \/workspace/i.test(value)
|
|
139
|
+
return legacy ? DEFAULT_SYSTEM_PROMPT : value
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function sanitizeBaseUrls(saved: AppSettings['providers'] | undefined) {
|
|
143
|
+
const providers: AppSettings['providers'] = {}
|
|
144
|
+
for (const [id, value] of Object.entries(saved ?? {})) {
|
|
145
|
+
if (!(id in PROVIDER_MAP) || !value || typeof value !== 'object') continue
|
|
146
|
+
if (typeof value.baseUrl === 'string' && value.baseUrl.trim()) {
|
|
147
|
+
providers[id] = { baseUrl: value.baseUrl.trim() }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return providers
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildSettings(persisted?: Partial<AppSettings>): AppSettings {
|
|
154
|
+
const saved = persisted ?? {}
|
|
155
|
+
const provider =
|
|
156
|
+
typeof saved.provider === 'string' && saved.provider in PROVIDER_MAP
|
|
157
|
+
? (saved.provider as ProviderId)
|
|
158
|
+
: DEFAULTS.provider
|
|
159
|
+
const mode = saved.mode === 'plan' ? 'plan' : saved.mode === 'build' ? 'build' : DEFAULTS.mode
|
|
160
|
+
return {
|
|
161
|
+
...DEFAULTS,
|
|
162
|
+
...saved,
|
|
163
|
+
provider,
|
|
164
|
+
model: typeof saved.model === 'string' ? saved.model.trim() : '',
|
|
165
|
+
mode,
|
|
166
|
+
effort: typeof saved.effort === 'string' && EFFORTS.has(saved.effort as Effort)
|
|
167
|
+
? (saved.effort as Effort)
|
|
168
|
+
: DEFAULTS.effort,
|
|
169
|
+
webSearchEnabled: saved.webSearchEnabled !== false,
|
|
170
|
+
showTokenEstimate: saved.showTokenEstimate !== false,
|
|
171
|
+
temperature:
|
|
172
|
+
typeof saved.temperature === 'number' && Number.isFinite(saved.temperature)
|
|
173
|
+
? Math.max(0, Math.min(2, saved.temperature))
|
|
174
|
+
: DEFAULTS.temperature,
|
|
175
|
+
maxTokens:
|
|
176
|
+
typeof saved.maxTokens === 'number' && Number.isFinite(saved.maxTokens)
|
|
177
|
+
? Math.max(256, Math.min(200_000, Math.round(saved.maxTokens)))
|
|
178
|
+
: DEFAULTS.maxTokens,
|
|
179
|
+
systemPrompt: upgradePrompt(saved.systemPrompt),
|
|
180
|
+
providers: sanitizeBaseUrls(saved.providers),
|
|
181
|
+
searchProvider:
|
|
182
|
+
saved.searchProvider && ['duckduckgo', 'tavily', 'brave', 'serper'].includes(saved.searchProvider)
|
|
183
|
+
? saved.searchProvider
|
|
184
|
+
: DEFAULTS.searchProvider,
|
|
185
|
+
tools: mergeTools(saved.tools),
|
|
186
|
+
skills: mergeSkills(saved.skills),
|
|
187
|
+
theme: normalizeTheme(saved.theme),
|
|
188
|
+
chatFontSize:
|
|
189
|
+
typeof saved.chatFontSize === 'number' && Number.isFinite(saved.chatFontSize)
|
|
190
|
+
? Math.max(12, Math.min(20, Math.round(saved.chatFontSize)))
|
|
191
|
+
: DEFAULTS.chatFontSize,
|
|
192
|
+
workDetail: saved.workDetail === 'concise' ? 'concise' : 'detailed',
|
|
193
|
+
autoCollapseWork: saved.autoCollapseWork !== false,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let persistEnabled = false
|
|
198
|
+
let pending: Partial<AppSettings> = {}
|
|
199
|
+
let saveTimer: number | undefined
|
|
200
|
+
let maxTimer: number | undefined
|
|
201
|
+
|
|
202
|
+
function persistedPatch(patch: Partial<AppSettings>): Partial<AppSettings> {
|
|
203
|
+
const out: Record<string, unknown> = {}
|
|
204
|
+
const keys: Array<keyof AppSettings> = [
|
|
205
|
+
'provider',
|
|
206
|
+
'model',
|
|
207
|
+
'effort',
|
|
208
|
+
'webSearchEnabled',
|
|
209
|
+
'temperature',
|
|
210
|
+
'maxTokens',
|
|
211
|
+
'systemPrompt',
|
|
212
|
+
'providers',
|
|
213
|
+
'searchProvider',
|
|
214
|
+
'tools',
|
|
215
|
+
'skills',
|
|
216
|
+
'theme',
|
|
217
|
+
'showTokenEstimate',
|
|
218
|
+
'mode',
|
|
219
|
+
'chatFontSize',
|
|
220
|
+
'workDetail',
|
|
221
|
+
'autoCollapseWork',
|
|
222
|
+
]
|
|
223
|
+
for (const key of keys) {
|
|
224
|
+
if (key in patch) out[key] = patch[key]
|
|
225
|
+
}
|
|
226
|
+
return out as Partial<AppSettings>
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function flushSettings() {
|
|
230
|
+
if (!persistEnabled || Object.keys(pending).length === 0) return Promise.resolve()
|
|
231
|
+
const body = persistedPatch(pending)
|
|
232
|
+
pending = {}
|
|
233
|
+
if (saveTimer !== undefined) window.clearTimeout(saveTimer)
|
|
234
|
+
if (maxTimer !== undefined) window.clearTimeout(maxTimer)
|
|
235
|
+
saveTimer = undefined
|
|
236
|
+
maxTimer = undefined
|
|
237
|
+
return patchSettings(body)
|
|
238
|
+
.then(() => undefined)
|
|
239
|
+
.catch((error) => {
|
|
240
|
+
notify(error instanceof Error ? error.message : 'Could not save settings to the local daemon.')
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function queueSave(patch: Partial<AppSettings>) {
|
|
245
|
+
if (!persistEnabled) return
|
|
246
|
+
pending = { ...pending, ...persistedPatch(patch) }
|
|
247
|
+
if (saveTimer !== undefined) window.clearTimeout(saveTimer)
|
|
248
|
+
saveTimer = window.setTimeout(() => void flushSettings(), 550)
|
|
249
|
+
if (maxTimer === undefined) {
|
|
250
|
+
maxTimer = window.setTimeout(() => void flushSettings(), 4000)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function enableSettingsPersistence() {
|
|
255
|
+
persistEnabled = true
|
|
256
|
+
if (Object.keys(pending).length > 0) queueSave({})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export const useSettings = create<SettingsStore>()((set, get) => ({
|
|
260
|
+
...DEFAULTS,
|
|
261
|
+
hydrated: false,
|
|
262
|
+
daemon: null,
|
|
263
|
+
secretStatus: null,
|
|
264
|
+
providerStatus: [],
|
|
265
|
+
deployTools: null,
|
|
266
|
+
deploySecrets: null,
|
|
267
|
+
secretsError: '',
|
|
268
|
+
hydrate: (snapshot) => {
|
|
269
|
+
const settings = buildSettings(snapshot.settings as Partial<AppSettings>)
|
|
270
|
+
const statuses = snapshot.providers
|
|
271
|
+
const configured = statuses.filter((provider) => provider.configured)
|
|
272
|
+
if (!providerHasCredentialsRaw(settings, statuses, settings.provider) && configured.length > 0) {
|
|
273
|
+
settings.provider = configured[0].id
|
|
274
|
+
settings.model = ''
|
|
275
|
+
}
|
|
276
|
+
if (!providerHasCredentialsRaw(settings, statuses, settings.provider)) settings.model = ''
|
|
277
|
+
set({
|
|
278
|
+
...settings,
|
|
279
|
+
hydrated: true,
|
|
280
|
+
daemon: snapshot,
|
|
281
|
+
secretStatus: snapshot.secrets,
|
|
282
|
+
providerStatus: statuses,
|
|
283
|
+
deployTools: snapshot.deployTools,
|
|
284
|
+
deploySecrets: snapshot.deploySecrets,
|
|
285
|
+
secretsError: snapshot.secretsError,
|
|
286
|
+
})
|
|
287
|
+
},
|
|
288
|
+
update: (patch) => {
|
|
289
|
+
set(patch)
|
|
290
|
+
queueSave(patch)
|
|
291
|
+
},
|
|
292
|
+
selectModel: (provider, model, effort) => {
|
|
293
|
+
const nextEffort = effort ?? get().effort
|
|
294
|
+
const patch: Partial<AppSettings> = { provider, model: model.trim(), effort: nextEffort }
|
|
295
|
+
set(patch)
|
|
296
|
+
queueSave(patch)
|
|
297
|
+
},
|
|
298
|
+
updateProvider: (provider, patch) => {
|
|
299
|
+
const next = {
|
|
300
|
+
providers: {
|
|
301
|
+
...get().providers,
|
|
302
|
+
[provider]: { ...(get().providers[provider] ?? {}), ...patch },
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
set(next)
|
|
306
|
+
queueSave(next)
|
|
307
|
+
},
|
|
308
|
+
saveProviderKey: async (provider, apiKey) => {
|
|
309
|
+
const result = await setProviderKey(provider, apiKey.trim())
|
|
310
|
+
const wasConfigured = providerHasCredentials(get(), provider)
|
|
311
|
+
set((state) => {
|
|
312
|
+
const secretStatus = state.secretStatus
|
|
313
|
+
? {
|
|
314
|
+
...state.secretStatus,
|
|
315
|
+
providers: {
|
|
316
|
+
...state.secretStatus.providers,
|
|
317
|
+
[provider]: { configured: result.configured, hint: result.hint },
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
: state.secretStatus
|
|
321
|
+
const providerStatus = state.providerStatus.map((entry) =>
|
|
322
|
+
entry.id === provider ? { ...entry, configured: result.configured, hint: result.hint } : entry,
|
|
323
|
+
)
|
|
324
|
+
const activate = !result.configured ? false : !hasCredentials(state) || state.provider === provider
|
|
325
|
+
const lost = state.provider === provider && !result.configured
|
|
326
|
+
const patch: Partial<AppSettings> = {}
|
|
327
|
+
if (activate) patch.provider = provider
|
|
328
|
+
if (lost) patch.model = ''
|
|
329
|
+
return {
|
|
330
|
+
secretStatus,
|
|
331
|
+
providerStatus,
|
|
332
|
+
...patch,
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
if (!wasConfigured && result.configured) {
|
|
336
|
+
notify(`${PROVIDER_MAP[provider]?.name ?? provider} is connected.`, 'success')
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
saveSearchKey: async (apiKey) => {
|
|
340
|
+
const result = await saveSearchKey(apiKey.trim())
|
|
341
|
+
set((state) => ({
|
|
342
|
+
secretStatus: state.secretStatus
|
|
343
|
+
? { ...state.secretStatus, search: { configured: result.configured, hint: result.hint } }
|
|
344
|
+
: state.secretStatus,
|
|
345
|
+
}))
|
|
346
|
+
},
|
|
347
|
+
saveDeployCredentials: async (provider, fields) => {
|
|
348
|
+
const result = (await saveDeployCredentials(provider, fields)) as DeploySecretEntry
|
|
349
|
+
set((state) => ({
|
|
350
|
+
secretStatus: state.secretStatus
|
|
351
|
+
? {
|
|
352
|
+
...state.secretStatus,
|
|
353
|
+
deploy: { ...state.secretStatus.deploy, [provider]: result },
|
|
354
|
+
}
|
|
355
|
+
: state.secretStatus,
|
|
356
|
+
deploySecrets: state.deploySecrets
|
|
357
|
+
? { ...state.deploySecrets, [provider]: result }
|
|
358
|
+
: state.deploySecrets,
|
|
359
|
+
}))
|
|
360
|
+
},
|
|
361
|
+
refreshProviders: async () => {
|
|
362
|
+
const result = await listProviders()
|
|
363
|
+
set({ providerStatus: result.providers })
|
|
364
|
+
},
|
|
365
|
+
updateTool: (tool, patch) => {
|
|
366
|
+
const next = { tools: { ...get().tools, [tool]: { ...get().tools[tool], ...patch } } }
|
|
367
|
+
set(next)
|
|
368
|
+
queueSave(next)
|
|
369
|
+
},
|
|
370
|
+
addSkill: (skill) => {
|
|
371
|
+
const next = { skills: [...get().skills, skill] }
|
|
372
|
+
set(next)
|
|
373
|
+
queueSave(next)
|
|
374
|
+
},
|
|
375
|
+
updateSkill: (id, patch) => {
|
|
376
|
+
const next = { skills: get().skills.map((skill) => (skill.id === id ? { ...skill, ...patch } : skill)) }
|
|
377
|
+
set(next)
|
|
378
|
+
queueSave(next)
|
|
379
|
+
},
|
|
380
|
+
removeSkill: (id) => {
|
|
381
|
+
const next = { skills: get().skills.filter((skill) => skill.id !== id) }
|
|
382
|
+
set(next)
|
|
383
|
+
queueSave(next)
|
|
384
|
+
},
|
|
385
|
+
setMode: (mode) => {
|
|
386
|
+
const patch = { mode }
|
|
387
|
+
set(patch)
|
|
388
|
+
queueSave(patch)
|
|
389
|
+
},
|
|
390
|
+
flush: () => flushSettings(),
|
|
391
|
+
}))
|
|
392
|
+
|
|
393
|
+
export function activeModelId(settings: AppSettings): string {
|
|
394
|
+
return settings.model.trim()
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function providerHasCredentials(settings: AppSettings, provider: ProviderId): boolean {
|
|
398
|
+
return providerHasCredentialsRaw(settings, useSettings.getState().providerStatus, provider)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function providerStatusFor(provider: ProviderId) {
|
|
402
|
+
return useSettings.getState().providerStatus.find((entry) => entry.id === provider)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function providerHasCredentialsRaw(
|
|
406
|
+
settings: AppSettings,
|
|
407
|
+
statuses: DaemonSnapshot['providers'],
|
|
408
|
+
provider: ProviderId,
|
|
409
|
+
) {
|
|
410
|
+
const preset = PROVIDER_MAP[provider]
|
|
411
|
+
if (!preset) return false
|
|
412
|
+
const status = statuses.find((entry) => entry.id === provider)
|
|
413
|
+
if (status) return status.configured
|
|
414
|
+
if (provider === 'custom') return Boolean(settings.providers.custom?.baseUrl?.trim())
|
|
415
|
+
return Boolean(preset.keyless)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function hasCredentials(settings: AppSettings): boolean {
|
|
419
|
+
return providerHasCredentials(settings, settings.provider)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function resolveBaseUrl(settings: AppSettings): string {
|
|
423
|
+
const preset = PROVIDER_MAP[settings.provider]
|
|
424
|
+
const custom = settings.providers[settings.provider]?.baseUrl?.trim()
|
|
425
|
+
return (custom || preset?.baseUrl || '').replace(/\/+$/, '')
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function activeEffort(settings: AppSettings, model?: Parameters<typeof normalizeEffort>[1]) {
|
|
429
|
+
return normalizeEffort(settings.effort, model)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function preferredEffort(model?: Parameters<typeof defaultEffort>[0]) {
|
|
433
|
+
return defaultEffort(model)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export type { SecretEntry, DeployToolInfo }
|
|
437
|
+
|
|
438
|
+
if (typeof window !== 'undefined') {
|
|
439
|
+
window.addEventListener('beforeunload', () => {
|
|
440
|
+
void useSettings.getState().flush()
|
|
441
|
+
})
|
|
442
|
+
document.addEventListener('visibilitychange', () => {
|
|
443
|
+
if (document.visibilityState === 'hidden') void useSettings.getState().flush()
|
|
444
|
+
})
|
|
445
|
+
}
|
package/src/store/ui.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
|
|
3
|
+
interface RuntimeState {
|
|
4
|
+
streaming: Record<string, boolean>
|
|
5
|
+
setStreaming: (sessionId: string, active: boolean) => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const useRuntime = create<RuntimeState>()((set) => ({
|
|
9
|
+
streaming: {},
|
|
10
|
+
setStreaming: (sessionId, active) =>
|
|
11
|
+
set((state) => {
|
|
12
|
+
if (Boolean(state.streaming[sessionId]) === active) return state
|
|
13
|
+
const streaming = { ...state.streaming }
|
|
14
|
+
if (active) streaming[sessionId] = true
|
|
15
|
+
else delete streaming[sessionId]
|
|
16
|
+
return { streaming }
|
|
17
|
+
}),
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
type ToastTone = 'info' | 'success' | 'error'
|
|
21
|
+
|
|
22
|
+
interface Toast {
|
|
23
|
+
id: number
|
|
24
|
+
message: string
|
|
25
|
+
tone: ToastTone
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ConfirmOptions {
|
|
29
|
+
title: string
|
|
30
|
+
body: string
|
|
31
|
+
confirmLabel?: string
|
|
32
|
+
tone?: 'danger' | 'default'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface PendingConfirm extends ConfirmOptions {
|
|
36
|
+
resolve: (value: boolean) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface UiState {
|
|
40
|
+
sidebarCollapsed: boolean
|
|
41
|
+
toggleSidebar: () => void
|
|
42
|
+
mobileSidebarOpen: boolean
|
|
43
|
+
setMobileSidebarOpen: (open: boolean) => void
|
|
44
|
+
workspaceOpen: boolean
|
|
45
|
+
setWorkspaceOpen: (open: boolean) => void
|
|
46
|
+
settingsOpen: boolean
|
|
47
|
+
settingsTab:
|
|
48
|
+
| 'general'
|
|
49
|
+
| 'models'
|
|
50
|
+
| 'tools'
|
|
51
|
+
| 'search'
|
|
52
|
+
| 'skills'
|
|
53
|
+
| 'plugins'
|
|
54
|
+
| 'deploy'
|
|
55
|
+
| 'appearance'
|
|
56
|
+
| 'about'
|
|
57
|
+
openSettings: (tab?: UiState['settingsTab']) => void
|
|
58
|
+
closeSettings: () => void
|
|
59
|
+
deployOpen: boolean
|
|
60
|
+
setDeployOpen: (open: boolean) => void
|
|
61
|
+
paletteOpen: boolean
|
|
62
|
+
setPaletteOpen: (open: boolean) => void
|
|
63
|
+
composerFocus: number
|
|
64
|
+
focusComposer: () => void
|
|
65
|
+
toast: Toast | null
|
|
66
|
+
notify: (message: string, tone?: ToastTone) => void
|
|
67
|
+
dismissToast: (id: number) => void
|
|
68
|
+
confirm: PendingConfirm | null
|
|
69
|
+
askConfirm: (options: ConfirmOptions) => Promise<boolean>
|
|
70
|
+
resolveConfirm: (value: boolean) => void
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sidebarWasCollapsed() {
|
|
74
|
+
try {
|
|
75
|
+
return localStorage.getItem('maxcode.sidebar-collapsed') === '1'
|
|
76
|
+
} catch {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function rememberSidebar(collapsed: boolean) {
|
|
82
|
+
try {
|
|
83
|
+
localStorage.setItem('maxcode.sidebar-collapsed', collapsed ? '1' : '0')
|
|
84
|
+
} catch {
|
|
85
|
+
// The preference is only a convenience; the session still works without it.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const useUi = create<UiState>()((set, get) => ({
|
|
90
|
+
sidebarCollapsed: sidebarWasCollapsed(),
|
|
91
|
+
toggleSidebar: () =>
|
|
92
|
+
set((state) => {
|
|
93
|
+
const next = !state.sidebarCollapsed
|
|
94
|
+
rememberSidebar(next)
|
|
95
|
+
return { sidebarCollapsed: next }
|
|
96
|
+
}),
|
|
97
|
+
mobileSidebarOpen: false,
|
|
98
|
+
setMobileSidebarOpen: (open) => set({ mobileSidebarOpen: open }),
|
|
99
|
+
workspaceOpen: false,
|
|
100
|
+
setWorkspaceOpen: (open) => set({ workspaceOpen: open }),
|
|
101
|
+
settingsOpen: false,
|
|
102
|
+
settingsTab: 'general',
|
|
103
|
+
openSettings: (tab = 'general') => set({ settingsOpen: true, settingsTab: tab, mobileSidebarOpen: false }),
|
|
104
|
+
closeSettings: () => set({ settingsOpen: false }),
|
|
105
|
+
deployOpen: false,
|
|
106
|
+
setDeployOpen: (open) => set({ deployOpen: open }),
|
|
107
|
+
paletteOpen: false,
|
|
108
|
+
setPaletteOpen: (open) => set({ paletteOpen: open }),
|
|
109
|
+
composerFocus: 0,
|
|
110
|
+
focusComposer: () => set((state) => ({ composerFocus: state.composerFocus + 1 })),
|
|
111
|
+
toast: null,
|
|
112
|
+
notify: (message, tone = 'info') =>
|
|
113
|
+
set({ toast: { id: Date.now() + Math.random(), message, tone } }),
|
|
114
|
+
dismissToast: (id) =>
|
|
115
|
+
set((state) => (state.toast?.id === id ? { toast: null } : state)),
|
|
116
|
+
confirm: null,
|
|
117
|
+
askConfirm: (options) =>
|
|
118
|
+
new Promise<boolean>((resolve) => {
|
|
119
|
+
if (get().confirm) get().confirm?.resolve(false)
|
|
120
|
+
set({ confirm: { ...options, resolve } })
|
|
121
|
+
}),
|
|
122
|
+
resolveConfirm: (value) => {
|
|
123
|
+
const confirm = get().confirm
|
|
124
|
+
set({ confirm: null })
|
|
125
|
+
confirm?.resolve(value)
|
|
126
|
+
},
|
|
127
|
+
}))
|
|
128
|
+
|
|
129
|
+
window.addEventListener('maxcode:notice', (event) => {
|
|
130
|
+
const detail = (event as CustomEvent<{ message: string; tone?: ToastTone }>).detail
|
|
131
|
+
if (detail?.message) useUi.getState().notify(detail.message, detail.tone ?? 'info')
|
|
132
|
+
})
|