@g3nomaly/g3n-mods 0.1.2 → 0.1.3

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.
@@ -157,6 +157,7 @@ export const register: Register = on => {
157
157
  on('session.start', async ($, e, next) => {
158
158
  await $.command.register({ name: 'agents-panel', description: 'Show or hide the agents panel' })
159
159
  await loadAgents($)
160
+ void $.ui.open({ id: PANE, title: TITLE })
160
161
  $.clock.every(1000, () => void pollRunning($))
161
162
  return next(e)
162
163
  })
@@ -340,6 +340,7 @@ export const register: Register = on => {
340
340
  on('session.start', async ($, e, next) => {
341
341
  await $.command.register({ name: 'crabs', description: 'Show or hide the subagent crabs' })
342
342
  await readWindow($)
343
+ void $.ui.open({ id: PANE, title: TITLE })
343
344
  $.clock.every(1000, () => void tick($))
344
345
  return next(e)
345
346
  })
@@ -122,6 +122,7 @@ export const register: Register = (on, options) => {
122
122
  description: `Start a focus block (${defaultMinutes} min by default), or end the running one`,
123
123
  argumentHint: '[minutes] | end',
124
124
  })
125
+ void $.ui.open({ id: PANE, title: TITLE })
125
126
  $.clock.every(1000, () => void tick($))
126
127
  return next(e)
127
128
  })
@@ -187,6 +187,7 @@ export const register: Register = on => {
187
187
  await $.command.register({ name: 'mini-game', description: 'Play Minesweeper while Claude works' })
188
188
  const stored = await $.store.get(BEST_KEY)
189
189
  if (typeof stored === 'number') await update($, best, () => stored)
190
+ void $.ui.open({ id: PANE, title: TITLE })
190
191
  $.clock.every(1000, () => void tick($))
191
192
  return next(e)
192
193
  })
@@ -28,6 +28,15 @@ const TOOL_COLORS: ReadonlyArray<[RegExp, string]> = [
28
28
  [/^(Agent|Task)$/, '#f09bb5'],
29
29
  ]
30
30
 
31
+ /** Background of a row's hover card. */
32
+ const CARD = '#1b1f27'
33
+
34
+ /** Idle time between calls, in milliseconds, kept on the bars' axis; longer idle stretches shrink to this. */
35
+ const IDLE_MS = 200
36
+
37
+ /** Most characters of a call's input the hover card keeps. */
38
+ const DETAIL_MAX = 2000
39
+
31
40
  /** Input fields that say most about a call, tried in order. */
32
41
  const LABEL_FIELDS = ['command', 'file_path', 'notebook_path', 'pattern', 'url', 'query', 'description', 'skill', 'prompt'] as const
33
42
 
@@ -59,6 +68,47 @@ export function labelOf(input: Record<string, unknown>): string {
59
68
  return first?.replace(/\s+/g, ' ').trim() ?? ''
60
69
  }
61
70
 
71
+ /**
72
+ * @brief Finds a call's whole input text for the row's hover card: the full command, path, pattern or first text field.
73
+ *
74
+ * @param input - Arguments of the tool call.
75
+ * @returns The text with its line breaks, cut at `DETAIL_MAX` characters.
76
+ */
77
+ export function detailOf(input: Record<string, unknown>): string {
78
+ const field = LABEL_FIELDS.find(name => typeof input[name] === 'string' && input[name] !== '')
79
+ const value = field === undefined ? Object.values(input).find((one): one is string => typeof one === 'string') : (input[field] as string)
80
+ const text = value?.trim() ?? ''
81
+ return text.length > DETAIL_MAX ? `${text.slice(0, DETAIL_MAX - 1)}…` : text
82
+ }
83
+
84
+ /** Where a call sits on the bars' axis, in milliseconds from the first shown call's start. */
85
+ type Span = { from: number; to: number }
86
+
87
+ /**
88
+ * @brief Places calls on one time axis, shrinking idle stretches between them to `IDLE_MS`, so the bars read as a waterfall.
89
+ *
90
+ * @param list - Calls in start order.
91
+ * @param now - Current time, the end of calls still running.
92
+ * @returns Each call's span, in `list` order, and the axis length (at least 1).
93
+ */
94
+ export function placeCalls(list: readonly TimelineCall[], now: number): { spans: Span[]; length: number } {
95
+ const origin = list[0]?.startedAt ?? now
96
+ let busyUntil = origin
97
+ let shift = 0
98
+ let length = 1
99
+ const spans = list.map(call => {
100
+ const end = Math.max(call.startedAt, call.endedAt ?? now)
101
+ const idle = call.startedAt - busyUntil
102
+ if (idle > IDLE_MS) shift += idle - IDLE_MS
103
+ busyUntil = Math.max(busyUntil, end)
104
+ const from = call.startedAt - origin - shift
105
+ const to = end - origin - shift
106
+ length = Math.max(length, to)
107
+ return { from, to }
108
+ })
109
+ return { spans, length }
110
+ }
111
+
62
112
  /**
63
113
  * @brief Joins the text of a tool result's content, which is a string or a list of text blocks.
64
114
  *
@@ -146,16 +196,16 @@ function replacementOf(list: readonly TimelineCall[], index: number): TimelineCa
146
196
  }
147
197
 
148
198
  /**
149
- * @brief Builds the SVG of a track with a bar filled to `fill` pixels.
199
+ * @brief Builds the SVG of a track with a bar from `x` that is `fill` pixels long.
150
200
  *
151
201
  * @param width - Width of the track in pixels.
152
- * @param fill - Filled length in pixels, clamped to the width.
202
+ * @param x - Start of the bar in pixels.
203
+ * @param fill - Length of the bar in pixels.
153
204
  * @param color - Fill color.
154
205
  * @returns The SVG markup.
155
206
  */
156
- function barSvg(width: number, fill: number, color: string): string {
157
- const w = Math.max(0, Math.min(width, fill))
158
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="8" viewBox="0 0 ${width} 8"><rect width="${width}" height="8" rx="2" fill="${TRACK}"/><rect width="${w.toFixed(1)}" height="8" rx="2" fill="${color}"/></svg>`
207
+ function barSvg(width: number, x: number, fill: number, color: string): string {
208
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="8" viewBox="0 0 ${width} 8"><rect width="${width}" height="8" rx="2" fill="${TRACK}"/><rect x="${x.toFixed(1)}" width="${fill.toFixed(1)}" height="8" rx="2" fill="${color}"/></svg>`
159
209
  }
160
210
 
161
211
  /**
@@ -182,12 +232,15 @@ export const register: Register = on => {
182
232
 
183
233
  on('tool.call', async ($, e, next) => {
184
234
  const startedAt = await $.clock.now()
185
- const label = labelOf(e as unknown as Record<string, unknown>)
235
+ const input = e as unknown as Record<string, unknown>
236
+ const label = labelOf(input)
237
+ const detail = detailOf(input)
186
238
  await patchCall($, e.tool_use_id, call => ({
187
239
  ...call,
188
240
  id: e.tool_use_id,
189
241
  tool: e.tool,
190
242
  label,
243
+ detail,
191
244
  status: 'running',
192
245
  startedAt,
193
246
  agentId: e.agentId,
@@ -209,7 +262,7 @@ export const register: Register = on => {
209
262
  const input = typeof block.input === 'object' && block.input !== null ? (block.input as Record<string, unknown>) : {}
210
263
  const id = block.id
211
264
  const tool = block.name
212
- await patchCall($, id, call => call ?? { id, tool, label: labelOf(input), status: 'queued', startedAt: now, agentId: e.agentId })
265
+ await patchCall($, id, call => call ?? { id, tool, label: labelOf(input), detail: detailOf(input), status: 'queued', startedAt: now, agentId: e.agentId })
213
266
  }
214
267
  }
215
268
  if (e.door === 'tool-result') {
@@ -236,27 +289,32 @@ export const register: Register = on => {
236
289
  const shown = list.slice(-room)
237
290
  /** Length of a call so far, or up to its end when it has one. */
238
291
  const durationOf = (call: TimelineCall): number => (call.endedAt ?? now) - call.startedAt
239
- const longest = Math.max(1, ...shown.map(durationOf))
240
292
  const total = list.reduce((sum, call) => sum + (call.status === 'denied' ? 0 : durationOf(call)), 0)
293
+ const axis = placeCalls(shown, now)
241
294
  const barCells = Math.max(8, Math.floor(e.props.bodyColumns * 0.3))
242
295
  const labelCells = Math.max(12, e.props.bodyColumns - barCells - 12)
243
296
 
244
- /** Draws the bar showing a call's share of the longest call's time. */
245
- const bar = (call: TimelineCall) => {
297
+ /** Draws a call's bar on the shared axis: where it started and how long it ran, idle time between calls shrunk. */
298
+ const bar = (call: TimelineCall, span: Span) => {
246
299
  const color = call.status === 'error' ? ERROR : call.status === 'ok' && /^Bash$/.test(call.tool) ? '#5fae6e' : colorOf(call.tool)
247
- const share = call.status === 'denied' ? 0.04 : Math.max(0.03, durationOf(call) / longest)
300
+ const start = span.from / axis.length
301
+ const share = call.status === 'denied' ? 0.03 : Math.max(0.02, (span.to - span.from) / axis.length)
248
302
  if (e.surface === 'terminal') {
249
303
  const filled = Math.max(1, Math.round(share * barCells))
304
+ const before = Math.min(barCells - filled, Math.round(start * barCells))
250
305
  return (
251
306
  <Text>
307
+ <Text color={TRACK}>{'▬'.repeat(before)}</Text>
252
308
  <Text color={color}>{'▬'.repeat(filled)}</Text>
253
- <Text color={TRACK}>{'▬'.repeat(barCells - filled)}</Text>
309
+ <Text color={TRACK}>{'▬'.repeat(barCells - before - filled)}</Text>
254
310
  </Text>
255
311
  )
256
312
  }
257
313
  const { Svg } = $.ui.resolve(e)
258
314
  const width = barCells * CELL_PX
259
- return <Svg source={barSvg(width, share * width, color)} alt={`${call.tool} ${seconds(durationOf(call))}`} width={width} height={8} />
315
+ const fill = Math.max(3, share * width)
316
+ const x = Math.min(width - fill, start * width)
317
+ return <Svg source={barSvg(width, x, fill, color)} alt={`${call.tool} ${seconds(durationOf(call))}`} width={width} height={8} />
260
318
  }
261
319
 
262
320
  /** Renders a call's status at the end of its row: its time, `denied`, `queued` or a mark for an error. */
@@ -270,32 +328,37 @@ export const register: Register = on => {
270
328
  }
271
329
  }
272
330
 
273
- const rows = shown.map((call, index) => {
274
- const replacement = call.status === 'denied' ? replacementOf(shown, index) : undefined
275
- return (
276
- <Box flexDirection="column">
277
- <Box flexDirection="row" columnGap={1}>
278
- <Text bold color="#1b1f27" backgroundColor={colorOf(call.tool)}> {badgeOf(call.tool)} </Text>
279
- <Box width={labelCells}>
280
- <Text wrap="truncate-end" strikethrough={call.status === 'denied'} color={call.status === 'denied' ? ERROR : undefined}>
281
- {call.agentId === undefined ? '' : '↳ '}{call.tool} <Text dimColor>{call.label}</Text>
282
- </Text>
283
- </Box>
284
- {bar(call)}
285
- <Box width={8} justifyContent="flex-end">{outcome(call)}</Box>
286
- </Box>
287
- {call.status === 'denied' && (
288
- <Box borderStyle="round" borderColor="#7a3b3b" paddingX={1} flexDirection="row" justifyContent="space-between">
289
- <Text wrap="truncate-end">
290
- <Text color={ERROR}>⊘ </Text>
291
- <Text strikethrough dimColor>{call.label}</Text>
292
- {replacement === undefined ? '' : <Text> → <Text color="#7cc47f">{replacement.label}</Text></Text>}
293
- </Text>
294
- <Text color={ERROR}>denied{call.note === undefined ? '' : ` · ${shortNote(call.note)}`}</Text>
295
- </Box>
296
- )}
331
+ // One line per call; the keyed row reveals a card with the call's whole input while the pointer is on it.
332
+ const rows = shown.map((call, index) => (
333
+ <Box key={call.id} flexDirection="row" columnGap={1}>
334
+ <Text bold color={CARD} backgroundColor={colorOf(call.tool)}> {badgeOf(call.tool)} </Text>
335
+ <Box width={labelCells}>
336
+ <Text wrap="truncate-end" strikethrough={call.status === 'denied'} color={call.status === 'denied' ? ERROR : undefined}>
337
+ {call.agentId === undefined ? '' : '↳ '}{call.tool} <Text dimColor>{call.label}</Text>
338
+ </Text>
339
+ </Box>
340
+ {bar(call, axis.spans[index] ?? { from: 0, to: 0 })}
341
+ <Box width={8} justifyContent="flex-end">{outcome(call)}</Box>
342
+ <Box position="absolute" top={1} left={4} right={0} display="none" hover={{ display: 'flex' }} backgroundColor={CARD} borderStyle="round" borderColor={DIM} paddingX={1}>
343
+ <Text wrap="wrap">{call.tool} <Text dimColor>{call.detail ?? call.label}</Text></Text>
297
344
  </Box>
298
- )
345
+ </Box>
346
+ ))
347
+
348
+ // Denied calls get their callout under the list, so every call keeps to its one line.
349
+ const callouts = shown.flatMap((call, index) => {
350
+ if (call.status !== 'denied') return []
351
+ const replacement = replacementOf(shown, index)
352
+ return [
353
+ <Box borderStyle="round" borderColor="#7a3b3b" paddingX={1} flexDirection="row" justifyContent="space-between">
354
+ <Text wrap="truncate-end">
355
+ <Text color={ERROR}>⊘ </Text>
356
+ <Text strikethrough dimColor>{call.label}</Text>
357
+ {replacement === undefined ? '' : <Text> → <Text color="#7cc47f">{replacement.label}</Text></Text>}
358
+ </Text>
359
+ <Text color={ERROR}>denied{call.note === undefined ? '' : ` · ${shortNote(call.note)}`}</Text>
360
+ </Box>,
361
+ ]
299
362
  })
300
363
 
301
364
  return (
@@ -305,6 +368,7 @@ export const register: Register = on => {
305
368
  <Text dimColor>{list.length} call{list.length === 1 ? '' : 's'} · {seconds(total)}</Text>
306
369
  </Box>
307
370
  {rows}
371
+ {callouts.length > 0 && <Box flexDirection="column" marginTop={1}>{callouts}</Box>}
308
372
  </Box>
309
373
  )
310
374
  })
@@ -9,6 +9,8 @@ export type TimelineCall = {
9
9
  tool: string
10
10
  /** Short form of the call's input, e.g. `rm -rf dist` or `package.json` */
11
11
  label: string
12
+ /** The call's whole input text for the row's hover card, e.g. the full command or path */
13
+ detail?: string
12
14
  status: CallStatus
13
15
  startedAt: number
14
16
  endedAt?: number
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@g3nomaly/g3n-mods",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "The g3n Claude Code mods: panes, status lines, commands and hooks, installed with @g3nomaly/mods.",
5
5
  "type": "module",
6
6
  "files": [