eklavya 1.71.0 → 1.71.1
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.
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ level.
|
|
|
83
83
|
right one, 2 for a near miss, 1 for a misconception), the right one in the
|
|
84
84
|
plan's `answer_position` slot, and an explanation. It stores the question and
|
|
85
85
|
returns at once; it writes no attempt. The panel records the answer through
|
|
86
|
-
`panel_answer` with the same code `record_attempt` uses, exactly once. `kind: "dont_know"` (the panel's **Teach me** button) records `dont_know` with grade 0, replies with the right answer and `taught: true`, and always carries `explain`. On a miss with `explain_on_wrong` on, its reply carries the same `explain` block `record_attempt` returns, and the panel starts the explainer agent from it. `present_question` sends a question back once with `conspicuous_options` (nothing stored) when one option's label plus description outruns the next by a few words or a description explains itself, and presents the same stem as written on the second call. It returns `panel_disabled` while `quiz.panel` is off, and
|
|
86
|
+
`panel_answer` with the same code `record_attempt` uses, exactly once. `kind: "dont_know"` (the panel's **New to me — Teach me** button) records `dont_know` with grade 0, replies with the right answer and `taught: true`, and always carries `explain`. On a miss with `explain_on_wrong` on, its reply carries the same `explain` block `record_attempt` returns, and the panel starts the explainer agent from it. `present_question` sends a question back once with `conspicuous_options` (nothing stored) when one option's label plus description outruns the next by a few words or a description explains itself, and presents the same stem as written on the second call. It returns `panel_disabled` while `quiz.panel` is off, and
|
|
87
87
|
`panel_sync` returns `{disabled: true}` and expires the session's open question without recording an attempt, so turning the panel back on cannot revive it; `panel_answer` answers only a question
|
|
88
88
|
that already exists. A round the learner asked for (a topic, `max`) is remembered per session: `present_question` takes each question off it and sets the last one's `more` false, and `get_session_quiz_plan` with `resume_round: true` plans what is left. This is
|
|
89
89
|
a Claude Code mod feature: a standalone MCP client has the tools but not the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eklavya",
|
|
3
3
|
"displayName": "Eklavya",
|
|
4
|
-
"version": "1.71.
|
|
4
|
+
"version": "1.71.1",
|
|
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"
|
|
@@ -22,6 +22,8 @@ export const TIER: Record<number, string> = { 1: 'recall', 2: 'mechanism', 3: 'j
|
|
|
22
22
|
export const BRAND = '#79D5C4'
|
|
23
23
|
/** The fill and text of the picked answer: set together so the contrast holds in a light or a dark theme. */
|
|
24
24
|
export const PICKED = { bg: '#1E5A52', text: '#FFFFFF', note: '#C9E8E3' }
|
|
25
|
+
/** Teach me is Claude's orange (the logo's mascot), fill and border. The text is dark, as white on that orange is too faint at body size. */
|
|
26
|
+
export const TEACH = { bg: '#D97757', text: '#1F1E1D' }
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* The logo: the Claude mascot (orange) holding an Eklavya bow and arrow (white), drawn on a
|
|
@@ -106,14 +108,14 @@ export function codify(text: string): string {
|
|
|
106
108
|
.join('')
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
/** Block Markdown
|
|
110
|
-
const
|
|
111
|
+
/** Block Markdown a heading mark would break: a fence, heading, list, quote, or bold already in it. */
|
|
112
|
+
const NOT_HEADING = /```|\*\*|^\s*(?:#|[-*+>]|\d+[.)])\s/m
|
|
111
113
|
|
|
112
|
-
/**
|
|
113
|
-
export function
|
|
114
|
+
/** Makes each plain paragraph of a question a heading, so the app draws it larger and bold; a heading is one line, so its line breaks become spaces. */
|
|
115
|
+
export function headingParagraphs(text: string): string {
|
|
114
116
|
return text
|
|
115
117
|
.split(/\n{2,}/)
|
|
116
|
-
.map(p => (p.trim() && !
|
|
118
|
+
.map(p => (p.trim() && !NOT_HEADING.test(p) ? `### ${p.trim().replace(/\s*\n\s*/g, ' ')}` : p))
|
|
117
119
|
.join('\n\n')
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -132,7 +134,7 @@ export const STR = {
|
|
|
132
134
|
brand: 'Eklavya',
|
|
133
135
|
submit: 'Submit answer',
|
|
134
136
|
skip: 'Skip question',
|
|
135
|
-
teach: 'Teach me',
|
|
137
|
+
teach: 'New to me — Teach me',
|
|
136
138
|
next: 'Next question',
|
|
137
139
|
done: 'Done',
|
|
138
140
|
autoClose: 'Closes by itself in 15 seconds.',
|
|
@@ -159,7 +161,6 @@ export const STR = {
|
|
|
159
161
|
askedOther: 'Eklavya: a note for Claude',
|
|
160
162
|
focus: 'Ctrl+X then Tab moves the keys here.',
|
|
161
163
|
keys: '1–4 pick · o other · s submit · k skip · t teach me · Esc back to prompt',
|
|
162
|
-
keysApp: '1–4 pick · o other · s submit · k skip · t teach me',
|
|
163
164
|
off: 'The quiz panel is off. Turn it on with: eklavya config set quiz.panel true',
|
|
164
165
|
} as const
|
|
165
166
|
|
|
@@ -202,11 +203,11 @@ export function explainerBrief(x: ExplainBlock): string {
|
|
|
202
203
|
export const answerWas = (label: string): string => `The answer was: ${label}`
|
|
203
204
|
|
|
204
205
|
/**
|
|
205
|
-
* The
|
|
206
|
-
*
|
|
207
|
-
*
|
|
206
|
+
* The key hints. Only the terminal gets them: there Ctrl+X then Tab moves the keys into the pane. The
|
|
207
|
+
* desktop app keeps typing in its prompt even after a click on the pane (seen 2026-10-16), so a mod's
|
|
208
|
+
* hotkeys never reach it there, and it shows no keys at all.
|
|
208
209
|
*/
|
|
209
|
-
export const hintLines = (surface: string): string[] => (surface === 'terminal' ? [STR.focus, STR.keys] : [
|
|
210
|
+
export const hintLines = (surface: string): string[] => (surface === 'terminal' ? [STR.focus, STR.keys] : [])
|
|
210
211
|
|
|
211
212
|
export const unplacedNotice = (): string => `A question is waiting. ${REOPEN} to open it.`
|
|
212
213
|
export const levelLine = (level: string, project: string): string => `You've cleared ${level} on ${project}.`
|
|
@@ -4,6 +4,7 @@ import type { EngineInterface, Register } from 'claude-code'
|
|
|
4
4
|
import {
|
|
5
5
|
BRAND,
|
|
6
6
|
PICKED,
|
|
7
|
+
TEACH,
|
|
7
8
|
EMPTY,
|
|
8
9
|
EXPLAINER,
|
|
9
10
|
NEXT_PROMPT,
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
gradingRequest,
|
|
19
20
|
logoRows,
|
|
20
21
|
boxRows,
|
|
21
|
-
|
|
22
|
+
headingParagraphs,
|
|
22
23
|
plainLabel,
|
|
23
24
|
levelLine,
|
|
24
25
|
parseVerdict,
|
|
@@ -364,8 +365,8 @@ export const register: Register = on => {
|
|
|
364
365
|
const s: PanelState = await read($, quiz)
|
|
365
366
|
const q = s.question
|
|
366
367
|
|
|
367
|
-
// Any surface but the terminal is the desktop app's: it sets the question
|
|
368
|
-
// apart and draws the mark in boxes. The terminal keeps its text layout.
|
|
368
|
+
// Any surface but the terminal is the desktop app's: it sets the question as a heading, spaces the
|
|
369
|
+
// answers apart and draws the mark in boxes. The terminal keeps its text layout.
|
|
369
370
|
const app = e.surface !== 'terminal'
|
|
370
371
|
// The mark is drawn in text, as Claude Code draws its own, so it shows in every terminal. The
|
|
371
372
|
// desktop app sets text in a proportional font with its own line height, so glyph cells do not
|
|
@@ -408,7 +409,9 @@ export const register: Register = on => {
|
|
|
408
409
|
</Box>
|
|
409
410
|
</Box>
|
|
410
411
|
)
|
|
411
|
-
|
|
412
|
+
// The desktop app never sends the pane its keys (see hintLines), so it gets no hotkeys and no hints.
|
|
413
|
+
const hk = (k: string) => (app ? undefined : k)
|
|
414
|
+
const hint = app ? null : (
|
|
412
415
|
<Box flexDirection="column">
|
|
413
416
|
{hintLines(e.surface).map(line => (
|
|
414
417
|
<Text key={line} dimColor>
|
|
@@ -418,8 +421,9 @@ export const register: Register = on => {
|
|
|
418
421
|
</Box>
|
|
419
422
|
)
|
|
420
423
|
// One answer: a bordered block. The Button holds the whole answer text, so a press anywhere on it
|
|
421
|
-
// selects; the border and
|
|
422
|
-
// the text more room inside the border; the
|
|
424
|
+
// selects; the border and fill show the pick, and nothing implies it is
|
|
425
|
+
// right. The desktop app gives the text more room inside the border; the padding itself is not
|
|
426
|
+
// pressable, since a Button takes none. The terminal keeps its tight cards.
|
|
423
427
|
// Other is never filled: the learner types inside it, in the host's own text colour, which a fixed fill could hide.
|
|
424
428
|
const card = (id: string, selected: boolean, button: unknown, body: unknown[], roomy = false) => (
|
|
425
429
|
<Box
|
|
@@ -455,17 +459,17 @@ export const register: Register = on => {
|
|
|
455
459
|
)
|
|
456
460
|
}
|
|
457
461
|
|
|
458
|
-
const stem = <Markdown text={app ?
|
|
462
|
+
const stem = <Markdown text={app ? headingParagraphs(codify(q.stem)) : codify(q.stem)} />
|
|
459
463
|
|
|
460
464
|
if (s.step === 'feedback' || s.step === 'skipped') {
|
|
461
465
|
const r = s.result
|
|
462
466
|
if (s.step === 'skipped' || !r) {
|
|
463
|
-
return frame(<Text>{STR.skipped}</Text>, q.more ? null : <Text dimColor>{STR.autoClose}</Text>, <Button key="done" label={STR.done} variant="primary" hotkey=
|
|
467
|
+
return frame(<Text>{STR.skipped}</Text>, q.more ? null : <Text dimColor>{STR.autoClose}</Text>, <Button key="done" label={STR.done} variant="primary" hotkey={hk('d')} onPress={() => done($)} />)
|
|
464
468
|
}
|
|
465
469
|
const right = r.correct === true
|
|
466
470
|
return frame(
|
|
467
471
|
stem,
|
|
468
|
-
<Text bold color={right ? 'suggestion' : r.taught ?
|
|
472
|
+
<Text bold color={right ? 'suggestion' : r.taught ? BRAND : 'error'}>
|
|
469
473
|
{right ? STR.correct : r.taught ? STR.taught : STR.wrong}
|
|
470
474
|
</Text>,
|
|
471
475
|
right || !r.correct_label ? null : <Text>{answerWas(r.correct_label)}</Text>,
|
|
@@ -474,8 +478,8 @@ export const register: Register = on => {
|
|
|
474
478
|
r.explain ? <Text dimColor>{r.taught ? STR.taughtExplainer : STR.explainer}</Text> : null,
|
|
475
479
|
q.more ? null : <Text dimColor>{STR.autoClose}</Text>,
|
|
476
480
|
<Box flexDirection="row" gap={2}>
|
|
477
|
-
{q.more ? <Button key="next" label={STR.next} hotkey=
|
|
478
|
-
<Button key="done" label={STR.done} variant="primary" hotkey=
|
|
481
|
+
{q.more ? <Button key="next" label={STR.next} hotkey={hk('n')} onPress={() => askNext($)} /> : null}
|
|
482
|
+
<Button key="done" label={STR.done} variant="primary" hotkey={hk('d')} onPress={() => done($)} />
|
|
479
483
|
</Box>,
|
|
480
484
|
)
|
|
481
485
|
}
|
|
@@ -485,16 +489,20 @@ export const register: Register = on => {
|
|
|
485
489
|
const hasDraft = s.draft.other ? s.draft.text.trim().length > 0 : s.draft.picked !== null
|
|
486
490
|
// The answer and its note are the Button's own inline Text, so a press anywhere on them selects.
|
|
487
491
|
// Markdown marks cannot show inside a Button, so the code and bold marks are dropped from both.
|
|
492
|
+
// The pick shows by the card's border and fill, never a mark in the text, so picking moves nothing.
|
|
493
|
+
// On the desktop app the note starts where the answer does; the terminal indents it under the `1: `
|
|
494
|
+
// its plain Button draws.
|
|
495
|
+
// `wrap` asks the app to wrap a long answer rather than cut it off.
|
|
488
496
|
const answer = (id: string, key: string, hotkey: string, label: string, chosen: boolean, note?: string) => (
|
|
489
497
|
<Button
|
|
490
498
|
key={key}
|
|
491
499
|
plain
|
|
492
|
-
hotkey={hotkey}
|
|
500
|
+
hotkey={hk(hotkey)}
|
|
493
501
|
label={label}
|
|
494
502
|
onPress={() => (locked ? undefined : id === 'other' ? chooseOther($) : pick($, id))}
|
|
495
503
|
>
|
|
496
|
-
<Text bold={chosen} color={chosen && id !== 'other' ? PICKED.text : undefined}>{
|
|
497
|
-
{note ? <Text dimColor={!chosen} color={chosen ? PICKED.note : undefined}>{`\n ${note}`}</Text> : null}
|
|
504
|
+
<Text wrap="wrap" bold={chosen} color={chosen && id !== 'other' ? PICKED.text : undefined}>{label}</Text>
|
|
505
|
+
{note ? <Text wrap="wrap" dimColor={!chosen} color={chosen ? PICKED.note : undefined}>{`\n${app ? '' : ' '}${note}`}</Text> : null}
|
|
498
506
|
</Button>
|
|
499
507
|
)
|
|
500
508
|
const options = q.options.map((o, i) => {
|
|
@@ -509,7 +517,7 @@ export const register: Register = on => {
|
|
|
509
517
|
|
|
510
518
|
return frame(
|
|
511
519
|
stem,
|
|
512
|
-
<Box flexDirection="column" gap={app ? 2 : 0}>
|
|
520
|
+
<Box flexDirection="column" gap={app ? 2 : 0} marginTop={app ? 1 : 0}>
|
|
513
521
|
{options}
|
|
514
522
|
{card(
|
|
515
523
|
'other',
|
|
@@ -538,14 +546,29 @@ export const register: Register = on => {
|
|
|
538
546
|
s.step === 'awaiting' && s.message ? <Text color="warning">{s.message}</Text> : null,
|
|
539
547
|
<Box flexDirection="row" gap={2}>
|
|
540
548
|
{locked ? null : s.step === 'error' ? (
|
|
541
|
-
<Button key="retry" label={STR.retry} variant="primary" hotkey=
|
|
549
|
+
<Button key="retry" label={STR.retry} variant="primary" hotkey={hk('r')} onPress={() => submit($, null)} />
|
|
542
550
|
) : (
|
|
543
|
-
<Button key="submit" label={STR.submit} variant="primary" dimColor={!hasDraft} hotkey=
|
|
551
|
+
<Button key="submit" label={STR.submit} variant="primary" dimColor={!hasDraft} hotkey={hk('s')} onPress={() => submit($, null)} />
|
|
544
552
|
)}
|
|
545
|
-
{locked ? null : <Button key="
|
|
546
|
-
{locked ? null : <Button key="skip" label={STR.skip} dimColor hotkey="k" onPress={() => skip($)} />}
|
|
553
|
+
{locked ? null : <Button key="skip" label={STR.skip} dimColor hotkey={hk('k')} onPress={() => skip($)} />}
|
|
547
554
|
{s.step === 'error' ? <Button key="close" label={STR.close} onPress={() => $.ui.close({ id: PANE })} /> : null}
|
|
548
555
|
</Box>,
|
|
556
|
+
// Teach me has a row of its own, under Submit. A Button takes no fill, and a desktop draws a plain
|
|
557
|
+
// one without its grey chrome: the orange box is the button's body, and as a column it
|
|
558
|
+
// stretches the Button across its width, so all but the thin padding is pressable.
|
|
559
|
+
locked ? null : app ? (
|
|
560
|
+
<Box key="teach-box" flexDirection="column" backgroundColor={TEACH.bg} borderStyle="round" borderColor={TEACH.bg} paddingX={1} paddingY={0.5}>
|
|
561
|
+
<Button key="teach" plain label={STR.teach} onPress={() => teach($)}>
|
|
562
|
+
<Text color={TEACH.text}>{STR.teach}</Text>
|
|
563
|
+
</Button>
|
|
564
|
+
</Box>
|
|
565
|
+
) : (
|
|
566
|
+
<Box key="teach-row" flexDirection="row">
|
|
567
|
+
<Button key="teach" label={STR.teach} hotkey={hk('t')} onPress={() => teach($)}>
|
|
568
|
+
<Text color={TEACH.text} backgroundColor={TEACH.bg}>{STR.teach}</Text>
|
|
569
|
+
</Button>
|
|
570
|
+
</Box>
|
|
571
|
+
),
|
|
549
572
|
)
|
|
550
573
|
})
|
|
551
574
|
}
|
package/package.json
CHANGED