Skip to content

Commit ceba74a

Browse files
committed
feat(tui):增加web端的tui入口
1 parent a1deb84 commit ceba74a

5 files changed

Lines changed: 153 additions & 0 deletions

File tree

internal/tui/core/app/commands.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const (
3636
slashCommandSkills = "/skills"
3737
slashCommandSkill = "/skill"
3838
slashCommandCheckpoint = "/checkpoint"
39+
slashCommandWeb = "/web"
3940

4041
slashUsageHelp = "/help"
4142
slashUsageExit = "/exit"
@@ -56,6 +57,7 @@ const (
5657
slashUsageCheckpointRestore = "/checkpoint restore <id>"
5758
slashUsageCheckpointUndo = "/checkpoint undo"
5859
slashUsageCheckpointDiff = "/checkpoint diff <id>"
60+
slashUsageWeb = "/web"
5961

6062
commandMenuTitle = "Suggestions"
6163
providerPickerTitle = "Select Provider"
@@ -155,6 +157,7 @@ var builtinSlashCommands = []slashCommand{
155157
{Usage: slashUsageProviderAdd, Description: "Add a new custom provider"},
156158
{Usage: slashUsageModel, Description: "Open the interactive model picker"},
157159
{Usage: slashUsageSession, Description: "Switch to another session"},
160+
{Usage: slashUsageWeb, Description: "Start Web UI in browser"},
158161
{Usage: slashUsageExit, Description: "Exit NeoCode"},
159162
}
160163

internal/tui/core/app/commands_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func TestBuiltinSlashCommands(t *testing.T) {
2323
foundSkills := false
2424
foundSkillUse := false
2525
foundCheckpoint := false
26+
foundWeb := false
2627
foundStatus := false
2728
for _, cmd := range builtinSlashCommands {
2829
if cmd.Usage == slashUsageHelp {
@@ -40,6 +41,9 @@ func TestBuiltinSlashCommands(t *testing.T) {
4041
if cmd.Usage == slashUsageCheckpoint {
4142
foundCheckpoint = true
4243
}
44+
if cmd.Usage == slashUsageWeb {
45+
foundWeb = true
46+
}
4347
if strings.EqualFold(cmd.Usage, "/status") {
4448
foundStatus = true
4549
}
@@ -59,6 +63,9 @@ func TestBuiltinSlashCommands(t *testing.T) {
5963
if !foundCheckpoint {
6064
t.Error("expected to find /checkpoint command")
6165
}
66+
if !foundWeb {
67+
t.Error("expected to find /web command")
68+
}
6269
if foundStatus {
6370
t.Error("did not expect /status command in builtin slash commands")
6471
}

internal/tui/core/app/update.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5780,6 +5780,8 @@ func (a *App) handleImmediateSlashCommand(input string) (bool, tea.Cmd) {
57805780
return true, a.handleSkillCommand(rest)
57815781
case slashCommandCheckpoint:
57825782
return true, a.handleCheckpointCommand(rest)
5783+
case slashCommandWeb:
5784+
return true, a.handleWebCommand(rest)
57835785
case slashCommandSession:
57845786
if err := a.ensureSessionSwitchAllowed(""); err != nil {
57855787
a.state.ExecutionError = err.Error()

internal/tui/core/app/update_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3355,6 +3355,77 @@ func TestHandleImmediateSlashCommandCompactBranches(t *testing.T) {
33553355
}
33563356
}
33573357

3358+
func TestHandleImmediateSlashCommandWebBranches(t *testing.T) {
3359+
app, _ := newTestApp(t)
3360+
3361+
handled, cmd := app.handleImmediateSlashCommand("/web now")
3362+
if !handled || cmd != nil {
3363+
t.Fatalf("expected /web with args to be handled without cmd")
3364+
}
3365+
if !strings.Contains(app.state.StatusText, "usage:") {
3366+
t.Fatalf("expected usage error for /web with args")
3367+
}
3368+
3369+
prevStart := startWebUIProcess
3370+
defer func() {
3371+
startWebUIProcess = prevStart
3372+
}()
3373+
3374+
capturedWorkdir := ""
3375+
startWebUIProcess = func(workdir string) error {
3376+
capturedWorkdir = workdir
3377+
return nil
3378+
}
3379+
3380+
app.state.CurrentWorkdir = t.TempDir()
3381+
handled, cmd = app.handleImmediateSlashCommand("/web")
3382+
if !handled || cmd == nil {
3383+
t.Fatalf("expected /web to return command")
3384+
}
3385+
3386+
msg := cmd()
3387+
result, ok := msg.(localCommandResultMsg)
3388+
if !ok {
3389+
t.Fatalf("expected localCommandResultMsg, got %T", msg)
3390+
}
3391+
if result.Err != nil {
3392+
t.Fatalf("expected nil error, got %v", result.Err)
3393+
}
3394+
if !strings.Contains(result.Notice, "Web UI startup requested") {
3395+
t.Fatalf("expected web startup notice, got %q", result.Notice)
3396+
}
3397+
if capturedWorkdir != app.state.CurrentWorkdir {
3398+
t.Fatalf("captured workdir = %q, want %q", capturedWorkdir, app.state.CurrentWorkdir)
3399+
}
3400+
}
3401+
3402+
func TestHandleImmediateSlashCommandWebStartError(t *testing.T) {
3403+
app, _ := newTestApp(t)
3404+
3405+
prevStart := startWebUIProcess
3406+
defer func() {
3407+
startWebUIProcess = prevStart
3408+
}()
3409+
startWebUIProcess = func(workdir string) error {
3410+
_ = workdir
3411+
return errors.New("web start failed")
3412+
}
3413+
3414+
handled, cmd := app.handleImmediateSlashCommand("/web")
3415+
if !handled || cmd == nil {
3416+
t.Fatalf("expected /web to return command")
3417+
}
3418+
3419+
msg := cmd()
3420+
result, ok := msg.(localCommandResultMsg)
3421+
if !ok {
3422+
t.Fatalf("expected localCommandResultMsg, got %T", msg)
3423+
}
3424+
if result.Err == nil || !strings.Contains(result.Err.Error(), "web start failed") {
3425+
t.Fatalf("expected web start error, got %v", result.Err)
3426+
}
3427+
}
3428+
33583429
func TestHandleMemoCommandsRouteToSystemTools(t *testing.T) {
33593430
app, runtime := newTestApp(t)
33603431

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package tui
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"strings"
9+
10+
tea "github.com/charmbracelet/bubbletea"
11+
12+
tuiservices "neo-code/internal/tui/services"
13+
)
14+
15+
var webUIExecutablePath = os.Executable
16+
var startWebUIProcess = defaultStartWebUIProcess
17+
18+
func (a *App) handleWebCommand(rest string) tea.Cmd {
19+
if strings.TrimSpace(rest) != "" {
20+
a.applyInlineCommandError(fmt.Sprintf("usage: %s", slashUsageWeb))
21+
return nil
22+
}
23+
24+
workdir := strings.TrimSpace(a.state.CurrentWorkdir)
25+
if workdir == "" {
26+
workdir = strings.TrimSpace(a.configManager.Get().Workdir)
27+
}
28+
29+
return tuiservices.RunLocalCommandCmd(
30+
func(_ context.Context) (string, error) {
31+
if err := startWebUIProcess(workdir); err != nil {
32+
return "", err
33+
}
34+
if workdir == "" {
35+
return "Web UI startup requested. Browser will open when server is ready.", nil
36+
}
37+
return fmt.Sprintf("Web UI startup requested for workdir: %s", workdir), nil
38+
},
39+
func(notice string, err error) tea.Msg {
40+
return localCommandResultMsg{Notice: notice, Err: err}
41+
},
42+
)
43+
}
44+
45+
func defaultStartWebUIProcess(workdir string) error {
46+
executablePath, err := webUIExecutablePath()
47+
if err != nil {
48+
return fmt.Errorf("resolve executable path: %w", err)
49+
}
50+
51+
args := []string{"web", "--open-browser=true"}
52+
if strings.TrimSpace(workdir) != "" {
53+
args = append([]string{"--workdir", strings.TrimSpace(workdir)}, args...)
54+
}
55+
56+
command := exec.Command(executablePath, args...)
57+
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
58+
if err != nil {
59+
return fmt.Errorf("prepare web process output: %w", err)
60+
}
61+
defer devNull.Close()
62+
63+
command.Stdout = devNull
64+
command.Stderr = devNull
65+
66+
if err := command.Start(); err != nil {
67+
return fmt.Errorf("start web ui process: %w", err)
68+
}
69+
return nil
70+
}

0 commit comments

Comments
 (0)