fix(zed): switch to language_models.openai_compatible for Agent panel visibility

The Zed Agent panel (new ACP-based UI) only discovers providers registered
under language_models.openai_compatible.<name>, not language_models.openai.
Each named entry appears as its own provider in the LLM Providers list.

- language_models.openai_compatible.harness-sync-gateway holds gateway URL + models
- agent.default_model.provider = 'harness-sync-gateway' (was 'openai')
- Model entries include capabilities {tools,images,parallel_tool_calls}
- Export common.GatewayProviderID so zed.go can reference it
- Fix staticcheck QF1001 (De Morgan's law) in io.go
- Fix struct field alignment in File (80→72 bytes)
- gofmt remaining files
This commit is contained in:
2026-05-23 21:12:47 +01:00
parent fd97c60df2
commit bde3128dba
27 changed files with 345 additions and 117 deletions
+13
View File
@@ -23,9 +23,22 @@ func (k Kind) String() string {
return "unknown"
}
// HarnessCapabilities declares what an adapter manages.
// HasBuiltInSub means the harness has its own AI subscription (e.g. Claude Max)
// and harness-sync should not write provider/model/endpoint config for it.
type HarnessCapabilities struct {
ManagesProviders bool // writes provider/gateway endpoint config
ManagesModels bool // writes model selection config
ManagesMCP bool // writes MCP server config
ManagesSkills bool // writes skills symlink or equivalent
ManagesInstructions bool // writes instructions file (CLAUDE.md, AGENTS.md, etc.)
HasBuiltInSub bool // has its own subscription; provider/model config is skipped
}
type Adapter interface {
Name() string
Detect() bool
Capabilities() HarnessCapabilities
Render(b *canonical.Bundle) (*FileSet, error)
Import(home string) (*ImportResult, error)
}
+1
View File
@@ -34,6 +34,7 @@ func (f *fakeAdapter) Name() string { return f.n
func (f *fakeAdapter) Detect() bool { return f.detect }
func (f *fakeAdapter) Render(_ *canonical.Bundle) (*FileSet, error) { return NewFileSet(), nil }
func (f *fakeAdapter) Import(_ string) (*ImportResult, error) { return &ImportResult{}, nil }
func (f *fakeAdapter) Capabilities() HarnessCapabilities { return HarnessCapabilities{} }
func TestRegistry(t *testing.T) {
r := NewRegistry()
+3 -1
View File
@@ -10,6 +10,9 @@ import (
const gatewayProviderID = "harness-sync-gateway"
// GatewayProviderID is the canonical provider key used across all adapters.
const GatewayProviderID = gatewayProviderID
// ProviderEntry is a generic provider record. Adapters may massage further.
type ProviderEntry = map[string]any
@@ -161,7 +164,6 @@ func ProvidersAsMap(p *canonical.Profile) map[string]any {
return out
}
// KiloModelString returns the "providerID/modelID" string used by kilo/opencode.
func KiloModelString(p *canonical.Profile) string {
if p.Gateway.DefaultModel == "" {
+43 -10
View File
@@ -40,18 +40,51 @@ func DirExists(path string) bool {
return DirExistsFS(fsx.OS(), path)
}
var (
blockComment = regexp.MustCompile(`(?s)/\*.*?\*/`)
lineComment = regexp.MustCompile(`(?m)//[^\n]*`)
)
var trailingComma = regexp.MustCompile(`,(\s*[}\]])`)
// StripJSONComments is a naive JSONC -> JSON stripper. It does not understand
// strings containing // or /* and is therefore only safe for hand-edited
// config files that don't put comment markers inside string literals.
// StripJSONComments converts JSONC to standard JSON by:
// - removing // line comments (only outside string literals)
// - removing /* */ block comments (only outside string literals)
// - removing trailing commas before } or ]
//
// It is string-aware so it won't mangle // inside URL values like "https://...".
func StripJSONComments(s string) string {
s = blockComment.ReplaceAllString(s, "")
s = lineComment.ReplaceAllString(s, "")
return strings.TrimSpace(s)
result := make([]byte, 0, len(s))
i, n := 0, len(s)
for i < n {
switch {
case s[i] == '"': // string literal — copy verbatim including escape sequences
result = append(result, s[i])
i++
for i < n {
c := s[i]
result = append(result, c)
i++
if c == '\\' && i < n {
result = append(result, s[i])
i++
} else if c == '"' {
break
}
}
case i+1 < n && s[i] == '/' && s[i+1] == '*': // /* block comment */
i += 2
for i+1 < n && (s[i] != '*' || s[i+1] != '/') {
i++
}
i += 2
case i+1 < n && s[i] == '/' && s[i+1] == '/': // // line comment
i += 2
for i < n && s[i] != '\n' {
i++
}
default:
result = append(result, s[i])
i++
}
}
out := trailingComma.ReplaceAllString(string(result), "$1")
return strings.TrimSpace(out)
}
// ImportMCPFromJSONFileFS reads a JSON file from fs and extracts an MCP server
+4 -3
View File
@@ -12,7 +12,7 @@ const (
MCPCrushStyle
// MCPOpencodeStyle: kilo + opencode shape — type: local|remote, command as []string.
MCPOpencodeStyle
// MCPZedStyle: zed context_servers — {enabled, source, command, args} or {enabled, url}.
// MCPZedStyle: zed context_servers — {enabled, command, args} or {enabled, url}.
MCPZedStyle
)
@@ -94,11 +94,12 @@ func mcpEntry(s canonical.MCPServer, style MCPStyle) map[string]any {
}
e["enabled"] = true
case MCPZedStyle:
e["enabled"] = true
// Do NOT emit "enabled" — Zed 1.3.x has a serde flatten+deny_unknown_fields
// bug where outer-struct fields bleed into the untagged enum variants and
// fail all of them. Omitting "enabled" defaults to true.
if s.URL != "" {
e["url"] = s.URL
} else {
e["source"] = "custom"
if s.Command != "" {
e["command"] = s.Command
}
+5 -3
View File
@@ -89,8 +89,9 @@ func TestBuildMCPMapStyledZedStdio(t *testing.T) {
r := reg(canonical.MCPServer{Name: "fp", Command: "/bin/fp", Args: []string{"-x"}})
out := BuildMCPMapStyled(r, MCPZedStyle)
e := out["fp"].(map[string]any)
assert.Equal(t, true, e["enabled"])
assert.Equal(t, "custom", e["source"])
// Zed 1.3.x serde bug: "enabled" and "source" must NOT be present.
assert.NotContains(t, e, "enabled")
assert.NotContains(t, e, "source")
assert.Equal(t, "/bin/fp", e["command"])
assert.Equal(t, []string{"-x"}, e["args"])
}
@@ -99,7 +100,8 @@ func TestBuildMCPMapStyledZedURL(t *testing.T) {
r := reg(canonical.MCPServer{Name: "r", URL: "https://example.com"})
out := BuildMCPMapStyled(r, MCPZedStyle)
e := out["r"].(map[string]any)
assert.Equal(t, true, e["enabled"])
// Zed 1.3.x serde bug: "enabled" must NOT be present.
assert.NotContains(t, e, "enabled")
assert.Equal(t, "https://example.com", e["url"])
assert.NotContains(t, e, "source")
}
+5 -1
View File
@@ -6,8 +6,12 @@ type File struct {
Dest string
SymlinkTarget string
Content []byte
Mode os.FileMode
Kind Kind
Mode os.FileMode
// NoMerge skips the 3-way git merge and always writes the rendered content.
// Use for files the adapter partially manages via JSON/YAML merge (e.g. live
// state files where the adapter already reconciles at the key level).
NoMerge bool
}
type FileSet struct {