Skip to content

Commit 6c7ca94

Browse files
feat(docker-compose): add code server configuration support
Add support for configuring code-server deployment defaults in full-custom profile mode. Changes: - Add CodeServerAuthMode type and code server configuration fields to DockerComposeConfig - Add buildCodeServerEnvVars and getCodeServerCommentCopy helper functions - Add code server UI section in ConfigForm with host, port, auth mode, and publishing options - Add validation rules for code server host, port, auth mode, and published port - Add bilingual translations (en-US/zh-CN) for code server configuration UI - Add workspace section definition for executor-code-server child node - Update snapshot and test files to cover code server configuration scenarios Co-Authored-By: Hagicode <noreply@hagicode.com> Signed-off-by: newbe36524 <newbe36524@qq.com>
1 parent 8242208 commit 6c7ca94

16 files changed

Lines changed: 763 additions & 14 deletions

File tree

src/components/docker-compose/ConfigForm.tsx

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,153 @@ export function ConfigForm({ sections, onSelectSection }: ConfigFormProps) {
638638
</div>
639639
) : null}
640640

641+
{config.profile === 'full-custom' ? (
642+
<div id="executor-code-server" tabIndex={-1} className={subSectionClass}>
643+
<div>
644+
<h4 className="text-base font-semibold">{t('configForm.codeServerConfiguration')}</h4>
645+
<p className="text-sm text-muted-foreground">{t('configForm.codeServerDescription')}</p>
646+
</div>
647+
648+
<div className="rounded-2xl border border-border/60 bg-background/80 p-4">
649+
<div className="flex items-start gap-3">
650+
<Checkbox
651+
id="enableCodeServer"
652+
checked={config.enableCodeServer}
653+
onCheckedChange={(checked) => updateConfig('enableCodeServer', checked === true)}
654+
/>
655+
<div className="space-y-1">
656+
<Label htmlFor="enableCodeServer" className="cursor-pointer text-sm font-medium">
657+
{t('configForm.codeServerEnable')}
658+
</Label>
659+
<p className="text-sm text-muted-foreground">{t('configForm.codeServerEnableHint')}</p>
660+
</div>
661+
</div>
662+
</div>
663+
664+
<div className="rounded-2xl border border-blue-200 bg-blue-50 p-4 text-sm dark:border-blue-950 dark:bg-blue-950/30">
665+
<p className="font-medium">{t('configForm.codeServerPrivateTitle')}</p>
666+
<p className="mt-1 text-muted-foreground">{t('configForm.codeServerPrivateHint')}</p>
667+
</div>
668+
669+
<div className="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm dark:border-emerald-950 dark:bg-emerald-950/25">
670+
<p className="font-medium">{t('configForm.codeServerPersistenceTitle')}</p>
671+
<p className="mt-1 text-muted-foreground">{t('configForm.codeServerPersistenceHint')}</p>
672+
<p className="mt-2 text-xs font-mono">{t('configForm.codeServerPersistencePathValue')}</p>
673+
</div>
674+
675+
{config.enableCodeServer ? (
676+
<div className="space-y-4">
677+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
678+
<FieldBlock label={t('configForm.codeServerHost')} htmlFor="codeServerHost">
679+
<Input
680+
id="codeServerHost"
681+
type="text"
682+
value={config.codeServerHost}
683+
onChange={(event) => updateConfig('codeServerHost', event.target.value)}
684+
placeholder="127.0.0.1"
685+
/>
686+
{renderFieldError('codeServerHost')}
687+
<p className="text-xs text-muted-foreground">{t('configForm.codeServerHostHint')}</p>
688+
</FieldBlock>
689+
690+
<FieldBlock label={t('configForm.codeServerPort')} htmlFor="codeServerPort">
691+
<Input
692+
id="codeServerPort"
693+
type="number"
694+
value={config.codeServerPort}
695+
onChange={(event) => updateConfig('codeServerPort', event.target.value)}
696+
placeholder="36529"
697+
/>
698+
{renderFieldError('codeServerPort')}
699+
<p className="text-xs text-muted-foreground">{t('configForm.codeServerPortHint')}</p>
700+
</FieldBlock>
701+
</div>
702+
703+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
704+
<FieldBlock label={t('configForm.codeServerAuthMode')} htmlFor="codeServerAuthMode">
705+
<Select
706+
value={config.codeServerAuthMode}
707+
onValueChange={(value: DockerComposeConfig['codeServerAuthMode']) => updateConfig('codeServerAuthMode', value)}
708+
>
709+
<SelectTrigger id="codeServerAuthMode">
710+
<SelectValue placeholder={t('configForm.codeServerAuthMode')} />
711+
</SelectTrigger>
712+
<SelectContent>
713+
<SelectItem value="none">{t('configForm.codeServerAuthModeNone')}</SelectItem>
714+
<SelectItem value="password">{t('configForm.codeServerAuthModePassword')}</SelectItem>
715+
</SelectContent>
716+
</Select>
717+
{renderFieldError('codeServerAuthMode')}
718+
<p className="text-xs text-muted-foreground">{t('configForm.codeServerAuthModeHint')}</p>
719+
</FieldBlock>
720+
721+
{config.codeServerAuthMode === 'password' ? (
722+
<FieldBlock label={t('configForm.codeServerPassword')} htmlFor="codeServerPassword">
723+
<Input
724+
id="codeServerPassword"
725+
type="password"
726+
value={config.codeServerPassword}
727+
onChange={(event) => updateConfig('codeServerPassword', event.target.value)}
728+
placeholder="change-me"
729+
/>
730+
{renderFieldError('codeServerPassword')}
731+
<p className="text-xs text-muted-foreground">{t('configForm.codeServerPasswordHint')}</p>
732+
</FieldBlock>
733+
) : (
734+
<div className="rounded-2xl border border-border/60 bg-muted/20 p-4 text-sm text-muted-foreground">
735+
<p className="font-medium">{t('configForm.codeServerAuthModeNone')}</p>
736+
<p className="mt-1">{t('configForm.codeServerPasswordSkippedHint')}</p>
737+
</div>
738+
)}
739+
</div>
740+
741+
<div className="space-y-4 rounded-2xl border border-border/60 bg-background/80 p-4">
742+
<div className="flex items-start gap-3">
743+
<Checkbox
744+
id="codeServerPublishToHost"
745+
checked={config.codeServerPublishToHost}
746+
onCheckedChange={(checked) => updateConfig('codeServerPublishToHost', checked === true)}
747+
/>
748+
<div className="space-y-1">
749+
<Label htmlFor="codeServerPublishToHost" className="cursor-pointer text-sm font-medium">
750+
{t('configForm.codeServerPublishToHost')}
751+
</Label>
752+
<p className="text-sm text-muted-foreground">{t('configForm.codeServerPublishToHostHint')}</p>
753+
</div>
754+
</div>
755+
756+
{config.codeServerPublishToHost ? (
757+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
758+
<FieldBlock label={t('configForm.codeServerPublishedPort')} htmlFor="codeServerPublishedPort">
759+
<Input
760+
id="codeServerPublishedPort"
761+
type="number"
762+
value={config.codeServerPublishedPort}
763+
onChange={(event) => updateConfig('codeServerPublishedPort', event.target.value)}
764+
placeholder="36529"
765+
/>
766+
{renderFieldError('codeServerPublishedPort')}
767+
<p className="text-xs text-muted-foreground">{t('configForm.codeServerPublishedPortHint')}</p>
768+
</FieldBlock>
769+
770+
<div className="rounded-2xl border border-blue-200 bg-blue-50 p-4 text-sm dark:border-blue-950 dark:bg-blue-950/30">
771+
<p className="font-medium">{t('configForm.codeServerPublishBindTitle')}</p>
772+
<p className="mt-1 text-muted-foreground">{t('configForm.codeServerPublishBindHint')}</p>
773+
<p className="mt-2 text-xs font-mono">{t('configForm.codeServerPublishBindValue')}</p>
774+
</div>
775+
</div>
776+
) : null}
777+
</div>
778+
</div>
779+
) : (
780+
<div className="rounded-2xl border border-dashed border-border/70 bg-muted/15 p-4 text-sm text-muted-foreground">
781+
<p className="font-medium text-foreground">{t('configForm.codeServerDisabledTitle')}</p>
782+
<p className="mt-1">{t('configForm.codeServerDisabledHint')}</p>
783+
</div>
784+
)}
785+
</div>
786+
) : null}
787+
641788
</div>
642789
</ConfigSectionCard>
643790
) : null}

src/components/docker-compose/layout/workspaceSections.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export type WorkspaceSectionId =
1212
export type WorkspaceSectionChildId =
1313
| 'executor-claude'
1414
| 'executor-codex'
15-
| 'executor-opencode';
15+
| 'executor-opencode'
16+
| 'executor-code-server';
1617

1718
export type WorkspaceSectionStatus = 'active' | 'complete' | 'error' | 'pending';
1819

@@ -105,6 +106,13 @@ const sectionDefinitions: WorkspaceSectionDefinition[] = [
105106
'openCodeModel',
106107
'openCodeConfigMode',
107108
'openCodeConfigHostPath',
109+
'enableCodeServer',
110+
'codeServerHost',
111+
'codeServerPort',
112+
'codeServerPublishToHost',
113+
'codeServerPublishedPort',
114+
'codeServerAuthMode',
115+
'codeServerPassword',
108116
],
109117
isVisible: () => true,
110118
isComplete: (config) => {
@@ -119,10 +127,19 @@ const sectionDefinitions: WorkspaceSectionDefinition[] = [
119127
const openCodeReady = !config.enabledExecutors.includes('opencode')
120128
|| config.openCodeConfigMode === 'default-managed'
121129
|| hasTextValue(config.openCodeConfigHostPath);
130+
const codeServerReady = config.profile !== 'full-custom'
131+
|| !config.enableCodeServer
132+
|| (
133+
hasTextValue(config.codeServerHost)
134+
&& hasTextValue(config.codeServerPort)
135+
&& (!config.codeServerPublishToHost || hasTextValue(config.codeServerPublishedPort))
136+
&& (config.codeServerAuthMode !== 'password' || hasTextValue(config.codeServerPassword))
137+
);
122138

123139
return claudeReady
124140
&& codexReady
125-
&& openCodeReady;
141+
&& openCodeReady
142+
&& codeServerReady;
126143
},
127144
},
128145
{
@@ -233,6 +250,30 @@ const childDefinitions: WorkspaceSectionChildDefinition[] = [
233250
isComplete: (config) =>
234251
config.openCodeConfigMode === 'default-managed' || hasTextValue(config.openCodeConfigHostPath),
235252
},
253+
{
254+
id: 'executor-code-server',
255+
parentId: 'executors',
256+
titleKey: 'workspace.executorItems.codeServer.title',
257+
descriptionKey: 'workspace.executorItems.codeServer.description',
258+
fieldIds: [
259+
'enableCodeServer',
260+
'codeServerHost',
261+
'codeServerPort',
262+
'codeServerPublishToHost',
263+
'codeServerPublishedPort',
264+
'codeServerAuthMode',
265+
'codeServerPassword',
266+
],
267+
isVisible: (config) => config.profile === 'full-custom',
268+
isComplete: (config) =>
269+
!config.enableCodeServer
270+
|| (
271+
hasTextValue(config.codeServerHost)
272+
&& hasTextValue(config.codeServerPort)
273+
&& (!config.codeServerPublishToHost || hasTextValue(config.codeServerPublishedPort))
274+
&& (config.codeServerAuthMode !== 'password' || hasTextValue(config.codeServerPassword))
275+
),
276+
},
236277
];
237278

238279
export const getWorkspaceSections = (

src/i18n/locales/en-US.json

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@
170170
"customApiEndpoint": "Custom API Endpoint",
171171
"apiEndpointUrl": "API Endpoint URL",
172172
"apiToken": "API Token",
173-
"getApiToken": "Get API Token ",
173+
"getApiToken": "Get API Token \u2192",
174174
"enterApiToken": "Enter your API Token",
175175
"enterAnthropicApiToken": "Enter your Anthropic API Token (sk-ant-...)",
176176
"apiEndpointAutoSet": "API Endpoint will be automatically set to",
@@ -189,7 +189,7 @@
189189
"claudeCodeExperimentalAgentTeamsHint": "Enable experimental Agent Teams collaboration feature",
190190
"volumeMounts": "Volume Mounts",
191191
"workdirDescription": "Work directory points to a folder containing your code repositories. We will scan multiple repositories within this folder and it will be mapped to /app/workdir inside the container.",
192-
"workdirRecommendation": "💡 Recommendation: Point to a parent folder that contains multiple repositories (e.g., /home/user/repos or C:\\Users\\YourName\\source\\repos). The application will scan all subfolders to detect repositories.",
192+
"workdirRecommendation": "\ud83d\udca1 Recommendation: Point to a parent folder that contains multiple repositories (e.g., /home/user/repos or C:\\Users\\YourName\\source\\repos). The application will scan all subfolders to detect repositories.",
193193
"workdirPath": "Work Directory Path",
194194
"workdirCreatedByRoot": "Work directory created by root user",
195195
"permissionIssueWarning": "Permission Issue Warning",
@@ -203,7 +203,36 @@
203203
"puid": "PUID (User ID)",
204204
"pgid": "PGID (Group ID)",
205205
"permissionNotRequired": "Permission Not Required",
206-
"permissionNotRequiredDescription": "When workdir is created by root, permission mapping is not required."
206+
"permissionNotRequiredDescription": "When workdir is created by root, permission mapping is not required.",
207+
"codeServerConfiguration": "Code Server Configuration",
208+
"codeServerDescription": "Configure the managed browser IDE defaults exported only in full custom mode.",
209+
"codeServerEnable": "Enable code-server as the default browser IDE",
210+
"codeServerEnableHint": "When disabled, Builder keeps your saved values but exports code serve-web as the default implementation instead.",
211+
"codeServerPrivateTitle": "Private by default",
212+
"codeServerPrivateHint": "No dedicated host port is published until you explicitly enable host publishing below.",
213+
"codeServerPersistenceTitle": "Persistence stays shared",
214+
"codeServerPersistenceHint": "Deployment defaults are exported into compose, while runtime state continues to live under the shared HagiCode data volume after the container starts.",
215+
"codeServerPersistencePathValue": "hagicode_data:/app/data -> /app/data/code-server",
216+
"codeServerHost": "Listen Host",
217+
"codeServerHostHint": "Use 127.0.0.1 for private container-only access. Switch to 0.0.0.0 or :: before enabling host publishing.",
218+
"codeServerPort": "Container Port",
219+
"codeServerPortHint": "Valid range: 1-65535. Port 36529 matches the HagiCode code-server default.",
220+
"codeServerAuthMode": "Auth Mode",
221+
"codeServerAuthModeNone": "none",
222+
"codeServerAuthModePassword": "password",
223+
"codeServerAuthModeHint": "Use password when you want Builder to export a startup secret for code-server.",
224+
"codeServerPassword": "CODE_SERVER_PASSWORD",
225+
"codeServerPasswordHint": "This value is passed to the container bootstrap path and bridged to code-server without being echoed in comments.",
226+
"codeServerPasswordSkippedHint": "No password secret is exported while auth mode stays set to none.",
227+
"codeServerPublishToHost": "Publish Code Server to Host",
228+
"codeServerPublishToHostHint": "Adds a dedicated host-to-container port mapping for code-server while keeping the bind address fixed to host loopback.",
229+
"codeServerPublishedPort": "Published Host Port",
230+
"codeServerPublishedPortHint": "Builder emits a separate loopback-only mapping for this host port.",
231+
"codeServerPublishBindTitle": "Host bind address",
232+
"codeServerPublishBindHint": "Builder keeps the host-side bind fixed to loopback for the first publish step. Add a reverse proxy if you need wider exposure.",
233+
"codeServerPublishBindValue": "127.0.0.1:<published-port>:<container-port>",
234+
"codeServerDisabledTitle": "code-server defaults are currently disabled",
235+
"codeServerDisabledHint": "Builder will export code serve-web as the active implementation until you re-enable code-server for this profile."
207236
},
208237
"configPreview": {
209238
"generatedConfiguration": "Generated Configuration",
@@ -318,6 +347,10 @@
318347
"opencode": {
319348
"title": "OpenCode runtime",
320349
"description": "Managed model selection plus config persistence mode."
350+
},
351+
"codeServer": {
352+
"title": "Code Server defaults",
353+
"description": "Container host/port, auth mode, and optional host publishing for code-server."
321354
}
322355
}
323356
}

src/i18n/locales/zh-CN.json

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,36 @@
203203
"puid": "PUID(用户 ID)",
204204
"pgid": "PGID(组 ID)",
205205
"permissionNotRequired": "无需权限配置",
206-
"permissionNotRequiredDescription": "当工作目录由 root 用户创建时,不需要权限映射。"
206+
"permissionNotRequiredDescription": "当工作目录由 root 用户创建时,不需要权限映射。",
207+
"codeServerConfiguration": "Code Server 配置",
208+
"codeServerDescription": "只在完整自定义模式下导出托管浏览器 IDE 的部署默认值。",
209+
"codeServerEnable": "将 code-server 设为默认浏览器 IDE",
210+
"codeServerEnableHint": "关闭后会保留你填写的值,但导出的默认实现会切回 code serve-web。",
211+
"codeServerPrivateTitle": "默认保持私有",
212+
"codeServerPrivateHint": "只有在下方显式开启宿主机发布后,Builder 才会额外导出专用端口映射。",
213+
"codeServerPersistenceTitle": "持久化继续复用现有数据卷",
214+
"codeServerPersistenceHint": "Compose 中导出的是部署默认值;容器启动后的运行时状态仍会继续落在共享的 HagiCode 数据卷里。",
215+
"codeServerPersistencePathValue": "hagicode_data:/app/data -> /app/data/code-server",
216+
"codeServerHost": "监听 Host",
217+
"codeServerHostHint": "默认使用 127.0.0.1 保持容器内私有访问。若要开启宿主机发布,请先切换为 0.0.0.0 或 ::。",
218+
"codeServerPort": "容器端口",
219+
"codeServerPortHint": "有效范围:1-65535。36529 与 HagiCode 当前的 code-server 默认端口一致。",
220+
"codeServerAuthMode": "认证模式",
221+
"codeServerAuthModeNone": "none",
222+
"codeServerAuthModePassword": "password",
223+
"codeServerAuthModeHint": "当你希望 Builder 一并导出 code-server 的启动密码时,请选择 password。",
224+
"codeServerPassword": "CODE_SERVER_PASSWORD",
225+
"codeServerPasswordHint": "该值会交给容器启动引导桥接到 code-server,不会在注释里明文解释。",
226+
"codeServerPasswordSkippedHint": "当认证模式为 none 时,不会导出密码类环境变量。",
227+
"codeServerPublishToHost": "发布到宿主机",
228+
"codeServerPublishToHostHint": "为 code-server 增加独立的宿主机到容器端口映射,同时把宿主机绑定地址固定为回环地址。",
229+
"codeServerPublishedPort": "宿主机发布端口",
230+
"codeServerPublishedPortHint": "Builder 会为该端口生成一条仅回环可访问的独立映射。",
231+
"codeServerPublishBindTitle": "宿主机绑定地址",
232+
"codeServerPublishBindHint": "Builder 首阶段固定绑定到 loopback;如果你需要更广泛的暴露范围,请在外层显式增加反向代理。",
233+
"codeServerPublishBindValue": "127.0.0.1:<宿主机端口>:<容器端口>",
234+
"codeServerDisabledTitle": "当前未启用 code-server 默认值",
235+
"codeServerDisabledHint": "在重新启用之前,Builder 会导出 code serve-web 作为当前配置模式的默认实现。"
207236
},
208237
"configPreview": {
209238
"generatedConfiguration": "生成的配置",
@@ -318,6 +347,10 @@
318347
"opencode": {
319348
"title": "OpenCode 运行时",
320349
"description": "托管模型选择与配置持久化模式。"
350+
},
351+
"codeServer": {
352+
"title": "Code Server 默认值",
353+
"description": "配置 code-server 的容器 host/port、认证模式,以及可选的宿主机端口发布。"
321354
}
322355
}
323356
}

0 commit comments

Comments
 (0)