mirror of
https://github.com/lukaszraczylo/harness-sync.git
synced 2026-10-12 00:45:22 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user