Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
65038b2
feat(web):基础的网页端,应用端界面
Yumiue Apr 28, 2026
b09e5c2
feat:网页端,应用端初始化
Yumiue Apr 30, 2026
508d993
fix(web): 修复合并后 TypeScript 类型错误
Yumiue Apr 30, 2026
d104f93
fix:修复一些规范问题
Yumiue Apr 30, 2026
a74fa22
fix(web): 修复 Gateway 自动连接因编译超时失败的问题
Yumiue Apr 30, 2026
d48eca4
chore: 移除误提交的构建产物
Yumiue Apr 30, 2026
36cd5ac
fix:修复会话授权的问题
Yumiue Apr 30, 2026
73a6a49
fix:修复会话区会话渲染问题
Yumiue Apr 30, 2026
2696e28
fix:会话区文件夹能正常展开
Yumiue Apr 30, 2026
a89622d
fix:迁移的小修改
Yumiue May 1, 2026
3945f65
feat(web):接入slash
Yumiue May 1, 2026
19a52a2
fix:略微调整了布局
Yumiue May 1, 2026
6f93384
fix:provider完善
Yumiue May 1, 2026
eb9ce65
fix:修复一些硬编码问题
Yumiue May 1, 2026
c093bdd
fix(mcp):支持热加入mcp
Yumiue May 1, 2026
0569665
fix:加了并发管控,避免并发卡死
Yumiue May 1, 2026
456251f
feat:build,paln模式的接入
Yumiue May 1, 2026
284e9e2
fix:优化对话的工具调用位置
Yumiue May 1, 2026
00ff1f2
feat:新增工作区管理,现在可以自定义工作区了,并且修复了provider的延迟刷新bug
Yumiue May 3, 2026
652eb40
feat:新增cli启动指令,现在可以neocode web启动
Yumiue May 3, 2026
00c7be2
fix:修改工具调用内容的样式和代码块的呈现效果
Yumiue May 3, 2026
d74e2b5
feat:接入plan执行确认
Yumiue May 3, 2026
093c1b3
fix:修复了provider,mcp等全局配置不同工作区不共同切换的问题
Yumiue May 3, 2026
60693bb
Revert "feat:接入plan执行确认"
Yumiue May 3, 2026
54eee85
feat:支持输出时向上滚动,并且回到底部自动跟随输出滚动
Yumiue May 3, 2026
2fa2ca9
fix:修复新建会话报错和第一个对话不能停止的bug
Yumiue May 3, 2026
205dd58
fix:空会话不再报会话列表加载失败
Yumiue May 4, 2026
11719fa
fix:现在electron端能正常构建使用了
Yumiue May 4, 2026
ab7886c
fix:修复electron端的一个莫名组件bug
Yumiue May 4, 2026
ba16969
fix:修复视觉问题
Yumiue May 4, 2026
f4b742e
fix:修复electron构建的错误
Yumiue May 4, 2026
7054598
test: fix rebase compatibility checks
Yumiue May 4, 2026
fbdd079
fix:修复Windows平台路径兼容性导致的测试失败
Yumiue May 4, 2026
75e02ee
fix:修复Windows平台路径兼容性导致的测试失败
Yumiue May 4, 2026
0f4a56a
Merge branch 'html_gui_build' of https://github.com/Yumiue/neo-code i…
Yumiue May 4, 2026
8287f36
fix:修复测试错误
Yumiue May 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,10 @@ workspace.xml
# VitePress / frontend build artifacts
www/.vitepress/cache/
www/.vitepress/dist/

# Web frontend build artifacts
web/dist/
web/.vite/
web/build/
web/release/
web/dist-electron/
2 changes: 2 additions & 0 deletions docs/reference/gateway-rpc-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ type RunParams struct {
InputText string `json:"input_text,omitempty"` // 与 input_parts 至少一个非空
InputParts []RunInputPart `json:"input_parts,omitempty"` // text|image
Workdir string `json:"workdir,omitempty"` // 请求级工作目录覆盖
Mode string `json:"mode,omitempty"` // Agent 工作模式:build|plan,可选,默认沿用 session 当前 mode
}

type RunInputPart struct {
Expand All @@ -320,6 +321,7 @@ type RunInputPart struct {
2. `type=image` 时 `media.uri` 与 `media.mime_type` `MUST` 非空。
3. 未知字段会因严格解码触发 `invalid_frame`。
4. `run_id` 归一化顺序为:显式 `run_id` > `request_id` > 网关生成 `run_<timestamp>`。
5. `mode` 可选值为 `"build"` 或 `"plan"`,为空时默认沿用 session 当前 mode(新会话默认为 `"build"`)。切换 mode 后,后端会更新 session 并影响后续运行的工具可用性和 prompt 策略。

Response Schema:

Expand Down
27 changes: 26 additions & 1 deletion internal/app/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package app

import (
"context"
"fmt"
"log"
"os"
"path/filepath"
Expand Down Expand Up @@ -123,6 +124,7 @@ type RuntimeBundle struct {
Runtime agentruntime.Runtime
SessionStore *agentsession.SQLiteStore
ProviderSelection *configstate.Service
ToolRegistry *tools.Registry
MemoService *memo.Service
Close func() error // 用于清理 bundle 运行期间拉起的系统资源
}
Expand Down Expand Up @@ -270,6 +272,7 @@ func BuildGatewayServerDeps(ctx context.Context, opts BootstrapOptions) (Runtime
Runtime: runtimeImpl,
SessionStore: sessionStore,
ProviderSelection: sharedDeps.ProviderSelection,
ToolRegistry: toolRegistry,
MemoService: memoSvc,
Close: closeBundle,
}, nil
Expand Down Expand Up @@ -405,6 +408,7 @@ func BuildTUIClientDeps(ctx context.Context, opts BootstrapOptions) (RuntimeBund
Config: sharedDeps.Config,
ConfigManager: sharedDeps.ConfigManager,
ProviderSelection: sharedDeps.ProviderSelection,
ToolRegistry: nil,
MemoService: nil,
Close: nil,
}, nil
Expand Down Expand Up @@ -452,7 +456,7 @@ func buildToolRegistry(cfg config.Config) (*tools.Registry, func() error, error)
}))
toolRegistry.Register(todo.New())
toolRegistry.Register(spawnsubagent.New())
mcpRegistry, err := buildMCPRegistry(cfg)
mcpRegistry, err := BuildMCPRegistry(cfg)
if err != nil {
return nil, nil, err
}
Expand All @@ -471,6 +475,27 @@ func buildToolRegistry(cfg config.Config) (*tools.Registry, func() error, error)
}

// buildSkillsRegistry 负责按“项目优先全局”顺序构建本地 skills registry。
// RebuildMCPServersForRegistry 根据最新配置重建指定 registry 中的 MCP server;失败时保留旧 registry 不替换。
func RebuildMCPServersForRegistry(registry *tools.Registry, cfg config.Config) error {
if registry == nil {
return nil
}
newMcpRegistry, err := BuildMCPRegistry(cfg)
if err != nil {
return fmt.Errorf("app: build mcp registry: %w", err)
}
var filter mcp.ExposureFilter
if newMcpRegistry != nil {
filter = mcp.NewExposureFilter(mcp.ExposureFilterConfig{
Allowlist: cfg.Tools.MCP.Exposure.Allowlist,
Denylist: cfg.Tools.MCP.Exposure.Denylist,
Agents: buildMCPAgentExposureRules(cfg.Tools.MCP.Exposure.Agents),
})
}
registry.ReplaceMCPRegistry(newMcpRegistry, filter)
return nil
}

func buildSkillsRegistry(ctx context.Context, baseDir string, workdir string) skills.Registry {
loaders := make([]skills.Loader, 0, 2)
projectRoot := resolveWorkspaceSkillsRoot(workdir)
Expand Down
14 changes: 7 additions & 7 deletions internal/app/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,9 +386,9 @@ func TestBuildMCPRegistryFromConfig(t *testing.T) {
return registry.RefreshServerTools(context.Background(), server.ID)
}

registry, err := buildMCPRegistry(cfg)
registry, err := BuildMCPRegistry(cfg)
if err != nil {
t.Fatalf("buildMCPRegistry() error = %v", err)
t.Fatalf("BuildMCPRegistry() error = %v", err)
}
if registry == nil {
t.Fatalf("expected non-nil mcp registry")
Expand All @@ -415,7 +415,7 @@ func TestBuildMCPRegistryUnsupportedSource(t *testing.T) {
},
}

registry, err := buildMCPRegistry(cfg)
registry, err := BuildMCPRegistry(cfg)
if err == nil {
t.Fatalf("expected unsupported source error")
}
Expand Down Expand Up @@ -735,9 +735,9 @@ func TestBuildMCPRegistryNoEnabledServerReturnsNil(t *testing.T) {
{ID: "docs", Enabled: false, Source: "stdio"},
}

registry, err := buildMCPRegistry(cfg)
registry, err := BuildMCPRegistry(cfg)
if err != nil {
t.Fatalf("buildMCPRegistry() error = %v", err)
t.Fatalf("BuildMCPRegistry() error = %v", err)
}
if registry != nil {
t.Fatalf("expected nil registry when no enabled server")
Expand All @@ -757,7 +757,7 @@ func TestBuildMCPRegistryRegisterError(t *testing.T) {
return errors.New("register failed")
}

_, err := buildMCPRegistry(cfg)
_, err := BuildMCPRegistry(cfg)
if err == nil || !strings.Contains(err.Error(), "register failed") {
t.Fatalf("expected wrapped register error, got %v", err)
}
Expand Down Expand Up @@ -789,7 +789,7 @@ func TestBuildMCPRegistryRollbackRegisteredServersOnFailure(t *testing.T) {
return nil
}

registry, err := buildMCPRegistry(cfg)
registry, err := BuildMCPRegistry(cfg)
if err == nil || !strings.Contains(err.Error(), "search register failed") {
t.Fatalf("expected wrapped register error, got %v", err)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/app/mcp_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import (
var newMCPStdioClient = mcp.NewStdIOClient
var registerMCPStdioServer = defaultRegisterMCPStdioServer

// buildMCPRegistry 按配置构建并初始化 MCP registry;若无启用 server 则返回 nil。
func buildMCPRegistry(cfg config.Config) (*mcp.Registry, error) {
// BuildMCPRegistry 按配置构建并初始化 MCP registry;若无启用 server 则返回 nil。
func BuildMCPRegistry(cfg config.Config) (*mcp.Registry, error) {
if len(cfg.Tools.MCP.Servers) == 0 {
return nil, nil
}
Expand Down
80 changes: 52 additions & 28 deletions internal/cli/gateway_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type gatewayCommandOptions struct {

MetricsEnabled bool
MetricsEnabledOverridden bool
SkipIPC bool
}

// defaultNewAuthManager 创建默认网关认证器,并把具体持久化实现收敛在 CLI 装配层内部。
Expand Down Expand Up @@ -181,8 +182,18 @@ func mustReadInheritedWorkdir(cmd *cobra.Command) string {

// defaultGatewayCommandRunner 使用网关服务骨架启动本地 IPC 监听并处理中断退出。
func defaultGatewayCommandRunner(ctx context.Context, options gatewayCommandOptions) error {
return startGatewayServer(ctx, options, "", nil)
}

// startGatewayServer 启动网关服务的共享实现,staticFileDir 非空时同时提供 SPA 静态文件服务。
// onNetworkReady 在网络服务器开始监听后回调,传出实际监听地址。
func startGatewayServer(ctx context.Context, options gatewayCommandOptions, staticFileDir string, onNetworkReady func(address string)) error {
logger := log.New(os.Stderr, "neocode-gateway: ", log.LstdFlags)
logger.Printf("starting gateway (log-level=%s)", options.LogLevel)
logPrefix := "starting gateway"
if staticFileDir != "" {
logPrefix = "starting gateway with web UI"
}
logger.Printf("%s (log-level=%s)", logPrefix, options.LogLevel)

signalContext, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()
Expand Down Expand Up @@ -233,24 +244,34 @@ func defaultGatewayCommandRunner(ctx context.Context, options gatewayCommandOpti
idleCloser := newGatewayIdleShutdownController(logger, cancelRuntime)
defer idleCloser.close()

ipcServer, err := newGatewayServer(gateway.ServerOptions{
ListenAddress: options.ListenAddress,
Logger: logger,
MaxConnections: gatewayConfig.Limits.IPCMaxConnections,
MaxFrameSize: int64(gatewayConfig.Limits.MaxFrameBytes),
ReadTimeout: time.Duration(gatewayConfig.Timeouts.IPCReadSec) * time.Second,
WriteTimeout: time.Duration(gatewayConfig.Timeouts.IPCWriteSec) * time.Second,
Relay: relay,
Authenticator: authManager,
ACL: acl,
Metrics: metrics,
ConnectionCountChanged: func(active int) {
idleCloser.observe(active)
},
})
if err != nil {
return err
type transportAdapterEntry struct {
name string
adapter gateway.TransportAdapter
}
var transportAdapters []transportAdapterEntry

if !options.SkipIPC {
ipcServer, err := newGatewayServer(gateway.ServerOptions{
ListenAddress: options.ListenAddress,
Logger: logger,
MaxConnections: gatewayConfig.Limits.IPCMaxConnections,
MaxFrameSize: int64(gatewayConfig.Limits.MaxFrameBytes),
ReadTimeout: time.Duration(gatewayConfig.Timeouts.IPCReadSec) * time.Second,
WriteTimeout: time.Duration(gatewayConfig.Timeouts.IPCWriteSec) * time.Second,
Relay: relay,
Authenticator: authManager,
ACL: acl,
Metrics: metrics,
ConnectionCountChanged: func(active int) {
idleCloser.observe(active)
},
})
if err != nil {
return err
}
transportAdapters = append(transportAdapters, transportAdapterEntry{name: "ipc", adapter: ipcServer})
}

networkServer, err := newGatewayNetwork(gateway.NetworkServerOptions{
ListenAddress: options.HTTPAddress,
Logger: logger,
Expand All @@ -264,22 +285,19 @@ func defaultGatewayCommandRunner(ctx context.Context, options gatewayCommandOpti
ACL: acl,
Metrics: metrics,
AllowedOrigins: gatewayConfig.Security.AllowOrigins,
StaticFileDir: staticFileDir,
ConnectionCountChanged: func(active int) {
idleCloser.observe(active)
},
})
if err != nil {
_ = ipcServer.Close(context.Background())
for _, entry := range transportAdapters {
_ = entry.adapter.Close(context.Background())
}
return err
}
type transportAdapterEntry struct {
name string
adapter gateway.TransportAdapter
}
transportAdapters := []transportAdapterEntry{
{name: "ipc", adapter: ipcServer},
{name: "network", adapter: networkServer},
}
transportAdapters = append(transportAdapters, transportAdapterEntry{name: "network", adapter: networkServer})

defer func() {
relay.Stop()
for index := len(transportAdapters) - 1; index >= 0; index-- {
Expand All @@ -291,6 +309,11 @@ func defaultGatewayCommandRunner(ctx context.Context, options gatewayCommandOpti
logger.Printf("gateway %s listen address: %s", entry.name, entry.adapter.ListenAddress())
}

// 网络服务器就绪后通知调用方(用于打开浏览器)
if onNetworkReady != nil {
onNetworkReady(networkServer.ListenAddress())
}

for index, entry := range transportAdapters {
if index == 0 {
continue
Expand All @@ -299,8 +322,9 @@ func defaultGatewayCommandRunner(ctx context.Context, options gatewayCommandOpti
serveErr := networkAdapter.Serve(runtimeContext, runtimePort)
if serveErr != nil && runtimeContext.Err() == nil {
logger.Printf(
"warning: HTTP server failed to start on %s (port in use?), but IPC server is still running: %v",
"warning: %s server failed to start on %s: %v",
networkAdapter.ListenAddress(),
entry.name,
serveErr,
)
}
Expand Down
Loading
Loading