Some checks failed
PR Check / check (pull_request) Failing after 33s
New feature: give Studio's AI control of any browser tab to test buttons,
read the console, and report which buttons work / fail.
Backend (internal/api/browser_test.go, ~480 LOC):
- WebSocket endpoint /api/ws/browser-test, auth by single-use 5-min token
- BrowserTestStore: session map (capped at 16, LRU evict), token store
- REST: /api/test/snippet (issues token + JS snippet), /api/test/sessions,
/api/test/console/{id}
- Agent tool 'browser_test' wired into the registry, with actions:
list_clickables / click / eval / console / current_url / type / wait /
summary. click returns the console_delta produced during the click.
- Embedded JS runner: opens WS, hooks console + window.onerror +
unhandledrejection, dispatches dispatcher commands, replies with
correlation IDs, watches for URL changes.
Frontend:
- New Tests tab (web/src/components/Tests.jsx): snippet copy + connected
sessions list + live console viewer
- App.jsx: 5th tab + Ctrl+4 shortcut (Config moves to Ctrl+5)
- api/client.js: getTestSnippet / getTestSessions / getTestConsole
Studio prompt:
- internal/agent/prompts/studio_system.md: added browser_test entry to the
tools table + <browser_test_strategy> section explaining the recommended
loop (summary → list_clickables → click → check console_delta → report)
Versioning:
- v0.6.0 → v0.7.0
- CHANGELOG.md: full entry under v0.7.0
168 lines
8.2 KiB
JavaScript
168 lines
8.2 KiB
JavaScript
const API_BASE = '/api'
|
|
|
|
async function request(path, options = {}) {
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
...options,
|
|
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
|
|
})
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: res.statusText }))
|
|
throw new Error(err.error || res.statusText)
|
|
}
|
|
return res.json()
|
|
}
|
|
|
|
const api = {
|
|
getInfo: () => request('/info'),
|
|
getSystem: () => request('/system'),
|
|
getTools: () => request('/tools'),
|
|
getConfig: () => request('/config'),
|
|
getProviders: () => request('/providers'),
|
|
getSkills: () => request('/skills'),
|
|
getLSP: () => request('/lsp'),
|
|
getMCP: () => request('/mcp'),
|
|
getUpdates: () => request('/updates'),
|
|
getEditors: () => request('/editors'),
|
|
runScan: () => request('/scan', { method: 'POST' }),
|
|
installTools: (tools) => request('/install', { method: 'POST', body: JSON.stringify({ tools }) }),
|
|
configureMCP: () => request('/mcp/configure', { method: 'POST' }),
|
|
configureMCPForEditor: (editor) => request('/mcp/configure', { method: 'POST', body: JSON.stringify({ editor }) }),
|
|
getMCPStatus: () => request('/mcp/status'),
|
|
getMCPRegistry: () => request('/mcp/registry'),
|
|
getLSPHealth: () => request('/lsp/health'),
|
|
autoInstallLSP: (projectDir) => request('/lsp/auto-install', { method: 'POST', body: JSON.stringify({ project_dir: projectDir || '' }) }),
|
|
generateLSPConfig: (editor, names) => request('/lsp/editor-config', { method: 'POST', body: JSON.stringify({ editor, names }) }),
|
|
validateSkill: (name) => request('/skills/validate', { method: 'POST', body: JSON.stringify({ name }) }),
|
|
testSkill: (name, sampleTask) => request('/skills/test', { method: 'POST', body: JSON.stringify({ name, sample_task: sampleTask || '' }) }),
|
|
exportSkill: (name) => request('/skills/export', { method: 'POST', body: JSON.stringify({ name }) }),
|
|
importSkill: (path) => request('/skills/import', { method: 'POST', body: JSON.stringify({ import_path: path }) }),
|
|
deploySkill: (name) => request('/skills/deploy', { method: 'POST', body: JSON.stringify({ name }) }),
|
|
undeploySkill: (name) => request('/skills/undeploy', { method: 'POST', body: JSON.stringify({ name }) }),
|
|
getDashboardStatus: () => request('/dashboard/status'),
|
|
getProvidersQuota: () => request('/providers/quota'),
|
|
getProvidersConsumption: () => request('/providers/consumption'),
|
|
getRecentCommands: () => request('/recent-commands'),
|
|
getRunningProcesses: () => request('/running-processes'),
|
|
getSystemMetrics: () => request('/system/metrics'),
|
|
getTestSnippet: () => request('/test/snippet'),
|
|
getTestSessions: () => request('/test/sessions'),
|
|
getTestConsole: (sessionId) => request(`/test/console/${encodeURIComponent(sessionId || '')}`),
|
|
savePreferences: (prefs) => request('/preferences', { method: 'PUT', body: JSON.stringify(prefs) }),
|
|
saveProfile: (profile) => request('/config/profile', { method: 'PUT', body: JSON.stringify(profile) }),
|
|
saveProvider: (provider) => request('/config/provider', { method: 'PUT', body: JSON.stringify(provider) }),
|
|
resetConfig: () => request('/config/reset', { method: 'POST' }),
|
|
applyStarshipTheme: (theme) => request('/starship/apply-theme', { method: 'POST', body: JSON.stringify({ theme }) }),
|
|
validateProvider: (provider) => request('/providers/validate', { method: 'POST', body: JSON.stringify(provider) }),
|
|
runUpdate: (tool) => request('/update/run', { method: 'POST', body: JSON.stringify({ tool: tool || '' }) }),
|
|
aiTask: (task, tool) => request('/ai/task', { method: 'POST', body: JSON.stringify({ task, tool: tool || '' }) }),
|
|
runCommand: (command, cwd) => request('/terminal', { method: 'POST', body: JSON.stringify({ command, cwd }) }),
|
|
getTerminalSessions: () => request('/terminal/sessions'),
|
|
addSSHConnection: (conn) => request('/terminal/sessions', { method: 'POST', body: JSON.stringify(conn) }),
|
|
deleteSSHConnection: (name) => request(`/terminal/sessions/${encodeURIComponent(name)}`, { method: 'DELETE' }),
|
|
getTerminalThemes: () => request('/terminal/themes'),
|
|
saveTerminalSettings: (settings) => request('/terminal/settings', { method: 'PUT', body: JSON.stringify(settings) }),
|
|
getChatHistory: () => request('/chat/history'),
|
|
clearChat: () => request('/chat/clear', { method: 'POST' }),
|
|
summarizeChat: () => request('/chat/summarize', { method: 'POST' }),
|
|
getShellChatHistory: () => request('/shell/chat/history'),
|
|
clearShellChat: () => request('/shell/chat/clear', { method: 'POST' }),
|
|
analyzeSystem: () => request('/shell/analyze', { method: 'POST' }),
|
|
getShellAnalysis: () => request('/shell/analysis'),
|
|
sendChat: (message, stream = true, onChunk, signal, images = [], advancedReflection = false) => {
|
|
if (!stream) {
|
|
return request('/chat', { method: 'POST', body: JSON.stringify({ message, stream: false, images, advanced_reflection: advancedReflection }) })
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
fetch(`${API_BASE}/chat`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ message, stream: true, images, advanced_reflection: advancedReflection }),
|
|
signal,
|
|
}).then(async (res) => {
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: res.statusText }))
|
|
reject(new Error(err.error || res.statusText))
|
|
return
|
|
}
|
|
const reader = res.body.getReader()
|
|
const decoder = new TextDecoder()
|
|
let full = ''
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
const text = decoder.decode(value, { stream: true })
|
|
for (const line of text.split('\n')) {
|
|
if (!line.startsWith('data: ')) continue
|
|
try {
|
|
const data = JSON.parse(line.slice(6))
|
|
if (data.error) { reject(new Error(data.error)); return }
|
|
if (data.done) { resolve(full); return }
|
|
if (data.content) {
|
|
full += data.content
|
|
if (onChunk) onChunk(full, data)
|
|
} else if (data.thinking !== undefined || data.thinking_end) {
|
|
if (onChunk) onChunk(full, data)
|
|
} else if (data.tool_call || data.tool_result) {
|
|
if (onChunk) onChunk(full, data)
|
|
}
|
|
} catch {}
|
|
}
|
|
}
|
|
resolve(full)
|
|
}).catch(reject)
|
|
})
|
|
},
|
|
sendShellChat: (message, context = {}, stream = true, onChunk, signal) => {
|
|
const payload = {
|
|
message,
|
|
cwd: context.cwd || '',
|
|
platform: context.platform || '',
|
|
stream,
|
|
}
|
|
if (!stream) {
|
|
return request('/shell/chat', { method: 'POST', body: JSON.stringify(payload) })
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
fetch(`${API_BASE}/shell/chat`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
signal,
|
|
}).then(async (res) => {
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: res.statusText }))
|
|
reject(new Error(err.error || res.statusText))
|
|
return
|
|
}
|
|
const reader = res.body.getReader()
|
|
const decoder = new TextDecoder()
|
|
let full = ''
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
const text = decoder.decode(value, { stream: true })
|
|
for (const line of text.split('\n')) {
|
|
if (!line.startsWith('data: ')) continue
|
|
try {
|
|
const data = JSON.parse(line.slice(6))
|
|
if (data.error) { reject(new Error(data.error)); return }
|
|
if (data.done) { resolve({ content: full, tokens: data.tokens }); return }
|
|
if (data.content) {
|
|
full += data.content
|
|
if (onChunk) onChunk(full, data)
|
|
} else if (data.tool_call || data.tool_result) {
|
|
if (onChunk) onChunk(full, data)
|
|
} else if (data.thinking !== undefined || data.thinking_end) {
|
|
if (onChunk) onChunk(full, data)
|
|
}
|
|
} catch {}
|
|
}
|
|
}
|
|
resolve({ content: full })
|
|
}).catch(reject)
|
|
})
|
|
},
|
|
}
|
|
|
|
export default api
|