fix(agent-manager): clarify browser diagnostics and restore DevTools

This commit is contained in:
marius-kilocode
2026-08-31 13:26:55 +02:00
parent 50d0397d45
commit 71e21a30ec
30 changed files with 240 additions and 87 deletions
+1 -1
View File
@@ -3,4 +3,4 @@
"kilo-code": minor
---
Inspect local applications in Agent Manager with embedded developer tools and review-style element feedback for precise frontend changes.
Preview local applications in Agent Manager with embedded developer tools, grouped diagnostics, and review-style element feedback for precise frontend changes.
@@ -495,14 +495,6 @@ export class BrowserBroker {
entry.response = response.status()
}
})
entry.page.on("requestfailed", (request) => {
const target = request.url()
if (this.allowed(entry, target)) return
entry.state.errors++
entry.state.error = `Blocked browser request: ${new URL(target).origin}`
this.record(entry, entry.state.error)
this.emit(entry.state)
})
entry.page.on("console", (message) => {
const type = message.type()
if (type === "error") entry.state.errors++
@@ -516,8 +508,7 @@ export class BrowserBroker {
})
entry.page.on("popup", (page) => {
entry.state.errors++
entry.state.error = "Blocked browser popup"
this.record(entry, entry.state.error)
this.record(entry, "Blocked browser popup")
this.emit(entry.state)
void page.close().catch((error: unknown) => this.opts.log("Browser popup close failed", error))
})
@@ -536,6 +527,9 @@ export class BrowserBroker {
}
const origin = URL.canParse(target) ? new URL(target).origin : "invalid"
this.opts.log("Blocked browser request", { sessionId: entry.route.sessionId, origin })
entry.state.errors++
this.record(entry, `Blocked browser request: ${origin}`)
this.emit(entry.state)
await route.abort("blockedbyclient")
})
await entry.context.routeWebSocket("**/*", async (socket) => {
@@ -547,7 +541,7 @@ export class BrowserBroker {
const origin = URL.canParse(target) ? new URL(target).origin : "invalid"
this.opts.log("Blocked browser WebSocket", { sessionId: entry.route.sessionId, origin })
entry.state.errors++
entry.state.error = `Blocked browser request: ${origin}`
this.record(entry, `Blocked browser request: ${origin}`)
this.emit(entry.state)
await socket.close({ code: 1008, reason: "Blocked browser origin" })
})
@@ -103,7 +103,6 @@ export class BrowserDevtools {
this.targets.set(browser, target)
const path = `/browser/devtools/${browser}/${secret}`
const query = new URLSearchParams({
can_dock: "false",
ws: `127.0.0.1:${this.port}${path}/connect`,
})
return `http://127.0.0.1:${this.port}${path}/inspector.html?${query}`
@@ -16,6 +16,7 @@ Object.assign(globalThis, {
ResizeObserver: window.ResizeObserver,
Event: window.Event,
MouseEvent: window.MouseEvent,
getComputedStyle: window.getComputedStyle.bind(window),
requestAnimationFrame: window.requestAnimationFrame.bind(window),
cancelAnimationFrame: window.cancelAnimationFrame.bind(window),
})
@@ -42,6 +43,8 @@ const [labels, update] = createSignal<BrowserLabels>({
close: "Close",
inspect: "Select element",
devtoolsTitle: "Developer tools",
diagnostics: "Browser diagnostics",
diagnosticsHint: "Recent events from the automation browser. Security blocks are not console errors.",
empty: "Open a local page",
noSession: "Choose a session",
screenshotAlt: "Preview",
@@ -82,8 +85,11 @@ assert.equal(root.querySelector('[role="alert"]'), null)
await window.happyDOM.waitUntilComplete()
const frame = root.querySelector(".am-browser-frame")
assert.ok(frame)
assert.equal(root.querySelector(".am-browser-site"), null)
receive?.({ type: "state", value: { ...state, logs: ["[info] Updated"] } })
assert.equal(root.querySelector(".am-browser-frame"), frame)
assert.equal(root.querySelector(".am-browser-diagnostics button")?.textContent, "Browser diagnostics")
assert.equal(root.querySelector(".am-browser-console"), null)
;(root.querySelector("button[aria-label=Reload]") as HTMLButtonElement).click()
assert.deepEqual(sent.at(-1), { type: "refresh", scope })
receive?.({ type: "state", value: { ...state, navigation: 1 } })
@@ -135,8 +141,37 @@ assert.equal(references[0]?.selector, "#save")
update((value) => ({ ...value, close: "Fermer" }))
await window.happyDOM.waitUntilComplete()
assert.ok(root.querySelector("button[aria-label=Fermer]"))
const blocked = "Blocked browser request: http://127.0.0.1:4097"
const other = "Blocked browser request: http://127.0.0.1:4098"
receive?.({
type: "state",
value: { ...state, navigation: 1, errors: 5, logs: [blocked, blocked, other, blocked, "[error] Failed to load"] },
})
assert.equal(root.querySelector('[role="alert"]'), null)
assert.equal(root.querySelector(".am-browser-error-count"), null)
assert.ok(root.querySelector(".am-browser-tools-action button[aria-label='Developer tools']"))
const diagnostics = root.querySelector(".am-browser-diagnostics button") as HTMLButtonElement
assert.equal(diagnostics.textContent, "5 errors")
assert.equal(diagnostics.getAttribute("aria-expanded"), "false")
assert.equal(root.querySelector(".am-browser-console"), null)
diagnostics.click()
await window.happyDOM.waitUntilComplete()
assert.equal(diagnostics.getAttribute("aria-expanded"), "true")
const entries = [...root.querySelectorAll(".am-browser-console-entry")]
assert.equal(entries.length, 3, root.querySelector(".am-browser-diagnostics")?.outerHTML)
assert.equal(entries.at(0)?.textContent, `${blocked}×3`)
assert.equal(entries.at(1)?.textContent, other)
assert.equal(entries.at(2)?.textContent, "[error] Failed to load")
assert.equal(root.querySelector(".am-browser-frame"), refreshed)
;(root.querySelector(".am-browser-tools-action button") as HTMLButtonElement).click()
assert.deepEqual(sent.at(-1), { type: "devtools", scope, theme: "light" })
receive?.({ type: "devtools", value: { scope, browserId: state.browserId, url: "about:blank" } })
await window.happyDOM.waitUntilComplete()
assert.ok(root.querySelector(".am-browser-devtools-frame"))
assert.equal(root.querySelectorAll(".am-browser-console-entry").length, 3)
assert.equal(diagnostics.getAttribute("aria-expanded"), "true")
receive?.({ type: "state", value: { ...state, navigation: 2 } })
assert.equal(root.querySelector(".am-browser-diagnostics"), null)
;(root.querySelector("button[aria-label=Fermer]") as HTMLButtonElement).click()
assert.equal(closed, 1)
assert.deepEqual(sent.at(-1), { type: "close", scope })
@@ -284,6 +284,7 @@ describe("BrowserBroker", () => {
const second = await broker.devtools("second", "project", "light")
expect(first.browserId).not.toBe(second.browserId)
expect(first.url).not.toContain(env.KILO_BROWSER_BROKER_TOKEN)
expect(new URL(first.url).searchParams.has("can_dock")).toBe(false)
const frontend = await fetch(first.url)
expect(frontend.status).toBe(200)
expect(await frontend.text()).toContain('<script src="./kilo-bootstrap.js"></script>')
@@ -414,8 +415,9 @@ describe("BrowserBroker", () => {
expect(broker.sessions()).toEqual([])
})
test("keeps browser contexts isolated by session", async () => {
test("keeps browser contexts and HTTP rejection diagnostics isolated by session", async () => {
const contexts: Array<{ close: () => Promise<void> }> = []
const listeners = new Map<string, (value: unknown) => void>()
let routeHandler:
| ((
route: { continue: () => Promise<void>; abort: () => Promise<void> },
@@ -429,7 +431,9 @@ describe("BrowserBroker", () => {
url: () => "http://localhost:3000",
title: async () => "Local app",
screenshot: async () => Buffer.from("jpeg"),
on: (_type: string, _listener: (...args: never[]) => void) => undefined,
on: (type: string, listener: (value: unknown) => void) => {
listeners.set(type, listener)
},
mainFrame: () => undefined,
goto: async () => undefined,
}
@@ -447,6 +451,7 @@ describe("BrowserBroker", () => {
close: async () => undefined,
}
const broker = new BrowserBroker({ log: () => {}, launch: async () => browser })
brokers.push(broker)
await broker.open({ sessionId: "one", directory: "/tmp/project" }, "http://localhost:3000")
await broker.open({ sessionId: "two", directory: "/tmp/project" }, "http://localhost:3000")
expect(contexts).toHaveLength(2)
@@ -456,12 +461,26 @@ describe("BrowserBroker", () => {
{ url: () => "http://example.com" },
)
expect(aborted).toBe(true)
const blocked = broker.get("two")
expect(blocked).toMatchObject({
errors: 1,
logs: ["Blocked browser request: http://example.com"],
error: undefined,
})
listeners.get("requestfailed")?.({ url: () => "http://example.com" })
expect(broker.get("two")).toEqual(blocked)
expect(broker.get("one")).toMatchObject({ errors: 0, logs: [], error: undefined })
aborted = false
await routeHandler!(
{ continue: async () => undefined, abort: async () => void (aborted = true) },
{ url: () => "data:text/html,<script>alert(1)</script>", isNavigationRequest: () => true },
)
expect(aborted).toBe(true)
expect(broker.get("two")).toMatchObject({
errors: 2,
logs: ["Blocked browser request: http://example.com", "Blocked browser request: null"],
error: undefined,
})
await broker.close("one")
expect(broker.get("two")?.status).toBe("ready")
})
@@ -744,7 +763,7 @@ describe("BrowserBroker", () => {
expect(broker.get("feedback", "project")?.logs).toHaveLength(20)
})
test("blocks browser popups instead of opening a second page", async () => {
test("blocks browser popups without replacing navigation failures", async () => {
const listeners = new Map<string, (...args: never[]) => void>()
const page = {
url: () => "http://localhost:3000/",
@@ -755,13 +774,26 @@ describe("BrowserBroker", () => {
},
mainFrame: () => undefined,
goto: async () => undefined,
reload: async () => {
throw new Error("Navigation failed")
},
}
const broker = fixture(page)
await broker.open({ sessionId: "popup", directory: "/tmp/project" }, "http://localhost:3000/")
let closed = false
listeners.get("popup")!({ close: async () => void (closed = true) } as never)
expect(closed).toBe(true)
expect(broker.get("popup")?.error).toBe("Blocked browser popup")
expect(broker.get("popup")).toMatchObject({ errors: 1, logs: ["Blocked browser popup"], error: undefined })
await expect(broker.refresh("popup")).rejects.toThrow("Navigation failed")
closed = false
listeners.get("popup")!({ close: async () => void (closed = true) } as never)
expect(closed).toBe(true)
expect(broker.get("popup")).toMatchObject({
status: "error",
errors: 1,
logs: ["Blocked browser popup"],
error: "Navigation failed",
})
})
test("allows only same-origin WebSockets", async () => {
@@ -799,12 +831,19 @@ describe("BrowserBroker", () => {
})
expect(connected).toBe(true)
expect(closed).toBe(false)
expect(broker.get("socket")).toMatchObject({ errors: 0, logs: [], error: undefined })
connected = false
await handler!({
url: () => "ws://localhost:4000/private",
connectToServer: () => void (connected = true),
close: async () => void (closed = true),
})
expect(connected).toBe(false)
expect(closed).toBe(true)
expect(broker.get("socket")?.error).toBe("Blocked browser request: ws://localhost:4000")
expect(broker.get("socket")).toMatchObject({
errors: 1,
logs: ["Blocked browser request: ws://localhost:4000"],
error: undefined,
})
})
})
@@ -143,6 +143,8 @@ function BrowserAdapter(props: {
close: language.t("agentManager.browser.close"),
inspect: language.t("agentManager.browser.inspect"),
devtoolsTitle: language.t("agentManager.browser.devtoolsTitle"),
diagnostics: language.t("agentManager.browser.diagnostics"),
diagnosticsHint: language.t("agentManager.browser.diagnosticsHint"),
empty: language.t("agentManager.browser.empty"),
noSession: language.t("agentManager.browser.noSession"),
screenshotAlt: language.t("agentManager.browser.screenshotAlt"),
+4 -1
View File
@@ -253,7 +253,10 @@ export const dict = {
"agentManager.browser.empty": "افتح تطبيقًا محليًا لمعاينته هنا.",
"agentManager.browser.noSession": "اختر جلسة Agent Manager أولًا.",
"agentManager.browser.screenshotAlt": "صفحة المتصفح الحالية",
"agentManager.browser.errors": "{{count}} أخطاء في وحدة التحكم",
"agentManager.browser.errors": "مشكلات المتصفح: {{count}}",
"agentManager.browser.diagnostics": "تشخيص المتصفح",
"agentManager.browser.diagnosticsHint":
"الأحداث الأخيرة من متصفح الأتمتة. عمليات الحظر لأسباب أمنية ليست أخطاء في وحدة التحكم.",
"agentManager.pr.error.gh_auth.title": "مصادقة GitHub مطلوبة",
"agentManager.pr.error.gh_auth.description": "شغّل 'gh auth login' في الطرفية لاستعادة حالة PR.",
+4 -1
View File
@@ -258,7 +258,10 @@ export const dict = {
"agentManager.browser.empty": "Abra uma aplicação local para visualizá-la aqui.",
"agentManager.browser.noSession": "Selecione primeiro uma sessão do Agent Manager.",
"agentManager.browser.screenshotAlt": "Página atual do navegador",
"agentManager.browser.errors": "{{count}} erros do console",
"agentManager.browser.errors": "Problemas do navegador: {{count}}",
"agentManager.browser.diagnostics": "Diagnóstico do navegador",
"agentManager.browser.diagnosticsHint":
"Eventos recentes do navegador de automação. Bloqueios de segurança não são erros do console.",
"agentManager.pr.error.gh_auth.title": "Autenticação do GitHub necessária",
"agentManager.pr.error.gh_auth.description": "Execute 'gh auth login' no terminal para restaurar o status do PR.",
+4 -1
View File
@@ -256,7 +256,10 @@ export const dict = {
"agentManager.browser.empty": "Otvorite lokalnu aplikaciju da biste je ovdje pregledali.",
"agentManager.browser.noSession": "Najprije odaberite sesiju aplikacije Agent Manager.",
"agentManager.browser.screenshotAlt": "Trenutna stranica preglednika",
"agentManager.browser.errors": "{{count}} grešaka konzole",
"agentManager.browser.errors": "Problemi preglednika: {{count}}",
"agentManager.browser.diagnostics": "Dijagnostika preglednika",
"agentManager.browser.diagnosticsHint":
"Nedavni događaji iz preglednika za automatizaciju. Sigurnosne blokade nisu greške konzole.",
"agentManager.pr.error.gh_auth.title": "Potrebna GitHub autentikacija",
"agentManager.pr.error.gh_auth.description": "Pokrenite 'gh auth login' u terminalu da vratite status PR-a.",
+4 -1
View File
@@ -258,7 +258,10 @@ export const dict = {
"agentManager.browser.empty": "Åbn en lokal applikation for at få vist en forhåndsvisning her.",
"agentManager.browser.noSession": "Vælg først en session i Agent Manager.",
"agentManager.browser.screenshotAlt": "Aktuel browserside",
"agentManager.browser.errors": "{{count}} konsolfejl",
"agentManager.browser.errors": "Browserproblemer: {{count}}",
"agentManager.browser.diagnostics": "Browserdiagnostik",
"agentManager.browser.diagnosticsHint":
"Seneste hændelser fra automatiseringsbrowseren. Sikkerhedsblokeringer er ikke konsolfejl.",
"agentManager.pr.error.gh_auth.title": "GitHub-godkendelse påkrævet",
"agentManager.pr.error.gh_auth.description": "Kør 'gh auth login' i din terminal for at gendanne PR-status.",
@@ -260,7 +260,10 @@ export const dict = {
"agentManager.browser.empty": "Öffnen Sie eine lokale Anwendung, um sie hier in der Vorschau anzuzeigen.",
"agentManager.browser.noSession": "Wählen Sie zuerst eine Sitzung im Agent Manager aus.",
"agentManager.browser.screenshotAlt": "Aktuelle Browserseite",
"agentManager.browser.errors": "{{count}} Konsolenfehler",
"agentManager.browser.errors": "Browserprobleme: {{count}}",
"agentManager.browser.diagnostics": "Browserdiagnose",
"agentManager.browser.diagnosticsHint":
"Aktuelle Ereignisse aus dem Automatisierungsbrowser. Sicherheitsblockierungen sind keine Konsolenfehler.",
"agentManager.pr.error.gh_auth.title": "GitHub-Authentifizierung erforderlich",
"agentManager.pr.error.gh_auth.description":
@@ -247,7 +247,10 @@ export const dict = {
"agentManager.browser.empty": "Open a local application to preview it here.",
"agentManager.browser.noSession": "Select an Agent Manager session first.",
"agentManager.browser.screenshotAlt": "Current browser page",
"agentManager.browser.errors": "{{count}} console errors",
"agentManager.browser.errors": "Browser issues: {{count}}",
"agentManager.browser.diagnostics": "Browser diagnostics",
"agentManager.browser.diagnosticsHint":
"Recent events from the automation browser. Security blocks are not console errors.",
"agentManager.import.pullRequest": "Pull Request",
"agentManager.import.pastePrUrl": "Paste PR URL...",
+4 -1
View File
@@ -259,7 +259,10 @@ export const dict = {
"agentManager.browser.empty": "Abre una aplicación local para previsualizarla aquí.",
"agentManager.browser.noSession": "Selecciona primero una sesión de Agent Manager.",
"agentManager.browser.screenshotAlt": "Página actual del navegador",
"agentManager.browser.errors": "{{count}} errores de consola",
"agentManager.browser.errors": "Problemas del navegador: {{count}}",
"agentManager.browser.diagnostics": "Diagnóstico del navegador",
"agentManager.browser.diagnosticsHint":
"Eventos recientes del navegador de automatización. Los bloqueos de seguridad no son errores de consola.",
"agentManager.pr.error.gh_auth.title": "Se requiere autenticación de GitHub",
"agentManager.pr.error.gh_auth.description":
+3 -1
View File
@@ -261,7 +261,9 @@ export const dict = {
"agentManager.browser.empty": "برای پیش‌نمایش، یک برنامه محلی را باز کنید.",
"agentManager.browser.noSession": "ابتدا یک جلسه Agent Manager را انتخاب کنید.",
"agentManager.browser.screenshotAlt": "صفحه فعلی مرورگر",
"agentManager.browser.errors": "{{count}} خطای کنسول",
"agentManager.browser.errors": "مشکلات مرورگر: {{count}}",
"agentManager.browser.diagnostics": "عیب‌یابی مرورگر",
"agentManager.browser.diagnosticsHint": "رویدادهای اخیر مرورگر خودکار. مسدودسازی‌های امنیتی خطاهای کنسول نیستند.",
"agentManager.pr.error.gh_auth.title": "احراز هویت GitHub لازم است",
"agentManager.pr.error.gh_auth.description": "برای بازیابی وضعیت PR، دستور 'gh auth login' را در ترمینال اجرا کنید.",
+4 -1
View File
@@ -260,7 +260,10 @@ export const dict = {
"agentManager.browser.empty": "Ouvrez une application locale pour l'afficher ici en aperçu.",
"agentManager.browser.noSession": "Sélectionnez d'abord une session dans Agent Manager.",
"agentManager.browser.screenshotAlt": "Page actuelle du navigateur",
"agentManager.browser.errors": "{{count}} erreurs de console",
"agentManager.browser.errors": "Problèmes du navigateur : {{count}}",
"agentManager.browser.diagnostics": "Diagnostics du navigateur",
"agentManager.browser.diagnosticsHint":
"Événements récents du navigateur d'automatisation. Les blocages de sécurité ne sont pas des erreurs de console.",
"agentManager.pr.error.gh_auth.title": "Authentification GitHub requise",
"agentManager.pr.error.gh_auth.description":
+4 -1
View File
@@ -266,7 +266,10 @@ export const dict = {
"agentManager.browser.empty": "Apri un'applicazione locale per visualizzarla qui.",
"agentManager.browser.noSession": "Seleziona prima una sessione di Agent Manager.",
"agentManager.browser.screenshotAlt": "Pagina corrente del browser",
"agentManager.browser.errors": "{{count}} errori della console",
"agentManager.browser.errors": "Problemi del browser: {{count}}",
"agentManager.browser.diagnostics": "Diagnostica del browser",
"agentManager.browser.diagnosticsHint":
"Eventi recenti del browser di automazione. I blocchi di sicurezza non sono errori della console.",
"agentManager.pr.error.gh_auth.title": "Autenticazione GitHub richiesta",
"agentManager.pr.error.gh_auth.description":
+4 -1
View File
@@ -257,7 +257,10 @@ export const dict = {
"agentManager.browser.empty": "ローカルアプリケーションを開くと、ここでプレビューできます。",
"agentManager.browser.noSession": "先に Agent Manager セッションを選択してください。",
"agentManager.browser.screenshotAlt": "現在のブラウザーページ",
"agentManager.browser.errors": "{{count}} 件のコンソールエラー",
"agentManager.browser.errors": "ブラウザーの問題: {{count}} 件",
"agentManager.browser.diagnostics": "ブラウザー診断",
"agentManager.browser.diagnosticsHint":
"自動操作用ブラウザーの最近のイベントです。セキュリティによるブロックはコンソールエラーではありません。",
"agentManager.pr.error.gh_auth.title": "GitHub認証が必要です",
"agentManager.pr.error.gh_auth.description":
+3 -1
View File
@@ -255,7 +255,9 @@ export const dict = {
"agentManager.browser.empty": "로컬 애플리케이션을 열어 여기에서 미리 보세요.",
"agentManager.browser.noSession": "먼저 Agent Manager 세션을 선택하세요.",
"agentManager.browser.screenshotAlt": "현재 브라우저 페이지",
"agentManager.browser.errors": "{{count}}개 콘솔 오류",
"agentManager.browser.errors": "브라우저 문제: {{count}}개",
"agentManager.browser.diagnostics": "브라우저 진단",
"agentManager.browser.diagnosticsHint": "자동화 브라우저의 최근 이벤트입니다. 보안 차단은 콘솔 오류가 아닙니다.",
"agentManager.pr.error.gh_auth.title": "GitHub 인증 필요",
"agentManager.pr.error.gh_auth.description": "PR 상태를 복원하려면 터미널에서 'gh auth login'을 실행하세요.",
+4 -1
View File
@@ -263,7 +263,10 @@ export const dict = {
"agentManager.browser.empty": "Open een lokale applicatie om deze hier te bekijken.",
"agentManager.browser.noSession": "Selecteer eerst een sessie in Agent Manager.",
"agentManager.browser.screenshotAlt": "Huidige browserpagina",
"agentManager.browser.errors": "{{count}} consolefouten",
"agentManager.browser.errors": "Browserproblemen: {{count}}",
"agentManager.browser.diagnostics": "Browserdiagnostiek",
"agentManager.browser.diagnosticsHint":
"Recente gebeurtenissen van de automatiseringsbrowser. Beveiligingsblokkeringen zijn geen consolefouten.",
"agentManager.pr.error.gh_auth.title": "GitHub-authenticatie vereist",
"agentManager.pr.error.gh_auth.description": "Voer 'gh auth login' uit in je terminal om de PR-status te herstellen.",
+4 -1
View File
@@ -255,7 +255,10 @@ export const dict = {
"agentManager.browser.empty": "Åpne en lokal applikasjon for å forhåndsvise den her.",
"agentManager.browser.noSession": "Velg en økt i Agent Manager først.",
"agentManager.browser.screenshotAlt": "Gjeldende nettleserside",
"agentManager.browser.errors": "{{count}} konsollfeil",
"agentManager.browser.errors": "Nettleserproblemer: {{count}}",
"agentManager.browser.diagnostics": "Nettleserdiagnostikk",
"agentManager.browser.diagnosticsHint":
"Nylige hendelser fra automatiseringsnettleseren. Sikkerhetsblokkeringer er ikke konsollfeil.",
"agentManager.pr.error.gh_auth.title": "GitHub-autentisering kreves",
"agentManager.pr.error.gh_auth.description": "Kjør 'gh auth login' i terminalen for å gjenopprette PR-status.",
+4 -1
View File
@@ -256,7 +256,10 @@ export const dict = {
"agentManager.browser.empty": "Otwórz lokalną aplikację, aby wyświetlić ją tutaj.",
"agentManager.browser.noSession": "Najpierw wybierz sesję aplikacji Agent Manager.",
"agentManager.browser.screenshotAlt": "Bieżąca strona przeglądarki",
"agentManager.browser.errors": "{{count}} błędów konsoli",
"agentManager.browser.errors": "Problemy przeglądarki: {{count}}",
"agentManager.browser.diagnostics": "Diagnostyka przeglądarki",
"agentManager.browser.diagnosticsHint":
"Ostatnie zdarzenia z przeglądarki używanej do automatyzacji. Blokady ze względów bezpieczeństwa nie są błędami konsoli.",
"agentManager.pr.error.gh_auth.title": "Wymagana autoryzacja GitHub",
"agentManager.pr.error.gh_auth.description": "Uruchom 'gh auth login' w terminalu, aby przywrócić status PR.",
+4 -1
View File
@@ -258,7 +258,10 @@ export const dict = {
"agentManager.browser.empty": "Откройте локальное приложение, чтобы просмотреть его здесь.",
"agentManager.browser.noSession": "Сначала выберите сеанс Agent Manager.",
"agentManager.browser.screenshotAlt": "Текущая страница браузера",
"agentManager.browser.errors": "{{count}} ошибок в консоли",
"agentManager.browser.errors": "Проблемы браузера: {{count}}",
"agentManager.browser.diagnostics": "Диагностика браузера",
"agentManager.browser.diagnosticsHint":
"Последние события из браузера, используемого для автоматизации. Блокировки в целях безопасности не являются ошибками консоли.",
"agentManager.pr.error.gh_auth.title": "Требуется аутентификация GitHub",
"agentManager.pr.error.gh_auth.description": "Выполните 'gh auth login' в терминале, чтобы восстановить статус PR.",
+4 -1
View File
@@ -251,7 +251,10 @@ export const dict = {
"agentManager.browser.empty": "เปิดแอปพลิเคชันในเครื่องเพื่อดูตัวอย่างที่นี่",
"agentManager.browser.noSession": "เลือกเซสชัน Agent Manager ก่อน",
"agentManager.browser.screenshotAlt": "หน้าปัจจุบันของเบราว์เซอร์",
"agentManager.browser.errors": "{{count}} ข้อผิดพลาดในคอนโซล",
"agentManager.browser.errors": "ปัญหาเบราว์เซอร์: {{count}} รายการ",
"agentManager.browser.diagnostics": "การวินิจฉัยเบราว์เซอร์",
"agentManager.browser.diagnosticsHint":
"เหตุการณ์ล่าสุดจากเบราว์เซอร์สำหรับงานอัตโนมัติ การบล็อกเพื่อความปลอดภัยไม่ใช่ข้อผิดพลาดในคอนโซล",
"agentManager.pr.error.gh_auth.title": "ต้องยืนยันตัวตน GitHub",
"agentManager.pr.error.gh_auth.description": "รันคำสั่ง 'gh auth login' ในเทอร์มินัลเพื่อกู้คืนสถานะ PR",
+4 -1
View File
@@ -265,7 +265,10 @@ export const dict = {
"agentManager.browser.empty": "Burada önizlemek için yerel bir uygulama açın.",
"agentManager.browser.noSession": "Önce bir Agent Manager oturumu seçin.",
"agentManager.browser.screenshotAlt": "Geçerli tarayıcı sayfası",
"agentManager.browser.errors": "{{count}} konsol hatası",
"agentManager.browser.errors": "Tarayıcı sorunları: {{count}}",
"agentManager.browser.diagnostics": "Tarayıcı tanılaması",
"agentManager.browser.diagnosticsHint":
"Otomasyon tarayıcısındaki son olaylar. Güvenlik engellemeleri konsol hatası değildir.",
"agentManager.pr.error.gh_auth.title": "GitHub kimlik doğrulaması gerekli",
"agentManager.pr.error.gh_auth.description":
+4 -1
View File
@@ -266,7 +266,10 @@ export const dict = {
"agentManager.browser.empty": "Відкрийте локальну програму, щоб переглянути її тут.",
"agentManager.browser.noSession": "Спочатку виберіть сесію Agent Manager.",
"agentManager.browser.screenshotAlt": "Поточна сторінка браузера",
"agentManager.browser.errors": "{{count}} помилок консолі",
"agentManager.browser.errors": "Проблеми браузера: {{count}}",
"agentManager.browser.diagnostics": "Діагностика браузера",
"agentManager.browser.diagnosticsHint":
"Останні події з браузера для автоматизації. Блокування з міркувань безпеки не є помилками консолі.",
"agentManager.pr.error.gh_auth.title": "Потрібна автентифікація GitHub",
"agentManager.pr.error.gh_auth.description": "Виконайте 'gh auth login' у терміналі, щоб відновити статус PR.",
+3 -1
View File
@@ -250,7 +250,9 @@ export const dict = {
"agentManager.browser.empty": "打开本地应用以在此处预览。",
"agentManager.browser.noSession": "请先选择 Agent Manager 会话。",
"agentManager.browser.screenshotAlt": "当前浏览器页面",
"agentManager.browser.errors": "{{count}} 个控制台错误",
"agentManager.browser.errors": "浏览器问题:{{count}}",
"agentManager.browser.diagnostics": "浏览器诊断",
"agentManager.browser.diagnosticsHint": "来自自动化浏览器的近期事件。安全拦截不是控制台错误。",
"agentManager.pr.error.gh_auth.title": "需要 GitHub 身份验证",
"agentManager.pr.error.gh_auth.description": "在终端中运行 'gh auth login' 以恢复 PR 状态。",
+3 -1
View File
@@ -249,7 +249,9 @@ export const dict = {
"agentManager.browser.empty": "開啟本機應用程式,即可在此處預覽。",
"agentManager.browser.noSession": "請先選取 Agent Manager 工作階段。",
"agentManager.browser.screenshotAlt": "目前的瀏覽器頁面",
"agentManager.browser.errors": "{{count}} 個主控台錯誤",
"agentManager.browser.errors": "瀏覽器問題:{{count}}",
"agentManager.browser.diagnostics": "瀏覽器診斷",
"agentManager.browser.diagnosticsHint": "來自自動化瀏覽器的近期事件。安全性封鎖不是主控台錯誤。",
"agentManager.pr.error.gh_auth.title": "需要 GitHub 驗證",
"agentManager.pr.error.gh_auth.description": "在終端機中執行 'gh auth login' 以還原 PR 狀態。",
@@ -1,5 +1,6 @@
import { For, Show, type Accessor, type Component } from "solid-js"
import { For, Show, createMemo, type Accessor, type Component } from "solid-js"
import { Card } from "@kilocode/kilo-ui/card"
import { Collapsible } from "@kilocode/kilo-ui/collapsible"
import { Icon } from "@kilocode/kilo-ui/icon"
import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { Spinner } from "@kilocode/kilo-ui/spinner"
@@ -28,10 +29,6 @@ const Toolbar: Component<{
active: boolean
}> = (props) => {
const ready = () => !!props.controller.state()?.url && props.controller.state()?.status !== "closed"
const diagnostics = () =>
props.controller.state()?.errors
? `${props.labels.devtoolsTitle}, ${props.labels.errors(props.controller.state()!.errors)}`
: props.labels.devtoolsTitle
return (
<div class="am-browser-toolbar">
<Tooltip value={props.labels.refresh} placement="bottom">
@@ -52,11 +49,11 @@ const Toolbar: Component<{
if (props.active && props.controller.url().trim() && !props.controller.loading()) props.controller.open()
}}
>
<span class="am-browser-site" aria-hidden="true">
<Show when={props.controller.loading()} fallback={<Icon name="globe" size="small" />}>
<Show when={props.controller.loading()}>
<span class="am-browser-site" aria-hidden="true">
<Spinner />
</Show>
</span>
</span>
</Show>
<TextField
class="am-browser-url"
variant="ghost"
@@ -91,22 +88,17 @@ const Toolbar: Component<{
/>
</Tooltip>
<div class="am-browser-tools-action">
<Tooltip value={diagnostics()} placement="bottom">
<Tooltip value={props.labels.devtoolsTitle} placement="bottom">
<IconButton
icon="console"
size="small"
variant={props.controller.tools() ? "secondary" : "ghost"}
aria-label={diagnostics()}
aria-label={props.labels.devtoolsTitle}
aria-pressed={!!props.controller.tools()}
onClick={props.controller.toggleTools}
disabled={!ready() || props.controller.loading()}
/>
</Tooltip>
<Show when={(props.controller.state()?.errors ?? 0) > 0}>
<span class="am-browser-error-count" aria-hidden="true">
{(props.controller.state()?.errors ?? 0) > 99 ? "99+" : props.controller.state()?.errors}
</span>
</Show>
</div>
<Tooltip value={props.labels.close} placement="bottom">
<IconButton
@@ -227,19 +219,38 @@ const Tools: Component<{ url: string; labels: BrowserLabels }> = (props) => (
</section>
)
const Diagnostics: Component<{ logs: string[] }> = (props) => (
<Show when={props.logs.length}>
<div class="am-browser-console" role="log" aria-live="polite">
<For each={props.logs}>
{(line) => (
<div class="am-browser-console-entry" data-level={line.match(/^\[([^\]]+)\]/)?.[1] ?? "error"}>
{line}
const Diagnostics: Component<{ logs: string[]; errors: number; labels: BrowserLabels }> = (props) => {
const entries = createMemo(() => {
const counts = new Map<string, number>()
for (const text of props.logs) counts.set(text, (counts.get(text) ?? 0) + 1)
return Array.from(counts, ([text, count]) => ({ text, count }))
})
return (
<Show when={props.logs.length || props.errors}>
<Collapsible variant="ghost" class="am-browser-diagnostics">
<Collapsible.Trigger>
<span>{props.errors ? props.labels.errors(props.errors) : props.labels.diagnostics}</span>
<Collapsible.Arrow />
</Collapsible.Trigger>
<Collapsible.Content>
<div class="am-browser-diagnostics-hint">{props.labels.diagnosticsHint}</div>
<div class="am-browser-console" role="log" aria-live="polite">
<For each={entries()}>
{(entry) => (
<div class="am-browser-console-entry" data-level={entry.text.match(/^\[([^\]]+)\]/)?.[1] ?? "error"}>
<span>{entry.text}</span>
<Show when={entry.count > 1}>
<span class="am-browser-console-count">×{entry.count}</span>
</Show>
</div>
)}
</For>
</div>
)}
</For>
</div>
</Show>
)
</Collapsible.Content>
</Collapsible>
</Show>
)
}
export interface BrowserPanelProps {
scope: Accessor<BrowserScope | undefined>
@@ -278,9 +289,7 @@ export const BrowserPanel: Component<BrowserPanelProps> = (props) => {
{(entry) => <Tools url={entry.url} labels={props.labels} />}
</Show>
</div>
<Show when={!controller.tools()}>
<Diagnostics logs={state()?.logs ?? []} />
</Show>
<Diagnostics logs={state()?.logs ?? []} errors={state()?.errors ?? 0} labels={props.labels} />
</div>
)
}
@@ -74,21 +74,6 @@
flex-shrink: 0;
}
.am-browser-error-count {
position: absolute;
top: -3px;
right: -3px;
min-width: 12px;
padding: 0 3px;
font-size: var(--kilo-font-size-11);
line-height: 14px;
color: var(--text-on-accent);
text-align: center;
pointer-events: none;
background: var(--syntax-diff-delete, #da3319);
border-radius: 7px;
}
.am-browser-workspace {
display: flex;
flex: 1;
@@ -193,20 +178,49 @@
transform: none;
}
[data-component="collapsible"].am-browser-diagnostics {
flex-shrink: 0;
border-top: 1px solid var(--border-weak-base);
border-radius: 0;
}
[data-component="collapsible"].am-browser-diagnostics > [data-slot="collapsible-trigger"] {
justify-content: space-between;
padding: 0 12px;
}
.am-browser-diagnostics [data-slot="collapsible-arrow"] {
opacity: 1;
}
.am-browser-diagnostics-hint {
padding: 0 12px 4px;
font-size: var(--font-size-small);
color: var(--text-weak);
}
.am-browser-console {
max-height: 140px;
padding: 8px 12px;
padding: 4px 12px 8px;
overflow: auto;
font-family: var(--font-family-mono);
font-size: var(--font-size-small);
color: var(--text-weak);
border-top: 1px solid var(--border-weak-base);
}
.am-browser-console-entry {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
overflow-wrap: anywhere;
}
.am-browser-console-count {
flex-shrink: 0;
color: var(--text-weak);
}
.am-browser-console-entry[data-level="warning"],
.am-browser-console-entry[data-level="warn"] {
color: var(--text-warning, #d6a23a);
@@ -82,6 +82,8 @@ export interface BrowserLabels {
close: string
inspect: string
devtoolsTitle: string
diagnostics: string
diagnosticsHint: string
empty: string
noSession: string
screenshotAlt: string