@bibobonking/opensol 3.1.1 → 3.1.3
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 +1 -1
- package/dist/assets/index-DaJcH9ts.css +1 -0
- package/dist/assets/index-hs0KWMP6.js +288 -0
- package/dist/favicon.svg +11 -5
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/public/favicon.svg +11 -5
- package/server/daemon.mjs +44 -5
- package/server/lib/deploy.mjs +78 -7
- package/server/lib/skills.mjs +128 -0
- package/server/lib/tools.mjs +40 -0
- package/src/agent/adapters/openai-responses.ts +5 -1
- package/src/agent/adapters/openai.ts +15 -2
- package/src/agent/prompt.ts +4 -7
- package/src/agent/tools.ts +1 -0
- package/src/components/DeployModal.tsx +77 -15
- package/src/components/FileIcon.tsx +108 -0
- package/src/components/HomeDashboard.tsx +9 -2
- package/src/components/Logo.tsx +11 -5
- package/src/components/ModelDeck.tsx +12 -2
- package/src/components/SettingsModal.tsx +144 -199
- package/src/components/Sidebar.tsx +4 -5
- package/src/components/SystemPromptModal.tsx +168 -0
- package/src/components/TopBar.tsx +19 -0
- package/src/components/WorkspacePanel.tsx +10 -8
- package/src/core/kernel.ts +2 -0
- package/src/core/plugins/tools/skill.ts +101 -0
- package/src/core/services/agent-loop.ts +10 -22
- package/src/core/services/system-prompt.ts +15 -13
- package/src/core/services/tools.ts +1 -0
- package/src/lib/daemon.ts +19 -0
- package/src/lib/permissions.ts +1 -1
- package/src/lib/skills.test.mjs +22 -0
- package/src/lib/skills.ts +93 -0
- package/src/lib/usage.ts +12 -0
- package/src/store/chats.ts +1 -0
- package/src/store/settings.ts +21 -9
- package/src/types.ts +14 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/assets/index-CeN54A2N.css +0 -1
- package/dist/assets/index-fhIO9a7_.js +0 -318
- package/src/data/skills.ts +0 -68
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
Moon,
|
|
24
24
|
Package,
|
|
25
25
|
Palette,
|
|
26
|
-
Plus,
|
|
27
26
|
RefreshCw,
|
|
28
27
|
Rocket,
|
|
29
28
|
RotateCcw,
|
|
@@ -42,7 +41,6 @@ import {
|
|
|
42
41
|
import type { DeployProviderId, Effort, ModelInfo, ProviderId, SearchProvider, Skill, BuiltinToolName } from '../types'
|
|
43
42
|
import type { LucideIcon } from 'lucide-react'
|
|
44
43
|
import { EFFORT_LABELS, PROVIDERS, PROVIDER_MAP } from '../data/providers'
|
|
45
|
-
import { STARTER_SKILL } from '../data/skills'
|
|
46
44
|
import {
|
|
47
45
|
DEFAULT_SYSTEM_PROMPT,
|
|
48
46
|
activeModelId,
|
|
@@ -55,10 +53,11 @@ import { useChats, exportChats, importChats } from '../store/chats'
|
|
|
55
53
|
import { useWorkspace } from '../store/workspace'
|
|
56
54
|
import { stopAllRuns } from '../agent'
|
|
57
55
|
import { applySnapshot } from '../lib/boot'
|
|
58
|
-
import { executeDaemonTool, resetScope } from '../lib/daemon'
|
|
56
|
+
import { executeDaemonTool, installSkillFromNpmRequest, resetScope } from '../lib/daemon'
|
|
57
|
+
import { skillFromFolder } from '../lib/skills'
|
|
59
58
|
import { effortOptions, reasoningSummary } from '../lib/reasoning'
|
|
60
59
|
import { PERMISSION_MODES, permissionMode, permissionTools } from '../lib/permissions'
|
|
61
|
-
import { cn, formatBytes
|
|
60
|
+
import { cn, formatBytes } from '../lib/utils'
|
|
62
61
|
import { LogoMark } from './Logo'
|
|
63
62
|
import { PluginManager } from './PluginManager'
|
|
64
63
|
import { useMcp } from '../store/mcp'
|
|
@@ -70,7 +69,7 @@ const TABS = [
|
|
|
70
69
|
{ id: 'models', label: 'Models', icon: Cpu, description: 'Providers, encrypted keys, live models and reasoning defaults.' },
|
|
71
70
|
{ id: 'tools', label: 'Tools', icon: Wrench, description: 'What OpenSol can do on this machine, and when it must ask.' },
|
|
72
71
|
{ id: 'search', label: 'Search', icon: Globe2, description: 'Give the agent current web results with your own key.' },
|
|
73
|
-
{ id: 'skills', label: 'Skills', icon: Sparkles, description: '
|
|
72
|
+
{ id: 'skills', label: 'Skills', icon: Sparkles, description: 'Install skill folders or npm packages. The skill tool loads SKILL.md and runs the scripts a skill ships.' },
|
|
74
73
|
{ id: 'plugins', label: 'Plugins', icon: Package, description: 'Install local plugin packages, review what they ask for, and run them once you trust the code.' },
|
|
75
74
|
{ id: 'mcp', label: 'MCP', icon: Link2, description: 'Model Context Protocol servers now have their own panel, with OAuth sign-in and local, HTTP and SSE connections.' },
|
|
76
75
|
{ id: 'deploy', label: 'Deploy', icon: Rocket, description: 'Hosting credentials and command-line tools.' },
|
|
@@ -631,7 +630,7 @@ function ModelsTab() {
|
|
|
631
630
|
className="thin-scroll w-full resize-y rounded-lg border border-line bg-panel px-2.5 py-2 font-mono text-[10.5px] leading-relaxed text-ink outline-none focus:border-accent/50"
|
|
632
631
|
/>
|
|
633
632
|
<p className="mt-1 text-[9px] leading-relaxed text-faint">
|
|
634
|
-
|
|
633
|
+
This is the shared prompt. Each session and workspace can override it from the prompt button in the chat header.
|
|
635
634
|
</p>
|
|
636
635
|
</div>
|
|
637
636
|
</div>
|
|
@@ -647,7 +646,7 @@ const TOOL_ORDER: Array<{ title: string; tools: BuiltinToolName[] }> = [
|
|
|
647
646
|
tools: ['read_file', 'list_files', 'grep', 'write_file', 'edit_file', 'rewrite_file', 'shell', 'git'],
|
|
648
647
|
},
|
|
649
648
|
{ title: 'Web', tools: ['web_search', 'read_url'] },
|
|
650
|
-
{ title: 'Delegation', tools: ['subagent'] },
|
|
649
|
+
{ title: 'Delegation', tools: ['subagent', 'skill'] },
|
|
651
650
|
]
|
|
652
651
|
|
|
653
652
|
const TOOL_ICONS: Record<BuiltinToolName, LucideIcon> = {
|
|
@@ -662,6 +661,7 @@ const TOOL_ICONS: Record<BuiltinToolName, LucideIcon> = {
|
|
|
662
661
|
read_url: Link2,
|
|
663
662
|
git: GitBranch,
|
|
664
663
|
subagent: Bot,
|
|
664
|
+
skill: Sparkles,
|
|
665
665
|
}
|
|
666
666
|
|
|
667
667
|
const TOOL_COPY: Record<BuiltinToolName, { label: string; description: string }> = {
|
|
@@ -676,6 +676,7 @@ const TOOL_COPY: Record<BuiltinToolName, { label: string; description: string }>
|
|
|
676
676
|
read_url: { label: 'Read URL', description: 'Open a public http(s) page as plain text.' },
|
|
677
677
|
git: { label: 'Git', description: 'Inspect or change repository state. Plan mode is read-only.' },
|
|
678
678
|
subagent: { label: 'Subagent', description: 'Delegate a read-only investigation and return a concise report.' },
|
|
679
|
+
skill: { label: 'Skill', description: 'List installed skills, load SKILL.md, and run commands inside a skill folder.' },
|
|
679
680
|
}
|
|
680
681
|
|
|
681
682
|
function ToolsTab() {
|
|
@@ -971,17 +972,6 @@ function SearchTab() {
|
|
|
971
972
|
)
|
|
972
973
|
}
|
|
973
974
|
|
|
974
|
-
interface SkillDraft {
|
|
975
|
-
id?: string
|
|
976
|
-
name: string
|
|
977
|
-
description: string
|
|
978
|
-
instructions: string
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
function newSkillId() {
|
|
982
|
-
return typeof crypto !== 'undefined' && 'randomUUID' in crypto ? `skill_${crypto.randomUUID()}` : `skill_${Date.now()}`
|
|
983
|
-
}
|
|
984
|
-
|
|
985
975
|
function SkillsTab() {
|
|
986
976
|
const settings = useSettings()
|
|
987
977
|
const addSkill = useSettings((state) => state.addSkill)
|
|
@@ -989,63 +979,58 @@ function SkillsTab() {
|
|
|
989
979
|
const removeSkill = useSettings((state) => state.removeSkill)
|
|
990
980
|
const askConfirm = useUi((state) => state.askConfirm)
|
|
991
981
|
const notify = useUi((state) => state.notify)
|
|
992
|
-
const [
|
|
993
|
-
const
|
|
982
|
+
const [installing, setInstalling] = useState('')
|
|
983
|
+
const [npmName, setNpmName] = useState('')
|
|
984
|
+
const folderRef = useRef<HTMLInputElement>(null)
|
|
994
985
|
const active = settings.skills.filter((skill) => skill.enabled).length
|
|
995
986
|
|
|
996
|
-
|
|
987
|
+
useEffect(() => {
|
|
988
|
+
const input = folderRef.current
|
|
989
|
+
if (!input) return
|
|
990
|
+
input.setAttribute('webkitdirectory', '')
|
|
991
|
+
input.setAttribute('directory', '')
|
|
992
|
+
input.setAttribute('multiple', '')
|
|
993
|
+
}, [])
|
|
994
|
+
|
|
995
|
+
const store = (skill: Skill, verb: string) => {
|
|
996
|
+
const exists = useSettings.getState().skills.some((entry) => entry.id === skill.id)
|
|
997
|
+
if (exists) updateSkill(skill.id, skill)
|
|
998
|
+
else addSkill(skill)
|
|
999
|
+
notify(`${skill.name} ${verb}.`, 'success')
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const installFolder = async (list: FileList | null) => {
|
|
1003
|
+
if (!list || list.length === 0) return
|
|
1004
|
+
setInstalling('folder')
|
|
997
1005
|
try {
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
const name = heading?.[1]?.trim() || file.name.replace(/\.[^.]+$/, '')
|
|
1001
|
-
const body = heading ? text.replace(heading[0], '').trim() : text.trim()
|
|
1002
|
-
const paragraph = body.split(/\n\s*\n/).find((part) => part.trim() && !part.trim().startsWith('#'))
|
|
1003
|
-
setDraft({
|
|
1004
|
-
name,
|
|
1005
|
-
description: (paragraph ?? '').replace(/[#>*`]/g, '').replace(/\s+/g, ' ').trim().slice(0, 180),
|
|
1006
|
-
instructions: text.trim(),
|
|
1007
|
-
})
|
|
1008
|
-
notify(`Loaded ${file.name}. Review it, then save the skill.`, 'info')
|
|
1006
|
+
const skill = await skillFromFolder(Array.from(list))
|
|
1007
|
+
store(skill, 'installed')
|
|
1009
1008
|
} catch (cause) {
|
|
1010
|
-
notify(cause instanceof Error ? cause.message : 'Could not read that
|
|
1009
|
+
notify(cause instanceof Error ? cause.message : 'Could not read that folder.', 'error')
|
|
1010
|
+
} finally {
|
|
1011
|
+
setInstalling('')
|
|
1011
1012
|
}
|
|
1012
1013
|
}
|
|
1013
1014
|
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
updateSkill(draft.id, {
|
|
1027
|
-
name,
|
|
1028
|
-
description: draft.description.trim(),
|
|
1029
|
-
instructions: draft.instructions.trim(),
|
|
1030
|
-
})
|
|
1031
|
-
notify('Skill updated.', 'success')
|
|
1032
|
-
} else {
|
|
1033
|
-
addSkill({
|
|
1034
|
-
id: newSkillId(),
|
|
1035
|
-
name,
|
|
1036
|
-
description: draft.description.trim() || 'Custom skill',
|
|
1037
|
-
instructions: draft.instructions.trim(),
|
|
1038
|
-
enabled: true,
|
|
1039
|
-
})
|
|
1040
|
-
notify('Skill saved and enabled.', 'success')
|
|
1015
|
+
const installNpm = async () => {
|
|
1016
|
+
const name = npmName.trim()
|
|
1017
|
+
if (!name) return
|
|
1018
|
+
setInstalling('npm')
|
|
1019
|
+
try {
|
|
1020
|
+
const { skill } = await installSkillFromNpmRequest(name)
|
|
1021
|
+
store(skill, 'installed from npm')
|
|
1022
|
+
setNpmName('')
|
|
1023
|
+
} catch (cause) {
|
|
1024
|
+
notify(cause instanceof Error ? cause.message : 'Could not install that npm package.', 'error')
|
|
1025
|
+
} finally {
|
|
1026
|
+
setInstalling('')
|
|
1041
1027
|
}
|
|
1042
|
-
setDraft(null)
|
|
1043
1028
|
}
|
|
1044
1029
|
|
|
1045
1030
|
const remove = async (skill: Skill) => {
|
|
1046
1031
|
const confirmed = await askConfirm({
|
|
1047
1032
|
title: 'Delete this skill?',
|
|
1048
|
-
body: `“${skill.name}” will be removed from this computer.`,
|
|
1033
|
+
body: `“${skill.name}” and its files will be removed from this computer.`,
|
|
1049
1034
|
confirmLabel: 'Delete skill',
|
|
1050
1035
|
tone: 'danger',
|
|
1051
1036
|
})
|
|
@@ -1054,159 +1039,115 @@ function SkillsTab() {
|
|
|
1054
1039
|
|
|
1055
1040
|
return (
|
|
1056
1041
|
<SettingsSection
|
|
1057
|
-
title="
|
|
1058
|
-
description={`${active} of ${settings.skills.length} enabled.
|
|
1042
|
+
title="Installed skills"
|
|
1043
|
+
description={`${active} of ${settings.skills.length} enabled. The model sees each skill’s name and description, loads SKILL.md through the skill tool, and can run the scripts inside the skill folder.`}
|
|
1059
1044
|
>
|
|
1060
|
-
<div className="mb-3
|
|
1061
|
-
<button
|
|
1062
|
-
type="button"
|
|
1063
|
-
onClick={() => setDraft({ name: '', description: '', instructions: '' })}
|
|
1064
|
-
className="inline-flex h-8 items-center gap-1.5 rounded-lg bg-ink px-2.5 text-[10.5px] font-medium text-panel"
|
|
1065
|
-
>
|
|
1066
|
-
<Plus size={12} /> New skill
|
|
1067
|
-
</button>
|
|
1045
|
+
<div className="mb-3 grid gap-2 sm:grid-cols-[auto_minmax(0,1fr)]">
|
|
1068
1046
|
<button
|
|
1069
1047
|
type="button"
|
|
1070
|
-
onClick={() =>
|
|
1071
|
-
|
|
1048
|
+
onClick={() => folderRef.current?.click()}
|
|
1049
|
+
disabled={Boolean(installing)}
|
|
1050
|
+
className="inline-flex h-9 items-center justify-center gap-1.5 rounded-lg bg-ink px-3 text-[10.5px] font-medium text-panel disabled:opacity-50"
|
|
1072
1051
|
>
|
|
1073
|
-
<
|
|
1052
|
+
{installing === 'folder' ? <Spinner size={11} /> : <Upload size={12} />}
|
|
1053
|
+
Install folder
|
|
1074
1054
|
</button>
|
|
1075
1055
|
<input
|
|
1076
|
-
ref={
|
|
1056
|
+
ref={folderRef}
|
|
1077
1057
|
type="file"
|
|
1078
|
-
accept=".md,.markdown,.txt"
|
|
1079
1058
|
className="hidden"
|
|
1080
1059
|
onChange={(event) => {
|
|
1081
|
-
|
|
1082
|
-
if (file) void loadFile(file)
|
|
1060
|
+
void installFolder(event.target.files)
|
|
1083
1061
|
event.target.value = ''
|
|
1084
1062
|
}}
|
|
1085
1063
|
/>
|
|
1064
|
+
<div className="flex gap-1.5">
|
|
1065
|
+
<input
|
|
1066
|
+
value={npmName}
|
|
1067
|
+
onChange={(event) => setNpmName(event.target.value)}
|
|
1068
|
+
onKeyDown={(event) => {
|
|
1069
|
+
if (event.key === 'Enter') void installNpm()
|
|
1070
|
+
}}
|
|
1071
|
+
placeholder="npm package, e.g. @scope/my-skill"
|
|
1072
|
+
spellCheck={false}
|
|
1073
|
+
className={cn(fieldClass, 'font-mono text-[10.5px]')}
|
|
1074
|
+
/>
|
|
1075
|
+
<button
|
|
1076
|
+
type="button"
|
|
1077
|
+
onClick={() => void installNpm()}
|
|
1078
|
+
disabled={!npmName.trim() || Boolean(installing)}
|
|
1079
|
+
className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-line bg-panel px-2.5 text-[10.5px] font-medium text-ink hover:bg-raised disabled:opacity-40"
|
|
1080
|
+
>
|
|
1081
|
+
{installing === 'npm' ? <Spinner size={11} /> : <ArrowDownToLine size={12} />}
|
|
1082
|
+
Install
|
|
1083
|
+
</button>
|
|
1084
|
+
</div>
|
|
1086
1085
|
</div>
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
</
|
|
1096
|
-
<
|
|
1097
|
-
<div>
|
|
1098
|
-
<Label>Name</Label>
|
|
1099
|
-
<input
|
|
1100
|
-
value={draft.name}
|
|
1101
|
-
onChange={(event) => setDraft({ ...draft, name: event.target.value })}
|
|
1102
|
-
placeholder="API contract reviewer"
|
|
1103
|
-
className={fieldClass}
|
|
1104
|
-
autoFocus
|
|
1105
|
-
/>
|
|
1106
|
-
</div>
|
|
1107
|
-
<div>
|
|
1108
|
-
<Label hint="Shown in the skills list">Description</Label>
|
|
1109
|
-
<input
|
|
1110
|
-
value={draft.description}
|
|
1111
|
-
onChange={(event) => setDraft({ ...draft, description: event.target.value })}
|
|
1112
|
-
placeholder="Checks a diff against an API contract"
|
|
1113
|
-
className={fieldClass}
|
|
1114
|
-
/>
|
|
1115
|
-
</div>
|
|
1116
|
-
</div>
|
|
1117
|
-
<div className="mt-2">
|
|
1118
|
-
<Label hint="Markdown">Instructions</Label>
|
|
1119
|
-
<textarea
|
|
1120
|
-
value={draft.instructions}
|
|
1121
|
-
onChange={(event) => setDraft({ ...draft, instructions: event.target.value })}
|
|
1122
|
-
placeholder={STARTER_SKILL}
|
|
1123
|
-
rows={8}
|
|
1124
|
-
className="thin-scroll w-full resize-y rounded-lg border border-line bg-panel px-2.5 py-2 font-mono text-[10.5px] leading-relaxed text-ink outline-none focus:border-accent/50"
|
|
1125
|
-
/>
|
|
1126
|
-
</div>
|
|
1127
|
-
<div className="mt-2 flex justify-end gap-2">
|
|
1128
|
-
<button type="button" className="chip h-7 text-[10.5px]" onClick={() => setDraft(null)}>
|
|
1129
|
-
Cancel
|
|
1130
|
-
</button>
|
|
1131
|
-
<button
|
|
1132
|
-
type="button"
|
|
1133
|
-
onClick={save}
|
|
1134
|
-
className="inline-flex h-7 items-center gap-1 rounded-lg bg-accent px-2.5 text-[10.5px] font-medium text-on-accent hover:bg-accent/90"
|
|
1135
|
-
>
|
|
1136
|
-
<Check size={11} /> Save skill
|
|
1137
|
-
</button>
|
|
1138
|
-
</div>
|
|
1086
|
+
<p className="mb-3 text-[9.5px] leading-relaxed text-faint">
|
|
1087
|
+
A skill is a folder with a <span className="font-mono">SKILL.md</span> (name, description, instructions) plus any scripts it needs.
|
|
1088
|
+
Folder uploads are read in your browser; npm packages come from the public registry through the local daemon.
|
|
1089
|
+
</p>
|
|
1090
|
+
|
|
1091
|
+
{settings.skills.length === 0 ? (
|
|
1092
|
+
<div className="rounded-xl border border-dashed border-line px-4 py-8 text-center">
|
|
1093
|
+
<Sparkles size={17} className="mx-auto text-faint" />
|
|
1094
|
+
<p className="mt-2 text-[11.5px] font-medium text-ink">No skills installed</p>
|
|
1095
|
+
<p className="mt-1 text-[10px] text-muted">Install a skill folder or an npm package to give the agent new procedures and scripts.</p>
|
|
1139
1096
|
</div>
|
|
1140
|
-
)
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1097
|
+
) : (
|
|
1098
|
+
<div className="overflow-hidden rounded-xl border border-line">
|
|
1099
|
+
{settings.skills.map((skill, index) => (
|
|
1100
|
+
<div key={skill.id} className={cn('px-3 py-3', index !== 0 && 'border-t border-line')}>
|
|
1101
|
+
<div className="flex items-start gap-2.5">
|
|
1102
|
+
<span className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-accent-soft text-accent">
|
|
1103
|
+
<Sparkles size={11} />
|
|
1104
|
+
</span>
|
|
1105
|
+
<div className="min-w-0 flex-1">
|
|
1106
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
1107
|
+
<p className="text-[11.5px] font-medium text-ink">{skill.name}</p>
|
|
1108
|
+
<span className="rounded bg-raised px-1.5 py-0.5 text-[8px] font-medium uppercase tracking-wide text-faint">
|
|
1109
|
+
{skill.source ?? 'folder'}
|
|
1110
|
+
</span>
|
|
1111
|
+
{skill.version && <span className="font-mono text-[8.5px] text-faint">v{skill.version}</span>}
|
|
1112
|
+
<span className={cn('text-[8.5px]', skill.enabled ? 'text-success' : 'text-faint')}>
|
|
1113
|
+
{skill.enabled ? 'Enabled' : 'Off'}
|
|
1114
|
+
</span>
|
|
1115
|
+
</div>
|
|
1116
|
+
<p className="mt-0.5 text-[9.5px] leading-relaxed text-muted">{skill.description}</p>
|
|
1117
|
+
<p className="mt-0.5 truncate font-mono text-[8.5px] text-faint" title={skill.package || skill.id}>
|
|
1118
|
+
{skill.package || skill.id} · {Object.keys(skill.files ?? {}).length} files
|
|
1119
|
+
</p>
|
|
1120
|
+
<details className="group/skill mt-1">
|
|
1121
|
+
<summary className="inline-flex cursor-pointer list-none items-center gap-1 text-[9px] font-medium text-faint hover:text-muted [&::-webkit-details-marker]:hidden">
|
|
1122
|
+
View SKILL.md
|
|
1123
|
+
<ChevronDown size={9} className="transition-transform group-open/skill:rotate-180" />
|
|
1124
|
+
</summary>
|
|
1125
|
+
<pre className="thin-scroll mt-1.5 max-h-44 overflow-auto whitespace-pre-wrap rounded-lg border border-line bg-raised/40 p-2 font-mono text-[9.5px] leading-relaxed text-muted">
|
|
1126
|
+
{skill.instructions}
|
|
1127
|
+
</pre>
|
|
1128
|
+
</details>
|
|
1129
|
+
</div>
|
|
1130
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
1131
|
+
<button
|
|
1132
|
+
type="button"
|
|
1133
|
+
onClick={() => void remove(skill)}
|
|
1134
|
+
className="icon-button h-7 w-7 hover:text-danger"
|
|
1135
|
+
aria-label={`Delete ${skill.name}`}
|
|
1136
|
+
title="Delete skill"
|
|
1137
|
+
>
|
|
1138
|
+
<Trash2 size={12} />
|
|
1139
|
+
</button>
|
|
1140
|
+
<Switch
|
|
1141
|
+
checked={skill.enabled}
|
|
1142
|
+
onChange={(enabled) => updateSkill(skill.id, { enabled })}
|
|
1143
|
+
label={`Enable ${skill.name}`}
|
|
1144
|
+
/>
|
|
1158
1145
|
</div>
|
|
1159
|
-
<p className="mt-0.5 text-[9.5px] leading-relaxed text-muted">{skill.description}</p>
|
|
1160
|
-
<details className="group/skill mt-1">
|
|
1161
|
-
<summary className="inline-flex cursor-pointer list-none items-center gap-1 text-[9px] font-medium text-faint hover:text-muted [&::-webkit-details-marker]:hidden">
|
|
1162
|
-
View instructions
|
|
1163
|
-
<ChevronDown size={9} className="transition-transform group-open/skill:rotate-180" />
|
|
1164
|
-
</summary>
|
|
1165
|
-
<pre className="thin-scroll mt-1.5 max-h-44 overflow-auto whitespace-pre-wrap rounded-lg border border-line bg-raised/40 p-2 font-mono text-[9.5px] leading-relaxed text-muted">
|
|
1166
|
-
{skill.instructions}
|
|
1167
|
-
</pre>
|
|
1168
|
-
</details>
|
|
1169
|
-
</div>
|
|
1170
|
-
<div className="flex shrink-0 items-center gap-1">
|
|
1171
|
-
{!skill.builtIn && (
|
|
1172
|
-
<>
|
|
1173
|
-
<button
|
|
1174
|
-
type="button"
|
|
1175
|
-
onClick={() =>
|
|
1176
|
-
setDraft({
|
|
1177
|
-
id: skill.id,
|
|
1178
|
-
name: skill.name,
|
|
1179
|
-
description: skill.description,
|
|
1180
|
-
instructions: skill.instructions,
|
|
1181
|
-
})
|
|
1182
|
-
}
|
|
1183
|
-
className="icon-button h-7 w-7"
|
|
1184
|
-
aria-label={`Edit ${skill.name}`}
|
|
1185
|
-
title="Edit skill"
|
|
1186
|
-
>
|
|
1187
|
-
<FilePenLine size={12} />
|
|
1188
|
-
</button>
|
|
1189
|
-
<button
|
|
1190
|
-
type="button"
|
|
1191
|
-
onClick={() => void remove(skill)}
|
|
1192
|
-
className="icon-button h-7 w-7 hover:text-danger"
|
|
1193
|
-
aria-label={`Delete ${skill.name}`}
|
|
1194
|
-
title="Delete skill"
|
|
1195
|
-
>
|
|
1196
|
-
<Trash2 size={12} />
|
|
1197
|
-
</button>
|
|
1198
|
-
</>
|
|
1199
|
-
)}
|
|
1200
|
-
<Switch
|
|
1201
|
-
checked={skill.enabled}
|
|
1202
|
-
onChange={(enabled) => updateSkill(skill.id, { enabled })}
|
|
1203
|
-
label={`Enable ${skill.name}`}
|
|
1204
|
-
/>
|
|
1205
1146
|
</div>
|
|
1206
1147
|
</div>
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1148
|
+
))}
|
|
1149
|
+
</div>
|
|
1150
|
+
)}
|
|
1210
1151
|
</SettingsSection>
|
|
1211
1152
|
)
|
|
1212
1153
|
}
|
|
@@ -2038,14 +1979,17 @@ export function SettingsModal() {
|
|
|
2038
1979
|
{TABS.map((item, index) => {
|
|
2039
1980
|
const Icon = item.icon
|
|
2040
1981
|
const active = tab === item.id
|
|
1982
|
+
const disabled = item.id === 'plugins'
|
|
2041
1983
|
return (
|
|
2042
1984
|
<button
|
|
2043
1985
|
key={item.id}
|
|
2044
1986
|
type="button"
|
|
1987
|
+
disabled={disabled}
|
|
2045
1988
|
onClick={() => openSettings(item.id)}
|
|
2046
|
-
title={item.description}
|
|
1989
|
+
title={disabled ? 'Plugins are disabled for maintenance' : item.description}
|
|
2047
1990
|
className={cn(
|
|
2048
1991
|
'group/tab relative mb-[2px] flex h-10 w-full items-center gap-3 rounded-2xl px-3 text-left text-[12.5px] transition-colors duration-200',
|
|
1992
|
+
disabled && 'cursor-not-allowed opacity-40',
|
|
2049
1993
|
active ? 'font-semibold text-ink' : 'text-muted hover:text-ink',
|
|
2050
1994
|
)}
|
|
2051
1995
|
>
|
|
@@ -2054,6 +1998,7 @@ export function SettingsModal() {
|
|
|
2054
1998
|
</span>
|
|
2055
1999
|
<Icon size={14} className={cn('transition-transform duration-300 group-hover/tab:scale-110', active ? 'text-accent' : 'text-faint')} />
|
|
2056
2000
|
{item.label}
|
|
2001
|
+
{disabled && <span className="ml-auto text-[8px] text-faint">off</span>}
|
|
2057
2002
|
</button>
|
|
2058
2003
|
)
|
|
2059
2004
|
})}
|
|
@@ -243,14 +243,13 @@ export function Sidebar() {
|
|
|
243
243
|
</button>
|
|
244
244
|
<button
|
|
245
245
|
type="button"
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}}
|
|
250
|
-
className="nav-item"
|
|
246
|
+
disabled
|
|
247
|
+
title="Plugins are disabled for maintenance"
|
|
248
|
+
className="nav-item cursor-not-allowed opacity-45"
|
|
251
249
|
>
|
|
252
250
|
<Package size={15} strokeWidth={1.7} className="shrink-0 text-faint" />
|
|
253
251
|
Plugins
|
|
252
|
+
<span className="ml-auto rounded bg-raised px-1.5 py-0.5 text-[8.5px] font-medium text-faint">maintenance</span>
|
|
254
253
|
</button>
|
|
255
254
|
<button
|
|
256
255
|
type="button"
|
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
}
|