Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 32 additions & 0 deletions packages/app/component-tests/new-session-wordmark.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,38 @@ story("shows a static pin board with reduced motion", async ({ mount, page }) =>
expect(await requestedFrames(page)).toBe(0)
})

story("the lodger pops the pin board out and packs it back into the e", async ({ mount, page }) => {
await page.emulateMedia({ reducedMotion: "no-preference" })
await mount("app-new-session-wordmark--flat")
const grip = page.locator('[data-slot="wordmark-lodger-grip"]')
const flat = page.locator('[data-slot="wordmark-flat"]')
const board = page.locator("canvas")
await expect(board).toHaveCount(0)

// Pulling the lodger well past its slip distance pops every pin out into the board.
const hole = (await grip.boundingBox())!
await page.mouse.move(hole.x + hole.width / 2, hole.y + hole.height / 2)
await page.mouse.down()
await page.mouse.move(hole.x + hole.width / 2 + 160, hole.y + hole.height / 2 + 80, { steps: 12 })
await page.mouse.up()
await expect(flat).toBeHidden()
await expect(board).not.toHaveCount(0)
await expect.poll(() => requestedFrames(page)).toBe(0)

// Carrying it from its perch back into the "e" reels the pins in and tucks the lodger away.
const lodger = (await page.locator('[data-slot="wordmark-lodger"]').boundingBox())!
const unit = lodger.width / 234
const perch = (await grip.boundingBox())!
await page.mouse.move(perch.x + perch.width / 2, perch.y + perch.height / 2)
await page.mouse.down()
await page.mouse.move(perch.x + perch.width / 2 - 24 * unit, perch.y + perch.height / 2 - 16 * unit, { steps: 12 })
await page.mouse.up()
await expect(board).toHaveCount(0)
await expect(flat).toBeVisible()
await expect.poll(() => requestedFrames(page)).toBe(0)
await expect(grip).toBeVisible()
})

// Reads the on-screen pixel at a grid cell. The board may be drawn with WebGL, whose canvas cannot be read
// back once presented, so the pixel comes from a screenshot decoded in the page.
async function pinPixel(page: Page, cell = PIN) {
Expand Down
68 changes: 32 additions & 36 deletions packages/app/src/new-session/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Icon } from "@opencode/ui/icon"
import { Slash } from "@opencode/ui/slash"
import { Keybind } from "@opencode/ui/keybind"
import { useCommand } from "@/shell/commands/command"
import { Show, createMemo, createSignal, type JSX } from "solid-js"
import { Show, createMemo, createSignal, untrack, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { getFilename } from "@opencode/util/path"
Expand Down Expand Up @@ -36,8 +36,15 @@ import { useSettings } from "@/settings/model"
import type { NewSessionWorkspaceController } from "./workspace/controller"
import { NewSessionBranch } from "./workspace/selector"
import { NewSessionBrickBreaker } from "./brick-breaker"
import { NewSessionPinBoard, type PinBoardHandle } from "./pin-board"
import { NewSessionWordmark, type LodgerBall } from "./wordmark"
import { NewSessionPinBoard } from "./pin-board"
import { NewSessionWordmark } from "./wordmark"
import {
createWordmarkState,
selectBrickBreaker,
selectPinBoard,
transitionWordmark,
type WordmarkEvent,
} from "./wordmark-machine"

const providerTipDismissalDuration = 30 * 24 * 60 * 60 * 1000

Expand All @@ -49,17 +56,6 @@ export const ProviderTipSchema = Persistence.struct({
dismissedAt: Schema.Finite,
})

type WordmarkState = {
box?: HTMLDivElement
mode: "flat" | "open" | "closing"
game: boolean
}

type BrickBreakerHandles = {
board?: PinBoardHandle
ball?: LodgerBall
}

export function NewSessionView(props: {
composer: ComposerModel
project: PromptProjectController
Expand All @@ -74,7 +70,16 @@ export function NewSessionView(props: {

const settings = useSettings()

const [wordmark, setWordmark] = createStore<WordmarkState>({ mode: "flat", game: false })
const [wordmark, setWordmark] = createSignal(createWordmarkState(false))
const [logo, setLogo] = createSignal<HTMLDivElement>()

const dispatchWordmark = (event: WordmarkEvent) =>
untrack(() => {
const next = transitionWordmark(wordmark(), event)
setWordmark(next.state)

if (next.effects.some((effect) => effect.type === "focus.composer")) props.composer.restoreFocus()
})

const location = (project?: PromptProject) =>
props.location({
Expand All @@ -84,8 +89,6 @@ export function NewSessionView(props: {
},
})

// Imperative handles for the brick breaker; they are not reactive state.
const handles: BrickBreakerHandles = {}
let panel!: HTMLDivElement

return (
Expand All @@ -102,15 +105,11 @@ export function NewSessionView(props: {
<div class="absolute inset-x-0 top-[25.375%] flex justify-center px-6">
<div class={NEW_SESSION_CONTENT_WIDTH}>
<NewSessionWordmark
ref={(box) => setWordmark("box", box)}
ref={setLogo}
state={wordmark}
onEvent={dispatchWordmark}
play
sound={settings.sounds.interfaceEnabled}
onOpen={() => setWordmark("mode", "open")}
onClose={() => setWordmark("mode", "closing")}
onFlat={() => setWordmark("mode", "flat")}
onPlay={(ball) => {
handles.ball = ball
setWordmark("game", true)
}}
/>
<div class="mt-8 flex flex-col gap-3">
<div data-slot="new-session-composer" class="flex flex-col gap-1">
Expand All @@ -137,28 +136,25 @@ export function NewSessionView(props: {
</div>
</div>
</div>
<Show when={wordmark.mode !== "flat" && wordmark.box}>
{(logo) => (
<Show when={selectPinBoard(wordmark(), logo())}>
{(board) => (
<NewSessionPinBoard
logo={logo()}
logo={board().logo}
sound={settings.sounds.interfaceEnabled}
entrance="chain"
closing={wordmark.mode === "closing"}
ref={(board) => (handles.board = board)}
entrance={board().entrance}
closing={board().closing}
ref={(handle) => dispatchWordmark({ type: "board.mounted", board: handle })}
/>
)}
</Show>
<Show when={wordmark.game && handles.board && handles.ball && { board: handles.board, ball: handles.ball }}>
<Show when={selectBrickBreaker(wordmark())}>
{(game) => (
<NewSessionBrickBreaker
panel={panel}
board={game().board}
ball={game().ball}
sound={settings.sounds.interfaceEnabled}
onDone={() => {
setWordmark("game", false)
props.composer.restoreFocus()
}}
onDone={() => dispatchWordmark({ type: "game.done" })}
/>
)}
</Show>
Expand Down
Loading
Loading