Skip to content

Commit d0a0325

Browse files
shanturShanturShantur
authored
feat(sidecars): add proxied sidecar tabs (#279)
## Summary - add SideCar support across the server and UI, including proxied tabs, picker/settings flows, and websocket-aware proxying - unify top-level tab handling so workspace instances and SideCars share the same tab model and navigation flows - limit SideCars to port-based services only, removing server-managed process control from the final API and UI --------- Co-authored-by: Shantur <shantur@Mac.home> Co-authored-by: Shantur <shantur@Shanturs-MacBook-Pro-M5.local>
1 parent 19a4c3d commit d0a0325

47 files changed

Lines changed: 2139 additions & 218 deletions

Some content is hidden

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

packages/electron-app/electron/main/main.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { app, BrowserView, BrowserWindow, nativeImage, session, shell } from "electron"
22
import http from "node:http"
33
import https from "node:https"
4-
import { existsSync } from "fs"
4+
import { existsSync, mkdirSync } from "fs"
55
import { dirname, join } from "path"
66
import { fileURLToPath } from "url"
77
import { createApplicationMenu } from "./menu"
@@ -14,6 +14,31 @@ const mainDirname = dirname(mainFilename)
1414

1515
const isMac = process.platform === "darwin"
1616

17+
function configureDevStoragePaths() {
18+
if (app.isPackaged) {
19+
return
20+
}
21+
22+
const appName = "CodeNomad"
23+
24+
try {
25+
app.setName(appName)
26+
27+
const userDataPath = join(app.getPath("appData"), appName)
28+
const sessionDataPath = join(userDataPath, "session-data")
29+
30+
mkdirSync(userDataPath, { recursive: true })
31+
mkdirSync(sessionDataPath, { recursive: true })
32+
33+
app.setPath("userData", userDataPath)
34+
app.setPath("sessionData", sessionDataPath)
35+
} catch (error) {
36+
console.warn("[cli] failed to configure dev storage paths", error)
37+
}
38+
}
39+
40+
configureDevStoragePaths()
41+
1742
const cliManager = new CliProcessManager()
1843
let mainWindow: BrowserWindow | null = null
1944
let currentCliUrl: string | null = null

packages/server/src/api-types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,24 @@ export interface InstanceStreamEvent {
170170
[key: string]: unknown
171171
}
172172

173+
export type SideCarKind = "port"
174+
175+
export type SideCarPrefixMode = "strip" | "preserve"
176+
177+
export type SideCarStatus = "running" | "stopped"
178+
179+
export interface SideCar {
180+
id: string
181+
kind: SideCarKind
182+
name: string
183+
port: number
184+
insecure: boolean
185+
prefixMode: SideCarPrefixMode
186+
status: SideCarStatus
187+
createdAt: string
188+
updatedAt: string
189+
}
190+
173191
export interface BinaryRecord {
174192
id: string
175193
path: string
@@ -276,6 +294,8 @@ export type WorkspaceEventType =
276294
| "workspace.error"
277295
| "workspace.stopped"
278296
| "workspace.log"
297+
| "sidecar.updated"
298+
| "sidecar.removed"
279299
| "storage.configChanged"
280300
| "storage.stateChanged"
281301
| "instance.dataChanged"
@@ -288,6 +308,8 @@ export type WorkspaceEventPayload =
288308
| { type: "workspace.error"; workspace: WorkspaceDescriptor }
289309
| { type: "workspace.stopped"; workspaceId: string }
290310
| { type: "workspace.log"; entry: WorkspaceLogEntry }
311+
| { type: "sidecar.updated"; sidecar: SideCar }
312+
| { type: "sidecar.removed"; sidecarId: string }
291313
| { type: "storage.configChanged"; owner: SettingsOwner; value: SettingsBucket }
292314
| { type: "storage.stateChanged"; owner: SettingsOwner; value: SettingsBucket }
293315
| { type: "instance.dataChanged"; instanceId: string; data: InstanceData }

packages/server/src/auth/manager.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,18 @@ export class AuthManager {
104104
}
105105

106106
getSessionFromRequest(request: FastifyRequest): { username: string; sessionId: string } | null {
107+
return this.getSessionFromHeaders(request.headers)
108+
}
109+
110+
getSessionFromHeaders(headers: { cookie?: string | string[] | undefined }): { username: string; sessionId: string } | null {
107111
if (!this.authEnabled) {
108112
// When auth is disabled, treat all requests as authenticated.
109113
// We still return a stable username so callers can display it.
110114
return { username: this.init.username, sessionId: "auth-disabled" }
111115
}
112116

113-
const cookies = parseCookies(request.headers.cookie)
117+
const cookieHeader = Array.isArray(headers.cookie) ? headers.cookie.join("; ") : headers.cookie
118+
const cookies = parseCookies(cookieHeader)
114119
const sessionId = cookies[this.cookieName]
115120
const session = this.sessionManager.getSession(sessionId)
116121
if (!session) return null

packages/server/src/events/bus.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export class EventBus extends EventEmitter {
2424
this.on("workspace.error", handler)
2525
this.on("workspace.stopped", handler)
2626
this.on("workspace.log", handler)
27+
this.on("sidecar.updated", handler)
28+
this.on("sidecar.removed", handler)
2729
this.on("storage.configChanged", handler)
2830
this.on("storage.stateChanged", handler)
2931
this.on("instance.dataChanged", handler)
@@ -35,6 +37,8 @@ export class EventBus extends EventEmitter {
3537
this.off("workspace.error", handler)
3638
this.off("workspace.stopped", handler)
3739
this.off("workspace.log", handler)
40+
this.off("sidecar.updated", handler)
41+
this.off("sidecar.removed", handler)
3842
this.off("storage.configChanged", handler)
3943
this.off("storage.stateChanged", handler)
4044
this.off("instance.dataChanged", handler)

packages/server/src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { resolveHttpsOptions } from "./server/tls"
2424
import { resolveNetworkAddresses, resolveRemoteAddresses } from "./server/network-addresses"
2525
import { startDevReleaseMonitor } from "./releases/dev-release-monitor"
2626
import { SpeechService } from "./speech/service"
27+
import { SideCarManager } from "./sidecars/manager"
2728

2829
const require = createRequire(import.meta.url)
2930

@@ -315,6 +316,11 @@ async function main() {
315316
const fileSystemBrowser = new FileSystemBrowser({ rootDir: options.rootDir, unrestricted: options.unrestrictedRoot })
316317
const instanceStore = new InstanceStore(configLocation.instancesDir)
317318
const speechService = new SpeechService(settings, logger.child({ component: "speech" }))
319+
const sidecarManager = new SideCarManager({
320+
settings,
321+
eventBus,
322+
logger: logger.child({ component: "sidecars" }),
323+
})
318324
const instanceEventBridge = new InstanceEventBridge({
319325
workspaceManager,
320326
eventBus,
@@ -400,6 +406,7 @@ async function main() {
400406
serverMeta,
401407
instanceStore,
402408
speechService,
409+
sidecarManager,
403410
authManager,
404411
uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR,
405412
uiDevServerUrl: uiResolution.uiDevServerUrl,
@@ -421,6 +428,7 @@ async function main() {
421428
serverMeta,
422429
instanceStore,
423430
speechService,
431+
sidecarManager,
424432
authManager,
425433
uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR,
426434
uiDevServerUrl: undefined,
@@ -520,6 +528,12 @@ async function main() {
520528
logger.warn({ err: error }, "Instance event bridge shutdown failed")
521529
}
522530

531+
try {
532+
await sidecarManager.shutdown()
533+
} catch (error) {
534+
logger.error({ err: error }, "SideCar manager shutdown failed")
535+
}
536+
523537
try {
524538
await workspaceManager.shutdown()
525539
logger.info("Workspace manager shutdown complete")

0 commit comments

Comments
 (0)