sparkecoder 0.1.64 → 0.1.66

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 (151) hide show
  1. package/dist/agent/index.d.ts +3 -3
  2. package/dist/agent/index.js +815 -30
  3. package/dist/agent/index.js.map +1 -1
  4. package/dist/cli.js +1042 -165
  5. package/dist/cli.js.map +1 -1
  6. package/dist/db/index.d.ts +2 -2
  7. package/dist/db/index.js.map +1 -1
  8. package/dist/{index-Dn-eCGLe.d.ts → index-Db23cukG.d.ts} +35 -25
  9. package/dist/index.d.ts +5 -5
  10. package/dist/index.js +1058 -146
  11. package/dist/index.js.map +1 -1
  12. package/dist/{schema-XcP0dedO.d.ts → schema-C7Mm4Ykn.d.ts} +3 -3
  13. package/dist/{search-DINnDTgj.d.ts → search-CVVfuBPZ.d.ts} +6 -4
  14. package/dist/server/index.js +1058 -146
  15. package/dist/server/index.js.map +1 -1
  16. package/dist/skills/default/qa.md +317 -0
  17. package/dist/tools/index.d.ts +31 -4
  18. package/dist/tools/index.js +433 -7
  19. package/dist/tools/index.js.map +1 -1
  20. package/package.json +3 -1
  21. package/src/skills/default/qa.md +317 -0
  22. package/web/.next/BUILD_ID +1 -1
  23. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  24. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  25. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  26. package/web/.next/standalone/web/.next/server/app/(main)/page.js.nft.json +1 -1
  27. package/web/.next/standalone/web/.next/server/app/(main)/page_client-reference-manifest.js +1 -1
  28. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page.js.nft.json +1 -1
  29. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  31. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  32. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  34. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  35. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  37. package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  38. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  39. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +2 -2
  40. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  41. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  43. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  46. package/web/.next/standalone/web/.next/server/app/api/config/route.js.nft.json +1 -1
  47. package/web/.next/standalone/web/.next/server/app/api/health/route.js.nft.json +1 -1
  48. package/web/.next/standalone/web/.next/server/app/docs/installation/page_client-reference-manifest.js +1 -1
  49. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  50. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +2 -2
  51. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +2 -2
  52. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +2 -2
  54. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +2 -2
  55. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  56. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  57. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  58. package/web/.next/standalone/web/.next/server/app/docs/page_client-reference-manifest.js +1 -1
  59. package/web/.next/standalone/web/.next/server/app/docs/skills/page_client-reference-manifest.js +1 -1
  60. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  61. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +2 -2
  62. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +2 -2
  63. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  64. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +2 -2
  65. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +2 -2
  66. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  69. package/web/.next/standalone/web/.next/server/app/docs/tools/page_client-reference-manifest.js +1 -1
  70. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  71. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +2 -2
  72. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +2 -2
  73. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  74. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +2 -2
  75. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +2 -2
  76. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  77. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  78. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  79. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  80. package/web/.next/standalone/web/.next/server/app/docs.rsc +2 -2
  81. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +2 -2
  82. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  83. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +2 -2
  84. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +2 -2
  85. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  86. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  87. package/web/.next/standalone/web/.next/server/app/embed/[id]/page.js.nft.json +1 -1
  88. package/web/.next/standalone/web/.next/server/app/embed/[id]/page_client-reference-manifest.js +1 -1
  89. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  90. package/web/.next/standalone/web/.next/server/app/index.rsc +4 -4
  91. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +2 -2
  92. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +2 -2
  93. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
  94. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  95. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
  96. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  97. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_5c78460e._.js → 2374f_02a118f9._.js} +1 -1
  98. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_bfc8ef7d._.js → 2374f_0ed477f8._.js} +1 -1
  99. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_04a544c8._.js → 2374f_12bad06e._.js} +1 -1
  100. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_e366206f._.js → 2374f_2526ca80._.js} +1 -1
  101. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_45534372._.js → 2374f_3b51a934._.js} +1 -1
  102. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_d5f5b9ba._.js → 2374f_3e519469._.js} +1 -1
  103. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_5e9eb6da._.js → 2374f_5ebfcf1a._.js} +1 -1
  104. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_db790cfe._.js → 2374f_a0f483d1._.js} +1 -1
  105. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_c61a33b3._.js → 2374f_acf3dfe4._.js} +1 -1
  106. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_ab5b97d8._.js → 2374f_ad08e83a._.js} +1 -1
  107. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_68abddfe._.js → 2374f_c1d54c16._.js} +1 -1
  108. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_de60e6ea._.js → 2374f_db3e363b._.js} +1 -1
  109. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_72fb9db7._.js → 2374f_f0d7e130._.js} +1 -1
  110. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_5d0b3394._.js → 2374f_fc992d90._.js} +1 -1
  111. package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__aa788b85._.js → [root-of-the-server]__06818a54._.js} +2 -2
  112. package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__d04c460d._.js → [root-of-the-server]__c71f29f9._.js} +4 -4
  113. package/web/.next/standalone/web/.next/server/chunks/ssr/web_2b3a5919._.js +1 -1
  114. package/web/.next/standalone/web/.next/server/chunks/ssr/web_38156da8._.js +1 -1
  115. package/web/.next/standalone/web/.next/server/chunks/ssr/web_5cca707f._.js +7 -0
  116. package/web/.next/standalone/web/.next/server/chunks/ssr/web_935e81f5._.js +7 -0
  117. package/web/.next/standalone/web/.next/server/chunks/ssr/{web_6fb589ac._.js → web_cc5f7515._.js} +2 -2
  118. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  119. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  120. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  121. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  122. package/web/.next/standalone/web/.next/static/chunks/{eea48be65cdb3f4b.js → 31208ade542a0fcb.js} +3 -3
  123. package/web/.next/{static/chunks/054deec0c7b19894.js → standalone/web/.next/static/chunks/4e673433173ad456.js} +3 -3
  124. package/web/.next/standalone/web/.next/static/chunks/{7fb141141caa4fac.js → 515f0c0bd6087843.js} +5 -5
  125. package/web/.next/standalone/web/.next/static/chunks/fd39dd62879495e1.css +1 -0
  126. package/web/.next/{static/chunks/eea48be65cdb3f4b.js → standalone/web/.next/static/static/chunks/31208ade542a0fcb.js} +3 -3
  127. package/web/.next/standalone/web/.next/static/static/chunks/{054deec0c7b19894.js → 4e673433173ad456.js} +3 -3
  128. package/web/.next/standalone/web/.next/static/static/chunks/{7fb141141caa4fac.js → 515f0c0bd6087843.js} +5 -5
  129. package/web/.next/standalone/web/.next/static/static/chunks/fd39dd62879495e1.css +1 -0
  130. package/web/.next/standalone/web/src/components/ai-elements/browser-recording.tsx +100 -0
  131. package/web/.next/standalone/web/src/components/browser-preview.tsx +196 -0
  132. package/web/.next/standalone/web/src/components/chat-interface.tsx +188 -4
  133. package/web/.next/standalone/web/src/lib/api.ts +119 -0
  134. package/web/.next/{standalone/web/.next/static/static/chunks/eea48be65cdb3f4b.js → static/chunks/31208ade542a0fcb.js} +3 -3
  135. package/web/.next/{standalone/web/.next/static/chunks/054deec0c7b19894.js → static/chunks/4e673433173ad456.js} +3 -3
  136. package/web/.next/static/chunks/{7fb141141caa4fac.js → 515f0c0bd6087843.js} +5 -5
  137. package/web/.next/static/chunks/fd39dd62879495e1.css +1 -0
  138. package/web/.next/standalone/web/.next/server/chunks/ssr/web_08bbd8c8._.js +0 -7
  139. package/web/.next/standalone/web/.next/server/chunks/ssr/web_c729ad51._.js +0 -7
  140. package/web/.next/standalone/web/.next/static/chunks/1f42a42914068041.css +0 -1
  141. package/web/.next/standalone/web/.next/static/static/chunks/1f42a42914068041.css +0 -1
  142. package/web/.next/static/chunks/1f42a42914068041.css +0 -1
  143. /package/web/.next/standalone/web/.next/static/{9M2ys377uFtNH8BEy1_vL → UMGGmtMDTCI6fL-AIFkiM}/_buildManifest.js +0 -0
  144. /package/web/.next/standalone/web/.next/static/{9M2ys377uFtNH8BEy1_vL → UMGGmtMDTCI6fL-AIFkiM}/_clientMiddlewareManifest.json +0 -0
  145. /package/web/.next/standalone/web/.next/static/{9M2ys377uFtNH8BEy1_vL → UMGGmtMDTCI6fL-AIFkiM}/_ssgManifest.js +0 -0
  146. /package/web/.next/standalone/web/.next/static/static/{9M2ys377uFtNH8BEy1_vL → UMGGmtMDTCI6fL-AIFkiM}/_buildManifest.js +0 -0
  147. /package/web/.next/standalone/web/.next/static/static/{9M2ys377uFtNH8BEy1_vL → UMGGmtMDTCI6fL-AIFkiM}/_clientMiddlewareManifest.json +0 -0
  148. /package/web/.next/standalone/web/.next/static/static/{9M2ys377uFtNH8BEy1_vL → UMGGmtMDTCI6fL-AIFkiM}/_ssgManifest.js +0 -0
  149. /package/web/.next/static/{9M2ys377uFtNH8BEy1_vL → UMGGmtMDTCI6fL-AIFkiM}/_buildManifest.js +0 -0
  150. /package/web/.next/static/{9M2ys377uFtNH8BEy1_vL → UMGGmtMDTCI6fL-AIFkiM}/_clientMiddlewareManifest.json +0 -0
  151. /package/web/.next/static/{9M2ys377uFtNH8BEy1_vL → UMGGmtMDTCI6fL-AIFkiM}/_ssgManifest.js +0 -0
@@ -112,6 +112,11 @@ import {
112
112
  type Checkpoint,
113
113
  type RunAgentAttachment,
114
114
  type VersionInfo,
115
+ getSessionFiles,
116
+ getBrowserStreamStatus,
117
+ getBrowserRecordings,
118
+ type SessionFile as ApiSessionFile,
119
+ type BrowserRecordingInfo,
115
120
  } from '@/lib/api';
116
121
  import { TodoPanel } from '@/components/ai-elements/todo-panel';
117
122
  import { getConfig, type AppConfig } from '@/lib/config';
@@ -124,7 +129,7 @@ import {
124
129
  SelectTrigger,
125
130
  SelectValue,
126
131
  } from '@/components/ui/select';
127
- import { MessageSquare, Copy, RefreshCw, AlertTriangle, Terminal as TerminalIcon, FileCode, Radio, Pencil, Check, Settings, RotateCcw, FolderOpen, PanelLeft, FileIcon, Download, X, ChevronDown, ChevronUp, Play, ArrowUp, Trash2, Monitor, ListChecks } from 'lucide-react';
132
+ import { MessageSquare, Copy, RefreshCw, AlertTriangle, Terminal as TerminalIcon, FileCode, Radio, Pencil, Check, Settings, RotateCcw, FolderOpen, PanelLeft, FileIcon, Download, X, ChevronDown, ChevronUp, Play, ArrowUp, Trash2, Monitor, ListChecks, MoreVertical, Globe, Video } from 'lucide-react';
128
133
  import { useSidebar } from '@/components/ui/sidebar';
129
134
  import {
130
135
  Dialog,
@@ -136,6 +141,13 @@ import {
136
141
  import { Switch } from '@/components/ui/switch';
137
142
  import { Label } from '@/components/ui/label';
138
143
  import { Input } from '@/components/ui/input';
144
+ import {
145
+ DropdownMenu,
146
+ DropdownMenuContent,
147
+ DropdownMenuItem,
148
+ DropdownMenuSeparator,
149
+ DropdownMenuTrigger,
150
+ } from '@/components/ui/dropdown-menu';
139
151
  import { WriteFileTool, type WriteFileInput, type WriteFileOutput } from '@/components/ai-elements/write-file-tool';
140
152
  import { ReadFileTool, type ReadFileInput, type ReadFileOutput } from '@/components/ai-elements/read-file-tool';
141
153
  import { BashTool, type BashInput, type BashOutput } from '@/components/ai-elements/bash-tool';
@@ -146,6 +158,8 @@ import { LinterTool, type LinterInput, type LinterOutput } from '@/components/ai
146
158
  import { CodeGraphTool, type CodeGraphInput, type CodeGraphOutput } from '@/components/ai-elements/code-graph-tool';
147
159
  import { CompleteTaskTool } from '@/components/ai-elements/complete-task-tool';
148
160
  import { SpeechInput } from '@/components/ai-elements/speech-input';
161
+ import { BrowserPreview } from '@/components/browser-preview';
162
+ import { BrowserRecording } from '@/components/ai-elements/browser-recording';
149
163
  import {
150
164
  MentionInputProvider,
151
165
  MentionTextarea,
@@ -424,6 +438,25 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
424
438
  const [updateBannerDismissed, setUpdateBannerDismissed] = useState(false);
425
439
  const [isCreatingUpdateSession, setIsCreatingUpdateSession] = useState(false);
426
440
 
441
+ // Browser PIP preview state
442
+ const [browserPreviewActive, setBrowserPreviewActive] = useState(false);
443
+ const [browserPreviewFrame, setBrowserPreviewFrame] = useState<string | null>(null);
444
+ const [browserPreviewMetadata, setBrowserPreviewMetadata] = useState<{
445
+ deviceWidth: number;
446
+ deviceHeight: number;
447
+ pageScaleFactor: number;
448
+ offsetTop: number;
449
+ scrollOffsetX: number;
450
+ scrollOffsetY: number;
451
+ } | null>(null);
452
+ const [browserPreviewDismissed, setBrowserPreviewDismissed] = useState(false);
453
+ const browserFrameTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
454
+
455
+ // Session files (GCS-backed recordings, uploads, etc.)
456
+ const [sessionFiles, setSessionFiles] = useState<ApiSessionFile[]>([]);
457
+ const [browserRecordingsOpen, setBrowserRecordingsOpen] = useState(false);
458
+ const [browserRecordings, setBrowserRecordings] = useState<BrowserRecordingInfo[]>([]);
459
+
427
460
  // Load config for available models
428
461
  useEffect(() => {
429
462
  getConfig().then(setConfig);
@@ -947,6 +980,36 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
947
980
  return;
948
981
  }
949
982
 
983
+ // Browser PIP preview events
984
+ if (event.type === 'browser-frame') {
985
+ setBrowserPreviewFrame(event.data);
986
+ setBrowserPreviewMetadata(event.metadata || null);
987
+ if (!browserPreviewDismissed) {
988
+ setBrowserPreviewActive(true);
989
+ }
990
+ if (browserFrameTimeoutRef.current) {
991
+ clearTimeout(browserFrameTimeoutRef.current);
992
+ }
993
+ browserFrameTimeoutRef.current = setTimeout(() => {
994
+ setBrowserPreviewActive(false);
995
+ }, 30000);
996
+ return;
997
+ }
998
+
999
+ if (event.type === 'browser-status') {
1000
+ if (event.connected === false) {
1001
+ if (browserFrameTimeoutRef.current) {
1002
+ clearTimeout(browserFrameTimeoutRef.current);
1003
+ }
1004
+ browserFrameTimeoutRef.current = setTimeout(() => {
1005
+ setBrowserPreviewActive(false);
1006
+ setBrowserPreviewFrame(null);
1007
+ setBrowserPreviewDismissed(false);
1008
+ }, 5000);
1009
+ }
1010
+ return;
1011
+ }
1012
+
950
1013
  switch (event.type) {
951
1014
  case 'start':
952
1015
  // Message started - could use messageId for tracking
@@ -1325,7 +1388,7 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
1325
1388
  setIsWatching(false);
1326
1389
  playDing();
1327
1390
 
1328
- // Only refresh checkpoints (for revert buttons) - NOT chatItems.
1391
+ // Refresh checkpoints and session files (recordings may have been uploaded)
1329
1392
  {
1330
1393
  const finishSessionId = session.id;
1331
1394
  getSessionCheckpoints(finishSessionId)
@@ -1334,6 +1397,16 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
1334
1397
  setCheckpoints(checkpointsData.checkpoints || []);
1335
1398
  })
1336
1399
  .catch(() => {});
1400
+ // Delay file refresh slightly to allow recording upload to complete
1401
+ setTimeout(() => {
1402
+ if (sessionIdRef.current !== finishSessionId) return;
1403
+ getSessionFiles(finishSessionId)
1404
+ .then((files) => {
1405
+ if (sessionIdRef.current !== finishSessionId) return;
1406
+ setSessionFiles(files);
1407
+ })
1408
+ .catch(() => {});
1409
+ }, 3000);
1337
1410
  }
1338
1411
 
1339
1412
  // Send next queued message directly from the finish event.
@@ -1414,12 +1487,17 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
1414
1487
  currentTextRef.current = '';
1415
1488
  currentReasoningRef.current = '';
1416
1489
  toolCallsRef.current = [];
1490
+ setBrowserPreviewActive(false);
1491
+ setBrowserPreviewFrame(null);
1492
+ setBrowserPreviewDismissed(false);
1493
+ setSessionFiles([]);
1417
1494
 
1418
1495
  try {
1419
- // Load existing messages and checkpoints in parallel
1420
- const [apiMessages, checkpointsData] = await Promise.all([
1496
+ // Load existing messages, checkpoints, and session files in parallel
1497
+ const [apiMessages, checkpointsData, filesData] = await Promise.all([
1421
1498
  getSessionMessages(session.id),
1422
1499
  getSessionCheckpoints(session.id).catch(() => ({ checkpoints: [] })),
1500
+ getSessionFiles(session.id).catch(() => []),
1423
1501
  ]);
1424
1502
 
1425
1503
  // Don't update state if session changed during async work
@@ -1431,6 +1509,7 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
1431
1509
  const converted = convertApiMessages(sorted);
1432
1510
  setChatItems(converted);
1433
1511
  setCheckpoints(checkpointsData.checkpoints || []);
1512
+ setSessionFiles(filesData);
1434
1513
 
1435
1514
  // Check if there's an active stream to watch
1436
1515
  const streamInfo = await getActiveStream(session.id);
@@ -2404,6 +2483,16 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
2404
2483
  </TooltipProvider>
2405
2484
  </div>
2406
2485
  <div className="flex items-center gap-2 shrink-0">
2486
+ {/* Browser recordings */}
2487
+ {sessionFiles.filter(f => f.category === 'browser-recording' && f.downloadUrl).map((file) => (
2488
+ <BrowserRecording
2489
+ key={file.id}
2490
+ fileName={file.fileName}
2491
+ downloadUrl={file.downloadUrl!}
2492
+ sizeBytes={file.sizeBytes}
2493
+ createdAt={file.createdAt}
2494
+ />
2495
+ ))}
2407
2496
  {isWatching && (
2408
2497
  <TooltipProvider>
2409
2498
  <Tooltip>
@@ -2451,6 +2540,44 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
2451
2540
  <TooltipContent>Session Settings</TooltipContent>
2452
2541
  </Tooltip>
2453
2542
  </TooltipProvider>
2543
+
2544
+ {/* Browser / More actions menu */}
2545
+ <DropdownMenu>
2546
+ <DropdownMenuTrigger asChild>
2547
+ <Button size="icon" variant="ghost" className="size-7 hover:bg-accent transition-colors">
2548
+ <MoreVertical className="size-4" />
2549
+ </Button>
2550
+ </DropdownMenuTrigger>
2551
+ <DropdownMenuContent align="end" className="w-52">
2552
+ <DropdownMenuItem
2553
+ onClick={() => {
2554
+ setBrowserPreviewDismissed(false);
2555
+ // Check if there's an active browser stream
2556
+ getBrowserStreamStatus(session.id).then((status) => {
2557
+ if (status?.active) {
2558
+ setBrowserPreviewActive(true);
2559
+ console.log('[browser] Reconnecting PIP — stream active');
2560
+ } else {
2561
+ console.log('[browser] No active browser stream');
2562
+ }
2563
+ });
2564
+ }}
2565
+ >
2566
+ <Globe className="size-4 mr-2" />
2567
+ Open Browser PIP
2568
+ </DropdownMenuItem>
2569
+ <DropdownMenuItem
2570
+ onClick={async () => {
2571
+ const recs = await getBrowserRecordings(session.id);
2572
+ setBrowserRecordings(recs);
2573
+ setBrowserRecordingsOpen(true);
2574
+ }}
2575
+ >
2576
+ <Video className="size-4 mr-2" />
2577
+ Browser Recordings
2578
+ </DropdownMenuItem>
2579
+ </DropdownMenuContent>
2580
+ </DropdownMenu>
2454
2581
  </div>
2455
2582
  </div>
2456
2583
  )}
@@ -2531,6 +2658,51 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
2531
2658
  </DialogContent>
2532
2659
  </Dialog>
2533
2660
 
2661
+ {/* Browser Recordings Dialog */}
2662
+ <Dialog open={browserRecordingsOpen} onOpenChange={setBrowserRecordingsOpen}>
2663
+ <DialogContent className="max-w-2xl w-full p-0 overflow-hidden rounded-xl">
2664
+ <DialogHeader className="px-5 pt-5 pb-3">
2665
+ <DialogTitle className="flex items-center gap-2 text-sm font-medium">
2666
+ <div className="flex items-center justify-center size-6 rounded-lg bg-violet-500/10 dark:bg-violet-400/10">
2667
+ <Video className="size-3.5 text-violet-600 dark:text-violet-400" />
2668
+ </div>
2669
+ Browser Recordings
2670
+ </DialogTitle>
2671
+ </DialogHeader>
2672
+ <div className="px-5 pb-5 space-y-3">
2673
+ {browserRecordings.length === 0 ? (
2674
+ <p className="text-sm text-muted-foreground py-4 text-center">No browser recordings for this session</p>
2675
+ ) : (
2676
+ browserRecordings.map((rec) => (
2677
+ <div key={rec.id} className="flex items-center gap-3 p-3 rounded-lg border bg-muted/30 hover:bg-muted/50 transition-colors">
2678
+ <div className="flex items-center justify-center size-10 rounded-lg bg-violet-500/10 dark:bg-violet-400/10 shrink-0">
2679
+ <Play className="size-4 text-violet-600 dark:text-violet-400 ml-0.5" />
2680
+ </div>
2681
+ <div className="min-w-0 flex-1">
2682
+ <p className="text-sm font-medium truncate">{rec.fileName}</p>
2683
+ <p className="text-xs text-muted-foreground">
2684
+ {rec.createdAt && new Date(rec.createdAt).toLocaleString()}
2685
+ {rec.sizeBytes != null && ` · ${rec.sizeBytes > 1024 * 1024 ? `${(rec.sizeBytes / (1024 * 1024)).toFixed(1)} MB` : `${(rec.sizeBytes / 1024).toFixed(0)} KB`}`}
2686
+ </p>
2687
+ </div>
2688
+ {rec.downloadUrl && (
2689
+ <Button
2690
+ size="sm"
2691
+ variant="outline"
2692
+ className="shrink-0"
2693
+ onClick={() => window.open(rec.downloadUrl!, '_blank')}
2694
+ >
2695
+ <Play className="size-3 mr-1" />
2696
+ Watch
2697
+ </Button>
2698
+ )}
2699
+ </div>
2700
+ ))
2701
+ )}
2702
+ </div>
2703
+ </DialogContent>
2704
+ </Dialog>
2705
+
2534
2706
  {/* Update Available Banner - hidden in embed mode */}
2535
2707
  {!isEmbed && versionInfo?.updateAvailable && !updateBannerDismissed && (
2536
2708
  <div className="px-4 py-2 border-b border-amber-500/30 bg-amber-500/10 flex items-center justify-between gap-3">
@@ -3431,6 +3603,18 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
3431
3603
  </MentionInputProvider>
3432
3604
  </div>
3433
3605
  </div>
3606
+
3607
+ {/* Browser PIP preview */}
3608
+ <BrowserPreview
3609
+ sessionId={session.id}
3610
+ frame={browserPreviewFrame}
3611
+ metadata={browserPreviewMetadata}
3612
+ active={browserPreviewActive && !browserPreviewDismissed}
3613
+ onDismiss={() => {
3614
+ setBrowserPreviewDismissed(true);
3615
+ setBrowserPreviewActive(false);
3616
+ }}
3617
+ />
3434
3618
  </div>
3435
3619
  );
3436
3620
  }
@@ -486,11 +486,22 @@ export type SSEEvent =
486
486
  | { type: 'data-stream-id'; streamId: string }
487
487
  | { type: 'data-session'; data: { id: string; name: string; workingDirectory: string; model: string } }
488
488
  | { type: 'data-user-message'; data: { id: string; content: string } }
489
+ | { type: 'browser-frame'; data: string; metadata: BrowserFrameMetadata }
490
+ | { type: 'browser-status'; connected: boolean; screencasting: boolean; viewportWidth?: number; viewportHeight?: number }
489
491
  | { type: 'finish-step' }
490
492
  | { type: 'finish'; finishReason?: string }
491
493
  | { type: 'abort' }
492
494
  | { type: 'error'; errorText: string };
493
495
 
496
+ export interface BrowserFrameMetadata {
497
+ deviceWidth: number;
498
+ deviceHeight: number;
499
+ pageScaleFactor: number;
500
+ offsetTop: number;
501
+ scrollOffsetX: number;
502
+ scrollOffsetY: number;
503
+ }
504
+
494
505
  // Terminal stream events
495
506
  export interface TerminalStreamEvent {
496
507
  type: 'status' | 'stdout' | 'exit';
@@ -848,3 +859,111 @@ export async function getWorkspaceFiles(
848
859
 
849
860
  return res.json();
850
861
  }
862
+
863
+ // ============================================
864
+ // Browser Input API (pair-browsing)
865
+ // ============================================
866
+
867
+ export interface BrowserInputEvent {
868
+ type: 'input_mouse' | 'input_keyboard' | 'input_touch';
869
+ eventType: string;
870
+ x?: number;
871
+ y?: number;
872
+ button?: string;
873
+ clickCount?: number;
874
+ deltaX?: number;
875
+ deltaY?: number;
876
+ key?: string;
877
+ code?: string;
878
+ text?: string;
879
+ modifiers?: number;
880
+ touchPoints?: Array<{ x: number; y: number; id?: number }>;
881
+ }
882
+
883
+ export async function sendBrowserInput(
884
+ sessionId: string,
885
+ event: BrowserInputEvent
886
+ ): Promise<void> {
887
+ await fetch(`${getApiBase()}/agents/${sessionId}/browser-input`, {
888
+ method: 'POST',
889
+ headers: { 'Content-Type': 'application/json' },
890
+ body: JSON.stringify(event),
891
+ });
892
+ }
893
+
894
+ // ============================================
895
+ // Session Files API (GCS-backed)
896
+ // ============================================
897
+
898
+ export interface SessionFile {
899
+ id: string;
900
+ fileName: string;
901
+ contentType: string;
902
+ sizeBytes: number | null;
903
+ category: string;
904
+ createdAt: string;
905
+ downloadUrl: string | null;
906
+ downloadUrlExpiresAt: string | null;
907
+ }
908
+
909
+ export async function getSessionFiles(sessionId: string): Promise<SessionFile[]> {
910
+ const res = await fetch(`${getApiBase()}/sessions/${sessionId}/session-files`);
911
+ if (!res.ok) {
912
+ if (res.status === 404) return [];
913
+ throw new Error(`Failed to get session files: ${res.statusText}`);
914
+ }
915
+ const data = await res.json();
916
+ return data.files || [];
917
+ }
918
+
919
+ export async function getFileDownloadUrl(fileId: string): Promise<{ downloadUrl: string; expiresAt: string }> {
920
+ const res = await fetch(`${getApiBase()}/sessions/files/${fileId}/download`);
921
+ if (!res.ok) {
922
+ throw new Error(`Failed to get download URL: ${res.statusText}`);
923
+ }
924
+ return res.json();
925
+ }
926
+
927
+ // ============================================
928
+ // Browser Recording API
929
+ // ============================================
930
+
931
+ export interface BrowserRecordingInfo {
932
+ id: string;
933
+ fileName: string;
934
+ sizeBytes: number | null;
935
+ createdAt: string;
936
+ downloadUrl: string | null;
937
+ expiresAt: string | null;
938
+ }
939
+
940
+ export async function getBrowserRecordings(sessionId: string): Promise<BrowserRecordingInfo[]> {
941
+ const res = await fetch(`${getApiBase()}/sessions/${sessionId}/browser-recording`);
942
+ if (!res.ok) return [];
943
+ const data = await res.json();
944
+ return data.recordings || [];
945
+ }
946
+
947
+ // ============================================
948
+ // Browser Stream Status API
949
+ // ============================================
950
+
951
+ export interface BrowserStreamStatus {
952
+ sessionId: string;
953
+ active: boolean;
954
+ hasProxy: boolean;
955
+ latestFrame: {
956
+ metadata: { deviceWidth: number; deviceHeight: number };
957
+ timestamp: number;
958
+ } | null;
959
+ }
960
+
961
+ export async function getBrowserStreamStatus(sessionId: string): Promise<BrowserStreamStatus | null> {
962
+ try {
963
+ const res = await fetch(`${getApiBase()}/agents/${sessionId}/browser-stream`);
964
+ if (!res.ok) return null;
965
+ return res.json();
966
+ } catch {
967
+ return null;
968
+ }
969
+ }