@lucashca/claudecontrol 0.3.31 → 0.3.33
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/backend/routes/agents.ts +4 -0
- package/backend/routes/workspaces.ts +28 -1
- package/backend/workspaces.ts +7 -0
- package/frontend/src/api.ts +6 -0
- package/frontend/src/components/ChatPanel.tsx +12 -10
- package/frontend/src/components/side/RoleItem.tsx +136 -3
- package/frontend/src/types/index.ts +7 -0
- package/package.json +1 -1
- package/version.json +1 -1
package/backend/routes/agents.ts
CHANGED
|
@@ -24,6 +24,7 @@ export async function handleAgentRoutes(
|
|
|
24
24
|
const role = body.role as string;
|
|
25
25
|
if (!role) { jsonError(res, 'role is required'); return true; }
|
|
26
26
|
|
|
27
|
+
const template = workspace.roleTemplates?.[role];
|
|
27
28
|
const task = (body.task as string) || `Voce e o agente ${role}. Leia o CLAUDE.md. Diga que esta pronto.`;
|
|
28
29
|
const bypass = body.bypass === true;
|
|
29
30
|
const spawnedBy = (body.spawnedBy as string) || undefined;
|
|
@@ -54,6 +55,9 @@ export async function handleAgentRoutes(
|
|
|
54
55
|
lastActivity: new Date().toISOString(),
|
|
55
56
|
currentTask: task,
|
|
56
57
|
log: [initEntry],
|
|
58
|
+
...(template?.model && { model: template.model }),
|
|
59
|
+
...(template?.isOrchestrator !== undefined && { isOrchestrator: template.isOrchestrator }),
|
|
60
|
+
...(template?.canSpawnSubagents !== undefined && { canSpawnSubagents: template.canSpawnSubagents }),
|
|
57
61
|
};
|
|
58
62
|
|
|
59
63
|
addAgentToIndex(agentSession);
|
|
@@ -3,7 +3,7 @@ import { readdirSync, statSync, mkdirSync } from 'fs';
|
|
|
3
3
|
import { resolve } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { listExistingSessions } from '../sessions.js';
|
|
6
|
-
import { loadWorkspaces, addWorkspace, removeWorkspace, updateWorkspace, getWorkspaceById, type Workspace } from '../workspaces.js';
|
|
6
|
+
import { loadWorkspaces, addWorkspace, removeWorkspace, updateWorkspace, getWorkspaceById, type Workspace, type RoleTemplate } from '../workspaces.js';
|
|
7
7
|
import { json, jsonError, parseBody, matchRoute } from '../http.js';
|
|
8
8
|
import { broadcast } from '../ws.js';
|
|
9
9
|
import { genId, agentsByWorkspace, removeAgentFromIndex, getAgentsForWorkspace } from '../agents/store.js';
|
|
@@ -142,6 +142,33 @@ export async function handleWorkspaceRoutes(
|
|
|
142
142
|
return true;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// ── GET /api/workspaces/:id/role-template/:role ──
|
|
146
|
+
// ── PUT /api/workspaces/:id/role-template/:role ──
|
|
147
|
+
const roleTemplateParams = matchRoute(path, '/api/workspaces/:id/role-template/:role');
|
|
148
|
+
if (roleTemplateParams) {
|
|
149
|
+
const workspace = getWorkspaceById(roleTemplateParams.id);
|
|
150
|
+
if (!workspace) { jsonError(res, 'Workspace not found', 404); return true; }
|
|
151
|
+
|
|
152
|
+
if (method === 'GET') {
|
|
153
|
+
const tpl = workspace.roleTemplates?.[roleTemplateParams.role] ?? {};
|
|
154
|
+
json(res, tpl);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (method === 'PUT') {
|
|
159
|
+
const body = await parseBody(req);
|
|
160
|
+
const tpl: RoleTemplate = {
|
|
161
|
+
model: (body.model as string) || undefined,
|
|
162
|
+
isOrchestrator: body.isOrchestrator as boolean | undefined,
|
|
163
|
+
canSpawnSubagents: body.canSpawnSubagents as boolean | undefined,
|
|
164
|
+
};
|
|
165
|
+
const templates = { ...(workspace.roleTemplates || {}), [roleTemplateParams.role]: tpl };
|
|
166
|
+
updateWorkspace(roleTemplateParams.id, { roleTemplates: templates });
|
|
167
|
+
json(res, tpl);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
145
172
|
// ── DELETE /api/workspaces/:id ──
|
|
146
173
|
const wsDeleteParams = matchRoute(path, '/api/workspaces/:id');
|
|
147
174
|
if (wsDeleteParams && method === 'DELETE') {
|
package/backend/workspaces.ts
CHANGED
|
@@ -6,6 +6,12 @@ function workspacesFile(): string {
|
|
|
6
6
|
return resolve(getDataDir(), 'workspaces.json');
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export interface RoleTemplate {
|
|
10
|
+
model?: string;
|
|
11
|
+
isOrchestrator?: boolean;
|
|
12
|
+
canSpawnSubagents?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
export interface Workspace {
|
|
10
16
|
id: string;
|
|
11
17
|
name: string;
|
|
@@ -13,6 +19,7 @@ export interface Workspace {
|
|
|
13
19
|
agents: string[]; // roles disponiveis neste workspace
|
|
14
20
|
color: string;
|
|
15
21
|
orchestratorRole?: string; // role that acts as orchestrator (gets subagent tools + pool context)
|
|
22
|
+
roleTemplates?: Record<string, RoleTemplate>; // per-role default config
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
const DEFAULT_WORKSPACES: Workspace[] = [];
|
package/frontend/src/api.ts
CHANGED
|
@@ -116,6 +116,12 @@ export const api = {
|
|
|
116
116
|
getClaudeAccount: () => get('/settings/claude-account'),
|
|
117
117
|
openVSCode: (wsId: string) => post(`/workspaces/${wsId}/open-vscode`),
|
|
118
118
|
|
|
119
|
+
getRoleTemplate: (wsId: string, role: string) =>
|
|
120
|
+
get(`/workspaces/${wsId}/role-template/${encodeURIComponent(role)}`) as Promise<import('./types').RoleTemplate>,
|
|
121
|
+
|
|
122
|
+
updateRoleTemplate: (wsId: string, role: string, template: import('./types').RoleTemplate) =>
|
|
123
|
+
put(`/workspaces/${wsId}/role-template/${encodeURIComponent(role)}`, template) as Promise<import('./types').RoleTemplate>,
|
|
124
|
+
|
|
119
125
|
respondSpawnRequest: (requestId: string, approved: boolean, maxCount?: number, approvedRoles?: string[]) =>
|
|
120
126
|
post(`/agents/spawn-request/${requestId}/respond`, { approved, maxCount, approvedRoles }),
|
|
121
127
|
|
|
@@ -107,16 +107,18 @@ export function ChatPanel({ agentId, compact }: { agentId: string; compact?: boo
|
|
|
107
107
|
<GitBranch size={12} />
|
|
108
108
|
</button>
|
|
109
109
|
</Tooltip>
|
|
110
|
-
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
110
|
+
<Tooltip text="Parar agente" position="bottom">
|
|
111
|
+
<button
|
|
112
|
+
onClick={handleStop}
|
|
113
|
+
className={`inline-flex items-center justify-center w-6 h-6 rounded-md border-none cursor-pointer transition-colors ${
|
|
114
|
+
agent.status === 'working'
|
|
115
|
+
? 'text-cc-error hover:bg-cc-error/15'
|
|
116
|
+
: 'text-cc-muted hover:text-cc-error hover:bg-cc-error/15'
|
|
117
|
+
}`}
|
|
118
|
+
>
|
|
119
|
+
<Square size={12} />
|
|
120
|
+
</button>
|
|
121
|
+
</Tooltip>
|
|
120
122
|
<Badge variant={badgeVariant} className="text-[11px] px-2.5 py-0.5 rounded-md">
|
|
121
123
|
{agent.status}
|
|
122
124
|
</Badge>
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
2
|
import type { LucideIcon } from 'lucide-react';
|
|
3
3
|
import {
|
|
4
4
|
Blocks, Code2, TestTube2, Cloud, Database, Layout,
|
|
5
|
-
MessageCircle, Bot, Plus, ArrowRightFromLine,
|
|
5
|
+
MessageCircle, Bot, Plus, ArrowRightFromLine, Settings, Check,
|
|
6
6
|
} from 'lucide-react';
|
|
7
|
+
import { useAtomValue } from 'jotai';
|
|
8
|
+
import { getDefaultStore } from 'jotai';
|
|
7
9
|
import { AGENT_LABELS } from '@/types';
|
|
8
|
-
import type { RoleInfo } from '@/types';
|
|
10
|
+
import type { RoleInfo, RoleTemplate } from '@/types';
|
|
11
|
+
import { activeWorkspaceAtom, workspacesAtom } from '@/atoms';
|
|
12
|
+
import { api } from '@/api';
|
|
13
|
+
import { Dialog } from '@/ui';
|
|
14
|
+
|
|
15
|
+
const store = getDefaultStore();
|
|
9
16
|
|
|
10
17
|
const ROLE_ICON: Record<string, LucideIcon> = {
|
|
11
18
|
arquiteto: Blocks, dev: Code2, qa: TestTube2,
|
|
@@ -13,6 +20,13 @@ const ROLE_ICON: Record<string, LucideIcon> = {
|
|
|
13
20
|
chat: MessageCircle,
|
|
14
21
|
};
|
|
15
22
|
|
|
23
|
+
const MODEL_OPTIONS = [
|
|
24
|
+
{ value: '', label: 'Padrão do sistema' },
|
|
25
|
+
{ value: 'claude-opus-4-5', label: 'Opus 4.5 — mais capaz' },
|
|
26
|
+
{ value: 'claude-sonnet-4-5', label: 'Sonnet 4.5 — equilibrado' },
|
|
27
|
+
{ value: 'claude-haiku-4-5', label: 'Haiku 4.5 — mais rápido' },
|
|
28
|
+
];
|
|
29
|
+
|
|
16
30
|
function label(name: string) {
|
|
17
31
|
return AGENT_LABELS[name] || name.replace(/^agent-/, '').replace(/-/g, ' ');
|
|
18
32
|
}
|
|
@@ -23,10 +37,114 @@ interface RoleItemProps {
|
|
|
23
37
|
onInject: (name: string, type?: 'agent' | 'command') => void;
|
|
24
38
|
}
|
|
25
39
|
|
|
40
|
+
function Toggle({ value, onChange, children }: { value: boolean; onChange: (v: boolean) => void; children: string }) {
|
|
41
|
+
return (
|
|
42
|
+
<label className="flex items-center justify-between cursor-pointer py-0.5">
|
|
43
|
+
<span className="text-sm text-cc-text-secondary">{children}</span>
|
|
44
|
+
<div
|
|
45
|
+
onClick={() => onChange(!value)}
|
|
46
|
+
className={`w-8 h-4.5 rounded-full transition-colors flex items-center px-0.5 cursor-pointer shrink-0 ${value ? 'bg-cc-accent' : 'bg-cc-border'}`}
|
|
47
|
+
style={{ height: '18px', width: '32px' }}
|
|
48
|
+
>
|
|
49
|
+
<div className={`w-3 h-3 rounded-full bg-white transition-transform ${value ? 'translate-x-3.5' : 'translate-x-0'}`} />
|
|
50
|
+
</div>
|
|
51
|
+
</label>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function TemplateDialog({ roleName, wsId, open, onClose }: {
|
|
56
|
+
roleName: string;
|
|
57
|
+
wsId: string;
|
|
58
|
+
open: boolean;
|
|
59
|
+
onClose: () => void;
|
|
60
|
+
}) {
|
|
61
|
+
const [model, setModel] = useState('');
|
|
62
|
+
const [isOrch, setIsOrch] = useState(false);
|
|
63
|
+
const [subagents, setSubagents] = useState(false);
|
|
64
|
+
const [loading, setLoading] = useState(false);
|
|
65
|
+
const [saving, setSaving] = useState(false);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!open) return;
|
|
69
|
+
setLoading(true);
|
|
70
|
+
api.getRoleTemplate(wsId, roleName).then(tpl => {
|
|
71
|
+
setModel(tpl.model || '');
|
|
72
|
+
setIsOrch(tpl.isOrchestrator ?? false);
|
|
73
|
+
setSubagents(tpl.canSpawnSubagents ?? false);
|
|
74
|
+
}).finally(() => setLoading(false));
|
|
75
|
+
}, [open, wsId, roleName]);
|
|
76
|
+
|
|
77
|
+
async function save() {
|
|
78
|
+
setSaving(true);
|
|
79
|
+
try {
|
|
80
|
+
const tpl: RoleTemplate = {
|
|
81
|
+
model: model || undefined,
|
|
82
|
+
isOrchestrator: isOrch,
|
|
83
|
+
canSpawnSubagents: subagents,
|
|
84
|
+
};
|
|
85
|
+
const saved = await api.updateRoleTemplate(wsId, roleName, tpl);
|
|
86
|
+
const ws = store.get(workspacesAtom);
|
|
87
|
+
store.set(workspacesAtom, ws.map(w =>
|
|
88
|
+
w.id === wsId ? { ...w, roleTemplates: { ...(w.roleTemplates || {}), [roleName]: saved } } : w,
|
|
89
|
+
));
|
|
90
|
+
onClose();
|
|
91
|
+
} finally {
|
|
92
|
+
setSaving(false);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Dialog open={open} onClose={onClose} title={`Template — ${label(roleName)}`}>
|
|
98
|
+
{loading ? (
|
|
99
|
+
<div className="py-6 flex justify-center text-sm text-cc-muted">Carregando...</div>
|
|
100
|
+
) : (
|
|
101
|
+
<div className="flex flex-col gap-4">
|
|
102
|
+
<div className="flex flex-col gap-1.5">
|
|
103
|
+
<label className="text-xs font-medium text-cc-text-secondary">Modelo</label>
|
|
104
|
+
<select
|
|
105
|
+
value={model}
|
|
106
|
+
onChange={e => setModel(e.target.value)}
|
|
107
|
+
className="text-sm bg-cc-bg border border-cc-border rounded-lg px-3 py-2 text-cc-text outline-none focus:border-cc-accent/60 cursor-pointer"
|
|
108
|
+
>
|
|
109
|
+
{MODEL_OPTIONS.map(o => (
|
|
110
|
+
<option key={o.value} value={o.value}>{o.label}</option>
|
|
111
|
+
))}
|
|
112
|
+
</select>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div className="flex flex-col gap-2 border-t border-cc-border/40 pt-3">
|
|
116
|
+
<Toggle value={isOrch} onChange={setIsOrch}>Orquestrador — pode delegar tarefas</Toggle>
|
|
117
|
+
<Toggle value={subagents} onChange={setSubagents}>Subagentes — pode usar Agent tool</Toggle>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div className="flex justify-end gap-2 pt-1">
|
|
121
|
+
<button
|
|
122
|
+
onClick={onClose}
|
|
123
|
+
className="px-4 py-1.5 rounded-lg text-sm text-cc-muted hover:text-cc-text-secondary hover:bg-cc-surface transition-colors"
|
|
124
|
+
>
|
|
125
|
+
Cancelar
|
|
126
|
+
</button>
|
|
127
|
+
<button
|
|
128
|
+
onClick={save}
|
|
129
|
+
disabled={saving}
|
|
130
|
+
className="flex items-center gap-1.5 px-4 py-1.5 rounded-lg text-sm bg-cc-accent text-cc-bg font-medium hover:opacity-90 transition-opacity disabled:opacity-50"
|
|
131
|
+
>
|
|
132
|
+
<Check size={13} /> {saving ? 'Salvando...' : 'Salvar'}
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</Dialog>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
26
141
|
export function RoleItem({ role, onSpawn, onInject }: RoleItemProps) {
|
|
27
142
|
const [open, setOpen] = useState(false);
|
|
143
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
144
|
+
const workspace = useAtomValue(activeWorkspaceAtom);
|
|
28
145
|
const Icon = ROLE_ICON[role.name] || Bot;
|
|
29
146
|
const t = role.type as 'agent' | 'command';
|
|
147
|
+
const hasTemplate = workspace?.roleTemplates?.[role.name] !== undefined;
|
|
30
148
|
|
|
31
149
|
return (
|
|
32
150
|
<div>
|
|
@@ -36,6 +154,7 @@ export function RoleItem({ role, onSpawn, onInject }: RoleItemProps) {
|
|
|
36
154
|
>
|
|
37
155
|
<Icon size={12} className="text-cc-muted shrink-0" />
|
|
38
156
|
<span className="flex-1 text-[11px] truncate">{label(role.name)}</span>
|
|
157
|
+
{hasTemplate && <span className="w-1.5 h-1.5 rounded-full bg-cc-accent/60 shrink-0" title="Template configurado" />}
|
|
39
158
|
</div>
|
|
40
159
|
{open && (
|
|
41
160
|
<div className="ml-5 mb-1 flex flex-col gap-0.5">
|
|
@@ -51,8 +170,22 @@ export function RoleItem({ role, onSpawn, onInject }: RoleItemProps) {
|
|
|
51
170
|
>
|
|
52
171
|
<Plus size={10} /> Novo chat
|
|
53
172
|
</button>
|
|
173
|
+
<button
|
|
174
|
+
onClick={() => { setDialogOpen(true); setOpen(false); }}
|
|
175
|
+
className="flex items-center gap-1.5 px-2 py-1 rounded text-[10px] text-cc-muted hover:text-cc-text-secondary hover:bg-cc-surface transition-colors text-left"
|
|
176
|
+
>
|
|
177
|
+
<Settings size={10} /> Configurar template
|
|
178
|
+
</button>
|
|
54
179
|
</div>
|
|
55
180
|
)}
|
|
181
|
+
{workspace && (
|
|
182
|
+
<TemplateDialog
|
|
183
|
+
roleName={role.name}
|
|
184
|
+
wsId={workspace.id}
|
|
185
|
+
open={dialogOpen}
|
|
186
|
+
onClose={() => setDialogOpen(false)}
|
|
187
|
+
/>
|
|
188
|
+
)}
|
|
56
189
|
</div>
|
|
57
190
|
);
|
|
58
191
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export interface RoleTemplate {
|
|
2
|
+
model?: string;
|
|
3
|
+
isOrchestrator?: boolean;
|
|
4
|
+
canSpawnSubagents?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
export interface Workspace {
|
|
2
8
|
id: string;
|
|
3
9
|
name: string;
|
|
@@ -5,6 +11,7 @@ export interface Workspace {
|
|
|
5
11
|
agents: string[];
|
|
6
12
|
color: string;
|
|
7
13
|
orchestratorRole?: string;
|
|
14
|
+
roleTemplates?: Record<string, RoleTemplate>;
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
export interface Agent {
|
package/package.json
CHANGED
package/version.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.3.
|
|
1
|
+
{"version":"0.3.33","build":"2026-04-04T00:53:39.801Z"}
|