@aion0/forge 0.5.21 → 0.5.22
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/.forge/agent-context.json +1 -1
- package/.forge/mcp.json +1 -1
- package/RELEASE_NOTES.md +31 -9
- package/app/api/plugins/route.ts +75 -0
- package/components/Dashboard.tsx +1 -0
- package/components/PipelineEditor.tsx +135 -9
- package/components/PluginsPanel.tsx +472 -0
- package/components/ProjectDetail.tsx +36 -98
- package/components/SessionView.tsx +4 -4
- package/components/SettingsModal.tsx +160 -66
- package/components/SkillsPanel.tsx +14 -5
- package/components/TerminalLauncher.tsx +398 -0
- package/components/WebTerminal.tsx +84 -84
- package/components/WorkspaceView.tsx +256 -76
- package/lib/agents/index.ts +7 -4
- package/lib/builtin-plugins/docker.yaml +70 -0
- package/lib/builtin-plugins/http.yaml +66 -0
- package/lib/builtin-plugins/jenkins.yaml +92 -0
- package/lib/builtin-plugins/llm-vision.yaml +85 -0
- package/lib/builtin-plugins/playwright.yaml +111 -0
- package/lib/builtin-plugins/shell-command.yaml +60 -0
- package/lib/builtin-plugins/slack.yaml +48 -0
- package/lib/builtin-plugins/webhook.yaml +56 -0
- package/lib/forge-mcp-server.ts +116 -2
- package/lib/pipeline.ts +62 -5
- package/lib/plugins/executor.ts +347 -0
- package/lib/plugins/registry.ts +228 -0
- package/lib/plugins/types.ts +103 -0
- package/lib/project-sessions.ts +7 -2
- package/lib/session-utils.ts +7 -3
- package/lib/terminal-standalone.ts +6 -34
- package/lib/workspace/agent-worker.ts +1 -1
- package/lib/workspace/orchestrator.ts +414 -136
- package/lib/workspace/presets.ts +5 -3
- package/lib/workspace/session-monitor.ts +14 -10
- package/lib/workspace/types.ts +3 -1
- package/lib/workspace-standalone.ts +38 -21
- package/package.json +1 -1
- package/qa/.forge/agent-context.json +1 -1
|
@@ -442,8 +442,7 @@ export default function SessionView({
|
|
|
442
442
|
onClick={(e) => {
|
|
443
443
|
e.stopPropagation();
|
|
444
444
|
const pp = projects.find(p => p.name === project)?.path || '';
|
|
445
|
-
if (pp
|
|
446
|
-
else if (pp) window.dispatchEvent(new CustomEvent('forge:open-terminal', { detail: { projectPath: pp, projectName: project, agentId: 'claude', resumeMode: true, sessionId: s.sessionId } }));
|
|
445
|
+
if (pp) window.dispatchEvent(new CustomEvent('forge:open-terminal', { detail: { projectPath: pp, projectName: project, agentId: 'claude', resumeMode: true, sessionId: s.sessionId } }));
|
|
447
446
|
}}
|
|
448
447
|
className="text-[8px] px-1 py-0.5 rounded bg-green-500/10 text-green-400 hover:bg-green-500/20"
|
|
449
448
|
title="Open this session in terminal"
|
|
@@ -551,11 +550,11 @@ export default function SessionView({
|
|
|
551
550
|
Monitor
|
|
552
551
|
</button>
|
|
553
552
|
)}
|
|
554
|
-
{
|
|
553
|
+
{activeSessionId && (
|
|
555
554
|
<button
|
|
556
555
|
onClick={() => {
|
|
557
556
|
const proj = projects.find(p => p.name === selectedProject);
|
|
558
|
-
if (proj)
|
|
557
|
+
if (proj) window.dispatchEvent(new CustomEvent('forge:open-terminal', { detail: { projectPath: proj.path, projectName: selectedProject, agentId: 'claude', resumeMode: true, sessionId: activeSessionId } }));
|
|
559
558
|
}}
|
|
560
559
|
className="text-[10px] px-2 py-0.5 bg-[var(--accent)] text-white rounded hover:opacity-90"
|
|
561
560
|
>
|
|
@@ -602,6 +601,7 @@ export default function SessionView({
|
|
|
602
601
|
<div ref={bottomRef} />
|
|
603
602
|
</div>
|
|
604
603
|
</div>
|
|
604
|
+
|
|
605
605
|
</div>
|
|
606
606
|
);
|
|
607
607
|
}
|
|
@@ -809,7 +809,7 @@ function ProfileRow({ id, cfg, inputClass, onUpdate, onDelete }: {
|
|
|
809
809
|
const isApi = cfg.type === 'api';
|
|
810
810
|
const summary = isApi
|
|
811
811
|
? `API: ${cfg.provider || '?'} / ${cfg.model || '?'}`
|
|
812
|
-
: `CLI: ${cfg.base || '?'} / ${cfg.model || cfg.models?.task || 'default'}`;
|
|
812
|
+
: `CLI: ${cfg.cliType || cfg.base || '?'} / ${cfg.model || cfg.models?.task || 'default'}`;
|
|
813
813
|
const envStr = cfg.env ? Object.entries(cfg.env).map(([k, v]) => `${k}=${v}`).join('\n') : '';
|
|
814
814
|
|
|
815
815
|
return (
|
|
@@ -831,7 +831,22 @@ function ProfileRow({ id, cfg, inputClass, onUpdate, onDelete }: {
|
|
|
831
831
|
</div>
|
|
832
832
|
<div className="flex-1">
|
|
833
833
|
<label className="text-[8px] text-[var(--text-secondary)]">Model</label>
|
|
834
|
-
<input value={cfg.model || ''} onChange={e => onUpdate({ ...cfg, model: e.target.value })}
|
|
834
|
+
<input value={cfg.model || ''} onChange={e => onUpdate({ ...cfg, model: e.target.value })}
|
|
835
|
+
list={`profile-model-${id}`} className={inputClass} />
|
|
836
|
+
{(cfg.cliType === 'claude-code' || (!cfg.cliType && !cfg.base && !isApi)) && (
|
|
837
|
+
<datalist id={`profile-model-${id}`}>
|
|
838
|
+
<option value="claude-opus-4-6" />
|
|
839
|
+
<option value="claude-sonnet-4-6" />
|
|
840
|
+
<option value="claude-haiku-4-5-20251001" />
|
|
841
|
+
</datalist>
|
|
842
|
+
)}
|
|
843
|
+
{(cfg.cliType === 'codex' || cfg.base === 'codex') && (
|
|
844
|
+
<datalist id={`profile-model-${id}`}>
|
|
845
|
+
<option value="codex-mini" />
|
|
846
|
+
<option value="o4-mini" />
|
|
847
|
+
<option value="gpt-4o" />
|
|
848
|
+
</datalist>
|
|
849
|
+
)}
|
|
835
850
|
</div>
|
|
836
851
|
</div>
|
|
837
852
|
{isApi ? (
|
|
@@ -854,7 +869,7 @@ function ProfileRow({ id, cfg, inputClass, onUpdate, onDelete }: {
|
|
|
854
869
|
<>
|
|
855
870
|
<div>
|
|
856
871
|
<label className="text-[8px] text-[var(--text-secondary)]">CLI Type</label>
|
|
857
|
-
<select value={cfg.base || 'claude'} onChange={e => onUpdate({ ...cfg,
|
|
872
|
+
<select value={cfg.cliType === 'claude-code' ? 'claude' : (cfg.cliType || cfg.base || 'claude')} onChange={e => onUpdate({ ...cfg, cliType: e.target.value === 'claude' ? 'claude-code' : e.target.value })} className={inputClass}>
|
|
858
873
|
<option value="claude">Claude Code</option>
|
|
859
874
|
<option value="codex">Codex</option>
|
|
860
875
|
<option value="aider">Aider</option>
|
|
@@ -864,14 +879,15 @@ function ProfileRow({ id, cfg, inputClass, onUpdate, onDelete }: {
|
|
|
864
879
|
<div>
|
|
865
880
|
<div className="flex items-center gap-2">
|
|
866
881
|
<label className="text-[8px] text-[var(--text-secondary)]">Environment Variables (KEY=VALUE per line)</label>
|
|
867
|
-
{cfg.base && (
|
|
882
|
+
{(cfg.cliType || cfg.base) && (
|
|
868
883
|
<button onClick={() => {
|
|
869
884
|
const templates: Record<string, string> = {
|
|
885
|
+
'claude-code': 'ANTHROPIC_AUTH_TOKEN=\nANTHROPIC_BASE_URL=\nANTHROPIC_SMALL_FAST_MODEL=\nCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=true\nDISABLE_TELEMETRY=true\nDISABLE_ERROR_REPORTING=true\nDISABLE_AUTOUPDATER=true\nDISABLE_NON_ESSENTIAL_MODEL_CALLS=true',
|
|
870
886
|
claude: 'ANTHROPIC_AUTH_TOKEN=\nANTHROPIC_BASE_URL=\nANTHROPIC_SMALL_FAST_MODEL=\nCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=true\nDISABLE_TELEMETRY=true\nDISABLE_ERROR_REPORTING=true\nDISABLE_AUTOUPDATER=true\nDISABLE_NON_ESSENTIAL_MODEL_CALLS=true',
|
|
871
887
|
codex: 'OPENAI_API_KEY=\nOPENAI_BASE_URL=',
|
|
872
888
|
aider: 'ANTHROPIC_API_KEY=\nOPENAI_API_KEY=',
|
|
873
889
|
};
|
|
874
|
-
const tpl = templates[cfg.base!];
|
|
890
|
+
const tpl = templates[cfg.cliType || cfg.base!];
|
|
875
891
|
if (tpl) {
|
|
876
892
|
const env: Record<string, string> = {};
|
|
877
893
|
for (const line of tpl.split('\n')) {
|
|
@@ -883,7 +899,7 @@ function ProfileRow({ id, cfg, inputClass, onUpdate, onDelete }: {
|
|
|
883
899
|
onUpdate({ ...cfg, env: merged });
|
|
884
900
|
}
|
|
885
901
|
}} className="text-[7px] px-1.5 py-0.5 rounded bg-[var(--accent)]/10 text-[var(--accent)] hover:bg-[var(--accent)]/20">
|
|
886
|
-
Fill {cfg.base} template
|
|
902
|
+
Fill {cfg.cliType === 'claude-code' ? 'claude' : (cfg.cliType || cfg.base)} template
|
|
887
903
|
</button>
|
|
888
904
|
)}
|
|
889
905
|
</div>
|
|
@@ -978,7 +994,7 @@ function AddProfileForm({ type, baseAgents, onAdd }: {
|
|
|
978
994
|
const handleAdd = () => {
|
|
979
995
|
if (!id) return;
|
|
980
996
|
if (type === 'cli') {
|
|
981
|
-
onAdd(id, {
|
|
997
|
+
onAdd(id, { cliType: base === 'claude' ? 'claude-code' : base, name: name || id, model: model || undefined, env: parseEnv() });
|
|
982
998
|
} else {
|
|
983
999
|
onAdd(id, { type: 'api', name: name || id, provider, model: model || undefined, apiKey: apiKey || undefined });
|
|
984
1000
|
}
|
|
@@ -1013,7 +1029,23 @@ function AddProfileForm({ type, baseAgents, onAdd }: {
|
|
|
1013
1029
|
</div>
|
|
1014
1030
|
<div className="flex-1">
|
|
1015
1031
|
<label className="text-[8px] text-[var(--text-secondary)]">Model</label>
|
|
1016
|
-
<input value={model} onChange={e => setModel(e.target.value)}
|
|
1032
|
+
<input value={model} onChange={e => setModel(e.target.value)}
|
|
1033
|
+
placeholder={base === 'claude' ? 'claude-sonnet-4-6' : base === 'codex' ? 'codex-mini' : ''}
|
|
1034
|
+
list={`model-list-${base}`} className={inputClass} />
|
|
1035
|
+
{base === 'claude' && (
|
|
1036
|
+
<datalist id="model-list-claude">
|
|
1037
|
+
<option value="claude-opus-4-6" />
|
|
1038
|
+
<option value="claude-sonnet-4-6" />
|
|
1039
|
+
<option value="claude-haiku-4-5-20251001" />
|
|
1040
|
+
</datalist>
|
|
1041
|
+
)}
|
|
1042
|
+
{base === 'codex' && (
|
|
1043
|
+
<datalist id="model-list-codex">
|
|
1044
|
+
<option value="codex-mini" />
|
|
1045
|
+
<option value="o4-mini" />
|
|
1046
|
+
<option value="gpt-4o" />
|
|
1047
|
+
</datalist>
|
|
1048
|
+
)}
|
|
1017
1049
|
</div>
|
|
1018
1050
|
</div>
|
|
1019
1051
|
<div>
|
|
@@ -1065,17 +1097,35 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1065
1097
|
const [loading, setLoading] = useState(true);
|
|
1066
1098
|
const [expandedAgent, setExpandedAgent] = useState<string | null>(null);
|
|
1067
1099
|
const [showAdd, setShowAdd] = useState(false);
|
|
1068
|
-
const
|
|
1100
|
+
const cliDefaults: Record<string, any> = {
|
|
1101
|
+
'claude-code': { taskFlags: '-p --verbose --output-format stream-json --dangerously-skip-permissions', resumeFlag: '-c', outputFormat: 'stream-json', skipPermissionsFlag: '--dangerously-skip-permissions' },
|
|
1102
|
+
'codex': { taskFlags: '', resumeFlag: '', outputFormat: 'text', skipPermissionsFlag: '--full-auto' },
|
|
1103
|
+
'aider': { taskFlags: '--message', resumeFlag: '', outputFormat: 'text', skipPermissionsFlag: '--yes' },
|
|
1104
|
+
'generic': { taskFlags: '', resumeFlag: '', outputFormat: 'text', skipPermissionsFlag: '' },
|
|
1105
|
+
};
|
|
1106
|
+
const makeNewAgent = (cliType = 'claude-code') => ({
|
|
1107
|
+
id: '', name: '', path: '', interactiveCmd: '',
|
|
1108
|
+
models: { terminal: 'default', task: 'default', telegram: 'default', help: 'default', mobile: 'default' },
|
|
1109
|
+
requiresTTY: false, cliType,
|
|
1110
|
+
...cliDefaults[cliType],
|
|
1111
|
+
});
|
|
1112
|
+
const [newAgent, setNewAgent] = useState(makeNewAgent());
|
|
1069
1113
|
|
|
1070
1114
|
// Fetch detected + configured agents
|
|
1071
1115
|
useEffect(() => {
|
|
1072
1116
|
(async () => {
|
|
1073
1117
|
setLoading(true);
|
|
1074
1118
|
try {
|
|
1075
|
-
|
|
1076
|
-
|
|
1119
|
+
// Fetch both agents and settings together to avoid race condition
|
|
1120
|
+
// (settings prop may not be loaded yet when this effect runs)
|
|
1121
|
+
const [agentsRes, settingsRes] = await Promise.all([
|
|
1122
|
+
fetch('/api/agents'),
|
|
1123
|
+
fetch('/api/settings'),
|
|
1124
|
+
]);
|
|
1125
|
+
const data = await agentsRes.json();
|
|
1126
|
+
const settingsData = await settingsRes.json();
|
|
1077
1127
|
const detected = (data.agents || []) as any[];
|
|
1078
|
-
const configured =
|
|
1128
|
+
const configured = settingsData.agents || {};
|
|
1079
1129
|
|
|
1080
1130
|
const merged: AgentEntry[] = [];
|
|
1081
1131
|
|
|
@@ -1084,16 +1134,16 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1084
1134
|
const cfg = configured[a.id] || {};
|
|
1085
1135
|
merged.push({
|
|
1086
1136
|
id: a.id,
|
|
1087
|
-
name: cfg.name
|
|
1088
|
-
path: cfg.path
|
|
1137
|
+
name: cfg.name ?? a.name,
|
|
1138
|
+
path: cfg.path ?? a.path,
|
|
1089
1139
|
enabled: cfg.enabled !== false,
|
|
1090
1140
|
type: a.type || 'generic',
|
|
1091
|
-
taskFlags: cfg.taskFlags
|
|
1092
|
-
interactiveCmd: cfg.interactiveCmd
|
|
1093
|
-
resumeFlag: cfg.resumeFlag
|
|
1094
|
-
outputFormat: cfg.outputFormat
|
|
1095
|
-
models: cfg.models
|
|
1096
|
-
skipPermissionsFlag: cfg.skipPermissionsFlag
|
|
1141
|
+
taskFlags: cfg.taskFlags ?? (a.id === 'claude' ? '-p --verbose --output-format stream-json --dangerously-skip-permissions' : cfg.flags?.join(' ') ?? ''),
|
|
1142
|
+
interactiveCmd: cfg.interactiveCmd ?? a.path,
|
|
1143
|
+
resumeFlag: cfg.resumeFlag ?? (a.capabilities?.supportsResume ? '-c' : ''),
|
|
1144
|
+
outputFormat: cfg.outputFormat ?? (a.capabilities?.supportsStreamJson ? 'stream-json' : 'text'),
|
|
1145
|
+
models: cfg.models ?? { terminal: "default", task: "default", telegram: "default", help: "default", mobile: "default" },
|
|
1146
|
+
skipPermissionsFlag: cfg.skipPermissionsFlag ?? a.skipPermissionsFlag ?? "",
|
|
1097
1147
|
requiresTTY: cfg.requiresTTY ?? a.capabilities?.requiresTTY ?? false,
|
|
1098
1148
|
detected: a.detected !== false,
|
|
1099
1149
|
});
|
|
@@ -1104,16 +1154,16 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1104
1154
|
if (merged.find(a => a.id === id)) continue;
|
|
1105
1155
|
merged.push({
|
|
1106
1156
|
id,
|
|
1107
|
-
name: cfg.name
|
|
1108
|
-
path: cfg.path
|
|
1157
|
+
name: cfg.name ?? id,
|
|
1158
|
+
path: cfg.path ?? '',
|
|
1109
1159
|
enabled: cfg.enabled !== false,
|
|
1110
1160
|
type: 'generic',
|
|
1111
|
-
taskFlags: cfg.taskFlags
|
|
1112
|
-
interactiveCmd: cfg.interactiveCmd
|
|
1113
|
-
resumeFlag: cfg.resumeFlag
|
|
1114
|
-
outputFormat: cfg.outputFormat
|
|
1115
|
-
models: cfg.models
|
|
1116
|
-
skipPermissionsFlag: cfg.skipPermissionsFlag
|
|
1161
|
+
taskFlags: cfg.taskFlags ?? cfg.flags?.join(' ') ?? '',
|
|
1162
|
+
interactiveCmd: cfg.interactiveCmd ?? cfg.path ?? '',
|
|
1163
|
+
resumeFlag: cfg.resumeFlag ?? '',
|
|
1164
|
+
outputFormat: cfg.outputFormat ?? 'text',
|
|
1165
|
+
models: cfg.models ?? { terminal: "default", task: "default", telegram: "default", help: "default", mobile: "default" },
|
|
1166
|
+
skipPermissionsFlag: cfg.skipPermissionsFlag ?? '',
|
|
1117
1167
|
requiresTTY: cfg.requiresTTY ?? false,
|
|
1118
1168
|
detected: false,
|
|
1119
1169
|
});
|
|
@@ -1129,27 +1179,29 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1129
1179
|
const defaultAgent = settings.defaultAgent || 'claude';
|
|
1130
1180
|
|
|
1131
1181
|
const saveAgentConfig = (updated: AgentEntry[]) => {
|
|
1132
|
-
//
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1182
|
+
// Use functional update to avoid stale closure — each call sees the latest settings
|
|
1183
|
+
setSettings((prev: any) => {
|
|
1184
|
+
const agentsCfg: Record<string, any> = { ...(prev.agents || {}) };
|
|
1185
|
+
for (const a of updated) {
|
|
1186
|
+
const existing = agentsCfg[a.id] || {};
|
|
1187
|
+
agentsCfg[a.id] = {
|
|
1188
|
+
...existing, // preserve profile-specific fields
|
|
1189
|
+
name: a.name,
|
|
1190
|
+
path: a.path,
|
|
1191
|
+
enabled: a.enabled,
|
|
1192
|
+
taskFlags: a.taskFlags,
|
|
1193
|
+
interactiveCmd: a.interactiveCmd,
|
|
1194
|
+
resumeFlag: a.resumeFlag,
|
|
1195
|
+
outputFormat: a.outputFormat,
|
|
1196
|
+
models: a.models,
|
|
1197
|
+
skipPermissionsFlag: a.skipPermissionsFlag,
|
|
1198
|
+
requiresTTY: a.requiresTTY,
|
|
1199
|
+
cliType: (a as any).cliType || existing.cliType,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
const claude = updated.find(a => a.id === 'claude');
|
|
1203
|
+
return { ...prev, agents: agentsCfg, claudePath: claude?.path || prev.claudePath };
|
|
1204
|
+
});
|
|
1153
1205
|
};
|
|
1154
1206
|
|
|
1155
1207
|
const [agentsDirty, setAgentsDirty] = useState(false);
|
|
@@ -1166,8 +1218,8 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1166
1218
|
const updateAgent = (id: string, field: string, value: any) => {
|
|
1167
1219
|
const updated = agents.map(a => a.id === id ? { ...a, [field]: value } : a);
|
|
1168
1220
|
setAgents(updated);
|
|
1169
|
-
|
|
1170
|
-
|
|
1221
|
+
// Sync to settings immediately (no debounce) so global Save always has latest data
|
|
1222
|
+
saveAgentConfig(updated);
|
|
1171
1223
|
};
|
|
1172
1224
|
|
|
1173
1225
|
const saveAgents = () => {
|
|
@@ -1195,7 +1247,7 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1195
1247
|
setAgents(updated);
|
|
1196
1248
|
debouncedSave(updated);
|
|
1197
1249
|
setShowAdd(false);
|
|
1198
|
-
setNewAgent(
|
|
1250
|
+
setNewAgent(makeNewAgent());
|
|
1199
1251
|
};
|
|
1200
1252
|
|
|
1201
1253
|
const inputClass = "w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] font-mono focus:outline-none focus:border-[var(--accent)]";
|
|
@@ -1216,7 +1268,12 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1216
1268
|
className="text-[9px] px-2 py-0.5 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white ml-auto"
|
|
1217
1269
|
>Detect</button>
|
|
1218
1270
|
<button
|
|
1219
|
-
onClick={() =>
|
|
1271
|
+
onClick={() => {
|
|
1272
|
+
// Auto-fill path from detected claude agent when opening Add form
|
|
1273
|
+
const claude = agents.find(a => a.id === 'claude');
|
|
1274
|
+
if (claude?.path && !newAgent.path) setNewAgent((prev: any) => ({ ...prev, path: claude.path, interactiveCmd: claude.path }));
|
|
1275
|
+
setShowAdd(v => !v);
|
|
1276
|
+
}}
|
|
1220
1277
|
className="text-[9px] px-2 py-0.5 border border-[var(--border)] text-[var(--text-secondary)] rounded hover:text-[var(--text-primary)]"
|
|
1221
1278
|
>+ Add</button>
|
|
1222
1279
|
{agentsDirty && (
|
|
@@ -1339,12 +1396,12 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1339
1396
|
{/* Preset models */}
|
|
1340
1397
|
<div className="flex items-center gap-1 mt-1.5 flex-wrap">
|
|
1341
1398
|
<span className="text-[8px] text-[var(--text-secondary)]">Presets:</span>
|
|
1342
|
-
{(
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
).map(preset => (
|
|
1399
|
+
{((() => {
|
|
1400
|
+
const ct = (settings.agents?.[a.id] as any)?.cliType || (a.id === 'claude' ? 'claude-code' : a.id === 'codex' ? 'codex' : a.id === 'aider' ? 'aider' : 'generic');
|
|
1401
|
+
if (ct === 'claude-code') return ['default', 'sonnet', 'opus', 'haiku', 'claude-sonnet-4-6', 'claude-opus-4-6', 'claude-haiku-4-5-20251001'];
|
|
1402
|
+
if (ct === 'codex') return ['default', 'o3-mini', 'o4-mini', 'gpt-4.1'];
|
|
1403
|
+
return ['default'];
|
|
1404
|
+
})()).map(preset => (
|
|
1348
1405
|
<button
|
|
1349
1406
|
key={preset}
|
|
1350
1407
|
onClick={() => navigator.clipboard.writeText(preset)}
|
|
@@ -1403,7 +1460,7 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1403
1460
|
{showAdd && (
|
|
1404
1461
|
<div className="border border-[var(--accent)]/30 rounded-lg p-3 space-y-2 bg-[var(--bg-secondary)]">
|
|
1405
1462
|
<div className="text-[10px] text-[var(--text-primary)] font-semibold">Add Custom Agent</div>
|
|
1406
|
-
<div className="grid grid-cols-
|
|
1463
|
+
<div className="grid grid-cols-3 gap-2">
|
|
1407
1464
|
<div>
|
|
1408
1465
|
<label className="text-[9px] text-[var(--text-secondary)]">ID (unique)</label>
|
|
1409
1466
|
<input value={newAgent.id} onChange={e => setNewAgent({ ...newAgent, id: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '') })} placeholder="my-agent" className={inputClass} />
|
|
@@ -1412,14 +1469,51 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1412
1469
|
<label className="text-[9px] text-[var(--text-secondary)]">Display Name</label>
|
|
1413
1470
|
<input value={newAgent.name} onChange={e => setNewAgent({ ...newAgent, name: e.target.value })} placeholder="My Agent" className={inputClass} />
|
|
1414
1471
|
</div>
|
|
1472
|
+
<div>
|
|
1473
|
+
<label className="text-[9px] text-[var(--text-secondary)]">CLI Type</label>
|
|
1474
|
+
<select value={newAgent.cliType} onChange={e => {
|
|
1475
|
+
const ct = e.target.value;
|
|
1476
|
+
// Auto-fill path from detected agent if available
|
|
1477
|
+
const baseId = ct === 'claude-code' ? 'claude' : ct;
|
|
1478
|
+
const detected = agents.find(a => a.id === baseId);
|
|
1479
|
+
setNewAgent({ ...newAgent, cliType: ct, ...(cliDefaults[ct] || {}), path: detected?.path || newAgent.path, interactiveCmd: detected?.path || newAgent.interactiveCmd });
|
|
1480
|
+
}} className={inputClass}>
|
|
1481
|
+
<option value="claude-code">Claude Code</option>
|
|
1482
|
+
<option value="codex">Codex</option>
|
|
1483
|
+
<option value="aider">Aider</option>
|
|
1484
|
+
<option value="generic">Generic</option>
|
|
1485
|
+
</select>
|
|
1486
|
+
</div>
|
|
1415
1487
|
</div>
|
|
1416
|
-
<div>
|
|
1417
|
-
<
|
|
1418
|
-
|
|
1488
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1489
|
+
<div>
|
|
1490
|
+
<label className="text-[9px] text-[var(--text-secondary)]">Binary Path</label>
|
|
1491
|
+
<input value={newAgent.path} onChange={e => setNewAgent({ ...newAgent, path: e.target.value })}
|
|
1492
|
+
placeholder={newAgent.cliType === 'claude-code' ? 'claude' : newAgent.cliType === 'codex' ? 'codex' : newAgent.cliType === 'aider' ? 'aider' : '/usr/local/bin/agent'}
|
|
1493
|
+
className={inputClass} />
|
|
1494
|
+
</div>
|
|
1495
|
+
<div>
|
|
1496
|
+
<label className="text-[9px] text-[var(--text-secondary)]">Task Flags (non-interactive)</label>
|
|
1497
|
+
<input value={newAgent.taskFlags} onChange={e => setNewAgent({ ...newAgent, taskFlags: e.target.value })} placeholder="--prompt" className={inputClass} />
|
|
1498
|
+
</div>
|
|
1419
1499
|
</div>
|
|
1420
|
-
<div>
|
|
1421
|
-
<
|
|
1422
|
-
|
|
1500
|
+
<div className="grid grid-cols-3 gap-2">
|
|
1501
|
+
<div>
|
|
1502
|
+
<label className="text-[9px] text-[var(--text-secondary)]">Resume Flag</label>
|
|
1503
|
+
<input value={newAgent.resumeFlag} onChange={e => setNewAgent({ ...newAgent, resumeFlag: e.target.value })} placeholder="-c or --resume" className={inputClass} />
|
|
1504
|
+
</div>
|
|
1505
|
+
<div>
|
|
1506
|
+
<label className="text-[9px] text-[var(--text-secondary)]">Output Format</label>
|
|
1507
|
+
<select value={newAgent.outputFormat} onChange={e => setNewAgent({ ...newAgent, outputFormat: e.target.value })} className={inputClass}>
|
|
1508
|
+
<option value="stream-json">stream-json</option>
|
|
1509
|
+
<option value="json">json</option>
|
|
1510
|
+
<option value="text">text</option>
|
|
1511
|
+
</select>
|
|
1512
|
+
</div>
|
|
1513
|
+
<div>
|
|
1514
|
+
<label className="text-[9px] text-[var(--text-secondary)]">Skip Permissions Flag</label>
|
|
1515
|
+
<input value={newAgent.skipPermissionsFlag} onChange={e => setNewAgent({ ...newAgent, skipPermissionsFlag: e.target.value })} placeholder="--dangerously-skip-permissions" className={inputClass} />
|
|
1516
|
+
</div>
|
|
1423
1517
|
</div>
|
|
1424
1518
|
<div className="flex gap-2">
|
|
1425
1519
|
<button onClick={addAgent} disabled={!newAgent.id || !newAgent.path} className="text-[10px] px-3 py-1 bg-[var(--accent)] text-white rounded disabled:opacity-50">Add</button>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { useState, useEffect, useCallback, lazy, Suspense } from 'react';
|
|
4
4
|
import { useSidebarResize } from '@/hooks/useSidebarResize';
|
|
5
5
|
|
|
6
|
+
const PluginsPanel = lazy(() => import('./PluginsPanel'));
|
|
7
|
+
|
|
6
8
|
type ItemType = 'skill' | 'command';
|
|
7
9
|
|
|
8
10
|
interface Skill {
|
|
@@ -131,7 +133,7 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
|
|
|
131
133
|
const [syncing, setSyncing] = useState(false);
|
|
132
134
|
const [loading, setLoading] = useState(true);
|
|
133
135
|
const [installTarget, setInstallTarget] = useState<{ skill: string; show: boolean }>({ skill: '', show: false });
|
|
134
|
-
const [typeFilter, setTypeFilter] = useState<'all' | 'skill' | 'command' | 'local' | 'rules'>('all');
|
|
136
|
+
const [typeFilter, setTypeFilter] = useState<'all' | 'skill' | 'command' | 'local' | 'rules' | 'plugins'>('all');
|
|
135
137
|
const [localItems, setLocalItems] = useState<{ name: string; type: string; scope: string; fileCount: number; projectPath?: string }[]>([]);
|
|
136
138
|
// Rules (CLAUDE.md templates)
|
|
137
139
|
const [rulesTemplates, setRulesTemplates] = useState<{ id: string; name: string; description: string; tags: string[]; builtin: boolean; isDefault: boolean; content: string }[]>([]);
|
|
@@ -363,7 +365,7 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
|
|
|
363
365
|
<div className="flex items-center gap-2">
|
|
364
366
|
<span className="text-xs font-semibold text-[var(--text-primary)]">Marketplace</span>
|
|
365
367
|
<div className="flex items-center bg-[var(--bg-tertiary)] rounded p-0.5">
|
|
366
|
-
{([['all', `All (${skills.length})`], ['skill', `Skills (${skillCount})`], ['command', `Commands (${commandCount})`], ['local', `Local (${localCount})`], ['rules', 'Rules']] as const).map(([value, label]) => (
|
|
368
|
+
{([['all', `All (${skills.length})`], ['skill', `Skills (${skillCount})`], ['command', `Commands (${commandCount})`], ['local', `Local (${localCount})`], ['rules', 'Rules'], ['plugins', 'Plugins']] as const).map(([value, label]) => (
|
|
367
369
|
<button
|
|
368
370
|
key={value}
|
|
369
371
|
onClick={() => setTypeFilter(value)}
|
|
@@ -388,7 +390,7 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
|
|
|
388
390
|
</button>
|
|
389
391
|
</div>
|
|
390
392
|
{/* Search — hide on rules tab */}
|
|
391
|
-
{typeFilter !== 'rules' && <div className="px-3 py-1.5 border-b border-[var(--border)] shrink-0">
|
|
393
|
+
{typeFilter !== 'rules' && typeFilter !== 'plugins' && <div className="px-3 py-1.5 border-b border-[var(--border)] shrink-0">
|
|
392
394
|
<input
|
|
393
395
|
type="text"
|
|
394
396
|
value={searchQuery}
|
|
@@ -398,7 +400,7 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
|
|
|
398
400
|
/>
|
|
399
401
|
</div>}
|
|
400
402
|
|
|
401
|
-
{typeFilter === 'rules' ? null : skills.length === 0 ? (
|
|
403
|
+
{typeFilter === 'rules' || typeFilter === 'plugins' ? null : skills.length === 0 ? (
|
|
402
404
|
<div className="flex-1 flex flex-col items-center justify-center gap-2 text-[var(--text-secondary)]">
|
|
403
405
|
<p className="text-xs">No skills yet</p>
|
|
404
406
|
<button onClick={sync} className="text-xs px-3 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90">
|
|
@@ -955,6 +957,13 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
|
|
|
955
957
|
</div>
|
|
956
958
|
</div>
|
|
957
959
|
)}
|
|
960
|
+
|
|
961
|
+
{/* Plugins — full-page view */}
|
|
962
|
+
{typeFilter === 'plugins' && (
|
|
963
|
+
<Suspense fallback={<div className="p-4 text-xs text-[var(--text-secondary)]">Loading...</div>}>
|
|
964
|
+
<PluginsPanel />
|
|
965
|
+
</Suspense>
|
|
966
|
+
)}
|
|
958
967
|
</div>
|
|
959
968
|
);
|
|
960
969
|
}
|