@g3nomaly/g3n-mods 0.1.2 → 0.1.4

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.
@@ -19,19 +19,19 @@
19
19
  {
20
20
  "name": "custom-crab-agents",
21
21
  "source": "./mods/custom-crab-agents",
22
- "description": "Each subagent as a crab with a hat: effort tier, model, steps, context, tokens, cost and time.",
22
+ "description": "Draws each subagent in the agents panel as a crab with a hat: effort tier, model, steps, context, tokens, cost and time.",
23
23
  "version": "0.1.0"
24
24
  },
25
25
  {
26
26
  "name": "delete-guard",
27
27
  "source": "./mods/delete-guard",
28
- "description": "Denies rm -rf and other permanent deletes and points Claude to the trash.",
28
+ "description": "Holds rm -rf and other permanent deletes on a card that lists what they remove, until you press Delete or Cancel.",
29
29
  "version": "0.1.0"
30
30
  },
31
31
  {
32
32
  "name": "focus-mode",
33
33
  "source": "./mods/focus-mode",
34
- "description": "A timed focus block with a countdown, a progress bar and an end button.",
34
+ "description": "A timed focus block in which you're away: Claude asks no questions and gets no permission prompts. Countdown, progress bar and end button.",
35
35
  "version": "0.1.0"
36
36
  },
37
37
  {
package/README.md CHANGED
@@ -22,16 +22,16 @@ Then open the project in VS Code and trust the workspace. New sessions load the
22
22
  |---|---|---|
23
23
  | `agents-panel` | The agents in `.claude/agents` with their model and a run button. A running agent shows a spinner, and a toast says when it starts and finishes | `/agents-panel` |
24
24
  | `context-bar` | One bar of context-window use split by source, with the total and where compaction starts | `/context-bar` |
25
- | `custom-crab-agents` | Each subagent as a crab with a hat: effort tier, model, steps, context, tokens, cost and time | `/crabs` |
26
- | `delete-guard` | Denies `rm -rf` and other permanent deletes and tells Claude which trash command to use instead | `/delete-guard on \| off` |
27
- | `focus-mode` | A timed focus block with a countdown, a progress bar and an end button. 25 minutes unless you pass a number | `/focus-mode [minutes] \| end` |
25
+ | `custom-crab-agents` | Adds each subagent to the agents panel as a crab with a hat: effort tier, model, steps, context, tokens, cost and time. Needs `agents-panel`, which installing it adds | `/agents-panel` |
26
+ | `delete-guard` | Off by default: deletes run unasked. Turn it on with the switch in its pane, or `/delete-guard on`; it stays on in later sessions. While on, `rm -rf` and other permanent deletes wait on a card listing the files, their sizes and whether git can restore them, until you press **Delete** or **Cancel**. Cancel tells Claude which trash command to use instead. With nothing to draw the card on, the delete is refused | `/delete-guard on \| off` |
27
+ | `focus-mode` | A timed focus block in which you're away: Claude's questions are refused, so it picks for itself and says what it assumed, and permission prompts are refused, so it works around the step and lists it for later. Countdown, progress bar and end button. 25 minutes unless you pass a number | `/focus-mode [minutes] \| end` |
28
28
  | `handoff` | Writes a handoff with the `handoff` skill, clears the session, and attaches the handoff to your next prompt | `/handoff-new [what the next session is for]` |
29
29
  | `hide-personal-data` | Masks names, emails, phone numbers, IBANs and card numbers in replies. Hover a mask to read it | `/hide-personal-data on \| off` |
30
30
  | `mini-game` | Minesweeper, to play while Claude works | `/mini-game` |
31
31
  | `tool-timeline` | Each tool call with its duration. A denied call is struck through, next to the call that replaced it | `/tool-timeline` |
32
32
  | `you-should-know` | A card under long replies with their key point, a headline and one supporting line. It costs one small Haiku call per reply of 1500 characters or more | none |
33
33
 
34
- For `agents-panel`, `context-bar`, `custom-crab-agents` and `tool-timeline`, the command opens the pane and running it again closes it.
34
+ For `agents-panel`, `context-bar` and `tool-timeline`, the command opens the pane and running it again closes it.
35
35
 
36
36
  Three mods take settings, declared under `userConfig` in their `.claude-plugin/plugin.json`:
37
37
 
@@ -1,6 +1,6 @@
1
1
  // packages/g3n-mods/mods/agents-panel/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, RenderElement } from 'claude-code'
4
4
 
5
5
  import type { AgentDef, RunningAgent } from '../types'
6
6
 
@@ -20,6 +20,27 @@ const NAMED_COLORS: Readonly<Record<string, string>> = {
20
20
  const SPINNER = ['◐', '◓', '◑', '◒']
21
21
  const DONE_STATUSES = new Set(['completed', 'failed', 'killed', 'idle'])
22
22
 
23
+ /** Key prefix of the panel's own elements; an add-on looks for it to find the panel's tree. */
24
+ export const OWN_KEY = 'agents:'
25
+
26
+ /** Key prefix an add-on's section carries, such as custom-crab-agents' crabs, so the panel knows to show it. */
27
+ export const ADDON_KEY = 'addon:'
28
+
29
+ /**
30
+ * Tells whether a drawn tree holds an element whose key starts with `prefix`.
31
+ *
32
+ * @param tree - Tree as a `ui.render` chain returned it, or one of its children.
33
+ * @param prefix - Key prefix to look for, e.g. `addon:`.
34
+ * @returns True when some element's key starts with `prefix`.
35
+ */
36
+ export function holdsKey(tree: unknown, prefix: string): boolean {
37
+ if (Array.isArray(tree)) return tree.some(child => holdsKey(child, prefix))
38
+ if (typeof tree !== 'object' || tree === null) return false
39
+ const { props, children } = tree as { props?: { key?: unknown }; children?: unknown }
40
+ if (typeof props?.key === 'string' && props.key.startsWith(prefix)) return true
41
+ return holdsKey(children, prefix)
42
+ }
43
+
23
44
  /**
24
45
  * Parses the frontmatter of a Markdown file into flat `key: value` pairs.
25
46
  *
@@ -157,6 +178,7 @@ export const register: Register = on => {
157
178
  on('session.start', async ($, e, next) => {
158
179
  await $.command.register({ name: 'agents-panel', description: 'Show or hide the agents panel' })
159
180
  await loadAgents($)
181
+ void $.ui.open({ id: PANE, title: TITLE })
160
182
  $.clock.every(1000, () => void pollRunning($))
161
183
  return next(e)
162
184
  })
@@ -183,8 +205,13 @@ export const register: Register = on => {
183
205
  return result
184
206
  }).catch(($, e, next) => next(e))
185
207
 
186
- on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
208
+ on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e, next) => {
187
209
  const { Box, Text, Button } = $.ui.resolve(e)
210
+ // An add-on that depends on this panel, such as custom-crab-agents, may draw beneath this hook; its section joins the panel.
211
+ const below: RenderElement | undefined = await next(e).catch(() => undefined)
212
+ const addOn = below !== undefined && holdsKey(below, ADDON_KEY) ? below : undefined
213
+ /** Puts an add-on's section under the panel's own tree, when one was drawn beneath. */
214
+ const withAddOn = (own: RenderElement) => (addOn === undefined ? own : <Box flexDirection="column" gap={1}>{own}{addOn}</Box>)
188
215
  const list = await read($, agents)
189
216
  const runs = await read($, running)
190
217
  const now = await $.clock.now()
@@ -202,11 +229,11 @@ export const register: Register = on => {
202
229
  )
203
230
 
204
231
  if (list.length === 0) {
205
- return (
206
- <Box flexDirection="column" gap={1}>
232
+ return withAddOn(
233
+ <Box key={`${OWN_KEY}body`} flexDirection="column" gap={1}>
207
234
  {header}
208
235
  <Text dimColor>No agents in {AGENTS_DIR} or ~/{AGENTS_DIR}. Add one with /agents.</Text>
209
- </Box>
236
+ </Box>,
210
237
  )
211
238
  }
212
239
 
@@ -264,14 +291,14 @@ export const register: Register = on => {
264
291
  )
265
292
  })()
266
293
 
267
- return (
268
- <Box flexDirection="column" gap={1}>
294
+ return withAddOn(
295
+ <Box key={`${OWN_KEY}body`} flexDirection="column" gap={1}>
269
296
  {header}
270
297
  {input}
271
298
  {section('project')}
272
299
  {section('user')}
273
300
  <Text dimColor>click ▶ run to start one · /agents-panel to hide</Text>
274
- </Box>
301
+ </Box>,
275
302
  )
276
303
  })
277
304
  }
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "custom-crab-agents",
3
3
  "version": "0.1.0",
4
- "description": "Each subagent as a crab with a hat: effort tier, model, steps, context, tokens, cost and time.",
4
+ "description": "Draws each subagent in the agents panel as a crab with a hat: effort tier, model, steps, context, tokens, cost and time.",
5
5
  "author": {
6
6
  "name": "g3ntrovert"
7
7
  },
8
- "types": "./types/index.d.ts"
8
+ "types": "./types/index.d.ts",
9
+ "dependencies": [
10
+ "agents-panel"
11
+ ]
9
12
  }
@@ -1,11 +1,15 @@
1
1
  // packages/g3n-mods/mods/custom-crab-agents/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, RenderElement } from 'claude-code'
4
4
 
5
5
  import type { Crab, CrabSteps, PlannedItem } from '../types'
6
6
 
7
- const PANE = 'crabs'
8
- const TITLE = 'Crabs'
7
+ /** The agents-panel mod's pane, which this mod depends on and draws its crabs into. */
8
+ const PANEL = 'agents-panel'
9
+
10
+ /** Key prefix of the agents panel's own elements, and the one this section carries so the panel shows it. */
11
+ const PANEL_KEY = 'agents:'
12
+ const ADDON_KEY = 'addon:'
9
13
  const crabs = atom({ plugin: 'custom-crab-agents', key: 'crabs' } as const, [])
10
14
  const planned = atom({ plugin: 'custom-crab-agents', key: 'planned' } as const, [])
11
15
  const contextWindow = atom({ plugin: 'custom-crab-agents', key: 'window' } as const, 1_000_000)
@@ -178,6 +182,21 @@ const isNumber = (v: unknown): v is number => typeof v === 'number'
178
182
  /** Tells whether `v` is a non-null object. */
179
183
  const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null
180
184
 
185
+ /**
186
+ * Tells whether a drawn tree holds an element whose key starts with `prefix`.
187
+ *
188
+ * @param tree - Tree as a `ui.render` chain returned it, or one of its children.
189
+ * @param prefix - Key prefix to look for, e.g. `agents:`.
190
+ * @returns True when some element's key starts with `prefix`.
191
+ */
192
+ export function holdsKey(tree: unknown, prefix: string): boolean {
193
+ if (Array.isArray(tree)) return tree.some(child => holdsKey(child, prefix))
194
+ if (typeof tree !== 'object' || tree === null) return false
195
+ const { props, children } = tree as { props?: { key?: unknown }; children?: unknown }
196
+ if (typeof props?.key === 'string' && props.key.startsWith(prefix)) return true
197
+ return holdsKey(children, prefix)
198
+ }
199
+
181
200
  /**
182
201
  * Applies `patch` to every crab that `match` selects.
183
202
  *
@@ -190,14 +209,13 @@ async function patchCrab($: EngineInterface, match: (crab: Crab) => boolean, pat
190
209
  }
191
210
 
192
211
  /**
193
- * Records a subagent the main thread just started and shows the pane.
212
+ * Records a subagent the main thread just started.
194
213
  *
195
214
  * @param $ - Engine handle the hook received, see `EngineInterface`.
196
215
  * @param crab - New running crab to add, see `Crab`.
197
216
  */
198
217
  async function startCrab($: EngineInterface, crab: Crab): Promise<void> {
199
218
  await update($, crabs, list => [...list.filter(one => one.id !== crab.id), crab].slice(-KEEP))
200
- void $.ui.open({ id: PANE, title: TITLE })
201
219
  }
202
220
 
203
221
  /**
@@ -332,27 +350,17 @@ async function readWindow($: EngineInterface): Promise<void> {
332
350
  }
333
351
 
334
352
  /**
335
- * Registers this mod's `crabs` command, its session, tool, subagent and turn hooks, and its pane.
353
+ * Registers this mod's session, tool, subagent and turn hooks, and its section of the agents panel.
336
354
  *
337
355
  * @param on - Hook registration function the engine passes in.
338
356
  */
339
357
  export const register: Register = on => {
340
358
  on('session.start', async ($, e, next) => {
341
- await $.command.register({ name: 'crabs', description: 'Show or hide the subagent crabs' })
342
359
  await readWindow($)
343
360
  $.clock.every(1000, () => void tick($))
344
361
  return next(e)
345
362
  })
346
363
 
347
- on('command.run', { command: 'crabs' }, async $ => {
348
- if ((await $.ui.panes()).some(pane => pane.id === PANE)) {
349
- await $.ui.close({ id: PANE })
350
- return { text: 'Crabs hidden.' }
351
- }
352
- await $.ui.open({ id: PANE, title: TITLE })
353
- return { text: 'Crabs shown.' }
354
- })
355
-
356
364
  on('tool.call', { tool: 'Agent' }, async ($, e, next) => {
357
365
  if (e.agentId !== undefined) return next(e)
358
366
  const startedAt = await $.clock.now()
@@ -398,13 +406,22 @@ export const register: Register = on => {
398
406
  return result
399
407
  }).catch(($, e, next) => next(e))
400
408
 
401
- on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
409
+ // Draws into the agents panel: under the panel's tree when the panel's hook lies beneath this one, or alone with an
410
+ // `addon:` key that the panel's hook picks up when it lies above. Either way the crabs end up under the agents.
411
+ on('ui.render', { component: 'Pane', requestId: PANEL }, async ($, e, next) => {
402
412
  const { Box, Text } = $.ui.resolve(e)
413
+ const below: RenderElement | undefined = await next(e).catch(() => undefined)
414
+ const panel = below !== undefined && holdsKey(below, PANEL_KEY) ? below : undefined
415
+ /** Puts the crabs' section under the panel's tree, when it was drawn beneath. */
416
+ const joined = (section: RenderElement) => (panel === undefined ? section : <Box flexDirection="column" gap={1}>{panel}{section}</Box>)
417
+
403
418
  const list = await read($, crabs)
404
419
  const plan = await read($, planned)
405
420
  const window = await read($, contextWindow)
406
421
  const now = await $.clock.now()
407
- if (list.length === 0 && plan.length === 0) return <Text dimColor>No subagents yet. Each one shows up here as a crab.</Text>
422
+ if (list.length === 0 && plan.length === 0) {
423
+ return joined(<Box key={`${ADDON_KEY}crabs`}><Text dimColor>No subagents yet. Each one shows up here as a crab.</Text></Box>)
424
+ }
408
425
 
409
426
  const barWidth = Math.max(120, (e.props.bodyColumns - 10) * 7)
410
427
  /**
@@ -484,8 +501,9 @@ export const register: Register = on => {
484
501
  const running = list.filter(crab => crab.status === 'running')
485
502
  const done = list.filter(crab => crab.status !== 'running')
486
503
 
487
- return (
488
- <Box flexDirection="column">
504
+ return joined(
505
+ <Box key={`${ADDON_KEY}crabs`} flexDirection="column">
506
+ <Text bold>Subagents</Text>
489
507
  {running.map(card)}
490
508
  {done.length > 0 && <Text dimColor>▾ Finished · {done.length}</Text>}
491
509
  {done.slice().reverse().map(card)}
@@ -502,7 +520,7 @@ export const register: Register = on => {
502
520
  </Box>
503
521
  </Box>
504
522
  ))}
505
- </Box>
523
+ </Box>,
506
524
  )
507
525
  })
508
526
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "delete-guard",
3
3
  "version": "0.1.0",
4
- "description": "Denies rm -rf and other permanent deletes and points Claude to the trash.",
4
+ "description": "Holds rm -rf and other permanent deletes on a card that lists what they remove, until you press Delete or Cancel.",
5
5
  "author": {
6
6
  "name": "g3ntrovert"
7
7
  },
@@ -1 +1 @@
1
- { "modules": ["./register.ts"] }
1
+ { "modules": ["./register.tsx"] }