Skip to content

Commit d643318

Browse files
committed
refactor:美化/help界面
1 parent 2b9ca84 commit d643318

10 files changed

Lines changed: 239 additions & 2 deletions

File tree

internal/tui/core/app/app.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const (
3737
pickerProvider pickerMode = tuistate.PickerProvider
3838
pickerModel pickerMode = tuistate.PickerModel
3939
pickerFile pickerMode = tuistate.PickerFile
40+
pickerHelp pickerMode = tuistate.PickerHelp
4041
)
4142

4243
type RuntimeMsg = tuistate.RuntimeMsg
@@ -74,6 +75,7 @@ type appComponents struct {
7475
commandMenuMeta tuistate.CommandMenuMeta
7576
providerPicker list.Model
7677
modelPicker list.Model
78+
helpPicker list.Model
7779
fileBrowser filepicker.Model
7880
progress progress.Model
7981
transcript viewport.Model
@@ -226,6 +228,7 @@ func newApp(container tuibootstrap.Container) (App, error) {
226228
commandMenu: commandMenu,
227229
providerPicker: newSelectionPickerItems(nil),
228230
modelPicker: newSelectionPickerItems(nil),
231+
helpPicker: newHelpPickerItems(nil),
229232
fileBrowser: fileBrowser,
230233
progress: progressBar,
231234
transcript: viewport.New(0, 0),
@@ -260,6 +263,9 @@ func newApp(container tuibootstrap.Container) (App, error) {
260263
if err := app.refreshModelPicker(); err != nil {
261264
return App{}, err
262265
}
266+
if err := app.refreshHelpPicker(); err != nil {
267+
return App{}, err
268+
}
263269
app.selectCurrentProvider(cfg.SelectedProvider)
264270
app.selectCurrentModel(cfg.CurrentModel)
265271
app.modelRefreshID = cfg.SelectedProvider

internal/tui/core/app/commands.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ const (
3939
providerPickerSubtitle = "Up/Down choose, Enter confirm, Esc cancel"
4040
modelPickerTitle = "Select Model"
4141
modelPickerSubtitle = "Up/Down choose, Enter confirm, Esc cancel"
42+
helpPickerTitle = "Slash Commands"
43+
helpPickerSubtitle = "Up/Down choose, Enter run, Esc cancel"
4244
filePickerTitle = "Browse Files"
4345
filePickerSubtitle = "Navigate folders, Enter choose file, Esc cancel"
4446

@@ -69,6 +71,7 @@ const (
6971
statusCompacting = "Compacting context"
7072
statusChooseProvider = "Choose a provider"
7173
statusChooseModel = "Choose a model"
74+
statusChooseHelp = "Choose a slash command"
7275
statusBrowseFile = "Browse workspace files"
7376
statusAwaitingPermission = "Awaiting permission (y=once, a=session, n=deny)"
7477
statusPermissionApproved = "Permission approved"
@@ -123,6 +126,13 @@ func newSelectionPicker(items []list.Item) list.Model {
123126
return picker
124127
}
125128

129+
// newHelpPicker 创建 /help 专用选择器,禁用分页以保持单页展示体验。
130+
func newHelpPicker(items []list.Item) list.Model {
131+
picker := newSelectionPicker(items)
132+
picker.SetShowPagination(false)
133+
return picker
134+
}
135+
126136
func newCommandMenuModel(uiStyles styles) list.Model {
127137
delegate := commandMenuDelegate{styles: uiStyles}
128138
menu := list.New([]list.Item{}, delegate, 0, 0)
@@ -145,6 +155,15 @@ func newSelectionPickerItems(items []selectionItem) list.Model {
145155
return newSelectionPicker(listItems)
146156
}
147157

158+
// newHelpPickerItems 将 slash 命令映射为 /help 弹层列表项。
159+
func newHelpPickerItems(items []selectionItem) list.Model {
160+
listItems := make([]list.Item, 0, len(items))
161+
for _, item := range items {
162+
listItems = append(listItems, item)
163+
}
164+
return newHelpPicker(listItems)
165+
}
166+
148167
func mapProviderItems(items []config.ProviderCatalogItem) []selectionItem {
149168
mapped := make([]selectionItem, 0, len(items))
150169
for _, item := range items {
@@ -175,6 +194,13 @@ func replacePickerItems(current *list.Model, items []selectionItem) {
175194
*current = next
176195
}
177196

197+
// replaceHelpPickerItems 替换 /help 弹层条目并保持尺寸。
198+
func replaceHelpPickerItems(current *list.Model, items []selectionItem) {
199+
next := newHelpPickerItems(items)
200+
next.SetSize(current.Width(), current.Height())
201+
*current = next
202+
}
203+
178204
func (a *App) refreshProviderPicker() error {
179205
items, err := a.providerSvc.ListProviders(context.Background())
180206
if err != nil {
@@ -197,6 +223,21 @@ func (a *App) refreshModelPicker() error {
197223
return nil
198224
}
199225

226+
// refreshHelpPicker 刷新 /help 弹层中的 slash 命令列表。
227+
func (a *App) refreshHelpPicker() error {
228+
items := make([]selectionItem, 0, len(builtinSlashCommands))
229+
for _, command := range builtinSlashCommands {
230+
items = append(items, selectionItem{
231+
id: command.Usage,
232+
name: command.Usage,
233+
description: command.Description,
234+
})
235+
}
236+
replaceHelpPickerItems(&a.helpPicker, items)
237+
selectPickerItemByID(&a.helpPicker, "")
238+
return nil
239+
}
240+
200241
func (a *App) openProviderPicker() {
201242
a.openPicker(pickerProvider, statusChooseProvider, &a.providerPicker, a.state.CurrentProvider)
202243
}
@@ -205,6 +246,11 @@ func (a *App) openModelPicker() {
205246
a.openPicker(pickerModel, statusChooseModel, &a.modelPicker, a.state.CurrentModel)
206247
}
207248

249+
// openHelpPicker 打开 slash 命令帮助弹层并进入可选择状态。
250+
func (a *App) openHelpPicker() {
251+
a.openPicker(pickerHelp, statusChooseHelp, &a.helpPicker, "")
252+
}
253+
208254
func (a *App) openPicker(mode pickerMode, statusText string, picker *list.Model, selectedID string) {
209255
a.state.ActivePicker = mode
210256
a.state.StatusText = statusText

internal/tui/core/app/commands_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func TestStatusConstants(t *testing.T) {
7474
{"statusCompacting", statusCompacting},
7575
{"statusChooseProvider", statusChooseProvider},
7676
{"statusChooseModel", statusChooseModel},
77+
{"statusChooseHelp", statusChooseHelp},
7778
{"statusBrowseFile", statusBrowseFile},
7879
}
7980

@@ -296,3 +297,24 @@ func TestExecuteStatusCommandFormatting(t *testing.T) {
296297
t.Fatalf("expected Status header, got %q", output)
297298
}
298299
}
300+
301+
func TestRefreshHelpPicker(t *testing.T) {
302+
app, _ := newTestApp(t)
303+
if err := app.refreshHelpPicker(); err != nil {
304+
t.Fatalf("refreshHelpPicker() error = %v", err)
305+
}
306+
if len(app.helpPicker.Items()) != len(builtinSlashCommands) {
307+
t.Fatalf("expected %d help items, got %d", len(builtinSlashCommands), len(app.helpPicker.Items()))
308+
}
309+
}
310+
311+
func TestOpenHelpPicker(t *testing.T) {
312+
app, _ := newTestApp(t)
313+
app.openHelpPicker()
314+
if app.state.ActivePicker != pickerHelp {
315+
t.Fatalf("expected help picker to open")
316+
}
317+
if app.state.StatusText != statusChooseHelp {
318+
t.Fatalf("expected help picker status, got %q", app.state.StatusText)
319+
}
320+
}

internal/tui/core/app/update.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,15 @@ func (a App) updateInputPanel(msg tea.Msg, typed tea.KeyMsg, cmds []tea.Cmd) (te
372372
a.resetPasteHeuristics()
373373

374374
switch strings.ToLower(input) {
375+
case slashCommandHelp:
376+
if err := a.refreshHelpPicker(); err != nil {
377+
a.state.ExecutionError = err.Error()
378+
a.state.StatusText = err.Error()
379+
a.appendActivity("system", "Failed to refresh slash help", err.Error(), true)
380+
return a, tea.Batch(cmds...)
381+
}
382+
a.openHelpPicker()
383+
return a, tea.Batch(cmds...)
375384
case slashCommandProvider:
376385
if err := a.refreshProviderPicker(); err != nil {
377386
a.state.ExecutionError = err.Error()
@@ -572,6 +581,13 @@ func (a App) updatePicker(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
572581
return a, nil
573582
}
574583
return a, runModelSelection(a.providerSvc, item.id)
584+
case pickerHelp:
585+
item, ok := a.helpPicker.SelectedItem().(selectionItem)
586+
a.closePicker()
587+
if !ok {
588+
return a, nil
589+
}
590+
return a, a.runSlashCommandSelection(item.id)
575591
}
576592
}
577593

@@ -581,6 +597,8 @@ func (a App) updatePicker(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
581597
a.providerPicker, cmd = a.providerPicker.Update(msg)
582598
case pickerModel:
583599
a.modelPicker, cmd = a.modelPicker.Update(msg)
600+
case pickerHelp:
601+
a.helpPicker, cmd = a.helpPicker.Update(msg)
584602
case pickerFile:
585603
a.fileBrowser, cmd = a.fileBrowser.Update(msg)
586604
if didSelect, path := a.fileBrowser.DidSelectFile(msg); didSelect {
@@ -1455,6 +1473,12 @@ func (a *App) applyComponentLayout(rebuildTranscript bool) {
14551473

14561474
a.providerPicker.SetSize(max(24, tuiutils.Clamp(lay.rightWidth-14, 28, 52)), max(4, tuiutils.Clamp(lay.rightHeight-10, 6, 10)))
14571475
a.modelPicker.SetSize(max(24, tuiutils.Clamp(lay.rightWidth-14, 28, 52)), max(4, tuiutils.Clamp(lay.rightHeight-10, 6, 10)))
1476+
helpPickerMaxHeight := max(8, lay.rightHeight-6)
1477+
helpPickerDesiredHeight := (len(a.helpPicker.Items()) * 3) + 1
1478+
a.helpPicker.SetSize(
1479+
max(24, tuiutils.Clamp(lay.rightWidth-14, 28, 52)),
1480+
max(6, tuiutils.Clamp(helpPickerDesiredHeight, 6, helpPickerMaxHeight)),
1481+
)
14581482
a.fileBrowser.SetHeight(max(6, tuiutils.Clamp(lay.rightHeight-8, 8, 16)))
14591483
if rebuildTranscript || prevTranscriptWidth != a.transcript.Width {
14601484
a.rebuildTranscript()
@@ -1602,6 +1626,55 @@ func (a *App) handleImmediateSlashCommand(input string) (bool, tea.Cmd) {
16021626
}
16031627
}
16041628

1629+
// runSlashCommandSelection 根据 /help 弹层选中的命令执行对应 slash 行为。
1630+
func (a *App) runSlashCommandSelection(command string) tea.Cmd {
1631+
command = strings.ToLower(strings.TrimSpace(command))
1632+
if command == "" {
1633+
return nil
1634+
}
1635+
1636+
if handled, cmd := a.handleImmediateSlashCommand(command); handled {
1637+
return cmd
1638+
}
1639+
1640+
switch command {
1641+
case slashCommandHelp:
1642+
if err := a.refreshHelpPicker(); err != nil {
1643+
a.state.ExecutionError = err.Error()
1644+
a.state.StatusText = err.Error()
1645+
a.appendActivity("system", "Failed to refresh slash help", err.Error(), true)
1646+
return nil
1647+
}
1648+
a.openHelpPicker()
1649+
return nil
1650+
case slashCommandProvider:
1651+
if err := a.refreshProviderPicker(); err != nil {
1652+
a.state.ExecutionError = err.Error()
1653+
a.state.StatusText = err.Error()
1654+
a.appendActivity("system", "Failed to refresh providers", err.Error(), true)
1655+
return nil
1656+
}
1657+
a.openProviderPicker()
1658+
return nil
1659+
case slashCommandModelPick:
1660+
if err := a.refreshModelPicker(); err != nil {
1661+
a.state.ExecutionError = err.Error()
1662+
a.state.StatusText = err.Error()
1663+
a.appendActivity("system", "Failed to refresh models", err.Error(), true)
1664+
return nil
1665+
}
1666+
a.openModelPicker()
1667+
return a.requestModelCatalogRefresh(a.state.CurrentProvider)
1668+
default:
1669+
a.state.StatusText = statusApplyingCommand
1670+
a.state.ExecutionError = ""
1671+
if isWorkspaceSlashCommand(command) {
1672+
return runSessionWorkdirCommand(a.runtime, a.state.ActiveSessionID, a.state.CurrentWorkdir, command)
1673+
}
1674+
return runLocalCommand(a.configManager, a.providerSvc, a.currentStatusSnapshot(), command)
1675+
}
1676+
}
1677+
16051678
func (a App) currentStatusSnapshot() tuistatus.Snapshot {
16061679
return tuistatus.BuildFromUIState(
16071680
a.state,

internal/tui/core/app/update_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,3 +1064,77 @@ func TestHandleViewportKeys(t *testing.T) {
10641064
app.handleViewportKeys(&app.transcript, tea.KeyMsg{Type: tea.KeyDown})
10651065
app.handleViewportKeys(&app.transcript, tea.KeyMsg{Type: tea.KeyUp})
10661066
}
1067+
1068+
func TestUpdateEnterHelpOpensHelpPicker(t *testing.T) {
1069+
app, _ := newTestApp(t)
1070+
app.input.SetValue("/help")
1071+
app.state.InputText = "/help"
1072+
1073+
model, cmd := app.Update(tea.KeyMsg{Type: tea.KeyEnter})
1074+
if model == nil {
1075+
t.Fatalf("expected non-nil model")
1076+
}
1077+
app = model.(App)
1078+
if cmd != nil {
1079+
t.Fatalf("expected no async cmd when opening help picker")
1080+
}
1081+
if app.state.ActivePicker != pickerHelp {
1082+
t.Fatalf("expected help picker to be active")
1083+
}
1084+
if app.state.StatusText != statusChooseHelp {
1085+
t.Fatalf("expected status %q, got %q", statusChooseHelp, app.state.StatusText)
1086+
}
1087+
if len(app.helpPicker.Items()) != len(builtinSlashCommands) {
1088+
t.Fatalf("expected %d help options, got %d", len(builtinSlashCommands), len(app.helpPicker.Items()))
1089+
}
1090+
}
1091+
1092+
func TestUpdatePickerHelpSelectionOpensModelPicker(t *testing.T) {
1093+
app, _ := newTestApp(t)
1094+
if err := app.refreshHelpPicker(); err != nil {
1095+
t.Fatalf("refreshHelpPicker() error = %v", err)
1096+
}
1097+
app.openHelpPicker()
1098+
selectPickerItemByID(&app.helpPicker, slashCommandModelPick)
1099+
1100+
model, cmd := app.updatePicker(tea.KeyMsg{Type: tea.KeyEnter})
1101+
if model == nil {
1102+
t.Fatalf("expected model")
1103+
}
1104+
app = model.(App)
1105+
if cmd != nil {
1106+
_ = cmd()
1107+
}
1108+
if app.state.ActivePicker != pickerModel {
1109+
t.Fatalf("expected model picker to open from help selection")
1110+
}
1111+
}
1112+
1113+
func TestUpdatePickerHelpSelectionRunsSlashCommand(t *testing.T) {
1114+
app, _ := newTestApp(t)
1115+
if err := app.refreshHelpPicker(); err != nil {
1116+
t.Fatalf("refreshHelpPicker() error = %v", err)
1117+
}
1118+
app.openHelpPicker()
1119+
selectPickerItemByID(&app.helpPicker, slashCommandStatus)
1120+
1121+
model, cmd := app.updatePicker(tea.KeyMsg{Type: tea.KeyEnter})
1122+
if model == nil {
1123+
t.Fatalf("expected model")
1124+
}
1125+
app = model.(App)
1126+
if app.state.ActivePicker != pickerNone {
1127+
t.Fatalf("expected help picker to close after selecting /status")
1128+
}
1129+
if cmd == nil {
1130+
t.Fatalf("expected local slash command cmd")
1131+
}
1132+
msg := cmd()
1133+
result, ok := msg.(localCommandResultMsg)
1134+
if !ok {
1135+
t.Fatalf("expected localCommandResultMsg, got %T", msg)
1136+
}
1137+
if !strings.Contains(result.Notice, "Status:") {
1138+
t.Fatalf("expected status output in slash result, got %q", result.Notice)
1139+
}
1140+
}

internal/tui/core/app/view.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ func (a App) renderPicker(width int, height int) string {
179179
subtitle = filePickerSubtitle
180180
body = a.fileBrowser.View()
181181
}
182+
if a.state.ActivePicker == pickerHelp {
183+
title = helpPickerTitle
184+
subtitle = helpPickerSubtitle
185+
body = a.helpPicker.View()
186+
}
182187
content := lipgloss.JoinVertical(
183188
lipgloss.Left,
184189
a.styles.panelTitle.Render(title),

internal/tui/core/utils/view_helpers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ func PickerLabelFromMode(mode tuistate.PickerMode) string {
1515
return "model"
1616
case tuistate.PickerFile:
1717
return "file"
18+
case tuistate.PickerHelp:
19+
return "help"
1820
default:
1921
return "none"
2022
}

internal/tui/core/utils/view_helpers_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func TestPickerLabelFromMode(t *testing.T) {
1414
{tuistate.PickerProvider, "provider"},
1515
{tuistate.PickerModel, "model"},
1616
{tuistate.PickerFile, "file"},
17+
{tuistate.PickerHelp, "help"},
1718
{tuistate.PickerMode(999), "none"},
1819
}
1920

internal/tui/state/state_test.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@ func TestPanelAndPickerConstants(t *testing.T) {
66
if PanelSessions != 0 || PanelTranscript != 1 || PanelActivity != 2 || PanelInput != 3 {
77
t.Fatalf("unexpected panel constants: %d %d %d %d", PanelSessions, PanelTranscript, PanelActivity, PanelInput)
88
}
9-
if PickerNone != 0 || PickerProvider != 1 || PickerModel != 2 || PickerFile != 3 {
10-
t.Fatalf("unexpected picker constants: %d %d %d %d", PickerNone, PickerProvider, PickerModel, PickerFile)
9+
if PickerNone != 0 || PickerProvider != 1 || PickerModel != 2 || PickerFile != 3 || PickerHelp != 4 {
10+
t.Fatalf(
11+
"unexpected picker constants: %d %d %d %d %d",
12+
PickerNone,
13+
PickerProvider,
14+
PickerModel,
15+
PickerFile,
16+
PickerHelp,
17+
)
1118
}
1219
}
1320

0 commit comments

Comments
 (0)