farming-code 2.2.28 → 2.2.30
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/README.md +27 -90
- package/README.zh_cn.md +17 -90
- package/THIRD_PARTY_NOTICES.md +25 -6
- package/backend/acp/packaged-claude-acp.js +13 -0
- package/backend/acp/packaged-codex-acp.js +2 -2
- package/backend/acp-runtime.js +38 -17
- package/backend/acp-session-state.js +78 -88
- package/backend/acp-transcript.js +174 -9
- package/backend/agent-manager.js +27 -2
- package/backend/agent-session-history.js +45 -12
- package/backend/codex-session-history.js +1 -1
- package/backend/codex-transcript.js +0 -202
- package/backend/config-manager.js +44 -16
- package/backend/data/LICENSE.agent-browser +201 -0
- package/backend/data/runtime-dependency-manifest.json +199 -0
- package/backend/data/runtime-dependency-sources.json +5 -0
- package/backend/executable-discovery.js +7 -0
- package/backend/farming-agent-bootstrap.zh_cn.md +4 -2
- package/backend/farming-app-cli.js +161 -65
- package/backend/farming-cli.js +7 -59
- package/backend/index-html.js +31 -0
- package/backend/main-agent-skills.js +7 -28
- package/backend/npm-update-helper.js +43 -6
- package/backend/provider-adapters.js +10 -0
- package/backend/runtime-dependency-manager.js +759 -0
- package/backend/runtime-executable-invocation.js +20 -0
- package/backend/server-process-identity.js +47 -0
- package/backend/server.js +145 -204
- package/backend/slash-command-discovery.js +138 -1
- package/backend/storage-layout.js +30 -0
- package/backend/update-service.js +60 -914
- package/config/farming.deploy.env.example +1 -1
- package/config/farming.install.env.example +5 -1
- package/dist/acp/LICENSE.claude-agent-acp +191 -0
- package/dist/acp/LICENSE.claude-agent-sdk +1 -0
- package/dist/acp/claude-agent-acp-0.59.0.mjs +41606 -0
- package/dist/acp/{codex-acp-1.1.4.js → codex-acp-1.1.4.mjs} +387 -7
- package/dist/assets/{App-CM7Cy-l4.css → App-BKNUILuL.css} +1 -1
- package/dist/assets/App-Z1N7T0eM.js +695 -0
- package/dist/assets/{FileEditorMarkdownPreview-DYOMm-b1.js → FileEditorMarkdownPreview-Cs9cLv8a.js} +1 -1
- package/dist/assets/FileEditorPane-CNtuGjQ3.js +2 -0
- package/dist/assets/IconGlyphs-BClgag6E.js +1 -0
- package/dist/assets/ProjectFilesSection-K3GHuQ4U.js +12 -0
- package/dist/assets/ReviewPage-BRuxFaH5.js +3 -0
- package/dist/assets/code-dark-kLsQLvpf.css +1 -0
- package/dist/assets/{index-BzQO3pUr.js → index-D-zetSol.js} +2 -2
- package/dist/assets/main-DUTz-Rd2.css +1 -0
- package/dist/assets/{workspace-editor-monaco-tXA-udGx.js → workspace-editor-monaco-CJNwbpCy.js} +1 -1
- package/dist/assets/workspace-editor-monaco-CO9w8YV0.js +1 -0
- package/dist/assets/workspace-view-state-BjKPYp0E.js +7 -0
- package/dist/index.html +33 -3
- package/extensions/browser/backend/agent-browser-runtime.js +1041 -0
- package/extensions/browser/backend/browser-mcp-server.js +352 -0
- package/extensions/browser/backend/browser-resource-manager.js +921 -114
- package/extensions/browser/backend/browser-resource-store.js +47 -6
- package/extensions/browser/backend/browser-router.js +15 -2
- package/extensions/browser/backend/executable-discovery.js +164 -17
- package/extensions/browser/backend/managed-chromium-installer.js +593 -0
- package/extensions/browser/bin/farming-browser +384 -30
- package/extensions/browser/frontend/BrowserSidebarPortals.tsx +17 -19
- package/extensions/browser/frontend/BrowserViewer.tsx +217 -46
- package/extensions/browser/frontend/browser.css +68 -17
- package/extensions/browser/frontend/types.ts +13 -0
- package/extensions/browser/frontend/useBrowserResources.ts +18 -3
- package/index.html +32 -2
- package/package.json +10 -6
- package/shared/backend-connection-status.d.ts +12 -0
- package/shared/backend-connection-status.js +35 -4
- package/backend/agent-activity.js +0 -35
- package/backend/agent-memory-report.js +0 -583
- package/backend/bundle-update-helper.js +0 -113
- package/backend/codex-rollout-follower.js +0 -326
- package/dist/assets/App-CPG4WcjO.js +0 -658
- package/dist/assets/FileEditorPane-DXXq82aM.js +0 -2
- package/dist/assets/IconGlyphs-DrdPjYXr.js +0 -1
- package/dist/assets/ProjectFilesSection-CP66XVPl.js +0 -12
- package/dist/assets/ReviewPage-w21MEPD6.js +0 -3
- package/dist/assets/code-dark-BDjwj7Um.css +0 -1
- package/dist/assets/main-BlVUXfxa.css +0 -1
- package/dist/assets/workspace-editor-monaco-CFEfFNcA.js +0 -1
- package/dist/assets/workspace-view-state-BS_V8JVs.js +0 -7
- package/extensions/browser/backend/cdp-browser-runtime.js +0 -648
- package/extensions/browser/backend/cdp-client.js +0 -167
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { ArrowLeftGlyph, ArrowRightGlyph, CopyGlyph, SquareGlyph } from '@/components/IconGlyphs'
|
|
2
|
+
import { ArrowLeftGlyph, ArrowRightGlyph, BackToAgentGlyph, CopyGlyph, SquareGlyph } from '@/components/IconGlyphs'
|
|
3
3
|
import { appPath } from '@/lib/base-path'
|
|
4
4
|
import type { UiPreferences } from '@/lib/ui-preferences'
|
|
5
5
|
import type { BrowserResource } from './types'
|
|
@@ -15,18 +15,20 @@ function viewerCopy(language: UiPreferences['language']) {
|
|
|
15
15
|
const zh = language === 'zh'
|
|
16
16
|
return {
|
|
17
17
|
back: zh ? '后退' : 'Back',
|
|
18
|
+
backToAgent: zh ? '返回 Agent' : 'Back to Agent',
|
|
18
19
|
forward: zh ? '前进' : 'Forward',
|
|
19
20
|
reload: zh ? '重新加载' : 'Reload',
|
|
20
21
|
address: zh ? '浏览器地址' : 'Browser address',
|
|
21
22
|
connected: zh ? 'Viewer 已连接' : 'Viewer connected',
|
|
22
23
|
disconnected: zh ? 'Viewer 未连接' : 'Viewer disconnected',
|
|
23
24
|
copyLink: zh ? '复制链接' : 'Copy link',
|
|
25
|
+
more: zh ? '更多' : 'More',
|
|
24
26
|
start: zh ? '启动' : 'Start',
|
|
25
27
|
stop: zh ? '停止' : 'Stop',
|
|
26
|
-
startBrowser: zh ? '
|
|
27
|
-
stoppedTitle: zh ? '
|
|
28
|
-
failedTitle: zh ? '
|
|
29
|
-
stoppedHint: zh ? '启动后,用户和 Agent
|
|
28
|
+
startBrowser: zh ? '启动标签页' : 'Start Tab',
|
|
29
|
+
stoppedTitle: zh ? '标签页已停止' : 'Tab stopped',
|
|
30
|
+
failedTitle: zh ? '标签页失败' : 'Tab failed',
|
|
31
|
+
stoppedHint: zh ? '启动后,用户和 Agent 将操作同一个标签页。' : 'Start it to share one tab between the user and Agent.',
|
|
30
32
|
pageLabel: (name: string) => zh ? `${name} 浏览器页面` : `${name} browser page`,
|
|
31
33
|
textInput: zh ? '浏览器文本输入' : 'Browser text input',
|
|
32
34
|
viewerFailed: zh ? 'Browser Viewer 失败' : 'Browser Viewer failed',
|
|
@@ -52,29 +54,125 @@ function PlayGlyph() {
|
|
|
52
54
|
)
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function MoreGlyph() {
|
|
58
|
+
return (
|
|
59
|
+
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
|
|
60
|
+
<circle cx="3" cy="8" r="1.1" />
|
|
61
|
+
<circle cx="8" cy="8" r="1.1" />
|
|
62
|
+
<circle cx="13" cy="8" r="1.1" />
|
|
63
|
+
</svg>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
export function BrowserViewer({
|
|
56
68
|
resource,
|
|
57
69
|
controller,
|
|
58
70
|
language,
|
|
59
71
|
onResource,
|
|
72
|
+
onOpenResource,
|
|
73
|
+
onBackToAgent,
|
|
60
74
|
}: {
|
|
61
75
|
resource: BrowserResource
|
|
62
76
|
controller: BrowserResourcesController
|
|
63
77
|
language: UiPreferences['language']
|
|
64
78
|
onResource: (resource: BrowserResource) => void
|
|
79
|
+
onOpenResource: (resource: BrowserResource) => void
|
|
80
|
+
onBackToAgent: () => void
|
|
65
81
|
}) {
|
|
66
82
|
const copy = viewerCopy(language)
|
|
83
|
+
const viewportRef = useRef<HTMLDivElement>(null)
|
|
67
84
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
85
|
+
const addressInputRef = useRef<HTMLInputElement>(null)
|
|
86
|
+
const addressEditingRef = useRef(false)
|
|
68
87
|
const textInputRef = useRef<HTMLTextAreaElement>(null)
|
|
69
88
|
const composingTextRef = useRef(false)
|
|
70
89
|
const socketRef = useRef<WebSocket | null>(null)
|
|
90
|
+
const moreButtonRef = useRef<HTMLButtonElement>(null)
|
|
91
|
+
const moreMenuRef = useRef<HTMLDivElement>(null)
|
|
71
92
|
const imageSequenceRef = useRef(0)
|
|
72
93
|
const paintFrameRef = useRef<number | null>(null)
|
|
94
|
+
const resizeFrameRef = useRef<number | null>(null)
|
|
95
|
+
const frameViewportRef = useRef<{ width: number; height: number } | null>(null)
|
|
73
96
|
const [address, setAddress] = useState(resource.url)
|
|
74
97
|
const [connected, setConnected] = useState(false)
|
|
98
|
+
const [navigating, setNavigating] = useState(false)
|
|
75
99
|
const [viewerError, setViewerError] = useState('')
|
|
100
|
+
const [moreOpen, setMoreOpen] = useState(false)
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!addressEditingRef.current) setAddress(resource.url)
|
|
104
|
+
}, [resource.url])
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!moreOpen) return undefined
|
|
108
|
+
const closeOnPointerDown = (event: PointerEvent) => {
|
|
109
|
+
if (
|
|
110
|
+
event.target instanceof Node
|
|
111
|
+
&& (moreMenuRef.current?.contains(event.target) || moreButtonRef.current?.contains(event.target))
|
|
112
|
+
) return
|
|
113
|
+
setMoreOpen(false)
|
|
114
|
+
}
|
|
115
|
+
const closeOnKeydown = (event: KeyboardEvent) => {
|
|
116
|
+
if (event.key !== 'Escape') return
|
|
117
|
+
event.preventDefault()
|
|
118
|
+
setMoreOpen(false)
|
|
119
|
+
moreButtonRef.current?.focus()
|
|
120
|
+
}
|
|
121
|
+
window.addEventListener('pointerdown', closeOnPointerDown, true)
|
|
122
|
+
window.addEventListener('keydown', closeOnKeydown, true)
|
|
123
|
+
return () => {
|
|
124
|
+
window.removeEventListener('pointerdown', closeOnPointerDown, true)
|
|
125
|
+
window.removeEventListener('keydown', closeOnKeydown, true)
|
|
126
|
+
}
|
|
127
|
+
}, [moreOpen])
|
|
128
|
+
|
|
129
|
+
const sendViewerSize = useCallback((socket = socketRef.current, claim = false) => {
|
|
130
|
+
const viewport = viewportRef.current
|
|
131
|
+
if (!viewport || !socket || socket.readyState !== WebSocket.OPEN) return
|
|
132
|
+
const width = Math.round(viewport.clientWidth)
|
|
133
|
+
const height = Math.round(viewport.clientHeight)
|
|
134
|
+
if (width <= 0 || height <= 0) return
|
|
135
|
+
socket.send(JSON.stringify({
|
|
136
|
+
type: 'resize',
|
|
137
|
+
generation: resource.generation,
|
|
138
|
+
width,
|
|
139
|
+
height,
|
|
140
|
+
deviceScaleFactor: Math.max(1, Math.min(2, window.devicePixelRatio || 1)),
|
|
141
|
+
claim,
|
|
142
|
+
}))
|
|
143
|
+
}, [resource.generation])
|
|
76
144
|
|
|
77
|
-
useEffect(() =>
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const viewport = viewportRef.current
|
|
147
|
+
if (!viewport) return undefined
|
|
148
|
+
const observer = new ResizeObserver(() => {
|
|
149
|
+
if (resizeFrameRef.current !== null) window.cancelAnimationFrame(resizeFrameRef.current)
|
|
150
|
+
resizeFrameRef.current = window.requestAnimationFrame(() => {
|
|
151
|
+
resizeFrameRef.current = null
|
|
152
|
+
sendViewerSize()
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
observer.observe(viewport)
|
|
156
|
+
return () => {
|
|
157
|
+
observer.disconnect()
|
|
158
|
+
if (resizeFrameRef.current !== null) window.cancelAnimationFrame(resizeFrameRef.current)
|
|
159
|
+
resizeFrameRef.current = null
|
|
160
|
+
}
|
|
161
|
+
}, [sendViewerSize])
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
const claimViewport = () => {
|
|
165
|
+
if (document.visibilityState === 'visible' && document.hasFocus()) {
|
|
166
|
+
sendViewerSize(undefined, true)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
window.addEventListener('focus', claimViewport)
|
|
170
|
+
document.addEventListener('visibilitychange', claimViewport)
|
|
171
|
+
return () => {
|
|
172
|
+
window.removeEventListener('focus', claimViewport)
|
|
173
|
+
document.removeEventListener('visibilitychange', claimViewport)
|
|
174
|
+
}
|
|
175
|
+
}, [sendViewerSize])
|
|
78
176
|
|
|
79
177
|
useEffect(() => {
|
|
80
178
|
if (resource.status !== 'running') {
|
|
@@ -92,14 +190,22 @@ export function BrowserViewer({
|
|
|
92
190
|
socket.onopen = () => {
|
|
93
191
|
setConnected(true)
|
|
94
192
|
setViewerError('')
|
|
193
|
+
sendViewerSize(socket, document.visibilityState === 'visible' && document.hasFocus())
|
|
95
194
|
}
|
|
96
195
|
socket.onmessage = event => {
|
|
97
196
|
const message = JSON.parse(String(event.data)) as {
|
|
98
197
|
type: string
|
|
99
198
|
data?: string
|
|
199
|
+
format?: 'jpeg' | 'png'
|
|
200
|
+
viewport?: { width: number; height: number }
|
|
100
201
|
resource?: BrowserResource
|
|
101
202
|
message?: string
|
|
102
203
|
}
|
|
204
|
+
if (message.type === 'browser-tab-opened' && message.resource) {
|
|
205
|
+
onResource(message.resource)
|
|
206
|
+
onOpenResource(message.resource)
|
|
207
|
+
return
|
|
208
|
+
}
|
|
103
209
|
if (message.type === 'browser-state' && message.resource) {
|
|
104
210
|
onResource(message.resource)
|
|
105
211
|
return
|
|
@@ -120,12 +226,20 @@ export function BrowserViewer({
|
|
|
120
226
|
const canvas = canvasRef.current
|
|
121
227
|
const context = canvas?.getContext('2d')
|
|
122
228
|
if (!canvas || !context) return
|
|
229
|
+
const frameViewport = message.viewport
|
|
230
|
+
&& Number.isFinite(message.viewport.width)
|
|
231
|
+
&& Number.isFinite(message.viewport.height)
|
|
232
|
+
? message.viewport
|
|
233
|
+
: { width: image.naturalWidth, height: image.naturalHeight }
|
|
234
|
+
frameViewportRef.current = frameViewport
|
|
123
235
|
canvas.width = image.naturalWidth
|
|
124
236
|
canvas.height = image.naturalHeight
|
|
237
|
+
canvas.style.width = `${frameViewport.width}px`
|
|
238
|
+
canvas.style.height = `${frameViewport.height}px`
|
|
125
239
|
context.drawImage(image, 0, 0)
|
|
126
240
|
})
|
|
127
241
|
}
|
|
128
|
-
image.src = `data:image
|
|
242
|
+
image.src = `data:image/${message.format === 'png' ? 'png' : 'jpeg'};base64,${message.data}`
|
|
129
243
|
}
|
|
130
244
|
socket.onclose = () => {
|
|
131
245
|
if (socketRef.current === socket) socketRef.current = null
|
|
@@ -143,7 +257,7 @@ export function BrowserViewer({
|
|
|
143
257
|
socketRef.current?.close()
|
|
144
258
|
socketRef.current = null
|
|
145
259
|
}
|
|
146
|
-
}, [copy.connectionFailed, copy.viewerFailed, onResource, resource.id, resource.status])
|
|
260
|
+
}, [copy.connectionFailed, copy.viewerFailed, onOpenResource, onResource, resource.id, resource.status, sendViewerSize])
|
|
147
261
|
|
|
148
262
|
const send = useCallback((message: Record<string, unknown>) => {
|
|
149
263
|
const socket = socketRef.current
|
|
@@ -158,27 +272,37 @@ export function BrowserViewer({
|
|
|
158
272
|
}) => {
|
|
159
273
|
const canvas = event.currentTarget
|
|
160
274
|
const bounds = canvas.getBoundingClientRect()
|
|
275
|
+
const frameViewport = frameViewportRef.current
|
|
161
276
|
return {
|
|
162
|
-
x: (event.clientX - bounds.left) * canvas.width / Math.max(1, bounds.width),
|
|
163
|
-
y: (event.clientY - bounds.top) * canvas.height / Math.max(1, bounds.height),
|
|
277
|
+
x: (event.clientX - bounds.left) * (frameViewport?.width || canvas.width) / Math.max(1, bounds.width),
|
|
278
|
+
y: (event.clientY - bounds.top) * (frameViewport?.height || canvas.height) / Math.max(1, bounds.height),
|
|
164
279
|
}
|
|
165
280
|
}
|
|
166
281
|
const navigate = async () => {
|
|
282
|
+
const submittedAddress = (addressInputRef.current?.value ?? address).trim()
|
|
283
|
+
setNavigating(true)
|
|
284
|
+
setViewerError('')
|
|
167
285
|
try {
|
|
168
|
-
const normalized = /^https?:\/\//i.test(address) || address === 'about:blank' ? address : `http://${address}`
|
|
169
286
|
const response = await fetch(appPath(`/api/browsers/${encodeURIComponent(resource.id)}/navigate`), {
|
|
170
287
|
method: 'POST',
|
|
171
288
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
172
|
-
body: JSON.stringify({ url:
|
|
289
|
+
body: JSON.stringify({ url: submittedAddress }),
|
|
173
290
|
})
|
|
174
291
|
const next = await response.json() as BrowserResource & { error?: string }
|
|
175
292
|
if (!response.ok) throw new Error(next.error || copy.navigationFailed)
|
|
293
|
+
addressEditingRef.current = false
|
|
294
|
+
setAddress(next.url)
|
|
295
|
+
setViewerError('')
|
|
176
296
|
onResource(next)
|
|
177
297
|
} catch (error) {
|
|
178
298
|
setViewerError(error instanceof Error ? error.message : copy.navigationFailed)
|
|
299
|
+
} finally {
|
|
300
|
+
setNavigating(false)
|
|
179
301
|
}
|
|
180
302
|
}
|
|
181
303
|
const browserAction = async (kind: 'back' | 'forward' | 'reload') => {
|
|
304
|
+
setNavigating(true)
|
|
305
|
+
setViewerError('')
|
|
182
306
|
try {
|
|
183
307
|
const response = await fetch(appPath(`/api/browsers/${encodeURIComponent(resource.id)}/action`), {
|
|
184
308
|
method: 'POST',
|
|
@@ -187,58 +311,104 @@ export function BrowserViewer({
|
|
|
187
311
|
})
|
|
188
312
|
const next = await response.json() as BrowserResource & { error?: string }
|
|
189
313
|
if (!response.ok) throw new Error(next.error || `Browser ${kind} failed`)
|
|
314
|
+
setViewerError('')
|
|
190
315
|
onResource(next)
|
|
191
316
|
} catch (error) {
|
|
192
317
|
setViewerError(error instanceof Error ? error.message : `Browser ${kind} failed`)
|
|
318
|
+
} finally {
|
|
319
|
+
setNavigating(false)
|
|
193
320
|
}
|
|
194
321
|
}
|
|
195
322
|
|
|
196
323
|
return (
|
|
197
324
|
<section className="farming-browser-viewer" data-testid="farming-browser-viewer">
|
|
198
325
|
<header className="farming-browser-toolbar">
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
className="farming-browser-toolbar-icon farming-browser-agent-return"
|
|
329
|
+
aria-label={copy.backToAgent}
|
|
330
|
+
title={copy.backToAgent}
|
|
331
|
+
onClick={onBackToAgent}
|
|
332
|
+
>
|
|
333
|
+
<BackToAgentGlyph />
|
|
334
|
+
</button>
|
|
335
|
+
<span className="farming-browser-toolbar-separator" aria-hidden="true" />
|
|
199
336
|
<button type="button" className="farming-browser-toolbar-icon" aria-label={copy.back} title={copy.back} disabled={resource.status !== 'running'} onClick={() => void browserAction('back')}><ArrowLeftGlyph /></button>
|
|
200
337
|
<button type="button" className="farming-browser-toolbar-icon" aria-label={copy.forward} title={copy.forward} disabled={resource.status !== 'running'} onClick={() => void browserAction('forward')}><ArrowRightGlyph /></button>
|
|
201
338
|
<button type="button" className="farming-browser-toolbar-icon" aria-label={copy.reload} title={copy.reload} disabled={resource.status !== 'running'} onClick={() => void browserAction('reload')}><ReloadGlyph /></button>
|
|
202
|
-
<form onSubmit={event => {
|
|
339
|
+
<form aria-busy={navigating} onSubmit={event => {
|
|
203
340
|
event.preventDefault()
|
|
204
341
|
void navigate()
|
|
205
342
|
}}>
|
|
206
343
|
<input
|
|
344
|
+
ref={addressInputRef}
|
|
207
345
|
value={address}
|
|
208
346
|
aria-label={copy.address}
|
|
209
347
|
disabled={resource.status !== 'running'}
|
|
210
|
-
onChange={event =>
|
|
348
|
+
onChange={event => {
|
|
349
|
+
addressEditingRef.current = true
|
|
350
|
+
setAddress(event.currentTarget.value)
|
|
351
|
+
}}
|
|
352
|
+
onKeyDown={event => {
|
|
353
|
+
if (event.key !== 'Enter' || event.nativeEvent.isComposing) return
|
|
354
|
+
event.preventDefault()
|
|
355
|
+
void navigate()
|
|
356
|
+
}}
|
|
357
|
+
onBlur={() => {
|
|
358
|
+
addressEditingRef.current = false
|
|
359
|
+
setAddress(resource.url)
|
|
360
|
+
}}
|
|
211
361
|
/>
|
|
362
|
+
<span className={`farming-browser-connection ${connected ? 'connected' : ''} ${navigating ? 'navigating' : ''}`} title={connected ? copy.connected : copy.disconnected} />
|
|
212
363
|
</form>
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
364
|
+
<div className="farming-browser-more-wrap">
|
|
365
|
+
<button
|
|
366
|
+
ref={moreButtonRef}
|
|
367
|
+
type="button"
|
|
368
|
+
className="farming-browser-toolbar-icon"
|
|
369
|
+
aria-label={copy.more}
|
|
370
|
+
title={copy.more}
|
|
371
|
+
aria-haspopup="menu"
|
|
372
|
+
aria-expanded={moreOpen}
|
|
373
|
+
onClick={() => setMoreOpen(current => !current)}
|
|
374
|
+
>
|
|
375
|
+
<MoreGlyph />
|
|
376
|
+
</button>
|
|
377
|
+
{moreOpen ? (
|
|
378
|
+
<div ref={moreMenuRef} className="farming-browser-more-menu" role="menu">
|
|
379
|
+
<button
|
|
380
|
+
type="button"
|
|
381
|
+
role="menuitem"
|
|
382
|
+
onClick={() => {
|
|
383
|
+
setMoreOpen(false)
|
|
384
|
+
const stableUrl = new URL(window.location.href)
|
|
385
|
+
stableUrl.searchParams.set('browser', resource.id)
|
|
386
|
+
void navigator.clipboard.writeText(stableUrl.href)
|
|
387
|
+
}}
|
|
388
|
+
>
|
|
389
|
+
<CopyGlyph />
|
|
390
|
+
<span>{copy.copyLink}</span>
|
|
391
|
+
</button>
|
|
392
|
+
<button
|
|
393
|
+
type="button"
|
|
394
|
+
role="menuitem"
|
|
395
|
+
disabled={resource.status === 'starting' || resource.status === 'stopping'}
|
|
396
|
+
onClick={() => {
|
|
397
|
+
setMoreOpen(false)
|
|
398
|
+
const transition = resource.status === 'running'
|
|
399
|
+
? controller.stop(resource.id)
|
|
400
|
+
: controller.start(resource.id)
|
|
401
|
+
void transition.catch(error => setViewerError(error instanceof Error ? error.message : copy.transitionFailed))
|
|
402
|
+
}}
|
|
403
|
+
>
|
|
404
|
+
{resource.status === 'running' ? <SquareGlyph /> : <PlayGlyph />}
|
|
405
|
+
<span>{resource.status === 'running' ? copy.stop : copy.start}</span>
|
|
406
|
+
</button>
|
|
407
|
+
</div>
|
|
408
|
+
) : null}
|
|
409
|
+
</div>
|
|
240
410
|
</header>
|
|
241
|
-
<div className="farming-browser-viewport">
|
|
411
|
+
<div ref={viewportRef} className="farming-browser-viewport">
|
|
242
412
|
{resource.status === 'running' ? (
|
|
243
413
|
<canvas
|
|
244
414
|
ref={canvasRef}
|
|
@@ -253,9 +423,7 @@ export function BrowserViewer({
|
|
|
253
423
|
event.currentTarget.setPointerCapture(event.pointerId)
|
|
254
424
|
const position = point(event)
|
|
255
425
|
send({ type: 'pointer', action: 'down', button: event.button === 2 ? 'right' : event.button === 1 ? 'middle' : 'left', ...position })
|
|
256
|
-
|
|
257
|
-
window.requestAnimationFrame(() => textInputRef.current?.focus({ preventScroll: true }))
|
|
258
|
-
}
|
|
426
|
+
window.requestAnimationFrame(() => textInputRef.current?.focus({ preventScroll: true }))
|
|
259
427
|
}}
|
|
260
428
|
onPointerUp={event => {
|
|
261
429
|
const position = point(event)
|
|
@@ -306,8 +474,11 @@ export function BrowserViewer({
|
|
|
306
474
|
onCompositionStart={() => {
|
|
307
475
|
composingTextRef.current = true
|
|
308
476
|
}}
|
|
309
|
-
onCompositionEnd={
|
|
477
|
+
onCompositionEnd={event => {
|
|
310
478
|
composingTextRef.current = false
|
|
479
|
+
const text = event.currentTarget.value
|
|
480
|
+
if (text) send({ type: 'text', text })
|
|
481
|
+
event.currentTarget.value = ''
|
|
311
482
|
}}
|
|
312
483
|
onInput={event => {
|
|
313
484
|
if (composingTextRef.current) return
|
|
@@ -260,13 +260,13 @@
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
.farming-browser-toolbar {
|
|
263
|
-
min-height:
|
|
263
|
+
min-height: 38px;
|
|
264
264
|
display: flex;
|
|
265
265
|
align-items: center;
|
|
266
|
-
gap:
|
|
267
|
-
padding: 4px
|
|
266
|
+
gap: 2px;
|
|
267
|
+
padding: 4px 6px;
|
|
268
268
|
border-bottom: 1px solid rgba(36, 41, 47, 0.08);
|
|
269
|
-
background: #
|
|
269
|
+
background: #ffffff;
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
.farming-browser-toolbar button,
|
|
@@ -314,10 +314,24 @@
|
|
|
314
314
|
padding: 0 !important;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
.farming-browser-toolbar .farming-browser-agent-return {
|
|
318
|
+
color: #454d44;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.farming-browser-toolbar-separator {
|
|
322
|
+
width: 1px;
|
|
323
|
+
height: 18px;
|
|
324
|
+
flex: 0 0 1px;
|
|
325
|
+
margin: 0 4px 0 2px;
|
|
326
|
+
background: rgba(36, 41, 47, 0.1);
|
|
327
|
+
}
|
|
328
|
+
|
|
317
329
|
.farming-browser-toolbar form {
|
|
318
330
|
min-width: 90px;
|
|
331
|
+
position: relative;
|
|
319
332
|
flex: 1 1 auto;
|
|
320
333
|
display: flex;
|
|
334
|
+
margin-left: 3px;
|
|
321
335
|
}
|
|
322
336
|
|
|
323
337
|
.farming-browser-toolbar input {
|
|
@@ -325,10 +339,10 @@
|
|
|
325
339
|
min-width: 0;
|
|
326
340
|
height: 28px;
|
|
327
341
|
box-sizing: border-box;
|
|
328
|
-
padding: 0
|
|
329
|
-
border: 1px solid
|
|
330
|
-
border-radius:
|
|
331
|
-
background:
|
|
342
|
+
padding: 0 25px 0 10px;
|
|
343
|
+
border: 1px solid transparent;
|
|
344
|
+
border-radius: 7px;
|
|
345
|
+
background: #f1f3ef;
|
|
332
346
|
color: #343a34;
|
|
333
347
|
font: inherit;
|
|
334
348
|
font-size: 12px;
|
|
@@ -344,7 +358,10 @@
|
|
|
344
358
|
width: 7px;
|
|
345
359
|
height: 7px;
|
|
346
360
|
flex: 0 0 7px;
|
|
347
|
-
|
|
361
|
+
position: absolute;
|
|
362
|
+
top: 50%;
|
|
363
|
+
right: 9px;
|
|
364
|
+
margin-top: -3.5px;
|
|
348
365
|
border-radius: 999px;
|
|
349
366
|
background: #9aa198;
|
|
350
367
|
}
|
|
@@ -353,6 +370,10 @@
|
|
|
353
370
|
background: #2da44e;
|
|
354
371
|
}
|
|
355
372
|
|
|
373
|
+
.farming-browser-connection.navigating {
|
|
374
|
+
background: #d29922;
|
|
375
|
+
}
|
|
376
|
+
|
|
356
377
|
.farming-browser-viewport {
|
|
357
378
|
min-height: 0;
|
|
358
379
|
position: relative;
|
|
@@ -360,7 +381,7 @@
|
|
|
360
381
|
align-items: center;
|
|
361
382
|
justify-content: center;
|
|
362
383
|
overflow: hidden;
|
|
363
|
-
background: #
|
|
384
|
+
background: #ffffff;
|
|
364
385
|
}
|
|
365
386
|
|
|
366
387
|
.farming-browser-viewport canvas {
|
|
@@ -370,7 +391,31 @@
|
|
|
370
391
|
background: #ffffff;
|
|
371
392
|
cursor: default;
|
|
372
393
|
outline: none;
|
|
373
|
-
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.farming-browser-more-wrap {
|
|
397
|
+
position: relative;
|
|
398
|
+
flex: 0 0 auto;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.farming-browser-more-menu {
|
|
402
|
+
width: 168px;
|
|
403
|
+
position: absolute;
|
|
404
|
+
z-index: 30;
|
|
405
|
+
top: calc(100% + 5px);
|
|
406
|
+
right: 0;
|
|
407
|
+
padding: 5px;
|
|
408
|
+
border: 1px solid #d8dbd2;
|
|
409
|
+
border-radius: 8px;
|
|
410
|
+
background: #ffffff;
|
|
411
|
+
box-shadow: 0 14px 36px rgba(31, 35, 40, 0.16);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.farming-browser-more-menu button {
|
|
415
|
+
width: 100%;
|
|
416
|
+
justify-content: flex-start;
|
|
417
|
+
min-height: 30px;
|
|
418
|
+
padding: 0 9px;
|
|
374
419
|
}
|
|
375
420
|
|
|
376
421
|
.farming-browser-text-input-proxy {
|
|
@@ -468,7 +513,11 @@ body.code-mode[data-appearance='dark'] .farming-browser-viewer {
|
|
|
468
513
|
|
|
469
514
|
body.code-mode[data-appearance='dark'] .farming-browser-toolbar {
|
|
470
515
|
border-color: #21262d;
|
|
471
|
-
background: #
|
|
516
|
+
background: #0d1117;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
body.code-mode[data-appearance='dark'] .farming-browser-toolbar-separator {
|
|
520
|
+
background: #30363d;
|
|
472
521
|
}
|
|
473
522
|
|
|
474
523
|
body.code-mode[data-appearance='dark'] .farming-browser-toolbar button,
|
|
@@ -488,6 +537,11 @@ body.code-mode[data-appearance='dark'] .farming-browser-viewport {
|
|
|
488
537
|
background: #010409;
|
|
489
538
|
}
|
|
490
539
|
|
|
540
|
+
body.code-mode[data-appearance='dark'] .farming-browser-more-menu {
|
|
541
|
+
border-color: #30363d;
|
|
542
|
+
background: #161b22;
|
|
543
|
+
}
|
|
544
|
+
|
|
491
545
|
body.code-mode[data-appearance='dark'] .farming-browser-placeholder {
|
|
492
546
|
color: #8b949e;
|
|
493
547
|
}
|
|
@@ -506,11 +560,8 @@ body.code-mode[data-appearance='dark'] .farming-browser-placeholder button {
|
|
|
506
560
|
padding-inline: 5px;
|
|
507
561
|
}
|
|
508
562
|
|
|
509
|
-
.farming-browser-toolbar
|
|
510
|
-
|
|
563
|
+
.farming-browser-toolbar form {
|
|
564
|
+
margin-left: 1px;
|
|
511
565
|
}
|
|
512
566
|
|
|
513
|
-
.farming-browser-copy-link {
|
|
514
|
-
display: none !important;
|
|
515
|
-
}
|
|
516
567
|
}
|
|
@@ -21,5 +21,18 @@ export interface BrowserCapability {
|
|
|
21
21
|
enabled: boolean
|
|
22
22
|
available: boolean
|
|
23
23
|
browser: { kind: string; path: string } | null
|
|
24
|
+
selection?: {
|
|
25
|
+
source: 'external-cdp' | 'managed' | 'system'
|
|
26
|
+
executablePath: string
|
|
27
|
+
externalCdpUrl: string
|
|
28
|
+
}
|
|
29
|
+
options?: Array<{ kind: string; path: string }>
|
|
30
|
+
installation: {
|
|
31
|
+
state: 'absent' | 'failed' | 'installing' | 'ready'
|
|
32
|
+
agentBrowserVersion: string
|
|
33
|
+
installedVersion: string
|
|
34
|
+
updateAvailable: boolean
|
|
35
|
+
error: string
|
|
36
|
+
}
|
|
24
37
|
message: string
|
|
25
38
|
}
|
|
@@ -84,6 +84,14 @@ export function useBrowserResources() {
|
|
|
84
84
|
}
|
|
85
85
|
}, [capability?.available, mergeResource])
|
|
86
86
|
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (capability?.installation?.state !== 'installing') return undefined
|
|
89
|
+
const timeout = window.setTimeout(() => {
|
|
90
|
+
setRefreshVersion(version => version + 1)
|
|
91
|
+
}, 2_000)
|
|
92
|
+
return () => window.clearTimeout(timeout)
|
|
93
|
+
}, [capability?.installation?.state, refreshVersion])
|
|
94
|
+
|
|
87
95
|
const create = useCallback(async (workspace: string, options: { name?: string; url?: string } = {}) => {
|
|
88
96
|
const resource = await browserRequest<BrowserResource>('/api/browsers', {
|
|
89
97
|
method: 'POST',
|
|
@@ -115,6 +123,9 @@ export function useBrowserResources() {
|
|
|
115
123
|
return resource
|
|
116
124
|
}, [mergeResource])
|
|
117
125
|
|
|
126
|
+
const start = useCallback((id: string) => transition(id, 'start'), [transition])
|
|
127
|
+
const stop = useCallback((id: string) => transition(id, 'stop'), [transition])
|
|
128
|
+
|
|
118
129
|
const remove = useCallback(async (id: string) => {
|
|
119
130
|
const deletion = await browserRequest<BrowserResourceDeletion>(
|
|
120
131
|
`/api/browsers/${encodeURIComponent(id)}`,
|
|
@@ -123,6 +134,10 @@ export function useBrowserResources() {
|
|
|
123
134
|
setCollection(current => applyBrowserResourceDeletion(current, deletion))
|
|
124
135
|
}, [])
|
|
125
136
|
|
|
137
|
+
const refreshCapability = useCallback(() => {
|
|
138
|
+
setRefreshVersion(version => version + 1)
|
|
139
|
+
}, [])
|
|
140
|
+
|
|
126
141
|
const byWorkspace = useMemo(() => {
|
|
127
142
|
const result = new Map<string, BrowserResource[]>()
|
|
128
143
|
for (const resource of collection.resources) {
|
|
@@ -140,11 +155,11 @@ export function useBrowserResources() {
|
|
|
140
155
|
loading,
|
|
141
156
|
create,
|
|
142
157
|
rename,
|
|
143
|
-
start
|
|
144
|
-
stop
|
|
158
|
+
start,
|
|
159
|
+
stop,
|
|
145
160
|
remove,
|
|
146
161
|
mergeResource,
|
|
147
|
-
refreshCapability
|
|
162
|
+
refreshCapability,
|
|
148
163
|
}
|
|
149
164
|
}
|
|
150
165
|
|
package/index.html
CHANGED
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
2
|
+
<html lang="en" data-appearance-preference="system">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
|
-
<meta name="
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
7
|
+
<meta name="theme-color" content="#ffffff" />
|
|
8
|
+
<style>
|
|
9
|
+
html,
|
|
10
|
+
body {
|
|
11
|
+
background: #ffffff;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
html[data-appearance='dark'],
|
|
15
|
+
html[data-appearance='dark'] body {
|
|
16
|
+
background: #181818;
|
|
17
|
+
color-scheme: dark;
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
20
|
+
<script>
|
|
21
|
+
(() => {
|
|
22
|
+
const root = document.documentElement
|
|
23
|
+
const preference = ['light', 'dark'].includes(root.dataset.appearancePreference)
|
|
24
|
+
? root.dataset.appearancePreference
|
|
25
|
+
: 'system'
|
|
26
|
+
const appearance = preference === 'system'
|
|
27
|
+
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
|
28
|
+
: preference
|
|
29
|
+
root.dataset.appearance = appearance
|
|
30
|
+
document.querySelector('meta[name="color-scheme"]')?.setAttribute('content', appearance)
|
|
31
|
+
document.querySelector('meta[name="theme-color"]')?.setAttribute(
|
|
32
|
+
'content',
|
|
33
|
+
appearance === 'dark' ? '#181818' : '#ffffff',
|
|
34
|
+
)
|
|
35
|
+
})()
|
|
36
|
+
</script>
|
|
7
37
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
8
38
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
9
39
|
<meta name="apple-mobile-web-app-title" content="Farming 2" />
|