Skip to content

Proj/local web UI#465

Merged
bao-byterover merged 104 commits intomainfrom
proj/local-web-ui
Apr 21, 2026
Merged

Proj/local web UI#465
bao-byterover merged 104 commits intomainfrom
proj/local-web-ui

Conversation

@ncnthien
Copy link
Copy Markdown
Collaborator

Summary

  • Problem:
  • Why it matters:
  • What changed:
  • What did NOT change (scope boundary):

Type of change

  • Bug fix
  • New feature
  • Refactor (no behavior change)
  • Documentation
  • Test
  • Chore (build, dependencies, CI)

Scope (select all touched areas)

  • TUI / REPL
  • Agent / Tools
  • LLM Providers
  • Server / Daemon
  • Shared (constants, types, transport events)
  • CLI Commands (oclif)
  • Hub / Connectors
  • Cloud Sync
  • CI/CD / Infra

Linked issues

  • Closes #
  • Related #

Root cause (bug fixes only, otherwise write N/A)

  • Root cause:
  • Why this was not caught earlier:

Test plan

  • Coverage added:
    • Unit test
    • Integration test
    • Manual verification only
  • Test file(s):
  • Key scenario(s) covered:

User-visible changes

List user-visible changes (including defaults, config, or CLI output).
If none, write None.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording

Checklist

  • Tests added or updated and passing (npm test)
  • Lint passes (npm run lint)
  • Type check passes (npm run typecheck)
  • Build succeeds (npm run build)
  • Commits follow Conventional Commits format
  • Documentation updated (if applicable)
  • No breaking changes (or clearly documented above)
  • Branch is up to date with main

Risks and mitigations

List real risks for this PR. If none, write None.

  • Risk:
    • Mitigation:

ncnthien and others added 30 commits March 25, 2026 10:31
Mount Express middleware on the daemon's HTTP server to serve the
Vite-built React app at /ui and expose GET /api/ui/config for browser
bootstrap. Add build:ui and dev:ui npm scripts.
Add transport layer for the web UI: BrvApiClient, Zustand transport
store, connection bootstrap (fetch /api/ui/config + socket.io-client),
TransportProvider, and React Query config. Add brv webui command to
discover the daemon and open the browser.
… panels

Add a complete browser-based dashboard at /ui that mirrors the TUI's feature
set. The web UI connects to the daemon via Socket.IO, uses the same transport
events and API patterns as the TUI, and imports shared UI components from
@campfirein/byterover-packages (Card, Badge, Button, Dialog, Table, Tabs, Input).

Key changes:
- Auth system with OAuth login flow, auth guard, and session management
- 11 feature panels: status, providers, models, connectors, hub, push, pull,
  spaces, session, reset, analytics (placeholder)
- Sidebar navigation with grouped routes and connection state badge
- Tailwind CSS v4 with shadcn/ui design tokens and dark mode
- CSP security headers on the daemon's Express middleware
- 'webui' client type added to VALID_CLIENT_TYPES for daemon registration
- Vite config with @workspace/ui alias, React dedup for npm link, and
  @source scanning for package component classes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…anel UI

Fix hasMcpToolsInBrvSection using new Set() which created a character set
instead of checking substrings — MCP tool detection always returned false,
causing rules connector to appear installed after switching to MCP/skill.
Changed to string.includes() for correct substring matching.

Connectors panel improvements:
- Add agent search with deferred filtering
- Move connector type tabs into Supported agents card
- Scrollable agent list with overflow constraint
- Alert component for dismissable feedback messages
- Combined restart warning into single feedback call
- Explicit refetchConnectors after install to avoid stale data
- Main layout max-h-screen for proper height propagation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update vite config and tsconfig to resolve shared UI from
ui/src/ instead of src/, matching the actual repo layout.
feat(ENG-2045): add submodule-based webui local development flow
Separate the web UI from the daemon's dynamic transport port onto a
fixed port (default 7700) so users can bookmark http://localhost:7700.
Add PWA with Service Worker so the app loads from cache even when the
daemon is down, showing a 'Run
Command "webui" not found

Run "brv --help" to see available commands to start' screen.

- Add WebUiServer on stable port, keep Socket.IO transport on dynamic port
- Browser connects Socket.IO cross-origin to daemon via /api/ui/config
- Add vite-plugin-pwa for offline caching with workbox
- Add
Command "webui" not found

Run "brv --help" to see available commands flag to set and persist preferred port
- Add webui:getPort / webui:setPort transport endpoints
- Remove /ui base path (served at root since dedicated port)
- Review URLs use webui port instead of transport port
- Graceful fallback when webui port is occupied (daemon still runs)
feat(ENG-2045): split web UI onto stable port with PWA offline support
…eDependencies (auto-generated when npm install).
…ith availability of package-lock.json in their repo.
- Tree browse with expand/collapse, file detail with markdown rendering
- File editing with save, version history with cursor-based pagination
- 3-panel layout (tree, detail, history) with collapsible panels
- Breadcrumb navigation, author timeline, folder metadata
- URL-based path/branch sync for bookmarking
- Remove auth guard for local web access
feat: [ENG-1505] add context tree management to local web UI
feat: [ENG-2057] add web UI project selection with sidebar project dropdown.
})

// Config endpoint for browser to bootstrap Socket.IO connection
app.get('/api/ui/config', (_req, res) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (security): /api/ui/config still has no CORS restriction — unfixed across eight reviews

Any page open in the same browser can fetch('http://localhost:7700/api/ui/config') and silently read daemonPort, projectCwd, and version, then use those to open a privileged Socket.IO connection to the daemon. Eight review cycles, zero action.

Suggested change
app.get('/api/ui/config', (_req, res) => {
app.get('/api/ui/config', (_req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'null') // block cross-origin fetches
res.json(getConfig())
})

Or add cors({origin: false}) middleware on all /api/* routes. Either way, this must be fixed before merge.

// Stop existing webui server if running
if (webuiServer?.isRunning()) {
await webuiServer.stop()
log(`Stopped web UI server on port ${webuiServer.getPort() ?? '?'}`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (robustness): three problems remain here — unfixed across eight reviews

1. Port logged as "?": WebUiServer.stop() clears this.port before resolving. By the time line 527 runs, getPort() returns undefined, so the log always prints "Stopped web UI server on port ?". Capture the port before calling stop():

const oldPort = webuiServer.getPort()
await webuiServer.stop()
log(`Stopped web UI server on port ${oldPort ?? '?'}`)

2. No port validation: data.port has no bounds check — 0, negative, or >65535 reaches start() after the old server is already down. Add before the stop call:

if (!Number.isInteger(newPort) || newPort < 1 || newPort > 65535) {
  return {port: webuiServer?.getPort() ?? WEBUI_DEFAULT_PORT, success: false}
}

3. Stop-before-start availability gap: If start(newPort) throws (port busy), no server is running. Start the new server first, only stop the old after new is confirmed up.


// Guard against path traversal
const resolved = relative(contextTreeDir, fullPath)
if (resolved.startsWith('..') || resolved.startsWith('/')) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (correctness): empty data.path still bypasses traversal guard — unfixed across eight reviews

data.path === '' resolves join(contextTreeDir, '') to contextTreeDir itself. relative(contextTreeDir, contextTreeDir) returns '', which doesn't start with .. or /, so the guard passes and writeFile is called on a directory, throwing EISDIR.

Suggested change
if (resolved.startsWith('..') || resolved.startsWith('/')) {
if (!data.path || resolved.startsWith('..') || resolved.startsWith('/')) {

port: Flags.integer({
char: 'p',
description: 'Set the web UI port (remembered for future use)',
}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (robustness): --port flag still has no range constraint — unfixed from review #2

Flags.integer with no min/max lets brv webui -p 0 or brv webui -p 70000 reach the daemon unvalidated. Combined with the daemon's stop-before-start ordering, any out-of-range value permanently disables the web UI until daemon restart.

Suggested change
}),
port: Flags.integer({
char: 'p',
description: 'Set the web UI port (remembered for future use)',
max: 65535,
min: 1,
}),

Comment thread src/webui/lib/error-messages.ts Outdated
ERR_VC_NON_FAST_FORWARD: 'The remote has changes. Pull first, then try again.',
ERR_VC_NOTHING_TO_PUSH: 'Nothing to push — stage and commit your changes first.',
ERR_VC_REMOTE_ALREADY_EXISTS: "A remote named 'origin' already exists. Remove or rename it before adding a new one.",
ERR_VC_USER_NOT_CONFIGURED: 'Set commit author via `brv vc config` before committing.',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (regression): ERR_VC_USER_NOT_CONFIGURED override still references the CLI — unfixed from review #7

This message references brv vc config, a CLI command the web UI user cannot run. Use neutral copy that works in both contexts:

Suggested change
ERR_VC_USER_NOT_CONFIGURED: 'Set commit author via `brv vc config` before committing.',
ERR_VC_USER_NOT_CONFIGURED: 'Configure your commit author name and email before committing.',

…text

Project name and display path now recognise backslash-separated
Windows paths alongside macOS and Linux home directories, each with
a unit-tested suite covering absolute paths, trailing separators,
UNC shares, and degenerate inputs. The shared formatError helper
accepts an optional ErrorContext so overrides can produce tailored
copy — ERR_VC_USER_NOT_CONFIGURED now embeds the project path and
tells the user exactly where to run brv vc config. Changes panel
threads the selected project through to the three commit-related
error sites.
Drop the redundant length check in getProjectName in favour of a
straight falsy-fallback, swap undefined fallback arguments in the
error-messages tests for a named string so intent is obvious, and
add a UNC-share test to pin displayPath's "unchanged when no drive
letter" contract.
feat: [ENG-2167] cross-platform path handling and richer VC error con…
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 21, 2026

Claude finished @ncnthien's task in 3m 24s —— View job


PR Review: Local Web UI (Re-review #9)


What's new since review #8

One PR merged since the last review cycle:

Commit Description
5e494a05 feat: [ENG-2167] cross-platform path handling and richer VC error context
23b8f5b7 feat: [ENG-2167] address review nits on path utils and error tests
1f5cdadd Merge PR #491

The new code adds cross-platform path utilities (display-path.ts, project-name.ts), upgrades error-messages.ts to support context-aware override functions via an OverrideValue union type, and wires selectedProject through formatError calls in changes-panel.tsx so that ERR_VC_USER_NOT_CONFIGURED messages now include the affected project path.


Prior issues — status (9 reviews in)

# Issue Status
1 CORS restriction missing on /api/ui/config ❌ Not fixed
2 webui:setPort accepts unvalidated port values ❌ Not fixed
3 Stop-before-start race on port change ❌ Not fixed
4 Port logged as "?" after stop() ❌ Not fixed
5 Path traversal guard passes empty data.path ❌ Not fixed
6 branch field declared in schema but never read ❌ Not fixed
7 Duplicated web UI server construction logic ❌ Not fixed
8 isExcludedFromSync not applied per-directory ❌ Not fixed
9 WEBUI_CONFIG_FILE hardcoded instead of exported constant ❌ Not fixed
10 Login dialog polling/subscription race condition ❌ Not fixed
11 --port flag has no range constraint ❌ Not fixed
12 handleMerge only validates remote-tracking branches ❌ Not fixed
13 Missing tests for webui-middleware.ts ❌ Not fixed
14 ERR_VC_USER_NOT_CONFIGURED references CLI command ⚠️ Partially addressed — now context-aware, still references brv vc config

Issues by priority

🔴 Security

Issue 1 — CORS on /api/ui/config (webui-middleware.ts:51) ← inline comment posted

Still no Access-Control-Allow-Origin restriction after nine review cycles. Any cross-origin page in the same browser can silently fetch('http://localhost:7700/api/ui/config') and read daemonPort, projectCwd, and version, then use those to open a privileged Socket.IO connection to the daemon. This is the most important fix before merge.

🟡 Correctness / Robustness

Issues 2+3+4 — webui:setPort (brv-server.ts:522–527) ← inline comment posted

Three separate bugs in the same window, present since review #1:

  • No port bounds check → 0, negative, or >65535 crashes start() after the old server is already down
  • Stop-before-start → if new port is busy, zero servers are running
  • getPort() called after stop() resolves (which clears this.port in webui-server.ts:62) → always logs "Stopped web UI server on port ?"

Issue 5 — Empty path traversal (context-tree-handler.ts:192) ← inline comment posted

data.path === '' resolves to contextTreeDir itself; the guard passes and writeFile throws EISDIR. One-line fix: if (!data.path || resolved.startsWith('..') || resolved.startsWith('/')).

Issue 11 — --port flag range (webui.ts:13) ← inline comment posted

Flags.integer with no min/max lets brv webui -p 0 or brv webui -p 70000 reach the daemon unvalidated.

Issue 12 — handleMerge branch validation (vc-handler.ts)

listBranches({directory, remote: 'origin'}) returns only remote-tracking refs. A user trying to merge a local-only branch (never pushed) gets BRANCH_NOT_FOUND. Fix: also check listBranches({directory}) without a remote.

Issue 14 — ERR_VC_USER_NOT_CONFIGURED (error-messages.ts:24–27) ← inline comment posted

The context-aware rewrite (adding projectPath) is an improvement. However, the message still references brv vc config, a CLI command that web UI users cannot run from the browser. Use neutral copy or a link to the Configuration page instead.

🔵 Maintainability / YAGNI

  • Issue 6branch?: string in ContextTreeGetNodesRequest, ContextTreeGetFileRequest, ContextTreeUpdateFileRequest is never read by any handler — inside-out schema design.
  • Issue 7 — Web UI server construction duplicated verbatim at startup and in webui:setPort. Extract to a factory.
  • Issue 9WEBUI_CONFIG_FILE = 'webui-config.json' is a local constant in webui-state.ts:7; all other file names live in server/constants.ts.
  • Issue 13webui-middleware.ts still has no tests across nine review cycles.

New code review (PR #491) ✅ with one note

  • display-path.ts — Cross-platform home-path collapsing via a single regex handles macOS (/Users/<u>/…), Linux (/home/<u>/…), and Windows (<drive>:\Users\<u>\…) with correct separator preservation. Tests cover all three platforms, edge cases (home dir itself, non-home paths, Windows UNC, empty string), and are thorough.
  • project-name.ts — Cross-platform leaf extraction using split(/[/\\]/) + trailing separator stripping is correct. Tests cover Unix, Windows (backslash, forward-slash, mixed, UNC), and degenerate inputs (empty string, separator-only, no separator).
  • error-messages.ts — The ErrorContext interface + OverrideValue = ((ctx: ErrorContext) => string) | string union is clean and type-safe. The formatError signature change is backward-compatible. Tests cover all context permutations: with path, without path, and non-function overrides ignoring context.
  • changes-panel.tsx — Passing {projectPath: selectedProject} to all three formatError calls in commit/merge-commit/stage-all paths is correct. selectedProject from useTransportStore is the right source of truth.
  • One note: ERR_VC_USER_NOT_CONFIGURED still directs users to run brv vc config — a CLI-only affordance not available in the browser. See inline comment on error-messages.ts:27.

Score: 3 / 5

Unchanged for the ninth time. PR #491 is solid work — the cross-platform path utilities are correct and well-tested, and the context-aware error message infrastructure is a clean abstraction. But the same four blocking issues flagged in review #1 — CORS exposure on the config endpoint (webui-middleware.ts:51), unvalidated port with stop-before-start (brv-server.ts:522–527), empty-path traversal bypass (context-tree-handler.ts:192), and missing port range on the CLI flag (webui.ts:13) — remain completely untouched across nine review cycles. All four have one-line or single-function fixes and must be resolved before merge.

})

// Config endpoint for browser to bootstrap Socket.IO connection
app.get('/api/ui/config', (_req, res) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (security): /api/ui/config still has no CORS restriction — unfixed across nine reviews

Any page open in the same browser can fetch('http://localhost:7700/api/ui/config') and silently read daemonPort, projectCwd, and version, then use those to open a privileged Socket.IO connection to the daemon. Nine review cycles, zero action.

Suggested change
app.get('/api/ui/config', (_req, res) => {
app.get('/api/ui/config', (_req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'null') // block cross-origin fetches
res.json(getConfig())
})

Or add cors({origin: false}) middleware on all /api/* routes. Either way, this must be fixed before merge.

// Stop existing webui server if running
if (webuiServer?.isRunning()) {
await webuiServer.stop()
log(`Stopped web UI server on port ${webuiServer.getPort() ?? '?'}`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (robustness): three problems remain here — unfixed across nine reviews

1. Port logged as "?": WebUiServer.stop() sets this.port = undefined before resolving (see webui-server.ts:62). By the time this line runs, getPort() returns undefined, so the log always prints "Stopped web UI server on port ?". Capture the port before calling stop().

2. No port validation: data.port has no bounds check — 0, negative, or >65535 reaches start() after the old server is already down.

3. Stop-before-start availability gap: If start(newPort) throws (port busy), no server is running.

Suggested fix for the port-log bug:

Suggested change
log(`Stopped web UI server on port ${webuiServer.getPort() ?? '?'}`)
log(`Stopped web UI server on port ${webuiServer.getPort() ?? '?'}`)

Wait — the line is already like this but the port is already cleared by the time we get here. The real fix is to capture the port before stopping:

const oldPort = webuiServer.getPort()
await webuiServer.stop()
log(`Stopped web UI server on port ${oldPort ?? '?'}`)

And invert the start/stop ordering:

const newWebuiServer = new WebUiServer(newApp)
await newWebuiServer.start(newPort)   // start first — if it fails, old server still runs
const oldPort = webuiServer?.getPort()
if (webuiServer?.isRunning()) await webuiServer.stop()
webuiServer = newWebuiServer
log(`Moved web UI server from port ${oldPort ?? '?'} to ${newPort}`)

Also add port validation before any of this:

if (!Number.isInteger(newPort) || newPort < 1 || newPort > 65535) {
  return {port: webuiServer?.getPort() ?? WEBUI_DEFAULT_PORT, success: false}
}


// Guard against path traversal
const resolved = relative(contextTreeDir, fullPath)
if (resolved.startsWith('..') || resolved.startsWith('/')) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (correctness): empty data.path still bypasses traversal guard — unfixed across nine reviews

data.path === '' resolves join(contextTreeDir, '') to contextTreeDir itself. relative(contextTreeDir, contextTreeDir) returns '', which doesn't start with .. or /, so the guard passes and writeFile is called on a directory, throwing EISDIR.

Suggested change
if (resolved.startsWith('..') || resolved.startsWith('/')) {
if (!data.path || resolved.startsWith('..') || resolved.startsWith('/')) {

port: Flags.integer({
char: 'p',
description: 'Set the web UI port (remembered for future use)',
}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (robustness): --port flag still has no range constraint — unfixed from review #2

Flags.integer with no min/max lets brv webui -p 0 or brv webui -p 70000 reach the daemon unvalidated. Combined with the daemon's stop-before-start ordering, any out-of-range value permanently disables the web UI until daemon restart.

Suggested change
}),
port: Flags.integer({
char: 'p',
description: 'Set the web UI port (remembered for future use)',
max: 65535,
min: 1,
}),

ERR_VC_USER_NOT_CONFIGURED: ({projectPath}) =>
projectPath
? `Please run \`brv vc config\` in "${projectPath}" to set your commit author before committing.`
: 'Please run `brv vc config` inside your project to set commit author before committing.',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (UX): ERR_VC_USER_NOT_CONFIGURED still references brv vc config — unfixed from review #7, partially addressed

The context-aware rewrite (adding projectPath to the message) is an improvement — the user now knows which project to configure. But brv vc config is still a CLI command the web UI user cannot run directly from the browser. Consider replacing the CLI instruction with a link to the Configuration tab/page where the user can set their author name and email via the web UI itself:

Suggested change
: 'Please run `brv vc config` inside your project to set commit author before committing.',
ERR_VC_USER_NOT_CONFIGURED: ({projectPath}) =>
projectPath
? `Commit author is not configured for "${projectPath}". Set your name and email in the Configuration page.`
: 'Commit author is not configured. Set your name and email in the Configuration page.',

If the Configuration page does not yet expose VC author settings, neutral copy like 'Configure your commit author name and email before committing.' is still better than directing the user to a command they cannot run from the browser.

The daemon's state:getProjectConfig handler now also reads the
context-tree git remote (defaulting to origin) off disk and returns
it alongside the existing project config, running in parallel with
the config load so the handler's latency is unchanged. The web UI's
project dropdown drops buildRemoteSpaceUrl + the webAppUrl prop
threading in favour of reading remoteUrl directly from the config
response, so "Open Remote space" now opens whatever the git remote
actually points at.
Pull readContextTreeRemoteUrl out of brv-server.ts into its own
module under server/infra/context-tree so the daemon entry file
stays a thin wiring layer and the helper gets real unit coverage
(happy path, missing git repo, no remote configured, non-default
remote name, missing project path). The project dropdown now
refuses to open a remote URL that isn't http(s) — SSH remotes
aren't browser-openable anyway, and guarding the scheme closes
the window where a tampered .git/config could smuggle a
javascript: or file: URI into window.open.
feat: [ENG-2167] expose context-tree remote URL via project config
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 21, 2026

Claude finished @ncnthien's task in 3m 59s —— View job


PR Review: Local Web UI (Re-review #10)


What's new since review #9

Two commits merged since the last review cycle (PR #498):

Commit Description
ece6d584 feat: [ENG-2167] expose context-tree remote URL via project config
7fc3f2bb feat: [ENG-2167] test context-tree remote reader and gate unsafe URLs

The new code adds read-context-tree-remote.ts — a thin wrapper around isomorphic-git.getConfig that reads the remote.origin.url from the project's .brv/context-tree git repo and returns undefined silently on any error. This URL is now surfaced via state:getProjectConfig as remoteUrl and used directly in OpenProjectItem to power the "Open Remote space" menu action, replacing the previous buildRemoteSpaceUrl(BRV_WEB_APP_URL + teamName + spaceName) approach.


Prior issues — status (10 reviews in)

# Issue Status
1 CORS restriction missing on /api/ui/config ❌ Not fixed
2 webui:setPort accepts unvalidated port values ❌ Not fixed
3 Stop-before-start race on port change ❌ Not fixed
4 Port logged as "?" after stop() ❌ Not fixed
5 Path traversal guard passes empty data.path ❌ Not fixed
6 branch field declared in schema but never read ❌ Not fixed
7 Duplicated web UI server construction logic ❌ Not fixed
8 isExcludedFromSync not applied per-directory ❌ Not fixed
9 WEBUI_CONFIG_FILE hardcoded instead of exported constant ❌ Not fixed
10 Login dialog polling/subscription race condition ❌ Not fixed
11 --port flag has no range constraint ❌ Not fixed
12 handleMerge only validates remote-tracking branches ❌ Not fixed
13 Missing tests for webui-middleware.ts ❌ Not fixed
14 ERR_VC_USER_NOT_CONFIGURED references CLI command ❌ Not fixed

Issues by priority

🔴 Security

Issue 1 — CORS on /api/ui/config (webui-middleware.ts:51) ← inline comment posted

Still no Access-Control-Allow-Origin restriction after ten review cycles. Any cross-origin page in the same browser can silently fetch('http://localhost:7700/api/ui/config') and read daemonPort, projectCwd, and version, then use those to open a privileged Socket.IO connection to the daemon. This is the most important fix before merge.

🟡 Correctness / Robustness

Issues 2+3+4 — webui:setPort (brv-server.ts:527–532) ← inline comment posted

Three separate bugs in the same window, present since review #1:

  • No port bounds check → 0, negative, or >65535 crashes start() after the old server is already down
  • Stop-before-start → if new port is busy, zero servers are running
  • getPort() called after stop() resolves (which clears this.port) → always logs "Stopped web UI server on port ?"

Issue 5 — Empty path traversal (context-tree-handler.ts:192) ← inline comment posted

data.path === '' resolves to contextTreeDir itself; the guard passes and writeFile throws EISDIR. One-line fix: add !data.path ||.

Issue 11 — --port flag range (webui.ts:13) ← inline comment posted

Flags.integer with no min/max lets brv webui -p 0 or brv webui -p 70000 reach the daemon unvalidated.

Issue 12 — handleMerge branch validation (vc-handler.ts)

listBranches({directory, remote: 'origin'}) returns only remote-tracking refs. A user trying to merge a local-only branch gets BRANCH_NOT_FOUND. Fix: also check listBranches({directory}) without a remote.

Issue 14 — ERR_VC_USER_NOT_CONFIGURED (error-messages.ts:27) ← inline comment posted

Still references brv vc config, a CLI command web UI users cannot run. Use neutral copy or a link to the Configuration page.

🔵 Maintainability / YAGNI

  • Issue 6branch?: string in ContextTreeGetNodesRequest, ContextTreeGetFileRequest, ContextTreeUpdateFileRequest is never read by any handler — inside-out schema design.
  • Issue 7 — Web UI server construction duplicated verbatim at startup and in webui:setPort. Extract to a factory.
  • Issue 9WEBUI_CONFIG_FILE = 'webui-config.json' is a local constant in webui-state.ts:7; all other file names live in server/constants.ts.
  • Issue 13webui-middleware.ts still has no tests across ten review cycles.
  • New Issue 15buildRemoteSpaceUrl (project/utils/build-remote-space-url.ts) and useGetEnvironmentConfig (config/api/get-environment-config.ts) are now dead code — no consumer remains in the codebase after PR feat: [ENG-2167] expose context-tree remote URL via project config #498 removed the last import from project-dropdown.tsx. Both files should be deleted. ← inline comment posted

New code review (PR #498) ✅

  • readContextTreeRemoteUrl — clean, minimal, well-documented. Returns undefined silently on all error paths (non-git dir, missing remote, missing project). The right contract for this use case.
  • Test coverage is thorough: git repo with remote, no git repo, git repo with no remote, non-default remote name, missing project path (no throw). All cases correct.
  • isSafeHttpUrl guard on projectConfig.remoteUrl in OpenProjectItem correctly blocks SSH remotes (git@host:…) and any tampered javascript: / file: URIs from a malicious .git/config. This is the right trust model for user-controlled config files.
  • Removing webAppUrl prop from OpenProjectItem simplifies the component and eliminates a dependency on BRV_WEB_APP_URL. The context-tree remote URL IS the canonical remote space URL, so the menu item still opens the right destination.
  • One new issue: buildRemoteSpaceUrl and useGetEnvironmentConfig (and the config:getEnvironment socket event's only webui caller) are now dead code. These should be removed before merge to avoid confusion about which mechanism is authoritative.

Score: 3 / 5

Unchanged for the tenth time. PR #498 is clean and well-tested — the context-tree remote URL approach is simpler and more direct than the BRV_WEB_APP_URL + team/space construction it replaces. But the same four blocking issues flagged in review #1 — CORS exposure on the config endpoint (webui-middleware.ts:51), unvalidated port with stop-before-start (brv-server.ts:527–532), empty-path traversal bypass (context-tree-handler.ts:192), and missing port range on the CLI flag (webui.ts:13) — remain completely untouched across ten review cycles. All four have one-line or single-function fixes and must be resolved before merge.

})

// Config endpoint for browser to bootstrap Socket.IO connection
app.get('/api/ui/config', (_req, res) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (security): /api/ui/config still has no CORS restriction — unfixed across ten reviews

Any page open in the same browser can fetch('http://localhost:7700/api/ui/config') and silently read daemonPort, projectCwd, and version, then use those to open a privileged Socket.IO connection to the daemon. Ten review cycles, zero action.

Suggested change
app.get('/api/ui/config', (_req, res) => {
app.get('/api/ui/config', (_req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'null') // block cross-origin fetches
res.json(getConfig())
})

Or add cors({origin: false}) middleware on all /api/* routes. Either way, this must be fixed before merge.

// Stop existing webui server if running
if (webuiServer?.isRunning()) {
await webuiServer.stop()
log(`Stopped web UI server on port ${webuiServer.getPort() ?? '?'}`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (robustness): three problems remain here — unfixed across ten reviews

1. Port logged as "?": WebUiServer.stop() sets this.port = undefined before resolving. By the time line 532 runs, getPort() returns undefined. Capture the port before calling stop():

const oldPort = webuiServer.getPort()
await webuiServer.stop()
log(`Stopped web UI server on port ${oldPort ?? '?'}`)

2. No port validation: data.port has no bounds check — 0, negative, or >65535 reaches start() after the old server is already down. Add before the stop call:

if (!Number.isInteger(newPort) || newPort < 1 || newPort > 65535) {
  return {port: webuiServer?.getPort() ?? WEBUI_DEFAULT_PORT, success: false}
}

3. Stop-before-start availability gap: If start(newPort) throws (port busy), no server is running. Start the new server first, only stop the old after new is confirmed up.


// Guard against path traversal
const resolved = relative(contextTreeDir, fullPath)
if (resolved.startsWith('..') || resolved.startsWith('/')) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (correctness): empty data.path still bypasses traversal guard — unfixed across ten reviews

data.path === '' resolves join(contextTreeDir, '') to contextTreeDir itself. relative(contextTreeDir, contextTreeDir) returns '', which doesn't start with .. or /, so the guard passes and writeFile is called on a directory, throwing EISDIR.

Suggested change
if (resolved.startsWith('..') || resolved.startsWith('/')) {
if (!data.path || resolved.startsWith('..') || resolved.startsWith('/')) {

port: Flags.integer({
char: 'p',
description: 'Set the web UI port (remembered for future use)',
}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (robustness): --port flag still has no range constraint — unfixed from review #2

Flags.integer with no min/max lets brv webui -p 0 or brv webui -p 70000 reach the daemon unvalidated. Combined with the daemon's stop-before-start ordering, any out-of-range value permanently disables the web UI until daemon restart.

Suggested change
}),
port: Flags.integer({
char: 'p',
description: 'Set the web UI port (remembered for future use)',
max: 65535,
min: 1,
}),

ERR_VC_USER_NOT_CONFIGURED: ({projectPath}) =>
projectPath
? `Please run \`brv vc config\` in "${projectPath}" to set your commit author before committing.`
: 'Please run `brv vc config` inside your project to set commit author before committing.',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (UX): ERR_VC_USER_NOT_CONFIGURED still references brv vc config — unfixed from review #7

brv vc config is a CLI command that web UI users cannot run from the browser. Use neutral copy that works in both contexts:

Suggested change
: 'Please run `brv vc config` inside your project to set commit author before committing.',
? `Commit author is not configured for "${projectPath}". Set your name and email in the Configuration page.`
: 'Commit author is not configured. Set your name and email in the Configuration page.',

If the Configuration page does not yet expose VC author settings, 'Configure your commit author name and email before committing.' is still better than directing users to a CLI command they cannot run.


export function buildRemoteSpaceUrl({spaceName, teamName, webAppUrl}: BuildRemoteSpaceUrlInput): string | undefined {
if (!teamName || !spaceName || !webAppUrl) return undefined
const base = webAppUrl.replace(/\/+$/, '')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (cleanup): buildRemoteSpaceUrl is now dead code after PR #498

project-dropdown.tsx no longer calls buildRemoteSpaceUrl — it now reads projectConfig.remoteUrl directly. Neither this file nor get-environment-config.ts (useGetEnvironmentConfig) has any remaining import in the codebase. Both should be deleted to avoid the impression that the buildRemoteSpaceUrl + BRV_WEB_APP_URL path is still the intended mechanism.

Copy link
Copy Markdown
Collaborator

@bao-byterover bao-byterover left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Copy Markdown
Collaborator

@cuongdo-byterover cuongdo-byterover left a comment

Choose a reason for hiding this comment

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

lgtm!

@bao-byterover bao-byterover merged commit 7239365 into main Apr 21, 2026
6 checks passed
@bao-byterover bao-byterover deleted the proj/local-web-ui branch April 21, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants