@bibobonking/opensol 3.1.3 → 3.1.5
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/CHANGELOG.md +13 -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/dist/assets/index-C0yvs6Vl.js +281 -0
- package/dist/assets/index-CxNPLBg7.css +1 -0
- package/dist/favicon.svg +18 -15
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/public/favicon.svg +18 -15
- package/server/daemon.mjs +66 -49
- package/server/lib/deploy.mjs +222 -121
- package/server/lib/providers.mjs +23 -1
- package/server/lib/skills.mjs +385 -100
- package/server/lib/tools.mjs +0 -40
- package/src/agent/adapters/openai-responses.ts +1 -5
- package/src/agent/adapters/openai.ts +4 -11
- package/src/agent/prompt.ts +7 -14
- package/src/agent/subagent.ts +3 -14
- package/src/agent/tools.ts +2 -0
- package/src/components/ChatView.tsx +2 -1
- package/src/components/Composer.tsx +2 -1
- package/src/components/ContextBar.tsx +10 -0
- package/src/components/DeployModal.tsx +266 -252
- package/src/components/FileIcon.tsx +391 -99
- package/src/components/HomeDashboard.tsx +5 -10
- package/src/components/Logo.tsx +9 -11
- package/src/components/ModelDeck.tsx +8 -11
- package/src/components/SettingsModal.tsx +160 -122
- package/src/components/Sidebar.tsx +4 -3
- package/src/components/SystemPromptButton.tsx +198 -0
- package/src/components/ToolCallCard.tsx +2 -0
- package/src/components/TopBar.tsx +2 -19
- package/src/components/WorkspacePanel.tsx +48 -19
- package/src/core/plugins/tools/skill.ts +19 -76
- package/src/core/services/agent-loop.ts +16 -21
- package/src/core/services/subagents.ts +2 -2
- package/src/core/services/system-prompt.ts +16 -23
- package/src/core/services/tools.ts +0 -1
- package/src/lib/boot.ts +2 -0
- package/src/lib/credits.ts +33 -0
- package/src/lib/daemon.ts +40 -19
- package/src/lib/permissions.ts +1 -1
- package/src/lib/retry.test.mjs +45 -0
- package/src/lib/retry.ts +46 -0
- package/src/lib/system-prompt.ts +23 -0
- package/src/lib/usage.ts +8 -13
- package/src/store/chats.ts +0 -1
- package/src/store/settings.ts +12 -44
- package/src/store/skills.ts +79 -0
- package/src/store/ui.ts +3 -1
- package/src/types.ts +30 -15
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/assets/index-DaJcH9ts.css +0 -1
- package/dist/assets/index-hs0KWMP6.js +0 -288
- package/src/components/SystemPromptModal.tsx +0 -168
- package/src/lib/skills.test.mjs +0 -22
- package/src/lib/skills.ts +0 -93
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import { Check, MessageSquareCode, RotateCcw } from 'lucide-react'
|
|
3
|
-
import type { ChatSession } from '../types'
|
|
4
|
-
import { useChats } from '../store/chats'
|
|
5
|
-
import { DEFAULT_SYSTEM_PROMPT, useSettings } from '../store/settings'
|
|
6
|
-
import { useUi } from '../store/ui'
|
|
7
|
-
import { useWorkspace } from '../store/workspace'
|
|
8
|
-
import { cn } from '../lib/utils'
|
|
9
|
-
import { Modal, ModalHeader } from './ui'
|
|
10
|
-
|
|
11
|
-
type Mode = 'shared' | 'workspace' | 'session'
|
|
12
|
-
|
|
13
|
-
const MODES: Array<{ id: Mode; label: string; hint: string }> = [
|
|
14
|
-
{ id: 'shared', label: 'Shared prompt', hint: 'The general prompt from Settings → Models.' },
|
|
15
|
-
{ id: 'workspace', label: 'This workspace', hint: 'Applies to every session opened in this folder.' },
|
|
16
|
-
{ id: 'session', label: 'This session', hint: 'Only this conversation. Overrides the workspace prompt.' },
|
|
17
|
-
]
|
|
18
|
-
|
|
19
|
-
export function SystemPromptModal({
|
|
20
|
-
session,
|
|
21
|
-
open,
|
|
22
|
-
onClose,
|
|
23
|
-
}: {
|
|
24
|
-
session: ChatSession | null
|
|
25
|
-
open: boolean
|
|
26
|
-
onClose: () => void
|
|
27
|
-
}) {
|
|
28
|
-
const settings = useSettings()
|
|
29
|
-
const update = useSettings((state) => state.update)
|
|
30
|
-
const updateSession = useChats((state) => state.updateSession)
|
|
31
|
-
const notify = useUi((state) => state.notify)
|
|
32
|
-
const workspaceRoot = useWorkspace((state) => state.root)
|
|
33
|
-
const root = session?.workspace || workspaceRoot || ''
|
|
34
|
-
const workspacePrompt = root ? settings.workspacePrompts[root] ?? '' : ''
|
|
35
|
-
const sessionPrompt = session?.systemPrompt ?? ''
|
|
36
|
-
const current: Mode = sessionPrompt.trim() ? 'session' : workspacePrompt.trim() ? 'workspace' : 'shared'
|
|
37
|
-
|
|
38
|
-
const [mode, setMode] = useState<Mode>(current)
|
|
39
|
-
const [draft, setDraft] = useState('')
|
|
40
|
-
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
if (!open) return
|
|
43
|
-
setMode(current)
|
|
44
|
-
setDraft(current === 'session' ? sessionPrompt : current === 'workspace' ? workspacePrompt : '')
|
|
45
|
-
}, [open, current, sessionPrompt, workspacePrompt])
|
|
46
|
-
|
|
47
|
-
const editorValue = mode === 'shared' ? settings.systemPrompt : draft
|
|
48
|
-
|
|
49
|
-
const save = () => {
|
|
50
|
-
if (mode === 'shared') {
|
|
51
|
-
if (session) updateSession(session.id, { systemPrompt: '' })
|
|
52
|
-
if (root) update({ workspacePrompts: { ...settings.workspacePrompts, [root]: '' } })
|
|
53
|
-
notify('Using the shared system prompt.', 'success')
|
|
54
|
-
onClose()
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
if (mode === 'workspace') {
|
|
58
|
-
if (!root) {
|
|
59
|
-
notify('Open a workspace folder before saving a workspace prompt.', 'error')
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
const value = draft.trim()
|
|
63
|
-
if (!value) {
|
|
64
|
-
notify('Write a prompt or choose the shared prompt.', 'error')
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
update({ workspacePrompts: { ...settings.workspacePrompts, [root]: value } })
|
|
68
|
-
if (session) updateSession(session.id, { systemPrompt: '' })
|
|
69
|
-
notify('Workspace system prompt saved.', 'success')
|
|
70
|
-
onClose()
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
if (!session) return
|
|
74
|
-
const value = draft.trim()
|
|
75
|
-
if (!value) {
|
|
76
|
-
notify('Write a prompt or choose the shared prompt.', 'error')
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
updateSession(session.id, { systemPrompt: value })
|
|
80
|
-
notify('Session system prompt saved.', 'success')
|
|
81
|
-
onClose()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
<Modal open={open} onClose={onClose} labelledBy="system-prompt-title">
|
|
86
|
-
<ModalHeader
|
|
87
|
-
id="system-prompt-title"
|
|
88
|
-
title="System prompt"
|
|
89
|
-
description="Choose what the model is told before this conversation. Session overrides workspace, which overrides the shared prompt."
|
|
90
|
-
onClose={onClose}
|
|
91
|
-
/>
|
|
92
|
-
<div className="thin-scroll min-h-0 flex-1 overflow-y-auto p-5">
|
|
93
|
-
<div className="grid gap-1.5 sm:grid-cols-3">
|
|
94
|
-
{MODES.map((entry) => {
|
|
95
|
-
const active = mode === entry.id
|
|
96
|
-
const disabled = entry.id === 'workspace' && !root
|
|
97
|
-
return (
|
|
98
|
-
<button
|
|
99
|
-
key={entry.id}
|
|
100
|
-
type="button"
|
|
101
|
-
disabled={disabled}
|
|
102
|
-
onClick={() => {
|
|
103
|
-
setMode(entry.id)
|
|
104
|
-
setDraft(entry.id === 'workspace' ? workspacePrompt : entry.id === 'session' ? sessionPrompt : '')
|
|
105
|
-
}}
|
|
106
|
-
className={cn(
|
|
107
|
-
'rounded-xl border px-3 py-2.5 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-40',
|
|
108
|
-
active ? 'border-accent/40 bg-accent-soft' : 'border-line bg-panel hover:bg-raised/60',
|
|
109
|
-
)}
|
|
110
|
-
>
|
|
111
|
-
<span className={cn('flex items-center gap-1.5 text-[11.5px] font-medium', active ? 'text-accent' : 'text-ink')}>
|
|
112
|
-
<MessageSquareCode size={11} />
|
|
113
|
-
{entry.label}
|
|
114
|
-
</span>
|
|
115
|
-
<span className="mt-1 block text-[9.5px] leading-relaxed text-faint">{entry.hint}</span>
|
|
116
|
-
</button>
|
|
117
|
-
)
|
|
118
|
-
})}
|
|
119
|
-
</div>
|
|
120
|
-
|
|
121
|
-
<div className="mt-4">
|
|
122
|
-
<div className="mb-1 flex items-center justify-between gap-3">
|
|
123
|
-
<span className="text-[10.5px] font-medium text-muted">
|
|
124
|
-
{mode === 'shared' ? 'Shared prompt (edited in Settings → Models)' : mode === 'workspace' ? `Workspace prompt · ${root}` : 'Session prompt'}
|
|
125
|
-
</span>
|
|
126
|
-
{mode !== 'shared' && (
|
|
127
|
-
<button
|
|
128
|
-
type="button"
|
|
129
|
-
onClick={() => setDraft(DEFAULT_SYSTEM_PROMPT)}
|
|
130
|
-
className="inline-flex items-center gap-1 text-[9.5px] font-medium text-faint hover:text-ink"
|
|
131
|
-
>
|
|
132
|
-
<RotateCcw size={10} /> Start from default
|
|
133
|
-
</button>
|
|
134
|
-
)}
|
|
135
|
-
</div>
|
|
136
|
-
<textarea
|
|
137
|
-
value={editorValue}
|
|
138
|
-
onChange={(event) => {
|
|
139
|
-
if (mode === 'shared') update({ systemPrompt: event.target.value })
|
|
140
|
-
else setDraft(event.target.value)
|
|
141
|
-
}}
|
|
142
|
-
readOnly={mode === 'shared'}
|
|
143
|
-
rows={11}
|
|
144
|
-
spellCheck={false}
|
|
145
|
-
data-autofocus
|
|
146
|
-
className="thin-scroll w-full resize-y rounded-xl border border-line bg-canvas/50 px-3 py-2.5 font-mono text-[10.5px] leading-relaxed text-ink outline-none focus:border-accent/50 read-only:opacity-70"
|
|
147
|
-
placeholder="You are a focused React reviewer…"
|
|
148
|
-
/>
|
|
149
|
-
<p className="mt-1.5 text-[9px] leading-relaxed text-faint">
|
|
150
|
-
Workspace context, project instructions, skills and tool rules are appended after this prompt. A custom prompt replaces the shared one.
|
|
151
|
-
</p>
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
<div className="flex shrink-0 items-center justify-end gap-2 border-t border-line px-5 py-3">
|
|
155
|
-
<button type="button" onClick={onClose} className="chip h-8 px-3 text-[11px]">
|
|
156
|
-
Cancel
|
|
157
|
-
</button>
|
|
158
|
-
<button
|
|
159
|
-
type="button"
|
|
160
|
-
onClick={save}
|
|
161
|
-
className="inline-flex h-8 items-center gap-1.5 rounded-lg bg-accent px-3 text-[11px] font-medium text-on-accent hover:bg-accent/90"
|
|
162
|
-
>
|
|
163
|
-
<Check size={12} /> Save
|
|
164
|
-
</button>
|
|
165
|
-
</div>
|
|
166
|
-
</Modal>
|
|
167
|
-
)
|
|
168
|
-
}
|
package/src/lib/skills.test.mjs
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { parseSkillMarkdown } from './skills.ts'
|
|
4
|
-
|
|
5
|
-
test('frontmatter supplies the name and description', () => {
|
|
6
|
-
const skill = parseSkillMarkdown('---\nname: API reviewer\ndescription: "Checks a diff"\n---\n# Ignored\n\nBody text.', 'fallback')
|
|
7
|
-
assert.equal(skill.name, 'API reviewer')
|
|
8
|
-
assert.equal(skill.description, 'Checks a diff')
|
|
9
|
-
assert.equal(skill.instructions, '# Ignored\n\nBody text.')
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
test('a plain SKILL.md falls back to its heading and first paragraph', () => {
|
|
13
|
-
const skill = parseSkillMarkdown('# Deploy helper\n\nRuns the release checklist.\n\n## Steps\n1. build', 'fallback')
|
|
14
|
-
assert.equal(skill.name, 'Deploy helper')
|
|
15
|
-
assert.equal(skill.description, 'Runs the release checklist.')
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
test('an empty fallback markdown still returns a usable skill', () => {
|
|
19
|
-
const skill = parseSkillMarkdown('', 'my-skill')
|
|
20
|
-
assert.equal(skill.name, 'my-skill')
|
|
21
|
-
assert.equal(skill.description, 'Installed from a folder')
|
|
22
|
-
})
|
package/src/lib/skills.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import type { Skill } from '../types'
|
|
2
|
-
|
|
3
|
-
const TEXT_FILE = /\.(md|markdown|txt|json|ya?ml|toml|js|mjs|cjs|ts|tsx|jsx|py|sh|bash|zsh|css|html|xml|csv)$/i
|
|
4
|
-
const MAX_FILE = 512 * 1024
|
|
5
|
-
const MAX_TOTAL = 4 * 1024 * 1024
|
|
6
|
-
const MAX_FILES = 200
|
|
7
|
-
|
|
8
|
-
export interface ParsedSkill {
|
|
9
|
-
name: string
|
|
10
|
-
description: string
|
|
11
|
-
instructions: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Read the optional YAML frontmatter, then the first heading and paragraph. */
|
|
15
|
-
export function parseSkillMarkdown(markdown: string, fallbackName: string): ParsedSkill {
|
|
16
|
-
const text = String(markdown ?? '').replace(/\r\n/g, '\n')
|
|
17
|
-
const front = /^---\n([\s\S]*?)\n---\n?/.exec(text)
|
|
18
|
-
const meta: Record<string, string> = {}
|
|
19
|
-
let body = text
|
|
20
|
-
if (front) {
|
|
21
|
-
for (const line of front[1].split('\n')) {
|
|
22
|
-
const match = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim())
|
|
23
|
-
if (match) meta[match[1].toLowerCase()] = match[2].replace(/^["']|["']$/g, '').trim()
|
|
24
|
-
}
|
|
25
|
-
body = text.slice(front[0].length)
|
|
26
|
-
}
|
|
27
|
-
const heading = /^#\s+(.+)$/m.exec(body)
|
|
28
|
-
const paragraph = body
|
|
29
|
-
.split(/\n\s*\n/)
|
|
30
|
-
.map((part) => part.trim())
|
|
31
|
-
.find((part) => part && !part.startsWith('#') && !part.startsWith('---'))
|
|
32
|
-
return {
|
|
33
|
-
name: meta.name || heading?.[1]?.trim() || fallbackName,
|
|
34
|
-
description: (meta.description || paragraph || 'Installed from a folder')
|
|
35
|
-
.replace(/[#>*`]/g, '')
|
|
36
|
-
.replace(/\s+/g, ' ')
|
|
37
|
-
.slice(0, 200),
|
|
38
|
-
instructions: body.trim(),
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function slug(value: string) {
|
|
43
|
-
return (
|
|
44
|
-
value
|
|
45
|
-
.toLowerCase()
|
|
46
|
-
.replace(/[^a-z0-9-]+/g, '-')
|
|
47
|
-
.replace(/^-+|-+$/g, '')
|
|
48
|
-
.slice(0, 48) || 'skill'
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
type FolderFile = { path: string; text: string }
|
|
53
|
-
|
|
54
|
-
function pickSkillPath(files: FolderFile[]) {
|
|
55
|
-
const candidates = files.filter((file) => /(^|\/)SKILL\.md$/i.test(file.path))
|
|
56
|
-
candidates.sort((a, b) => a.path.split('/').length - b.path.split('/').length || a.path.localeCompare(b.path))
|
|
57
|
-
return candidates[0]?.path ?? ''
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Turn a folder upload into a skill. Files stay in settings so the skill tool can run them. */
|
|
61
|
-
export async function skillFromFolder(files: File[]): Promise<Skill> {
|
|
62
|
-
const queue = files.slice(0, MAX_FILES)
|
|
63
|
-
const read: FolderFile[] = []
|
|
64
|
-
let total = 0
|
|
65
|
-
for (const file of queue) {
|
|
66
|
-
const path = file.webkitRelativePath || file.name
|
|
67
|
-
if (file.size > MAX_FILE || !TEXT_FILE.test(path)) continue
|
|
68
|
-
if (total + file.size > MAX_TOTAL) break
|
|
69
|
-
const text = await file.text()
|
|
70
|
-
total += text.length
|
|
71
|
-
read.push({ path, text })
|
|
72
|
-
}
|
|
73
|
-
if (read.length === 0) throw new Error('That folder has no readable text files.')
|
|
74
|
-
const skillPath = pickSkillPath(read)
|
|
75
|
-
if (!skillPath) throw new Error('That folder has no SKILL.md.')
|
|
76
|
-
// Strip the folder the user picked so paths are relative to the skill root.
|
|
77
|
-
const rootMatch = /^(.*?)\//.exec(skillPath)
|
|
78
|
-
const root = rootMatch?.[1] ?? ''
|
|
79
|
-
const scoped = read
|
|
80
|
-
.filter((file) => !root || file.path === root || file.path.startsWith(`${root}/`))
|
|
81
|
-
.map((file) => ({ path: root ? file.path.slice(root.length + 1) : file.path, text: file.text }))
|
|
82
|
-
const manifest = scoped.find((file) => /^SKILL\.md$/i.test(file.path)) ?? scoped[0]
|
|
83
|
-
const parsed = parseSkillMarkdown(manifest.text, root.split('/').pop() || 'skill')
|
|
84
|
-
return {
|
|
85
|
-
id: `skill_${slug(parsed.name)}`,
|
|
86
|
-
name: parsed.name,
|
|
87
|
-
description: parsed.description,
|
|
88
|
-
instructions: parsed.instructions,
|
|
89
|
-
enabled: true,
|
|
90
|
-
source: 'folder',
|
|
91
|
-
files: Object.fromEntries(scoped.map((file) => [file.path, file.text])),
|
|
92
|
-
}
|
|
93
|
-
}
|