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


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!
Also don't forget to join our community!
Each vessel embodies a different genre of gameplay:
| Vessel | Genre | Description |
|---|---|---|
| Squirrel | Racing / Drift | Vaporwave arcade racer — tube-riding along player-generated trails (F-Zero / Redout feel) |
| Sparrow | Shooter | Arcade 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 |

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.

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.

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.

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.

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.
AppManager as root IInstaller, lazy singleton factories, [Inject] across gameplay and UI systemsApplicationStateMachine — table-driven phase tracking (Bootstrap → Auth → Menu → Game → GameOver) via SOAP ApplicationStateDataVariableAuthenticationDataVariableFriendsServiceFacade single-writer facade, relationship data via FriendsDataSOFMODUnity) + SOAP gameplay SFX events for decoupled audio triggersSee CLAUDE.md for architecture patterns, coding standards, and detailed project structure.
See GIT_RULES.md for branching model, commit conventions, and PR standards.
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.
Docs/archive/PERFORMANCE_LOG_2026.md)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 2022 - 2026 Froglet Inc. Code released under the MIT license.

hooks/register.tsx 315 lines1// 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}
315types/index.d.ts 35 lines1/** 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