@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
@@ -21,7 +21,7 @@ export const PERMISSION_MODES: Array<{ value: PermissionMode; label: string; des
21
21
  ]
22
22
 
23
23
  const WRITE_TOOLS: BuiltinToolName[] = ['write_file', 'edit_file', 'rewrite_file']
24
- const GUARDED_TOOLS: BuiltinToolName[] = ['shell', 'git', 'subagent', 'skill']
24
+ const GUARDED_TOOLS: BuiltinToolName[] = ['shell', 'git', 'subagent']
25
25
 
26
26
  export function permissionMode(tools: AppSettings['tools']): PermissionMode {
27
27
  const writesAuto = WRITE_TOOLS.every((tool) => tools[tool]?.approval === 'auto')
@@ -0,0 +1,45 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+ import { backoffMs, classifyProviderError, retryAfterMs } from './retry.ts'
4
+
5
+ test('retries transient errors', () => {
6
+ for (const message of [
7
+ 'OpenAI returned 429: Rate limit reached for requests per minute. Please retry in 6s.',
8
+ 'Anthropic is overloaded (529)',
9
+ '503 Service Unavailable',
10
+ 'The request timed out',
11
+ 'Lost the connection to the local daemon',
12
+ '500 Internal Server Error',
13
+ ]) {
14
+ assert.equal(classifyProviderError(message).retry, true, message)
15
+ }
16
+ })
17
+
18
+ test('never retries limits that were reached or other permanent errors', () => {
19
+ for (const message of [
20
+ 'You exceeded your current quota, please check your plan and billing details. (429)',
21
+ 'Usage limit reached for this month',
22
+ 'insufficient_quota',
23
+ 'Insufficient credits. Add credits to continue (402)',
24
+ '401 Invalid API key',
25
+ '403 Forbidden',
26
+ '400 invalid_request_error: context length exceeded',
27
+ '404 model not found',
28
+ 'Daily limit reached',
29
+ ]) {
30
+ assert.equal(classifyProviderError(message).retry, false, message)
31
+ }
32
+ })
33
+
34
+ test('unknown errors are not retried', () => {
35
+ assert.equal(classifyProviderError('Something odd happened').retry, false)
36
+ })
37
+
38
+ test('reads provider retry hints and backs off with a floor', () => {
39
+ assert.equal(retryAfterMs('Please retry in 6s'), 6000)
40
+ assert.equal(retryAfterMs('retry-after: 2'), 2000)
41
+ assert.equal(retryAfterMs('nothing here'), undefined)
42
+ assert.equal(backoffMs(1, '', () => 0.5), 1500)
43
+ assert.equal(backoffMs(3, '', () => 0.5), 6000)
44
+ assert.equal(backoffMs(1, 'retry in 10s', () => 0.5), 10000)
45
+ })
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Decides whether a provider error is worth retrying.
3
+ *
4
+ * Transient problems (rate-limit bursts, overload, 5xx, dropped connections) are retried.
5
+ * Permanent ones (quota or credits exhausted, bad key, bad request, context too long) never are,
6
+ * because waiting cannot fix them. A 429 that says the quota or limit was *reached* is permanent too.
7
+ */
8
+ export const MAX_RETRIES = 5
9
+
10
+ const PERMANENT =
11
+ /quota|insufficient[_ ]?(funds|credit|balance)|out of credits?|no credits?|billing|payment required|\b402\b|credit balance|limit (has been )?(reached|exceeded)|reached (the |your )?(\w+ )?limit|exceeded (your |the )?(current )?(quota|limit|plan)|usage limit|monthly limit|daily limit|spend(ing)? limit|invalid[_ ]?(api[_ ]?)?key|incorrect api key|unauthori[sz]ed|\b401\b|\b403\b|forbidden|permission|not found|\b404\b|invalid[_ ]request|\b400\b|context (length|window)|too many tokens|maximum context|content[_ ]policy|safety|model .*(not|does not)|credentials|deprecated/i
12
+
13
+ const TRANSIENT =
14
+ /\b(429|500|502|503|504|520|521|522|523|524|529)\b|rate.?limit|too many requests|overloaded|capacity|temporar|try again|unavailable|timed? ?out|timeout|could not be reached|lost the connection|failed to fetch|networkerror|network error|econnreset|etimedout|socket hang up|bad gateway|gateway|stream (ended|closed)|internal server error|server error/i
15
+
16
+ export type RetryDecision = { retry: boolean; reason: 'transient' | 'permanent' | 'unknown' }
17
+
18
+ export function classifyProviderError(message: string): RetryDecision {
19
+ const text = message || ''
20
+ // Short-window throttling ("rate limit … per minute", "retry in 6s") clears by itself, even when it says
21
+ // "reached". Anything about quota, credits, billing or a monthly/daily cap does not.
22
+ const burst = /rate.?limit|too many requests|slow down|per[- ]minute|per[- ]second|requests? per|retry (in|after)|try again (in|later)/i.test(text)
23
+ const hardCap = /quota|credit|billing|balance|usage limit|monthly|daily|spend(ing)? limit|exceeded your (current )?(plan|limit)|payment|\b402\b/i.test(text)
24
+ if (burst && !hardCap && !/invalid|unauthori|forbidden|\b40[0134]\b/i.test(text)) return { retry: true, reason: 'transient' }
25
+ if (PERMANENT.test(text)) return { retry: false, reason: 'permanent' }
26
+ if (TRANSIENT.test(text)) return { retry: true, reason: 'transient' }
27
+ return { retry: false, reason: 'unknown' }
28
+ }
29
+
30
+ /** Seconds the provider asked us to wait ("retry in 12s", "retry-after: 30"), if it said. */
31
+ export function retryAfterMs(message: string): number | undefined {
32
+ const match = /(?:retry[- ]after|retry in|try again in|wait)\D{0,6}(\d+(?:\.\d+)?)\s*(ms|milliseconds?|s|sec|seconds?|m|min)?/i.exec(message || '')
33
+ if (!match) return undefined
34
+ const value = Number(match[1])
35
+ if (!Number.isFinite(value)) return undefined
36
+ const unit = (match[2] ?? 's').toLowerCase()
37
+ const ms = unit.startsWith('ms') || unit.startsWith('milli') ? value : unit.startsWith('m') && unit !== 'ms' && !unit.startsWith('milli') ? value * 60_000 : value * 1000
38
+ return Math.min(Math.max(ms, 500), 60_000)
39
+ }
40
+
41
+ /** Exponential backoff (1.5s, 3s, 6s, 12s, 24s) with jitter, never shorter than the provider's hint. */
42
+ export function backoffMs(attempt: number, message = '', random = Math.random): number {
43
+ const base = Math.min(1500 * 2 ** (attempt - 1), 24_000)
44
+ const jittered = base * (0.8 + random() * 0.4)
45
+ return Math.round(Math.max(jittered, retryAfterMs(message) ?? 0))
46
+ }
@@ -0,0 +1,23 @@
1
+ import type { AppSettings, ChatSession } from '../types'
2
+
3
+ export type PromptSource = 'session' | 'workspace' | 'app'
4
+
5
+ /** The prompt a scope would fall back to when it is set to "shared". */
6
+ export function inheritedPrompt(settings: Pick<AppSettings, 'systemPrompt' | 'workspacePrompts'>, root: string): {
7
+ text: string
8
+ source: Exclude<PromptSource, 'session'>
9
+ } {
10
+ const custom = root ? settings.workspacePrompts?.[root] : undefined
11
+ if (typeof custom === 'string') return { text: custom, source: 'workspace' }
12
+ return { text: settings.systemPrompt, source: 'app' }
13
+ }
14
+
15
+ /** Session custom → workspace custom → app-wide prompt. */
16
+ export function resolveBasePrompt(
17
+ settings: Pick<AppSettings, 'systemPrompt' | 'workspacePrompts'>,
18
+ session: Pick<ChatSession, 'systemPrompt'> | null | undefined,
19
+ root: string,
20
+ ): { text: string; source: PromptSource } {
21
+ if (typeof session?.systemPrompt === 'string') return { text: session.systemPrompt, source: 'session' }
22
+ return inheritedPrompt(settings, root)
23
+ }
package/src/lib/usage.ts CHANGED
@@ -6,6 +6,7 @@ export interface ModelUsage {
6
6
  model: string
7
7
  messages: number
8
8
  tokens: number
9
+ cost: number
9
10
  }
10
11
 
11
12
  export interface HeatmapDay {
@@ -18,7 +19,7 @@ export interface UsageStats {
18
19
  sessions: number
19
20
  messages: number
20
21
  tokens: number
21
- credits: number
22
+ cost: number
22
23
  activeDays: number
23
24
  peakHour: string
24
25
  favoriteModel: string
@@ -82,7 +83,7 @@ export function computeUsage(sessions: ChatSession[], range: UsageRange, now = D
82
83
  const days = new Map<string, HeatmapDay>()
83
84
  const models = new Map<string, ModelUsage>()
84
85
  let tokens = 0
85
- let credits = 0
86
+ let cost = 0
86
87
 
87
88
  for (const { message, session } of messages) {
88
89
  const at = messageTime(message, session.updatedAt)
@@ -95,14 +96,14 @@ export function computeUsage(sessions: ChatSession[], range: UsageRange, now = D
95
96
  if (message.role !== 'assistant') continue
96
97
  const messageTokens = estimateTokens(message)
97
98
  tokens += messageTokens
98
- if (typeof message.usage?.cost === 'number' && Number.isFinite(message.usage.cost)) {
99
- credits += Math.max(0, message.usage.cost)
100
- }
99
+ const messageCredits = typeof message.cost === 'number' ? message.cost : 0
100
+ cost += messageCredits
101
101
  const model = typeof message.model === 'string' ? message.model.trim() : ''
102
102
  if (!model) continue
103
- const entry = models.get(model) ?? { model, messages: 0, tokens: 0 }
103
+ const entry = models.get(model) ?? { model, messages: 0, tokens: 0, cost: 0 }
104
104
  entry.messages += 1
105
105
  entry.tokens += messageTokens
106
+ entry.cost += messageCredits
106
107
  models.set(model, entry)
107
108
  }
108
109
 
@@ -124,7 +125,7 @@ export function computeUsage(sessions: ChatSession[], range: UsageRange, now = D
124
125
  sessions: filteredSessions.length,
125
126
  messages: messages.length,
126
127
  tokens,
127
- credits,
128
+ cost,
128
129
  activeDays: days.size,
129
130
  peakHour: messages.length ? hourLabel(peak) : '—',
130
131
  favoriteModel: favorite?.model ?? '—',
@@ -154,12 +155,6 @@ export function formatTokens(value: number): string {
154
155
  return String(value)
155
156
  }
156
157
 
157
- export function formatCredits(value: number): string {
158
- if (!value) return '—'
159
- if (value < 0.01) return `$${value.toFixed(4)}`
160
- return `$${value.toFixed(2)}`
161
- }
162
-
163
158
  export function shortModelName(model: string): string {
164
159
  if (!model || model === '—') return model
165
160
  const parts = model.split('/')
@@ -48,7 +48,6 @@ function normalizeSession(session: ChatSession): ChatSession {
48
48
  updatedAt: typeof session.updatedAt === 'number' ? session.updatedAt : Date.now(),
49
49
  mode: session.mode === 'plan' ? 'plan' : session.mode === 'build' ? 'build' : undefined,
50
50
  workspace: typeof session.workspace === 'string' ? session.workspace : undefined,
51
- systemPrompt: typeof session.systemPrompt === 'string' ? session.systemPrompt : undefined,
52
51
  titleSource:
53
52
  session.titleSource === 'ai' || session.titleSource === 'user' || session.titleSource === 'auto'
54
53
  ? session.titleSource
@@ -9,7 +9,6 @@ import type {
9
9
  ProviderId,
10
10
  SecretEntry,
11
11
  SecretsSummary,
12
- Skill,
13
12
  BuiltinToolName,
14
13
  ToolSetting,
15
14
  } from '../types'
@@ -58,7 +57,7 @@ const DEFAULT_TOOLS: Record<BuiltinToolName, ToolSetting> = {
58
57
  read_url: { enabled: true, approval: 'auto' },
59
58
  git: { enabled: true, approval: 'ask' },
60
59
  subagent: { enabled: true, approval: 'ask' },
61
- skill: { enabled: true, approval: 'ask' },
60
+ skill: { enabled: true, approval: 'auto' },
62
61
  }
63
62
 
64
63
  const DEFAULTS: AppSettings = {
@@ -73,7 +72,6 @@ const DEFAULTS: AppSettings = {
73
72
  providers: {},
74
73
  searchProvider: 'duckduckgo',
75
74
  tools: DEFAULT_TOOLS,
76
- skills: [],
77
75
  theme: 'system',
78
76
  showTokenEstimate: true,
79
77
  mode: 'build',
@@ -102,9 +100,6 @@ interface SettingsStore extends AppSettings {
102
100
  saveDeployCredentials: (provider: DeployProviderId, fields: Record<string, string>) => Promise<void>
103
101
  refreshProviders: () => Promise<void>
104
102
  updateTool: (tool: BuiltinToolName, patch: Partial<ToolSetting>) => void
105
- addSkill: (skill: Skill) => void
106
- updateSkill: (id: string, patch: Partial<Skill>) => void
107
- removeSkill: (id: string) => void
108
103
  setMode: (mode: AppSettings['mode']) => void
109
104
  flush: () => Promise<void>
110
105
  }
@@ -122,18 +117,6 @@ function mergeTools(saved?: Partial<Record<BuiltinToolName, Partial<ToolSetting>
122
117
  return merged
123
118
  }
124
119
 
125
- function mergeSkills(saved?: Skill[]) {
126
- // Built-in skills were removed; drop any leftovers persisted by older versions.
127
- return (saved ?? [])
128
- .filter((skill): skill is Skill => Boolean(skill) && typeof skill.id === 'string' && typeof skill.name === 'string' && !skill.builtIn)
129
- .map((skill) => ({
130
- ...skill,
131
- description: typeof skill.description === 'string' ? skill.description : '',
132
- instructions: typeof skill.instructions === 'string' ? skill.instructions : '',
133
- enabled: skill.enabled !== false,
134
- }))
135
- }
136
-
137
120
  function upgradePrompt(value: unknown) {
138
121
  if (typeof value !== 'string' || !value.trim()) return DEFAULT_SYSTEM_PROMPT
139
122
  const legacy =
@@ -143,6 +126,15 @@ function upgradePrompt(value: unknown) {
143
126
  return legacy ? DEFAULT_SYSTEM_PROMPT : value
144
127
  }
145
128
 
129
+ function sanitizeWorkspacePrompts(value: unknown): Record<string, string> {
130
+ const out: Record<string, string> = {}
131
+ if (!value || typeof value !== 'object') return out
132
+ for (const [root, prompt] of Object.entries(value as Record<string, unknown>)) {
133
+ if (root && typeof prompt === 'string') out[root] = prompt
134
+ }
135
+ return out
136
+ }
137
+
146
138
  function sanitizeBaseUrls(saved: AppSettings['providers'] | undefined) {
147
139
  const providers: AppSettings['providers'] = {}
148
140
  for (const [id, value] of Object.entries(saved ?? {})) {
@@ -181,21 +173,12 @@ function buildSettings(persisted?: Partial<AppSettings>): AppSettings {
181
173
  ? Math.max(256, Math.min(200_000, Math.round(saved.maxTokens)))
182
174
  : DEFAULTS.maxTokens,
183
175
  systemPrompt: upgradePrompt(saved.systemPrompt),
184
- workspacePrompts:
185
- saved.workspacePrompts && typeof saved.workspacePrompts === 'object'
186
- ? Object.fromEntries(
187
- Object.entries(saved.workspacePrompts).filter(
188
- ([key, value]) => key.startsWith('/') && typeof value === 'string',
189
- ),
190
- )
191
- : {},
192
176
  providers: sanitizeBaseUrls(saved.providers),
193
177
  searchProvider:
194
178
  saved.searchProvider && ['duckduckgo', 'tavily', 'brave', 'serper'].includes(saved.searchProvider)
195
179
  ? saved.searchProvider
196
180
  : DEFAULTS.searchProvider,
197
181
  tools: mergeTools(saved.tools),
198
- skills: mergeSkills(saved.skills),
199
182
  theme: normalizeTheme(saved.theme),
200
183
  chatFontSize:
201
184
  typeof saved.chatFontSize === 'number' && Number.isFinite(saved.chatFontSize)
@@ -204,6 +187,7 @@ function buildSettings(persisted?: Partial<AppSettings>): AppSettings {
204
187
  workDetail: saved.workDetail === 'concise' ? 'concise' : 'detailed',
205
188
  autoCollapseWork: saved.autoCollapseWork !== false,
206
189
  aiSessionTitles: saved.aiSessionTitles !== false,
190
+ workspacePrompts: sanitizeWorkspacePrompts(saved.workspacePrompts),
207
191
  }
208
192
  }
209
193
 
@@ -222,11 +206,9 @@ function persistedPatch(patch: Partial<AppSettings>): Partial<AppSettings> {
222
206
  'temperature',
223
207
  'maxTokens',
224
208
  'systemPrompt',
225
- 'workspacePrompts',
226
209
  'providers',
227
210
  'searchProvider',
228
211
  'tools',
229
- 'skills',
230
212
  'theme',
231
213
  'showTokenEstimate',
232
214
  'mode',
@@ -234,6 +216,7 @@ function persistedPatch(patch: Partial<AppSettings>): Partial<AppSettings> {
234
216
  'workDetail',
235
217
  'autoCollapseWork',
236
218
  'aiSessionTitles',
219
+ 'workspacePrompts',
237
220
  ]
238
221
  for (const key of keys) {
239
222
  if (key in patch) out[key] = patch[key]
@@ -382,21 +365,6 @@ export const useSettings = create<SettingsStore>()((set, get) => ({
382
365
  set(next)
383
366
  queueSave(next)
384
367
  },
385
- addSkill: (skill) => {
386
- const next = { skills: [...get().skills, skill] }
387
- set(next)
388
- queueSave(next)
389
- },
390
- updateSkill: (id, patch) => {
391
- const next = { skills: get().skills.map((skill) => (skill.id === id ? { ...skill, ...patch } : skill)) }
392
- set(next)
393
- queueSave(next)
394
- },
395
- removeSkill: (id) => {
396
- const next = { skills: get().skills.filter((skill) => skill.id !== id) }
397
- set(next)
398
- queueSave(next)
399
- },
400
368
  setMode: (mode) => {
401
369
  const patch = { mode }
402
370
  set(patch)
@@ -0,0 +1,79 @@
1
+ import { create } from 'zustand'
2
+ import type { InstalledSkill } from '../types'
3
+ import {
4
+ installSkillNpm,
5
+ listSkillsApi,
6
+ removeSkillApi,
7
+ setSkillEnabledApi,
8
+ uploadSkillFiles,
9
+ type SkillUploadFile,
10
+ } from '../lib/daemon'
11
+
12
+ interface SkillsState {
13
+ skills: InstalledSkill[]
14
+ loaded: boolean
15
+ loading: boolean
16
+ error: string
17
+ load: () => Promise<void>
18
+ upload: (files: SkillUploadFile[]) => Promise<string[]>
19
+ installNpm: (spec: string) => Promise<string[]>
20
+ setEnabled: (name: string, enabled: boolean) => Promise<void>
21
+ remove: (name: string) => Promise<void>
22
+ }
23
+
24
+ export const useSkills = create<SkillsState>()((set) => ({
25
+ skills: [],
26
+ loaded: false,
27
+ loading: false,
28
+ error: '',
29
+ load: async () => {
30
+ set({ loading: true })
31
+ try {
32
+ const { skills } = await listSkillsApi()
33
+ set({ skills, loaded: true, loading: false, error: '' })
34
+ } catch (error) {
35
+ set({ loading: false, error: error instanceof Error ? error.message : 'Could not load skills.' })
36
+ }
37
+ },
38
+ upload: async (files) => {
39
+ const result = await uploadSkillFiles(files)
40
+ set({ skills: result.skills, loaded: true })
41
+ return result.installed
42
+ },
43
+ installNpm: async (spec) => {
44
+ const result = await installSkillNpm(spec)
45
+ set({ skills: result.skills, loaded: true })
46
+ return result.installed
47
+ },
48
+ setEnabled: async (name, enabled) => {
49
+ set((state) => ({ skills: state.skills.map((skill) => (skill.name === name ? { ...skill, enabled } : skill)) }))
50
+ try {
51
+ const result = await setSkillEnabledApi(name, enabled)
52
+ set({ skills: result.skills })
53
+ } catch (error) {
54
+ set((state) => ({
55
+ skills: state.skills.map((skill) => (skill.name === name ? { ...skill, enabled: !enabled } : skill)),
56
+ error: error instanceof Error ? error.message : 'Could not update the skill.',
57
+ }))
58
+ }
59
+ },
60
+ remove: async (name) => {
61
+ const result = await removeSkillApi(name)
62
+ set({ skills: result.skills })
63
+ },
64
+ }))
65
+
66
+ export function enabledSkills(): InstalledSkill[] {
67
+ return useSkills.getState().skills.filter((skill) => skill.enabled)
68
+ }
69
+
70
+ /** Catalog section for the system prompt: names and descriptions only; bodies load through the skill tool. */
71
+ export function skillCatalogPrompt(): string {
72
+ const skills = enabledSkills()
73
+ if (skills.length === 0) return ''
74
+ return [
75
+ '## Skills',
76
+ 'Skills are folders of expert instructions. When a task matches a skill below, call the `skill` tool with its name BEFORE starting, then follow what it returns. Load bundled files (scripts, references) with the same tool via `file`. Do not guess a skill\'s contents.',
77
+ ...skills.map((skill) => `- ${skill.name}: ${skill.description || 'No description.'}`),
78
+ ].join('\n')
79
+ }
package/src/store/ui.ts CHANGED
@@ -117,7 +117,9 @@ export const useUi = create<UiState>()((set, get) => ({
117
117
  setMcpOpen: (open) => set({ mcpOpen: open, ...(open ? { mobileSidebarOpen: false, settingsOpen: false } : {}) }),
118
118
  settingsOpen: false,
119
119
  settingsTab: 'general',
120
- openSettings: (tab = 'general') => set({ settingsOpen: true, settingsTab: tab, mobileSidebarOpen: false }),
120
+ // Plugins are paused for maintenance: any request for that tab lands on General instead.
121
+ openSettings: (tab = 'general') =>
122
+ set({ settingsOpen: true, settingsTab: tab === 'plugins' ? 'general' : tab, mobileSidebarOpen: false }),
121
123
  closeSettings: () => set({ settingsOpen: false }),
122
124
  deployOpen: false,
123
125
  setDeployOpen: (open) => set({ deployOpen: open }),
package/src/types.ts CHANGED
@@ -38,6 +38,8 @@ export interface ModelInfo {
38
38
  apiKind: ApiKind
39
39
  free?: boolean
40
40
  canonicalId?: string
41
+ /** USD per 1M tokens, when the provider or catalog publishes it. */
42
+ pricing?: { input: number; output: number }
41
43
  note?: string
42
44
  }
43
45
 
@@ -113,19 +115,18 @@ export interface ToolSetting {
113
115
 
114
116
  export type SearchProvider = 'duckduckgo' | 'tavily' | 'brave' | 'serper'
115
117
 
116
- export interface Skill {
117
- id: string
118
+ /** A skill folder installed on the daemon (SKILL.md + bundled files). */
119
+ export interface InstalledSkill {
118
120
  name: string
121
+ title: string
119
122
  description: string
120
- instructions: string
121
123
  enabled: boolean
122
- builtIn?: boolean
123
- /** Where the skill came from. Folder skills keep their uploaded files. */
124
- source?: 'folder' | 'npm'
125
- package?: string
126
- version?: string
127
- /** Text files from the folder, keyed by relative path (SKILL.md included). */
128
- files?: Record<string, string>
124
+ /** 'upload' | 'folder' | 'legacy' | `npm:<spec>` */
125
+ source: string
126
+ installedAt: string
127
+ fileCount: number
128
+ size: number
129
+ path: string
129
130
  }
130
131
 
131
132
  export interface AppSettings {
@@ -136,12 +137,11 @@ export interface AppSettings {
136
137
  temperature: number
137
138
  maxTokens: number
138
139
  systemPrompt: string
139
- /** Per-workspace system prompt overrides, keyed by workspace root. */
140
+ /** Custom base prompts per workspace folder (absolute root path). Missing = use the shared prompt. */
140
141
  workspacePrompts: Record<string, string>
141
142
  providers: Record<string, ProviderCredentials>
142
143
  searchProvider: SearchProvider
143
144
  tools: Record<BuiltinToolName, ToolSetting>
144
- skills: Skill[]
145
145
  theme: ThemeId
146
146
  showTokenEstimate: boolean
147
147
  mode: AgentMode
@@ -195,6 +195,8 @@ export interface AssistantMessage {
195
195
  model?: string
196
196
  provider?: ProviderId
197
197
  usage?: { input?: number; output?: number; cost?: number }
198
+ /** Credits spent on this reply (provider-reported, else tokens × model price). */
199
+ cost?: number
198
200
  error?: string
199
201
  stopped?: boolean
200
202
  durationMs?: number
@@ -236,10 +238,10 @@ export interface ChatSession {
236
238
  mode?: AgentMode
237
239
  /** Workspace folder the session was started in. */
238
240
  workspace?: string
239
- /** Per-session system prompt. Empty means the workspace or shared prompt is used. */
240
- systemPrompt?: string
241
241
  /** Who last set the title; only 'auto' titles may be replaced by the AI. */
242
242
  titleSource?: 'auto' | 'ai' | 'user'
243
+ /** Custom base prompt for this session only. Undefined = inherit (workspace prompt, else the shared one). */
244
+ systemPrompt?: string
243
245
  }
244
246
 
245
247
  export type McpStatus = 'disabled' | 'disconnected' | 'connecting' | 'connected' | 'needs_auth' | 'error'
@@ -286,7 +288,7 @@ export interface McpServer {
286
288
  export interface Usage {
287
289
  input?: number
288
290
  output?: number
289
- /** Cost/credits reported by the provider, when it reports one. */
291
+ /** Credits/USD the provider itself reported for this request. */
290
292
  cost?: number
291
293
  }
292
294
 
@@ -379,6 +381,18 @@ export interface DeployRequest {
379
381
  mode?: AgentMode
380
382
  }
381
383
 
384
+ export interface DeployDetection {
385
+ path: string
386
+ name: string
387
+ framework: string
388
+ packageManager: string
389
+ hasNodeModules: boolean
390
+ buildCommand: string
391
+ outputDir: string
392
+ expectedOutputDir: string
393
+ built: boolean
394
+ }
395
+
382
396
  export interface DeployResult {
383
397
  provider: DeployProviderId
384
398
  url: string
@@ -388,6 +402,7 @@ export interface DeployResult {
388
402
 
389
403
  export type DeployEvent =
390
404
  | { type: 'start'; provider: DeployProviderId; path?: string }
405
+ | { type: 'step'; id: string; label: string }
391
406
  | { type: 'log'; stream: 'stdout' | 'stderr' | 'meta'; text: string }
392
407
  | { type: 'done'; result: DeployResult }
393
408
  | { type: 'error'; message: string; code?: string }
@@ -1 +1 @@
1
- {"root":["./src/App.tsx","./src/main.tsx","./src/types.ts","./src/vite-env.d.ts","./src/agent/approval.ts","./src/agent/compact.ts","./src/agent/index.ts","./src/agent/model.ts","./src/agent/prompt.ts","./src/agent/queue.ts","./src/agent/subagent.ts","./src/agent/tools.ts","./src/agent/adapters/anthropic.ts","./src/agent/adapters/gemini.ts","./src/agent/adapters/openai-responses.ts","./src/agent/adapters/openai.ts","./src/agent/adapters/shared.ts","./src/components/ChatView.tsx","./src/components/CommandPalette.tsx","./src/components/Composer.tsx","./src/components/ContextBar.tsx","./src/components/DeployModal.tsx","./src/components/DiffView.tsx","./src/components/ErrorBoundary.tsx","./src/components/FileIcon.tsx","./src/components/HomeDashboard.tsx","./src/components/Logo.tsx","./src/components/Markdown.tsx","./src/components/McpPanel.tsx","./src/components/MessageItem.tsx","./src/components/ModelDeck.tsx","./src/components/PixleSol.tsx","./src/components/PluginBuilder.tsx","./src/components/PluginManager.tsx","./src/components/PreviewPane.tsx","./src/components/SessionsTab.tsx","./src/components/SettingsModal.tsx","./src/components/Sidebar.tsx","./src/components/Splash.tsx","./src/components/SystemPromptModal.tsx","./src/components/ToolCallCard.tsx","./src/components/TopBar.tsx","./src/components/TrajectoryView.tsx","./src/components/WorkspacePanel.tsx","./src/components/ui.tsx","./src/core/kernel.ts","./src/core/mcp-tools.ts","./src/core/types.ts","./src/core/plugins/tools/fs.ts","./src/core/plugins/tools/git.ts","./src/core/plugins/tools/grep.ts","./src/core/plugins/tools/shell.ts","./src/core/plugins/tools/skill.ts","./src/core/plugins/tools/subagent.ts","./src/core/plugins/tools/web.ts","./src/core/services/agent-loop.ts","./src/core/services/approval.ts","./src/core/services/compaction.ts","./src/core/services/llm.ts","./src/core/services/sessions.ts","./src/core/services/subagents.ts","./src/core/services/system-prompt.ts","./src/core/services/titles.ts","./src/core/services/tools.ts","./src/core/services/ui.ts","./src/data/providers.ts","./src/lib/boot.ts","./src/lib/context.ts","./src/lib/conversation.ts","./src/lib/daemon.ts","./src/lib/diff.ts","./src/lib/hooks.ts","./src/lib/mcp-config.ts","./src/lib/permissions.ts","./src/lib/pixlesol.ts","./src/lib/plugin-folder.ts","./src/lib/reasoning.ts","./src/lib/skills.ts","./src/lib/sse.ts","./src/lib/themes.ts","./src/lib/usage.ts","./src/lib/utils.ts","./src/plugins/api.ts","./src/plugins/bridge.d.mts","./src/plugins/index.ts","./src/plugins/preview.ts","./src/plugins/runtime.ts","./src/plugins/types.ts","./src/store/chats.ts","./src/store/mcp.ts","./src/store/models.ts","./src/store/settings.ts","./src/store/ui.ts","./src/store/workspace.ts"],"version":"5.9.3"}
1
+ {"root":["./src/App.tsx","./src/main.tsx","./src/types.ts","./src/vite-env.d.ts","./src/agent/approval.ts","./src/agent/compact.ts","./src/agent/index.ts","./src/agent/model.ts","./src/agent/prompt.ts","./src/agent/queue.ts","./src/agent/subagent.ts","./src/agent/tools.ts","./src/agent/adapters/anthropic.ts","./src/agent/adapters/gemini.ts","./src/agent/adapters/openai-responses.ts","./src/agent/adapters/openai.ts","./src/agent/adapters/shared.ts","./src/components/ChatView.tsx","./src/components/CommandPalette.tsx","./src/components/Composer.tsx","./src/components/ContextBar.tsx","./src/components/DeployModal.tsx","./src/components/DiffView.tsx","./src/components/ErrorBoundary.tsx","./src/components/FileIcon.tsx","./src/components/HomeDashboard.tsx","./src/components/Logo.tsx","./src/components/Markdown.tsx","./src/components/McpPanel.tsx","./src/components/MessageItem.tsx","./src/components/ModelDeck.tsx","./src/components/PixleSol.tsx","./src/components/PluginBuilder.tsx","./src/components/PluginManager.tsx","./src/components/PreviewPane.tsx","./src/components/SessionsTab.tsx","./src/components/SettingsModal.tsx","./src/components/Sidebar.tsx","./src/components/Splash.tsx","./src/components/SystemPromptButton.tsx","./src/components/ToolCallCard.tsx","./src/components/TopBar.tsx","./src/components/TrajectoryView.tsx","./src/components/WorkspacePanel.tsx","./src/components/ui.tsx","./src/core/kernel.ts","./src/core/mcp-tools.ts","./src/core/types.ts","./src/core/plugins/tools/fs.ts","./src/core/plugins/tools/git.ts","./src/core/plugins/tools/grep.ts","./src/core/plugins/tools/shell.ts","./src/core/plugins/tools/skill.ts","./src/core/plugins/tools/subagent.ts","./src/core/plugins/tools/web.ts","./src/core/services/agent-loop.ts","./src/core/services/approval.ts","./src/core/services/compaction.ts","./src/core/services/llm.ts","./src/core/services/sessions.ts","./src/core/services/subagents.ts","./src/core/services/system-prompt.ts","./src/core/services/titles.ts","./src/core/services/tools.ts","./src/core/services/ui.ts","./src/data/providers.ts","./src/lib/boot.ts","./src/lib/context.ts","./src/lib/conversation.ts","./src/lib/credits.ts","./src/lib/daemon.ts","./src/lib/diff.ts","./src/lib/hooks.ts","./src/lib/mcp-config.ts","./src/lib/permissions.ts","./src/lib/pixlesol.ts","./src/lib/plugin-folder.ts","./src/lib/reasoning.ts","./src/lib/retry.ts","./src/lib/sse.ts","./src/lib/system-prompt.ts","./src/lib/themes.ts","./src/lib/usage.ts","./src/lib/utils.ts","./src/plugins/api.ts","./src/plugins/bridge.d.mts","./src/plugins/index.ts","./src/plugins/preview.ts","./src/plugins/runtime.ts","./src/plugins/types.ts","./src/store/chats.ts","./src/store/mcp.ts","./src/store/models.ts","./src/store/settings.ts","./src/store/skills.ts","./src/store/ui.ts","./src/store/workspace.ts"],"version":"5.9.3"}