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.0",
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 that bold marks around it would break: a fence, heading, list, quote, or bold already in it. */
110
- const NOT_BOLDABLE = /```|\*\*|^\s*(?:#|[-*+>]|\d+[.)])\s/m
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
- /** Bolds each plain paragraph of a question, so a blank line never sits inside a bold span. */
113
- export function boldParagraphs(text: string): string {
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() && !NOT_BOLDABLE.test(p) ? `**${p.trim()}**` : p))
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 hint lines under the pane. Moving the keys with Ctrl+X then Tab and
206
- * leaving with Esc are the terminal's; an app draws native buttons, so it gets
207
- * the answer keys only.
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] : [STR.keysApp])
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
- boldParagraphs,
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 bold, spaces the answers
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
- const hint = (
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 a tick show the pick, and nothing implies it is right. The desktop app gives
422
- // the text more room inside the border; the terminal keeps its tight cards.
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 ? boldParagraphs(codify(q.stem)) : codify(q.stem)} />
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="d" onPress={() => done($)} />)
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 ? undefined : 'error'}>
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="n" onPress={() => askNext($)} /> : null}
478
- <Button key="done" label={STR.done} variant="primary" hotkey="d" onPress={() => done($)} />
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}>{`${chosen ? '✓ ' : ''}${label}`}</Text>
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="r" onPress={() => submit($, null)} />
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="s" onPress={() => submit($, null)} />
551
+ <Button key="submit" label={STR.submit} variant="primary" dimColor={!hasDraft} hotkey={hk('s')} onPress={() => submit($, null)} />
544
552
  )}
545
- {locked ? null : <Button key="teach" label={STR.teach} hotkey="t" onPress={() => teach($)} />}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eklavya",
3
- "version": "1.71.0",
3
+ "version": "1.71.1",
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": {