Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
961d4aa
chat: deterministic bash command summarizer for friendly tool labels
Cheggin May 11, 2026
ba244ad
chat: collapsible tool group with live and prose summaries
Cheggin May 11, 2026
d624874
hub: add Back-to-chat button to AgentPane header
Cheggin May 11, 2026
b707490
chore(deps): add zustand, immer, prism-react-renderer
Cheggin May 11, 2026
0834fec
feat(state): zustand sessions store + UI store + IPC bridge
Cheggin May 11, 2026
a185789
feat(main): per-session CDP screencast preview + browser-attached IPC
Cheggin May 11, 2026
2cc38ca
feat(chat): conversational view — pane, transcript, turns, tool grouping
Cheggin May 11, 2026
8e53b03
feat(chat): tool rendering — code blocks, labels, spinner, browser pr…
Cheggin May 11, 2026
77d8273
feat(hub): wire chat view and entry points
Cheggin May 11, 2026
ce75b68
test(chat): groupIntoTurns unit tests
Cheggin May 11, 2026
8ea1b7f
feat(toast): wire toasts into settings, connections, and cookie sync
Cheggin May 11, 2026
cea4055
Merge remote-tracking branch 'origin/main' into feature/chat-view
Cheggin May 11, 2026
db2313d
chat: clickable harness-output paths via shell.showItemInFolder
Cheggin May 11, 2026
00307ef
Merge remote-tracking branch 'origin/main' into feature/chat-view
Cheggin May 11, 2026
bd1cd89
chat: header polish — engine icons, badges, status colors
Cheggin May 11, 2026
87abe01
screenshots: inline-render user-facing captures via chatfile:// protocol
Cheggin May 11, 2026
5e534d9
chat: streaming prose, hoisted images, layout-shift fixes
Cheggin May 11, 2026
bf2bb45
feat(chat): quote-from-selection + composer layout fixes
Cheggin May 11, 2026
03cd563
chat: fix streaming typewriter stall + clear transcript on rerun
Cheggin May 11, 2026
cc085cc
Merge branch 'main' of https://github.com/browser-use/desktop-app int…
Cheggin May 13, 2026
246f972
Preserve product direction for chat view
Cheggin May 14, 2026
262ba2c
Let chat edits rerun with a replaced prompt
Cheggin May 14, 2026
9f08d6e
Restore chat-first UI polish without changing streaming
Cheggin May 14, 2026
9f16331
Package only Vite build output
Cheggin May 14, 2026
78333dd
Prevent validated chat regressions from reaching users
Cheggin May 14, 2026
8cec7b7
Stop follow-ups from replacing chat kickoff prompts
Cheggin May 14, 2026
8707b47
Keep chat preview rendering without visible browser views
Cheggin May 14, 2026
146cbf0
Keep product direction drafts local
Cheggin May 14, 2026
1e5d106
Surface agent skills directly in chat
Cheggin May 14, 2026
7ed6624
Make running-state copy configurable
Cheggin May 14, 2026
40cae4d
Run hub renderer specs with app aliases
Cheggin May 14, 2026
beef769
Keep session UI state truthful across startup races
Cheggin May 14, 2026
f38dac6
Preserve live session state across packaging and preview races
Cheggin May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Personal planning docs (reagan_ prefix per CLAUDE.md) — never committed
reagan_*
app/reagan_*
app/docs/PRODUCT_*.md

# Dependencies
node_modules/
Expand Down
42 changes: 16 additions & 26 deletions app/forge.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ const runNpm = (args: string[], cwd: string): void => {
execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, { cwd, stdio: 'inherit' });
};

function isViteOutputPath(file: string): boolean {
const normalized = file.split(path.sep).join('/');
if (normalized === '/.vite' || normalized.startsWith('/.vite/')) return true;
if (normalized === '.vite' || normalized.startsWith('.vite/')) return true;
if (!path.isAbsolute(file)) return false;
const rel = path.relative(__dirname, file).split(path.sep).join('/');
return rel === '.vite' || rel.startsWith('.vite/');
}

// ---------------------------------------------------------------------------
// Forge configuration
// ---------------------------------------------------------------------------
Expand All @@ -68,32 +77,13 @@ const config: ForgeConfig = {
name: 'Browser Use',
executableName: 'browser-use-desktop',

// Exclude dev-only files from the packaged app.asar. Without this,
// Forge ships src/, tests/, personal planning .md files, Vite
// source configs, CI configs, README, etc. — the DMG balloons and
// users get dev notes. Paths arrive with a leading slash.
ignore: [
/^\/src($|\/)/, // TS source; compiled output is in .vite/build
/^\/tests($|\/)/,
/^\/scripts($|\/)/,
/^\/python($|\/)/, // daemon removed; defensive
/^\/harnessless($|\/)/,
/^\/\.github($|\/)/,
/^\/\.claude($|\/)/,
/^\/\.vscode($|\/)/,
/^\/coverage($|\/)/,
/^\/reagan_.*$/, // personal planning docs
/^\/.*\.md$/, // all markdown (README, docs, etc.)
/^\/tsconfig.*\.json$/,
/^\/eslint\.config\.(ts|mts|js|mjs|cjs)$/,
/^\/vite\..*\.(ts|mts)$/, // dev-time vite configs
/^\/forge\.config\.(ts|js)$/,
/^\/playwright\.config\.(ts|js)$/,
/^\/knip\.json$/,
/^\/\.env(\..+)?$/, // never ship .env to users
/^\/Taskfile\.ya?ml$/,
/^\/.+\.map$/, // sourcemaps
],
// The Vite plugin expects packaged app contents to come only from .vite.
// Production externals are installed back into the build path in the
// packageAfterPrune hook below, and app-update.yml is copied as a resource.
ignore: (file) => {
if (!file) return false;
return !isViteOutputPath(file);
},

// macOS bundle identity — only set when credentials are available.
// Real Developer ID signing only runs via @electron/osx-sign when
Expand Down
5 changes: 4 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,19 @@
"dotenv": "^17.4.2",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.8.3",
"immer": "^11.1.8",
"keytar": "^7.9.0",
"node-cache": "^5.1.2",
"node-pty": "^1.1.0",
"prism-react-renderer": "^2.4.1",
"qrcode": "^1.5.4",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-markdown": "^10.1.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"ws": "^8.20.0",
"zod": "^4.3.6"
"zod": "^4.3.6",
"zustand": "^5.0.13"
}
}
5 changes: 5 additions & 0 deletions app/src/main/hl/engines/browserHarnessEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@ export function applyBrowserHarnessEnv(ctx: SpawnContext, env: NodeJS.ProcessEnv
env.CDP_REPL_PORT = env.CDP_REPL_PORT ?? browserHarnessReplPort(ctx.sessionId, ctx.targetId);
env.CDP_REPL_LOG = env.CDP_REPL_LOG ?? path.join(ctx.harnessDir, `browser-harness-js-${ctx.sessionId}.log`);
env.BU_SESSION_ID = ctx.sessionId;
// Watched session outputs dir — any file written here triggers a `file_output`
// event in runEngine. The Page.captureScreenshot wrapper in repl.ts auto-saves
// PNGs into this dir so screenshots surface in the chat instead of being
// dumped as base64 into stdout.
env.BU_OUTPUTS_DIR = path.join(ctx.harnessDir, 'outputs', ctx.sessionId);
return env;
}
25 changes: 12 additions & 13 deletions app/src/main/hl/engines/runEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { engineLogger } from '../../logger';
import { resolveAuth, loadOpenAIKey, loadClaudeSubscriptionType, loadBrowserCodeConfig } from '../../identity/authStore';
import { helpersPath, skillPath } from '../harness';
import { helpersPath, skillPath, skillMetaFromPath as resolveSkillMetaFromPath } from '../harness';
import { get as getAdapter } from './registry';
import { spawnCli } from './cliSpawn';
import { registerResourceOwner, unregisterResourceOwner } from '../../resourceMonitor';
Expand Down Expand Up @@ -454,12 +454,20 @@ export async function runEngine(opts: RunEngineOptions): Promise<void> {
if (!stat.isFile()) return;
if (seenOutputs.get(filename) === stat.size) return;
seenOutputs.set(filename, stat.size);
const mime = mimeFromExt(filename);
engineLogger.info('engines.run.outputs.fileDetected', {
sessionId: opts.sessionId,
filename,
absPath: filePath,
bytes: stat.size,
mime,
});
opts.onEvent({
type: 'file_output',
name: filename,
path: filePath,
size: stat.size,
mime: mimeFromExt(filename),
mime,
});
});
} catch (err) {
Expand All @@ -484,18 +492,9 @@ export async function runEngine(opts: RunEngineOptions): Promise<void> {
// reads. Harness edits are emitted by the file watcher above, using actual
// file content as the source of truth instead of provider-specific tool
// metadata.
// Shared resolver - keep path <-> skill-id conversion in one place (see harness.ts).
function skillMetaFromPath(resolved: string): { domain: string; topic: string } | null {
const rel = path.relative(opts.harnessDir, resolved).split(path.sep).join('/');
if (rel.startsWith('domain-skills/') && rel.endsWith('.md')) {
return { domain: 'domain', topic: rel.slice('domain-skills/'.length, -'.md'.length) };
}
if (rel.startsWith('interaction-skills/') && rel.endsWith('.md')) {
return { domain: 'interaction', topic: rel.slice('interaction-skills/'.length, -'.md'.length) };
}
if (rel.startsWith('skills/') && rel.endsWith('/SKILL.md')) {
return { domain: 'user', topic: rel.slice('skills/'.length, -'/SKILL.md'.length) };
}
return null;
return resolveSkillMetaFromPath(resolved, opts.harnessDir);
}

function skillMetaFromSkillId(id: string): { domain: string; topic: string } | null {
Expand Down
51 changes: 51 additions & 0 deletions app/src/main/hl/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,57 @@ export function browserHarnessJsDir(): string { return path.join(harnessDir(), '
export function agentSkillDir(): string { return path.join(harnessDir(), 'agent-skill'); }
export function userSkillsDir(): string { return path.join(harnessDir(), 'skills'); }

/**
* Skill IDs are `<domain>/<topic>` strings (e.g. `user/fun/page-word-count`,
* `domain/github/repo`, `interaction/cookies`). The on-disk layout differs per
* domain - these helpers are the single source of truth for the conversion so
* the engine post-processor and any IPC handler stay aligned.
*/
export interface SkillId { domain: string; topic: string }

function sanitizeSkillTopic(rawTopic: string): string | null {
const topic = rawTopic.replace(/\.md$/i, '').replace(/\\/g, '/');
if (!topic || topic.startsWith('/') || path.isAbsolute(rawTopic)) return null;
const segments = topic.split('/');
if (segments.some((segment) => !segment || segment === '.' || segment === '..' || /^[A-Za-z]:/.test(segment))) {
return null;
}
return segments.join('/');
}

export function skillPathFromMeta(meta: SkillId, rootDir: string = harnessDir()): string | null {
const topic = sanitizeSkillTopic(meta.topic);
if (!topic) return null;
if (meta.domain === 'user') return path.join(rootDir, 'skills', topic, 'SKILL.md');
if (meta.domain === 'domain') return path.join(rootDir, 'domain-skills', topic + '.md');
if (meta.domain === 'interaction') return path.join(rootDir, 'interaction-skills', topic + '.md');
return null;
}

export function skillMetaFromPath(resolved: string, rootDir: string = harnessDir()): SkillId | null {
const rel = path.relative(rootDir, resolved).split(path.sep).join('/');
if (rel.startsWith('domain-skills/') && rel.endsWith('.md')) {
return { domain: 'domain', topic: rel.slice('domain-skills/'.length, -'.md'.length) };
}
if (rel.startsWith('interaction-skills/') && rel.endsWith('.md')) {
return { domain: 'interaction', topic: rel.slice('interaction-skills/'.length, -'.md'.length) };
}
if (rel.startsWith('skills/') && rel.endsWith('/SKILL.md')) {
return { domain: 'user', topic: rel.slice('skills/'.length, -'/SKILL.md'.length) };
}
return null;
}

export function skillIdToPath(skillId: string, rootDir: string = harnessDir()): string | null {
const cleaned = skillId.trim().replace(/['"]+$/g, '').replace(/\\/g, '/');
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Quote sanitization only removes trailing quotes, so quoted skill IDs fail to resolve.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/hl/harness.ts, line 110:

<comment>Quote sanitization only removes trailing quotes, so quoted skill IDs fail to resolve.</comment>

<file context>
@@ -65,6 +65,57 @@ export function browserHarnessJsDir(): string { return path.join(harnessDir(), '
+}
+
+export function skillIdToPath(skillId: string, rootDir: string = harnessDir()): string | null {
+  const cleaned = skillId.trim().replace(/['"]+$/g, '').replace(/\\/g, '/');
+  if (!cleaned || cleaned.startsWith('--') || cleaned.startsWith('/') || path.isAbsolute(skillId)) return null;
+  const parts = cleaned.split('/');
</file context>
Suggested change
const cleaned = skillId.trim().replace(/['"]+$/g, '').replace(/\\/g, '/');
const cleaned = skillId.trim().replace(/^['"]+|['"]+$/g, '').replace(/\\/g, '/');
Fix with Cubic

if (!cleaned || cleaned.startsWith('--') || cleaned.startsWith('/') || path.isAbsolute(skillId)) return null;
const parts = cleaned.split('/');
if (parts.some((part) => !part)) return null;
if (parts.length < 2) return null;
const [domain, ...rest] = parts;
return skillPathFromMeta({ domain, topic: rest.join('/') }, rootDir);
}

/**
* Ensure `<userData>/harness/` exists and contains the stock files.
* - Writes helpers.js if missing OR if the on-disk version predates the
Expand Down
26 changes: 24 additions & 2 deletions app/src/main/hl/stock/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,36 @@ Verify after every meaningful browser action:
For screenshots:

```bash
# Internal screenshot (for your own vision — not shown to the user):
browser-harness-js <<'EOF'
await connectToAssignedTarget()
const { data } = await session.Page.captureScreenshot({ format: 'png' })
await Bun.write('/tmp/browser-use-shot.png', Buffer.from(data, 'base64'))
return '/tmp/browser-use-shot.png'
// inspect `data` (base64 PNG) however you need; do NOT save unless the user
// explicitly asked to see the screenshot.
EOF

# User-facing screenshot (renders inline in the chat):
browser-harness-js <<'EOF'
await connectToAssignedTarget()
const { data } = await session.Page.captureScreenshot({ format: 'png' })
await Bun.write(`${process.env.BU_OUTPUTS_DIR}/screenshot-${Date.now()}.png`, Buffer.from(data, 'base64'))
EOF
```

**When a screenshot is worth showing the user:** save to `$BU_OUTPUTS_DIR` when
the user genuinely benefits from seeing the page. Use your own judgment — these
are guideposts, not rules:
- Confirming a delegated task finished (a post went up, a message sent, a form
submitted, a checkout completed).
- Mid-progress check-in on a long task, so the user knows you haven't stalled.
- Something unexpected or interesting showed up that's worth flagging visually.
- You're stuck on a captcha, login wall, or page state you can't resolve, and
showing it helps the user see what you see.

Don't save screenshots you took purely to look at the page yourself (finding
a selector, checking element state, verifying navigation) — those clutter the
chat without giving the user new information.

## Uploads And Outputs

- Uploads from the user appear under `./uploads/<session_id>/`.
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/hl/stock/agent-skill/agent-skill
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,10 @@ function validateEntry(entry) {
const errors = [];
const warnings = [];
if (!entry.title.trim()) errors.push('missing title');
if (!entry.description.trim() && !String(data.description || '').trim()) errors.push('missing description');
// Description MUST come from frontmatter - falling back to the first
// paragraph used to mask broken skills and surface raw markdown (H2 hooks,
// code fences) anywhere the description was rendered.
if (!String(data.description || '').trim()) errors.push('missing frontmatter description (add `description: ...` to the front-matter block)');
if (entry.bytes > 100_000) warnings.push('skill is large; move examples/templates/scripts into support files');
if (!/(^|\n)#{1,3}\s+/m.test(entry.content)) warnings.push('no markdown section headings found');
if (!/(when|use|run|step|verify|check|done|success)/i.test(entry.content)) {
Expand Down
Loading
Loading