Skip to content

Commit 8644be6

Browse files
committed
feat: [ENG-2465] add web UI Configuration page + promote ByteRover provider onboarding
Configuration page - Git identity panel: get/set user.name & user.email via the daemon, one-click seed from the signed-in ByteRover account. - Remote panel: single `origin`, HTTPS-only validation, replace preview, Initialize VC CTA when git is uninitialized, "Find in ByteRover" link. - Shared SettingsSection + CalloutRow primitives; `bg-card` alignment with ConnectorsPanel; skeleton loading rows. Provider onboarding - ByteRover pinned to top of the selector with an amber "Native" pill. - OAuth popup opens on row click (drops the redundant login-prompt confirmation); 800ms minimum visible delay before popup navigates. - ProviderFlowDialog mountable globally via provider-store so any surface can open it (task error CTA, etc.). Error routing - toastVcError maps VC config gaps to a toast action that deep-links to /configuration#identity or #remotes. - Task ErrorSection shows "Configure provider" for provider-class errors; checks llmservice:error broadcast (task:error isn't always structured). - formatError overrides aligned with Configuration page copy. Header - Responsive: Local badge hides <lg, HelpMenu/BranchDropdown/version collapse at narrower sizes, provider label truncates by breakpoint. - StatusDot primitive (tone + pulsing) — pulsing amber when unconfigured, green corner dot on the Plug when active. - `brv vc init` invalidates the remote query so panel auto-refreshes. Other - ProjectDropdown: "Open local folder" opens OS file manager via new locations:reveal transport event (registry-guarded, no shell interp). - Global toast position = top-center; removed per-call overrides. - CSP: allow img-src https: for OAuth provider avatars. - BRV_GIT_REMOTE_BASE_URL surfaced via config:getEnvironment for the Remote URL input placeholder. Tests - Pure util tests: detect-git-url-type, is-valid-email, validate-remote-url, is-provider-task-error, reveal-command.
1 parent 2e0e45e commit 8644be6

62 files changed

Lines changed: 1610 additions & 198 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/server/infra/transport/handlers/config-handler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class ConfigHandler {
2222
this.transport.onRequest<void, ConfigGetEnvironmentResponse>(ConfigEvents.GET_ENVIRONMENT, () => {
2323
const config = getCurrentConfig()
2424
return {
25+
gitRemoteBaseUrl: config.gitRemoteBaseUrl,
2526
iamBaseUrl: config.iamBaseUrl,
2627
isDevelopment: isDevelopment(),
2728
webAppUrl: config.webAppUrl,

src/server/infra/transport/handlers/locations-handler.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
import {spawn} from 'node:child_process'
12
import {join} from 'node:path'
23

34
import type {ProjectLocationDTO} from '../../../../shared/transport/types/dto.js'
45
import type {IContextTreeService} from '../../../core/interfaces/context-tree/i-context-tree-service.js'
56
import type {IProjectRegistry} from '../../../core/interfaces/project/i-project-registry.js'
67
import type {ITransportServer} from '../../../core/interfaces/transport/i-transport-server.js'
78

8-
import {LocationsEvents, type LocationsGetResponse} from '../../../../shared/transport/events/locations-events.js'
9+
import {
10+
LocationsEvents,
11+
type LocationsGetResponse,
12+
type LocationsRevealRequest,
13+
type LocationsRevealResponse,
14+
} from '../../../../shared/transport/events/locations-events.js'
915
import {BRV_DIR, CONTEXT_TREE_DIR} from '../../../constants.js'
1016
import {type ProjectPathResolver} from './handler-types.js'
17+
import {resolveRevealCommand} from './reveal-command.js'
1118

1219
export interface LocationsHandlerDeps {
1320
contextTreeService: IContextTreeService
@@ -49,6 +56,11 @@ export class LocationsHandler {
4956
return {locations: []}
5057
}
5158
})
59+
60+
this.transport.onRequest<LocationsRevealRequest, LocationsRevealResponse>(
61+
LocationsEvents.REVEAL,
62+
async (data) => this.handleReveal(data),
63+
)
5264
}
5365

5466
private async buildLocations(currentProjectPath?: string): Promise<ProjectLocationDTO[]> {
@@ -96,4 +108,28 @@ export class LocationsHandler {
96108
return (all.get(b.projectPath)?.registeredAt ?? 0) - (all.get(a.projectPath)?.registeredAt ?? 0)
97109
})
98110
}
111+
112+
private async handleReveal(data: LocationsRevealRequest): Promise<LocationsRevealResponse> {
113+
const {projectPath} = data
114+
if (!projectPath) throw new Error('projectPath is required')
115+
116+
// Only allow revealing paths that are registered projects — the client
117+
// controls this argument, so we must not trust it blindly.
118+
const registered = this.projectRegistry.getAll()
119+
if (!registered.has(projectPath)) {
120+
throw new Error('Project is not registered.')
121+
}
122+
123+
const exists = await this.pathExists(projectPath).catch(() => false)
124+
if (!exists) throw new Error('Project folder no longer exists.')
125+
126+
const {args, command} = resolveRevealCommand(process.platform, projectPath)
127+
const child = spawn(command, args, {detached: true, stdio: 'ignore'})
128+
child.on('error', () => {
129+
/* best-effort — nothing to report back once the ack resolved */
130+
})
131+
child.unref()
132+
133+
return {projectPath}
134+
}
99135
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Resolves the OS-native command for revealing a path in the system file manager.
3+
* Extracted so the branching can be unit-tested without spawning real processes.
4+
*/
5+
export type RevealCommand = {
6+
args: string[]
7+
command: string
8+
}
9+
10+
export function resolveRevealCommand(platformName: NodeJS.Platform, targetPath: string): RevealCommand {
11+
if (platformName === 'darwin') return {args: [targetPath], command: 'open'}
12+
if (platformName === 'win32') return {args: [targetPath], command: 'explorer'}
13+
return {args: [targetPath], command: 'xdg-open'}
14+
}

src/server/infra/webui/webui-middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function createWebUiMiddleware({getConfig, webuiDistDir}: CreateWebUiMidd
3535
"connect-src 'self' ws: wss:",
3636
"font-src 'self' data: https://fonts.gstatic.com",
3737
"frame-ancestors 'none'",
38-
"img-src 'self' data:",
38+
"img-src 'self' data: https:",
3939
"object-src 'none'",
4040
"script-src 'self'",
4141
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",

src/shared/transport/events/config-events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const ConfigEvents = {
77
} as const
88

99
export interface ConfigGetEnvironmentResponse {
10+
gitRemoteBaseUrl: string
1011
iamBaseUrl: string
1112
isDevelopment: boolean
1213
webAppUrl: string

src/shared/transport/events/locations-events.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,17 @@ import type {ProjectLocationDTO} from '../types/dto.js'
22

33
export const LocationsEvents = {
44
GET: 'locations:get',
5+
REVEAL: 'locations:reveal',
56
} as const
67

78
export interface LocationsGetResponse {
89
locations: ProjectLocationDTO[]
910
}
11+
12+
export interface LocationsRevealRequest {
13+
projectPath: string
14+
}
15+
16+
export interface LocationsRevealResponse {
17+
projectPath: string
18+
}

src/webui/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export function App() {
88
return (
99
<AppProviders>
1010
<RouterProvider router={router} />
11-
<Toaster />
11+
<Toaster position="top-center" />
1212
</AppProviders>
1313
)
1414
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {cn} from '@campfirein/byterover-packages/lib/utils'
2+
3+
type Tone = 'amber' | 'destructive' | 'info' | 'success'
4+
5+
type Props = {
6+
className?: string
7+
/** When true, wrap the dot in a `ping`-animated halo to draw attention. */
8+
pulsing?: boolean
9+
tone: Tone
10+
}
11+
12+
const TONE_BG: Record<Tone, string> = {
13+
amber: 'bg-amber-500',
14+
destructive: 'bg-destructive',
15+
info: 'bg-blue-500',
16+
success: 'bg-primary-foreground',
17+
}
18+
19+
/**
20+
* Small colored dot for status annotations.
21+
*
22+
* - Default: a single solid dot — use for permanent indicators
23+
* (e.g. "connected", "X unread").
24+
* - `pulsing`: dot wrapped in a `ping`-animated halo — use for exceptions
25+
* that want attention (e.g. "configuration required").
26+
*
27+
* Default size is 6px; pass `size-*` via `className` to resize. `className`
28+
* can also add border/position utilities (e.g. overlaying as a corner badge
29+
* on an icon).
30+
*/
31+
export function StatusDot({className, pulsing = false, tone}: Props) {
32+
const bg = TONE_BG[tone]
33+
34+
if (pulsing) {
35+
return (
36+
<span className={cn('relative inline-flex size-1.5 shrink-0', className)}>
37+
<span className={cn('absolute inline-flex size-full animate-ping rounded-full opacity-75', bg)} />
38+
<span className={cn('relative inline-flex size-full rounded-full', bg)} />
39+
</span>
40+
)
41+
}
42+
43+
return <span className={cn('inline-block size-1.5 shrink-0 rounded-full', bg, className)} />
44+
}

src/webui/features/auth/components/auth-menu.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function UnauthorizedTrigger() {
2424

2525
return (
2626
<>
27-
<Button className="h-auto rounded-full p-0 ml-2.5" onClick={() => setIsOpen(true)} variant="ghost">
27+
<Button className="h-auto rounded-full p-0" onClick={() => setIsOpen(true)} variant="ghost">
2828
<Avatar>
2929
<AvatarFallback className="bg-transparent">
3030
<User className="size-4 shrink-0" />
@@ -49,7 +49,7 @@ function AuthorizedMenu() {
4949

5050
if (!user) {
5151
return (
52-
<Button className="ml-2.5" disabled size="sm" variant="outline">
52+
<Button disabled size="sm" variant="outline">
5353
<User className="size-4 shrink-0 text-muted-foreground" />
5454
<span>Signed in</span>
5555
</Button>
@@ -89,7 +89,7 @@ export function AuthMenu() {
8989

9090
if (isLoadingInitial) {
9191
return (
92-
<Button className="ml-2.5" disabled size="icon" variant="outline">
92+
<Button disabled size="icon" variant="outline">
9393
<User className="size-4 shrink-0 text-muted-foreground animate-pulse" />
9494
</Button>
9595
)

src/webui/features/auth/components/login-dialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function LoginDialog({onOpenChange, open}: LoginDialogProps) {
5151

5252
const unsubscribe = subscribeToLoginCompleted((data) => {
5353
if (data.success && data.user) {
54-
toast.success(`Logged in as ${data.user.email}`, {position: 'top-center'})
54+
toast.success(`Logged in as ${data.user.email}`)
5555
queryClient.invalidateQueries({queryKey: getAuthStateQueryOptions().queryKey})
5656
onOpenChange(false)
5757
} else {
@@ -76,7 +76,7 @@ export function LoginDialog({onOpenChange, open}: LoginDialogProps) {
7676
const result = await queryClient.fetchQuery(getAuthStateQueryOptions())
7777
if (cancelled) return
7878
if (result.isAuthorized && result.user) {
79-
toast.success(`Logged in as ${result.user.email}`, {position: 'top-center'})
79+
toast.success(`Logged in as ${result.user.email}`)
8080
setLoggingIn(false)
8181
onOpenChange(false)
8282
}

0 commit comments

Comments
 (0)