SLOPSHOPPER

cosmic-shore-deck

Cosmic Shore session deck: buttons for team activity, the C# gates, the reference compile, branch sync and ship, plus a branch status line.

newpanebandcommandtoaststatus
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cosmic-shore-deck
│ ┃ Cosmic Shore ✕ › fix the failing auth test and add an audit log call │ ┃ [ Team ] [ Gates ] [ Compile ] [ Sync ] [ Sh │ ┃ ⏺ Read(src/auth.ts) │ ┃ feat/auth-refresh ↑0 ↓0 · 2 changed ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ Sync just now ⎿ 3 pass, 1 fail │ ┃ Branch feat/auth-refresh: 0 to push, 0 to pu │ ┃ 2 changed file(s) not committed. ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ origin/bleeding-edge has 0 commit(s) this br │ ┃ ✻ Worked for 42s · done 4:20 PM │ ┃ │ › /deck │ ⎿ cosmic-shore-deck: Cosmic Shore deck opened. │ │ Deck Open Team Gates Sync ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ cosmic-shore-deck: feat/auth-refresh ↑0 ↓0 · 2 changed

Draws

Band
Deck Open Team Gates Sync
Pane · Cosmic Shore
[ Team ] [ Gates ] [ Compile ] [ Sync ] [ Ship ] feat/auth-refresh ↑0 ↓0 · 2 changed Sync just now Branch feat/auth-refresh: 0 to push, 0 to pull from origin/f 2 changed file(s) not committed. origin/bleeding-edge has 0 commit(s) this branch does not.
README

Cosmic Shore

Cosmic Shore Logo

This is the source code repository for Cosmic Shore, a multigenre space game — "the party game for pilots" — developed by Froglet Inc., a Delaware C-corp based in Grand Rapids, MI.

Different vessel classes embody gameplay from different genres to connect players across demographics: from casual, hotseat minigames and rewarded dailies to team missions and a structured esport. It's your game and you can play it your way.

Please don't forget to play and give feedback!

Google Play App Store

Also don't forget to join our community!

Discord Reddit


Vessel Classes

Each vessel embodies a different genre of gameplay:

VesselGenreDescription
SquirrelRacing / DriftVaporwave arcade racer — tube-riding along player-generated trails (F-Zero / Redout feel)
SparrowShooterArcade space combat with guns and missiles
Manta—Feature-complete playable vessel
Dolphin—Feature-complete playable vessel
Rhino—Feature-complete playable vessel
Serpent—Playable vessel
Urchin—Playable vessel (AI in development)
Grizzly—Playable vessel (AI in development)
Termite—In development
Falcon—In development
Shrike—In development

Gameplay

Create

Create

Every HyperSea-worthy vessel is capable of enhancing its colored mass output. Pacifying or controlling biomes is only possible if your color is the majority of mass in the biome. Producing mass is the fundamental way to win a majority and refuel along your journey.

Disrupt

Disrupt

Every vessel is also equipped with the ability to destroy, steal, shrink or otherwise disrupt other colors of mass. Filling populated biomes with additional mass can make them more hostile. Sometimes reducing hostile mass is your only option.

Maneuver

Maneuver

Maneuvering through the HyperSea is usually a thrilling delight. Knowing where and when to create your own or disrupt enemy mass is pivotal to victory. Each vessel has its own unique abilities to get where they need to be or otherwise extend their reach.


Game Modes

  • Get the Crystal (2-4 players) — Inspired by games like Ultimate Chicken Horse and Jenga, players progressively increase the challenge until a winner remains.
  • Dolphin Darts (1-2 players) — Accurately drift into crystals to blow up more of the dartboards than your opponent.
  • Ransack Rally (1-4 players) — Skim past trails on your way to a biome. Whoever steals more along the way wins.
  • Freestyle Toybox (1 player) — No rules, time, or score. Do what you want for as long as you like.
  • Duel for the Cell (1v1) — Create mass as trail blocks and disrupt your opponent's. Greatest mass volume wins.
  • SkimRace (1-4 players) — Crystal-collection racing on procedurally generated tracks. First player to collect all crystals wins. Supports solo with AI opponents, multiplayer with friends, or mixed human+AI lobbies. Intensity scales track complexity (segment count, helix radius). Features elemental comeback mechanics and server-authoritative scoring with golf rules (lower time = better rank).
  • Wildlife Blitz (1-4 players) — Co-op and competitive variants.
  • Joust (multiplayer) — Head-to-head combat mode.

Missions

Missions

Couriers, refugees, colonists, and more all need safe passage across the galaxy. Navigating the HyperSea is the only practical way to turn 100 light years into a five minute journey. Join the Cosmic Shore's roster of elite guides, leading and protecting the galaxy's travelers as they brave the hazards of the HyperSea.

Sport

Sport

A stepping stone to our future dreams of a multi-biome esport, Duel for the Cell provides replayability with different teammates, opponents, and difficulties in every match, all of which can be enjoyed in a variety of environments with their own unique flora, fauna, and effects. Duel for the Cell is a 1v1 match with every player creating mass in the form of trail blocks, and disrupting mass by shielding, stealing or otherwise affecting them. The main objective is to have the greatest mass volume by the end of the match. This adds a third dimension to the classic strategy game focus on area control.


Tech Stack

  • Engine: Unity 6+ with URP (Universal Render Pipeline)
  • Language: C#
  • Architecture: ScriptableObject-driven configuration + SOAP (Scriptable Object Architecture Pattern) for event-driven, decoupled communication
  • Async: UniTask with CancellationToken throughout
  • DI: Reflex dependency injection — AppManager as root IInstaller, lazy singleton factories, [Inject] across gameplay and UI systems
  • App state: ApplicationStateMachine — table-driven phase tracking (Bootstrap → Auth → Menu → Game → GameOver) via SOAP ApplicationStateDataVariable
  • Auth: Unity Gaming Services (UGS) Authentication — anonymous sign-in, cached sessions, SOAP-driven state via AuthenticationDataVariable
  • Friends: UGS Friends — FriendsServiceFacade single-writer facade, relationship data via FriendsDataSO
  • Networking: Unity Netcode for GameObjects (multiplayer with AI backfill)
  • Camera: Cinemachine 3.1.2 with per-vessel settings
  • VFX: VFX Graph, custom HLSL shaders, Shader Graph, procedural skybox
  • Input: Unity Input System with platform-specific strategy pattern (keyboard/mouse, gamepad, touch)
  • Audio: FMOD Studio (FMODUnity) + SOAP gameplay SFX events for decoupled audio triggers
  • Haptics: NiceVibrations (mobile)
  • Animation: Timeline, DOTween
  • Performance: Unity Jobs + Burst Compiler, Adaptive Performance, DOTS Entities (incremental adoption)
  • Backend: Unity Gaming Services (Analytics, CloudSave, Leaderboards, Multiplayer, IAP, Ads)
  • Testing: Unity Test Framework (NUnit)
  • Tutorial: Custom FTUE system with adapter pattern
  • Dialogue: Custom dialogue system with editor tools
  • Target platforms: Mobile-first (iOS/Android) with PC/console expansion

Development

See CLAUDE.md for architecture patterns, coding standards, and detailed project structure.

See GIT_RULES.md for branching model, commit conventions, and PR standards.

Application Flow

Bootstrap Scene → AppManager (DI root + orchestrator, persists across scenes)
    ├─ Reflex DI: registers all managers, SO assets, services
    ├─ ApplicationStateMachine: None → Bootstrapping → Authenticating
    ├─ AuthenticationServiceFacade → UGS sign-in → SOAP state
    ├─ FriendsServiceFacade → UGS Friends init on auth
    ├─ MultiplayerSetup → starts network host on sign-in
    ├─ SceneLoader → game launch, restart, return-to-menu (NetworkBehaviour)
    └─ SceneTransitionManager → Authentication → Menu_Main (networked)
                                                    │
                                                    ▼
                                        ApplicationState: MainMenu
                                              ScreenSwitcher
                                    ┌────┬────┬────┬────┬────┐
                                    │Store│Arcade│Home│Port│Hangar│
                                    └────┴────┴────┴────┴────┘
                                    ← slide left / right →
                                    + MenuServerPlayerVesselInitializer
                                      (autopilot Squirrel in background)

The app boots through a Bootstrap scene where AppManager serves as both the top-level orchestrator ([DefaultExecutionOrder(-100)]) and the Reflex DI root (IInstaller). It configures the platform, registers all persistent managers and SO assets (including GameDataSO, SceneNameListSO, ApplicationLifecycleEventsContainerSO, ApplicationStateDataVariable, and FriendsDataSO), starts authentication and network monitoring, then transitions to the Authentication scene.

An ApplicationStateMachine (pure C# DI singleton) tracks the top-level application phase (Bootstrapping → Authenticating → MainMenu → LoadingGame → InGame → GameOver) with special states for Paused, Disconnected, and ShuttingDown. It validates transitions via a table-driven state graph and auto-subscribes to gameplay SOAP events for automatic phase changes. All phase data is written to ApplicationStateDataVariable (SOAP), allowing any system to read current state or subscribe to OnStateChanged.

Authentication is handled by the AuthenticationServiceFacade which writes to a shared SOAP AuthenticationDataVariable. On sign-in, MultiplayerSetup starts the network host, a FriendsServiceFacade initializes the UGS Friends service (syncing into FriendsDataSO), and the menu is loaded as a networked scene. Scene loading is managed by SceneLoader, a NetworkBehaviour that auto-selects local vs network scene loading and handles game restarts.

The Menu_Main scene uses a ScreenSwitcher that manages horizontal sliding navigation between five screen panels. A MenuServerPlayerVesselInitializer spawns an autopilot Squirrel vessel in the background with camera tracking. Screens implement the IScreen interface for lifecycle callbacks (OnScreenEnter/OnScreenExit), allowing the switcher to notify screens without hard-coded references. See the Menu Screen Navigation and Authentication & Session Flow sections (moved from CLAUDE.md into Docs/claude/) for details.

Architecture Audits

  • Bootstrap Scene Audit — All 16 root GameObjects, execution order map, applied fixes, and deferred refactoring issues
  • Performance — State, History & Plan — where performance stands, what was done, the ranked lever list, and how to measure (raw evidence frozen in Docs/archive/PERFORMANCE_LOG_2026.md)

Project Structure

Assets/
├── _Scripts/                  # All first-party C# code (~1,100 files)
│   ├── Controller/            # Gameplay systems (~536 files)
│   │   ├── Vessel/            # Vessel core, actions, prisms, trails
│   │   ├── Environment/       # Cells, crystals, flora/fauna, spawning
│   │   ├── ImpactEffects/     # Impactors + Effect SOs
│   │   ├── Arcade/            # Mini-game controllers, scoring
│   │   ├── Multiplayer/       # Netcode: vessel init, lobby, network stats, menu autopilot
│   │   ├── Camera/            # Per-vessel camera system
│   │   ├── AI/                # AIPilot, AIGunner
│   │   └── ...                # Projectiles, IO, FX, Managers, etc.
│   ├── System/                # Application-level systems (~126 files)
│   │   ├── Bootstrap/         # BootstrapConfigSO, SceneTransitionManager, ApplicationLifecycleManager
│   │   ├── Systems/Auth/      # AuthenticationController (MonoBehaviour adapter)
│   │   ├── Instrumentation/   # Analytics (UGS)
│   │   ├── Runtime/           # Dialogue runtime
│   │   ├── AppManager.cs      # Top-level orchestrator + Reflex DI root
│   │   ├── ApplicationStateMachine.cs  # App phase state machine (SOAP single-writer)
│   │   ├── SceneLoader.cs     # Scene loading, restart, return-to-menu (NetworkBehaviour)
│   │   ├── AuthenticationServiceFacade.cs
│   │   ├── AuthenticationSceneController.cs
│   │   ├── FriendsServiceFacade.cs  # UGS Friends single-writer facade
│   │   ├── SplashToAuthFlow.cs
│   │   ├── NetworkMonitor.cs
│   │   └── ...                # Audio, LoadOut, Quest, Ads, etc.
│   ├── UI/                    # Game & app UI (~188 files)
│   │   ├── Screens/           # Menu screens (Home, Arcade, Store, Hangar, Leaderboards, Episodes)
│   │   ├── Interfaces/        # IScreen, IVesselHUDController, IVesselHUDView
│   │   ├── Elements/          # Reusable components (NavLink, NavGroup, ProfileDisplayWidget)
│   │   ├── Views/             # PlayerDataService, screen views
│   │   ├── Controller/        # HUD controllers
│   │   ├── Modals/            # ModalWindowManager, Settings, Profile, Purchase dialogs
│   │   ├── ScreenSwitcher.cs  # Central menu navigation (slide + IScreen lifecycle)
│   │   └── ...                # FX, Toast, Animations
│   ├── Data/                  # Enums & data structs
│   ├── ScriptableObjects/     # SO definitions & SOAP types
│   │   └── SOAP/              # Custom SOAP types (16 subdirectories)
│   ├── Utility/               # Effects, pooling, data persistence
│   └── Tests/                 # Edit-mode unit tests
├── _SO_Assets/                # ScriptableObject asset instances
├── _Prefabs/                  # Prefabs organized by category
├── _Scenes/                   # Game scenes (singleplayer, multiplayer, test)
├── FTUE/                      # Tutorial / first-time user experience
└── Plugins/                   # Third-party (SOAP, DOTween, etc.)

Copyright and License

Copyright 2022 - 2026 Froglet Inc. Code released under the MIT license.

Froglet Games

Source 2 files
hooks/register.tsx 315 lines
1// The Cosmic Shore deck: one-press buttons for the checks every session runs by hand
2// (team activity, the C# gates, the reference compile, branch sync, ship), drawn in a
3// pane on every surface and as a band above the prompt on the terminal and desktop,
4// plus a branch status line. Each job runs on the host and shows its result without a
5// model turn; "Fix with Claude", "Merge bleeding-edge" and "Ship" hand the model a prompt.
6// Docs/claude/SESSION_DECK.md says what each button runs and how to add one.
7import { atom, read, update } from 'claude-code'
8import type { ProcessRunInit, ProcessRunResult, Register } from 'claude-code'
9
10import type { DeckPanel, SyncInfo } from '../types'
11
12const PANE = 'cosmic-shore-deck'
13const TITLE = 'Cosmic Shore'
14const panel = atom({ plugin: 'cosmic-shore-deck', key: 'panel' } as const, null)
15const busy = atom({ plugin: 'cosmic-shore-deck', key: 'busy' } as const, '')
16const sync = atom({ plugin: 'cosmic-shore-deck', key: 'sync' } as const, null)
17
18const STATUS_EVERY_MS = 120_000
19const BODY_LINES = 28
20
21// The fast gates CLAUDE.md asks for before a C# commit (none needs Unity). Edit this list
22// to add one; each runs from the repository root, all of them at once.
23const GATES: ReadonlyArray<readonly [string, readonly string[]]> = [
24  ['Console logging', ['python3', 'Tools/Build/check_console_logging.py']],
25  ['Using directives', ['python3', 'Tools/Build/check_using_directives.py']],
26  ['Conditional compilation', ['python3', 'Tools/Build/check_conditional_compilation.py']],
27  ['AI writes no state', ['python3', 'Tools/Build/check_ai_no_state_writes.py']],
28  ['Switch label collisions', ['python3', 'Tools/Build/check_switch_label_collisions.py']],
29  ['Enum member references', ['python3', 'Tools/Build/check_enum_member_references.py']],
30  ['Self-referential locals', ['python3', 'Tools/Build/check_self_referential_locals.py']],
31  ['Team tasks board', ['python3', 'Tools/Build/team_tasks.py', '--check']],
32]
33
34type Job = 'Team' | 'Gates' | 'Compile' | 'Sync'
35
36// Everything the jobs need from the engine, as closures built once in session.start: the
37// engine is spelled at each call site there, and the jobs below stay plain functions.
38type Host = {
39  run: (argv: readonly string[], init?: ProcessRunInit) => Promise<ProcessRunResult>
40  now: () => Promise<number>
41  getBusy: () => Promise<string>
42  setBusy: (job: string) => Promise<unknown>
43  setPanel: (p: DeckPanel) => Promise<unknown>
44  getPanel: () => Promise<DeckPanel | null>
45  setSync: (s: SyncInfo) => Promise<unknown>
46  status: (text: string) => void
47  toast: (text: string) => void
48  open: () => Promise<unknown>
49  ask: (text: string) => void
50}
51
52let host: Host | null = null
53let root = ''
54
55async function repoRoot(h: Host): Promise<string> {
56  if (root) return root
57  const r = await h.run(['git', 'rev-parse', '--show-toplevel'])
58  root = r.exitCode === 0 ? r.stdout.trim() : ''
59  return root
60}
61
62async function git(h: Host, args: string[], timeoutMs = 30_000): Promise<string> {
63  const r = await h.run(['git', ...args], { cwd: await repoRoot(h), timeoutMs })
64  return r.exitCode === 0 ? r.stdout.trim() : ''
65}
66
67const tail = (text: string, lines: number) => text.trimEnd().split('\n').slice(-lines).join('\n')
68const count = (s: string | undefined) => Number.parseInt(s ?? '', 10) || 0
69
70async function readSync(h: Host): Promise<SyncInfo> {
71  const branch = (await git(h, ['rev-parse', '--abbrev-ref', 'HEAD'])) || '?'
72  const [lr, dirty, be] = await Promise.all([
73    git(h, ['rev-list', '--left-right', '--count', `HEAD...origin/${branch}`]),
74    git(h, ['status', '--porcelain']),
75    git(h, ['rev-list', '--count', 'HEAD..origin/bleeding-edge']),
76  ])
77  const [ahead, behind] = lr.split(/\s+/)
78  return {
79    branch,
80    ahead: count(ahead),
81    behind: count(behind),
82    dirty: dirty ? dirty.split('\n').length : 0,
83    bleedingEdgeAhead: count(be),
84  }
85}
86
87function statusText(s: SyncInfo): string {
88  const parts = [`${s.branch} ↑${s.ahead} ↓${s.behind}`]
89  if (s.dirty) parts.push(`${s.dirty} changed`)
90  if (s.bleedingEdgeAhead) parts.push(`bleeding-edge +${s.bleedingEdgeAhead}`)
91  return parts.join(' · ')
92}
93
94async function refreshStatus(h: Host): Promise<void> {
95  const s = await readSync(h)
96  await h.setSync(s)
97  h.status(statusText(s))
98}
99
100type Result = Omit<DeckPanel, 'at'>
101
102async function runTeam(h: Host): Promise<Result> {
103  const cwd = await repoRoot(h)
104  await git(h, ['fetch', '-q', '--prune', 'origin'], 60_000)
105  const r = await h.run(['python3', '.claude/hooks/team_activity.py', '--peek', '12'], {
106    cwd,
107    env: { CLAUDE_PROJECT_DIR: cwd },
108  })
109  const body = (r.stdout || r.stderr).trim()
110  return { title: 'Team (last 12 h)', body: tail(body, BODY_LINES + 4), ok: null, failure: '' }
111}
112
113async function runGates(h: Host): Promise<Result> {
114  const cwd = await repoRoot(h)
115  const results = await Promise.all(
116    GATES.map(async ([name, argv]) => {
117      const startedAt = await h.now()
118      try {
119        const r = await h.run(argv, { cwd, timeoutMs: 180_000 })
120        const out = `${r.stdout}\n${r.stderr}`.trim()
121        return { name, ok: r.exitCode === 0, seconds: ((await h.now()) - startedAt) / 1000, out }
122      } catch (err) {
123        return { name, ok: false, seconds: 0, out: String(err) }
124      }
125    }),
126  )
127  const failed = results.filter(r => !r.ok)
128  const body = results
129    .map(r => `${r.ok ? 'PASS' : 'FAIL'}  ${r.name} (${r.seconds.toFixed(1)} s)\n      ${tail(r.out, 1)}`)
130    .join('\n')
131  const failure = failed.map(r => `## ${r.name}\n${tail(r.out, 30)}`).join('\n\n')
132  return { title: `Gates: ${results.length - failed.length}/${results.length} pass`, body, ok: failed.length === 0, failure }
133}
134
135async function runCompile(h: Host): Promise<Result> {
136  const r = await h.run(['bash', 'Tools/Build/unity_refcompile/run.sh', '--config', 'player-dev'], {
137    cwd: await repoRoot(h),
138    timeoutMs: 600_000,
139  })
140  const out = `${r.stdout}\n${r.stderr}`.trim()
141  return {
142    title: `Reference compile (player-dev): ${r.exitCode === 0 ? 'clean' : `exit ${r.exitCode}`}`,
143    body: tail(out, BODY_LINES),
144    ok: r.exitCode === 0,
145    failure: r.exitCode === 0 ? '' : tail(out, 80),
146  }
147}
148
149async function runSync(h: Host): Promise<Result> {
150  await git(h, ['fetch', '-q', '--prune', 'origin'], 60_000)
151  const s = await readSync(h)
152  await h.setSync(s)
153  h.status(statusText(s))
154  const lines = [
155    `Branch ${s.branch}: ${s.ahead} to push, ${s.behind} to pull from origin/${s.branch}.`,
156    `${s.dirty} changed file(s) not committed.`,
157    `origin/bleeding-edge has ${s.bleedingEdgeAhead} commit(s) this branch does not.`,
158  ]
159  return { title: 'Sync', body: lines.join('\n'), ok: null, failure: '' }
160}
161
162const JOBS: Record<Job, (h: Host) => Promise<Result>> = {
163  Team: runTeam,
164  Gates: runGates,
165  Compile: runCompile,
166  Sync: runSync,
167}
168
169/** Runs one job at a time and puts its result in the pane; null when another job is running. */
170async function runJob(job: Job): Promise<DeckPanel | null> {
171  const h = host
172  if (!h) return null
173  const running = await h.getBusy()
174  if (running) {
175    h.toast(`${running} is still running.`)
176    return null
177  }
178  await h.setBusy(job)
179  let result: Result
180  try {
181    result = await JOBS[job](h)
182  } catch (err) {
183    result = { title: `${job} could not run`, body: String(err), ok: false, failure: '' }
184  } finally {
185    await h.setBusy('')
186  }
187  await h.setPanel({ ...result, at: await h.now() })
188  return h.getPanel()
189}
190
191async function openAndRun(job: Job): Promise<void> {
192  await host?.open()
193  const result = await runJob(job)
194  if (result) host?.toast(result.title)
195}
196
197const ask = (text: string) => host?.ask(text)
198
199const fixPrompt = (p: DeckPanel) =>
200  `The Cosmic Shore deck's "${p.title}" failed. Find the root cause in this branch's changes and fix it, ` +
201  `then re-run the same check. Output:\n\n${p.failure}`
202
203const mergePrompt = (s: SyncInfo) =>
204  `Merge origin/bleeding-edge into ${s.branch} with a merge commit (never rebase), ` +
205  `resolve any conflicts by building on both sides, run the gates, and push.`
206
207const pullPrompt = (s: SyncInfo) =>
208  `Pull origin/${s.branch} into the local branch (git pull --no-rebase), resolve any conflicts, and tell me what came in.`
209
210export const register: Register = on => {
211  on('session.start', async ($, e, next) => {
212    await $.command.register({ name: 'deck', description: 'Open the Cosmic Shore deck (team, gates, compile, sync, ship)' })
213    await $.command.register({ name: 'gates', description: 'Run the fast C# gates now and show which fail' })
214    await $.command.register({ name: 'team', description: 'Show what every branch pushed in the last 12 hours' })
215    await $.command.register({ name: 'sync', description: 'Fetch, then show ahead/behind against origin and bleeding-edge' })
216    const h: Host = {
217      run: (argv, init) => $.process.run(argv, init),
218      now: () => $.clock.now(),
219      getBusy: () => read($, busy),
220      setBusy: job => update($, busy, () => job),
221      setPanel: p => update($, panel, () => p),
222      getPanel: () => read($, panel),
223      setSync: s => update($, sync, () => s),
224      status: text => $.ui.status(text),
225      toast: text => $.ui.toast(text),
226      open: () => $.ui.open({ id: PANE, title: TITLE }),
227      ask: text => void $.prompt.submit({ text, asUser: true }),
228    }
229    host = h
230    void refreshStatus(h)
231    $.clock.every(STATUS_EVERY_MS, () => void refreshStatus(h))
232    return next(e)
233  })
234
235  on('turn.complete', async ($, e, next) => {
236    if (host) void refreshStatus(host)
237    return next(e)
238  })
239
240  on('command.run', { command: 'deck' }, async $ => {
241    await $.ui.open({ id: PANE, title: TITLE })
242    return { text: 'Cosmic Shore deck opened.' }
243  })
244
245  // /gates, /team and /sync answer with the result as text, so the model reads it too.
246  const answer = (result: DeckPanel | null, job: Job) => ({
247    text: result ? `${result.title}\n${result.body}` : `${job} is already running.`,
248  })
249  on('command.run', { command: 'gates' }, async () => answer(await runJob('Gates'), 'Gates'))
250  on('command.run', { command: 'team' }, async () => answer(await runJob('Team'), 'Team'))
251  on('command.run', { command: 'sync' }, async () => answer(await runJob('Sync'), 'Sync'))
252
253  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
254    if (e.props.hasSurvey) return next(e)
255    const { Box, Button, Text } = $.ui.resolve(e)
256    const running = await read($, busy)
257    return (
258      <Box flexWrap="wrap" columnGap={1}>
259        <Text dimColor>{running ? `Deck: ${running}…` : 'Deck'}</Text>
260        <Button key="open" plain label="Open" onPress={() => $.ui.open({ id: PANE, title: TITLE })} />
261        <Button key="team" plain label="Team" onPress={() => openAndRun('Team')} />
262        <Button key="gates" plain label="Gates" onPress={() => openAndRun('Gates')} />
263        <Button key="sync" plain label="Sync" onPress={() => openAndRun('Sync')} />
264      </Box>
265    )
266  })
267
268  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
269    const { Box, Button, Text, Code } = $.ui.resolve(e)
270    const [last, running, branch] = await Promise.all([read($, panel), read($, busy), read($, sync)])
271    const now = await $.clock.now()
272    const age = last ? Math.max(0, Math.round((now - last.at) / 60_000)) : 0
273    const verdict = last?.ok === true ? 'PASS' : last?.ok === false ? 'FAIL' : ''
274    const color = last?.ok === true ? 'success' : last?.ok === false ? 'error' : undefined
275    return (
276      <Box flexDirection="column" rowGap={1}>
277        <Box flexWrap="wrap" columnGap={1} rowGap={1}>
278          <Button key="team" hotkey="t" label="Team" onPress={() => runJob('Team')} />
279          <Button key="gates" hotkey="g" label="Gates" variant="primary" onPress={() => runJob('Gates')} />
280          <Button key="compile" hotkey="c" label="Compile" onPress={() => runJob('Compile')} />
281          <Button key="sync" hotkey="s" label="Sync" onPress={() => runJob('Sync')} />
282          <Button key="ship" hotkey="p" label="Ship" onPress={() => ask('/ship-quick')} />
283        </Box>
284        {branch && <Text dimColor wrap="wrap">{statusText(branch)}</Text>}
285        {running && <Text color="warning">{running} running… (Compile can take several minutes)</Text>}
286        {!running && !last && (
287          <Text dimColor wrap="wrap">
288            Team: pushes on every branch. Gates: the fast C# checks. Compile: the reference compile. Sync: fetch and
289            compare. Ship: /ship-quick.
290          </Text>
291        )}
292        {last && (
293          <Box flexDirection="column">
294            <Text bold color={color} wrap="wrap">
295              {verdict ? `${verdict}  ` : ''}
296              {last.title}
297              <Text dimColor>{`  ${age ? `${age} min ago` : 'just now'}`}</Text>
298            </Text>
299            <Code source={last.body || '(no output)'} />
300          </Box>
301        )}
302        <Box flexWrap="wrap" columnGap={1} rowGap={1}>
303          {last?.failure && <Button key="fix" label="Fix with Claude" onPress={() => ask(fixPrompt(last))} />}
304          {last?.title === 'Sync' && branch && branch.behind > 0 && (
305            <Button key="pull" label="Pull" onPress={() => ask(pullPrompt(branch))} />
306          )}
307          {last?.title === 'Sync' && branch && branch.bleedingEdgeAhead > 0 && (
308            <Button key="merge" label="Merge bleeding-edge" onPress={() => ask(mergePrompt(branch))} />
309          )}
310        </Box>
311      </Box>
312    )
313  })
314}
315
types/index.d.ts 35 lines
1/** What the deck's pane shows: the last job's result. */
2export type DeckPanel = {
3  /** The job: Team, Gates, Compile, Sync. */
4  title: string
5  /** Its output, already trimmed to what the pane draws. */
6  body: string
7  /** true passed, false failed, null informational. */
8  ok: boolean | null
9  /** For a failed job: the text a "Fix with Claude" button hands the model. */
10  failure: string
11  /** When it finished, ms since the epoch. */
12  at: number
13}
14
15/** The branch facts the status line and the Sync view draw. */
16export type SyncInfo = {
17  branch: string
18  ahead: number
19  behind: number
20  dirty: number
21  /** Commits on origin/bleeding-edge that this branch does not have. */
22  bleedingEdgeAhead: number
23}
24
25declare module 'claude-code' {
26  interface PluginState {
27    'cosmic-shore-deck': {
28      panel: DeckPanel | null
29      /** The running job's name, '' when idle. */
30      busy: string
31      sync: SyncInfo | null
32    }
33  }
34}
35