@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.
Files changed (145) hide show
  1. package/.forge/mcp.json +8 -0
  2. package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/01-settings.md +5 -5
  3. package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/07-projects.md +1 -1
  4. package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/01-settings.md +5 -5
  5. package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/07-projects.md +1 -1
  6. package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/01-settings.md +5 -5
  7. package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/07-projects.md +1 -1
  8. package/.forge/worktrees/pipeline-316c6574/lib/help-docs/01-settings.md +5 -5
  9. package/.forge/worktrees/pipeline-316c6574/lib/help-docs/07-projects.md +1 -1
  10. package/.forge/worktrees/pipeline-44a94121/lib/help-docs/01-settings.md +5 -5
  11. package/.forge/worktrees/pipeline-44a94121/lib/help-docs/07-projects.md +1 -1
  12. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +5 -5
  13. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +1 -1
  14. package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/01-settings.md +5 -5
  15. package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/07-projects.md +1 -1
  16. package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/01-settings.md +5 -5
  17. package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/07-projects.md +1 -1
  18. package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/01-settings.md +5 -5
  19. package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/07-projects.md +1 -1
  20. package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/01-settings.md +5 -5
  21. package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/07-projects.md +1 -1
  22. package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/01-settings.md +5 -5
  23. package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/07-projects.md +1 -1
  24. package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/01-settings.md +5 -5
  25. package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/07-projects.md +1 -1
  26. package/CLAUDE.md +2 -2
  27. package/RELEASE_NOTES.md +101 -5
  28. package/app/api/auth/check/route.ts +18 -0
  29. package/app/api/browser-bridge/route.ts +70 -0
  30. package/app/api/chat/sessions/[id]/events/route.ts +17 -0
  31. package/app/api/chat/sessions/[id]/fork/route.ts +15 -0
  32. package/app/api/chat/sessions/[id]/messages/route.ts +21 -0
  33. package/app/api/chat/sessions/[id]/route.ts +23 -0
  34. package/app/api/chat/sessions/route.ts +12 -0
  35. package/app/api/chat/temper-ping/route.ts +18 -0
  36. package/app/api/chat-proxy/[...path]/route.ts +83 -0
  37. package/app/api/connector-tool/route.ts +38 -0
  38. package/app/api/connectors/[id]/settings/route.ts +112 -0
  39. package/app/api/connectors/route.ts +108 -0
  40. package/app/api/health/tools/route.ts +14 -0
  41. package/app/api/issue-scanner-gitlab/route.ts +95 -0
  42. package/app/api/jobs/[id]/reset_dedup/route.ts +15 -0
  43. package/app/api/jobs/[id]/route.ts +31 -0
  44. package/app/api/jobs/[id]/run/route.ts +44 -0
  45. package/app/api/jobs/[id]/runs/[runId]/route.ts +15 -0
  46. package/app/api/jobs/[id]/runs/route.ts +15 -0
  47. package/app/api/jobs/preview/route.ts +193 -0
  48. package/app/api/jobs/route.ts +36 -0
  49. package/app/api/notify/test/route.ts +39 -7
  50. package/app/api/pipelines/[id]/route.ts +10 -1
  51. package/app/api/pipelines/route.ts +16 -2
  52. package/app/api/plugins/route.ts +40 -8
  53. package/app/api/project-sessions/route.ts +50 -10
  54. package/app/api/settings/route.ts +13 -0
  55. package/app/chat/page.tsx +531 -0
  56. package/bin/forge-server.mjs +3 -1
  57. package/cli/chat.ts +283 -0
  58. package/cli/jobs.ts +176 -0
  59. package/cli/mw.ts +28 -1
  60. package/cli/worktree.ts +245 -0
  61. package/components/ConnectorsPanel.tsx +275 -0
  62. package/components/Dashboard.tsx +90 -37
  63. package/components/JobsView.tsx +361 -0
  64. package/components/LogViewer.tsx +12 -2
  65. package/components/PipelineView.tsx +275 -56
  66. package/components/PluginsPanel.tsx +3 -1
  67. package/components/SettingsModal.tsx +229 -40
  68. package/components/SkillsPanel.tsx +12 -4
  69. package/components/TerminalLauncher.tsx +3 -1
  70. package/components/WebTerminal.tsx +32 -9
  71. package/components/WorkspaceView.tsx +18 -10
  72. package/docs/Connector-DeclarativeExtract-Handoff.md +471 -0
  73. package/docs/Connector-DeclarativeExtract-Spec.md +364 -0
  74. package/docs/Implementation-Plan-Browser-Agent.md +487 -0
  75. package/docs/Jobs-Design.md +240 -0
  76. package/docs/LOCAL-DEPLOY.md +3 -3
  77. package/docs/RFC-Browser-Connectors.md +509 -0
  78. package/lib/agents/index.ts +44 -6
  79. package/lib/agents/types.ts +1 -1
  80. package/lib/browser-bridge-standalone.ts +317 -0
  81. package/lib/builtin-plugins/github-api.yaml +93 -0
  82. package/lib/builtin-plugins/gitlab.yaml +860 -0
  83. package/lib/builtin-plugins/mantis.probe.js +176 -0
  84. package/lib/builtin-plugins/mantis.yaml +964 -0
  85. package/lib/builtin-plugins/pmdb.yaml +178 -0
  86. package/lib/builtin-plugins/teams.yaml +913 -0
  87. package/lib/chat/__test__/smoke.ts +30 -0
  88. package/lib/chat/agent-loop.ts +523 -0
  89. package/lib/chat/bridge-client.ts +59 -0
  90. package/lib/chat/llm/anthropic.ts +99 -0
  91. package/lib/chat/llm/index.ts +20 -0
  92. package/lib/chat/llm/openai.ts +215 -0
  93. package/lib/chat/llm/types.ts +42 -0
  94. package/lib/chat/local-memory.ts +300 -0
  95. package/lib/chat/memory-store.ts +87 -0
  96. package/lib/chat/memory-tools.ts +157 -0
  97. package/lib/chat/protocols/http.ts +118 -0
  98. package/lib/chat/protocols/shell.ts +101 -0
  99. package/lib/chat/proxy.ts +51 -0
  100. package/lib/chat/session-store.ts +272 -0
  101. package/lib/chat/telegram-bridge.ts +276 -0
  102. package/lib/chat/temper.ts +281 -0
  103. package/lib/chat/tool-dispatcher.ts +190 -0
  104. package/lib/chat/types.ts +50 -0
  105. package/lib/chat-standalone.ts +286 -0
  106. package/lib/crypto.ts +1 -1
  107. package/lib/health.ts +131 -0
  108. package/lib/help-docs/00-overview.md +2 -1
  109. package/lib/help-docs/01-settings.md +46 -25
  110. package/lib/help-docs/07-projects.md +1 -1
  111. package/lib/help-docs/10-troubleshooting.md +10 -2
  112. package/lib/help-docs/16-gitlab-autofix.md +114 -0
  113. package/lib/help-docs/17-connectors.md +322 -0
  114. package/lib/help-docs/18-chrome-mcp.md +134 -0
  115. package/lib/help-docs/19-jobs.md +140 -0
  116. package/lib/help-docs/20-mantis-bug-fix.md +115 -0
  117. package/lib/help-docs/CLAUDE.md +10 -0
  118. package/lib/init.ts +137 -50
  119. package/lib/iso-time.ts +30 -0
  120. package/lib/issue-scanner-gitlab.ts +281 -0
  121. package/lib/jobs/dispatcher.ts +217 -0
  122. package/lib/jobs/scheduler.ts +334 -0
  123. package/lib/jobs/store.ts +319 -0
  124. package/lib/jobs/types.ts +117 -0
  125. package/lib/pipeline-scheduler.ts +1 -6
  126. package/lib/pipeline.ts +790 -10
  127. package/lib/plugins/registry.ts +133 -8
  128. package/lib/plugins/templates.ts +83 -0
  129. package/lib/plugins/types.ts +140 -1
  130. package/lib/session-watcher.ts +36 -10
  131. package/lib/settings.ts +65 -33
  132. package/lib/skills.ts +3 -1
  133. package/lib/task-manager.ts +50 -22
  134. package/lib/telegram-bot.ts +71 -0
  135. package/lib/terminal-standalone.ts +58 -36
  136. package/lib/workspace/orchestrator.ts +1 -0
  137. package/middleware.ts +10 -0
  138. package/package.json +3 -2
  139. package/scripts/bench/README.md +1 -1
  140. package/scripts/bench/tasks/01-text-utils/validator.sh +1 -1
  141. package/scripts/bench/tasks/02-pagination/setup.sh +1 -1
  142. package/scripts/bench/tasks/02-pagination/validator.sh +1 -1
  143. package/scripts/bench/tasks/03-bug-fix/setup.sh +1 -1
  144. package/scripts/bench/tasks/03-bug-fix/validator.sh +1 -1
  145. 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
- <div className="flex gap-2">
854
- <div className="flex-1">
855
- <label className="text-[8px] text-[var(--text-secondary)]">Provider</label>
856
- <select value={cfg.provider || 'anthropic'} onChange={e => onUpdate({ ...cfg, provider: e.target.value })} className={inputClass}>
857
- <option value="anthropic">Anthropic</option>
858
- <option value="google">Google</option>
859
- <option value="openai">OpenAI</option>
860
- <option value="grok">Grok</option>
861
- </select>
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 className="flex-1">
864
- <label className="text-[8px] text-[var(--text-secondary)]">API Key (optional)</label>
865
- <input type="password" value={cfg.apiKey || ''} onChange={e => onUpdate({ ...cfg, apiKey: e.target.value })} className={inputClass} />
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
- </div>
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 (optional, uses provider key if empty)</label>
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: '--full-auto' },
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: '--full-auto' },
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
- {/* ── Providers Section ── */}
1560
- <div className="mt-4 pt-3 border-t border-[var(--border)]">
1561
- <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase mb-2 block">API Providers</label>
1562
- {['anthropic', 'google', 'openai', 'grok'].map(name => {
1563
- const provider = settings.providers?.[name] || {};
1564
- const secretKey = `providers.${name}.apiKey`;
1565
- const hasKey = (provider.apiKey && provider.apiKey !== '••••••••') || settings._secretStatus?.[secretKey];
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
- <div key={name} className="flex items-center gap-2 px-2 py-1.5 mb-1 rounded" style={{ background: 'var(--bg-tertiary)' }}>
1568
- <span className="text-[10px] text-[var(--text-primary)] w-20 font-semibold capitalize">{name}</span>
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="password"
1571
- placeholder="API Key"
1572
- value={provider.apiKey || ''}
1573
- onChange={e => setSettings({
1574
- ...settings,
1575
- providers: { ...settings.providers, [name]: { ...provider, apiKey: e.target.value } }
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
- <span className={`text-[8px] ${hasKey ? 'text-green-400' : 'text-gray-600'}`}>
1580
- {hasKey ? '● set' : '○'}
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.&nbsp;
1697
+ <b>Local</b>: always SQLite in <code>workflow.db</code> (keyword search).&nbsp;
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
- <span className="text-[8px] text-gray-600">
192
- {mouseOn ? 'scroll: trackpad · copy: Shift+drag' : 'scroll: Ctrl+B [ · copy: drag'}
193
- </span>
194
- <button onClick={toggle} title={mouseOn ? 'Click to disable mouse (easier text select)' : 'Click to enable mouse (trackpad scroll)'}
195
- 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'}`}>
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 onClick={() => onSplit('vertical')} className="text-[10px] px-2 py-0.5 text-gray-400 hover:text-white hover:bg-[var(--term-border)] rounded">
775
- Split Right
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 onClick={() => onSplit('horizontal')} className="text-[10px] px-2 py-0.5 text-gray-400 hover:text-white hover:bg-[var(--term-border)] rounded">
778
- Split Down
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 sf = skipPermissions ? (cliType === 'codex' ? ' --full-auto' : cliType === 'aider' ? ' --yes' : ' --dangerously-skip-permissions') : '';
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 sf = skipPermissions ? (cliType === 'codex' ? ' --full-auto' : cliType === 'aider' ? ' --yes' : ' --dangerously-skip-permissions') : '';
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}