cowork-os 0.3.21 → 0.3.23

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 (170) hide show
  1. package/README.md +293 -6
  2. package/connectors/README.md +20 -0
  3. package/connectors/asana-mcp/README.md +24 -0
  4. package/connectors/asana-mcp/dist/index.js +427 -0
  5. package/connectors/asana-mcp/package.json +15 -0
  6. package/connectors/asana-mcp/src/index.ts +553 -0
  7. package/connectors/asana-mcp/tsconfig.json +13 -0
  8. package/connectors/hubspot-mcp/README.md +35 -0
  9. package/connectors/hubspot-mcp/dist/index.js +454 -0
  10. package/connectors/hubspot-mcp/package.json +15 -0
  11. package/connectors/hubspot-mcp/src/index.ts +562 -0
  12. package/connectors/hubspot-mcp/tsconfig.json +13 -0
  13. package/connectors/jira-mcp/README.md +49 -0
  14. package/connectors/jira-mcp/dist/index.js +588 -0
  15. package/connectors/jira-mcp/package.json +15 -0
  16. package/connectors/jira-mcp/src/index.ts +711 -0
  17. package/connectors/jira-mcp/tsconfig.json +13 -0
  18. package/connectors/linear-mcp/README.md +22 -0
  19. package/connectors/linear-mcp/dist/index.js +402 -0
  20. package/connectors/linear-mcp/package.json +15 -0
  21. package/connectors/linear-mcp/src/index.ts +522 -0
  22. package/connectors/linear-mcp/tsconfig.json +13 -0
  23. package/connectors/okta-mcp/README.md +24 -0
  24. package/connectors/okta-mcp/dist/index.js +411 -0
  25. package/connectors/okta-mcp/package.json +15 -0
  26. package/connectors/okta-mcp/src/index.ts +520 -0
  27. package/connectors/okta-mcp/tsconfig.json +13 -0
  28. package/connectors/salesforce-mcp/README.md +47 -0
  29. package/connectors/salesforce-mcp/dist/index.js +584 -0
  30. package/connectors/salesforce-mcp/package.json +15 -0
  31. package/connectors/salesforce-mcp/src/index.ts +722 -0
  32. package/connectors/salesforce-mcp/tsconfig.json +13 -0
  33. package/connectors/servicenow-mcp/README.md +26 -0
  34. package/connectors/servicenow-mcp/dist/index.js +400 -0
  35. package/connectors/servicenow-mcp/package.json +15 -0
  36. package/connectors/servicenow-mcp/src/index.ts +500 -0
  37. package/connectors/servicenow-mcp/tsconfig.json +13 -0
  38. package/connectors/templates/mcp-connector/README.md +31 -0
  39. package/connectors/templates/mcp-connector/package.json +15 -0
  40. package/connectors/templates/mcp-connector/src/index.ts +330 -0
  41. package/connectors/templates/mcp-connector/tsconfig.json +13 -0
  42. package/connectors/zendesk-mcp/README.md +40 -0
  43. package/connectors/zendesk-mcp/dist/index.js +431 -0
  44. package/connectors/zendesk-mcp/package.json +15 -0
  45. package/connectors/zendesk-mcp/src/index.ts +543 -0
  46. package/connectors/zendesk-mcp/tsconfig.json +13 -0
  47. package/dist/electron/electron/agent/daemon.js +25 -0
  48. package/dist/electron/electron/agent/executor.js +181 -26
  49. package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
  50. package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
  51. package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
  52. package/dist/electron/electron/agent/llm/index.js +11 -1
  53. package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
  54. package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
  55. package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
  56. package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
  57. package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
  58. package/dist/electron/electron/agent/llm/provider-factory.js +318 -4
  59. package/dist/electron/electron/agent/llm/types.js +66 -1
  60. package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
  61. package/dist/electron/electron/agent/tools/box-tools.js +231 -0
  62. package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
  63. package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
  64. package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
  65. package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
  66. package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
  67. package/dist/electron/electron/agent/tools/registry.js +541 -0
  68. package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
  69. package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
  70. package/dist/electron/electron/agent/tools/x-tools.js +1 -1
  71. package/dist/electron/electron/gateway/index.js +1 -0
  72. package/dist/electron/electron/gateway/router.js +123 -143
  73. package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
  74. package/dist/electron/electron/ipc/handlers.js +627 -158
  75. package/dist/electron/electron/main.js +63 -0
  76. package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
  77. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
  78. package/dist/electron/electron/memory/MemoryService.js +1 -1
  79. package/dist/electron/electron/preload.js +74 -1
  80. package/dist/electron/electron/settings/box-manager.js +54 -0
  81. package/dist/electron/electron/settings/dropbox-manager.js +54 -0
  82. package/dist/electron/electron/settings/google-drive-manager.js +54 -0
  83. package/dist/electron/electron/settings/notion-manager.js +56 -0
  84. package/dist/electron/electron/settings/onedrive-manager.js +54 -0
  85. package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
  86. package/dist/electron/electron/utils/box-api.js +153 -0
  87. package/dist/electron/electron/utils/dropbox-api.js +144 -0
  88. package/dist/electron/electron/utils/env-migration.js +19 -0
  89. package/dist/electron/electron/utils/google-drive-api.js +152 -0
  90. package/dist/electron/electron/utils/notion-api.js +103 -0
  91. package/dist/electron/electron/utils/onedrive-api.js +113 -0
  92. package/dist/electron/electron/utils/sharepoint-api.js +109 -0
  93. package/dist/electron/electron/utils/validation.js +82 -3
  94. package/dist/electron/electron/utils/x-cli.js +1 -1
  95. package/dist/electron/shared/channelMessages.js +284 -3
  96. package/dist/electron/shared/llm-provider-catalog.js +198 -0
  97. package/dist/electron/shared/types.js +88 -1
  98. package/package.json +12 -2
  99. package/src/electron/agent/executor.ts +205 -28
  100. package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
  101. package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
  102. package/src/electron/agent/llm/groq-provider.ts +39 -0
  103. package/src/electron/agent/llm/index.ts +5 -0
  104. package/src/electron/agent/llm/kimi-provider.ts +39 -0
  105. package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
  106. package/src/electron/agent/llm/openai-compatible.ts +133 -0
  107. package/src/electron/agent/llm/openai-oauth.ts +2 -1
  108. package/src/electron/agent/llm/openrouter-provider.ts +2 -1
  109. package/src/electron/agent/llm/provider-factory.ts +414 -6
  110. package/src/electron/agent/llm/types.ts +90 -1
  111. package/src/electron/agent/llm/xai-provider.ts +39 -0
  112. package/src/electron/agent/tools/box-tools.ts +239 -0
  113. package/src/electron/agent/tools/builtin-settings.ts +34 -0
  114. package/src/electron/agent/tools/dropbox-tools.ts +237 -0
  115. package/src/electron/agent/tools/google-drive-tools.ts +228 -0
  116. package/src/electron/agent/tools/notion-tools.ts +330 -0
  117. package/src/electron/agent/tools/onedrive-tools.ts +217 -0
  118. package/src/electron/agent/tools/registry.ts +565 -0
  119. package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
  120. package/src/electron/agent/tools/shell-tools.ts +11 -3
  121. package/src/electron/agent/tools/x-tools.ts +1 -1
  122. package/src/electron/database/SecureSettingsRepository.ts +7 -1
  123. package/src/electron/gateway/index.ts +1 -0
  124. package/src/electron/gateway/router.ts +134 -149
  125. package/src/electron/ipc/canvas-handlers.ts +10 -0
  126. package/src/electron/ipc/handlers.ts +673 -153
  127. package/src/electron/main.ts +35 -0
  128. package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
  129. package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
  130. package/src/electron/memory/MemoryService.ts +5 -1
  131. package/src/electron/preload.ts +167 -4
  132. package/src/electron/settings/box-manager.ts +58 -0
  133. package/src/electron/settings/dropbox-manager.ts +58 -0
  134. package/src/electron/settings/google-drive-manager.ts +58 -0
  135. package/src/electron/settings/notion-manager.ts +60 -0
  136. package/src/electron/settings/onedrive-manager.ts +58 -0
  137. package/src/electron/settings/sharepoint-manager.ts +58 -0
  138. package/src/electron/utils/box-api.ts +184 -0
  139. package/src/electron/utils/dropbox-api.ts +171 -0
  140. package/src/electron/utils/env-migration.ts +22 -0
  141. package/src/electron/utils/google-drive-api.ts +183 -0
  142. package/src/electron/utils/notion-api.ts +126 -0
  143. package/src/electron/utils/onedrive-api.ts +137 -0
  144. package/src/electron/utils/sharepoint-api.ts +132 -0
  145. package/src/electron/utils/validation.ts +102 -1
  146. package/src/electron/utils/x-cli.ts +1 -1
  147. package/src/renderer/App.tsx +20 -2
  148. package/src/renderer/components/BoxSettings.tsx +203 -0
  149. package/src/renderer/components/BrowserView.tsx +101 -0
  150. package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
  151. package/src/renderer/components/CanvasPreview.tsx +68 -1
  152. package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
  153. package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
  154. package/src/renderer/components/ConnectorsSettings.tsx +397 -0
  155. package/src/renderer/components/DropboxSettings.tsx +202 -0
  156. package/src/renderer/components/GoogleDriveSettings.tsx +201 -0
  157. package/src/renderer/components/MCPSettings.tsx +56 -0
  158. package/src/renderer/components/MainContent.tsx +270 -34
  159. package/src/renderer/components/NotionSettings.tsx +231 -0
  160. package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
  161. package/src/renderer/components/OnboardingModal.tsx +70 -1
  162. package/src/renderer/components/OneDriveSettings.tsx +212 -0
  163. package/src/renderer/components/Settings.tsx +611 -8
  164. package/src/renderer/components/SharePointSettings.tsx +224 -0
  165. package/src/renderer/components/Sidebar.tsx +25 -9
  166. package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
  167. package/src/renderer/styles/index.css +438 -25
  168. package/src/shared/channelMessages.ts +367 -4
  169. package/src/shared/llm-provider-catalog.ts +217 -0
  170. package/src/shared/types.ts +226 -1
@@ -0,0 +1,203 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { BoxSettingsData } from '../../shared/types';
3
+
4
+ export function BoxSettings() {
5
+ const [settings, setSettings] = useState<BoxSettingsData | null>(null);
6
+ const [saving, setSaving] = useState(false);
7
+ const [testing, setTesting] = useState(false);
8
+ const [testResult, setTestResult] = useState<{ success: boolean; error?: string; name?: string; userId?: string } | null>(null);
9
+ const [status, setStatus] = useState<{ configured: boolean; connected: boolean; name?: string; error?: string } | null>(null);
10
+ const [statusLoading, setStatusLoading] = useState(false);
11
+
12
+ useEffect(() => {
13
+ loadSettings();
14
+ refreshStatus();
15
+ }, []);
16
+
17
+ const loadSettings = async () => {
18
+ try {
19
+ const loaded = await window.electronAPI.getBoxSettings();
20
+ setSettings(loaded);
21
+ } catch (error) {
22
+ console.error('Failed to load Box settings:', error);
23
+ }
24
+ };
25
+
26
+ const updateSettings = (updates: Partial<BoxSettingsData>) => {
27
+ if (!settings) return;
28
+ setSettings({ ...settings, ...updates });
29
+ };
30
+
31
+ const handleSave = async () => {
32
+ if (!settings) return;
33
+ setSaving(true);
34
+ setTestResult(null);
35
+ try {
36
+ const payload: BoxSettingsData = { ...settings };
37
+ await window.electronAPI.saveBoxSettings(payload);
38
+ setSettings(payload);
39
+ await refreshStatus();
40
+ } catch (error) {
41
+ console.error('Failed to save Box settings:', error);
42
+ } finally {
43
+ setSaving(false);
44
+ }
45
+ };
46
+
47
+ const refreshStatus = async () => {
48
+ try {
49
+ setStatusLoading(true);
50
+ const result = await window.electronAPI.getBoxStatus();
51
+ setStatus(result);
52
+ } catch (error) {
53
+ console.error('Failed to load Box status:', error);
54
+ } finally {
55
+ setStatusLoading(false);
56
+ }
57
+ };
58
+
59
+ const handleTestConnection = async () => {
60
+ setTesting(true);
61
+ setTestResult(null);
62
+ try {
63
+ const result = await window.electronAPI.testBoxConnection();
64
+ setTestResult(result);
65
+ await refreshStatus();
66
+ } catch (error: any) {
67
+ setTestResult({ success: false, error: error.message || 'Failed to test connection' });
68
+ } finally {
69
+ setTesting(false);
70
+ }
71
+ };
72
+
73
+ if (!settings) {
74
+ return <div className="settings-loading">Loading Box settings...</div>;
75
+ }
76
+
77
+ const statusLabel = !status?.configured
78
+ ? 'Missing Token'
79
+ : status.connected
80
+ ? 'Connected'
81
+ : 'Configured';
82
+
83
+ const statusClass = !status?.configured
84
+ ? 'missing'
85
+ : status.connected
86
+ ? 'connected'
87
+ : 'configured';
88
+
89
+ return (
90
+ <div className="box-settings">
91
+ <div className="settings-section">
92
+ <div className="settings-section-header">
93
+ <div className="settings-title-with-badge">
94
+ <h3>Connect Box</h3>
95
+ {status && (
96
+ <span
97
+ className={`box-status-badge ${statusClass}`}
98
+ title={!status.configured ? 'Access token not configured' : status.connected ? 'Connected to Box' : 'Configured'}
99
+ >
100
+ {statusLabel}
101
+ </span>
102
+ )}
103
+ {statusLoading && !status && (
104
+ <span className="box-status-badge configured">Checking…</span>
105
+ )}
106
+ </div>
107
+ <button className="btn-secondary btn-sm" onClick={refreshStatus} disabled={statusLoading}>
108
+ {statusLoading ? 'Checking...' : 'Refresh Status'}
109
+ </button>
110
+ </div>
111
+ <p className="settings-description">
112
+ Connect the agent to Box using a developer token or OAuth access token, then use the built-in `box_action`
113
+ tool to search and manage files.
114
+ </p>
115
+ {status?.error && (
116
+ <p className="settings-hint">Status check: {status.error}</p>
117
+ )}
118
+ <div className="settings-actions">
119
+ <button
120
+ className="btn-secondary btn-sm"
121
+ onClick={() => window.electronAPI.openExternal('https://app.box.com/developers/console')}
122
+ >
123
+ Open Box Console
124
+ </button>
125
+ </div>
126
+ </div>
127
+
128
+ <div className="settings-section">
129
+ <div className="settings-field">
130
+ <label>Enable Integration</label>
131
+ <label className="settings-toggle">
132
+ <input
133
+ type="checkbox"
134
+ checked={settings.enabled}
135
+ onChange={(e) => updateSettings({ enabled: e.target.checked })}
136
+ />
137
+ <span className="toggle-slider" />
138
+ </label>
139
+ </div>
140
+
141
+ <div className="settings-field">
142
+ <label>Access Token</label>
143
+ <input
144
+ type="password"
145
+ className="settings-input"
146
+ placeholder="Box access token"
147
+ value={settings.accessToken || ''}
148
+ onChange={(e) => updateSettings({ accessToken: e.target.value || undefined })}
149
+ />
150
+ <p className="settings-hint">Use a developer token or OAuth access token with required scopes.</p>
151
+ </div>
152
+
153
+ <div className="settings-field">
154
+ <label>Timeout (ms)</label>
155
+ <input
156
+ type="number"
157
+ className="settings-input"
158
+ min={1000}
159
+ max={120000}
160
+ value={settings.timeoutMs ?? 20000}
161
+ onChange={(e) => updateSettings({ timeoutMs: Number(e.target.value) })}
162
+ />
163
+ </div>
164
+
165
+ <div className="settings-actions">
166
+ <button className="btn-secondary btn-sm" onClick={handleTestConnection} disabled={testing}>
167
+ {testing ? 'Testing...' : 'Test Connection'}
168
+ </button>
169
+ <button className="btn-primary btn-sm" onClick={handleSave} disabled={saving}>
170
+ {saving ? 'Saving...' : 'Save Settings'}
171
+ </button>
172
+ </div>
173
+
174
+ {testResult && (
175
+ <div className={`test-result ${testResult.success ? 'success' : 'error'}`}>
176
+ {testResult.success ? (
177
+ <span>Connected{testResult.name ? ` as ${testResult.name}` : ''}</span>
178
+ ) : (
179
+ <span>Connection failed: {testResult.error}</span>
180
+ )}
181
+ </div>
182
+ )}
183
+ </div>
184
+
185
+ <div className="settings-section">
186
+ <h4>Quick Usage</h4>
187
+ <pre className="settings-info-box">{`// List root folder items
188
+ box_action({
189
+ action: "list_folder_items",
190
+ folder_id: "0",
191
+ limit: 25
192
+ });
193
+
194
+ // Upload a file to root
195
+ box_action({
196
+ action: "upload_file",
197
+ file_path: "reports/summary.pdf",
198
+ parent_id: "0"
199
+ });`}</pre>
200
+ </div>
201
+ </div>
202
+ );
203
+ }
@@ -0,0 +1,101 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ interface BrowserViewProps {
4
+ initialUrl?: string;
5
+ onBack: () => void;
6
+ }
7
+
8
+ export function BrowserView({ initialUrl, onBack }: BrowserViewProps) {
9
+ const [url, setUrl] = useState(initialUrl || '');
10
+ const [activeUrl, setActiveUrl] = useState(initialUrl || '');
11
+ const webviewRef = useRef<any>(null);
12
+
13
+ useEffect(() => {
14
+ if (initialUrl) {
15
+ setUrl(initialUrl);
16
+ setActiveUrl(initialUrl);
17
+ }
18
+ }, [initialUrl]);
19
+
20
+ const navigate = (nextUrl?: string) => {
21
+ const target = (nextUrl || url).trim();
22
+ if (!target) return;
23
+ const normalized = /^https?:\/\//i.test(target) ? target : `https://${target}`;
24
+ setActiveUrl(normalized);
25
+ };
26
+
27
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
28
+ if (event.key === 'Enter') {
29
+ event.preventDefault();
30
+ navigate();
31
+ }
32
+ };
33
+
34
+ return (
35
+ <div className="browser-view">
36
+ <div className="browser-toolbar">
37
+ <button className="browser-toolbar-btn" onClick={onBack} title="Back to app">
38
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
39
+ <polyline points="15 18 9 12 15 6" />
40
+ </svg>
41
+ Back
42
+ </button>
43
+ <button
44
+ className="browser-toolbar-btn"
45
+ onClick={() => webviewRef.current?.goBack()}
46
+ title="Back"
47
+ >
48
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
49
+ <polyline points="15 18 9 12 15 6" />
50
+ </svg>
51
+ </button>
52
+ <button
53
+ className="browser-toolbar-btn"
54
+ onClick={() => webviewRef.current?.goForward()}
55
+ title="Forward"
56
+ >
57
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
58
+ <polyline points="9 18 15 12 9 6" />
59
+ </svg>
60
+ </button>
61
+ <button
62
+ className="browser-toolbar-btn"
63
+ onClick={() => webviewRef.current?.reload()}
64
+ title="Reload"
65
+ >
66
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
67
+ <polyline points="23 4 23 10 17 10" />
68
+ <polyline points="1 20 1 14 7 14" />
69
+ <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
70
+ </svg>
71
+ </button>
72
+ <div className="browser-url">
73
+ <input
74
+ value={url}
75
+ onChange={(event) => setUrl(event.target.value)}
76
+ onKeyDown={handleKeyDown}
77
+ placeholder="https://example.com"
78
+ />
79
+ <button className="browser-toolbar-btn primary" onClick={() => navigate()} title="Go">
80
+ Go
81
+ </button>
82
+ </div>
83
+ </div>
84
+ <div className="browser-surface">
85
+ {activeUrl ? (
86
+ <webview
87
+ ref={webviewRef}
88
+ src={activeUrl}
89
+ className="browser-webview"
90
+ allowpopups={true}
91
+ webpreferences="contextIsolation=yes, nodeIntegration=no"
92
+ />
93
+ ) : (
94
+ <div className="browser-empty">
95
+ Enter a URL above to start browsing.
96
+ </div>
97
+ )}
98
+ </div>
99
+ </div>
100
+ );
101
+ }
@@ -17,6 +17,8 @@ interface BuiltinToolsSettingsData {
17
17
  image: ToolCategoryConfig;
18
18
  };
19
19
  toolOverrides: Record<string, { enabled: boolean; priority?: 'high' | 'normal' | 'low' }>;
20
+ toolTimeouts: Record<string, number>;
21
+ toolAutoApprove: Record<string, boolean>;
20
22
  version: string;
21
23
  }
22
24
 
@@ -182,6 +184,62 @@ export function BuiltinToolsSettings() {
182
184
  }
183
185
  };
184
186
 
187
+ const handleRunCommandAutoApprove = async (enabled: boolean) => {
188
+ if (!settings) return;
189
+
190
+ const nextAutoApprove = { ...(settings.toolAutoApprove || {}) };
191
+ if (enabled) {
192
+ nextAutoApprove.run_command = true;
193
+ } else {
194
+ delete nextAutoApprove.run_command;
195
+ }
196
+
197
+ const newSettings = {
198
+ ...settings,
199
+ toolAutoApprove: nextAutoApprove,
200
+ };
201
+
202
+ setSettings(newSettings);
203
+
204
+ try {
205
+ setSaving(true);
206
+ await window.electronAPI.saveBuiltinToolsSettings(newSettings);
207
+ } catch (error) {
208
+ console.error('Failed to save settings:', error);
209
+ } finally {
210
+ setSaving(false);
211
+ }
212
+ };
213
+
214
+ const handleRunCommandTimeout = async (value: string) => {
215
+ if (!settings) return;
216
+
217
+ const parsed = Number(value);
218
+ const nextTimeouts = { ...(settings.toolTimeouts || {}) };
219
+
220
+ if (!value || !Number.isFinite(parsed) || parsed <= 0) {
221
+ delete nextTimeouts.run_command;
222
+ } else {
223
+ nextTimeouts.run_command = Math.round(parsed);
224
+ }
225
+
226
+ const newSettings = {
227
+ ...settings,
228
+ toolTimeouts: nextTimeouts,
229
+ };
230
+
231
+ setSettings(newSettings);
232
+
233
+ try {
234
+ setSaving(true);
235
+ await window.electronAPI.saveBuiltinToolsSettings(newSettings);
236
+ } catch (error) {
237
+ console.error('Failed to save settings:', error);
238
+ } finally {
239
+ setSaving(false);
240
+ }
241
+ };
242
+
185
243
  if (loading) {
186
244
  return <div className="settings-loading">Loading settings...</div>;
187
245
  }
@@ -205,6 +263,12 @@ export function BuiltinToolsSettings() {
205
263
  const info = CATEGORY_INFO[category];
206
264
  const config = settings.categories[category];
207
265
  const tools = categories[category] || [];
266
+ const runCommandAutoApprove = category === 'shell'
267
+ ? Boolean(settings.toolAutoApprove?.run_command)
268
+ : false;
269
+ const runCommandTimeout = category === 'shell'
270
+ ? (settings.toolTimeouts?.run_command ?? '')
271
+ : '';
208
272
 
209
273
  return (
210
274
  <div
@@ -267,6 +331,47 @@ export function BuiltinToolsSettings() {
267
331
  </div>
268
332
  </div>
269
333
 
334
+ {category === 'shell' && expandedCategory === category && (
335
+ <div className="builtin-tool-advanced">
336
+ <div className="builtin-tool-advanced-row">
337
+ <div className="builtin-tool-advanced-text">
338
+ <div className="builtin-tool-advanced-label">Auto-approve safe commands</div>
339
+ <div className="builtin-tool-advanced-hint">
340
+ Skips approval prompts for non-destructive commands.
341
+ </div>
342
+ </div>
343
+ <label className="builtin-tool-toggle">
344
+ <input
345
+ type="checkbox"
346
+ checked={runCommandAutoApprove}
347
+ onChange={(e) => handleRunCommandAutoApprove(e.target.checked)}
348
+ disabled={!config.enabled}
349
+ />
350
+ <span className="builtin-tool-toggle-slider"></span>
351
+ </label>
352
+ </div>
353
+
354
+ <div className="builtin-tool-advanced-row">
355
+ <div className="builtin-tool-advanced-text">
356
+ <div className="builtin-tool-advanced-label">run_command timeout (ms)</div>
357
+ <div className="builtin-tool-advanced-hint">
358
+ Used when the command doesn't set its own timeout.
359
+ </div>
360
+ </div>
361
+ <input
362
+ className="builtin-tool-timeout-input"
363
+ type="number"
364
+ min={1000}
365
+ step={1000}
366
+ value={runCommandTimeout}
367
+ onChange={(e) => handleRunCommandTimeout(e.target.value)}
368
+ disabled={!config.enabled}
369
+ placeholder="30000"
370
+ />
371
+ </div>
372
+ </div>
373
+ )}
374
+
270
375
  {expandedCategory === category && tools.length > 0 && (
271
376
  <div className="builtin-tool-list">
272
377
  {tools.map((tool) => (
@@ -6,6 +6,7 @@ interface CanvasPreviewProps {
6
6
  session: CanvasSession;
7
7
  onClose?: () => void;
8
8
  forceSnapshot?: boolean;
9
+ onOpenBrowser?: (url?: string) => void;
9
10
  }
10
11
 
11
12
  interface SnapshotHistoryEntry {
@@ -132,7 +133,7 @@ const CanvasImage = memo(function CanvasImage({
132
133
  );
133
134
  });
134
135
 
135
- export function CanvasPreview({ session, onClose, forceSnapshot = false }: CanvasPreviewProps) {
136
+ export function CanvasPreview({ session, onClose, forceSnapshot = false, onOpenBrowser }: CanvasPreviewProps) {
136
137
  const isBrowserCanvas = session.mode === 'browser';
137
138
  const agentContext = useAgentContext();
138
139
  const [imageData, setImageData] = useState<string | null>(null);
@@ -158,8 +159,11 @@ export function CanvasPreview({ session, onClose, forceSnapshot = false }: Canva
158
159
  const [showConsole, setShowConsole] = useState(false);
159
160
  const [showExportMenu, setShowExportMenu] = useState(false);
160
161
  const [isInteractiveMode, setIsInteractiveMode] = useState(!isBrowserCanvas && !forceSnapshot);
162
+ const [showBrowserUrlInput, setShowBrowserUrlInput] = useState(false);
163
+ const [browserUrl, setBrowserUrl] = useState('');
161
164
 
162
165
  const refreshIntervalRef = useRef<NodeJS.Timeout | null>(null);
166
+ const browserInputRef = useRef<HTMLInputElement>(null);
163
167
  const containerRef = useRef<HTMLDivElement>(null);
164
168
  const retryCountRef = useRef(0);
165
169
  const retryTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -593,6 +597,27 @@ export function CanvasPreview({ session, onClose, forceSnapshot = false }: Canva
593
597
  }
594
598
  }, [session.id]);
595
599
 
600
+ // Open a remote web page inside the canvas window
601
+ const handleOpenBrowserCanvas = useCallback(() => {
602
+ setShowExportMenu(false);
603
+ if (!onOpenBrowser) {
604
+ setCopyFeedback('Browser view unavailable');
605
+ setTimeout(() => setCopyFeedback(null), 2000);
606
+ return;
607
+ }
608
+ onOpenBrowser(session.url || '');
609
+ }, [onOpenBrowser, session.url]);
610
+
611
+ // Submit URL from browser input
612
+ const handleSubmitBrowserUrl = useCallback(() => {
613
+ if (!browserUrl.trim()) return;
614
+ setShowBrowserUrlInput(false);
615
+ if (onOpenBrowser) {
616
+ onOpenBrowser(browserUrl.trim());
617
+ }
618
+ setBrowserUrl('');
619
+ }, [browserUrl, onOpenBrowser]);
620
+
596
621
  // Open session folder in Finder
597
622
  const handleOpenFolder = useCallback(async () => {
598
623
  setShowExportMenu(false);
@@ -1021,6 +1046,18 @@ export function CanvasPreview({ session, onClose, forceSnapshot = false }: Canva
1021
1046
  <line x1="10" y1="14" x2="21" y2="3" />
1022
1047
  </svg>
1023
1048
  </button>
1049
+ {/* Open web page in canvas */}
1050
+ <button
1051
+ className="canvas-action-btn"
1052
+ onClick={handleOpenBrowserCanvas}
1053
+ title="Open in browser view"
1054
+ >
1055
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1056
+ <circle cx="12" cy="12" r="10" />
1057
+ <line x1="2" y1="12" x2="22" y2="12" />
1058
+ <path d="M12 2a15.3 15.3 0 0 1 0 20a15.3 15.3 0 0 1 0-20" />
1059
+ </svg>
1060
+ </button>
1024
1061
  {/* Minimize */}
1025
1062
  <button
1026
1063
  className="canvas-action-btn"
@@ -1048,6 +1085,36 @@ export function CanvasPreview({ session, onClose, forceSnapshot = false }: Canva
1048
1085
  </button>
1049
1086
  </div>
1050
1087
  </div>
1088
+ {showBrowserUrlInput && (
1089
+ <div className="canvas-browser-input-row">
1090
+ <input
1091
+ ref={browserInputRef}
1092
+ className="canvas-browser-input"
1093
+ type="text"
1094
+ value={browserUrl}
1095
+ onChange={(e) => setBrowserUrl(e.target.value)}
1096
+ onKeyDown={(e) => {
1097
+ if (e.key === 'Enter') {
1098
+ e.preventDefault();
1099
+ handleSubmitBrowserUrl();
1100
+ } else if (e.key === 'Escape') {
1101
+ e.preventDefault();
1102
+ setShowBrowserUrlInput(false);
1103
+ }
1104
+ }}
1105
+ placeholder="https://example.com"
1106
+ />
1107
+ <button className="canvas-browser-btn" onClick={handleSubmitBrowserUrl}>
1108
+ Open
1109
+ </button>
1110
+ <button
1111
+ className="canvas-browser-btn ghost"
1112
+ onClick={() => setShowBrowserUrlInput(false)}
1113
+ >
1114
+ Cancel
1115
+ </button>
1116
+ </div>
1117
+ )}
1051
1118
  {!isMinimized && (
1052
1119
  <>
1053
1120
  <div className="canvas-preview-content">
@@ -0,0 +1,116 @@
1
+ import { useState } from 'react';
2
+
3
+ export interface ConnectorEnvField {
4
+ key: string;
5
+ label: string;
6
+ placeholder?: string;
7
+ type?: 'text' | 'password';
8
+ }
9
+
10
+ interface ConnectorEnvModalProps {
11
+ serverId: string;
12
+ serverName: string;
13
+ initialEnv?: Record<string, string>;
14
+ fields: ConnectorEnvField[];
15
+ onClose: () => void;
16
+ onSaved: () => void;
17
+ }
18
+
19
+ export function ConnectorEnvModal({
20
+ serverId,
21
+ serverName,
22
+ initialEnv = {},
23
+ fields,
24
+ onClose,
25
+ onSaved,
26
+ }: ConnectorEnvModalProps) {
27
+ const [values, setValues] = useState<Record<string, string>>(() => {
28
+ const seeded: Record<string, string> = {};
29
+ fields.forEach((field) => {
30
+ seeded[field.key] = initialEnv[field.key] || '';
31
+ });
32
+ return seeded;
33
+ });
34
+ const [saving, setSaving] = useState(false);
35
+ const [error, setError] = useState<string | null>(null);
36
+
37
+ const mergeEnv = (): Record<string, string> => {
38
+ const merged: Record<string, string> = { ...initialEnv };
39
+ Object.entries(values).forEach(([key, value]) => {
40
+ if (!value) {
41
+ delete merged[key];
42
+ } else {
43
+ merged[key] = value;
44
+ }
45
+ });
46
+ return merged;
47
+ };
48
+
49
+ const reconnectServer = async () => {
50
+ try {
51
+ await window.electronAPI.disconnectMCPServer(serverId);
52
+ } catch {
53
+ // ignore
54
+ }
55
+ await window.electronAPI.connectMCPServer(serverId);
56
+ };
57
+
58
+ const handleSave = async () => {
59
+ setSaving(true);
60
+ setError(null);
61
+ try {
62
+ await window.electronAPI.updateMCPServer(serverId, { env: mergeEnv() });
63
+ await reconnectServer();
64
+ onSaved();
65
+ onClose();
66
+ } catch (err: any) {
67
+ setError(err.message || 'Failed to save credentials');
68
+ } finally {
69
+ setSaving(false);
70
+ }
71
+ };
72
+
73
+ return (
74
+ <div className="mcp-modal-overlay" onClick={onClose}>
75
+ <div className="mcp-modal connector-setup-modal" onClick={(e) => e.stopPropagation()}>
76
+ <div className="mcp-modal-header">
77
+ <div className="registry-details-title">
78
+ <h3>{serverName} Configuration</h3>
79
+ </div>
80
+ <button className="mcp-modal-close" onClick={onClose}>
81
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
82
+ <path d="M18 6L6 18M6 6l12 12" />
83
+ </svg>
84
+ </button>
85
+ </div>
86
+ <div className="mcp-modal-content">
87
+ {fields.map((field) => (
88
+ <div key={field.key} className="settings-field">
89
+ <label>{field.label}</label>
90
+ <input
91
+ className="settings-input"
92
+ type={field.type || 'text'}
93
+ placeholder={field.placeholder}
94
+ value={values[field.key] || ''}
95
+ onChange={(e) => setValues({ ...values, [field.key]: e.target.value })}
96
+ />
97
+ </div>
98
+ ))}
99
+
100
+ <div className="connector-setup-actions">
101
+ <button className="button-primary" onClick={handleSave} disabled={saving}>
102
+ {saving ? 'Saving...' : 'Save Credentials'}
103
+ </button>
104
+ </div>
105
+
106
+ {error && (
107
+ <div className="mcp-server-error">
108
+ <span className="mcp-error-icon">⚠</span>
109
+ {error}
110
+ </div>
111
+ )}
112
+ </div>
113
+ </div>
114
+ </div>
115
+ );
116
+ }