@g3nomaly/g3n-mods 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.claude-plugin/marketplace.json +8 -2
  2. package/README.md +13 -6
  3. package/mods/agent-adventure/.claude-plugin/plugin.json +23 -0
  4. package/mods/agent-adventure/hooks/content.ts +263 -0
  5. package/mods/agent-adventure/hooks/engine.ts +1125 -0
  6. package/mods/agent-adventure/hooks/hooks.json +1 -0
  7. package/mods/agent-adventure/hooks/register.tsx +465 -0
  8. package/mods/agent-adventure/hooks/sprite.ts +424 -0
  9. package/mods/agent-adventure/types/index.d.ts +248 -0
  10. package/mods/agent-adventure/view/panes.json +8 -0
  11. package/mods/agent-adventure/view/view.css +230 -0
  12. package/mods/agent-adventure/view/view.ts +308 -0
  13. package/mods/agents-panel/.claude-plugin/plugin.json +9 -1
  14. package/mods/agents-panel/hooks/register.tsx +18 -4
  15. package/mods/agents-panel/view/panes.json +8 -0
  16. package/mods/context-bar/.claude-plugin/plugin.json +9 -1
  17. package/mods/context-bar/hooks/register.tsx +4 -3
  18. package/mods/context-bar/view/panes.json +8 -0
  19. package/mods/custom-crab-agents/hooks/register.tsx +15 -1
  20. package/mods/delete-guard/.claude-plugin/plugin.json +9 -1
  21. package/mods/delete-guard/hooks/register.tsx +15 -5
  22. package/mods/delete-guard/view/panes.json +8 -0
  23. package/mods/focus-mode/.claude-plugin/plugin.json +6 -0
  24. package/mods/focus-mode/hooks/register.tsx +7 -4
  25. package/mods/focus-mode/view/panes.json +8 -0
  26. package/mods/mini-game/.claude-plugin/plugin.json +10 -2
  27. package/mods/mini-game/hooks/bubbles.ts +27 -0
  28. package/mods/mini-game/hooks/minesweeper.ts +108 -0
  29. package/mods/mini-game/hooks/mole.ts +91 -0
  30. package/mods/mini-game/hooks/register.tsx +206 -148
  31. package/mods/mini-game/types/index.d.ts +18 -1
  32. package/mods/mini-game/view/panes.json +8 -0
  33. package/mods/tool-timeline/.claude-plugin/plugin.json +9 -1
  34. package/mods/tool-timeline/hooks/register.tsx +18 -4
  35. package/mods/tool-timeline/view/panes.json +8 -0
  36. package/mods/you-should-know/.claude-plugin/plugin.json +3 -3
  37. package/mods/you-should-know/hooks/register.tsx +173 -45
  38. package/mods/you-should-know/types/index.d.ts +13 -4
  39. package/package.json +2 -1
@@ -1,123 +1,80 @@
1
1
  // packages/g3n-mods/mods/mini-game/hooks/register.tsx
2
2
  import { atom, read, update } from 'claude-code'
3
- import type { EngineInterface, Register } from 'claude-code'
3
+ import type { EngineInterface, Register, RenderChildren } from 'claude-code'
4
4
 
5
- import type { Game } from '../types'
5
+ import type { Game, View, Whack } from '../types'
6
+ import { adjacentMines, newGame, reveal, toggleFlag } from './minesweeper'
7
+ import { LIVES, escape, newWhack, quit, startWhack, whack as whackHole } from './mole'
6
8
 
7
9
  const PANE = 'mini-game'
8
- const TITLE = 'Minesweeper'
10
+ const TITLES: Record<View, string> = { mines: 'Minesweeper', mole: 'Whack-a-mole' }
11
+ /** What `/mini-game` takes after its name, each mapped to the game it opens. */
12
+ const VIEW_ARGS: Record<string, View> = { mines: 'mines', minesweeper: 'mines', mole: 'mole', whack: 'mole' }
13
+
14
+ const view = atom({ plugin: 'mini-game', key: 'view' } as const, 'mines')
9
15
  const game = atom({ plugin: 'mini-game', key: 'game' } as const, null)
10
16
  const best = atom({ plugin: 'mini-game', key: 'best' } as const, null)
17
+ const whack = atom({ plugin: 'mini-game', key: 'whack' } as const, null)
18
+ const whackBest = atom({ plugin: 'mini-game', key: 'whackBest' } as const, null)
11
19
 
12
- const SIZE = 9
13
- const MINES = 10
14
20
  const BEST_KEY = 'best-seconds'
21
+ const WHACK_BEST_KEY = 'whack-best'
15
22
  /** How long a turn runs before the mod suggests a game, once per session. */
16
23
  const HINT_AFTER_MS = 60_000
17
- /** Width of every cell, in columns: room for the widest glyph (`■`, `⚑`, `✹`) on any surface. */
24
+ /** Width of every cell, in columns: room for the widest glyph (`■`, `⚑`, `✹`, `🐹`) on any surface. */
18
25
  const CELL_WIDTH = 2
19
26
 
20
27
  const NUMBER_COLORS = [undefined, '#6b8fd6', '#7cc47f', '#e06c6c', '#9b87f5', '#e0775a', '#5fb3c4', '#d0d0d0', '#8a8f98']
21
28
 
22
29
  /**
23
- * Lists the cells around `cell` on a `size` × `size` board.
24
- *
25
- * @param cell - Cell number, counted row by row from `0`.
26
- * @param size - Board width and height in cells.
27
- * @returns The neighboring cell numbers, from one to eight of them.
28
- */
29
- export function neighbors(cell: number, size: number): number[] {
30
- const row = Math.floor(cell / size)
31
- const col = cell % size
32
- const found: number[] = []
33
- for (let dr = -1; dr <= 1; dr++) {
34
- for (let dc = -1; dc <= 1; dc++) {
35
- const r = row + dr
36
- const c = col + dc
37
- if ((dr !== 0 || dc !== 0) && r >= 0 && r < size && c >= 0 && c < size) found.push(r * size + c)
38
- }
39
- }
40
- return found
41
- }
42
-
43
- /**
44
- * Counts the mines in the cells around `cell`.
45
- *
46
- * @param board - Board to read, see `Game`.
47
- * @param cell - Cell number to count around.
48
- * @returns Number of neighboring mines, from `0` to `8`.
49
- */
50
- function adjacentMines(board: Game, cell: number): number {
51
- return neighbors(cell, board.size).filter(n => board.mines.includes(n)).length
52
- }
53
-
54
- /**
55
- * Picks random cells for the mines, leaving `safe` and the cells around it free.
30
+ * Opens the pane in the Claude Code chat, titled after the game it holds.
56
31
  *
57
- * @param board - Board to place mines on, see `Game`.
58
- * @param safe - Cell the first reveal hit, which must not be a mine.
59
- * @returns Mine cell numbers, `board.mineCount` of them.
32
+ * @param $ - Engine handle the hook received, see `EngineInterface`.
60
33
  */
61
- function placeMines(board: Game, safe: number): number[] {
62
- const banned = new Set([safe, ...neighbors(safe, board.size)])
63
- const free = Array.from({ length: board.size * board.size }, (_, i) => i).filter(i => !banned.has(i))
64
- for (let i = free.length - 1; i > 0; i--) {
65
- const j = Math.floor(Math.random() * (i + 1))
66
- ;[free[i], free[j]] = [free[j] ?? 0, free[i] ?? 0]
67
- }
68
- return free.slice(0, board.mineCount)
34
+ async function openPane($: EngineInterface): Promise<void> {
35
+ await $.ui.open({ id: PANE, title: TITLES[await read($, view)] })
69
36
  }
70
37
 
71
38
  /**
72
- * @brief Reveals one cell: the first reveal places the mines, a zero opens its
73
- * neighbours, a mine ends the game, and the last safe cell wins it.
39
+ * Titles the pane after the game it holds where the Claude Code chat shows it, and redraws it. A press never opens
40
+ * it there: a pane the mods viewer alone draws stays out of the chat.
74
41
  *
75
- * @param board - Board to reveal on, see `Game`.
76
- * @param cell - Cell number to reveal.
77
- * @param now - Milliseconds since the epoch, stamped as the start or end time.
78
- * @returns The board after the reveal, or the same board when the cell cannot be revealed.
42
+ * @param $ - Engine handle the hook received, see `EngineInterface`.
79
43
  */
80
- export function reveal(board: Game, cell: number, now: number): Game {
81
- if (board.status === 'won' || board.status === 'lost') return board
82
- if (board.revealed.includes(cell) || board.flagged.includes(cell)) return board
83
-
84
- const mines = board.status === 'ready' ? placeMines(board, cell) : board.mines
85
- const started: Game = board.status === 'ready' ? { ...board, mines, status: 'playing', startedAt: now } : board
86
- if (mines.includes(cell)) return { ...started, revealed: [...started.revealed, cell], status: 'lost', endedAt: now }
87
-
88
- const open = new Set(started.revealed)
89
- const queue = [cell]
90
- while (queue.length > 0) {
91
- const next = queue.pop() ?? cell
92
- if (open.has(next) || started.flagged.includes(next)) continue
93
- open.add(next)
94
- if (adjacentMines(started, next) === 0) queue.push(...neighbors(next, started.size).filter(n => !open.has(n)))
95
- }
96
-
97
- const revealed = [...open]
98
- const isWon = revealed.length === started.size * started.size - started.mineCount
99
- return isWon ? { ...started, revealed, status: 'won', endedAt: now } : { ...started, revealed }
44
+ async function showGame($: EngineInterface): Promise<void> {
45
+ if ((await $.ui.panes()).some(pane => pane.id === PANE)) await openPane($)
46
+ else $.ui.invalidate('ui.render')
100
47
  }
101
48
 
102
49
  /**
103
- * Creates a board in the `ready` state, with no mines placed yet.
50
+ * Starts a new Minesweeper board and shows it.
104
51
  *
105
- * @param now - Milliseconds since the epoch, stamped as the start time.
106
- * @returns The new board, see `Game`.
52
+ * @param $ - Engine handle the hook received, see `EngineInterface`.
107
53
  */
108
- function newGame(now: number): Game {
109
- return { size: SIZE, mineCount: MINES, mines: [], revealed: [], flagged: [], status: 'ready', isFlagMode: false, startedAt: now }
54
+ async function startGame($: EngineInterface): Promise<void> {
55
+ const now = await $.clock.now()
56
+ await update($, game, () => newGame(now))
57
+ await showGame($)
110
58
  }
111
59
 
112
60
  /**
113
- * Starts a new board and shows the pane.
61
+ * Switches the pane to `next`, setting up its game when there is none to play.
62
+ * Leaving whack-a-mole mid-round ends the round, so moles do not escape unseen.
114
63
  *
115
64
  * @param $ - Engine handle the hook received, see `EngineInterface`.
65
+ * @param next - Game to show.
116
66
  */
117
- async function startGame($: EngineInterface): Promise<void> {
118
- const now = await $.clock.now()
119
- await update($, game, () => newGame(now))
120
- await $.ui.open({ id: PANE, title: TITLE })
67
+ async function showView($: EngineInterface, next: View): Promise<void> {
68
+ await update($, view, () => next)
69
+ if (next === 'mole') {
70
+ await update($, whack, current => current ?? newWhack())
71
+ await showGame($)
72
+ return
73
+ }
74
+ await changeWhack($, quit)
75
+ const current = await read($, game)
76
+ if (current === null || current.status === 'won' || current.status === 'lost') await startGame($)
77
+ else await showGame($)
121
78
  }
122
79
 
123
80
  /**
@@ -130,10 +87,7 @@ async function pressCell($: EngineInterface, cell: number): Promise<void> {
130
87
  const now = await $.clock.now()
131
88
  const after = await update($, game, board => {
132
89
  if (board === null) return board
133
- if (!board.isFlagMode) return reveal(board, cell, now)
134
- if (board.revealed.includes(cell) || board.status === 'won' || board.status === 'lost') return board
135
- const flagged = board.flagged.includes(cell) ? board.flagged.filter(c => c !== cell) : [...board.flagged, cell]
136
- return { ...board, flagged }
90
+ return board.isFlagMode ? toggleFlag(board, cell) : reveal(board, cell, now)
137
91
  })
138
92
  if (after?.status === 'won' && after.endedAt === now) {
139
93
  const seconds = Math.round((now - after.startedAt) / 1000)
@@ -156,12 +110,46 @@ async function toggleFlagMode($: EngineInterface): Promise<void> {
156
110
  }
157
111
 
158
112
  /**
159
- * Redraws the timer once a second while a game is on.
113
+ * Applies `change` to the whack-a-mole game, times each new mole's escape, and
114
+ * records the score when the round ends.
115
+ *
116
+ * @param $ - Engine handle the hook received, see `EngineInterface`.
117
+ * @param change - Pure step from `mole.ts` to apply.
118
+ */
119
+ async function changeWhack($: EngineInterface, change: (current: Whack) => Whack): Promise<void> {
120
+ let wasPlaying = false
121
+ let moleIdBefore = 0
122
+ const after = await update($, whack, current => {
123
+ if (current === null) return current
124
+ wasPlaying = current.status === 'playing'
125
+ moleIdBefore = current.moleId
126
+ return change(current)
127
+ })
128
+ if (after === null) return
129
+
130
+ if (after.status === 'playing' && after.moleId !== moleIdBefore) {
131
+ const moleId = after.moleId
132
+ $.clock.after(after.windowMs, () => void changeWhack($, current => escape(current, moleId)))
133
+ }
134
+
135
+ if (wasPlaying && after.status === 'over') {
136
+ const previous = await read($, whackBest)
137
+ const isBest = after.score > 0 && (previous === null || after.score > previous)
138
+ if (isBest) {
139
+ await update($, whackBest, () => after.score)
140
+ await $.store.set(WHACK_BEST_KEY, after.score)
141
+ }
142
+ $.ui.toast(`Game over: ${after.score} whacked${isBest ? ', a new high score' : ''}`)
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Redraws the timer once a second while a Minesweeper game is on.
160
148
  *
161
149
  * @param $ - Engine handle the hook received, see `EngineInterface`.
162
150
  */
163
151
  async function tick($: EngineInterface): Promise<void> {
164
- if ((await read($, game))?.status === 'playing') $.ui.invalidate('ui.render')
152
+ if ((await read($, view)) === 'mines' && (await read($, game))?.status === 'playing') $.ui.invalidate('ui.render')
165
153
  }
166
154
 
167
155
  let hintTurn: string | undefined
@@ -177,29 +165,39 @@ async function hint($: EngineInterface, turnId: string): Promise<void> {
177
165
  if (isHinted || hintTurn !== turnId) return
178
166
  if ((await $.ui.panes()).some(pane => pane.id === PANE)) return
179
167
  isHinted = true
180
- $.ui.toast('Waiting on Claude? /mini-game opens Minesweeper.')
168
+ $.ui.toast('Waiting on Claude? /mini-game opens Minesweeper, /mini-game mole opens whack-a-mole.')
181
169
  }
182
170
 
183
171
  /**
184
172
  * Registers this mod's `mini-game` command, its session, turn and pane hooks.
185
173
  *
186
174
  * @param on - Hook registration function the engine passes in.
175
+ * @param options - Plugin options; `showInClaudeCode` opens the pane in the Claude Code chat at session start.
187
176
  */
188
- export const register: Register = on => {
177
+ export const register: Register = (on, options) => {
189
178
  on('session.start', async ($, e, next) => {
190
- await $.command.register({ name: 'mini-game', description: 'Play Minesweeper while Claude works' })
191
- const stored = await $.store.get(BEST_KEY)
192
- if (typeof stored === 'number') await update($, best, () => stored)
193
- void $.ui.open({ id: PANE, title: TITLE })
179
+ await $.command.register({ name: 'mini-game', description: 'Play Minesweeper or whack-a-mole while Claude works' })
180
+ const storedBest = await $.store.get(BEST_KEY)
181
+ if (typeof storedBest === 'number') await update($, best, () => storedBest)
182
+ const storedWhackBest = await $.store.get(WHACK_BEST_KEY)
183
+ if (typeof storedWhackBest === 'number') await update($, whackBest, () => storedWhackBest)
184
+ if (options.showInClaudeCode === true) void openPane($)
194
185
  $.clock.every(1000, () => void tick($))
195
186
  return next(e)
196
187
  })
197
188
 
198
- on('command.run', { command: 'mini-game' }, async $ => {
199
- const current = await read($, game)
200
- if (current === null || current.status === 'won' || current.status === 'lost') await startGame($)
201
- else await $.ui.open({ id: PANE, title: TITLE })
202
- return { text: 'Minesweeper is open. Clear the board without hitting a mine.' }
189
+ on('command.run', { command: 'mini-game' }, async ($, e) => {
190
+ const arg = e.args.trim().toLowerCase()
191
+ const next = arg === '' ? await read($, view) : VIEW_ARGS[arg]
192
+ if (next === undefined) return { text: `Unknown game "${arg}". Use /mini-game mines or /mini-game mole.`, exitCode: 1 }
193
+ await showView($, next)
194
+ // Typed in the chat, so the game shows there too.
195
+ await openPane($)
196
+ return {
197
+ text: next === 'mines'
198
+ ? 'Minesweeper is open. Clear the board without hitting a mine.'
199
+ : 'Whack-a-mole is open. Hit the mole before it hides; three misses and the round ends.',
200
+ }
203
201
  })
204
202
 
205
203
  on('turn.start', async ($, e, next) => {
@@ -216,66 +214,126 @@ export const register: Register = on => {
216
214
 
217
215
  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
218
216
  const { Box, Text, Button } = $.ui.resolve(e)
219
- const board = await read($, game)
220
- if (board === null) {
221
- return <Button key="new" label="new game" variant="primary" onPress={() => startGame($)} />
222
- }
217
+ const current = await read($, view)
218
+
219
+ const tabs = (
220
+ <Box flexDirection="row" columnGap={1}>
221
+ {(Object.keys(TITLES) as View[]).map(v => (
222
+ <Button key={`view:${v}`} label={TITLES[v]} variant={v === current ? 'primary' : 'secondary'} onPress={() => showView($, v)} />
223
+ ))}
224
+ </Box>
225
+ )
223
226
 
224
- const now = await $.clock.now()
225
- const elapsed = board.status === 'ready' ? 0 : Math.round(((board.endedAt ?? now) - board.startedAt) / 1000)
226
- const record = await read($, best)
227
- const isOver = board.status === 'won' || board.status === 'lost'
227
+ // Every cell sits in a box of one fixed width, so changing a cell never moves the others: glyphs differ in width.
228
+ const grid = (size: number, face: (cell: number) => RenderChildren) => (
229
+ <Box flexDirection="column">
230
+ {Array.from({ length: size }, (_, r) => (
231
+ <Box flexDirection="row" columnGap={1}>
232
+ {Array.from({ length: size }, (_, c) => (
233
+ <Box width={CELL_WIDTH} flexShrink={0} justifyContent="center">{face(r * size + c)}</Box>
234
+ ))}
235
+ </Box>
236
+ ))}
237
+ </Box>
238
+ )
228
239
 
229
240
  /**
230
- * Draws what a cell shows: a number, a flag, a mine, or a button that reveals or flags it.
241
+ * Draws whack-a-mole: lives, score and high score, and the holes, each a button while a round is on.
231
242
  *
232
- * @param cell - Cell number to draw.
233
- * @returns The cell's content.
243
+ * @param round - Game to draw, see `Whack`.
244
+ * @returns The status line, the controls and the holes.
234
245
  */
235
- const cellFace = (cell: number) => {
236
- const isRevealed = board.revealed.includes(cell)
237
- const isMine = board.mines.includes(cell)
238
- if (isRevealed || (isOver && isMine)) {
239
- if (isMine) return <Text color="#e06c6c">✹</Text>
240
- const n = adjacentMines(board, cell)
241
- return <Text color={NUMBER_COLORS[n]}>{n === 0 ? ' ' : String(n)}</Text>
246
+ const moleView = async (round: Whack) => {
247
+ const record = await read($, whackBest)
248
+ const isPlaying = round.status === 'playing'
249
+
250
+ const holeFace = (hole: number) => {
251
+ if (!isPlaying) return <Text dimColor>○</Text>
252
+ return (
253
+ <Button key={`hole:${hole}`} plain onPress={() => changeWhack($, current => whackHole(current, hole))}>
254
+ {hole === round.mole ? <Text>🐹</Text> : <Text color="#6b7280">○</Text>}
255
+ </Button>
256
+ )
242
257
  }
243
- const label = board.flagged.includes(cell) ? '⚑' : '■'
244
- if (isOver) return <Text dimColor>{label}</Text>
258
+
259
+ const hearts = '♥'.repeat(round.lives) + '♡'.repeat(LIVES - round.lives)
260
+ const recordText = record === null ? '' : ` · best ${record}`
261
+ const status = round.status === 'over'
262
+ ? <Text color="#e06c6c" bold>Game over. {round.score} whacked{recordText}</Text>
263
+ : round.status === 'ready'
264
+ ? <Text dimColor>Hit the mole before it hides{recordText}</Text>
265
+ : <Text><Text color="#e06c6c">{hearts}</Text><Text dimColor> · {round.score} whacked{recordText}</Text></Text>
266
+
245
267
  return (
246
- <Button key={`cell:${cell}`} plain onPress={() => pressCell($, cell)}>
247
- <Text color={label === '⚑' ? '#e0775a' : '#6b7280'}>{label}</Text>
248
- </Button>
268
+ <Box flexDirection="column" gap={1}>
269
+ <Box flexDirection="row" justifyContent="space-between" alignItems="center">
270
+ {status}
271
+ {isPlaying ? '' : <Button key="play" label={round.status === 'over' ? 'play again' : 'start'} variant="primary" onPress={() => changeWhack($, startWhack)} />}
272
+ </Box>
273
+ {grid(round.size, holeFace)}
274
+ </Box>
249
275
  )
250
276
  }
251
277
 
252
- // Every cell sits in a box of one fixed width, so revealing a cell never moves the others: glyphs differ in width.
253
- const cellView = (cell: number) => (
254
- <Box width={CELL_WIDTH} flexShrink={0} justifyContent="center">{cellFace(cell)}</Box>
255
- )
278
+ /**
279
+ * Draws the Minesweeper board: numbers, flags, mines, and buttons for hidden cells.
280
+ *
281
+ * @param board - Board to draw, see `Game`.
282
+ * @returns The status line, the controls and the board.
283
+ */
284
+ const minesView = async (board: Game) => {
285
+ const now = await $.clock.now()
286
+ const elapsed = board.status === 'ready' ? 0 : Math.round(((board.endedAt ?? now) - board.startedAt) / 1000)
287
+ const record = await read($, best)
288
+ const isOver = board.status === 'won' || board.status === 'lost'
256
289
 
257
- const rows = Array.from({ length: board.size }, (_, r) => (
258
- <Box flexDirection="row" columnGap={1}>
259
- {Array.from({ length: board.size }, (_, c) => cellView(r * board.size + c))}
260
- </Box>
261
- ))
290
+ const cellFace = (cell: number) => {
291
+ const isRevealed = board.revealed.includes(cell)
292
+ const isMine = board.mines.includes(cell)
293
+ if (isRevealed || (isOver && isMine)) {
294
+ if (isMine) return <Text color="#e06c6c">✹</Text>
295
+ const n = adjacentMines(board, cell)
296
+ return <Text color={NUMBER_COLORS[n]}>{n === 0 ? ' ' : String(n)}</Text>
297
+ }
298
+ const label = board.flagged.includes(cell) ? '⚑' : '■'
299
+ if (isOver) return <Text dimColor>{label}</Text>
300
+ return (
301
+ <Button key={`cell:${cell}`} plain onPress={() => pressCell($, cell)}>
302
+ <Text color={label === '⚑' ? '#e0775a' : '#6b7280'}>{label}</Text>
303
+ </Button>
304
+ )
305
+ }
262
306
 
263
- const status = board.status === 'won'
264
- ? <Text color="#7cc47f" bold>Cleared in {elapsed}s</Text>
265
- : board.status === 'lost'
266
- ? <Text color="#e06c6c" bold>Boom. {board.revealed.length} cells cleared</Text>
267
- : <Text dimColor>💣 {board.mineCount - board.flagged.length} · ⏱ {elapsed}s{record === null ? '' : ` · best ${record}s`}</Text>
307
+ const status = board.status === 'won'
308
+ ? <Text color="#7cc47f" bold>Cleared in {elapsed}s</Text>
309
+ : board.status === 'lost'
310
+ ? <Text color="#e06c6c" bold>Boom. {board.revealed.length} cells cleared</Text>
311
+ : <Text dimColor>💣 {board.mineCount - board.flagged.length} · ⏱ {elapsed}s{record === null ? '' : ` · best ${record}s`}</Text>
268
312
 
269
- return (
270
- <Box flexDirection="column" gap={1}>
271
- <Box flexDirection="row" justifyContent="space-between" alignItems="center">
272
- {status}
273
- <Box flexDirection="row" columnGap={1}>
274
- {isOver ? '' : <Button key="flag" label={board.isFlagMode ? '⚑ flagging' : '⚑ flag'} variant={board.isFlagMode ? 'primary' : 'secondary'} onPress={() => toggleFlagMode($)} />}
275
- <Button key="new" label="new game" onPress={() => startGame($)} />
313
+ return (
314
+ <Box flexDirection="column" gap={1}>
315
+ <Box flexDirection="row" justifyContent="space-between" alignItems="center">
316
+ {status}
317
+ <Box flexDirection="row" columnGap={1}>
318
+ {isOver ? '' : <Button key="flag" label={board.isFlagMode ? '⚑ flagging' : '⚑ flag'} variant={board.isFlagMode ? 'primary' : 'secondary'} onPress={() => toggleFlagMode($)} />}
319
+ <Button key="new" label="new game" onPress={() => startGame($)} />
320
+ </Box>
276
321
  </Box>
322
+ {grid(board.size, cellFace)}
277
323
  </Box>
278
- <Box flexDirection="column">{rows}</Box>
324
+ )
325
+ }
326
+
327
+ const board = await read($, game)
328
+ const round = await read($, whack)
329
+ const body = current === 'mole'
330
+ ? await moleView(round ?? newWhack())
331
+ : (board === null ? <Button key="new" label="new game" variant="primary" onPress={() => startGame($)} /> : await minesView(board))
332
+
333
+ return (
334
+ <Box flexDirection="column" gap={1}>
335
+ {tabs}
336
+ {body}
279
337
  </Box>
280
338
  )
281
339
  })
@@ -14,8 +14,25 @@ export type Game = {
14
14
  endedAt?: number
15
15
  }
16
16
 
17
+ /** A whack-a-mole game; holes are numbered row by row from 0. */
18
+ export type Whack = {
19
+ size: number
20
+ /** Hole the mole is up in; `null` before the start and after the end */
21
+ mole: number | null
22
+ /** Counts the moles put up, so a timer knows whether its mole is still the one up */
23
+ moleId: number
24
+ score: number
25
+ lives: number
26
+ /** How long the current mole stays up; shrinks with each hit */
27
+ windowMs: number
28
+ status: 'ready' | 'playing' | 'over'
29
+ }
30
+
31
+ /** The game the pane shows. */
32
+ export type View = 'mines' | 'mole'
33
+
17
34
  declare module 'claude-code' {
18
35
  interface PluginState {
19
- 'mini-game': { game: Game | null; best: number | null }
36
+ 'mini-game': { view: View; game: Game | null; best: number | null; whack: Whack | null; whackBest: number | null }
20
37
  }
21
38
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "panes": [
3
+ {
4
+ "id": "mini-game",
5
+ "title": "Mini game"
6
+ }
7
+ ]
8
+ }
@@ -5,5 +5,13 @@
5
5
  "author": {
6
6
  "name": "g3ntrovert"
7
7
  },
8
- "types": "./types/index.d.ts"
8
+ "types": "./types/index.d.ts",
9
+ "userConfig": {
10
+ "showInClaudeCode": {
11
+ "type": "boolean",
12
+ "title": "Show in Claude Code",
13
+ "default": false,
14
+ "description": "Open this mod's pane in the Claude Code chat when a session starts. The mods viewer shows it either way."
15
+ }
16
+ }
9
17
  }
@@ -12,6 +12,19 @@ const selected = atom({ plugin: 'tool-timeline', key: 'selected' } as const, '')
12
12
  /** Calls kept in state; older ones fall off. */
13
13
  const KEEP = 100
14
14
 
15
+ /** Prop the mods viewer adds to every pane it draws; the Claude Code extension draws on the `vscode` surface too, without it. */
16
+ const VIEWER_PROP = 'g3nViewer'
17
+
18
+ /**
19
+ * Tells whether the mods viewer asked for this drawing, rather than the Claude Code extension on the same surface.
20
+ *
21
+ * @param props - The `Pane` props of the `ui.render` argument.
22
+ * @returns `true` when the viewer's prop is set.
23
+ */
24
+ function isViewer(props: object): boolean {
25
+ return (props as Readonly<Record<string, unknown>>)[VIEWER_PROP] === true
26
+ }
27
+
15
28
  /** Approximate pixel width of one cell of the editor's code font. */
16
29
  const CELL_PX = 7
17
30
 
@@ -250,11 +263,12 @@ async function toggleSelected($: EngineInterface, id: string): Promise<void> {
250
263
  * @brief Subscribes the timeline's handlers to the engine's events, its command and its pane.
251
264
  *
252
265
  * @param on - Subscribes a handler to one engine event, optionally filtered.
266
+ * @param options - Plugin options; `showInClaudeCode` opens the pane in the Claude Code chat at session start.
253
267
  */
254
- export const register: Register = on => {
268
+ export const register: Register = (on, options) => {
255
269
  on('session.start', async ($, e, next) => {
256
- await $.command.register({ name: 'tool-timeline', description: 'Show or hide the tool call timeline' })
257
- void $.ui.open({ id: PANE, title: TITLE })
270
+ await $.command.register({ name: 'tool-timeline', description: 'Show or hide the tool call timeline in the Claude Code chat' })
271
+ if (options.showInClaudeCode === true) void $.ui.open({ id: PANE, title: TITLE })
258
272
  $.clock.every(1000, () => void tick($))
259
273
  return next(e)
260
274
  })
@@ -329,7 +343,7 @@ export const register: Register = on => {
329
343
  const axis = placeCalls(shown, now)
330
344
 
331
345
  // The viewer draws the timeline with the mod's own view: hand it the calls as data.
332
- if (e.surface === 'vscode') {
346
+ if (e.surface === 'vscode' && isViewer(e.props)) {
333
347
  const { Input } = $.ui.resolve(e)
334
348
  const spans = screenSpans(list, now)
335
349
  const data: TimelineViewData = {
@@ -0,0 +1,8 @@
1
+ {
2
+ "panes": [
3
+ {
4
+ "id": "tool-timeline",
5
+ "title": "Tool timeline"
6
+ }
7
+ ]
8
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "you-should-know",
3
3
  "version": "0.1.0",
4
- "description": "A card under long replies that flags their key point: a headline and one supporting line.",
4
+ "description": "A compact card under a reply: the questions it asks you word for word, and for long replies what changed and what it concluded, each line tagged.",
5
5
  "author": {
6
6
  "name": "g3ntrovert"
7
7
  },
@@ -10,8 +10,8 @@
10
10
  "minChars": {
11
11
  "type": "number",
12
12
  "title": "Minimum reply length",
13
- "default": 1500,
14
- "description": "Replies shorter than this many characters get no card"
13
+ "default": 800,
14
+ "description": "Replies shorter than this many characters get no changes, conclusions or facts; their questions still show"
15
15
  },
16
16
  "model": {
17
17
  "type": "string",