@@ -3,10 +3,36 @@ import { Flag } from "@opencode-ai/core/flag/flag"
33import { Effect , Stream } from "effect"
44import { HttpBody , HttpClient , HttpClientRequest , HttpServerRequest , HttpServerResponse } from "effect/unstable/http"
55import { createHash } from "node:crypto"
6+ import os from "node:os"
67import path from "node:path"
78import { fileURLToPath } from "node:url"
89import { ProxyUtil } from "../proxy-util"
910
11+ const SERVER_HOSTNAME = ( ( ) => {
12+ try {
13+ return os . hostname ( ) . replace ( / \. l o c a l $ / i, "" )
14+ } catch {
15+ return ""
16+ }
17+ } ) ( )
18+
19+ function serverDisplayName ( request : HttpServerRequest . HttpServerRequest ) {
20+ if ( SERVER_HOSTNAME ) return SERVER_HOSTNAME
21+ const host = request . headers [ "host" ] ?? ""
22+ return host . split ( ":" ) [ 0 ] ?? ""
23+ }
24+
25+ function escapeHtml ( value : string ) {
26+ return value . replace ( / [ & < > " ' ] / g, ( c ) =>
27+ c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : c === '"' ? """ : "'" ,
28+ )
29+ }
30+
31+ function rewriteTitle ( html : string , name : string ) {
32+ if ( ! name ) return html
33+ return html . replace ( / < t i t l e \b [ ^ > ] * > [ \s \S ] * ?< \/ t i t l e > / i, `<title>${ escapeHtml ( name ) } - OpenCode</title>` )
34+ }
35+
1036const embeddedUIPromise = Flag . OPENCODE_DISABLE_EMBEDDED_WEB_UI
1137 ? Promise . resolve ( null )
1238 : // @ts -expect-error - generated file at build time
@@ -70,11 +96,13 @@ function notFound() {
7096 return HttpServerResponse . jsonUnsafe ( { error : "Not Found" } , { status : 404 } )
7197}
7298
73- function embeddedUIResponse ( file : string , body : Uint8Array ) {
99+ function embeddedUIResponse ( file : string , body : Uint8Array , name : string ) {
74100 const mime = AppFileSystem . mimeType ( file )
75101 const headers = new Headers ( { "content-type" : mime } )
76102 if ( mime . startsWith ( "text/html" ) ) {
77- headers . set ( "content-security-policy" , cspForHtml ( new TextDecoder ( ) . decode ( body ) ) )
103+ const rewritten = rewriteTitle ( new TextDecoder ( ) . decode ( body ) , name )
104+ headers . set ( "content-security-policy" , cspForHtml ( rewritten ) )
105+ return HttpServerResponse . raw ( new TextEncoder ( ) . encode ( rewritten ) , { headers } )
78106 }
79107 return HttpServerResponse . raw ( body , { headers } )
80108}
@@ -83,14 +111,15 @@ export function serveEmbeddedUIEffect(
83111 requestPath : string ,
84112 fs : AppFileSystem . Interface ,
85113 embeddedWebUI : Record < string , string > ,
114+ name : string ,
86115) {
87116 const file = embeddedWebUI [ requestPath . replace ( / ^ \/ / , "" ) ] ?? embeddedWebUI [ "index.html" ] ?? null
88117 if ( ! file ) return Effect . succeed ( notFound ( ) )
89118
90119 const resolved = embeddedUIFile ( file )
91120
92121 return fs . readFile ( resolved ) . pipe (
93- Effect . map ( ( body ) => embeddedUIResponse ( resolved , body ) ) ,
122+ Effect . map ( ( body ) => embeddedUIResponse ( resolved , body , name ) ) ,
94123 Effect . catchReason ( "PlatformError" , "NotFound" , ( ) => Effect . succeed ( notFound ( ) ) ) ,
95124 )
96125}
@@ -99,13 +128,14 @@ function serveLocalDirEffect(
99128 requestPath : string ,
100129 fs : AppFileSystem . Interface ,
101130 dir : string ,
131+ name : string ,
102132) {
103133 const filePath = path . join ( dir , requestPath === "/" ? "index.html" : requestPath )
104134 return fs . readFile ( filePath ) . pipe (
105- Effect . map ( ( body ) => embeddedUIResponse ( filePath , body ) ) ,
135+ Effect . map ( ( body ) => embeddedUIResponse ( filePath , body , name ) ) ,
106136 Effect . catchReason ( "PlatformError" , "NotFound" , ( ) =>
107137 fs . readFile ( path . join ( dir , "index.html" ) ) . pipe (
108- Effect . map ( ( body ) => embeddedUIResponse ( path . join ( dir , "index.html" ) , body ) ) ,
138+ Effect . map ( ( body ) => embeddedUIResponse ( path . join ( dir , "index.html" ) , body , name ) ) ,
109139 Effect . catchReason ( "PlatformError" , "NotFound" , ( ) => Effect . succeed ( notFound ( ) ) ) ,
110140 ) ,
111141 ) ,
@@ -119,11 +149,13 @@ export function serveUIEffect(
119149 return Effect . gen ( function * ( ) {
120150 const embeddedWebUI = yield * Effect . promise ( ( ) => embeddedUI ( ) )
121151 const requestPath = new URL ( request . url , "http://localhost" ) . pathname
152+ const name = serverDisplayName ( request )
122153
123- if ( embeddedWebUI ) return yield * serveEmbeddedUIEffect ( requestPath , services . fs , embeddedWebUI )
154+ if ( embeddedWebUI ) return yield * serveEmbeddedUIEffect ( requestPath , services . fs , embeddedWebUI , name )
124155
125156 // Dev mode: serve from local build directory if configured
126- if ( Flag . OPENCODE_DEV_UI_DIR ) return yield * serveLocalDirEffect ( requestPath , services . fs , Flag . OPENCODE_DEV_UI_DIR )
157+ if ( Flag . OPENCODE_DEV_UI_DIR )
158+ return yield * serveLocalDirEffect ( requestPath , services . fs , Flag . OPENCODE_DEV_UI_DIR , name )
127159
128160 const response = yield * services . client . execute (
129161 HttpClientRequest . make ( request . method ) ( upstreamURL ( requestPath ) , {
@@ -134,7 +166,7 @@ export function serveUIEffect(
134166 const headers = proxyResponseHeaders ( response . headers )
135167
136168 if ( response . headers [ "content-type" ] ?. includes ( "text/html" ) ) {
137- const body = yield * response . text
169+ const body = rewriteTitle ( yield * response . text , name )
138170 headers . set ( "Content-Security-Policy" , cspForHtml ( body ) )
139171 return HttpServerResponse . text ( body , { status : response . status , headers } )
140172 }
0 commit comments