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.
Files changed (83) hide show
  1. package/README.md +27 -90
  2. package/README.zh_cn.md +17 -90
  3. package/THIRD_PARTY_NOTICES.md +25 -6
  4. package/backend/acp/packaged-claude-acp.js +13 -0
  5. package/backend/acp/packaged-codex-acp.js +2 -2
  6. package/backend/acp-runtime.js +38 -17
  7. package/backend/acp-session-state.js +78 -88
  8. package/backend/acp-transcript.js +174 -9
  9. package/backend/agent-manager.js +27 -2
  10. package/backend/agent-session-history.js +45 -12
  11. package/backend/codex-session-history.js +1 -1
  12. package/backend/codex-transcript.js +0 -202
  13. package/backend/config-manager.js +44 -16
  14. package/backend/data/LICENSE.agent-browser +201 -0
  15. package/backend/data/runtime-dependency-manifest.json +199 -0
  16. package/backend/data/runtime-dependency-sources.json +5 -0
  17. package/backend/executable-discovery.js +7 -0
  18. package/backend/farming-agent-bootstrap.zh_cn.md +4 -2
  19. package/backend/farming-app-cli.js +161 -65
  20. package/backend/farming-cli.js +7 -59
  21. package/backend/index-html.js +31 -0
  22. package/backend/main-agent-skills.js +7 -28
  23. package/backend/npm-update-helper.js +43 -6
  24. package/backend/provider-adapters.js +10 -0
  25. package/backend/runtime-dependency-manager.js +759 -0
  26. package/backend/runtime-executable-invocation.js +20 -0
  27. package/backend/server-process-identity.js +47 -0
  28. package/backend/server.js +145 -204
  29. package/backend/slash-command-discovery.js +138 -1
  30. package/backend/storage-layout.js +30 -0
  31. package/backend/update-service.js +60 -914
  32. package/config/farming.deploy.env.example +1 -1
  33. package/config/farming.install.env.example +5 -1
  34. package/dist/acp/LICENSE.claude-agent-acp +191 -0
  35. package/dist/acp/LICENSE.claude-agent-sdk +1 -0
  36. package/dist/acp/claude-agent-acp-0.59.0.mjs +41606 -0
  37. package/dist/acp/{codex-acp-1.1.4.js → codex-acp-1.1.4.mjs} +387 -7
  38. package/dist/assets/{App-CM7Cy-l4.css → App-BKNUILuL.css} +1 -1
  39. package/dist/assets/App-Z1N7T0eM.js +695 -0
  40. package/dist/assets/{FileEditorMarkdownPreview-DYOMm-b1.js → FileEditorMarkdownPreview-Cs9cLv8a.js} +1 -1
  41. package/dist/assets/FileEditorPane-CNtuGjQ3.js +2 -0
  42. package/dist/assets/IconGlyphs-BClgag6E.js +1 -0
  43. package/dist/assets/ProjectFilesSection-K3GHuQ4U.js +12 -0
  44. package/dist/assets/ReviewPage-BRuxFaH5.js +3 -0
  45. package/dist/assets/code-dark-kLsQLvpf.css +1 -0
  46. package/dist/assets/{index-BzQO3pUr.js → index-D-zetSol.js} +2 -2
  47. package/dist/assets/main-DUTz-Rd2.css +1 -0
  48. package/dist/assets/{workspace-editor-monaco-tXA-udGx.js → workspace-editor-monaco-CJNwbpCy.js} +1 -1
  49. package/dist/assets/workspace-editor-monaco-CO9w8YV0.js +1 -0
  50. package/dist/assets/workspace-view-state-BjKPYp0E.js +7 -0
  51. package/dist/index.html +33 -3
  52. package/extensions/browser/backend/agent-browser-runtime.js +1041 -0
  53. package/extensions/browser/backend/browser-mcp-server.js +352 -0
  54. package/extensions/browser/backend/browser-resource-manager.js +921 -114
  55. package/extensions/browser/backend/browser-resource-store.js +47 -6
  56. package/extensions/browser/backend/browser-router.js +15 -2
  57. package/extensions/browser/backend/executable-discovery.js +164 -17
  58. package/extensions/browser/backend/managed-chromium-installer.js +593 -0
  59. package/extensions/browser/bin/farming-browser +384 -30
  60. package/extensions/browser/frontend/BrowserSidebarPortals.tsx +17 -19
  61. package/extensions/browser/frontend/BrowserViewer.tsx +217 -46
  62. package/extensions/browser/frontend/browser.css +68 -17
  63. package/extensions/browser/frontend/types.ts +13 -0
  64. package/extensions/browser/frontend/useBrowserResources.ts +18 -3
  65. package/index.html +32 -2
  66. package/package.json +10 -6
  67. package/shared/backend-connection-status.d.ts +12 -0
  68. package/shared/backend-connection-status.js +35 -4
  69. package/backend/agent-activity.js +0 -35
  70. package/backend/agent-memory-report.js +0 -583
  71. package/backend/bundle-update-helper.js +0 -113
  72. package/backend/codex-rollout-follower.js +0 -326
  73. package/dist/assets/App-CPG4WcjO.js +0 -658
  74. package/dist/assets/FileEditorPane-DXXq82aM.js +0 -2
  75. package/dist/assets/IconGlyphs-DrdPjYXr.js +0 -1
  76. package/dist/assets/ProjectFilesSection-CP66XVPl.js +0 -12
  77. package/dist/assets/ReviewPage-w21MEPD6.js +0 -3
  78. package/dist/assets/code-dark-BDjwj7Um.css +0 -1
  79. package/dist/assets/main-BlVUXfxa.css +0 -1
  80. package/dist/assets/workspace-editor-monaco-CFEfFNcA.js +0 -1
  81. package/dist/assets/workspace-view-state-BS_V8JVs.js +0 -7
  82. package/extensions/browser/backend/cdp-browser-runtime.js +0 -648
  83. 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 ? '启动浏览器' : 'Start Browser',
27
- stoppedTitle: zh ? '浏览器已停止' : 'Browser stopped',
28
- failedTitle: zh ? '浏览器失败' : 'Browser failed',
29
- stoppedHint: zh ? '启动后,用户和 Agent 将操作同一个页面。' : 'Start it to share one page between the user and 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(() => setAddress(resource.url), [resource.url])
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/jpeg;base64,${message.data}`
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: normalized }),
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 => setAddress(event.currentTarget.value)}
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
- <span className={`farming-browser-connection ${connected ? 'connected' : ''}`} title={connected ? copy.connected : copy.disconnected} />
214
- <button
215
- type="button"
216
- className="farming-browser-toolbar-action farming-browser-copy-link"
217
- onClick={() => {
218
- const stableUrl = new URL(window.location.href)
219
- stableUrl.searchParams.set('browser', resource.id)
220
- void navigator.clipboard.writeText(stableUrl.href)
221
- }}
222
- >
223
- <CopyGlyph />
224
- <span>{copy.copyLink}</span>
225
- </button>
226
- <button
227
- type="button"
228
- className="farming-browser-toolbar-action"
229
- disabled={resource.status === 'starting' || resource.status === 'stopping'}
230
- onClick={() => {
231
- const transition = resource.status === 'running'
232
- ? controller.stop(resource.id)
233
- : controller.start(resource.id)
234
- void transition.catch(error => setViewerError(error instanceof Error ? error.message : copy.transitionFailed))
235
- }}
236
- >
237
- {resource.status === 'running' ? <SquareGlyph /> : <PlayGlyph />}
238
- <span>{resource.status === 'running' ? copy.stop : copy.start}</span>
239
- </button>
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
- if (event.pointerType !== 'mouse') {
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: 40px;
263
+ min-height: 38px;
264
264
  display: flex;
265
265
  align-items: center;
266
- gap: 4px;
267
- padding: 4px 8px;
266
+ gap: 2px;
267
+ padding: 4px 6px;
268
268
  border-bottom: 1px solid rgba(36, 41, 47, 0.08);
269
- background: #f6f7f3;
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 9px;
329
- border: 1px solid rgba(82, 99, 135, 0.16);
330
- border-radius: 6px;
331
- background: rgba(255, 255, 255, 0.74);
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
- margin: 0 3px;
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: #e9ebe6;
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
- box-shadow: 0 1px 6px rgba(31, 35, 40, 0.1);
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: #161b22;
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-action span {
510
- display: none;
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: (id: string) => transition(id, 'start'),
144
- stop: (id: string) => transition(id, 'stop'),
158
+ start,
159
+ stop,
145
160
  remove,
146
161
  mergeResource,
147
- refreshCapability: () => setRefreshVersion(version => version + 1),
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="theme-color" content="#f8f4e8" />
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" />