Skip to content

Commit 79b3101

Browse files
committed
feat: --chat-base-path
1 parent 78d8847 commit 79b3101

File tree

8 files changed

+73
-25
lines changed

8 files changed

+73
-25
lines changed

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
CHAT_SOURCES_STAMP = chat/.sources.stamp
22
CHAT_SOURCES = $(shell find chat \( -path chat/node_modules -o -path chat/out -o -path chat/.next \) -prune -o -not -path chat/.sources.stamp -type f -print)
33
BINPATH ?= out/agentapi
4+
# This must be kept in sync with the magicBasePath in lib/httpapi/embed.go.
5+
BASE_PATH ?= /magic-base-path-placeholder
46

57
$(CHAT_SOURCES_STAMP): $(CHAT_SOURCES)
68
@echo "Chat sources changed. Running build steps..."
7-
cd chat && bun run build
9+
cd chat && BASE_PATH=${BASE_PATH} bun run build
810
rm -rf lib/httpapi/chat && mkdir -p lib/httpapi/chat && touch lib/httpapi/chat/marker
911
cp -r chat/out/. lib/httpapi/chat/
1012
touch $@

chat/next.config.mjs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
/** @type {import('next').NextConfig} */
2-
const isGitHubPages = process.env.GITHUB_PAGES === "true";
3-
const repo = "agentapi";
4-
const subPath = "chat"; // Subdirectory within the repo
2+
const basePath = process.env.BASE_PATH ?? "/chat";
53

64
const nextConfig = {
75
// Enable static exports
@@ -13,10 +11,10 @@ const nextConfig = {
1311
},
1412

1513
// Configure base path for GitHub Pages (repo/chat)
16-
basePath: isGitHubPages ? `/${repo}/${subPath}` : `/${subPath}`,
14+
basePath,
1715

1816
// Configure asset prefix for GitHub Pages - helps with static asset loading
19-
assetPrefix: isGitHubPages ? `/${repo}/${subPath}/` : `/${subPath}/`,
17+
assetPrefix: `${basePath}/`,
2018

2119
// Configure trailing slashes (recommended for static exports)
2220
trailingSlash: true,

cmd/server/server.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var (
2222
agentTypeVar string
2323
port int
2424
printOpenAPI bool
25+
chatBasePath string
2526
)
2627

2728
type AgentType = msgfmt.AgentType
@@ -86,7 +87,7 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er
8687
return xerrors.Errorf("failed to setup process: %w", err)
8788
}
8889
}
89-
srv := httpapi.NewServer(ctx, agentType, process, port)
90+
srv := httpapi.NewServer(ctx, agentType, process, port, chatBasePath)
9091
if printOpenAPI {
9192
fmt.Println(srv.GetOpenAPI())
9293
return nil
@@ -137,4 +138,5 @@ func init() {
137138
ServerCmd.Flags().StringVarP(&agentTypeVar, "type", "t", "", "Override the agent type (one of: claude, goose, aider, custom)")
138139
ServerCmd.Flags().IntVarP(&port, "port", "p", 3284, "Port to run the server on")
139140
ServerCmd.Flags().BoolVarP(&printOpenAPI, "print-openapi", "P", false, "Print the OpenAPI schema to stdout and exit")
141+
ServerCmd.Flags().StringVarP(&chatBasePath, "chat-base-path", "c", "/chat", "Base path for assets and routes used in the static files of the chat interface")
140142
}

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@ require (
4040
github.com/pmezard/go-difflib v1.0.0 // indirect
4141
github.com/rivo/uniseg v0.4.7 // indirect
4242
github.com/rogpeppe/go-internal v1.14.1 // indirect
43+
github.com/spf13/afero v1.14.0
4344
github.com/spf13/pflag v1.0.6 // indirect
44-
golang.org/x/sync v0.11.0 // indirect
45+
golang.org/x/sync v0.12.0 // indirect
4546
golang.org/x/sys v0.31.0 // indirect
46-
golang.org/x/text v0.21.0 // indirect
47+
golang.org/x/text v0.23.0 // indirect
4748
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
4849
gopkg.in/yaml.v3 v3.0.1 // indirect
4950
)

go.sum

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
7979
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
8080
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
8181
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
82+
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
83+
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
8284
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
8385
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
8486
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
@@ -96,8 +98,8 @@ golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
9698
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
9799
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
98100
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
99-
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
100-
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
101+
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
102+
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
101103
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
102104
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
103105
golang.org/x/sys v0.0.0-20200428200454-593003d681fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -109,8 +111,8 @@ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
109111
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
110112
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
111113
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
112-
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
113-
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
114+
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
115+
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
114116
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
115117
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
116118
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=

lib/httpapi/embed.go

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,65 @@ import (
77
"net/http"
88
"os"
99
"strings"
10+
11+
"github.com/spf13/afero"
12+
"golang.org/x/xerrors"
1013
)
1114

1215
//go:embed chat/*
1316
var chatStaticFiles embed.FS
1417

18+
// This must be kept in sync with the BASE_PATH in the Makefile.
19+
const magicBasePath = "/magic-base-path-placeholder"
20+
21+
func createModifiedFS(baseFS fs.FS, oldBasePath string, newBasePath string) (*afero.HttpFs, error) {
22+
ro := afero.FromIOFS{FS: baseFS}
23+
overlay := afero.NewMemMapFs()
24+
newFS := afero.NewCopyOnWriteFs(ro, overlay)
25+
26+
if err := afero.Walk(ro, ".", func(path string, info fs.FileInfo, err error) error {
27+
if err != nil {
28+
return xerrors.Errorf("failed to walk: %w", err)
29+
}
30+
if info.IsDir() {
31+
return nil
32+
}
33+
byteContents, err := afero.ReadFile(ro, path)
34+
if err != nil {
35+
return xerrors.Errorf("failed to read file: %w", err)
36+
}
37+
contents := string(byteContents)
38+
if newBasePath == "/" {
39+
contents = strings.ReplaceAll(contents, oldBasePath+"/", newBasePath)
40+
}
41+
contents = strings.ReplaceAll(contents, oldBasePath, newBasePath)
42+
if err := afero.WriteFile(overlay, path, []byte(contents), 0644); err != nil {
43+
return xerrors.Errorf("failed to write file: %w", err)
44+
}
45+
return nil
46+
}); err != nil {
47+
return nil, xerrors.Errorf("afero.Walk: %w", err)
48+
}
49+
50+
return afero.NewHttpFs(newFS), nil
51+
}
52+
1553
// FileServerWithIndexFallback creates a file server that serves the given filesystem
1654
// and falls back to index.html for any path that doesn't match a file
17-
func FileServerWithIndexFallback() http.Handler {
18-
// First, try to get the embedded files
55+
func FileServerWithIndexFallback(chatBasePath string) http.Handler {
1956
subFS, err := fs.Sub(chatStaticFiles, "chat")
2057
if err != nil {
2158
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2259
http.Error(w, fmt.Sprintf("failed to get subfs: %s", err), http.StatusInternalServerError)
2360
})
2461
}
25-
chatFS := http.FS(subFS)
26-
fileServer := http.FileServer(chatFS)
62+
chatFS, err := createModifiedFS(subFS, magicBasePath, chatBasePath)
63+
if err != nil {
64+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
65+
http.Error(w, fmt.Sprintf("failed to create modified fs: %s", err), http.StatusInternalServerError)
66+
})
67+
}
68+
fileServer := http.FileServer(chatFS.Dir("."))
2769
isChatDirEmpty := false
2870
if _, err := chatFS.Open("index.html"); err != nil {
2971
isChatDirEmpty = true
@@ -43,8 +85,9 @@ func FileServerWithIndexFallback() http.Handler {
4385
}
4486

4587
// Try to serve the file directly
46-
_, err := chatFS.Open(trimmedPath)
88+
f, err := chatFS.Open(trimmedPath)
4789
if err == nil {
90+
defer f.Close()
4891
fileServer.ServeHTTP(w, r)
4992
return
5093
}

lib/httpapi/server.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func (s *Server) GetOpenAPI() string {
5757
const snapshotInterval = 25 * time.Millisecond
5858

5959
// NewServer creates a new server instance
60-
func NewServer(ctx context.Context, agentType mf.AgentType, process *termexec.Process, port int) *Server {
60+
func NewServer(ctx context.Context, agentType mf.AgentType, process *termexec.Process, port int, chatBasePath string) *Server {
6161
router := chi.NewMux()
6262

6363
corsMiddleware := cors.New(cors.Options{
@@ -98,7 +98,7 @@ func NewServer(ctx context.Context, agentType mf.AgentType, process *termexec.Pr
9898
}
9999

100100
// Register API routes
101-
s.registerRoutes()
101+
s.registerRoutes(chatBasePath)
102102

103103
return s
104104
}
@@ -116,7 +116,7 @@ func (s *Server) StartSnapshotLoop(ctx context.Context) {
116116
}
117117

118118
// registerRoutes sets up all API endpoints
119-
func (s *Server) registerRoutes() {
119+
func (s *Server) registerRoutes(chatBasePath string) {
120120
// GET /status endpoint
121121
huma.Get(s.api, "/status", s.getStatus, func(o *huma.Operation) {
122122
o.Description = "Returns the current status of the agent."
@@ -156,7 +156,7 @@ func (s *Server) registerRoutes() {
156156
}, s.subscribeScreen)
157157

158158
// Serve static files for the chat interface under /chat
159-
s.registerStaticFileRoutes()
159+
s.registerStaticFileRoutes(chatBasePath)
160160
}
161161

162162
// getStatus handles GET /status
@@ -303,8 +303,8 @@ func (s *Server) Stop(ctx context.Context) error {
303303
}
304304

305305
// registerStaticFileRoutes sets up routes for serving static files
306-
func (s *Server) registerStaticFileRoutes() {
307-
chatHandler := FileServerWithIndexFallback()
306+
func (s *Server) registerStaticFileRoutes(chatBasePath string) {
307+
chatHandler := FileServerWithIndexFallback(chatBasePath)
308308

309309
// Mount the file server at /chat
310310
s.router.Handle("/chat", http.StripPrefix("/chat", chatHandler))

lib/httpapi/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func TestOpenAPISchema(t *testing.T) {
4444
t.Parallel()
4545

4646
ctx := logctx.WithLogger(context.Background(), slog.New(slog.NewTextHandler(os.Stdout, nil)))
47-
srv := httpapi.NewServer(ctx, msgfmt.AgentTypeClaude, nil, 0)
47+
srv := httpapi.NewServer(ctx, msgfmt.AgentTypeClaude, nil, 0, "/chat")
4848
currentSchemaStr := srv.GetOpenAPI()
4949
var currentSchema any
5050
if err := json.Unmarshal([]byte(currentSchemaStr), &currentSchema); err != nil {

0 commit comments

Comments
 (0)