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.
|
|
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