Skip to content

Commit 7bcd040

Browse files
authored
fix(app): advance create settings on enter (#329)
1 parent d9846c3 commit 7bcd040

7 files changed

Lines changed: 109 additions & 27 deletions

File tree

packages/app/src/docker-git/menu-create-shared.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,29 @@ export const applyCreateDisplaySettingsStep = (
898898
(nextValues) => continueCreateDisplayFlow(view, nextValues)
899899
))
900900

901+
/**
902+
* Applies one browser Create settings display row and advances selection downward.
903+
*
904+
* @pure true
905+
* @effect none
906+
* @invariant successful result preserves applied values and moves over the finite display row cycle
907+
* @precondition view.step points at a settings display row
908+
* @postcondition result._tag = "Continue" -> result.view.buffer = ""
909+
* @complexity O(1)
910+
*/
911+
export const advanceCreateDisplaySettingsStep = (
912+
contextOrCwd: string | CreateFlowContext,
913+
view: DisplayModeFlowView
914+
): AdvanceCreateFlowResult | null => {
915+
const applied = applyCreateDisplaySettingsStep(contextOrCwd, view)
916+
if (applied === null || applied._tag !== "Continue" || !isDisplayModeFlowView(applied.view)) {
917+
return applied
918+
}
919+
920+
const movedView = moveCreateDisplaySettingsStep(applied.view, "down")
921+
return movedView === null ? applied : { ...applied, view: movedView }
922+
}
923+
901924
/**
902925
* Completes browser Create settings by applying a non-empty active buffer first.
903926
*

packages/app/src/web/app-ready-create.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { type Dispatch, type SetStateAction, useEffect } from "react"
33
import { formatParseError } from "../docker-git/cli/usage.js"
44
import { nextBufferValue } from "../docker-git/menu-buffer-input.js"
55
import {
6+
advanceCreateDisplaySettingsStep,
67
advanceCreateFlow,
7-
applyCreateDisplaySettingsStep,
88
completeCreateDisplaySettingsFlow,
99
createDisplayFlowView,
1010
type CreateFlowView,
@@ -75,7 +75,7 @@ const resolveCreateSubmitResult = (
7575
): ReturnType<typeof advanceCreateFlow> => {
7676
if (isDisplayModeFlowView(createView)) {
7777
return mode === "advance"
78-
? applyCreateDisplaySettingsStep(createContext, createView)
78+
? advanceCreateDisplaySettingsStep(createContext, createView)
7979
: completeCreateDisplaySettingsFlow(createContext, createView)
8080
}
8181
const next = advanceCreateFlow(createContext, createView, { quickCreate: mode === "quick-create" })

packages/app/src/web/panel-create-select.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
type CreateFlowContext,
55
type CreateFlowView,
66
type CreateSettingsChoiceDirection,
7-
createSettingsHint,
87
isCreateFlowRepoStep,
98
isDisplayModeFlowView,
109
renderCreateStepLabel,
@@ -20,6 +19,7 @@ import type { CreateSubmitMode } from "./app-ready-create.js"
2019

2120
const renderStepColor = (active: boolean): string => active ? "#56f39a" : "#8fa6c4"
2221

22+
const webCreateSettingsNavigationHint = "↑ - up, ↓ - down, Enter - apply + down"
2323
const webCreateSettingsChoiceHint = "←/→ - choose yes/no or GPU"
2424

2525
const createPrompt = (
@@ -225,7 +225,7 @@ const CreateHintBlock = (
225225
<HelpLines
226226
lines={[
227227
...(isRepoStep ? ["Repo URL or URL + CLI flags."] : []),
228-
...(isRepoStep ? [] : [createSettingsHint]),
228+
...(isRepoStep ? [] : [webCreateSettingsNavigationHint]),
229229
...(isRepoStep ? [] : [webCreateSettingsChoiceHint]),
230230
...(compact && isRepoStep ? ["↑/↓ = menu, ←/→ = project"] : []),
231231
`Current cwd: ${controllerCwd}`

packages/app/tests/docker-git/app-ready-create-fixture.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { expect, vi } from "vitest"
33

44
import {
55
type CreateFlowView,
6-
type DisplayModeFlowView,
76
createInitialFlowView,
7+
type DisplayModeFlowView,
88
resolveCreateDisplaySteps
99
} from "../../src/docker-git/menu-create-shared.js"
1010
import type { CreateInputs, CreateStep } from "../../src/docker-git/menu-types.js"

packages/app/tests/docker-git/app-ready-create-settings.test.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,12 @@ describe("app-ready-create settings", () => {
7575
expect(arrowView.values.gpu).toBeUndefined()
7676
expect(enterResult.handled).toBe(true)
7777
expect(enteredView.values.gpu).toBe("all")
78-
expect(enteredView.step).toBe(resolveCreateDisplaySteps().indexOf("gpu"))
78+
expect(enteredView.step).toBe(resolveCreateDisplaySteps().indexOf("runUp"))
7979
expect(enteredView.buffer).toBe("")
8080
expect(submitCreateInputsMock).not.toHaveBeenCalled()
8181
})
8282

83-
it("keeps an applied settings row selected and visible instead of submitting", () => {
83+
it("wraps to the first settings row after applying the last settings row", () => {
8484
const createView: CreateFlowView = {
8585
...createSettingsFlowViewAtStep("force", "y"),
8686
values: {
@@ -97,21 +97,36 @@ describe("app-ready-create settings", () => {
9797

9898
expect(handled).toBe(true)
9999
expect(enteredView.values.force).toBe(true)
100-
expect(enteredView.step).toBe(resolveCreateDisplaySteps().indexOf("force"))
100+
expect(enteredView.step).toBe(resolveCreateDisplaySteps().indexOf("cpuLimit"))
101101
expect(enteredView.buffer).toBe("")
102102
expect(submitCreateInputsMock).not.toHaveBeenCalled()
103103
})
104104

105+
it("keeps the previous setting value when Enter applies an empty buffer", () => {
106+
const createView: CreateFlowView = {
107+
...createSettingsFlowViewAtStep("runUp", ""),
108+
values: {
109+
...createSettingsFlowView().values,
110+
runUp: false
111+
}
112+
}
113+
const emptyResult = runCreateKey(handleCreateKey, createView, "Enter")
114+
const emptyView = requireCreateViewValue(emptyResult.setCreateViewSpy.mock.calls[0]?.[0])
115+
116+
expect(emptyResult.handled).toBe(true)
117+
expect(emptyView.step).toBe(resolveCreateDisplaySteps().indexOf("mcpPlaywright"))
118+
expect(emptyView.values.runUp).toBe(false)
119+
expect(emptyView.buffer).toBe("")
120+
})
121+
105122
it("navigates to the next visible row after applying a settings row", () => {
106123
const enterResult = runCreateKey(handleCreateKey, createSettingsFlowViewAtStep("mcpPlaywright", "y"), "Enter")
107124
const enteredView = requireCreateViewValue(enterResult.setCreateViewSpy.mock.calls[0]?.[0])
108-
const downResult = runCreateKey(handleCreateKey, enteredView, "ArrowDown")
109-
const downView = requireCreateViewValue(downResult.setCreateViewSpy.mock.calls[0]?.[0])
110125

111-
expect(downResult.handled).toBe(true)
112-
expect(downView.step).toBe(resolveCreateDisplaySteps().indexOf("force"))
113-
expect(downView.values.enableMcpPlaywright).toBe(true)
114-
expect(downView.buffer).toBe("")
126+
expect(enterResult.handled).toBe(true)
127+
expect(enteredView.step).toBe(resolveCreateDisplaySteps().indexOf("force"))
128+
expect(enteredView.values.enableMcpPlaywright).toBe(true)
129+
expect(enteredView.buffer).toBe("")
115130
})
116131

117132
it("clears an unconfirmed preview when navigating away from a settings row", () => {

packages/app/tests/docker-git/create-flow-render.test.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {
77
type CreateFlowContext,
88
type CreateFlowView,
99
createInitialFlowView,
10-
createSettingsHint,
1110
type CreateModeFlowView,
11+
createSettingsHint,
1212
type DisplayModeFlowView,
1313
renderCreateStepLabel,
1414
resolveCreateDisplaySteps,
@@ -34,6 +34,7 @@ const createContext: CreateFlowContext = {
3434
const renderWithUi = (element: ReactElement): string =>
3535
renderToStaticMarkup(createElement(UiProvider, { primitives: webPrimitives }, element))
3636

37+
const webCreateSettingsNavigationHint = "↑ - up, ↓ - down, Enter - apply + down"
3738
const webCreateSettingsChoiceHint = "←/→ - choose yes/no or GPU"
3839
const createSettingsView = (): DisplayModeFlowView => createFeatureRepoDisplaySettingsView(createContext)
3940

@@ -54,6 +55,7 @@ const renderCreatePanel = (
5455
const activeStepMarker = "&gt; "
5556

5657
const countActiveStepMarkers = (html: string): number => html.split(activeStepMarker).length - 1
58+
const renderedHelpLine = (line: string): string => `>${line}</div>`
5759

5860
const renderStepLabels = (createView: CreateFlowView): ReadonlyArray<string> => {
5961
const defaults = resolveCreateInputs(createContext, createView.values)
@@ -204,10 +206,18 @@ describe("Create flow rendering", () => {
204206
})
205207

206208
it("renders the settings navigation hint only after leaving the repo URL step", () => {
207-
expect(renderCreatePanel(createInitialFlowView(featureCreateRepoUrl))).not.toContain(createSettingsHint)
208-
expect(renderCreatePanel(createInitialFlowView(featureCreateRepoUrl))).not.toContain(webCreateSettingsChoiceHint)
209-
expect(renderCreatePanel(createSettingsView())).toContain(createSettingsHint)
210-
expect(renderCreatePanel(createSettingsView())).toContain(webCreateSettingsChoiceHint)
209+
expect(renderCreatePanel(createInitialFlowView(featureCreateRepoUrl))).not.toContain(
210+
renderedHelpLine(createSettingsHint)
211+
)
212+
expect(renderCreatePanel(createInitialFlowView(featureCreateRepoUrl))).not.toContain(
213+
renderedHelpLine(webCreateSettingsNavigationHint)
214+
)
215+
expect(renderCreatePanel(createInitialFlowView(featureCreateRepoUrl))).not.toContain(
216+
renderedHelpLine(webCreateSettingsChoiceHint)
217+
)
218+
expect(renderCreatePanel(createSettingsView())).not.toContain(renderedHelpLine(createSettingsHint))
219+
expect(renderCreatePanel(createSettingsView())).toContain(renderedHelpLine(webCreateSettingsNavigationHint))
220+
expect(renderCreatePanel(createSettingsView())).toContain(renderedHelpLine(webCreateSettingsChoiceHint))
211221
})
212222

213223
it("renders terminal Create hints with the same repo/settings split", () => {
@@ -216,8 +226,9 @@ describe("Create flow rendering", () => {
216226

217227
expect(repoHtml).not.toContain("Enter = next, Esc = cancel.")
218228
expect(repoHtml).not.toContain("Shift+Enter")
219-
expect(settingsHtml).toContain(createSettingsHint)
220-
expect(settingsHtml).not.toContain(webCreateSettingsChoiceHint)
229+
expect(settingsHtml).toContain(renderedHelpLine(createSettingsHint))
230+
expect(settingsHtml).not.toContain(renderedHelpLine(webCreateSettingsNavigationHint))
231+
expect(settingsHtml).not.toContain(renderedHelpLine(webCreateSettingsChoiceHint))
221232
})
222233

223234
it("preserves hint visibility invariants for every Create step", () => {
@@ -231,10 +242,12 @@ describe("Create flow rendering", () => {
231242
const panelHtml = renderCreatePanel(view)
232243
const compactPanelHtml = renderCreatePanel(view, { compact: true })
233244

234-
expect(panelHtml.includes(createSettingsHint)).toBe(isSettings)
235-
expect(compactPanelHtml.includes(createSettingsHint)).toBe(isSettings)
236-
expect(panelHtml.includes(webCreateSettingsChoiceHint)).toBe(isSettings)
237-
expect(compactPanelHtml.includes(webCreateSettingsChoiceHint)).toBe(isSettings)
245+
expect(panelHtml).not.toContain(renderedHelpLine(createSettingsHint))
246+
expect(compactPanelHtml).not.toContain(renderedHelpLine(createSettingsHint))
247+
expect(panelHtml.includes(renderedHelpLine(webCreateSettingsNavigationHint))).toBe(isSettings)
248+
expect(compactPanelHtml.includes(renderedHelpLine(webCreateSettingsNavigationHint))).toBe(isSettings)
249+
expect(panelHtml.includes(renderedHelpLine(webCreateSettingsChoiceHint))).toBe(isSettings)
250+
expect(compactPanelHtml.includes(renderedHelpLine(webCreateSettingsChoiceHint))).toBe(isSettings)
238251
expect(panelHtml).not.toContain("Enter = next, Esc = cancel.")
239252
expect(compactPanelHtml).not.toContain("Enter = next, Esc = cancel.")
240253
expect(panelHtml).not.toContain("Shift+Enter")
@@ -250,8 +263,9 @@ describe("Create flow rendering", () => {
250263
const view = step === 0 ? createInitialFlowView(featureCreateRepoUrl) : { ...terminalSettingsView, step }
251264
const terminalHtml = renderTerminalCreate(view)
252265

253-
expect(terminalHtml.includes(createSettingsHint)).toBe(step > 0)
254-
expect(terminalHtml).not.toContain(webCreateSettingsChoiceHint)
266+
expect(terminalHtml.includes(renderedHelpLine(createSettingsHint))).toBe(step > 0)
267+
expect(terminalHtml).not.toContain(renderedHelpLine(webCreateSettingsNavigationHint))
268+
expect(terminalHtml).not.toContain(renderedHelpLine(webCreateSettingsChoiceHint))
255269
expect(terminalHtml).not.toContain("Enter = next, Esc = cancel.")
256270
expect(terminalHtml).not.toContain("Shift+Enter")
257271
})

packages/app/tests/docker-git/menu-create-display-settings.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as fc from "fast-check"
22
import { describe, expect, it } from "vitest"
33

44
import {
5+
advanceCreateDisplaySettingsStep,
56
applyCreateDisplaySettingsStep,
67
completeCreateDisplaySettingsFlow,
78
type DisplayModeFlowView,
@@ -82,6 +83,35 @@ describe("menu-create-shared display settings", () => {
8283
expect(resolveCreateDisplaySteps()[next.step]).toBe("mcpPlaywright")
8384
})
8485

86+
it("applies a browser display setting and advances to the next row", () => {
87+
const next = expectDisplayModeView(expectCreateContinueView(advanceCreateDisplaySettingsStep(
88+
cwd,
89+
{ ...createFlowViewAtStep(createFeatureRepoDisplaySettingsView(cwd), "mcpPlaywright"), buffer: "y" }
90+
)))
91+
92+
expect(next.step).toBe(resolveCreateDisplaySteps().indexOf("force"))
93+
expect(next.buffer).toBe("")
94+
expect(next.values.enableMcpPlaywright).toBe(true)
95+
})
96+
97+
it("preserves an existing setting value on empty Enter and wraps after the last row", () => {
98+
const view = createFlowViewAtStep(createFeatureRepoDisplaySettingsView(cwd), "force", "")
99+
const next = expectDisplayModeView(expectCreateContinueView(advanceCreateDisplaySettingsStep(
100+
cwd,
101+
{
102+
...view,
103+
values: {
104+
...view.values,
105+
force: true
106+
}
107+
}
108+
)))
109+
110+
expect(next.step).toBe(resolveCreateDisplaySteps().indexOf("cpuLimit"))
111+
expect(next.buffer).toBe("")
112+
expect(next.values.force).toBe(true)
113+
})
114+
85115
it("navigates browser display settings without skipping applied rows", () => {
86116
const view = createFeatureRepoDisplaySettingsView(cwd)
87117
const applied = expectDisplayModeView(expectCreateContinueView(applyCreateDisplaySettingsStep(

0 commit comments

Comments
 (0)