@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
package/src/lib/permissions.ts
CHANGED
|
@@ -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'
|
|
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
|
+
})
|
package/src/lib/retry.ts
ADDED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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('/')
|
package/src/store/chats.ts
CHANGED
|
@@ -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
|
package/src/store/settings.ts
CHANGED
|
@@ -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: '
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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 }
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -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/
|
|
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"}
|