@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/desktop/src-tauri/icons/128x128.png +0 -0
  3. package/desktop/src-tauri/icons/128x128@2x.png +0 -0
  4. package/desktop/src-tauri/icons/32x32.png +0 -0
  5. package/desktop/src-tauri/icons/64x64.png +0 -0
  6. package/desktop/src-tauri/icons/Square107x107Logo.png +0 -0
  7. package/desktop/src-tauri/icons/Square142x142Logo.png +0 -0
  8. package/desktop/src-tauri/icons/Square150x150Logo.png +0 -0
  9. package/desktop/src-tauri/icons/Square284x284Logo.png +0 -0
  10. package/desktop/src-tauri/icons/Square30x30Logo.png +0 -0
  11. package/desktop/src-tauri/icons/Square310x310Logo.png +0 -0
  12. package/desktop/src-tauri/icons/Square44x44Logo.png +0 -0
  13. package/desktop/src-tauri/icons/Square71x71Logo.png +0 -0
  14. package/desktop/src-tauri/icons/Square89x89Logo.png +0 -0
  15. package/desktop/src-tauri/icons/StoreLogo.png +0 -0
  16. package/desktop/src-tauri/icons/icon.icns +0 -0
  17. package/desktop/src-tauri/icons/icon.ico +0 -0
  18. package/desktop/src-tauri/icons/icon.png +0 -0
  19. package/dist/assets/index-C0yvs6Vl.js +281 -0
  20. package/dist/assets/index-CxNPLBg7.css +1 -0
  21. package/dist/favicon.svg +18 -15
  22. package/dist/index.html +2 -2
  23. package/package.json +1 -1
  24. package/public/favicon.svg +18 -15
  25. package/server/daemon.mjs +66 -49
  26. package/server/lib/deploy.mjs +222 -121
  27. package/server/lib/providers.mjs +23 -1
  28. package/server/lib/skills.mjs +385 -100
  29. package/server/lib/tools.mjs +0 -40
  30. package/src/agent/adapters/openai-responses.ts +1 -5
  31. package/src/agent/adapters/openai.ts +4 -11
  32. package/src/agent/prompt.ts +7 -14
  33. package/src/agent/subagent.ts +3 -14
  34. package/src/agent/tools.ts +2 -0
  35. package/src/components/ChatView.tsx +2 -1
  36. package/src/components/Composer.tsx +2 -1
  37. package/src/components/ContextBar.tsx +10 -0
  38. package/src/components/DeployModal.tsx +266 -252
  39. package/src/components/FileIcon.tsx +391 -99
  40. package/src/components/HomeDashboard.tsx +5 -10
  41. package/src/components/Logo.tsx +9 -11
  42. package/src/components/ModelDeck.tsx +8 -11
  43. package/src/components/SettingsModal.tsx +160 -122
  44. package/src/components/Sidebar.tsx +4 -3
  45. package/src/components/SystemPromptButton.tsx +198 -0
  46. package/src/components/ToolCallCard.tsx +2 -0
  47. package/src/components/TopBar.tsx +2 -19
  48. package/src/components/WorkspacePanel.tsx +48 -19
  49. package/src/core/plugins/tools/skill.ts +19 -76
  50. package/src/core/services/agent-loop.ts +16 -21
  51. package/src/core/services/subagents.ts +2 -2
  52. package/src/core/services/system-prompt.ts +16 -23
  53. package/src/core/services/tools.ts +0 -1
  54. package/src/lib/boot.ts +2 -0
  55. package/src/lib/credits.ts +33 -0
  56. package/src/lib/daemon.ts +40 -19
  57. package/src/lib/permissions.ts +1 -1
  58. package/src/lib/retry.test.mjs +45 -0
  59. package/src/lib/retry.ts +46 -0
  60. package/src/lib/system-prompt.ts +23 -0
  61. package/src/lib/usage.ts +8 -13
  62. package/src/store/chats.ts +0 -1
  63. package/src/store/settings.ts +12 -44
  64. package/src/store/skills.ts +79 -0
  65. package/src/store/ui.ts +3 -1
  66. package/src/types.ts +30 -15
  67. package/tsconfig.tsbuildinfo +1 -1
  68. package/dist/assets/index-DaJcH9ts.css +0 -1
  69. package/dist/assets/index-hs0KWMP6.js +0 -288
  70. package/src/components/SystemPromptModal.tsx +0 -168
  71. package/src/lib/skills.test.mjs +0 -22
  72. 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
- }
@@ -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
- }