diff --git a/packages/web/package.json b/packages/web/package.json
index 6d9195bb..d8d3100c 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -6,7 +6,8 @@
"dev": "concurrently -k \"next dev\" \"node scripts/terminal-ws.mjs\"",
"build": "next build",
"start": "next start",
- "lint": "eslint"
+ "lint": "eslint",
+ "test": "vitest run"
},
"dependencies": {
"@effect-template/lib": "workspace:*",
@@ -29,6 +30,7 @@
"eslint": "^9",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4",
- "typescript": "^5"
+ "typescript": "^5",
+ "vitest": "^4.0.17"
}
}
diff --git a/packages/web/src/app/page.tsx b/packages/web/src/app/page.tsx
index 3c6363c7..d36aa76a 100644
--- a/packages/web/src/app/page.tsx
+++ b/packages/web/src/app/page.tsx
@@ -533,9 +533,13 @@ export default function Home() {
useEffect(() => () => disposeTerminal(), [disposeTerminal])
- const connectedLabel = activeProject?.displayName ?? "none"
+ const activeTerminalSession = terminalSessions.find((session) => session.id === activeSessionId)
+ const connectedContainerLabel = terminalStatus === "detached"
+ ? "none"
+ : activeTerminalSession?.containerName ?? activeProject?.containerName ?? "unknown"
const statusLabel = activeProject?.statusLabel ?? "unknown"
const sshLabel = activeProject?.ssh ?? "-"
+ const containerLabel = activeProject?.containerName ?? "-"
const repoLabel = activeProject?.displayName ?? "-"
const refLabel = activeProject?.repoRef ?? "-"
const recreateStatus = activeProject?.recreateStatus
@@ -558,7 +562,7 @@ export default function Home() {
SSH: {sshLabel}
+ Container: {containerLabel}
Repo: {repoLabel}
Ref: {refLabel}
Status: {statusLabel}
@@ -685,43 +690,46 @@ export default function Home() {
{showDetails ? "No active terminals" : "None"}
) : (
- terminalSessions.map((session) => (
-
handleSessionSelect(session)}
- onKeyDown={(event) => {
- if (event.key === "Enter" || event.key === " ") {
- event.preventDefault()
- handleSessionSelect(session)
- }
- }}
- >
-
-
{session.displayName}
-
-
- {session.status}
-
-
+ terminalSessions.map((session) => {
+ const sessionContainer = session.containerName ?? session.projectId
+ return (
+
handleSessionSelect(session)}
+ onKeyDown={(event) => {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault()
+ handleSessionSelect(session)
+ }
+ }}
+ >
+
+
{session.displayName}
+
+
+ {session.status}
+
+
+
+
+ {session.source} · {session.mode} · {sessionContainer}
+
-
- {session.source} · {session.mode} · {session.projectId}
-
-
- ))
+ )
+ })
)}
diff --git a/packages/web/src/lib/api-schema.ts b/packages/web/src/lib/api-schema.ts
index d93cfe72..11cb10f8 100644
--- a/packages/web/src/lib/api-schema.ts
+++ b/packages/web/src/lib/api-schema.ts
@@ -108,6 +108,7 @@ const TerminalSessionSchema = Schema.Struct({
id: Schema.String,
projectId: Schema.String,
displayName: Schema.String,
+ containerName: Schema.optional(Schema.String),
mode: TerminalSessionModeSchema,
source: Schema.String,
status: TerminalSessionStatusSchema,
diff --git a/packages/web/src/lib/api-types.ts b/packages/web/src/lib/api-types.ts
index 96916160..45712e15 100644
--- a/packages/web/src/lib/api-types.ts
+++ b/packages/web/src/lib/api-types.ts
@@ -65,6 +65,7 @@ export type TerminalSession = {
readonly id: string
readonly projectId: string
readonly displayName: string
+ readonly containerName?: string
readonly mode: TerminalSessionMode
readonly source: string
readonly status: TerminalSessionStatus
diff --git a/packages/web/src/server/terminal-ws.ts b/packages/web/src/server/terminal-ws.ts
index d7f280b7..a9d273ea 100644
--- a/packages/web/src/server/terminal-ws.ts
+++ b/packages/web/src/server/terminal-ws.ts
@@ -23,6 +23,7 @@ type TerminalSessionRegistry = {
readonly id: string
readonly projectId: string
readonly displayName: string
+ readonly containerName?: string
readonly mode: TerminalSessionMode
readonly source: string
readonly status: TerminalSessionStatus
@@ -181,8 +182,15 @@ export const attachTerminalWs = (wss: WebSocketServer) => {
const privateKey = fs.readFileSync(target.identityPath)
client.on("ready", () => {
- updateSession(sessionId, { status: "connected", displayName: details.displayName })
- sendMessage(socket, { type: "info", data: `[docker-git] attached to ${details.displayName}` })
+ updateSession(sessionId, {
+ status: "connected",
+ displayName: details.displayName,
+ containerName: details.containerName
+ })
+ sendMessage(socket, {
+ type: "info",
+ data: `[docker-git] attached to ${details.displayName} (${details.containerName})`
+ })
client.shell(
{
diff --git a/packages/web/tests/api/terminal-sessions-route.test.ts b/packages/web/tests/api/terminal-sessions-route.test.ts
new file mode 100644
index 00000000..2f4d6e6b
--- /dev/null
+++ b/packages/web/tests/api/terminal-sessions-route.test.ts
@@ -0,0 +1,69 @@
+import fs from "node:fs"
+
+import { afterEach, beforeEach, describe, expect, it } from "vitest"
+
+import { GET } from "../../src/app/api/terminal-sessions/route"
+
+const sessionsFile = "/tmp/docker-git-terminal-sessions.json"
+
+let previousSessionsFileContent: string | null = null
+
+beforeEach(() => {
+ previousSessionsFileContent = fs.existsSync(sessionsFile)
+ ? fs.readFileSync(sessionsFile, "utf8")
+ : null
+})
+
+afterEach(() => {
+ if (previousSessionsFileContent === null) {
+ if (fs.existsSync(sessionsFile)) {
+ fs.unlinkSync(sessionsFile)
+ }
+ return
+ }
+ fs.writeFileSync(sessionsFile, previousSessionsFileContent, "utf8")
+})
+
+describe("GET /api/terminal-sessions", () => {
+ it("returns sessions with containerName", async () => {
+ fs.writeFileSync(
+ sessionsFile,
+ JSON.stringify({
+ sessions: [
+ {
+ id: "session-1",
+ projectId: "/tmp/project",
+ displayName: "org/repo",
+ containerName: "dg-repo-issue-47",
+ mode: "default",
+ source: "web",
+ status: "connected",
+ connectedAt: "2026-02-16T15:00:00.000Z",
+ updatedAt: "2026-02-16T15:00:01.000Z"
+ }
+ ]
+ }),
+ "utf8"
+ )
+
+ const response = GET()
+ const body = await response.json()
+ const sessions = Reflect.get(body as object, "sessions")
+ expect(Array.isArray(sessions)).toBe(true)
+ const first = Array.isArray(sessions) ? sessions[0] : null
+ expect(first).toMatchObject({
+ id: "session-1",
+ containerName: "dg-repo-issue-47",
+ status: "connected"
+ })
+ })
+
+ it("returns an empty list when sessions file is missing", async () => {
+ if (fs.existsSync(sessionsFile)) {
+ fs.unlinkSync(sessionsFile)
+ }
+ const response = GET()
+ const body = await response.json()
+ expect(body).toEqual({ sessions: [] })
+ })
+})
diff --git a/packages/web/tests/lib/api-schema-terminal-sessions.test.ts b/packages/web/tests/lib/api-schema-terminal-sessions.test.ts
new file mode 100644
index 00000000..8a9f4832
--- /dev/null
+++ b/packages/web/tests/lib/api-schema-terminal-sessions.test.ts
@@ -0,0 +1,58 @@
+import { Either } from "effect"
+import * as Schema from "effect/Schema"
+import { describe, expect, it } from "vitest"
+
+import { ApiSchema } from "../../src/lib/api-schema"
+
+const decodeTerminalSessions = Schema.decodeUnknownEither(ApiSchema.TerminalSessions)
+
+describe("ApiSchema.TerminalSessions", () => {
+ it("decodes sessions with containerName", () => {
+ const payload = {
+ sessions: [
+ {
+ id: "session-1",
+ projectId: "/tmp/project",
+ displayName: "org/repo",
+ containerName: "dg-repo-issue-47",
+ mode: "default",
+ source: "web",
+ status: "connected",
+ connectedAt: "2026-02-16T15:00:00.000Z",
+ updatedAt: "2026-02-16T15:00:01.000Z"
+ }
+ ]
+ }
+
+ const decoded = decodeTerminalSessions(payload)
+ expect(Either.isRight(decoded)).toBe(true)
+ if (Either.isLeft(decoded)) {
+ return
+ }
+ expect(decoded.right.sessions[0]?.containerName).toBe("dg-repo-issue-47")
+ })
+
+ it("keeps backward compatibility when containerName is absent", () => {
+ const payload = {
+ sessions: [
+ {
+ id: "session-legacy",
+ projectId: "/tmp/project-legacy",
+ displayName: "org/repo",
+ mode: "default",
+ source: "web",
+ status: "connected",
+ connectedAt: "2026-02-16T15:00:00.000Z",
+ updatedAt: "2026-02-16T15:00:01.000Z"
+ }
+ ]
+ }
+
+ const decoded = decodeTerminalSessions(payload)
+ expect(Either.isRight(decoded)).toBe(true)
+ if (Either.isLeft(decoded)) {
+ return
+ }
+ expect(decoded.right.sessions[0]?.containerName).toBeUndefined()
+ })
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 69e9e1ac..55ac4bdf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -431,6 +431,9 @@ importers:
typescript:
specifier: ^5
version: 5.9.3
+ vitest:
+ specifier: ^4.0.17
+ version: 4.0.17(@types/node@20.19.31)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)
packages:
@@ -6114,6 +6117,14 @@ snapshots:
optionalDependencies:
vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)
+ '@vitest/mocker@4.0.17(vite@7.3.1(@types/node@20.19.31)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))':
+ dependencies:
+ '@vitest/spy': 4.0.17
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 7.3.1(@types/node@20.19.31)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)
+
'@vitest/mocker@4.0.17(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))':
dependencies:
'@vitest/spy': 4.0.17
@@ -9116,6 +9127,21 @@ snapshots:
- supports-color
- typescript
+ vite@7.3.1(@types/node@20.19.31)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2):
+ dependencies:
+ esbuild: 0.27.2
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+ postcss: 8.5.6
+ rollup: 4.53.3
+ tinyglobby: 0.2.15
+ optionalDependencies:
+ '@types/node': 20.19.31
+ fsevents: 2.3.3
+ jiti: 2.6.1
+ lightningcss: 1.30.2
+ yaml: 2.8.2
+
vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2):
dependencies:
esbuild: 0.27.2
@@ -9172,6 +9198,43 @@ snapshots:
- tsx
- yaml
+ vitest@4.0.17(@types/node@20.19.31)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2):
+ dependencies:
+ '@vitest/expect': 4.0.17
+ '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@20.19.31)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))
+ '@vitest/pretty-format': 4.0.17
+ '@vitest/runner': 4.0.17
+ '@vitest/snapshot': 4.0.17
+ '@vitest/spy': 4.0.17
+ '@vitest/utils': 4.0.17
+ es-module-lexer: 1.7.0
+ expect-type: 1.2.2
+ magic-string: 0.30.21
+ obug: 2.1.1
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 3.10.0
+ tinybench: 2.9.0
+ tinyexec: 1.0.2
+ tinyglobby: 0.2.15
+ tinyrainbow: 3.0.3
+ vite: 7.3.1(@types/node@20.19.31)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 20.19.31
+ transitivePeerDependencies:
+ - jiti
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - terser
+ - tsx
+ - yaml
+
vitest@4.0.17(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2):
dependencies:
'@vitest/expect': 4.0.17