@aion0/forge 0.5.20 โ 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/RELEASE_NOTES.md +32 -6
- package/app/api/code/route.ts +10 -4
- 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 +371 -87
- 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/next-env.d.ts +1 -1
- package/package.json +1 -1
- package/qa/.forge/agent-context.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback, useMemo, useRef, forwardRef, useImperativeHandle, lazy, Suspense } from 'react';
|
|
4
|
+
import { TerminalSessionPickerLazy, fetchAgentSessions, type PickerSelection } from './TerminalLauncher';
|
|
4
5
|
import {
|
|
5
6
|
ReactFlow, Background, Controls, Handle, Position, useReactFlow, ReactFlowProvider,
|
|
6
7
|
type Node, type NodeProps, MarkerType, type NodeChange,
|
|
@@ -27,6 +28,7 @@ interface AgentConfig {
|
|
|
27
28
|
skipPermissions?: boolean;
|
|
28
29
|
boundSessionId?: string;
|
|
29
30
|
watch?: { enabled: boolean; interval: number; targets: any[]; action?: 'log' | 'analyze' | 'approve' | 'send_message'; prompt?: string; sendTo?: string };
|
|
31
|
+
plugins?: string[]; // plugin IDs to auto-install when agent is created
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
interface AgentState {
|
|
@@ -53,6 +55,7 @@ const COLORS = [
|
|
|
53
55
|
// Smith status colors
|
|
54
56
|
const SMITH_STATUS: Record<string, { label: string; color: string; glow?: boolean }> = {
|
|
55
57
|
down: { label: 'down', color: '#30363d' },
|
|
58
|
+
starting: { label: 'starting', color: '#f0883e' }, // orange: ensurePersistentSession in progress
|
|
56
59
|
active: { label: 'active', color: '#3fb950', glow: true },
|
|
57
60
|
};
|
|
58
61
|
|
|
@@ -104,6 +107,7 @@ Rules:
|
|
|
104
107
|
},
|
|
105
108
|
{
|
|
106
109
|
label: 'QA', icon: '๐งช', backend: 'cli', agentId: 'claude', dependsOn: [], outputs: ['tests/', 'docs/test-report.md'],
|
|
110
|
+
plugins: ['playwright', 'shell-command'],
|
|
107
111
|
role: `QA Engineer โ You ensure quality through comprehensive testing. You find bugs, you don't fix them.
|
|
108
112
|
|
|
109
113
|
Rules:
|
|
@@ -114,11 +118,28 @@ Rules:
|
|
|
114
118
|
- Run ALL tests (existing + new) and report results
|
|
115
119
|
- Report format: what failed, expected vs actual, steps to reproduce
|
|
116
120
|
- Check for security issues: injection, auth bypass, data leaks
|
|
117
|
-
- Check for performance: N+1 queries, unbounded loops, memory leaks
|
|
121
|
+
- Check for performance: N+1 queries, unbounded loops, memory leaks
|
|
122
|
+
|
|
123
|
+
Test setup (do this automatically if not already present):
|
|
124
|
+
- If no playwright.config.ts exists, create one based on the project's framework and structure
|
|
125
|
+
- If no tests/e2e/ directory exists, create it
|
|
126
|
+
- Detect the app's dev server command and base URL from package.json or project config
|
|
127
|
+
- Ensure the dev server is running before testing (start it if needed)
|
|
128
|
+
|
|
129
|
+
How to run Playwright tests:
|
|
130
|
+
- Run tests: npx playwright test tests/e2e/ --reporter=line
|
|
131
|
+
- Run headed (visible browser): npx playwright test tests/e2e/ --headed --reporter=line
|
|
132
|
+
- Screenshot: npx playwright screenshot http://localhost:3000 /tmp/screenshot.png
|
|
133
|
+
- Check URL: curl -sf -o /dev/null -w '%{http_code}' http://localhost:3000
|
|
134
|
+
- Run project tests: npm test
|
|
135
|
+
|
|
136
|
+
If Forge MCP tools are available (run_plugin, send_message), prefer those.
|
|
137
|
+
Otherwise use bash commands directly โ you have full terminal access.`,
|
|
118
138
|
steps: [
|
|
119
|
-
{ id: '
|
|
120
|
-
{ id: '
|
|
121
|
-
{ id: '
|
|
139
|
+
{ id: 'setup', label: 'Setup', prompt: 'Check the project structure. If playwright.config.ts does not exist, create one with testDir: "./tests/e2e" and baseURL from the project config. Create tests/e2e/ directory if missing. Check if the dev server is running (use check_url), start it if not.' },
|
|
140
|
+
{ id: 'plan', label: 'Test Plan', prompt: 'Read the source code and any PRD/docs. Understand what pages and features exist. Create a test plan in docs/test-plan.md covering: E2E user flows, edge cases, error states, responsive behavior. Map each test to a feature.' },
|
|
141
|
+
{ id: 'write-tests', label: 'Write Tests', prompt: 'Write Playwright test scripts in tests/e2e/. Cover all features from the test plan. Use page.goto(), locators, assertions. Test both happy path and error states. Use descriptive test names.' },
|
|
142
|
+
{ id: 'run-tests', label: 'Run & Report', prompt: 'Run tests via run_plugin with Playwright. If tests fail, read the error output carefully. Fix test scripts if the test itself is wrong, but report to Engineer if the app has a bug. Write docs/test-report.md with results. Send bug reports to Engineer via send_message.' },
|
|
122
143
|
],
|
|
123
144
|
},
|
|
124
145
|
{
|
|
@@ -143,21 +164,69 @@ Rules:
|
|
|
143
164
|
],
|
|
144
165
|
},
|
|
145
166
|
{
|
|
146
|
-
label: 'UI Designer', icon: '๐จ', backend: 'cli', agentId: 'claude', dependsOn: [], outputs: ['docs/ui-spec.md'],
|
|
147
|
-
|
|
167
|
+
label: 'UI Designer', icon: '๐จ', backend: 'cli', agentId: 'claude', dependsOn: [], outputs: ['docs/ui-spec.md', 'src/'],
|
|
168
|
+
plugins: ['playwright', 'shell-command'],
|
|
169
|
+
role: `UI/UX Designer โ You design and implement user interfaces. You write real UI code, preview it visually, and iterate until the quality meets your standards.
|
|
148
170
|
|
|
149
171
|
Rules:
|
|
172
|
+
- You WRITE CODE, not just specs. Implement the UI yourself.
|
|
173
|
+
- After writing UI code, always preview your work: take a screenshot and review it visually.
|
|
174
|
+
- Iterate: if the screenshot doesn't look right, fix the code and screenshot again. Aim for 3-5 review cycles.
|
|
150
175
|
- Focus on user experience first, aesthetics second
|
|
151
176
|
- Design for the existing tech stack (check project's UI framework)
|
|
152
177
|
- Be specific: colors (hex), spacing (px/rem), typography, component hierarchy
|
|
153
178
|
- Consider responsive design, accessibility (WCAG), dark/light mode
|
|
154
179
|
- Include interaction states: hover, active, disabled, loading, error, empty
|
|
155
|
-
-
|
|
156
|
-
|
|
180
|
+
- Reference existing UI patterns in the codebase for consistency
|
|
181
|
+
|
|
182
|
+
Visual review workflow:
|
|
183
|
+
1. Write/modify UI code
|
|
184
|
+
2. Start dev server if not running (e.g., npm run dev)
|
|
185
|
+
3. Take screenshot: run_plugin({ plugin: "<playwright-instance>", action: "screenshot", params: { url: "http://localhost:3000/page" } })
|
|
186
|
+
4. Read the screenshot file to visually evaluate your work
|
|
187
|
+
5. Grade yourself: layout correctness, visual polish, consistency with existing UI, responsiveness
|
|
188
|
+
6. If not satisfied, fix and repeat from step 2
|
|
189
|
+
7. When satisfied, document the final design in docs/ui-spec.md
|
|
190
|
+
|
|
191
|
+
If reference designs or mockups exist in the project (e.g., docs/designs/), study them before implementing.`,
|
|
157
192
|
steps: [
|
|
158
|
-
{ id: 'audit', label: 'UI Audit', prompt: 'Analyze the existing UI: framework used (React/Vue/etc), component library, design tokens (colors, spacing, fonts), layout patterns. Document the current design system.' },
|
|
159
|
-
{ id: '
|
|
160
|
-
{ id: '
|
|
193
|
+
{ id: 'audit', label: 'UI Audit', prompt: 'Analyze the existing UI: framework used (React/Vue/etc), component library, design tokens (colors, spacing, fonts), layout patterns. Take screenshots of existing pages to understand the current look and feel. Document the current design system.' },
|
|
194
|
+
{ id: 'implement', label: 'Implement UI', prompt: 'Based on the PRD, implement the UI. Write real component code. Start the dev server, take screenshots of your work, and iterate until the visual quality is high. Aim for at least 3 review cycles โ screenshot, evaluate, improve.' },
|
|
195
|
+
{ id: 'polish', label: 'Polish & Document', prompt: 'Final polish pass: check all states (loading, empty, error, hover, disabled), responsive breakpoints, dark/light mode. Take final screenshots. Write docs/ui-spec.md documenting: component hierarchy, design decisions, interaction patterns, and accessibility notes.' },
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
label: 'Design Evaluator', icon: '๐', backend: 'cli', agentId: 'claude', dependsOn: [], outputs: ['docs/design-review.md'],
|
|
200
|
+
plugins: ['playwright', 'llm-vision'],
|
|
201
|
+
role: `Design Evaluator โ You are a senior design critic. You evaluate UI implementations visually, not by reading code. You are deliberately skeptical and hold work to a high standard.
|
|
202
|
+
|
|
203
|
+
You evaluate on 4 dimensions (each scored 1-10):
|
|
204
|
+
1. **Design Quality** โ Visual coherence, distinct identity, not generic/template-like
|
|
205
|
+
2. **Originality** โ Evidence of intentional design decisions vs default AI patterns
|
|
206
|
+
3. **Craft** โ Typography, spacing, color harmony, alignment, pixel-level polish
|
|
207
|
+
4. **Functionality** โ Usability, interaction clarity, error states, responsiveness
|
|
208
|
+
|
|
209
|
+
Rules:
|
|
210
|
+
- NEVER modify code โ only evaluate and report
|
|
211
|
+
- Always take screenshots and visually inspect before scoring
|
|
212
|
+
- Use run_plugin with Playwright to screenshot every relevant page/state
|
|
213
|
+
- If llm-vision instances are available, use them for cross-model evaluation
|
|
214
|
+
- Be specific: "the spacing between header and content is 8px, should be 16px for breathing room"
|
|
215
|
+
- A score of 7+ means "good enough to ship". Below 7 means "needs revision"
|
|
216
|
+
- Send feedback to UI Designer via send_message with specific, actionable items
|
|
217
|
+
- If overall score < 7, request changes. If >= 7, approve with minor suggestions.
|
|
218
|
+
|
|
219
|
+
Workflow:
|
|
220
|
+
1. Receive notification that UI Designer has completed work
|
|
221
|
+
2. Take screenshots of all relevant pages and states (normal, loading, error, empty, mobile)
|
|
222
|
+
3. Evaluate each screenshot against the 4 dimensions
|
|
223
|
+
4. Optionally send screenshots to llm-vision instances for additional opinions
|
|
224
|
+
5. Write docs/design-review.md with scores, specific feedback, and verdict
|
|
225
|
+
6. send_message to UI Designer: APPROVE or REQUEST_CHANGES with actionable feedback`,
|
|
226
|
+
steps: [
|
|
227
|
+
{ id: 'screenshot', label: 'Visual Capture', prompt: 'Take screenshots of all pages and states the UI Designer worked on. Include: default view, loading state, error state, empty state, mobile viewport (375px), tablet viewport (768px). Save all screenshots to /tmp/ and list them.' },
|
|
228
|
+
{ id: 'evaluate', label: 'Evaluate', prompt: 'Review each screenshot. Score each page on the 4 dimensions (Design Quality, Originality, Craft, Functionality). Be critical and specific. If llm-vision plugin instances are available, send key screenshots for additional evaluation and compare opinions.' },
|
|
229
|
+
{ id: 'report', label: 'Report & Feedback', prompt: 'Write docs/design-review.md with: overall scores, per-page breakdown, specific issues with suggested fixes. Send verdict to UI Designer via send_message: APPROVE (score >= 7) or REQUEST_CHANGES (score < 7) with the top 3-5 actionable items.' },
|
|
161
230
|
],
|
|
162
231
|
},
|
|
163
232
|
];
|
|
@@ -379,6 +448,109 @@ function SessionTargetSelector({ target, agents, projectPath, onChange }: {
|
|
|
379
448
|
);
|
|
380
449
|
}
|
|
381
450
|
|
|
451
|
+
// โโโ Watch Path Picker (file/directory browser) โโโโโโโโโ
|
|
452
|
+
|
|
453
|
+
function WatchPathPicker({ value, projectPath, onChange }: { value: string; projectPath: string; onChange: (v: string) => void }) {
|
|
454
|
+
const [showBrowser, setShowBrowser] = useState(false);
|
|
455
|
+
const [tree, setTree] = useState<any[]>([]);
|
|
456
|
+
const [search, setSearch] = useState('');
|
|
457
|
+
const [flatFiles, setFlatFiles] = useState<string[]>([]);
|
|
458
|
+
|
|
459
|
+
const loadTree = useCallback(() => {
|
|
460
|
+
if (!projectPath) return;
|
|
461
|
+
fetch(`/api/code?dir=${encodeURIComponent(projectPath)}`)
|
|
462
|
+
.then(r => r.json())
|
|
463
|
+
.then(data => {
|
|
464
|
+
setTree(data.tree || []);
|
|
465
|
+
// Build flat list for search
|
|
466
|
+
const files: string[] = [];
|
|
467
|
+
const walk = (nodes: any[], prefix = '') => {
|
|
468
|
+
for (const n of nodes || []) {
|
|
469
|
+
const path = prefix ? `${prefix}/${n.name}` : n.name;
|
|
470
|
+
files.push(n.type === 'dir' ? path + '/' : path);
|
|
471
|
+
if (n.children) walk(n.children, path);
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
walk(data.tree || []);
|
|
475
|
+
setFlatFiles(files);
|
|
476
|
+
})
|
|
477
|
+
.catch(() => {});
|
|
478
|
+
}, [projectPath]);
|
|
479
|
+
|
|
480
|
+
const filtered = search ? flatFiles.filter(f => f.toLowerCase().includes(search.toLowerCase())).slice(0, 30) : [];
|
|
481
|
+
|
|
482
|
+
return (
|
|
483
|
+
<div className="flex-1 flex items-center gap-1 relative">
|
|
484
|
+
<input
|
|
485
|
+
value={value}
|
|
486
|
+
onChange={e => onChange(e.target.value)}
|
|
487
|
+
placeholder="./ (project root)"
|
|
488
|
+
className="text-[10px] bg-[#161b22] border border-[#30363d] rounded px-1 py-0.5 text-white flex-1"
|
|
489
|
+
/>
|
|
490
|
+
<button onClick={() => { setShowBrowser(!showBrowser); if (!showBrowser) loadTree(); }}
|
|
491
|
+
className="text-[9px] px-1 py-0.5 rounded bg-[#30363d] text-gray-400 hover:text-white shrink-0">๐</button>
|
|
492
|
+
|
|
493
|
+
{showBrowser && (
|
|
494
|
+
<div className="absolute left-0 right-0 top-full mt-1 z-50 bg-[#0d1117] border border-[#30363d] rounded-lg shadow-xl max-h-60 overflow-hidden flex flex-col" style={{ minWidth: 250 }}>
|
|
495
|
+
<input
|
|
496
|
+
value={search}
|
|
497
|
+
onChange={e => setSearch(e.target.value)}
|
|
498
|
+
placeholder="Search files & dirs..."
|
|
499
|
+
autoFocus
|
|
500
|
+
className="text-[10px] bg-[#161b22] border-b border-[#30363d] px-2 py-1 text-white focus:outline-none"
|
|
501
|
+
/>
|
|
502
|
+
<div className="overflow-y-auto flex-1">
|
|
503
|
+
{search ? (
|
|
504
|
+
// Search results
|
|
505
|
+
filtered.length > 0 ? filtered.map(f => (
|
|
506
|
+
<div key={f} onClick={() => { onChange(f); setShowBrowser(false); setSearch(''); }}
|
|
507
|
+
className="px-2 py-0.5 text-[9px] text-gray-300 hover:bg-[#161b22] cursor-pointer truncate font-mono">
|
|
508
|
+
{f.endsWith('/') ? `๐ ${f}` : `๐ ${f}`}
|
|
509
|
+
</div>
|
|
510
|
+
)) : <div className="px-2 py-1 text-[9px] text-gray-500">No matches</div>
|
|
511
|
+
) : (
|
|
512
|
+
// Tree view (first 2 levels)
|
|
513
|
+
tree.map(n => <PathTreeNode key={n.name} node={n} prefix="" onSelect={p => { onChange(p); setShowBrowser(false); }} />)
|
|
514
|
+
)}
|
|
515
|
+
</div>
|
|
516
|
+
<div className="flex items-center justify-between px-2 py-0.5 border-t border-[#30363d] bg-[#161b22]">
|
|
517
|
+
<span className="text-[8px] text-gray-600">{flatFiles.length} items</span>
|
|
518
|
+
<button onClick={() => setShowBrowser(false)} className="text-[8px] text-gray-500 hover:text-white">Close</button>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
)}
|
|
522
|
+
</div>
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function PathTreeNode({ node, prefix, onSelect, depth = 0 }: { node: any; prefix: string; onSelect: (path: string) => void; depth?: number }) {
|
|
527
|
+
const [expanded, setExpanded] = useState(depth < 1);
|
|
528
|
+
const path = prefix ? `${prefix}/${node.name}` : node.name;
|
|
529
|
+
const isDir = node.type === 'dir';
|
|
530
|
+
|
|
531
|
+
if (!isDir && depth > 1) return null; // only show files at top 2 levels
|
|
532
|
+
|
|
533
|
+
return (
|
|
534
|
+
<div>
|
|
535
|
+
<div
|
|
536
|
+
onClick={() => isDir ? setExpanded(!expanded) : onSelect(path)}
|
|
537
|
+
className="flex items-center px-2 py-0.5 text-[9px] hover:bg-[#161b22] cursor-pointer"
|
|
538
|
+
style={{ paddingLeft: 8 + depth * 12 }}
|
|
539
|
+
>
|
|
540
|
+
<span className="text-gray-500 mr-1 w-3">{isDir ? (expanded ? 'โผ' : 'โถ') : ''}</span>
|
|
541
|
+
<span className={isDir ? 'text-[var(--accent)]' : 'text-gray-400'}>{isDir ? '๐' : '๐'} {node.name}</span>
|
|
542
|
+
{isDir && (
|
|
543
|
+
<button onClick={e => { e.stopPropagation(); onSelect(path + '/'); }}
|
|
544
|
+
className="ml-auto text-[8px] text-gray-600 hover:text-[var(--accent)]">select</button>
|
|
545
|
+
)}
|
|
546
|
+
</div>
|
|
547
|
+
{isDir && expanded && node.children && depth < 2 && (
|
|
548
|
+
node.children.map((c: any) => <PathTreeNode key={c.name} node={c} prefix={path} onSelect={onSelect} depth={depth + 1} />)
|
|
549
|
+
)}
|
|
550
|
+
</div>
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
382
554
|
// โโโ Fixed Session Picker โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
383
555
|
|
|
384
556
|
function FixedSessionPicker({ projectPath, value, onChange }: { projectPath?: string; value: string; onChange: (v: string) => void }) {
|
|
@@ -448,6 +620,9 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
448
620
|
const [agentId, setAgentId] = useState(initial.agentId || 'claude');
|
|
449
621
|
const [availableAgents, setAvailableAgents] = useState<{ id: string; name: string; isProfile?: boolean; backendType?: string; base?: string; cliType?: string }[]>([]);
|
|
450
622
|
|
|
623
|
+
const [pluginInstances, setPluginInstances] = useState<{ id: string; name: string; icon: string; source?: string }[]>([]);
|
|
624
|
+
const [pluginDefs, setPluginDefs] = useState<{ id: string; name: string; icon: string }[]>([]);
|
|
625
|
+
|
|
451
626
|
useEffect(() => {
|
|
452
627
|
fetch('/api/agents').then(r => r.json()).then(data => {
|
|
453
628
|
const list = (data.agents || data || []).map((a: any) => ({
|
|
@@ -459,6 +634,19 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
459
634
|
}));
|
|
460
635
|
setAvailableAgents(list);
|
|
461
636
|
}).catch(() => {});
|
|
637
|
+
// Fetch both: plugin definitions + installed instances
|
|
638
|
+
Promise.all([
|
|
639
|
+
fetch('/api/plugins').then(r => r.json()),
|
|
640
|
+
fetch('/api/plugins?installed=true').then(r => r.json()),
|
|
641
|
+
]).then(([defData, instData]) => {
|
|
642
|
+
setPluginDefs((defData.plugins || []).map((p: any) => ({ id: p.id, name: p.name, icon: p.icon })));
|
|
643
|
+
setPluginInstances((instData.plugins || []).map((p: any) => ({
|
|
644
|
+
id: p.id,
|
|
645
|
+
name: p.instanceName || p.definition?.name || p.id,
|
|
646
|
+
icon: p.definition?.icon || '๐',
|
|
647
|
+
source: p.source,
|
|
648
|
+
})));
|
|
649
|
+
}).catch(() => {});
|
|
462
650
|
}, []);
|
|
463
651
|
const [workDirVal, setWorkDirVal] = useState(initial.workDir || '');
|
|
464
652
|
const [outputs, setOutputs] = useState((initial.outputs || []).join(', '));
|
|
@@ -471,11 +659,14 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
471
659
|
const hasPrimaryAlready = existingAgents.some(a => a.primary && a.id !== initial.id);
|
|
472
660
|
const [persistentSession, setPersistentSession] = useState(initial.persistentSession || initial.primary || false);
|
|
473
661
|
const [skipPermissions, setSkipPermissions] = useState(initial.skipPermissions !== false);
|
|
662
|
+
const [agentModel, setAgentModel] = useState(initial.model || '');
|
|
474
663
|
const [watchEnabled, setWatchEnabled] = useState(initial.watch?.enabled || false);
|
|
475
664
|
const [watchInterval, setWatchInterval] = useState(String(initial.watch?.interval || 60));
|
|
476
665
|
const [watchAction, setWatchAction] = useState<'log' | 'analyze' | 'approve' | 'send_message'>(initial.watch?.action || 'log');
|
|
477
666
|
const [watchPrompt, setWatchPrompt] = useState(initial.watch?.prompt || '');
|
|
478
667
|
const [watchSendTo, setWatchSendTo] = useState(initial.watch?.sendTo || '');
|
|
668
|
+
const [selectedPlugins, setSelectedPlugins] = useState<string[]>(initial.plugins || []);
|
|
669
|
+
const [recommendedTypes, setRecommendedTypes] = useState<string[]>([]);
|
|
479
670
|
const [watchDebounce, setWatchDebounce] = useState(String(initial.watch?.targets?.[0]?.debounce ?? 10));
|
|
480
671
|
const [watchTargets, setWatchTargets] = useState<{ type: string; path?: string; cmd?: string; pattern?: string }[]>(
|
|
481
672
|
initial.watch?.targets || []
|
|
@@ -487,14 +678,14 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
487
678
|
fetch(`/api/code?dir=${encodeURIComponent(projectPath)}`)
|
|
488
679
|
.then(r => r.json())
|
|
489
680
|
.then(data => {
|
|
490
|
-
//
|
|
681
|
+
// Collect directories with depth limit (max 2 levels for readability)
|
|
491
682
|
const dirs: string[] = [];
|
|
492
|
-
const walk = (nodes: any[], prefix = '') => {
|
|
683
|
+
const walk = (nodes: any[], prefix = '', depth = 0) => {
|
|
493
684
|
for (const n of nodes || []) {
|
|
494
685
|
if (n.type === 'dir') {
|
|
495
686
|
const path = prefix ? `${prefix}/${n.name}` : n.name;
|
|
496
687
|
dirs.push(path);
|
|
497
|
-
if (n.children) walk(n.children, path);
|
|
688
|
+
if (n.children && depth < 2) walk(n.children, path, depth + 1);
|
|
498
689
|
}
|
|
499
690
|
}
|
|
500
691
|
};
|
|
@@ -510,6 +701,8 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
510
701
|
setWorkDirVal(p.workDir || './');
|
|
511
702
|
setOutputs(p.outputs.join(', '));
|
|
512
703
|
setStepsText(p.steps.map(s => `${s.label}: ${s.prompt}`).join('\n'));
|
|
704
|
+
setRecommendedTypes(p.plugins || []);
|
|
705
|
+
setSelectedPlugins([]);
|
|
513
706
|
};
|
|
514
707
|
|
|
515
708
|
const toggleDep = (id: string) => {
|
|
@@ -619,8 +812,74 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
619
812
|
{/* Role */}
|
|
620
813
|
<div className="flex flex-col gap-1">
|
|
621
814
|
<label className="text-[9px] text-gray-500 uppercase">Role / System Prompt</label>
|
|
622
|
-
<textarea value={role} onChange={e => setRole(e.target.value)} rows={
|
|
623
|
-
|
|
815
|
+
<textarea value={role} onChange={e => setRole(e.target.value)} rows={5}
|
|
816
|
+
placeholder="Describe this agent's role, responsibilities, available tools, and decision criteria. This will be synced to CLAUDE.md in the agent's working directory."
|
|
817
|
+
className="text-xs bg-[#161b22] border border-[#30363d] rounded px-2 py-1 text-white focus:outline-none focus:border-[#58a6ff] resize-y" />
|
|
818
|
+
</div>
|
|
819
|
+
|
|
820
|
+
{/* Plugin Instances grouped by plugin */}
|
|
821
|
+
<div className="flex flex-col gap-1">
|
|
822
|
+
<label className="text-[9px] text-gray-500 uppercase">Plugin Instances</label>
|
|
823
|
+
{(() => {
|
|
824
|
+
const withSource = pluginInstances.filter(i => i.source);
|
|
825
|
+
if (withSource.length === 0) return <span className="text-[8px] text-gray-600">No instances โ create in Marketplace โ Plugins</span>;
|
|
826
|
+
// Group by source plugin
|
|
827
|
+
const groups: Record<string, typeof withSource> = {};
|
|
828
|
+
for (const inst of withSource) {
|
|
829
|
+
const key = inst.source!;
|
|
830
|
+
if (!groups[key]) groups[key] = [];
|
|
831
|
+
groups[key].push(inst);
|
|
832
|
+
}
|
|
833
|
+
// Show recommended types that have no instances yet
|
|
834
|
+
const missingRecommended = recommendedTypes.filter(rt =>
|
|
835
|
+
!withSource.some(i => i.source === rt)
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
return <>
|
|
839
|
+
{Object.entries(groups).map(([sourceId, insts]) => {
|
|
840
|
+
const def = pluginDefs.find(d => d.id === sourceId);
|
|
841
|
+
const isRecommended = recommendedTypes.includes(sourceId);
|
|
842
|
+
return (
|
|
843
|
+
<div key={sourceId} className="flex items-start gap-2">
|
|
844
|
+
<span className={`text-[9px] shrink-0 w-20 pt-1 truncate ${isRecommended ? 'text-[#58a6ff]' : 'text-gray-500'}`} title={def?.name || sourceId}>
|
|
845
|
+
{def?.icon || '๐'} {def?.name || sourceId}
|
|
846
|
+
{isRecommended && <span className="text-[7px] ml-0.5">โ
</span>}
|
|
847
|
+
</span>
|
|
848
|
+
<div className="flex flex-wrap gap-1 flex-1">
|
|
849
|
+
{insts.map(inst => {
|
|
850
|
+
const selected = selectedPlugins.includes(inst.id);
|
|
851
|
+
return (
|
|
852
|
+
<button key={inst.id}
|
|
853
|
+
onClick={() => setSelectedPlugins(prev => selected ? prev.filter(x => x !== inst.id) : [...prev, inst.id])}
|
|
854
|
+
className={`text-[9px] px-2 py-0.5 rounded border transition-colors ${
|
|
855
|
+
selected
|
|
856
|
+
? 'border-green-500/40 text-green-400 bg-green-500/10'
|
|
857
|
+
: isRecommended
|
|
858
|
+
? 'border-[#58a6ff]/30 text-[#58a6ff]/70 hover:text-[#58a6ff]'
|
|
859
|
+
: 'border-[#30363d] text-gray-500 hover:text-gray-300'
|
|
860
|
+
}`}>
|
|
861
|
+
{inst.name}
|
|
862
|
+
</button>
|
|
863
|
+
);
|
|
864
|
+
})}
|
|
865
|
+
</div>
|
|
866
|
+
</div>
|
|
867
|
+
);
|
|
868
|
+
})}
|
|
869
|
+
{missingRecommended.length > 0 && missingRecommended.map(rt => {
|
|
870
|
+
const def = pluginDefs.find(d => d.id === rt);
|
|
871
|
+
return (
|
|
872
|
+
<div key={rt} className="flex items-start gap-2">
|
|
873
|
+
<span className="text-[9px] text-[#58a6ff] shrink-0 w-20 pt-1 truncate">
|
|
874
|
+
{def?.icon || '๐'} {def?.name || rt}<span className="text-[7px] ml-0.5">โ
</span>
|
|
875
|
+
</span>
|
|
876
|
+
<span className="text-[8px] text-[#58a6ff]/50 italic pt-1">No instances โ create in Marketplace โ Plugins</span>
|
|
877
|
+
</div>
|
|
878
|
+
);
|
|
879
|
+
})}
|
|
880
|
+
</>;
|
|
881
|
+
|
|
882
|
+
})()}
|
|
624
883
|
</div>
|
|
625
884
|
|
|
626
885
|
{/* Depends On โ checkbox list of existing agents */}
|
|
@@ -687,7 +946,7 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
687
946
|
{(() => {
|
|
688
947
|
// Check if selected agent supports terminal mode (claude-code or its profiles)
|
|
689
948
|
const selectedAgent = availableAgents.find(a => a.id === agentId);
|
|
690
|
-
const isClaude =
|
|
949
|
+
const isClaude = selectedAgent?.cliType === 'claude-code' || selectedAgent?.base === 'claude' || !selectedAgent;
|
|
691
950
|
const canTerminal = isClaude || isPrimary;
|
|
692
951
|
return canTerminal ? (
|
|
693
952
|
<>
|
|
@@ -716,6 +975,27 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
716
975
|
);
|
|
717
976
|
})()}
|
|
718
977
|
|
|
978
|
+
{/* Model override โ only for claude-code agents */}
|
|
979
|
+
{(() => {
|
|
980
|
+
const sa = availableAgents.find(a => a.id === agentId);
|
|
981
|
+
const ct = sa?.cliType || (agentId === 'claude' ? 'claude-code' : '');
|
|
982
|
+
if (ct !== 'claude-code') return null;
|
|
983
|
+
return (
|
|
984
|
+
<div className="flex flex-col gap-0.5">
|
|
985
|
+
<label className="text-[9px] text-gray-500 uppercase">Model</label>
|
|
986
|
+
<input value={agentModel} onChange={e => setAgentModel(e.target.value)}
|
|
987
|
+
placeholder="default (uses profile or system default)"
|
|
988
|
+
list="workspace-model-list"
|
|
989
|
+
className="text-xs bg-[#161b22] border border-[#30363d] rounded px-2 py-1 text-white focus:outline-none focus:border-[#58a6ff] font-mono" />
|
|
990
|
+
<datalist id="workspace-model-list">
|
|
991
|
+
<option value="claude-sonnet-4-6" />
|
|
992
|
+
<option value="claude-opus-4-6" />
|
|
993
|
+
<option value="claude-haiku-4-5-20251001" />
|
|
994
|
+
</datalist>
|
|
995
|
+
</div>
|
|
996
|
+
);
|
|
997
|
+
})()}
|
|
998
|
+
|
|
719
999
|
{/* Steps */}
|
|
720
1000
|
<div className="flex flex-col gap-1">
|
|
721
1001
|
<label className="text-[9px] text-gray-500 uppercase">Steps (one per line โ Label: Prompt)</label>
|
|
@@ -785,14 +1065,15 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
785
1065
|
<option value="agent_status">Agent Status</option>
|
|
786
1066
|
</select>
|
|
787
1067
|
{t.type === 'directory' && (
|
|
788
|
-
<
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1068
|
+
<WatchPathPicker
|
|
1069
|
+
value={t.path || ''}
|
|
1070
|
+
projectPath={projectPath || ''}
|
|
1071
|
+
onChange={v => {
|
|
1072
|
+
const next = [...watchTargets];
|
|
1073
|
+
next[i] = { ...t, path: v };
|
|
1074
|
+
setWatchTargets(next);
|
|
1075
|
+
}}
|
|
1076
|
+
/>
|
|
796
1077
|
)}
|
|
797
1078
|
{t.type === 'agent_status' && (<>
|
|
798
1079
|
<select value={t.path || ''} onChange={e => {
|
|
@@ -905,8 +1186,15 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
905
1186
|
steps: parseSteps(),
|
|
906
1187
|
primary: isPrimary || undefined,
|
|
907
1188
|
requiresApproval: requiresApproval || undefined,
|
|
908
|
-
persistentSession:
|
|
1189
|
+
persistentSession: (() => {
|
|
1190
|
+
if (isPrimary) return true;
|
|
1191
|
+
// Non-terminal agents (codex, aider, etc.) force headless
|
|
1192
|
+
const sa = availableAgents.find(a => a.id === agentId);
|
|
1193
|
+
const isClaude = sa?.cliType === 'claude-code' || sa?.base === 'claude' || !sa;
|
|
1194
|
+
return (isClaude || isPrimary) ? (persistentSession || undefined) : false;
|
|
1195
|
+
})(),
|
|
909
1196
|
skipPermissions: persistentSession ? (skipPermissions ? undefined : false) : undefined,
|
|
1197
|
+
model: agentModel || undefined,
|
|
910
1198
|
watch: watchEnabled && watchTargets.length > 0 ? {
|
|
911
1199
|
enabled: true,
|
|
912
1200
|
interval: Math.max(10, parseInt(watchInterval) || 60),
|
|
@@ -915,6 +1203,7 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
915
1203
|
prompt: watchPrompt || undefined,
|
|
916
1204
|
sendTo: watchSendTo || undefined,
|
|
917
1205
|
} : undefined,
|
|
1206
|
+
plugins: selectedPlugins.length > 0 ? selectedPlugins : undefined,
|
|
918
1207
|
} as any);
|
|
919
1208
|
}} className="text-xs px-3 py-1.5 rounded bg-[#238636] text-white hover:bg-[#2ea043] disabled:opacity-40">
|
|
920
1209
|
{mode === 'add' ? 'Add' : 'Save'}
|
|
@@ -2344,9 +2633,11 @@ function AgentFlowNode({ data }: NodeProps<Node<AgentNodeData>>) {
|
|
|
2344
2633
|
)}
|
|
2345
2634
|
<div className="flex-1" />
|
|
2346
2635
|
<span className="flex items-center">
|
|
2347
|
-
<button onPointerDown={e => e.stopPropagation()}
|
|
2348
|
-
|
|
2349
|
-
|
|
2636
|
+
<button onPointerDown={e => e.stopPropagation()}
|
|
2637
|
+
onClick={e => { e.stopPropagation(); if (smithStatus === 'active') onOpenTerminal(); }}
|
|
2638
|
+
disabled={smithStatus !== 'active'}
|
|
2639
|
+
className={`text-[9px] px-1 ${smithStatus !== 'active' ? 'text-gray-700 cursor-not-allowed' : hasTmux && taskStatus === 'running' ? 'text-green-400 animate-pulse' : 'text-gray-600 hover:text-green-400'}`}
|
|
2640
|
+
title={smithStatus === 'starting' ? 'Starting sessionโฆ' : smithStatus === 'down' ? 'Smith not started' : 'Open terminal'}>โจ๏ธ</button>
|
|
2350
2641
|
{hasTmux && !config.primary && (
|
|
2351
2642
|
<button onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); onSwitchSession(); }}
|
|
2352
2643
|
className="text-[10px] text-gray-600 hover:text-yellow-400 px-0.5 py-0.5" title="Switch session">โพ</button>
|
|
@@ -2392,7 +2683,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2392
2683
|
const [inboxTarget, setInboxTarget] = useState<{ id: string; label: string } | null>(null);
|
|
2393
2684
|
const [showBusPanel, setShowBusPanel] = useState(false);
|
|
2394
2685
|
const [floatingTerminals, setFloatingTerminals] = useState<{ agentId: string; label: string; icon: string; cliId: string; cliCmd?: string; cliType?: string; workDir?: string; tmuxSession?: string; sessionName: string; resumeMode?: boolean; resumeSessionId?: string; profileEnv?: Record<string, string>; isPrimary?: boolean; skipPermissions?: boolean; persistentSession?: boolean; boundSessionId?: string; initialPos?: { x: number; y: number } }[]>([]);
|
|
2395
|
-
const [
|
|
2686
|
+
const [termPicker, setTermPicker] = useState<{ agent: AgentConfig; sessName: string; workDir?: string; supportsSession?: boolean; currentSessionId: string | null; initialPos?: { x: number; y: number } } | null>(null);
|
|
2396
2687
|
|
|
2397
2688
|
// Expose focusAgent to parent
|
|
2398
2689
|
useImperativeHandle(ref, () => ({
|
|
@@ -2502,48 +2793,16 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2502
2793
|
// Close existing terminal (config may have changed)
|
|
2503
2794
|
setFloatingTerminals(prev => prev.filter(t => t.agentId !== agent.id));
|
|
2504
2795
|
|
|
2505
|
-
// Get node screen position for initial terminal placement
|
|
2506
2796
|
const nodeEl = document.querySelector(`[data-id="${agent.id}"]`);
|
|
2507
2797
|
const nodeRect = nodeEl?.getBoundingClientRect();
|
|
2508
|
-
const initialPos = nodeRect
|
|
2509
|
-
? { x: nodeRect.left, y: nodeRect.bottom + 4 }
|
|
2510
|
-
: { x: 80, y: 60 };
|
|
2511
|
-
|
|
2512
|
-
const agentState = states[agent.id];
|
|
2513
|
-
const existingTmux = agentState?.tmuxSession;
|
|
2798
|
+
const initialPos = nodeRect ? { x: nodeRect.left, y: nodeRect.bottom + 4 } : { x: 80, y: 60 };
|
|
2514
2799
|
const safeName = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').slice(0, 20);
|
|
2515
2800
|
const sessName = `mw-forge-${safeName(projectName)}-${safeName(agent.label)}`;
|
|
2516
2801
|
const workDir = agent.workDir && agent.workDir !== './' && agent.workDir !== '.' ? agent.workDir : undefined;
|
|
2517
|
-
|
|
2518
|
-
// Always resolve launch info for this agent (cliCmd, env, model)
|
|
2802
|
+
// All agents: show picker (current session / new session / other sessions)
|
|
2519
2803
|
const resolveRes = await wsApi(workspaceId, 'open_terminal', { agentId: agent.id, resolveOnly: true }).catch(() => ({})) as any;
|
|
2520
|
-
const
|
|
2521
|
-
|
|
2522
|
-
cliType: resolveRes?.cliType || 'claude-code',
|
|
2523
|
-
profileEnv: {
|
|
2524
|
-
...(resolveRes?.env || {}),
|
|
2525
|
-
...(resolveRes?.model ? { CLAUDE_MODEL: resolveRes.model } : {}),
|
|
2526
|
-
FORGE_AGENT_ID: agent.id,
|
|
2527
|
-
FORGE_WORKSPACE_ID: workspaceId!,
|
|
2528
|
-
FORGE_PORT: String(window.location.port || 8403),
|
|
2529
|
-
},
|
|
2530
|
-
};
|
|
2531
|
-
|
|
2532
|
-
// All paths: let daemon create/ensure session, then attach
|
|
2533
|
-
if (existingTmux || agent.primary || agent.persistentSession || agent.boundSessionId) {
|
|
2534
|
-
// Daemon creates session via ensurePersistentSession (launch script, no truncation)
|
|
2535
|
-
const res = await wsApi(workspaceId, 'open_terminal', { agentId: agent.id }).catch(() => ({})) as any;
|
|
2536
|
-
const tmux = existingTmux || res?.tmuxSession || sessName;
|
|
2537
|
-
setFloatingTerminals(prev => [...prev, {
|
|
2538
|
-
agentId: agent.id, label: agent.label, icon: agent.icon,
|
|
2539
|
-
cliId: agent.agentId || 'claude', workDir,
|
|
2540
|
-
tmuxSession: tmux, sessionName: sessName,
|
|
2541
|
-
isPrimary: agent.primary, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
|
|
2542
|
-
}]);
|
|
2543
|
-
return;
|
|
2544
|
-
}
|
|
2545
|
-
// No persistent session, no bound session โ show launch dialog
|
|
2546
|
-
setTermLaunchDialog({ agent, sessName, workDir, sessions: [], supportsSession: resolveRes?.supportsSession ?? true, initialPos });
|
|
2804
|
+
const currentSessionId = resolveRes?.currentSessionId ?? null;
|
|
2805
|
+
setTermPicker({ agent, sessName, workDir, supportsSession: resolveRes?.supportsSession ?? true, currentSessionId, initialPos });
|
|
2547
2806
|
},
|
|
2548
2807
|
onSwitchSession: async () => {
|
|
2549
2808
|
if (!workspaceId) return;
|
|
@@ -2551,12 +2810,13 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2551
2810
|
if (agent.id) wsApi(workspaceId, 'close_terminal', { agentId: agent.id });
|
|
2552
2811
|
const nodeEl = document.querySelector(`[data-id="${agent.id}"]`);
|
|
2553
2812
|
const nodeRect = nodeEl?.getBoundingClientRect();
|
|
2554
|
-
const
|
|
2813
|
+
const initialPos = nodeRect ? { x: nodeRect.left, y: nodeRect.bottom + 4 } : { x: 80, y: 60 };
|
|
2555
2814
|
const safeName = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').slice(0, 20);
|
|
2556
2815
|
const sessName = `mw-forge-${safeName(projectName)}-${safeName(agent.label)}`;
|
|
2557
2816
|
const workDir = agent.workDir && agent.workDir !== './' && agent.workDir !== '.' ? agent.workDir : undefined;
|
|
2558
2817
|
const resolveRes = await wsApi(workspaceId, 'open_terminal', { agentId: agent.id, resolveOnly: true }).catch(() => ({})) as any;
|
|
2559
|
-
|
|
2818
|
+
const currentSessionId = resolveRes?.currentSessionId ?? null;
|
|
2819
|
+
setTermPicker({ agent, sessName, workDir, supportsSession: resolveRes?.supportsSession ?? true, currentSessionId, initialPos });
|
|
2560
2820
|
},
|
|
2561
2821
|
} satisfies AgentNodeData,
|
|
2562
2822
|
};
|
|
@@ -2610,6 +2870,17 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2610
2870
|
const handleAddAgent = async (cfg: Omit<AgentConfig, 'id'>) => {
|
|
2611
2871
|
if (!workspaceId) return;
|
|
2612
2872
|
const config: AgentConfig = { ...cfg, id: `${cfg.label.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}` };
|
|
2873
|
+
// Auto-install base plugins if not already installed (for preset templates)
|
|
2874
|
+
// User-selected instances are already installed, so this is a no-op for them
|
|
2875
|
+
if (cfg.plugins?.length) {
|
|
2876
|
+
await Promise.all(cfg.plugins.map(pluginId =>
|
|
2877
|
+
fetch('/api/plugins', {
|
|
2878
|
+
method: 'POST',
|
|
2879
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2880
|
+
body: JSON.stringify({ action: 'install', id: pluginId, config: {} }),
|
|
2881
|
+
}).catch(() => {})
|
|
2882
|
+
));
|
|
2883
|
+
}
|
|
2613
2884
|
// Optimistic update โ show immediately
|
|
2614
2885
|
setModal(null);
|
|
2615
2886
|
await wsApi(workspaceId, 'add', { config });
|
|
@@ -2911,33 +3182,46 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2911
3182
|
/>
|
|
2912
3183
|
)}
|
|
2913
3184
|
|
|
2914
|
-
{/* Terminal
|
|
2915
|
-
{
|
|
2916
|
-
<
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
if (
|
|
2928
|
-
|
|
3185
|
+
{/* Terminal session picker */}
|
|
3186
|
+
{termPicker && workspaceId && (
|
|
3187
|
+
<TerminalSessionPickerLazy
|
|
3188
|
+
agentLabel={termPicker.agent.label}
|
|
3189
|
+
currentSessionId={termPicker.currentSessionId}
|
|
3190
|
+
fetchSessions={() => fetchAgentSessions(workspaceId, termPicker.agent.id)}
|
|
3191
|
+
supportsSession={termPicker.supportsSession}
|
|
3192
|
+
onSelect={async (selection: PickerSelection) => {
|
|
3193
|
+
const { agent, sessName, workDir } = termPicker;
|
|
3194
|
+
const pickerInitialPos = termPicker.initialPos;
|
|
3195
|
+
setTermPicker(null);
|
|
3196
|
+
|
|
3197
|
+
let boundSessionId = agent.boundSessionId;
|
|
3198
|
+
if (selection.mode === 'session') {
|
|
3199
|
+
// Bind to a specific session
|
|
3200
|
+
await wsApi(workspaceId, 'update', { agentId: agent.id, config: { ...agent, boundSessionId: selection.sessionId } }).catch(() => {});
|
|
3201
|
+
boundSessionId = selection.sessionId;
|
|
3202
|
+
} else if (selection.mode === 'new') {
|
|
3203
|
+
// Clear bound session โ fresh start
|
|
3204
|
+
if (agent.boundSessionId) {
|
|
3205
|
+
await wsApi(workspaceId, 'update', { agentId: agent.id, config: { ...agent, boundSessionId: undefined } }).catch(() => {});
|
|
3206
|
+
}
|
|
3207
|
+
boundSessionId = undefined;
|
|
2929
3208
|
}
|
|
2930
|
-
//
|
|
2931
|
-
|
|
3209
|
+
// mode === 'current': keep existing boundSessionId
|
|
3210
|
+
|
|
3211
|
+
// 'current': just attach โ claude is running, don't interrupt.
|
|
3212
|
+
// 'session' or 'new': forceRestart โ rebuild launch script with correct --resume.
|
|
3213
|
+
const forceRestart = selection.mode !== 'current';
|
|
3214
|
+
const res = await wsApi(workspaceId, 'open_terminal', { agentId: agent.id, forceRestart }).catch(() => ({})) as any;
|
|
2932
3215
|
const tmux = res?.tmuxSession || sessName;
|
|
2933
3216
|
setFloatingTerminals(prev => [...prev, {
|
|
2934
3217
|
agentId: agent.id, label: agent.label, icon: agent.icon,
|
|
2935
3218
|
cliId: agent.agentId || 'claude', workDir,
|
|
2936
3219
|
tmuxSession: tmux, sessionName: sessName,
|
|
2937
|
-
isPrimary:
|
|
3220
|
+
isPrimary: agent.primary, skipPermissions: agent.skipPermissions !== false,
|
|
3221
|
+
persistentSession: agent.persistentSession, boundSessionId, initialPos: pickerInitialPos,
|
|
2938
3222
|
}]);
|
|
2939
3223
|
}}
|
|
2940
|
-
onCancel={() =>
|
|
3224
|
+
onCancel={() => setTermPicker(null)}
|
|
2941
3225
|
/>
|
|
2942
3226
|
)}
|
|
2943
3227
|
|