eklavya 1.87.0 → 1.88.0

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eklavya",
3
3
  "displayName": "Eklavya",
4
- "version": "1.87.0",
4
+ "version": "1.88.0",
5
5
  "description": "Learn while your agent works. Turns coding-agent generation time into adaptive, Socratic learning grounded in the code being written.",
6
6
  "author": {
7
7
  "name": "Ajay Kumar"
@@ -56,7 +56,7 @@ returns `{disabled: true}` and the mod opens and draws nothing. `presentation` i
56
56
  the Stop sweep and the delegation lines (`delegation-lib.ts`) read it, and an
57
57
  open panel question silences the checkpoint and the Stop sweep. Read
58
58
  `docs/verified-schemas.md` (mods section) before changing anything it records.
59
- `/eklavya-panel-demo` (`DEMO_COMMAND`) is the maintainer's shortcut for trying the pane: it seeds `PanelState.demo` with a mock question and never calls the server, so it works with `quiz.panel` off. While `demo` is set the heartbeat (`beat`) does not sync, `submit` grades locally with `demoResult` (no `panel_answer`, grader or explainer) and Next restarts the mock; `/eklavya-panel` and a real `present_question` sync replace it. On a non-terminal surface it also offers the Answer cards switch: `button` is the production layout, `client` draws each option as a `Client` region from `card.tsx` that posts `{pick}` to the `ui.message` hook, which accepts only an id of the open question. The switch is demo-only until the whole-card press is observed on Desktop; then make it the default and say so in `docs/verified-schemas.md`.
59
+ `/eklavya-panel-demo` (`DEMO_COMMAND`) is the maintainer's shortcut for trying the pane: it seeds `PanelState.demo` with a mock question and never calls the server, so it works with `quiz.panel` off. While `demo` is set the heartbeat (`beat`) does not sync, `submit` grades locally with `demoResult` (no `panel_answer`, grader or explainer) and Next restarts the mock; `/eklavya-panel` and a real `present_question` sync replace it. On a non-terminal surface it also offers the Answer cards switch: `button` is the production layout, `client` draws each option as a `Client` region from `card.tsx` that posts `{pick}` to the `ui.message` hook, which accepts only an id of the open question. Under the Teach me button the demo also draws an experiment box (`experiment` in `register.tsx`): `Image` with `DEMO_PNG` on the terminal, two `Svg` (`demoSvg`, `demoSvgWithPng`) on every other surface, never both, because a tree holding an element its surface lacks is replaced whole by the host's own pane. The switch is demo-only until the whole-card press is observed on Desktop; then make it the default and say so in `docs/verified-schemas.md`.
60
60
  Run `scripts/test-panel-mod.sh`: it stages the mod as its own plugin, validates
61
61
  it, runs `claude plugin test`, then validates the distributed plugin.
62
62
 
@@ -171,6 +171,11 @@ export const STR = {
171
171
  demoStyleClient: 'Whole card',
172
172
  demoExplanation: 'A rename inside one folder is atomic: the poller sees the finished file or none, never half of it. (This is a mock, so it was not recorded.)',
173
173
  demoTyped: 'Typed answers are not graded in the demo. (Nothing was sent or recorded.)',
174
+ experimentBanner: 'EXPERIMENT · picture test. Nothing here is part of a real question.',
175
+ experimentTerminal: 'Image: a PNG, drawn as a picture in kitty, Ghostty and iTerm2 (fullscreen); other terminals show the dim text instead. SVG is not drawn in the terminal.',
176
+ experimentImageAlt: 'The Eklavya mark: the Claude mascot holding a bow and arrow',
177
+ experimentSvg: 'SVG: the mark drawn as vector rectangles.',
178
+ experimentSvgPng: 'Image: the same mark as a PNG inside an SVG (Desktop has no Image element).',
174
179
  } as const
175
180
 
176
181
  /** The explainer agent the panel starts itself, so no prompt lands in the transcript. */
@@ -241,6 +246,29 @@ export function demoResult(kind: 'choice' | 'text' | 'skip' | 'dont_know', optio
241
246
  return { phase: 'answered', attempt_id: 0, correct, correct_label: right, explanation: kind === 'text' ? STR.demoTyped : STR.demoExplanation }
242
247
  }
243
248
 
249
+ /** The logo as a PNG (208 by 96, one grid cell is 8 pixels), for the demo's picture test: the mod has no filesystem. */
250
+ export const DEMO_PNG =
251
+ 'iVBORw0KGgoAAAANSUhEUgAAANAAAABgCAYAAABsbvwBAAABcElEQVR42u3dwQ2DMBBFQXdJKdRKNaYDDqDFu+t50j8mUpDnZCUZQ3pofswTFEAASQBJAEkASQABJAEkASTlBJL9/SWAJIAAEkAASQBJAEkAASSAAJJyHnDABBBAAgggCSAJIIAEEEACCCAJoLpd5zFt3aof4O2BOcQAAQQQQAABBBBAABlAABlAAAEEEEAAAQRQY0Ddga5+ftUPaHtgAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAAB5CLUXk7BfyDmkAEkgAwggAwggAwggAwgAWQEAGQAJQbkHskX6lykuogFCCCADCCADCCAAAIIIIAAAgggAwggAwigCGDRr9/98/lhRYAAAggggAACCCCAAHLAAAIIIIAAAggggAACCCCA1LPVBxQQAQSQAAJIAkgCCCABBJAAAkgCSKp5gAERQABJAEkASQABJIAAkgCSAJJqAgNEAkgCSAJIAgggCSAJIAkgSZIkSZIkSZIk6dduN5j9QcRn8BgAAAAASUVORK5CYII='
252
+
253
+ /** The logo as SVG, one rect per run of a colour, so the demo can test a vector picture without an asset file. */
254
+ export function demoSvg(): string {
255
+ const rects = boxRows()
256
+ .flatMap((runs, y) => {
257
+ let x = 0
258
+ return runs.map(run => {
259
+ const at = x
260
+ x += run.width
261
+ return run.color ? `<rect x="${at}" y="${y}" width="${run.width}" height="1" fill="${run.color}"/>` : ''
262
+ })
263
+ })
264
+ .join('')
265
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${LOGO_GRID[0]!.length} ${LOGO_GRID.length}" shape-rendering="crispEdges">${rects}</svg>`
266
+ }
267
+
268
+ /** The same picture as a PNG inside an SVG: Desktop has no Image element, so this is how a raster is tried there. */
269
+ export const demoSvgWithPng = (): string =>
270
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 208 96"><image href="data:image/png;base64,${DEMO_PNG}" width="208" height="96"/></svg>`
271
+
244
272
  /** The tester's line of what was just clicked, and how the click arrived. */
245
273
  export const demoLast = (what: string): string => `Last click: ${what}`
246
274
 
@@ -3,6 +3,7 @@ import type { EngineInterface, Register } from 'claude-code'
3
3
 
4
4
  import {
5
5
  BRAND,
6
+ DEMO_PNG,
6
7
  PICKED,
7
8
  TEACH,
8
9
  EMPTY,
@@ -17,6 +18,8 @@ import {
17
18
  answerWas,
18
19
  demoLast,
19
20
  demoResult,
21
+ demoSvg,
22
+ demoSvgWithPng,
20
23
  explainerBrief,
21
24
  codify,
22
25
  gradingRequest,
@@ -414,9 +417,10 @@ export const register: Register = on => {
414
417
  on('ui.message', { component: 'Pane' }, async ($, e, next) => {
415
418
  if (e.requestId === PANE) {
416
419
  try {
420
+ const started = Date.now()
417
421
  const id = (e.data as { pick?: unknown } | null)?.pick
418
422
  const s: PanelState = (await $.state.get(REF)).value ?? EMPTY
419
- if (typeof id === 'string' && s.question?.options.some(o => o.id === id)) await pick($, id, 'whole card')
423
+ if (typeof id === 'string' && s.question?.options.some(o => o.id === id)) await pick($, id, 'whole card', started)
420
424
  } catch {
421
425
  /* Fail open: the card stays as it was. */
422
426
  }
@@ -438,7 +442,7 @@ export const register: Register = on => {
438
442
  on('ui.render', { component: 'Pane', requestId: 'eklavya-quiz' }, async ($, e, next) => {
439
443
  // Only our pane: every other pane, and everything else, is the host's.
440
444
  if (e.requestId !== PANE) return next(e)
441
- const { Box, Text, Button, Input, Markdown, Client } = $.ui.resolve(e) as any
445
+ const { Box, Text, Button, Input, Markdown, Client, Svg, Image } = $.ui.resolve(e) as any
442
446
  const s: PanelState = await read($, quiz)
443
447
  const q = s.question
444
448
 
@@ -619,6 +623,31 @@ export const register: Register = on => {
619
623
  )
620
624
  })
621
625
 
626
+ // The demo's picture test, under Teach me. The terminal has Image and no Svg; every other surface has Svg and no Image
627
+ // (a tree with an element its surface lacks is replaced whole by the host's own pane, so each surface gets only its own).
628
+ const experiment = s.demo ? (
629
+ <Box key="experiment" flexDirection="column" gap={1} borderStyle="dashed" borderColor="warning" paddingX={1} paddingY={app ? 1 : 0}>
630
+ <Text color="warning" bold>
631
+ {STR.experimentBanner}
632
+ </Text>
633
+ {app ? (
634
+ <Box flexDirection="column" gap={1}>
635
+ <Text dimColor>{STR.experimentSvg}</Text>
636
+ <Svg alt={STR.experimentImageAlt} width={208} height={96} source={demoSvg()} />
637
+ <Text dimColor>{STR.experimentSvgPng}</Text>
638
+ <Svg alt={STR.experimentImageAlt} width={208} height={96} source={demoSvgWithPng()} />
639
+ </Box>
640
+ ) : (
641
+ <Box flexDirection="column">
642
+ <Text dimColor wrap="wrap">
643
+ {STR.experimentTerminal}
644
+ </Text>
645
+ <Image key="experiment-image" source={{ png: DEMO_PNG }} columns={26} rows={6} alt={STR.experimentImageAlt} />
646
+ </Box>
647
+ )}
648
+ </Box>
649
+ ) : null
650
+
622
651
  return frame(
623
652
  stem,
624
653
  <Box flexDirection="column" gap={app ? 2 : 0} marginTop={app ? 1 : 0}>
@@ -673,6 +702,7 @@ export const register: Register = on => {
673
702
  </Button>
674
703
  </Box>
675
704
  ),
705
+ experiment,
676
706
  )
677
707
  })
678
708
  }
@@ -683,12 +713,14 @@ async function setStyle($: EngineInterface, style: 'button' | 'client'): Promise
683
713
  }
684
714
 
685
715
  /** Selecting an option records a draft and nothing else: it never submits, grades or skips. */
686
- async function pick($: EngineInterface, id: string, via = 'button'): Promise<void> {
716
+ async function pick($: EngineInterface, id: string, via = 'button', since?: number): Promise<void> {
687
717
  await update($, quiz, (s: PanelState) =>
688
718
  s.step === 'awaiting' || s.step === 'error'
689
- ? { ...s, draft: { ...s.draft, picked: id, other: false }, message: null, ...(s.demo ? { demo: { ...s.demo, last: demoLast(`${id} picked (${via})`) } } : {}) }
719
+ ? { ...s, draft: { ...s.draft, picked: id, other: false }, message: null, ...(s.demo ? { demo: { ...s.demo, last: demoLast(`${id} picked (${via}${since === undefined ? '' : `, ${Date.now() - since} ms in the mod`})`) } } : {}) }
690
720
  : s,
691
721
  )
722
+ // A whole card has no Button to focus, so asking the host to find `opt-…` would only make the pick wait.
723
+ if (via !== 'button') return
692
724
  try {
693
725
  // The host's focus mark follows the pick, so the highlighted answer is the chosen one even after a hotkey.
694
726
  await $.ui.focus({ requestId: PANE, key: `opt-${id}` })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eklavya",
3
- "version": "1.87.0",
3
+ "version": "1.88.0",
4
4
  "description": "Learn while your agent works — local knowledge graph, spaced repetition, and quiz gating for agent-assisted development.",
5
5
  "license": "MIT",
6
6
  "repository": {