@aion0/forge 0.6.1 → 0.8.0
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/mcp.json +8 -0
- package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-316c6574/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-316c6574/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-44a94121/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-44a94121/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/07-projects.md +1 -1
- package/CLAUDE.md +2 -2
- package/RELEASE_NOTES.md +101 -5
- package/app/api/auth/check/route.ts +18 -0
- package/app/api/browser-bridge/route.ts +70 -0
- package/app/api/chat/sessions/[id]/events/route.ts +17 -0
- package/app/api/chat/sessions/[id]/fork/route.ts +15 -0
- package/app/api/chat/sessions/[id]/messages/route.ts +21 -0
- package/app/api/chat/sessions/[id]/route.ts +23 -0
- package/app/api/chat/sessions/route.ts +12 -0
- package/app/api/chat/temper-ping/route.ts +18 -0
- package/app/api/chat-proxy/[...path]/route.ts +83 -0
- package/app/api/connector-tool/route.ts +38 -0
- package/app/api/connectors/[id]/settings/route.ts +112 -0
- package/app/api/connectors/route.ts +108 -0
- package/app/api/health/tools/route.ts +14 -0
- package/app/api/issue-scanner-gitlab/route.ts +95 -0
- package/app/api/jobs/[id]/reset_dedup/route.ts +15 -0
- package/app/api/jobs/[id]/route.ts +31 -0
- package/app/api/jobs/[id]/run/route.ts +44 -0
- package/app/api/jobs/[id]/runs/[runId]/route.ts +15 -0
- package/app/api/jobs/[id]/runs/route.ts +15 -0
- package/app/api/jobs/preview/route.ts +193 -0
- package/app/api/jobs/route.ts +36 -0
- package/app/api/notify/test/route.ts +39 -7
- package/app/api/pipelines/[id]/route.ts +10 -1
- package/app/api/pipelines/route.ts +16 -2
- package/app/api/plugins/route.ts +40 -8
- package/app/api/project-sessions/route.ts +50 -10
- package/app/api/settings/route.ts +13 -0
- package/app/chat/page.tsx +531 -0
- package/bin/forge-server.mjs +3 -1
- package/cli/chat.ts +283 -0
- package/cli/jobs.ts +176 -0
- package/cli/mw.ts +28 -1
- package/cli/worktree.ts +245 -0
- package/components/ConnectorsPanel.tsx +275 -0
- package/components/Dashboard.tsx +90 -37
- package/components/JobsView.tsx +361 -0
- package/components/LogViewer.tsx +12 -2
- package/components/PipelineView.tsx +275 -56
- package/components/PluginsPanel.tsx +3 -1
- package/components/SettingsModal.tsx +229 -40
- package/components/SkillsPanel.tsx +12 -4
- package/components/TerminalLauncher.tsx +3 -1
- package/components/WebTerminal.tsx +32 -9
- package/components/WorkspaceView.tsx +18 -10
- package/docs/Connector-DeclarativeExtract-Handoff.md +471 -0
- package/docs/Connector-DeclarativeExtract-Spec.md +364 -0
- package/docs/Implementation-Plan-Browser-Agent.md +487 -0
- package/docs/Jobs-Design.md +240 -0
- package/docs/LOCAL-DEPLOY.md +3 -3
- package/docs/RFC-Browser-Connectors.md +509 -0
- package/lib/agents/index.ts +44 -6
- package/lib/agents/types.ts +1 -1
- package/lib/browser-bridge-standalone.ts +317 -0
- package/lib/builtin-plugins/github-api.yaml +93 -0
- package/lib/builtin-plugins/gitlab.yaml +860 -0
- package/lib/builtin-plugins/mantis.probe.js +176 -0
- package/lib/builtin-plugins/mantis.yaml +964 -0
- package/lib/builtin-plugins/pmdb.yaml +178 -0
- package/lib/builtin-plugins/teams.yaml +913 -0
- package/lib/chat/__test__/smoke.ts +30 -0
- package/lib/chat/agent-loop.ts +523 -0
- package/lib/chat/bridge-client.ts +59 -0
- package/lib/chat/llm/anthropic.ts +99 -0
- package/lib/chat/llm/index.ts +20 -0
- package/lib/chat/llm/openai.ts +215 -0
- package/lib/chat/llm/types.ts +42 -0
- package/lib/chat/local-memory.ts +300 -0
- package/lib/chat/memory-store.ts +87 -0
- package/lib/chat/memory-tools.ts +157 -0
- package/lib/chat/protocols/http.ts +118 -0
- package/lib/chat/protocols/shell.ts +101 -0
- package/lib/chat/proxy.ts +51 -0
- package/lib/chat/session-store.ts +272 -0
- package/lib/chat/telegram-bridge.ts +276 -0
- package/lib/chat/temper.ts +281 -0
- package/lib/chat/tool-dispatcher.ts +190 -0
- package/lib/chat/types.ts +50 -0
- package/lib/chat-standalone.ts +286 -0
- package/lib/crypto.ts +1 -1
- package/lib/health.ts +131 -0
- package/lib/help-docs/00-overview.md +2 -1
- package/lib/help-docs/01-settings.md +46 -25
- package/lib/help-docs/07-projects.md +1 -1
- package/lib/help-docs/10-troubleshooting.md +10 -2
- package/lib/help-docs/16-gitlab-autofix.md +114 -0
- package/lib/help-docs/17-connectors.md +322 -0
- package/lib/help-docs/18-chrome-mcp.md +134 -0
- package/lib/help-docs/19-jobs.md +140 -0
- package/lib/help-docs/20-mantis-bug-fix.md +115 -0
- package/lib/help-docs/CLAUDE.md +10 -0
- package/lib/init.ts +137 -50
- package/lib/iso-time.ts +30 -0
- package/lib/issue-scanner-gitlab.ts +281 -0
- package/lib/jobs/dispatcher.ts +217 -0
- package/lib/jobs/scheduler.ts +334 -0
- package/lib/jobs/store.ts +319 -0
- package/lib/jobs/types.ts +117 -0
- package/lib/pipeline-scheduler.ts +1 -6
- package/lib/pipeline.ts +790 -10
- package/lib/plugins/registry.ts +133 -8
- package/lib/plugins/templates.ts +83 -0
- package/lib/plugins/types.ts +140 -1
- package/lib/session-watcher.ts +36 -10
- package/lib/settings.ts +65 -33
- package/lib/skills.ts +3 -1
- package/lib/task-manager.ts +50 -22
- package/lib/telegram-bot.ts +71 -0
- package/lib/terminal-standalone.ts +58 -36
- package/lib/workspace/orchestrator.ts +1 -0
- package/middleware.ts +10 -0
- package/package.json +3 -2
- package/scripts/bench/README.md +1 -1
- package/scripts/bench/tasks/01-text-utils/validator.sh +1 -1
- package/scripts/bench/tasks/02-pagination/setup.sh +1 -1
- package/scripts/bench/tasks/02-pagination/validator.sh +1 -1
- package/scripts/bench/tasks/03-bug-fix/setup.sh +1 -1
- package/scripts/bench/tasks/03-bug-fix/validator.sh +1 -1
- package/src/core/db/database.ts +21 -12
|
@@ -426,6 +426,9 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
426
426
|
{/* Agents */}
|
|
427
427
|
<AgentsSection settings={settings} setSettings={setSettings} />
|
|
428
428
|
|
|
429
|
+
{/* Temper memory (used by chat backend) */}
|
|
430
|
+
<TemperSection settings={settings} setSettings={setSettings} secretStatus={secretStatus} setEditingSecret={setEditingSecret} />
|
|
431
|
+
|
|
429
432
|
{/* Telegram Notifications */}
|
|
430
433
|
<div className="space-y-2">
|
|
431
434
|
<label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
|
|
@@ -850,21 +853,33 @@ function ProfileRow({ id, cfg, inputClass, onUpdate, onDelete }: {
|
|
|
850
853
|
</div>
|
|
851
854
|
</div>
|
|
852
855
|
{isApi ? (
|
|
853
|
-
|
|
854
|
-
<div className="flex-
|
|
855
|
-
<
|
|
856
|
-
|
|
857
|
-
<
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
856
|
+
<>
|
|
857
|
+
<div className="flex gap-2">
|
|
858
|
+
<div className="flex-1">
|
|
859
|
+
<label className="text-[8px] text-[var(--text-secondary)]">Provider</label>
|
|
860
|
+
<select value={cfg.provider || 'anthropic'} onChange={e => onUpdate({ ...cfg, provider: e.target.value })} className={inputClass}>
|
|
861
|
+
<option value="anthropic">Anthropic</option>
|
|
862
|
+
<option value="openai">OpenAI</option>
|
|
863
|
+
<option value="litellm">LiteLLM (OpenAI-compatible proxy)</option>
|
|
864
|
+
<option value="grok">Grok</option>
|
|
865
|
+
<option value="google">Google</option>
|
|
866
|
+
</select>
|
|
867
|
+
</div>
|
|
868
|
+
<div className="flex-1">
|
|
869
|
+
<label className="text-[8px] text-[var(--text-secondary)]">API Key</label>
|
|
870
|
+
<input type="password" value={cfg.apiKey || ''} onChange={e => onUpdate({ ...cfg, apiKey: e.target.value })} className={inputClass} />
|
|
871
|
+
</div>
|
|
862
872
|
</div>
|
|
863
|
-
<div
|
|
864
|
-
<label className="text-[8px] text-[var(--text-secondary)]">
|
|
865
|
-
<input
|
|
873
|
+
<div>
|
|
874
|
+
<label className="text-[8px] text-[var(--text-secondary)]">Base URL (optional · LiteLLM / Azure / self-hosted proxy)</label>
|
|
875
|
+
<input
|
|
876
|
+
value={cfg.baseUrl || ''}
|
|
877
|
+
onChange={e => onUpdate({ ...cfg, baseUrl: e.target.value })}
|
|
878
|
+
placeholder={cfg.provider === 'anthropic' ? 'https://api.anthropic.com' : 'http://127.0.0.1:4000/v1'}
|
|
879
|
+
className={inputClass + ' font-mono'}
|
|
880
|
+
/>
|
|
866
881
|
</div>
|
|
867
|
-
|
|
882
|
+
</>
|
|
868
883
|
) : (
|
|
869
884
|
<>
|
|
870
885
|
<div>
|
|
@@ -936,6 +951,7 @@ function AddProfileForm({ type, baseAgents, onAdd }: {
|
|
|
936
951
|
const [base, setBase] = useState(baseAgents[0]?.id || 'claude');
|
|
937
952
|
const [model, setModel] = useState('');
|
|
938
953
|
const [provider, setProvider] = useState('anthropic');
|
|
954
|
+
const [baseUrl, setBaseUrl] = useState('');
|
|
939
955
|
const [envText, setEnvText] = useState('');
|
|
940
956
|
const [apiKey, setApiKey] = useState('');
|
|
941
957
|
|
|
@@ -996,10 +1012,10 @@ function AddProfileForm({ type, baseAgents, onAdd }: {
|
|
|
996
1012
|
if (type === 'cli') {
|
|
997
1013
|
onAdd(id, { cliType: base === 'claude' ? 'claude-code' : base, name: name || id, model: model || undefined, env: parseEnv() });
|
|
998
1014
|
} else {
|
|
999
|
-
onAdd(id, { type: 'api', name: name || id, provider, model: model || undefined, apiKey: apiKey || undefined });
|
|
1015
|
+
onAdd(id, { type: 'api', name: name || id, provider, model: model || undefined, apiKey: apiKey || undefined, baseUrl: baseUrl || undefined });
|
|
1000
1016
|
}
|
|
1001
1017
|
setOpen(false);
|
|
1002
|
-
setId(''); setName(''); setModel(''); setApiKey(''); setEnvText('');
|
|
1018
|
+
setId(''); setName(''); setModel(''); setApiKey(''); setEnvText(''); setBaseUrl('');
|
|
1003
1019
|
};
|
|
1004
1020
|
|
|
1005
1021
|
return (
|
|
@@ -1068,9 +1084,10 @@ function AddProfileForm({ type, baseAgents, onAdd }: {
|
|
|
1068
1084
|
<label className="text-[8px] text-[var(--text-secondary)]">Provider</label>
|
|
1069
1085
|
<select value={provider} onChange={e => setProvider(e.target.value)} className={inputClass}>
|
|
1070
1086
|
<option value="anthropic">Anthropic</option>
|
|
1071
|
-
<option value="google">Google</option>
|
|
1072
1087
|
<option value="openai">OpenAI</option>
|
|
1088
|
+
<option value="litellm">LiteLLM (OpenAI-compatible proxy)</option>
|
|
1073
1089
|
<option value="grok">Grok</option>
|
|
1090
|
+
<option value="google">Google</option>
|
|
1074
1091
|
</select>
|
|
1075
1092
|
</div>
|
|
1076
1093
|
<div className="flex-1">
|
|
@@ -1079,9 +1096,18 @@ function AddProfileForm({ type, baseAgents, onAdd }: {
|
|
|
1079
1096
|
</div>
|
|
1080
1097
|
</div>
|
|
1081
1098
|
<div>
|
|
1082
|
-
<label className="text-[8px] text-[var(--text-secondary)]">API Key
|
|
1099
|
+
<label className="text-[8px] text-[var(--text-secondary)]">API Key</label>
|
|
1083
1100
|
<input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} placeholder="sk-..." className={inputClass} />
|
|
1084
1101
|
</div>
|
|
1102
|
+
<div>
|
|
1103
|
+
<label className="text-[8px] text-[var(--text-secondary)]">Base URL (optional · LiteLLM / Azure / self-hosted)</label>
|
|
1104
|
+
<input
|
|
1105
|
+
value={baseUrl}
|
|
1106
|
+
onChange={e => setBaseUrl(e.target.value)}
|
|
1107
|
+
placeholder={provider === 'anthropic' ? 'https://api.anthropic.com' : 'http://127.0.0.1:4000/v1'}
|
|
1108
|
+
className={inputClass + ' font-mono'}
|
|
1109
|
+
/>
|
|
1110
|
+
</div>
|
|
1085
1111
|
</>
|
|
1086
1112
|
)}
|
|
1087
1113
|
<div className="flex gap-2">
|
|
@@ -1099,7 +1125,7 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1099
1125
|
const [showAdd, setShowAdd] = useState(false);
|
|
1100
1126
|
const cliDefaults: Record<string, any> = {
|
|
1101
1127
|
'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: '--
|
|
1128
|
+
'codex': { taskFlags: '', resumeFlag: '', outputFormat: 'text', skipPermissionsFlag: '--dangerously-bypass-approvals-and-sandbox' },
|
|
1103
1129
|
'aider': { taskFlags: '--message', resumeFlag: '', outputFormat: 'text', skipPermissionsFlag: '--yes' },
|
|
1104
1130
|
'generic': { taskFlags: '', resumeFlag: '', outputFormat: 'text', skipPermissionsFlag: '' },
|
|
1105
1131
|
};
|
|
@@ -1422,7 +1448,7 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1422
1448
|
<div className="flex gap-1 mt-1">
|
|
1423
1449
|
{[
|
|
1424
1450
|
{ label: 'Claude', flag: '--dangerously-skip-permissions' },
|
|
1425
|
-
{ label: 'Codex', flag: '--
|
|
1451
|
+
{ label: 'Codex', flag: '--dangerously-bypass-approvals-and-sandbox' },
|
|
1426
1452
|
{ label: 'Aider', flag: '--yes' },
|
|
1427
1453
|
].map(p => (
|
|
1428
1454
|
<button key={p.label} onClick={() => updateAgent(a.id, 'skipPermissionsFlag', p.flag)}
|
|
@@ -1556,33 +1582,196 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1556
1582
|
</div>
|
|
1557
1583
|
</div>
|
|
1558
1584
|
|
|
1559
|
-
{/* ──
|
|
1560
|
-
<
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1585
|
+
{/* ── Chat default agent ── */}
|
|
1586
|
+
<ChatAgentSelect settings={settings} setSettings={setSettings} />
|
|
1587
|
+
</div>
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// ─── Temper memory section ────────────────────────────────
|
|
1592
|
+
//
|
|
1593
|
+
// When configured, Forge's chat backend pre-fetches pinned blocks + all
|
|
1594
|
+
// blocks + a semantic search for each user turn, inlines them into the
|
|
1595
|
+
// system prompt, and exposes memory_* tools to the LLM. Leave both URL
|
|
1596
|
+
// and key empty to skip memory I/O entirely.
|
|
1597
|
+
function TemperSection({ settings, setSettings, secretStatus, setEditingSecret }: {
|
|
1598
|
+
settings: any;
|
|
1599
|
+
setSettings: (s: any) => void;
|
|
1600
|
+
secretStatus: Record<string, boolean>;
|
|
1601
|
+
setEditingSecret: (e: { field: string; label: string } | null) => void;
|
|
1602
|
+
}) {
|
|
1603
|
+
const [testing, setTesting] = useState(false);
|
|
1604
|
+
const [testResult, setTestResult] = useState<string>('');
|
|
1605
|
+
const [status, setStatus] = useState<{ backend: 'temper' | 'local'; ok: boolean; pinned: number } | null>(null);
|
|
1606
|
+
|
|
1607
|
+
// Probe the active backend on mount so the badge is accurate before
|
|
1608
|
+
// the user clicks Test. Cheap — local just counts a row in sqlite,
|
|
1609
|
+
// Temper does one GET.
|
|
1610
|
+
useEffect(() => {
|
|
1611
|
+
let aborted = false;
|
|
1612
|
+
fetch('/api/chat/temper-ping', { method: 'POST' })
|
|
1613
|
+
.then(r => r.json())
|
|
1614
|
+
.then(j => {
|
|
1615
|
+
if (aborted) return;
|
|
1616
|
+
if (j && (j.backend === 'temper' || j.backend === 'local')) {
|
|
1617
|
+
setStatus({ backend: j.backend, ok: !!j.ok, pinned: j.pinned ?? 0 });
|
|
1618
|
+
}
|
|
1619
|
+
})
|
|
1620
|
+
.catch(() => { /* ignore — badge stays "?" */ });
|
|
1621
|
+
return () => { aborted = true; };
|
|
1622
|
+
}, []);
|
|
1623
|
+
|
|
1624
|
+
async function test() {
|
|
1625
|
+
setTesting(true);
|
|
1626
|
+
setTestResult('');
|
|
1627
|
+
try {
|
|
1628
|
+
// Save first so server-side test sees current values
|
|
1629
|
+
await fetch('/api/settings', {
|
|
1630
|
+
method: 'PUT',
|
|
1631
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1632
|
+
body: JSON.stringify(settings),
|
|
1633
|
+
});
|
|
1634
|
+
const r = await fetch('/api/chat/temper-ping', { method: 'POST' });
|
|
1635
|
+
const j = await r.json();
|
|
1636
|
+
setTestResult(j.ok ? `✓ ${j.message}` : `✗ ${j.message}`);
|
|
1637
|
+
if (j && (j.backend === 'temper' || j.backend === 'local')) {
|
|
1638
|
+
setStatus({ backend: j.backend, ok: !!j.ok, pinned: j.pinned ?? 0 });
|
|
1639
|
+
}
|
|
1640
|
+
} catch (e) {
|
|
1641
|
+
setTestResult('✗ ' + (e instanceof Error ? e.message : String(e)));
|
|
1642
|
+
} finally { setTesting(false); }
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
const isLocal = status?.backend === 'local';
|
|
1646
|
+
const badgeColor = !status
|
|
1647
|
+
? 'border-[var(--border)] text-[var(--text-secondary)]'
|
|
1648
|
+
: status.ok && status.backend === 'temper'
|
|
1649
|
+
? 'border-green-500 text-green-400'
|
|
1650
|
+
: status.ok
|
|
1651
|
+
? 'border-[var(--accent)] text-[var(--accent)]'
|
|
1652
|
+
: 'border-red-500 text-red-400';
|
|
1653
|
+
|
|
1654
|
+
return (
|
|
1655
|
+
<div className="space-y-2">
|
|
1656
|
+
<div className="flex items-center gap-2">
|
|
1657
|
+
<label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
|
|
1658
|
+
Memory
|
|
1659
|
+
</label>
|
|
1660
|
+
<span className={`text-[9px] uppercase tracking-wide px-1.5 py-[1px] rounded border ${badgeColor}`}>
|
|
1661
|
+
{!status ? 'checking…' : status.backend === 'temper' ? `Temper · ${status.pinned} pinned` : `Local · ${status.pinned} pinned`}
|
|
1662
|
+
</span>
|
|
1663
|
+
</div>
|
|
1664
|
+
<p className="text-[10px] text-[var(--text-secondary)]">
|
|
1665
|
+
Long-term memory backend for the chat agent. Pinned blocks auto-inject into every system prompt; the LLM gets memory_* tools to read/write on demand.
|
|
1666
|
+
</p>
|
|
1667
|
+
|
|
1668
|
+
{/* Backend selector — explicit override, or auto-pick based on credentials */}
|
|
1669
|
+
<div className="flex items-center gap-3 pt-1">
|
|
1670
|
+
<span className="text-[10px] text-[var(--text-secondary)] uppercase">Backend</span>
|
|
1671
|
+
{(['auto', 'local', 'temper'] as const).map((mode) => {
|
|
1672
|
+
const active = (settings.memoryBackend || 'auto') === mode;
|
|
1566
1673
|
return (
|
|
1567
|
-
<
|
|
1568
|
-
|
|
1674
|
+
<label
|
|
1675
|
+
key={mode}
|
|
1676
|
+
className={`flex items-center gap-1 text-[10px] cursor-pointer px-2 py-0.5 rounded border transition-colors ${
|
|
1677
|
+
active
|
|
1678
|
+
? 'border-[var(--accent)] text-[var(--accent)] bg-[var(--accent)]/5'
|
|
1679
|
+
: 'border-[var(--border)] text-[var(--text-secondary)] hover:border-[var(--accent)]/40'
|
|
1680
|
+
}`}
|
|
1681
|
+
>
|
|
1569
1682
|
<input
|
|
1570
|
-
type="
|
|
1571
|
-
|
|
1572
|
-
value={
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
})}
|
|
1577
|
-
className="flex-1 text-[9px] px-2 py-0.5 bg-[var(--bg-secondary)] border border-[var(--border)] rounded text-[var(--text-primary)] font-mono focus:outline-none focus:border-[var(--accent)]"
|
|
1683
|
+
type="radio"
|
|
1684
|
+
name="memoryBackend"
|
|
1685
|
+
value={mode}
|
|
1686
|
+
checked={active}
|
|
1687
|
+
onChange={() => setSettings({ ...settings, memoryBackend: mode })}
|
|
1688
|
+
className="sr-only"
|
|
1578
1689
|
/>
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
</span>
|
|
1582
|
-
</div>
|
|
1690
|
+
{mode === 'auto' ? 'Auto' : mode === 'local' ? 'Local (SQLite)' : 'Temper'}
|
|
1691
|
+
</label>
|
|
1583
1692
|
);
|
|
1584
1693
|
})}
|
|
1585
1694
|
</div>
|
|
1695
|
+
<p className="text-[10px] text-[var(--text-secondary)] leading-snug">
|
|
1696
|
+
<b>Auto</b>: Temper if URL+key are set, otherwise local.
|
|
1697
|
+
<b>Local</b>: always SQLite in <code>workflow.db</code> (keyword search).
|
|
1698
|
+
<b>Temper</b>: always Temper for semantic + graph search via Graphiti.
|
|
1699
|
+
{isLocal && (
|
|
1700
|
+
<>
|
|
1701
|
+
<br />
|
|
1702
|
+
<span className="italic">Local mode active — <code>memory_search</code> is keyword LIKE; <code>memory_remember_event</code> appends rows without graph extraction.</span>
|
|
1703
|
+
</>
|
|
1704
|
+
)}
|
|
1705
|
+
</p>
|
|
1706
|
+
<input
|
|
1707
|
+
value={settings.temperUrl || ''}
|
|
1708
|
+
onChange={e => setSettings({ ...settings, temperUrl: e.target.value })}
|
|
1709
|
+
placeholder="http://127.0.0.1:18088"
|
|
1710
|
+
className="w-full px-2 py-1.5 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] font-mono focus:outline-none focus:border-[var(--accent)]"
|
|
1711
|
+
/>
|
|
1712
|
+
<SecretField
|
|
1713
|
+
label="API Key"
|
|
1714
|
+
description="X-API-Key header value"
|
|
1715
|
+
isSet={!!secretStatus.temperKey || !!settings.temperKey}
|
|
1716
|
+
onEdit={() => setEditingSecret({ field: 'temperKey', label: 'Temper API Key' })}
|
|
1717
|
+
/>
|
|
1718
|
+
<input
|
|
1719
|
+
value={settings.temperNamespace || ''}
|
|
1720
|
+
onChange={e => setSettings({ ...settings, temperNamespace: e.target.value })}
|
|
1721
|
+
placeholder="Namespace override (optional · empty = use the key's default)"
|
|
1722
|
+
className="w-full px-2 py-1.5 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] font-mono focus:outline-none focus:border-[var(--accent)]"
|
|
1723
|
+
/>
|
|
1724
|
+
<div className="flex items-center gap-3">
|
|
1725
|
+
<button
|
|
1726
|
+
type="button"
|
|
1727
|
+
onClick={test}
|
|
1728
|
+
disabled={testing}
|
|
1729
|
+
className="text-[10px] px-2 py-0.5 border border-[var(--accent)] text-[var(--accent)] rounded hover:bg-[var(--accent)] hover:text-white transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
1730
|
+
>
|
|
1731
|
+
{testing ? 'Testing…' : 'Test'}
|
|
1732
|
+
</button>
|
|
1733
|
+
{testResult && (
|
|
1734
|
+
<span className={`text-[10px] ${testResult.startsWith('✓') ? 'text-green-400' : 'text-red-400'}`}>{testResult}</span>
|
|
1735
|
+
)}
|
|
1736
|
+
</div>
|
|
1737
|
+
</div>
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// ─── Chat default agent selector ──────────────────────────
|
|
1742
|
+
//
|
|
1743
|
+
// Forge's chat backend (used by extension, web /chat tab, forge chat CLI,
|
|
1744
|
+
// Telegram /chat) picks the API profile listed here. The user can override
|
|
1745
|
+
// per-session by setting Session.provider to a profile id.
|
|
1746
|
+
function ChatAgentSelect({ settings, setSettings }: { settings: any; setSettings: (s: any) => void }) {
|
|
1747
|
+
const apiProfiles = Object.entries(settings.agents || {})
|
|
1748
|
+
.filter(([, a]: [string, any]) => a && a.type === 'api' && a.enabled !== false);
|
|
1749
|
+
|
|
1750
|
+
return (
|
|
1751
|
+
<div className="mt-4 pt-3 border-t border-[var(--border)]">
|
|
1752
|
+
<label className="text-xs text-[var(--text-secondary)] font-semibold uppercase mb-2 block">Chat backend</label>
|
|
1753
|
+
<div className="flex items-center gap-2">
|
|
1754
|
+
<span className="text-[9px] text-[var(--text-secondary)]">Default chat agent:</span>
|
|
1755
|
+
<select
|
|
1756
|
+
value={settings.chatAgent || ''}
|
|
1757
|
+
onChange={e => setSettings({ ...settings, chatAgent: e.target.value })}
|
|
1758
|
+
className="bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-0.5 text-[10px] text-[var(--text-primary)]"
|
|
1759
|
+
>
|
|
1760
|
+
<option value="">(first available API profile)</option>
|
|
1761
|
+
{apiProfiles.map(([id, a]: [string, any]) => (
|
|
1762
|
+
<option key={id} value={id}>{a.name || id} — {a.provider}/{a.model || '?'}</option>
|
|
1763
|
+
))}
|
|
1764
|
+
</select>
|
|
1765
|
+
</div>
|
|
1766
|
+
{apiProfiles.length === 0 ? (
|
|
1767
|
+
<div className="text-[9px] text-[var(--text-secondary)] mt-1.5">
|
|
1768
|
+
No API profile yet. Add one in the Profiles section above (type: API). LiteLLM proxies are supported via the Base URL field.
|
|
1769
|
+
</div>
|
|
1770
|
+
) : (
|
|
1771
|
+
<div className="text-[9px] text-[var(--text-secondary)] mt-1.5">
|
|
1772
|
+
Used by the extension chat, <code className="font-mono">forge chat</code>, and Telegram <code className="font-mono">/chat</code>. Per-session override is possible via the Session.provider field.
|
|
1773
|
+
</div>
|
|
1774
|
+
)}
|
|
1586
1775
|
</div>
|
|
1587
1776
|
);
|
|
1588
1777
|
}
|
|
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, lazy, Suspense } from 'react';
|
|
|
4
4
|
import { useSidebarResize } from '@/hooks/useSidebarResize';
|
|
5
5
|
|
|
6
6
|
const PluginsPanel = lazy(() => import('./PluginsPanel'));
|
|
7
|
+
const ConnectorsPanel = lazy(() => import('./ConnectorsPanel'));
|
|
7
8
|
const CraftsMarketplacePanelLazy = lazy(() => import('./CraftsMarketplacePanel'));
|
|
8
9
|
|
|
9
10
|
type ItemType = 'skill' | 'command';
|
|
@@ -140,7 +141,7 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
|
|
|
140
141
|
const [syncing, setSyncing] = useState(false);
|
|
141
142
|
const [loading, setLoading] = useState(true);
|
|
142
143
|
const [installTarget, setInstallTarget] = useState<{ skill: string; show: boolean }>({ skill: '', show: false });
|
|
143
|
-
const [typeFilter, setTypeFilter] = useState<'all' | 'skill' | 'command' | 'local' | 'rules' | 'plugins' | 'crafts'>('all');
|
|
144
|
+
const [typeFilter, setTypeFilter] = useState<'all' | 'skill' | 'command' | 'local' | 'rules' | 'plugins' | 'connectors' | 'crafts'>('all');
|
|
144
145
|
const [localItems, setLocalItems] = useState<{ name: string; type: string; scope: string; fileCount: number; projectPath?: string }[]>([]);
|
|
145
146
|
// Rules (CLAUDE.md templates)
|
|
146
147
|
const [rulesTemplates, setRulesTemplates] = useState<{ id: string; name: string; description: string; tags: string[]; builtin: boolean; isDefault: boolean; content: string }[]>([]);
|
|
@@ -373,7 +374,7 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
|
|
|
373
374
|
<div className="flex items-center gap-2">
|
|
374
375
|
<span className="text-xs font-semibold text-[var(--text-primary)]">Marketplace</span>
|
|
375
376
|
<div className="flex items-center bg-[var(--bg-tertiary)] rounded p-0.5">
|
|
376
|
-
{([['all', `All (${skills.length})`], ['skill', `Skills (${skillCount})`], ['command', `Commands (${commandCount})`], ['local', `Local (${localCount})`], ['rules', 'Rules'], ['plugins', 'Plugins'], ['crafts', 'Crafts']] as const).map(([value, label]) => (
|
|
377
|
+
{([['all', `All (${skills.length})`], ['skill', `Skills (${skillCount})`], ['command', `Commands (${commandCount})`], ['local', `Local (${localCount})`], ['rules', 'Rules'], ['plugins', 'Plugins'], ['connectors', 'Connectors'], ['crafts', 'Crafts']] as const).map(([value, label]) => (
|
|
377
378
|
<button
|
|
378
379
|
key={value}
|
|
379
380
|
onClick={() => setTypeFilter(value)}
|
|
@@ -398,7 +399,7 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
|
|
|
398
399
|
</button>
|
|
399
400
|
</div>
|
|
400
401
|
{/* Search — hide on rules tab */}
|
|
401
|
-
{typeFilter !== 'rules' && typeFilter !== 'plugins' && typeFilter !== 'crafts' && <div className="px-3 py-1.5 border-b border-[var(--border)] shrink-0">
|
|
402
|
+
{typeFilter !== 'rules' && typeFilter !== 'plugins' && typeFilter !== 'connectors' && typeFilter !== 'crafts' && <div className="px-3 py-1.5 border-b border-[var(--border)] shrink-0">
|
|
402
403
|
<input
|
|
403
404
|
type="text"
|
|
404
405
|
value={searchQuery}
|
|
@@ -408,7 +409,7 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
|
|
|
408
409
|
/>
|
|
409
410
|
</div>}
|
|
410
411
|
|
|
411
|
-
{typeFilter === 'rules' || typeFilter === 'plugins' || typeFilter === 'crafts' ? null : skills.length === 0 ? (
|
|
412
|
+
{typeFilter === 'rules' || typeFilter === 'plugins' || typeFilter === 'connectors' || typeFilter === 'crafts' ? null : skills.length === 0 ? (
|
|
412
413
|
<div className="flex-1 flex flex-col items-center justify-center gap-2 text-[var(--text-secondary)]">
|
|
413
414
|
<p className="text-xs">No skills yet</p>
|
|
414
415
|
<button onClick={sync} className="text-xs px-3 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90">
|
|
@@ -986,6 +987,13 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
|
|
|
986
987
|
</Suspense>
|
|
987
988
|
)}
|
|
988
989
|
|
|
990
|
+
{/* Connectors — full-page view */}
|
|
991
|
+
{typeFilter === 'connectors' && (
|
|
992
|
+
<Suspense fallback={<div className="p-4 text-xs text-[var(--text-secondary)]">Loading...</div>}>
|
|
993
|
+
<ConnectorsPanel />
|
|
994
|
+
</Suspense>
|
|
995
|
+
)}
|
|
996
|
+
|
|
989
997
|
{/* Crafts — registry browse view */}
|
|
990
998
|
{typeFilter === 'crafts' && (
|
|
991
999
|
<Suspense fallback={<div className="p-4 text-xs text-[var(--text-secondary)]">Loading...</div>}>
|
|
@@ -308,6 +308,7 @@ export interface WorkspaceTerminalInfo {
|
|
|
308
308
|
tmuxSession: string | null;
|
|
309
309
|
cliCmd?: string;
|
|
310
310
|
cliType?: string;
|
|
311
|
+
skipPermissionsFlag?: string;
|
|
311
312
|
supportsSession?: boolean;
|
|
312
313
|
}
|
|
313
314
|
|
|
@@ -332,6 +333,7 @@ export async function resolveWorkspaceTerminal(
|
|
|
332
333
|
tmuxSession: data.tmuxSession || null,
|
|
333
334
|
cliCmd: data.cliCmd,
|
|
334
335
|
cliType: data.cliType,
|
|
336
|
+
skipPermissionsFlag: data.skipPermissionsFlag,
|
|
335
337
|
supportsSession: data.supportsSession ?? true,
|
|
336
338
|
};
|
|
337
339
|
} catch {
|
|
@@ -346,7 +348,7 @@ export async function resolveWorkspaceTerminal(
|
|
|
346
348
|
export async function resolveWorkspaceAgentInfo(
|
|
347
349
|
workspaceId: string,
|
|
348
350
|
agentId: string,
|
|
349
|
-
): Promise<{ cliCmd?: string; cliType?: string; env?: Record<string, string>; model?: string; supportsSession?: boolean }> {
|
|
351
|
+
): Promise<{ cliCmd?: string; cliType?: string; skipPermissionsFlag?: string; env?: Record<string, string>; model?: string; supportsSession?: boolean }> {
|
|
350
352
|
try {
|
|
351
353
|
const res = await fetch(`/api/workspace/${workspaceId}/smith`, {
|
|
352
354
|
method: 'POST',
|
|
@@ -186,13 +186,20 @@ function MouseToggle() {
|
|
|
186
186
|
setMouseOn(next);
|
|
187
187
|
};
|
|
188
188
|
|
|
189
|
+
// Hover-only hint shown on the ON/OFF button. The inline always-visible
|
|
190
|
+
// text used to crowd the toolbar; tooltip keeps the toolbar clean and the
|
|
191
|
+
// hint still one hover away.
|
|
192
|
+
const hint = mouseOn
|
|
193
|
+
? 'mouse ON — scroll: trackpad · copy: Shift+drag\nclick to disable (easier text select)'
|
|
194
|
+
: 'mouse OFF — scroll: Ctrl+B [ · copy: drag\nclick to enable (trackpad scroll)';
|
|
195
|
+
|
|
189
196
|
return (
|
|
190
197
|
<div className="flex items-center gap-1 mr-2">
|
|
191
|
-
<
|
|
192
|
-
{
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
<button
|
|
199
|
+
onClick={toggle}
|
|
200
|
+
title={hint}
|
|
201
|
+
className={`text-[9px] px-1.5 py-0.5 rounded border transition-colors ${mouseOn ? 'border-green-600/40 text-green-400 bg-green-500/10' : 'border-gray-600 text-gray-500'}`}
|
|
202
|
+
>
|
|
196
203
|
🖱️ {mouseOn ? 'ON' : 'OFF'}
|
|
197
204
|
</button>
|
|
198
205
|
</div>
|
|
@@ -771,11 +778,27 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
771
778
|
{/* Toolbar */}
|
|
772
779
|
<div className="flex items-center gap-1 px-2 ml-auto">
|
|
773
780
|
<MouseToggle />
|
|
774
|
-
<button
|
|
775
|
-
|
|
781
|
+
<button
|
|
782
|
+
onClick={() => onSplit('vertical')}
|
|
783
|
+
className="px-1.5 py-0.5 text-gray-400 hover:text-white hover:bg-[var(--term-border)] rounded"
|
|
784
|
+
title="Split right (vertical pane)"
|
|
785
|
+
aria-label="Split right"
|
|
786
|
+
>
|
|
787
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.2">
|
|
788
|
+
<rect x="1" y="1" width="5" height="12" rx="0.5" />
|
|
789
|
+
<rect x="8" y="1" width="5" height="12" rx="0.5" />
|
|
790
|
+
</svg>
|
|
776
791
|
</button>
|
|
777
|
-
<button
|
|
778
|
-
|
|
792
|
+
<button
|
|
793
|
+
onClick={() => onSplit('horizontal')}
|
|
794
|
+
className="px-1.5 py-0.5 text-gray-400 hover:text-white hover:bg-[var(--term-border)] rounded"
|
|
795
|
+
title="Split down (horizontal pane)"
|
|
796
|
+
aria-label="Split down"
|
|
797
|
+
>
|
|
798
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.2">
|
|
799
|
+
<rect x="1" y="1" width="12" height="5" rx="0.5" />
|
|
800
|
+
<rect x="1" y="8" width="12" height="5" rx="0.5" />
|
|
801
|
+
</svg>
|
|
779
802
|
</button>
|
|
780
803
|
<button
|
|
781
804
|
onClick={() => { refreshSessions(); setShowSessionPicker(v => !v); }}
|
|
@@ -2119,7 +2119,7 @@ function fireSmithBell(label: string, status: 'done' | 'failed') {
|
|
|
2119
2119
|
}
|
|
2120
2120
|
|
|
2121
2121
|
// ─── Terminal Dock (right side panel with tabs) ──────────
|
|
2122
|
-
type TerminalEntry = { 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
|
|
2122
|
+
type TerminalEntry = { agentId: string; label: string; icon: string; cliId: string; cliCmd?: string; cliType?: string; skipPermissionsFlag?: string; workDir?: string; tmuxSession?: string; sessionName: string; resumeMode?: boolean; resumeSessionId?: string; profileEnv?: Record<string, string>; skipPermissions?: boolean };
|
|
2123
2123
|
|
|
2124
2124
|
function TerminalDock({ terminals, projectPath, workspaceId, onSessionReady, onClose }: {
|
|
2125
2125
|
terminals: TerminalEntry[];
|
|
@@ -2192,12 +2192,14 @@ function TerminalDock({ terminals, projectPath, workspaceId, onSessionReady, onC
|
|
|
2192
2192
|
agentCliId={active.cliId}
|
|
2193
2193
|
cliCmd={active.cliCmd}
|
|
2194
2194
|
cliType={active.cliType}
|
|
2195
|
+
skipPermissionsFlag={active.skipPermissionsFlag}
|
|
2195
2196
|
workDir={active.workDir}
|
|
2196
2197
|
preferredSessionName={active.sessionName}
|
|
2197
2198
|
existingSession={active.tmuxSession}
|
|
2198
2199
|
resumeMode={active.resumeMode}
|
|
2199
2200
|
resumeSessionId={active.resumeSessionId}
|
|
2200
2201
|
profileEnv={active.profileEnv}
|
|
2202
|
+
skipPermissions={active.skipPermissions}
|
|
2201
2203
|
onSessionReady={(name) => onSessionReady(active.agentId, name)}
|
|
2202
2204
|
/>
|
|
2203
2205
|
</div>
|
|
@@ -2208,13 +2210,14 @@ function TerminalDock({ terminals, projectPath, workspaceId, onSessionReady, onC
|
|
|
2208
2210
|
}
|
|
2209
2211
|
|
|
2210
2212
|
// ─── Inline Terminal (no drag/resize, fills parent) ──────
|
|
2211
|
-
function FloatingTerminalInline({ agentLabel, agentIcon, projectPath, agentCliId, cliCmd: cliCmdProp, cliType, workDir, preferredSessionName, existingSession, resumeMode, resumeSessionId, profileEnv, isPrimary, skipPermissions, boundSessionId, onSessionReady }: {
|
|
2213
|
+
function FloatingTerminalInline({ agentLabel, agentIcon, projectPath, agentCliId, cliCmd: cliCmdProp, cliType, skipPermissionsFlag, workDir, preferredSessionName, existingSession, resumeMode, resumeSessionId, profileEnv, isPrimary, skipPermissions, boundSessionId, onSessionReady }: {
|
|
2212
2214
|
agentLabel: string;
|
|
2213
2215
|
agentIcon: string;
|
|
2214
2216
|
projectPath: string;
|
|
2215
2217
|
agentCliId: string;
|
|
2216
2218
|
cliCmd?: string;
|
|
2217
2219
|
cliType?: string;
|
|
2220
|
+
skipPermissionsFlag?: string;
|
|
2218
2221
|
workDir?: string;
|
|
2219
2222
|
preferredSessionName?: string;
|
|
2220
2223
|
existingSession?: string;
|
|
@@ -2306,7 +2309,8 @@ function FloatingTerminalInline({ agentLabel, agentIcon, projectPath, agentCliId
|
|
|
2306
2309
|
const resumeFlag = isClaude && resumeId ? ` --resume ${resumeId}` : '';
|
|
2307
2310
|
let mcpFlag = '';
|
|
2308
2311
|
if (isClaude) { try { const { getMcpFlag } = await import('@/lib/session-utils'); mcpFlag = await getMcpFlag(projectPath); } catch {} }
|
|
2309
|
-
const
|
|
2312
|
+
const defaultSkipFlag = cliType === 'codex' ? ' --dangerously-bypass-approvals-and-sandbox' : cliType === 'aider' ? ' --yes' : ' --dangerously-skip-permissions';
|
|
2313
|
+
const sf = skipPermissions ? ` ${skipPermissionsFlag || defaultSkipFlag.trim()}` : '';
|
|
2310
2314
|
commands.push(`${cdCmd} && ${cli}${resumeFlag}${modelFlag}${sf}${mcpFlag}`);
|
|
2311
2315
|
commands.forEach((cmd, i) => {
|
|
2312
2316
|
setTimeout(() => {
|
|
@@ -2335,13 +2339,14 @@ function FloatingTerminalInline({ agentLabel, agentIcon, projectPath, agentCliId
|
|
|
2335
2339
|
return <div ref={containerRef} className="w-full h-full" style={{ background: '#0d1117' }} />;
|
|
2336
2340
|
}
|
|
2337
2341
|
|
|
2338
|
-
function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliCmd: cliCmdProp, cliType, workDir, preferredSessionName, existingSession, resumeMode, resumeSessionId, profileEnv, isPrimary, skipPermissions, persistentSession, boundSessionId, initialPos, docked, onSessionReady, onClose }: {
|
|
2342
|
+
function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliCmd: cliCmdProp, cliType, skipPermissionsFlag, workDir, preferredSessionName, existingSession, resumeMode, resumeSessionId, profileEnv, isPrimary, skipPermissions, persistentSession, boundSessionId, initialPos, docked, onSessionReady, onClose }: {
|
|
2339
2343
|
agentLabel: string;
|
|
2340
2344
|
agentIcon: string;
|
|
2341
2345
|
projectPath: string;
|
|
2342
2346
|
agentCliId: string;
|
|
2343
2347
|
cliCmd?: string; // resolved CLI binary (claude/codex/aider)
|
|
2344
2348
|
cliType?: string; // claude-code/codex/aider/generic
|
|
2349
|
+
skipPermissionsFlag?: string;
|
|
2345
2350
|
workDir?: string;
|
|
2346
2351
|
preferredSessionName?: string;
|
|
2347
2352
|
existingSession?: string;
|
|
@@ -2555,7 +2560,8 @@ function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliC
|
|
|
2555
2560
|
const resumeFlag = isClaude && resumeId ? ` --resume ${resumeId}` : '';
|
|
2556
2561
|
let mcpFlag = '';
|
|
2557
2562
|
if (isClaude) { try { const { getMcpFlag } = await import('@/lib/session-utils'); mcpFlag = await getMcpFlag(projectPath); } catch {} }
|
|
2558
|
-
const
|
|
2563
|
+
const defaultSkipFlag = cliType === 'codex' ? ' --dangerously-bypass-approvals-and-sandbox' : cliType === 'aider' ? ' --yes' : ' --dangerously-skip-permissions';
|
|
2564
|
+
const sf = skipPermissions ? ` ${skipPermissionsFlag || defaultSkipFlag.trim()}` : '';
|
|
2559
2565
|
commands.push(`${cdCmd} && ${cli}${resumeFlag}${modelFlag}${sf}${mcpFlag}`);
|
|
2560
2566
|
|
|
2561
2567
|
// Send each command with delay between them
|
|
@@ -3569,8 +3575,8 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
3569
3575
|
setMascotTheme(t);
|
|
3570
3576
|
if (typeof window !== 'undefined') localStorage.setItem('forge.mascotTheme', t);
|
|
3571
3577
|
};
|
|
3572
|
-
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 } }[]>([]);
|
|
3573
|
-
const [termPicker, setTermPicker] = useState<{ agent: AgentConfig; sessName: string; workDir?: string; supportsSession?: boolean; currentSessionId: string | null; initialPos?: { x: number; y: number } } | null>(null);
|
|
3578
|
+
const [floatingTerminals, setFloatingTerminals] = useState<{ agentId: string; label: string; icon: string; cliId: string; cliCmd?: string; cliType?: string; skipPermissionsFlag?: 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 } }[]>([]);
|
|
3579
|
+
const [termPicker, setTermPicker] = useState<{ agent: AgentConfig; sessName: string; workDir?: string; supportsSession?: boolean; skipPermissionsFlag?: string; currentSessionId: string | null; initialPos?: { x: number; y: number } } | null>(null);
|
|
3574
3580
|
// Terminal layout: floating (draggable windows) or docked (fixed grid at bottom)
|
|
3575
3581
|
const [terminalLayout, setTerminalLayout] = useState<'floating' | 'docked'>(() => {
|
|
3576
3582
|
if (typeof window === 'undefined') return 'floating';
|
|
@@ -3783,7 +3789,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
3783
3789
|
// All agents: show picker (current session / new session / other sessions)
|
|
3784
3790
|
const resolveRes = await wsApi(workspaceId, 'open_terminal', { agentId: agent.id, resolveOnly: true }).catch(() => ({})) as any;
|
|
3785
3791
|
const currentSessionId = resolveRes?.currentSessionId ?? null;
|
|
3786
|
-
setTermPicker({ agent, sessName, workDir, supportsSession: resolveRes?.supportsSession ?? true, currentSessionId, initialPos });
|
|
3792
|
+
setTermPicker({ agent, sessName, workDir, supportsSession: resolveRes?.supportsSession ?? true, skipPermissionsFlag: resolveRes?.skipPermissionsFlag, currentSessionId, initialPos });
|
|
3787
3793
|
},
|
|
3788
3794
|
onSaveAsTemplate: async () => {
|
|
3789
3795
|
const name = prompt('Template name:', agent.label);
|
|
@@ -3811,7 +3817,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
3811
3817
|
const workDir = agent.workDir && agent.workDir !== './' && agent.workDir !== '.' ? agent.workDir : undefined;
|
|
3812
3818
|
const resolveRes = await wsApi(workspaceId, 'open_terminal', { agentId: agent.id, resolveOnly: true }).catch(() => ({})) as any;
|
|
3813
3819
|
const currentSessionId = resolveRes?.currentSessionId ?? null;
|
|
3814
|
-
setTermPicker({ agent, sessName, workDir, supportsSession: resolveRes?.supportsSession ?? true, currentSessionId, initialPos });
|
|
3820
|
+
setTermPicker({ agent, sessName, workDir, supportsSession: resolveRes?.supportsSession ?? true, skipPermissionsFlag: resolveRes?.skipPermissionsFlag, currentSessionId, initialPos });
|
|
3815
3821
|
},
|
|
3816
3822
|
} satisfies AgentNodeData,
|
|
3817
3823
|
};
|
|
@@ -4247,7 +4253,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
4247
4253
|
const tmux = res?.tmuxSession || sessName;
|
|
4248
4254
|
setFloatingTerminals(prev => [...prev, {
|
|
4249
4255
|
agentId: agent.id, label: agent.label, icon: agent.icon,
|
|
4250
|
-
cliId: agent.agentId || 'claude', workDir,
|
|
4256
|
+
cliId: agent.agentId || 'claude', cliCmd: res?.cliCmd, cliType: res?.cliType, skipPermissionsFlag: res?.skipPermissionsFlag || termPicker.skipPermissionsFlag, workDir,
|
|
4251
4257
|
tmuxSession: tmux, sessionName: sessName,
|
|
4252
4258
|
isPrimary: agent.primary, skipPermissions: agent.skipPermissions !== false,
|
|
4253
4259
|
persistentSession: agent.persistentSession, boundSessionId, initialPos: pickerInitialPos,
|
|
@@ -4267,6 +4273,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
4267
4273
|
agentCliId={ft.cliId}
|
|
4268
4274
|
cliCmd={ft.cliCmd}
|
|
4269
4275
|
cliType={ft.cliType}
|
|
4276
|
+
skipPermissionsFlag={ft.skipPermissionsFlag}
|
|
4270
4277
|
workDir={ft.workDir}
|
|
4271
4278
|
preferredSessionName={ft.sessionName}
|
|
4272
4279
|
existingSession={ft.tmuxSession}
|
|
@@ -4325,6 +4332,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
4325
4332
|
agentCliId={ft.cliId}
|
|
4326
4333
|
cliCmd={ft.cliCmd}
|
|
4327
4334
|
cliType={ft.cliType}
|
|
4335
|
+
skipPermissionsFlag={ft.skipPermissionsFlag}
|
|
4328
4336
|
workDir={ft.workDir}
|
|
4329
4337
|
preferredSessionName={ft.sessionName}
|
|
4330
4338
|
existingSession={ft.tmuxSession}
|