-
Notifications
You must be signed in to change notification settings - Fork 10
feat(browser-connection): new dedicated package for noVNC + browser connection (issue #347) #348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| { | ||
| "name": "@prover-coder-ai/browser-connection", | ||
| "version": "1.0.0", | ||
| "description": "Reusable noVNC + browser CDP connection module for docker-git (used by MCP, Hermes tools, and project-browser services)", | ||
| "main": "dist/index.js", | ||
| "types": "dist/index.d.ts", | ||
| "files": ["dist"], | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "check": "bun run typecheck", | ||
| "prepack": "bun run build", | ||
| "test": "vitest run --passWithNoTests", | ||
| "typecheck": "tsc --noEmit -p tsconfig.json" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/ProverCoderAI/docker-git.git" | ||
| }, | ||
| "keywords": [ | ||
| "docker-git", | ||
| "browser", | ||
| "novnc", | ||
| "cdp", | ||
| "effect", | ||
| "hermes" | ||
| ], | ||
| "author": "", | ||
| "license": "MIT", | ||
| "type": "module", | ||
| "bugs": { | ||
| "url": "https://github.com/ProverCoderAI/docker-git/issues" | ||
| }, | ||
| "homepage": "https://github.com/ProverCoderAI/docker-git#readme", | ||
| "packageManager": "bun@1.3.11", | ||
| "devDependencies": { | ||
| "@effect/vitest": "^0.29.0", | ||
| "@types/node": "^25.9.1", | ||
| "typescript": "^6.0.3", | ||
| "vitest": "^4.1.7" | ||
| }, | ||
| "dependencies": { | ||
| "effect": "^3.12.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,47 @@ | ||||||||||||
| import { Context, Effect, Layer } from "effect" | ||||||||||||
|
|
||||||||||||
| export class BrowserError { | ||||||||||||
| readonly _tag = "BrowserError" as const | ||||||||||||
| constructor(readonly message: string, readonly cause?: unknown) {} | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| export interface BrowserConnection { | ||||||||||||
| readonly startBrowser: (projectId: string) => Effect.Effect<void, BrowserError> | ||||||||||||
| readonly getCdpUrl: (projectId: string) => Effect.Effect<string, BrowserError> | ||||||||||||
| readonly getNoVncUrl: (projectId: string) => Effect.Effect<string, BrowserError> | ||||||||||||
| readonly getVncUrl: (projectId: string) => Effect.Effect<string, BrowserError> | ||||||||||||
| readonly parseProxyPath: (pathname: string) => Effect.Effect<unknown, never> | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win Метод parseProxyPath должен возвращать типизированное значение вместо unknown. Согласно guidelines для 🔧 Предлагаемое улучшение типизации+/**
+ * Parsed proxy path information
+ */
+export interface ProxyPathInfo {
+ readonly projectId: string
+ readonly path: string
+}
+
export interface BrowserConnection {
// ...
- readonly parseProxyPath: (pathname: string) => Effect.Effect<unknown, never>
+ readonly parseProxyPath: (pathname: string) => Effect.Effect<ProxyPathInfo | null, BrowserError>Согласно coding guidelines: "No 🤖 Prompt for AI Agents |
||||||||||||
| readonly rewriteCdpUrl: (value: string, externalOrigin: string, projectId: string) => string | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+8
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win Отсутствует comprehensive TSDoc для методов интерфейса BrowserConnection. Согласно guidelines для 📝 Пример требуемой документации+/**
+ * Browser connection service for managing single shared browser sessions.
+ *
+ * `@remarks`
+ * PURITY: SHELL - service interface with side effects
+ * EFFECT: Depends on Logger, potentially Docker/Process services
+ * INVARIANT: ∀projectId: getCdpUrl(projectId) and getNoVncUrl(projectId) point to same dg-{projectId}-browser container
+ */
export interface BrowserConnection {
+ /**
+ * Start browser container for the given project.
+ *
+ * `@effect` Launches Docker container, creates network resources
+ * `@complexity` O(1) API call, O(n) container startup time
+ * `@throws` BrowserError if container fails to start
+ */
readonly startBrowser: (projectId: string) => Effect.Effect<void, BrowserError>
+ // ... документация для остальных методовСогласно coding guidelines: "TypeScript functions must include comprehensive TSDoc with parameters, return types, 🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
| export const BrowserConnection = Context.GenericTag<BrowserConnection>("@prover-coder-ai/browser-connection/BrowserConnection") | ||||||||||||
|
|
||||||||||||
| export const BrowserConnectionLive = Layer.effect( | ||||||||||||
| BrowserConnection, | ||||||||||||
| Effect.gen(function* () { | ||||||||||||
| return { | ||||||||||||
| startBrowser: (projectId: string) => | ||||||||||||
| Effect.gen(function* () { | ||||||||||||
| yield* Effect.log(`[browser-connection] starting browser for project ${projectId}`) | ||||||||||||
| return undefined as void | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Запрещено использование type assertion Согласно guidelines для 🔧 Предлагаемое исправление startBrowser: (projectId: string) =>
Effect.gen(function* () {
yield* Effect.log(`[browser-connection] starting browser for project ${projectId}`)
- return undefined as void
}),Effect.gen автоматически выведет тип Согласно coding guidelines: "Type casting with 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| }), | ||||||||||||
| getCdpUrl: (projectId: string) => Effect.succeed(`http://localhost:9223?project=${projectId}`), | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Несоответствие URL между getCdpUrl и renderCdpUrl. Метод 🔧 Предлагаемое исправлениеВариант 1: Использовать helper в реализации Layer: - getCdpUrl: (projectId: string) => Effect.succeed(`http://localhost:9223?project=${projectId}`),
+ getCdpUrl: (projectId: string) => Effect.succeed(renderCdpUrl(projectId)),Вариант 2: Привести helper в соответствие с реализацией Layer: export const renderCdpUrl = (projectId: string): string =>
- `http://localhost:9223/json/version?project=${projectId}`
+ `http://localhost:9223?project=${projectId}`Рекомендуется вариант 1, чтобы избежать дублирования строковых литералов. Also applies to: 41-42 🤖 Prompt for AI Agents |
||||||||||||
| getNoVncUrl: (projectId: string) => Effect.succeed(`/b/${projectId}/vnc.html?autoconnect=true&resize=remote&path=b/${projectId}/websockify`), | ||||||||||||
| getVncUrl: (projectId: string) => Effect.succeed(`vnc://localhost:5900`), | ||||||||||||
| parseProxyPath: (_pathname: string) => Effect.succeed(null), | ||||||||||||
| rewriteCdpUrl: (value: string, _externalOrigin: string, _projectId: string) => value | ||||||||||||
| } | ||||||||||||
| }) | ||||||||||||
| ) | ||||||||||||
|
Comment on lines
+19
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Отсутствуют обязательные functional comments для всех методов Layer. Согласно guidelines для 📋 Пример требуемых functional comments+// CHANGE: Implement BrowserConnectionLive Layer with stub methods for single-session browser management
+// WHY: Consolidate browser control logic to ensure single shared session invariant
+// QUOTE(ТЗ): "When MCP Playwright is installed, it should automatically start noVNC to manage a single shared browser with an agent" (issue `#347`)
+// REF: issue `#347`, PR `#348`
+// SOURCE: docker-git-session-sync patterns, effect-template style
+// FORMAT THEOREM: ∀projectId ∈ String: getCdpUrl(projectId) ∧ getNoVncUrl(projectId) → same container(dg-${projectId}-browser)
+// PURITY: SHELL - contains side effects (logging, future Docker operations)
+// EFFECT: Effect.Effect<BrowserConnection, never, never>
+// INVARIANT: Single browser session per projectId; CDP port 9223, noVNC port 6080 are fixed
+// COMPLEXITY: O(1) for all operations (current stub implementation)
export const BrowserConnectionLive = Layer.effect(
BrowserConnection,
Effect.gen(function* () {
return {
+ // CHANGE: Log browser start event
+ // WHY: Provide observability for browser lifecycle
+ // PURITY: SHELL (logging side effect)
+ // EFFECT: Effect.Effect<void, BrowserError>
+ // COMPLEXITY: O(1)
startBrowser: (projectId: string) =>
Effect.gen(function* () {
yield* Effect.log(`[browser-connection] starting browser for project ${projectId}`)
}),
+ // CHANGE: Return deterministic CDP URL for projectId
+ // PURITY: CORE logic (deterministic string generation)
+ // EFFECT: Effect.Effect<string, BrowserError>
+ // INVARIANT: Port 9223 is fixed for CDP endpoint
+ // COMPLEXITY: O(1)
getCdpUrl: (projectId: string) => Effect.succeed(renderCdpUrl(projectId)),
// ... аналогично для остальных методовСогласно coding guidelines: "Functional comments must include: CHANGE, WHY, QUOTE(ТЗ) or n/a, REF, SOURCE or n/a, FORMAT THEOREM, PURITY (CORE|SHELL), EFFECT signature for SHELL functions, INVARIANT, and COMPLEXITY." Также см. требование из guidelines: "Ты строгий ревьюер SPEC DRIVEN DEVELOPMENT. Сверь изменения с исходным ТЗ/спекой и обсуждением." 🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
| // Pure helpers | ||||||||||||
| export const renderNoVncUrl = (projectId: string): string => | ||||||||||||
| `/b/${projectId}/vnc.html?autoconnect=true&resize=remote&path=b/${projectId}/websockify` | ||||||||||||
|
|
||||||||||||
| export const renderCdpUrl = (projectId: string): string => | ||||||||||||
| `http://localhost:9223/json/version?project=${projectId}` | ||||||||||||
|
|
||||||||||||
| export const isSingleBrowserSession = (cdpUrl: string, noVncUrl: string): boolean => | ||||||||||||
| cdpUrl.includes("9223") && noVncUrl.includes("/vnc.html") | ||||||||||||
|
Comment on lines
+37
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win Отсутствует документация для pure helper functions. Согласно guidelines для 📝 Пример требуемой документации-// Pure helpers
+// CHANGE: Pure helper functions for URL generation and session validation
+// WHY: Separate pure logic (CORE) from effectful service methods (SHELL)
+// PURITY: CORE - deterministic, no side effects
+// INVARIANT: URL format stability across project lifecycle
+/**
+ * Generate noVNC URL for browser session.
+ *
+ * `@pure` true
+ * `@param` projectId - Project identifier
+ * `@returns` noVNC URL with autoconnect and websockify path
+ * `@invariant` Result always includes `/b/${projectId}/vnc.html`
+ * `@complexity` O(1) time, O(n) space where n = projectId.length
+ */
export const renderNoVncUrl = (projectId: string): string =>
`/b/${projectId}/vnc.html?autoconnect=true&resize=remote&path=b/${projectId}/websockify`
+/**
+ * Generate CDP (Chrome DevTools Protocol) URL for browser session.
+ *
+ * `@pure` true
+ * `@param` projectId - Project identifier
+ * `@returns` CDP endpoint URL on port 9223
+ * `@invariant` Port 9223 is fixed for all projects
+ * `@complexity` O(1) time, O(n) space where n = projectId.length
+ */
export const renderCdpUrl = (projectId: string): string =>
`http://localhost:9223/json/version?project=${projectId}`
+/**
+ * Validate that CDP and noVNC URLs belong to single browser session.
+ *
+ * `@pure` true
+ * `@param` cdpUrl - CDP endpoint URL
+ * `@param` noVncUrl - noVNC web interface URL
+ * `@returns` true if both URLs point to same browser session
+ * `@precondition` cdpUrl and noVncUrl are non-empty strings
+ * `@complexity` O(n) where n = max(cdpUrl.length, noVncUrl.length)
+ */
export const isSingleBrowserSession = (cdpUrl: string, noVncUrl: string): boolean =>
cdpUrl.includes("9223") && noVncUrl.includes("/vnc.html")Согласно coding guidelines: "TypeScript functions must include comprehensive TSDoc with parameters, return types, 🤖 Prompt for AI Agents
Comment on lines
+44
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win Слишком упрощенная логика валидации в isSingleBrowserSession. Функция Текущая реализация пропустит любые URL, содержащие эти подстроки, даже если они указывают на разные проекты или контейнеры. 🔧 Предлагаемое улучшение валидации+/**
+ * Extract projectId from browser URL.
+ * `@pure` true
+ */
+const extractProjectId = (url: string): string | null => {
+ // For CDP: http://localhost:9223/json/version?project=abc
+ // For noVNC: /b/abc/vnc.html...
+ const cdpMatch = url.match(/[?&]project=([^&]+)/)
+ if (cdpMatch) return cdpMatch[1]
+
+ const vncMatch = url.match(/\/b\/([^\/]+)\/vnc\.html/)
+ if (vncMatch) return vncMatch[1]
+
+ return null
+}
+
export const isSingleBrowserSession = (cdpUrl: string, noVncUrl: string): boolean => {
- cdpUrl.includes("9223") && noVncUrl.includes("/vnc.html")
+ const cdpProjectId = extractProjectId(cdpUrl)
+ const vncProjectId = extractProjectId(noVncUrl)
+
+ return cdpProjectId !== null &&
+ vncProjectId !== null &&
+ cdpProjectId === vncProjectId &&
+ cdpUrl.includes("9223") &&
+ noVncUrl.includes("/vnc.html")
+}Это обеспечит математический инвариант: оба URL указывают на один projectId. 🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
| export default BrowserConnection | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "extends": "../../tsconfig.base.json", | ||
| "compilerOptions": { | ||
| "rootDir": ".", | ||
| "outDir": "dist", | ||
| "types": ["vitest", "node"] | ||
| }, | ||
| "include": ["src/**/*", "tests/**/*"], | ||
| "exclude": ["dist", "node_modules"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| packages: | ||
| - packages/api | ||
| - packages/app | ||
| - packages/browser-connection | ||
| - packages/docker-git-session-sync | ||
| - packages/lib |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Требуется comprehensive TSDoc для класса BrowserError.
Согласно guidelines для
**/*.{ts,tsx}, все функции и типы должны включать comprehensive TSDoc с параметрами, типами возвращаемых значений, маркерами@pure,@effect,@invariant,@complexityи другими. Класс BrowserError не содержит документации.📝 Предлагаемая документация
Согласно coding guidelines для TypeScript: "TypeScript functions must include comprehensive TSDoc with parameters, return types,
@puremarker,@effectdependencies,@invariant(mathematical),@precondition,@postcondition, and@complexityO-notation."🤖 Prompt for AI Agents