Skip to content

Commit 16316d0

Browse files
authored
Merge pull request #673 from pionxe/main
feat(tui-v2): launch TUI v2 framework and agent behavior stream with smart scroll engine (Phases 0-8)
2 parents b269041 + c960288 commit 16316d0

34 files changed

Lines changed: 6118 additions & 0 deletions

AGENTS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@
3939

4040
如需了解当前各模块的具体实现细节(如 budget 闭环、事件协议、compact 策略),参考 `docs/` 目录下对应文档,而非本文件。
4141

42+
### TUI v2 Phase 0 开发边界
43+
- TUI v1 (`internal/tui/`) 冻结不改;TUI v2 的新实现统一放在 `internal/tuiv2/`
44+
- TUI v2 使用独立二进制入口 `cmd/neocode-tuiv2/main.go`,不复用或改写 TUI v1 入口。
45+
- TUI v2 禁止 import `internal/runtime``internal/session``internal/repository` 或任何 SQLite 相关包。
46+
- TUI v2 不直接使用真实 Gateway 内部 server;真实通信能力必须通过客户端适配器收敛。
47+
- Fake 实现模拟的是 Gateway 客户端接口,不是 `FakeRuntime`
48+
- UI 组件不直接写死假数据,所有数据流必须从 Gateway 客户端接口进入。
49+
4250
## 5. AI 修改代码时的执行流程
4351
- 先定位改动所属模块,再检查是否会破坏职责边界。
4452
- 优先做最小闭环改动,避免无关重构。

cmd/neocode-tuiv2/main.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"io"
7+
"os"
8+
9+
tea "github.com/charmbracelet/bubbletea"
10+
11+
"neo-code/internal/tuiv2"
12+
"neo-code/internal/tuiv2/fakegateway"
13+
"neo-code/internal/tuiv2/gateway"
14+
)
15+
16+
const (
17+
backendFake = "fake"
18+
backendGateway = "gateway"
19+
)
20+
21+
// main 是 TUI v2 独立二进制入口,只负责参数解析、客户端选择和启动 Bubble Tea 程序。
22+
func main() {
23+
cfg, err := parseStartupConfig(os.Args[1:], os.Stderr)
24+
if err != nil {
25+
fmt.Fprintln(os.Stderr, err)
26+
os.Exit(2)
27+
}
28+
29+
client, err := newGatewayClient(cfg)
30+
if err != nil {
31+
fmt.Fprintln(os.Stderr, err)
32+
os.Exit(2)
33+
}
34+
cfg.Client = client
35+
36+
if _, err := tea.NewProgram(
37+
tuiv2.NewApp(cfg),
38+
tea.WithInput(os.Stdin),
39+
tea.WithOutput(os.Stdout),
40+
tea.WithAltScreen(),
41+
).Run(); err != nil {
42+
fmt.Fprintf(os.Stderr, "start TUI v2: %v\n", err)
43+
os.Exit(1)
44+
}
45+
}
46+
47+
// parseStartupConfig 解析 TUI v2 独立入口参数,保持与 v1 cobra 命令树完全隔离。
48+
func parseStartupConfig(args []string, stderr io.Writer) (tuiv2.StartupConfig, error) {
49+
cfg := tuiv2.StartupConfig{
50+
Backend: backendFake,
51+
Scenario: fakegateway.ScenarioDefault,
52+
}
53+
54+
flags := flag.NewFlagSet("neocode-tuiv2", flag.ContinueOnError)
55+
flags.SetOutput(stderr)
56+
flags.StringVar(&cfg.Backend, "backend", cfg.Backend, "gateway backend: fake or gateway")
57+
flags.StringVar(&cfg.Scenario, "scenario", cfg.Scenario, "fake gateway scenario")
58+
flags.BoolVar(&cfg.Debug, "debug", false, "show TUI v2 debug information")
59+
60+
if err := flags.Parse(args); err != nil {
61+
return tuiv2.StartupConfig{}, err
62+
}
63+
if flags.NArg() > 0 {
64+
return tuiv2.StartupConfig{}, fmt.Errorf("unexpected positional arguments: %v", flags.Args())
65+
}
66+
if cfg.Backend == "" {
67+
return tuiv2.StartupConfig{}, fmt.Errorf("--backend must not be empty")
68+
}
69+
if cfg.Scenario == "" {
70+
return tuiv2.StartupConfig{}, fmt.Errorf("--scenario must not be empty")
71+
}
72+
return cfg, nil
73+
}
74+
75+
// newGatewayClient 根据启动参数创建 Gateway 客户端,真实 Gateway 后端保留到后续阶段接入。
76+
func newGatewayClient(cfg tuiv2.StartupConfig) (gateway.Client, error) {
77+
switch cfg.Backend {
78+
case backendFake:
79+
return fakegateway.New(fakegateway.Config{Scenario: cfg.Scenario})
80+
case backendGateway:
81+
return nil, fmt.Errorf("--backend=gateway is reserved for Phase 20")
82+
default:
83+
return nil, fmt.Errorf("unsupported --backend=%q", cfg.Backend)
84+
}
85+
}

cmd/neocode-tuiv2/main_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package main
2+
3+
import (
4+
"io"
5+
"testing"
6+
7+
"neo-code/internal/tuiv2"
8+
"neo-code/internal/tuiv2/fakegateway"
9+
)
10+
11+
func TestParseStartupConfigDefaults(t *testing.T) {
12+
cfg, err := parseStartupConfig(nil, io.Discard)
13+
if err != nil {
14+
t.Fatalf("parseStartupConfig() error = %v", err)
15+
}
16+
17+
if cfg.Backend != backendFake {
18+
t.Fatalf("Backend = %q, want %q", cfg.Backend, backendFake)
19+
}
20+
if cfg.Scenario != fakegateway.ScenarioDefault {
21+
t.Fatalf("Scenario = %q, want %q", cfg.Scenario, fakegateway.ScenarioDefault)
22+
}
23+
if cfg.Debug {
24+
t.Fatal("Debug = true, want false")
25+
}
26+
}
27+
28+
func TestParseStartupConfigExplicitValues(t *testing.T) {
29+
cfg, err := parseStartupConfig([]string{
30+
"--backend=fake",
31+
"--scenario=tool_approval",
32+
"--debug",
33+
}, io.Discard)
34+
if err != nil {
35+
t.Fatalf("parseStartupConfig() error = %v", err)
36+
}
37+
38+
if cfg.Backend != backendFake {
39+
t.Fatalf("Backend = %q, want %q", cfg.Backend, backendFake)
40+
}
41+
if cfg.Scenario != fakegateway.ScenarioToolApproval {
42+
t.Fatalf("Scenario = %q, want %q", cfg.Scenario, fakegateway.ScenarioToolApproval)
43+
}
44+
if !cfg.Debug {
45+
t.Fatal("Debug = false, want true")
46+
}
47+
}
48+
49+
func TestParseStartupConfigRejectsInvalidShape(t *testing.T) {
50+
tests := []struct {
51+
name string
52+
args []string
53+
}{
54+
{name: "positional", args: []string{"extra"}},
55+
{name: "empty backend", args: []string{"--backend="}},
56+
{name: "empty scenario", args: []string{"--scenario="}},
57+
}
58+
59+
for _, tt := range tests {
60+
t.Run(tt.name, func(t *testing.T) {
61+
if _, err := parseStartupConfig(tt.args, io.Discard); err == nil {
62+
t.Fatal("parseStartupConfig() error = nil, want error")
63+
}
64+
})
65+
}
66+
}
67+
68+
func TestNewGatewayClient(t *testing.T) {
69+
cfg, err := parseStartupConfig([]string{"--scenario=gateway_offline"}, io.Discard)
70+
if err != nil {
71+
t.Fatalf("parseStartupConfig() error = %v", err)
72+
}
73+
74+
client, err := newGatewayClient(cfg)
75+
if err != nil {
76+
t.Fatalf("newGatewayClient() error = %v", err)
77+
}
78+
if client == nil {
79+
t.Fatal("newGatewayClient() client = nil")
80+
}
81+
}
82+
83+
func TestNewGatewayClientRejectsReservedAndUnknownBackends(t *testing.T) {
84+
tests := []string{backendGateway, "other"}
85+
for _, backend := range tests {
86+
t.Run(backend, func(t *testing.T) {
87+
_, err := newGatewayClient(mustConfig(t, []string{"--backend=" + backend}))
88+
if err == nil {
89+
t.Fatal("newGatewayClient() error = nil, want error")
90+
}
91+
})
92+
}
93+
}
94+
95+
func mustConfig(t *testing.T, args []string) tuiv2.StartupConfig {
96+
t.Helper()
97+
cfg, err := parseStartupConfig(args, io.Discard)
98+
if err != nil {
99+
t.Fatalf("parseStartupConfig() error = %v", err)
100+
}
101+
return cfg
102+
}

0 commit comments

Comments
 (0)