Compare commits

...

8 Commits

Author SHA1 Message Date
92a8e2a6d0 fix: improve DevContainer IDE icon display with fallback mechanism
All checks were successful
DevStar Studio CI/CD Pipeline / DevStarStudio-CICD-Pipeline (pull_request) Successful in 1h2m52s
2026-01-14 14:27:32 +08:00
3e94b55e8b feat: make DevContainer IDE list configurable via URL templates with placeholder replacement and name/logo support
Some checks failed
DevStar Studio CI/CD Pipeline / DevStarStudio-CICD-Pipeline (pull_request) Failing after 6m37s
Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-14 13:17:41 +08:00
416c954119 t添加setting.ParentDomain 和 setting.ParentAccessToken
Some checks failed
DevStar Studio CI/CD Pipeline / DevStarStudio-CICD-Pipeline (pull_request) Failing after 30m22s
2026-01-13 14:25:22 +08:00
e4baca8811 Merge remote-tracking branch 'origin/main' into feat/devcontainer-ide-config
Some checks failed
DevStar Studio CI/CD Pipeline / DevStarStudio-CICD-Pipeline (pull_request) Failing after 29m15s
Resolved conflicts by keeping dynamic IDE configuration from HEAD,
as it enhances the hardcoded version from origin/main.
2026-01-12 18:58:16 +08:00
a1ea929a8b fix: add marshaller for DevContainerEditorApps config
Some checks failed
DevStar Studio CI/CD Pipeline / DevStarStudio-CICD-Pipeline (pull_request) Failing after 27m54s
2026-01-12 17:09:39 +08:00
85d3022496 fix: use current user for devstar_username parameter 2026-01-12 17:01:21 +08:00
98e8fec2d6 feat: add configurable DevContainer IDE support via admin settings 2026-01-12 16:18:47 +08:00
0333d323e0 refactor: remove redundant cmd parameter from IDE URLs
Some checks failed
DevStar Studio CI/CD Pipeline / DevStarStudio-CICD-Pipeline (pull_request) Failing after 29m12s
2026-01-12 15:55:38 +08:00
13 changed files with 345 additions and 1176 deletions

View File

@@ -4,6 +4,7 @@
package setting
import (
"strings"
"sync"
"code.gitea.io/gitea/modules/log"
@@ -47,9 +48,107 @@ func DefaultOpenWithEditorApps() OpenWithEditorAppsType {
}
}
// DevContainerEditorApp represents a configured IDE for DevContainer
type DevContainerEditorApp struct {
DisplayName string // Display name (from name= parameter or DisplayName)
Logo string // Logo URL (from logo= parameter)
OpenURL string // Complete URL template with placeholders, e.g. "vscode://mengning.devstar/openProject?host={host}&..."
}
type DevContainerEditorAppsType []DevContainerEditorApp
// ToTextareaString converts the configuration to textarea format
func (t DevContainerEditorAppsType) ToTextareaString() string {
ret := ""
for _, app := range t {
// Store the URL template with name and logo parameters included
urlTemplate := app.OpenURL
if app.DisplayName != "" && !strings.Contains(urlTemplate, "name=") {
separator := "&"
if !strings.Contains(urlTemplate, "?") {
separator = "?"
}
urlTemplate += separator + "name=" + app.DisplayName
}
if app.Logo != "" && !strings.Contains(urlTemplate, "logo=") {
urlTemplate += "&logo=" + app.Logo
}
ret += app.DisplayName + " = " + urlTemplate + "\n"
}
return ret
}
// DefaultDevContainerEditorApps returns the default DevContainer IDE configuration
// Based on current terminalURL format: ://mengning.devstar/openProject?host=xxx&hostname=xxx&port=xxx&username=xxx&path=xxx&access_token=xxx&devstar_username=xxx&devstar_domain=xxx
func DefaultDevContainerEditorApps() DevContainerEditorAppsType {
return DevContainerEditorAppsType{
{
DisplayName: "VSCode",
Logo: "https://code.visualstudio.com/favicon.ico",
OpenURL: "vscode://mengning.devstar/openProject?host={host}&hostname={hostname}&port={port}&username={username}&path={path}&access_token={token}&devstar_username={devstar_username}&devstar_domain={domain}",
},
{
DisplayName: "Cursor",
Logo: "https://cursor.sh/favicon.ico",
OpenURL: "cursor://mengning.devstar/openProject?host={host}&hostname={hostname}&port={port}&username={username}&path={path}&access_token={token}&devstar_username={devstar_username}&devstar_domain={domain}",
},
{
DisplayName: "Windsurf",
Logo: "https://windsurf.ai/favicon.ico",
OpenURL: "windsurf://mengning.devstar/openProject?host={host}&hostname={hostname}&port={port}&username={username}&path={path}&access_token={token}&devstar_username={devstar_username}&devstar_domain={domain}",
},
{
DisplayName: "Trae",
Logo: "https://lf-static.traecdn.us/obj/trae-ai-tx/trae_website/favicon.png",
OpenURL: "trae://mengning.devstar/openProject?host={host}&hostname={hostname}&port={port}&username={username}&path={path}&access_token={token}&devstar_username={devstar_username}&devstar_domain={domain}",
},
}
}
// ParseIDETemplate parses an IDE URL template and extracts name/logo parameters
// Returns: DisplayName, Logo, CleanURL (without name/logo parameters)
func ParseIDETemplate(urlTemplate string) (string, string, string) {
displayName := ""
logo := ""
cleanURL := urlTemplate
// Check if URL has query parameters
if strings.Contains(urlTemplate, "?") {
parts := strings.SplitN(urlTemplate, "?", 2)
if len(parts) == 2 {
baseURL := parts[0]
queryString := parts[1]
// Parse query parameters
queryParams := strings.Split(queryString, "&")
var cleanQueryParams []string
for _, param := range queryParams {
if strings.HasPrefix(param, "name=") {
displayName = strings.TrimPrefix(param, "name=")
} else if strings.HasPrefix(param, "logo=") {
logo = strings.TrimPrefix(param, "logo=")
} else {
cleanQueryParams = append(cleanQueryParams, param)
}
}
// Rebuild URL without name/logo parameters
if len(cleanQueryParams) > 0 {
cleanURL = baseURL + "?" + strings.Join(cleanQueryParams, "&")
} else {
cleanURL = baseURL
}
}
}
return displayName, logo, cleanURL
}
type RepositoryStruct struct {
OpenWithEditorApps *config.Value[OpenWithEditorAppsType]
GitGuideRemoteName *config.Value[string]
OpenWithEditorApps *config.Value[OpenWithEditorAppsType]
DevContainerEditorApps *config.Value[DevContainerEditorAppsType]
GitGuideRemoteName *config.Value[string]
}
type ConfigStruct struct {
@@ -70,8 +169,9 @@ func initDefaultConfig() {
EnableFederatedAvatar: config.ValueJSON[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}),
},
Repository: &RepositoryStruct{
OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
GitGuideRemoteName: config.ValueJSON[string]("repository.git-guide-remote-name").WithDefault("origin"),
OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
DevContainerEditorApps: config.ValueJSON[DevContainerEditorAppsType]("repository.devcontainer.editor-apps").WithDefault(DefaultDevContainerEditorApps()),
GitGuideRemoteName: config.ValueJSON[string]("repository.git-guide-remote-name").WithDefault("origin"),
},
}
}

View File

@@ -122,6 +122,8 @@ var (
ManifestData string
BeianNumber string // 网站备案号, e.g. 苏ICP备88888888888号-1
ParentDomain string // 父域名用于获取上级DevStar的相关服务 PARENT_DOMAIN
ParentAccessToken string // 父域名访问令牌用于获取上级DevStar的相关服务 PARENT_ACCESS_TOKEN
)
@@ -192,6 +194,8 @@ func loadServerFrom(rootCfg ConfigProvider) {
HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
HTTPPort = sec.Key("HTTP_PORT").MustString("3000")
BeianNumber = sec.Key("BEIAN_NUMBER").MustString("")
ParentDomain = sec.Key("PARENT_DOMAIN").MustString("https://devstar.cn")
ParentAccessToken = sec.Key("PARENT_ACCESS_TOKEN").MustString("439ffb6e2cce9ecb4568a5c54750ef290831d2ef")
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
// if these are removed, the warning will not be shown

View File

@@ -161,6 +161,12 @@ func NewFuncMap() template.FuncMap {
"BeianNumber": func() string {
return setting.BeianNumber
},
"ParentDomain": func() string {
return setting.ParentDomain
},
"ParentAccessToken": func() string {
return setting.ParentAccessToken
},
}
}

View File

@@ -3596,6 +3596,8 @@ config.disable_gravatar = Disable Gravatar
config.enable_federated_avatar = Enable Federated Avatars
config.open_with_editor_app_help = The "Open with" editors for the clone menu. If left empty, the default will be used. Expand to see the default.
config.git_guide_remote_name = Repository remote name for git commands in the guide
config.devcontainer_editor_apps = DevContainer Editor Apps
config.devcontainer_editor_app_help = Configure which AI IDEs appear in the devcontainer "Open with" menu. If left empty, the default will be used. Expand to see the default.
config.git_config = Git Configuration
config.git_disable_diff_highlight = Disable Diff Syntax Highlight

View File

@@ -3589,6 +3589,8 @@ config.disable_gravatar=禁用 Gravatar 头像
config.enable_federated_avatar=启用 Federated 头像
config.open_with_editor_app_help=用于克隆菜单的编辑器。如果为空将使用默认值。展开可以查看默认值。
config.git_guide_remote_name=指南中 git 命令使用的仓库远程名称
config.devcontainer_editor_apps=DevContainer 编辑器应用
config.devcontainer_editor_app_help=配置 DevContainer "打开方式" 菜单中显示的 AI IDE。如果为空将使用默认值。展开可以查看默认值。
config.git_config=Git 配置
config.git_disable_diff_highlight=禁用差异对比语法高亮

View File

@@ -197,6 +197,7 @@ func ConfigSettings(ctx *context.Context) {
ctx.Data["PageIsAdminConfig"] = true
ctx.Data["PageIsAdminConfigSettings"] = true
ctx.Data["DefaultOpenWithEditorAppsString"] = setting.DefaultOpenWithEditorApps().ToTextareaString()
ctx.Data["DefaultDevContainerEditorAppsString"] = setting.DefaultDevContainerEditorApps().ToTextareaString()
ctx.HTML(http.StatusOK, tplConfigSettings)
}
@@ -235,11 +236,44 @@ func ChangeConfig(ctx *context.Context) {
}
return json.Marshal(openWithEditorApps)
}
marshalDevContainerEditorApps := func(value string) ([]byte, error) {
lines := strings.Split(value, "\n")
var devContainerEditorApps setting.DevContainerEditorAppsType
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
configDisplayName, openURL, ok := strings.Cut(line, "=")
configDisplayName, openURL = strings.TrimSpace(configDisplayName), strings.TrimSpace(openURL)
if !ok || configDisplayName == "" || openURL == "" {
continue
}
// Parse name and logo from URL template
urlDisplayName, logo, cleanURL := setting.ParseIDETemplate(openURL)
// Use parsed name if available, otherwise use config display name
finalDisplayName := configDisplayName
if urlDisplayName != "" {
finalDisplayName = urlDisplayName
}
devContainerEditorApps = append(devContainerEditorApps, setting.DevContainerEditorApp{
DisplayName: finalDisplayName,
Logo: logo,
OpenURL: cleanURL,
})
}
return json.Marshal(devContainerEditorApps)
}
marshallers := map[string]func(string) ([]byte, error){
cfg.Picture.DisableGravatar.DynKey(): marshalBool,
cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
cfg.Repository.GitGuideRemoteName.DynKey(): marshalString(cfg.Repository.GitGuideRemoteName.DefaultValue()),
cfg.Picture.DisableGravatar.DynKey(): marshalBool,
cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
cfg.Repository.DevContainerEditorApps.DynKey(): marshalDevContainerEditorApps,
cfg.Repository.GitGuideRemoteName.DynKey(): marshalString(cfg.Repository.GitGuideRemoteName.DefaultValue()),
}
_ = ctx.Req.ParseForm()

View File

@@ -5,8 +5,10 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -20,6 +22,39 @@ const (
tplGetDevContainerDetails templates.TplName = "repo/devcontainer/details"
)
// IDEInfo represents IDE information for frontend
type IDEInfo struct {
Name string // Display name
Logo string // Logo URL
URL string // Complete IDE URL
}
// IDETemplateParams holds the parameters for IDE URL template replacement
type IDETemplateParams struct {
Host string
Hostname string
Port string
Username string
Path string
Token string
DevstarUsername string
Domain string
}
// replaceIDETemplate replaces placeholders in the IDE URL template with actual values
func replaceIDETemplate(template string, params IDETemplateParams) string {
result := template
result = strings.ReplaceAll(result, "{host}", params.Host)
result = strings.ReplaceAll(result, "{hostname}", params.Hostname)
result = strings.ReplaceAll(result, "{port}", params.Port)
result = strings.ReplaceAll(result, "{username}", params.Username)
result = strings.ReplaceAll(result, "{path}", params.Path)
result = strings.ReplaceAll(result, "{token}", params.Token)
result = strings.ReplaceAll(result, "{devstar_username}", params.DevstarUsername)
result = strings.ReplaceAll(result, "{domain}", params.Domain)
return result
}
// 获取仓库 Dev Container 详细信息
// GET /{username}/{reponame}/devcontainer
func GetDevContainerDetails(ctx *context.Context) {
@@ -159,10 +194,46 @@ func GetDevContainerDetails(ctx *context.Context) {
}
terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, ctx.Doer, ctx.Repo)
if err == nil {
ctx.Data["VSCodeUrl"] = "vscode" + terminalURL
ctx.Data["CursorUrl"] = "cursor" + terminalURL
ctx.Data["WindsurfUrl"] = "windsurf" + terminalURL
ctx.Data["TraeUrl"] = "trae" + terminalURL
// Parse terminalURL to extract parameter values
// terminalURL format: ://mengning.devstar/openProject?host=xxx&hostname=xxx&port=xxx&username=xxx&path=xxx&access_token=xxx&devstar_username=xxx&devstar_domain=xxx
// Add a fake scheme to make it parseable
parsedURL, err := url.Parse("http://dummy" + terminalURL)
if err == nil {
params := IDETemplateParams{
Host: parsedURL.Query().Get("host"),
Hostname: parsedURL.Query().Get("hostname"),
Port: parsedURL.Query().Get("port"),
Username: parsedURL.Query().Get("username"),
Path: parsedURL.Query().Get("path"),
Token: parsedURL.Query().Get("access_token"),
DevstarUsername: parsedURL.Query().Get("devstar_username"),
Domain: parsedURL.Query().Get("devstar_domain"),
}
// Get IDE apps from admin config or use local defaults
devContainerApps := setting.Config().Repository.DevContainerEditorApps.Value(ctx)
// If admin config is empty, use local defaults
if len(devContainerApps) == 0 {
devContainerApps = setting.DefaultDevContainerEditorApps()
log.Info("Using local default IDE configs: %d IDEs", len(devContainerApps))
} else {
log.Info("Using admin configured IDEs: %d IDEs", len(devContainerApps))
}
var ideInfos []IDEInfo
for _, app := range devContainerApps {
finalURL := replaceIDETemplate(app.OpenURL, params)
ideInfos = append(ideInfos, IDEInfo{
Name: app.DisplayName,
Logo: app.Logo,
URL: finalURL,
})
}
ctx.Data["DevContainerIDEs"] = ideInfos
} else {
log.Error("Failed to parse terminalURL: %v", err)
}
}
}
// 3. 携带数据渲染页面,返回
@@ -214,48 +285,70 @@ func GetDevContainerStatus(ctx *context.Context) {
log.Info("%v\n", err)
}
var vscodeUrl, cursorUrl, windsurfUrl, traeUrl string
var ideInfos []IDEInfo
// 增加对 ctx.Doer、ctx.Repo、ctx.Repo.Repository 的检查,避免出现空指针异常
// 只在第一次状态变为 "5" 时生成 URL避免频繁调用导致 token 被删除,随 Session 过期(默认 24 小时)
// 通过检查 session 中是否已有 terminal_url 来判断是否是第一次
// 通过检查 session 中是否已有 terminal_ides 来判断是否是第一次
if realTimeStatus == "5" && ctx.Doer != nil && ctx.Repo != nil && ctx.Repo.Repository != nil {
// 检查 session 中是否已有缓存的 URL
cachedUrls := ctx.Session.Get("terminal_urls")
// 检查 session 中是否已有缓存的 IDE 信息
cachedUrls := ctx.Session.Get("terminal_ides")
if cachedUrls == nil {
// 第一次状态为 "4",生成 URL 并缓存
// 第一次状态为 "5",生成 URL 并缓存
terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, ctx.Doer, ctx.Repo)
if err == nil {
vscodeUrl = "vscode" + terminalURL
cursorUrl = "cursor" + terminalURL
windsurfUrl = "windsurf" + terminalURL
traeUrl = "trae" + terminalURL
// 缓存 URL 到 session
ctx.Session.Set("terminal_urls", map[string]string{
"vscodeUrl": vscodeUrl,
"cursorUrl": cursorUrl,
"windsurfUrl": windsurfUrl,
"traeUrl": traeUrl,
})
// Parse terminalURL to extract parameter values
parsedURL, err := url.Parse("http://dummy" + terminalURL)
if err == nil {
params := IDETemplateParams{
Host: parsedURL.Query().Get("host"),
Hostname: parsedURL.Query().Get("hostname"),
Port: parsedURL.Query().Get("port"),
Username: parsedURL.Query().Get("username"),
Path: parsedURL.Query().Get("path"),
Token: parsedURL.Query().Get("access_token"),
DevstarUsername: parsedURL.Query().Get("devstar_username"),
Domain: parsedURL.Query().Get("devstar_domain"),
}
// Get IDE apps from admin config or use local defaults
devContainerApps := setting.Config().Repository.DevContainerEditorApps.Value(ctx)
// If admin config is empty, use local defaults
if len(devContainerApps) == 0 {
devContainerApps = setting.DefaultDevContainerEditorApps()
log.Info("Using local default IDE configs: %d IDEs", len(devContainerApps))
} else {
log.Info("Using admin configured IDEs: %d IDEs", len(devContainerApps))
}
var ideInfos []IDEInfo
for _, app := range devContainerApps {
finalURL := replaceIDETemplate(app.OpenURL, params)
ideInfos = append(ideInfos, IDEInfo{
Name: app.DisplayName,
Logo: app.Logo,
URL: finalURL,
})
}
// 缓存 IDE 信息到 session
ctx.Session.Set("terminal_ides", ideInfos)
} else {
log.Error("Failed to parse terminalURL: %v", err)
}
}
} else {
// 使用缓存的 URL
urls := cachedUrls.(map[string]string)
vscodeUrl = urls["vscodeUrl"]
cursorUrl = urls["cursorUrl"]
windsurfUrl = urls["windsurfUrl"]
traeUrl = urls["traeUrl"]
// 使用缓存的 IDE 信息
ides := cachedUrls.([]IDEInfo)
ideInfos = ides
}
} else {
// 状态不是 "4" 或 ctx.Doer 为 nil清除缓存的 URL
ctx.Session.Delete("terminal_urls")
// 状态不是 "5" 或 ctx.Doer 为 nil清除缓存的 IDE 信息
ctx.Session.Delete("terminal_ides")
}
ctx.JSON(http.StatusOK, map[string]string{
"status": realTimeStatus,
"vscodeUrl": vscodeUrl,
"cursorUrl": cursorUrl,
"windsurfUrl": windsurfUrl,
"traeUrl": traeUrl,
ctx.JSON(http.StatusOK, map[string]any{
"status": realTimeStatus,
"ideURLs": ideInfos,
})
}

View File

@@ -129,7 +129,7 @@ func Variables(ctx *context.Context) {
tagsJSONStr = "[]"
}
// 创建一个新的请求
req, err := http.NewRequest("GET", "http://devstar.cn/variables/export", nil)
req, err := http.NewRequest("GET", setting.ParentDomain + "/variables/export", nil)
if err != nil {
ctx.Data["DevstarVariables"] = []*devcontainer_model.DevcontainerVariable{}
} else {
@@ -237,7 +237,7 @@ func ScriptCreate(ctx *context.Context) {
}
if !exists {
// 创建一个新的请求来获取devstar变量
req, err := http.NewRequest("GET", "http://devstar.cn/variables/export", nil)
req, err := http.NewRequest("GET", setting.ParentDomain + "/variables/export", nil)
if err != nil {
log.Error("Failed to create request for devstar variables: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))

View File

@@ -1134,7 +1134,7 @@ func Get_IDE_TerminalURL(ctx *gitea_context.Context, doer *user.User, repo *gite
"&username=" + doer.Name +
"&path=" + fullWorkPath +
"&access_token=" + access_token +
"&devstar_username=" + repo.Repository.OwnerName +
"&devstar_username=" + doer.Name +
"&devstar_domain=" + setting.AppURL
// 添加 forwardPorts 参数(如果存在)

View File

@@ -21,6 +21,18 @@
<input type="hidden" name="key" value="{{$cfg.DynKey}}">
<input name="value" value="{{$cfg.Value ctx}}" placeholder="{{$cfg.DefaultValue}}" maxlength="100" dir="auto" required pattern="^[A-Za-z0-9][\-_A-Za-z0-9]*$">
</div>
<div class="field tw-mt-4">
<label>{{ctx.Locale.Tr "admin.config.devcontainer_editor_apps"}}</label>
<details>
<summary>{{ctx.Locale.Tr "admin.config.devcontainer_editor_app_help"}}</summary>
<pre class="tw-px-4">{{.DefaultDevContainerEditorAppsString}}</pre>
</details>
</div>
<div class="field">
{{$cfg = .SystemConfig.Repository.DevContainerEditorApps}}
<input type="hidden" name="key" value="{{$cfg.DynKey}}">
<textarea name="value" rows="6" placeholder="VSCode = vscode&#10;Cursor = cursor">{{($cfg.Value ctx).ToTextareaString}}</textarea>
</div>
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
</div>

View File

@@ -57,14 +57,12 @@
{{if .isAdmin}}
<div style=" display: none;" id="updateContainer" class="item"><a class="flex-text-inline" style="color:black; cursor:pointer; " href="#" onclick="if(typeof openSaveModal === 'function') { openSaveModal('{{.Repository.Link}}', '{{.Repository.Name}}'); return false; }">{{svg "octicon-database"}}{{ctx.Locale.Tr "repo.dev_container_control.update"}}</a></div>
{{end}}
<div style=" display: none;" id="webTerminal" class="item"><a class="flex-text-inline" style="color:black; " href="{{.WebSSHUrl}}" target="_blank">{{svg "octicon-code" 14}}open with WebTerminal</a></div>
<div style=" display: none;" id="vsTerminal" class="item"><a class="flex-text-inline" style="color:black; " onclick="window.location.href = '{{.VSCodeUrl}}'">{{svg "octicon-code" 14}}open with VSCode</a ></div>
<div style=" display: none;" id="cursorTerminal" class="item"><a class="flex-text-inline" style="color:black; " onclick="window.location.href = '{{.CursorUrl}}'">{{svg "octicon-code" 14}}open with Cursor</a ></div>
<div style=" display: none;" id="windsurfTerminal" class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.WindsurfUrl}}'">{{svg "octicon-code" 14}}open with Windsurf</a ></div>
<div style=" display: none;" id="traeTerminal" class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.TraeUrl}}'">{{svg "octicon-code" 14}}open with Trae</a ></div>
<div style=" display: none;" id="webTerminal" class="item"><a class="flex-text-inline" style="color:black; " href="{{.WebSSHUrl}}" target="_blank">{{svg "octicon-terminal" 14}}open with WebTerminal</a></div>
{{range .DevContainerIDEs}}
<div style=" display: none;" id="{{.Name}}Terminal" class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.URL}}'">{{svg "octicon-terminal" 14}}open with {{.Name}}</a></div>
{{end}}
{{end}}
{{if .ValidateDevContainerConfiguration}}
<div style=" display: none;" id="createContainer" class="item">

File diff suppressed because it is too large Load Diff

View File

@@ -15,13 +15,12 @@ function initDevContainerDetails() {
const restartContainer = document.getElementById('restartContainer');
const stopContainer = document.getElementById('stopContainer');
const webTerminal = document.getElementById('webTerminal');
const vsTerminal = document.getElementById('vsTerminal');
const cursorTerminal = document.getElementById('cursorTerminal');
const windsurfTerminal = document.getElementById('windsurfTerminal');
const traeTerminal = document.getElementById('traeTerminal');
const webTerminalContainer = document.getElementById('webTerminalContainer');
const loadingElement = document.getElementById('loading');
// Dynamically get all IDE terminal buttons
const ideTerminals = document.querySelectorAll('[id$="Terminal"]:not(#webTerminal)');
function concealElement() {
if (createContainer) createContainer.style.display = 'none';
if (deleteContainer) deleteContainer.style.display = 'none';
@@ -29,10 +28,10 @@ function initDevContainerDetails() {
if (restartContainer) restartContainer.style.display = 'none';
if (stopContainer) stopContainer.style.display = 'none';
if (webTerminal) webTerminal.style.display = 'none';
if (vsTerminal) vsTerminal.style.display = 'none';
if (cursorTerminal) cursorTerminal.style.display = 'none';
if (windsurfTerminal) windsurfTerminal.style.display = 'none';
if (traeTerminal) traeTerminal.style.display = 'none';
// Hide all IDE terminals
ideTerminals.forEach((terminal) => {
(terminal as HTMLElement).style.display = 'none';
});
if (webTerminalContainer) webTerminalContainer.style.display = 'none';
}
@@ -42,10 +41,10 @@ function initDevContainerDetails() {
if (restartContainer) restartContainer.style.display = 'block';
if (stopContainer) stopContainer.style.display = 'block';
if (webTerminal) webTerminal.style.display = 'block';
if (vsTerminal) vsTerminal.style.display = 'block';
if (cursorTerminal) cursorTerminal.style.display = 'block';
if (windsurfTerminal) windsurfTerminal.style.display = 'block';
if (traeTerminal) traeTerminal.style.display = 'block';
// Show all IDE terminals
ideTerminals.forEach((terminal) => {
(terminal as HTMLElement).style.display = 'block';
});
if (webTerminalContainer) webTerminalContainer.style.display = 'block';
}
@@ -81,21 +80,37 @@ function initDevContainerDetails() {
if (loadingElement) loadingElement.style.display = 'none';
if (restartContainer) restartContainer.style.display = 'none';
}
if (data.vscodeUrl) {
const vsBtn = document.querySelector('#vsTerminal a');
if (vsBtn) vsBtn.setAttribute('onclick', `window.location.href = '${data.vscodeUrl}'`);
}
if (data.cursorUrl) {
const cursorBtn = document.querySelector('#cursorTerminal a');
if (cursorBtn) cursorBtn.setAttribute('onclick', `window.location.href = '${data.cursorUrl}'`);
}
if (data.windsurfUrl) {
const windsurfBtn = document.querySelector('#windsurfTerminal a');
if (windsurfBtn) windsurfBtn.setAttribute('onclick', `window.location.href = '${data.windsurfUrl}'`);
}
if (data.traeUrl) {
const traeBtn = document.querySelector('#traeTerminal a');
if (traeBtn) traeBtn.setAttribute('onclick', `window.location.href = '${data.traeUrl}'`);
// Update IDE URLs dynamically
if (data.ideURLs && Array.isArray(data.ideURLs)) {
for (const ideInfo of data.ideURLs) {
const terminal = document.getElementById(`${ideInfo.Name}Terminal`);
if (terminal) {
const link = terminal.querySelector('a');
if (link) {
link.setAttribute('onclick', `window.location.href = '${ideInfo.URL}'`);
}
// Update logo if available
if (ideInfo.Logo) {
const icon = terminal.querySelector('svg');
if (icon) {
// Clone the SVG to preserve it for fallback
const svgClone = icon.cloneNode(true) as SVGElement;
// Replace SVG with img if logo is available
const imgElement = document.createElement('img');
imgElement.src = ideInfo.Logo;
imgElement.alt = ideInfo.Name;
imgElement.style.width = '14px';
imgElement.style.height = '14px';
// Add error handler to restore default SVG if logo fails to load
imgElement.onerror = function() {
console.warn(`Failed to load logo for ${ideInfo.Name}: ${ideInfo.Logo}`);
this.replaceWith(svgClone);
};
icon.replaceWith(imgElement);
}
}
}
}
}
displayElement();
if (loadingElement) loadingElement.style.display = 'none';