eklavya 1.52.4 → 1.53.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.
- package/dist/assets/tutor/references/panel.md +4 -0
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/hooks/panel/model.ts +85 -1
- package/dist/plugin/hooks/panel/register.tsx +143 -49
- package/dist/plugin/skills/tutor/references/panel.md +4 -0
- package/dist/tools/panel_tools.js +1 -1
- package/dist/tools/panel_tools.js.map +1 -1
- package/dist/user-skill/eklavya/SKILL.md +4 -0
- package/package.json +1 -1
|
@@ -27,6 +27,10 @@ Make one call to `present_question` instead:
|
|
|
27
27
|
misconception (rows one and three). Grade 3 is not available to a pick: only
|
|
28
28
|
typed words can show a hesitant "right".
|
|
29
29
|
- `more: true` only in an explicit round (the plan has more items after this one, as for `/eklavya:quiz`): the panel then offers a Next button. Leave it out otherwise. Once a round is planned the server tracks it: the last question's `more` is set false for you, and Next arrives as a prompt telling you to call `get_session_quiz_plan` with `resume_round: true`.
|
|
30
|
+
- **Mark the code.** The panel draws the stem, each `description` and the `explanation` as Markdown, so
|
|
31
|
+
put every function, call, class, file and flag in backticks (`` `tester.pumpWidget(widget)` ``,
|
|
32
|
+
`` `shouldRepaint` ``) and nothing else: no headings, lists or emphasis. Option `label`s are drawn as
|
|
33
|
+
plain text, so write them in words and leave the backticks out of them.
|
|
30
34
|
- `explanation`: one line saying why the right option is right. The panel shows
|
|
31
35
|
it after the answer, so it is the whole of your feedback.
|
|
32
36
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eklavya",
|
|
3
3
|
"displayName": "Eklavya",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.53.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"
|
|
@@ -18,14 +18,96 @@ export const EMPTY: PanelState = {
|
|
|
18
18
|
/** Tier names as the dashboard shows them (`TIER` in `assets/dashboard.html`; a test keeps the two equal). */
|
|
19
19
|
export const TIER: Record<number, string> = { 1: 'recall', 2: 'mechanism', 3: 'judgement', 4: 'failure modes', 5: 'design' }
|
|
20
20
|
|
|
21
|
+
/** Brand verdigris (`--vd-300`): the selected card's border and the name. */
|
|
22
|
+
export const BRAND = '#79D5C4'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The logo: the Claude mascot (orange) holding an Eklavya bow and arrow (white), drawn on a
|
|
26
|
+
* grid where one cell is a quarter of a text character. Designed in the logo maker; a letter
|
|
27
|
+
* names its colour in LOGO_PALETTE and a dot is empty.
|
|
28
|
+
*/
|
|
29
|
+
export const LOGO_GRID = [
|
|
30
|
+
'.................dd.......',
|
|
31
|
+
'................d..d......',
|
|
32
|
+
'................d...d.....',
|
|
33
|
+
'...aaaaaaaaaaaa.d....d....',
|
|
34
|
+
'...aa.aaaaaa.aa.d.....d...',
|
|
35
|
+
'.aaaaaaaaaaaaaaadddddddddd',
|
|
36
|
+
'...aaaaaaaaaaaa.d.....d...',
|
|
37
|
+
'....a.a....a.a..d....d....',
|
|
38
|
+
'................d...d.....',
|
|
39
|
+
'................d..d......',
|
|
40
|
+
'.................dd.......',
|
|
41
|
+
'..........................',
|
|
42
|
+
] as const
|
|
43
|
+
export const LOGO_PALETTE: Record<string, string> = { a: '#D97757', d: '#FFFFFF' }
|
|
44
|
+
|
|
45
|
+
/** One stretch of text cells that share a colour pair. A text character holds two colours at most. */
|
|
46
|
+
export type LogoRun = { text: string; fg?: string; bg?: string }
|
|
47
|
+
|
|
48
|
+
/** Quadrant glyphs by which of the four cells (upper left 1, upper right 2, lower left 4, lower right 8) are filled. */
|
|
49
|
+
const QUADRANT = ' ▘▝▀▖▌▞▛▗▚▐▜▄▙▟█'
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The grid as rows of text runs: each 2 by 2 block becomes one quadrant character, the
|
|
53
|
+
* first colour as its foreground and a second, if any, as its background. A third colour
|
|
54
|
+
* in one block is dropped, so the logo degrades and never throws.
|
|
55
|
+
*/
|
|
56
|
+
export function logoRows(grid: readonly string[] = LOGO_GRID, palette: Record<string, string> = LOGO_PALETTE): LogoRun[][] {
|
|
57
|
+
const out: LogoRun[][] = []
|
|
58
|
+
for (let cy = 0; cy < Math.ceil(grid.length / 2); cy++) {
|
|
59
|
+
const runs: LogoRun[] = []
|
|
60
|
+
for (let cx = 0; cx < Math.ceil((grid[0]?.length ?? 0) / 2); cx++) {
|
|
61
|
+
const quad = [grid[cy * 2]?.[cx * 2], grid[cy * 2]?.[cx * 2 + 1], grid[cy * 2 + 1]?.[cx * 2], grid[cy * 2 + 1]?.[cx * 2 + 1]].map(c => (c && c !== '.' ? c : ''))
|
|
62
|
+
const [first, second] = [...new Set(quad.filter(Boolean))]
|
|
63
|
+
let run: LogoRun = { text: ' ' }
|
|
64
|
+
if (first) {
|
|
65
|
+
const mask = quad.reduce((n, c, i) => (c === first ? n | (1 << i) : n), 0)
|
|
66
|
+
run = { text: QUADRANT[mask], fg: palette[first], bg: second ? palette[second] : undefined }
|
|
67
|
+
}
|
|
68
|
+
const last = runs[runs.length - 1]
|
|
69
|
+
if (last && last.fg === run.fg && last.bg === run.bg) last.text += run.text
|
|
70
|
+
else runs.push(run)
|
|
71
|
+
}
|
|
72
|
+
out.push(runs)
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The name as the header sets it: spaced capitals, the nearest a terminal gets to a larger type size. */
|
|
78
|
+
/** A call such as `tester.tap(find.byType(X))`, nested up to three deep, a lowerCamelCase name or a snake_case name. */
|
|
79
|
+
const CODE_LIKE = /[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\((?:[^()\n]|\((?:[^()\n]|\([^()\n]*\))*\))*\)|\b[a-z]{2,}(?:[A-Z][a-z0-9]+)+\b|\b[a-z0-9]+(?:_[a-z0-9]+)+\b/g
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Wraps code-looking words in backticks so the pane's Markdown draws them as code. A question
|
|
83
|
+
* is plain prose unless its author marked the code, and an old or careless one often is not.
|
|
84
|
+
* Text already in backticks is left as written.
|
|
85
|
+
*/
|
|
86
|
+
export function codify(text: string): string {
|
|
87
|
+
return text
|
|
88
|
+
.split(/(`[^`]*`)/)
|
|
89
|
+
.map((part, i) => (i % 2 ? part : part.replace(CODE_LIKE, '`$&`')))
|
|
90
|
+
.join('')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** What a Button label can show of Markdown: it keeps the words and drops the code and bold marks. */
|
|
94
|
+
export function plainLabel(text: string): string {
|
|
95
|
+
return text.replace(/`|\*\*/g, '')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const WORDMARK = 'EKLAVYA'.split('').join(' ')
|
|
99
|
+
|
|
100
|
+
/** How long a finished result stays before it closes itself, when no Next is waiting. */
|
|
101
|
+
export const CLOSE_MS = 15_000
|
|
102
|
+
|
|
21
103
|
/** Every sentence the learner reads, in one place. The handoff's table, verbatim. */
|
|
22
104
|
export const STR = {
|
|
23
105
|
brand: 'Eklavya',
|
|
24
|
-
powered: 'Powered by Claude',
|
|
25
106
|
submit: 'Submit answer',
|
|
26
107
|
skip: 'Skip question',
|
|
27
108
|
next: 'Next question',
|
|
28
109
|
done: 'Done',
|
|
110
|
+
autoClose: 'Closes by itself in 15 seconds.',
|
|
29
111
|
retry: 'Retry',
|
|
30
112
|
close: 'Close',
|
|
31
113
|
other: 'Other — explain in your own words',
|
|
@@ -41,6 +123,8 @@ export const STR = {
|
|
|
41
123
|
nothingTyped: 'Write your answer first, or pick one of the options.',
|
|
42
124
|
graderFailed: "We couldn't check that answer. Your text is saved here — Retry.",
|
|
43
125
|
explainer: 'A page explaining this is being written.',
|
|
126
|
+
focus: 'Ctrl+X then Tab moves the keys here.',
|
|
127
|
+
keys: '1–4 pick · o other · s submit · k skip · Esc back to prompt',
|
|
44
128
|
off: 'The quiz panel is off. Turn it on with: eklavya config set quiz.panel true',
|
|
45
129
|
} as const
|
|
46
130
|
|
|
@@ -2,15 +2,21 @@ import { atom, read, update } from 'claude-code'
|
|
|
2
2
|
import type { EngineInterface, Register } from 'claude-code'
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
BRAND,
|
|
5
6
|
EMPTY,
|
|
6
7
|
EXPLAINER,
|
|
7
8
|
NEXT_PROMPT,
|
|
8
9
|
PANE,
|
|
9
10
|
REOPEN,
|
|
10
11
|
STR,
|
|
12
|
+
WORDMARK,
|
|
13
|
+
CLOSE_MS,
|
|
11
14
|
answerWas,
|
|
12
15
|
explainerBrief,
|
|
16
|
+
codify,
|
|
13
17
|
gradingRequest,
|
|
18
|
+
logoRows,
|
|
19
|
+
plainLabel,
|
|
14
20
|
levelLine,
|
|
15
21
|
parseVerdict,
|
|
16
22
|
payloadOf,
|
|
@@ -49,6 +55,8 @@ let busy = false
|
|
|
49
55
|
/** Bumped when the session ends: a submit that began before it is stale and must write nothing. */
|
|
50
56
|
let epoch = 0
|
|
51
57
|
let explainedAttempt = 0
|
|
58
|
+
/** The result closes itself after CLOSE_MS when nothing else is pending (no Next). */
|
|
59
|
+
let closer: { cancel: () => void } | null = null
|
|
52
60
|
|
|
53
61
|
/**
|
|
54
62
|
* One call to an Eklavya tool through the host's own connection, as the parsed
|
|
@@ -219,14 +227,22 @@ async function submit($: EngineInterface, wanted: PanelPending | null): Promise<
|
|
|
219
227
|
message: null,
|
|
220
228
|
}))
|
|
221
229
|
|
|
230
|
+
// The last question of a round has no Next: the result closes itself, as Done would.
|
|
231
|
+
if (!q.more) arm($, mine, q.question_id)
|
|
232
|
+
|
|
222
233
|
// A miss with explain_on_wrong on: the panel starts the explainer itself in
|
|
223
234
|
// the background, so no prompt shows in the transcript and nothing waits.
|
|
224
|
-
//
|
|
235
|
+
// Auto mode judges every spawn (it drops Agent allow rules), and its
|
|
236
|
+
// classifier can be briefly unavailable, so a refusal is asked once more.
|
|
237
|
+
// A second refusal falls back to the queued prompt the model acts on.
|
|
225
238
|
if (out.explain?.instruction && explainedAttempt !== out.attempt_id) {
|
|
226
239
|
explainedAttempt = out.attempt_id
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
240
|
+
let spawned = null
|
|
241
|
+
for (let tries = 0; tries < 2 && (!spawned || spawned.deny); tries += 1) {
|
|
242
|
+
spawned = await $.agent
|
|
243
|
+
.spawn({ subagentType: EXPLAINER, description: 'Eklavya explainer', prompt: explainerBrief(out.explain) })
|
|
244
|
+
.catch(() => null)
|
|
245
|
+
}
|
|
230
246
|
if (!spawned || spawned.deny) await $.prompt.submit({ text: String(out.explain.instruction) })
|
|
231
247
|
}
|
|
232
248
|
} catch {
|
|
@@ -265,6 +281,7 @@ export const register: Register = on => {
|
|
|
265
281
|
on('session.end', async ($, e, next) => {
|
|
266
282
|
try {
|
|
267
283
|
epoch += 1
|
|
284
|
+
disarm()
|
|
268
285
|
openedFor = null
|
|
269
286
|
busy = false
|
|
270
287
|
await update($, quiz, () => EMPTY)
|
|
@@ -327,22 +344,61 @@ export const register: Register = on => {
|
|
|
327
344
|
on('ui.render', { component: 'Pane', requestId: 'eklavya-quiz' }, async ($, e, next) => {
|
|
328
345
|
// Only our pane: every other pane, and everything else, is the host's.
|
|
329
346
|
if (e.requestId !== PANE) return next(e)
|
|
330
|
-
const { Box, Text, Button, Input } = $.ui.resolve(e) as any
|
|
347
|
+
const { Box, Text, Button, Input, Markdown } = $.ui.resolve(e) as any
|
|
331
348
|
const s: PanelState = await read($, quiz)
|
|
332
349
|
const q = s.question
|
|
333
350
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
351
|
+
// The mark is drawn in text, as Claude Code draws its own, so it shows in every terminal.
|
|
352
|
+
const logo = (
|
|
353
|
+
<Box flexDirection="column" flexShrink={0}>
|
|
354
|
+
{logoRows().map((runs, y) => (
|
|
355
|
+
<Box key={`logo-${y}`} flexDirection="row" flexShrink={0}>
|
|
356
|
+
{runs.map((run, x) => (
|
|
357
|
+
<Text key={`logo-${y}-${x}`} color={run.fg} backgroundColor={run.bg} wrap="truncate">
|
|
358
|
+
{run.text}
|
|
359
|
+
</Text>
|
|
360
|
+
))}
|
|
361
|
+
</Box>
|
|
362
|
+
))}
|
|
363
|
+
</Box>
|
|
364
|
+
)
|
|
365
|
+
// A list tile: the mark on the left; the name over the topic beside it.
|
|
366
|
+
const head = (subtitle?: string) => (
|
|
367
|
+
<Box flexDirection="row" alignItems="center" gap={2}>
|
|
368
|
+
{logo}
|
|
369
|
+
<Box flexDirection="column" flexShrink={1}>
|
|
370
|
+
<Text bold color={BRAND}>
|
|
371
|
+
{WORDMARK}
|
|
372
|
+
</Text>
|
|
373
|
+
{subtitle ? <Text>{subtitle}</Text> : null}
|
|
374
|
+
</Box>
|
|
375
|
+
</Box>
|
|
376
|
+
)
|
|
377
|
+
const hint = (
|
|
378
|
+
<Box flexDirection="column">
|
|
379
|
+
<Text dimColor>{STR.focus}</Text>
|
|
380
|
+
<Text dimColor>{STR.keys}</Text>
|
|
381
|
+
</Box>
|
|
382
|
+
)
|
|
383
|
+
// One answer: a bordered block. The Button holds the whole answer text, so a press anywhere on it
|
|
384
|
+
// selects; the border and a tick show the pick, and nothing implies it is right.
|
|
385
|
+
const card = (id: string, selected: boolean, button: unknown, body: unknown[], roomy = false) => (
|
|
386
|
+
<Box
|
|
387
|
+
key={`card-${id}`}
|
|
388
|
+
flexDirection="column"
|
|
389
|
+
paddingX={1}
|
|
390
|
+
paddingY={roomy ? 1 : 0}
|
|
391
|
+
gap={roomy ? 1 : 0}
|
|
392
|
+
borderStyle="single"
|
|
393
|
+
borderColor={selected ? BRAND : 'inactive'}
|
|
394
|
+
>
|
|
395
|
+
{button}
|
|
396
|
+
{body}
|
|
340
397
|
</Box>
|
|
341
398
|
)
|
|
342
|
-
const hint = <Text dimColor>Ctrl+X then Tab moves focus here. Enter presses a button. Esc returns to the prompt.</Text>
|
|
343
399
|
const frame = (...body: unknown[]) => (
|
|
344
400
|
<Box flexDirection="column" paddingX={1} gap={1}>
|
|
345
|
-
{head}
|
|
401
|
+
{head(q ? topicLabel(q) : undefined)}
|
|
346
402
|
{body}
|
|
347
403
|
{hint}
|
|
348
404
|
</Box>
|
|
@@ -359,25 +415,24 @@ export const register: Register = on => {
|
|
|
359
415
|
)
|
|
360
416
|
}
|
|
361
417
|
|
|
362
|
-
const
|
|
363
|
-
const stem = <Text bold>{q.stem}</Text>
|
|
418
|
+
const stem = <Markdown text={codify(q.stem)} />
|
|
364
419
|
|
|
365
420
|
if (s.step === 'feedback' || s.step === 'skipped') {
|
|
366
421
|
const r = s.result
|
|
367
422
|
if (s.step === 'skipped' || !r) {
|
|
368
|
-
return frame(
|
|
423
|
+
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($)} />)
|
|
369
424
|
}
|
|
370
425
|
const right = r.correct === true
|
|
371
426
|
return frame(
|
|
372
|
-
topic,
|
|
373
427
|
stem,
|
|
374
428
|
<Text bold color={right ? 'suggestion' : 'error'}>
|
|
375
429
|
{right ? STR.correct : STR.wrong}
|
|
376
430
|
</Text>,
|
|
377
431
|
right || !r.correct_label ? null : <Text>{answerWas(r.correct_label)}</Text>,
|
|
378
|
-
r.explanation ? <
|
|
432
|
+
r.explanation ? <Markdown text={codify(r.explanation)} /> : null,
|
|
379
433
|
r.level_up ? <Text color="suggestion">{levelLine(r.level_up.from, projectName(q.repo))}</Text> : null,
|
|
380
434
|
r.explain ? <Text dimColor>{STR.explainer}</Text> : null,
|
|
435
|
+
q.more ? null : <Text dimColor>{STR.autoClose}</Text>,
|
|
381
436
|
<Box flexDirection="row" gap={2}>
|
|
382
437
|
{q.more ? <Button key="next" label={STR.next} hotkey="n" onPress={() => askNext($)} /> : null}
|
|
383
438
|
<Button key="done" label={STR.done} variant="primary" hotkey="d" onPress={() => done($)} />
|
|
@@ -388,45 +443,51 @@ export const register: Register = on => {
|
|
|
388
443
|
const locked = s.step === 'grading'
|
|
389
444
|
// Submit looks disabled until there is something to send; pressing it then does nothing.
|
|
390
445
|
const hasDraft = s.draft.other ? s.draft.text.trim().length > 0 : s.draft.picked !== null
|
|
446
|
+
// Markdown marks cannot show inside a Button's label, so the code and bold marks are dropped there; the note keeps them.
|
|
447
|
+
const answer = (id: string, key: string, hotkey: string, label: string, chosen: boolean) => (
|
|
448
|
+
<Button
|
|
449
|
+
key={key}
|
|
450
|
+
plain
|
|
451
|
+
hotkey={hotkey}
|
|
452
|
+
label={`${chosen ? '✓ ' : ''}${label}`}
|
|
453
|
+
onPress={() => (locked ? undefined : id === 'other' ? chooseOther($) : pick($, id))}
|
|
454
|
+
/>
|
|
455
|
+
)
|
|
391
456
|
const options = q.options.map((o, i) => {
|
|
392
457
|
const chosen = !s.draft.other && s.draft.picked === o.id
|
|
393
|
-
return (
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
label={`${chosen ? '●' : '○'} ${o.label}`}
|
|
399
|
-
hotkey={String(i + 1)}
|
|
400
|
-
onPress={() => (locked ? undefined : pick($, o.id))}
|
|
401
|
-
/>
|
|
402
|
-
{o.note ? <Text dimColor>{` ${o.note}`}</Text> : null}
|
|
403
|
-
</Box>
|
|
458
|
+
return card(
|
|
459
|
+
o.id,
|
|
460
|
+
chosen,
|
|
461
|
+
answer(o.id, `opt-${o.id}`, String(i + 1), plainLabel(o.label), chosen),
|
|
462
|
+
[o.note ? <Box key={`note-${o.id}`} paddingLeft={3}><Markdown text={codify(o.note)} dimColor /></Box> : null],
|
|
404
463
|
)
|
|
405
464
|
})
|
|
406
465
|
|
|
407
466
|
return frame(
|
|
408
|
-
topic,
|
|
409
467
|
stem,
|
|
410
|
-
<Box flexDirection="column"
|
|
468
|
+
<Box flexDirection="column">
|
|
411
469
|
{options}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
470
|
+
{card(
|
|
471
|
+
'other',
|
|
472
|
+
s.draft.other,
|
|
473
|
+
answer('other', 'opt-other', 'o', STR.other, s.draft.other),
|
|
474
|
+
[
|
|
475
|
+
s.draft.other ? (
|
|
476
|
+
<Box key="other-box" paddingLeft={3}>
|
|
477
|
+
<Input
|
|
478
|
+
key="other-text"
|
|
479
|
+
autoFocus
|
|
480
|
+
placeholder={STR.otherPlaceholder}
|
|
481
|
+
value={s.draft.text}
|
|
482
|
+
submitLabel="keep"
|
|
483
|
+
onInput={(value: string) => (locked ? undefined : typeOther($, value))}
|
|
484
|
+
onSubmit={(value: string) => (locked ? undefined : typeOther($, value))}
|
|
485
|
+
/>
|
|
486
|
+
</Box>
|
|
487
|
+
) : null,
|
|
488
|
+
],
|
|
489
|
+
s.draft.other,
|
|
490
|
+
)}
|
|
430
491
|
</Box>,
|
|
431
492
|
locked ? <Text dimColor>{STR.grading}</Text> : null,
|
|
432
493
|
s.step === 'error' && s.message ? <Text color="error">{s.message}</Text> : null,
|
|
@@ -447,6 +508,12 @@ export const register: Register = on => {
|
|
|
447
508
|
/** Selecting an option records a draft and nothing else: it never submits, grades or skips. */
|
|
448
509
|
async function pick($: EngineInterface, id: string): Promise<void> {
|
|
449
510
|
await update($, quiz, (s: PanelState) => (s.step === 'awaiting' || s.step === 'error' ? { ...s, draft: { ...s.draft, picked: id, other: false }, message: null } : s))
|
|
511
|
+
try {
|
|
512
|
+
// The host's focus mark follows the pick, so the highlighted answer is the chosen one even after a hotkey.
|
|
513
|
+
await $.ui.focus({ requestId: PANE, key: `opt-${id}` })
|
|
514
|
+
} catch {
|
|
515
|
+
/* Fail open: the pick stands without it. */
|
|
516
|
+
}
|
|
450
517
|
}
|
|
451
518
|
|
|
452
519
|
async function chooseOther($: EngineInterface): Promise<void> {
|
|
@@ -461,8 +528,34 @@ async function skip($: EngineInterface): Promise<void> {
|
|
|
461
528
|
await submit($, { kind: 'skip' })
|
|
462
529
|
}
|
|
463
530
|
|
|
531
|
+
/** Starts the auto-close for this result; any later Done, Next or new question cancels it. */
|
|
532
|
+
function arm($: EngineInterface, mine: number, id: string): void {
|
|
533
|
+
try {
|
|
534
|
+
closer?.cancel()
|
|
535
|
+
closer = $.clock.after(CLOSE_MS, async () => {
|
|
536
|
+
try {
|
|
537
|
+
closer = null
|
|
538
|
+
if (epoch !== mine) return
|
|
539
|
+
const s: PanelState = (await $.state.get(REF)).value ?? EMPTY
|
|
540
|
+
// Only the result it was started for: an answer-in-progress or a newer question stays.
|
|
541
|
+
if ((s.step === 'feedback' || s.step === 'skipped') && s.question?.question_id === id) await done($)
|
|
542
|
+
} catch {
|
|
543
|
+
/* Fail open. */
|
|
544
|
+
}
|
|
545
|
+
})
|
|
546
|
+
} catch {
|
|
547
|
+
/* No clock: the learner presses Done. */
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function disarm(): void {
|
|
552
|
+
closer?.cancel()
|
|
553
|
+
closer = null
|
|
554
|
+
}
|
|
555
|
+
|
|
464
556
|
/** Done closes the pane; another question may already be waiting, and then the pane comes straight back. */
|
|
465
557
|
async function done($: EngineInterface): Promise<void> {
|
|
558
|
+
disarm()
|
|
466
559
|
try {
|
|
467
560
|
openedFor = null
|
|
468
561
|
await update($, quiz, () => EMPTY)
|
|
@@ -475,6 +568,7 @@ async function done($: EngineInterface): Promise<void> {
|
|
|
475
568
|
|
|
476
569
|
/** Next asks the model, by a queued prompt, for the next question of an explicit round. */
|
|
477
570
|
async function askNext($: EngineInterface): Promise<void> {
|
|
571
|
+
disarm()
|
|
478
572
|
try {
|
|
479
573
|
openedFor = null
|
|
480
574
|
await update($, quiz, () => ({ ...EMPTY, step: 'loading' }))
|
|
@@ -27,6 +27,10 @@ Make one call to `present_question` instead:
|
|
|
27
27
|
misconception (rows one and three). Grade 3 is not available to a pick: only
|
|
28
28
|
typed words can show a hesitant "right".
|
|
29
29
|
- `more: true` only in an explicit round (the plan has more items after this one, as for `/eklavya:quiz`): the panel then offers a Next button. Leave it out otherwise. Once a round is planned the server tracks it: the last question's `more` is set false for you, and Next arrives as a prompt telling you to call `get_session_quiz_plan` with `resume_round: true`.
|
|
30
|
+
- **Mark the code.** The panel draws the stem, each `description` and the `explanation` as Markdown, so
|
|
31
|
+
put every function, call, class, file and flag in backticks (`` `tester.pumpWidget(widget)` ``,
|
|
32
|
+
`` `shouldRepaint` ``) and nothing else: no headings, lists or emphasis. Option `label`s are drawn as
|
|
33
|
+
plain text, so write them in words and leave the backticks out of them.
|
|
30
34
|
- `explanation`: one line saying why the right option is right. The panel shows
|
|
31
35
|
it after the answer, so it is the whole of your feedback.
|
|
32
36
|
|
|
@@ -10,7 +10,7 @@ export const presentQuestionTool = {
|
|
|
10
10
|
session_id: z.string().max(LIMITS.sessionId).optional().describe(SESSION_HINT),
|
|
11
11
|
cwd: z.string().max(LIMITS.cwd).optional().describe(CWD_HINT),
|
|
12
12
|
slug: z.string().max(LIMITS.slug).describe('The concept asked about.'),
|
|
13
|
-
question: z.string().min(1).max(LIMITS.question).describe('The question stem only: no options, no attribution, no settings line.'),
|
|
13
|
+
question: z.string().min(1).max(LIMITS.question).describe('The question stem only: no options, no attribution, no settings line. Markdown: put code names and calls in backticks.'),
|
|
14
14
|
options: z
|
|
15
15
|
.array(z.object({
|
|
16
16
|
label: z.string().min(1).max(LIMITS.option).describe('The option text itself, a complete claim in the option\'s own words: never a letter or a number (the panel numbers them). Labels must differ.'),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"panel_tools.js","sourceRoot":"","sources":["../../src/tools/panel_tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,eAAe,EAAuD,MAAM,aAAa,CAAC;AAC3H,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAgB,MAAM,YAAY,CAAC;AAE1E,MAAM,YAAY,GAAG,CAAC,CAAC;AAEvB,MAAM,CAAC,MAAM,mBAAmB,GAAY;IAC1C,IAAI,EAAE,kBAAkB;IACxB,KAAK,EAAE,2CAA2C;IAClD,WAAW,EACT,k0BAAk0B;IACp0B,WAAW,EAAE;QACX,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC;QAC9E,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAC7D,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,0BAA0B,CAAC;QACtE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"panel_tools.js","sourceRoot":"","sources":["../../src/tools/panel_tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -270,6 +270,10 @@ it is missing, and leave the file valid JSON. Do not allow the whole
|
|
|
270
270
|
`mcp__plugin_eklavya_eklavya` server instead: that would also let
|
|
271
271
|
`memory_delete` and `set_config` run unasked. Show them the entries you added.
|
|
272
272
|
If the denial continues, a restart of Claude Code reloads the settings.
|
|
273
|
+
`eklavya:eklavya-explainer denied by auto mode` needs no fix and has none: auto
|
|
274
|
+
mode drops `Agent` allow rules, so do not add one. The panel asks once more,
|
|
275
|
+
then queues a prompt for you to start the explainer; do that and tell them
|
|
276
|
+
the page is on its way.
|
|
273
277
|
|
|
274
278
|
**Updates are automatic.** Eklavya checks npm at session start, at most hourly,
|
|
275
279
|
and installs a newer release on its own, settings untouched. So "update
|
package/package.json
CHANGED