eklavya 1.85.2 → 1.86.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/README.md +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/hooks/CLAUDE.md +1 -0
- package/dist/plugin/hooks/panel/card.tsx +47 -0
- package/dist/plugin/hooks/panel/model.ts +43 -1
- package/dist/plugin/hooks/panel/register.tsx +101 -3
- package/dist/plugin/hooks/panel/types/index.d.ts +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Code's learning loop.
|
|
|
52
52
|
|---|---|
|
|
53
53
|
| Learner state and concepts | `get_learner_profile`, `log_session_concepts`, `upsert_concepts`, `get_concept_graph` |
|
|
54
54
|
| Questions and assessment (automatic questions wait for a code change, not a `git pull`; see `quiz.only_on_changes`) | `get_session_quiz_plan`, `record_attempt`, `get_gate_status` |
|
|
55
|
-
| Quiz side panel (experimental) | `present_question` for the model; `panel_sync` and `panel_answer`, called only by the panel mod, never by the model; its header stacks the mark above the topic in a pane under 60 cells wide; on the desktop app each option's note is its own line under the option, and a click on the option picks it |
|
|
55
|
+
| Quiz side panel (experimental) | `present_question` for the model; `panel_sync` and `panel_answer`, called only by the panel mod, never by the model; its header stacks the mark above the topic in a pane under 60 cells wide; on the desktop app each option's note is its own line under the option, and a click on the option picks it; `/eklavya-panel-demo` opens a mock question the panel answers itself, calling none of these tools |
|
|
56
56
|
| Settings | `get_config`, `set_config` |
|
|
57
57
|
| Find and read memory (`memory_status` includes a `health` verdict with reasons) | `memory_search`, `memory_get`, `memory_timeline`, `memory_file_history`, `memory_status` |
|
|
58
58
|
| Maintain memory | `memory_write`, `memory_correct`, `memory_delete`, `memory_collections` |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eklavya",
|
|
3
3
|
"displayName": "Eklavya",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.86.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,6 +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
60
|
Run `scripts/test-panel-mod.sh`: it stages the mod as its own plugin, validates
|
|
60
61
|
it, runs `claude plugin test`, then validates the distributed plugin.
|
|
61
62
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ClientModule } from 'claude-code'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One answer card drawn as a `Client` region, so a press anywhere on it (border,
|
|
5
|
+
* padding, title or note) picks it. A `Button` is only as large as its label, and a
|
|
6
|
+
* `Box` takes no press, so this is the one way to make the whole card the target.
|
|
7
|
+
*
|
|
8
|
+
* It gets no mods API: it holds only its hover and posts `{ pick: id }` to the hooks
|
|
9
|
+
* module, whose `ui.message` hook validates the id and records the pick. The colours
|
|
10
|
+
* come in as props, so the module imports nothing.
|
|
11
|
+
*/
|
|
12
|
+
export type CardProps = {
|
|
13
|
+
id: string
|
|
14
|
+
label: string
|
|
15
|
+
note: string
|
|
16
|
+
chosen: boolean
|
|
17
|
+
locked: boolean
|
|
18
|
+
brand: string
|
|
19
|
+
fill: string
|
|
20
|
+
text: string
|
|
21
|
+
noteText: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const Card: ClientModule<CardProps, { hover: boolean }> = (p, s) => {
|
|
25
|
+
const { Box, Text } = s.elements
|
|
26
|
+
const hover = s.state?.hover ?? false
|
|
27
|
+
// One listener, replaced on each call, so it always reads this render's props.
|
|
28
|
+
s.onPointer(e => {
|
|
29
|
+
if (e.type === 'enter' && !hover) s.setState({ hover: true })
|
|
30
|
+
else if (e.type === 'leave' && hover) s.setState({ hover: false })
|
|
31
|
+
else if (e.type === 'up' && !p.locked && (e.button === undefined || e.button === 'left')) s.post({ pick: p.id })
|
|
32
|
+
})
|
|
33
|
+
return Box({
|
|
34
|
+
flexDirection: 'column',
|
|
35
|
+
paddingX: 2,
|
|
36
|
+
paddingY: 1,
|
|
37
|
+
borderStyle: p.chosen ? 'bold' : 'single',
|
|
38
|
+
borderColor: p.chosen || (hover && !p.locked) ? p.brand : 'inactive',
|
|
39
|
+
backgroundColor: p.chosen ? p.fill : undefined,
|
|
40
|
+
children: [
|
|
41
|
+
Text({ wrap: 'wrap', bold: p.chosen, color: p.chosen ? p.text : undefined, children: [p.label] }),
|
|
42
|
+
Text({ wrap: 'wrap', dimColor: !p.chosen, color: p.chosen ? p.noteText : undefined, children: [p.note] }),
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default Card
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PanelQuestion, PanelState } from './types'
|
|
1
|
+
import type { PanelQuestion, PanelResult, PanelState } from './types'
|
|
2
2
|
|
|
3
3
|
/** The pane's id, as `$.ui.open` and the `ui.render` hook both name it. */
|
|
4
4
|
export const PANE = 'eklavya-quiz'
|
|
@@ -162,6 +162,15 @@ export const STR = {
|
|
|
162
162
|
focus: 'Ctrl+X then Tab moves the keys here.',
|
|
163
163
|
keys: '1–4 pick · o other · s submit · k skip · t teach me · Esc back to prompt',
|
|
164
164
|
off: 'The quiz panel is off. Turn it on with: eklavya config set quiz.panel true',
|
|
165
|
+
demoBanner: 'DEMO · a mock question. Nothing here is graded by the server or recorded.',
|
|
166
|
+
demoReady: 'Last click: none yet',
|
|
167
|
+
demoOpened: 'Eklavya quiz panel opened with a mock question. Nothing you answer is recorded.',
|
|
168
|
+
demoFailed: 'The demo panel could not open. Try /eklavya-panel-demo again.',
|
|
169
|
+
demoStyleLabel: 'Answer cards:',
|
|
170
|
+
demoStyleButton: 'Native button',
|
|
171
|
+
demoStyleClient: 'Whole card',
|
|
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
|
+
demoTyped: 'Typed answers are not graded in the demo. (Nothing was sent or recorded.)',
|
|
165
174
|
} as const
|
|
166
175
|
|
|
167
176
|
/** The explainer agent the panel starts itself, so no prompt lands in the transcript. */
|
|
@@ -202,6 +211,39 @@ export function explainerBrief(x: ExplainBlock): string {
|
|
|
202
211
|
|
|
203
212
|
export const answerWas = (label: string): string => `The answer was: ${label}`
|
|
204
213
|
|
|
214
|
+
/** The command that opens the mock question; it never reaches the server, so it needs no question to be waiting. */
|
|
215
|
+
export const DEMO_COMMAND = 'eklavya-panel-demo'
|
|
216
|
+
|
|
217
|
+
/** The mock question: long answers, each with a note, as real questions have. `o3` is the right one. */
|
|
218
|
+
export const DEMO_QUESTION: PanelQuestion = {
|
|
219
|
+
question_id: 'demo',
|
|
220
|
+
repo: '*',
|
|
221
|
+
stem: 'A job writes a text file into a shared folder that another system polls every minute. Why is it safer to write the file under a temporary name and then rename it into place, rather than writing it directly under its final name?',
|
|
222
|
+
options: [
|
|
223
|
+
{ id: 'o1', label: 'Renaming makes the write faster', note: 'A rename skips the disk write, so the final file appears sooner than a direct write would.' },
|
|
224
|
+
{ id: 'o2', label: 'Renaming encrypts the file in transit', note: 'The temporary name keeps the contents protected until the rename publishes the finished file.' },
|
|
225
|
+
{ id: 'o3', label: 'The poller never sees a half-written file', note: 'A rename within one folder is atomic, so the file appears complete or not at all to the reader.' },
|
|
226
|
+
{ id: 'o4', label: 'Renaming lets two jobs write at once', note: 'Each job writes its own temporary name, so both can write the same final file without any conflict.' },
|
|
227
|
+
],
|
|
228
|
+
concept_name: 'Atomic file replace',
|
|
229
|
+
tier: 2,
|
|
230
|
+
more: true,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export const DEMO_RIGHT = 'o3'
|
|
234
|
+
|
|
235
|
+
/** What the server would reply, made here: a demo answer is graded on this machine and recorded nowhere. */
|
|
236
|
+
export function demoResult(kind: 'choice' | 'text' | 'skip' | 'dont_know', optionId: string | null): PanelResult {
|
|
237
|
+
const right = DEMO_QUESTION.options.find(o => o.id === DEMO_RIGHT)!.label
|
|
238
|
+
if (kind === 'skip') return { phase: 'skipped', attempt_id: 0 }
|
|
239
|
+
if (kind === 'dont_know') return { phase: 'answered', attempt_id: 0, taught: true, correct: false, correct_label: right, explanation: STR.demoExplanation }
|
|
240
|
+
const correct = kind === 'choice' && optionId === DEMO_RIGHT
|
|
241
|
+
return { phase: 'answered', attempt_id: 0, correct, correct_label: right, explanation: kind === 'text' ? STR.demoTyped : STR.demoExplanation }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The tester's line of what was just clicked, and how the click arrived. */
|
|
245
|
+
export const demoLast = (what: string): string => `Last click: ${what}`
|
|
246
|
+
|
|
205
247
|
/**
|
|
206
248
|
* The key hints. Only the terminal gets them: there Ctrl+X then Tab moves the keys into the pane. The
|
|
207
249
|
* desktop app keeps typing in its prompt even after a click on the pane (seen 2026-10-16), so a mod's
|
|
@@ -13,7 +13,10 @@ import {
|
|
|
13
13
|
STR,
|
|
14
14
|
WORDMARK,
|
|
15
15
|
CLOSE_MS,
|
|
16
|
+
DEMO_QUESTION,
|
|
16
17
|
answerWas,
|
|
18
|
+
demoLast,
|
|
19
|
+
demoResult,
|
|
17
20
|
explainerBrief,
|
|
18
21
|
codify,
|
|
19
22
|
gradingRequest,
|
|
@@ -30,6 +33,7 @@ import {
|
|
|
30
33
|
hintLines,
|
|
31
34
|
} from './model'
|
|
32
35
|
import type { PanelPending, PanelQuestion, PanelState } from './types'
|
|
36
|
+
import type { CardProps } from './card'
|
|
33
37
|
|
|
34
38
|
/**
|
|
35
39
|
* The Eklavya quiz panel: a pane that shows the question the tutor model handed
|
|
@@ -168,9 +172,24 @@ async function sync($: EngineInterface): Promise<'disabled' | 'none' | 'question
|
|
|
168
172
|
/** A throttled sync for busy events: the heartbeat, and the way a question presented elsewhere finds its pane. */
|
|
169
173
|
async function beat($: EngineInterface): Promise<void> {
|
|
170
174
|
if (Date.now() - lastSyncAt < HEARTBEAT_MS) return
|
|
175
|
+
// A mock question is not in the database: a sync would find nothing waiting and clear it.
|
|
176
|
+
if (await inDemo($)) return
|
|
171
177
|
await sync($)
|
|
172
178
|
}
|
|
173
179
|
|
|
180
|
+
async function inDemo($: EngineInterface): Promise<boolean> {
|
|
181
|
+
const s: PanelState = (await $.state.get(REF)).value ?? EMPTY
|
|
182
|
+
return s.demo !== undefined
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The mock question, drawn with the given answer-card style. Opened from the person's own command, so at any width. */
|
|
186
|
+
async function startDemo($: EngineInterface, style: 'button' | 'client'): Promise<void> {
|
|
187
|
+
disarm()
|
|
188
|
+
openedFor = null
|
|
189
|
+
await update($, quiz, () => ({ ...EMPTY, step: 'awaiting', question: DEMO_QUESTION, demo: { style, last: STR.demoReady } }))
|
|
190
|
+
await $.ui.open({ id: PANE, title: STR.brand, closeOnEscape: true })
|
|
191
|
+
}
|
|
192
|
+
|
|
174
193
|
/** Says what went wrong, in words, and keeps whatever the learner had written. */
|
|
175
194
|
async function fail($: EngineInterface, message: string): Promise<void> {
|
|
176
195
|
await update($, quiz, (s: PanelState) => ({ ...s, step: 'error', message }))
|
|
@@ -203,6 +222,21 @@ async function submit($: EngineInterface, wanted: PanelPending | null): Promise<
|
|
|
203
222
|
}
|
|
204
223
|
}
|
|
205
224
|
|
|
225
|
+
// The mock question is answered here: nothing is sent, graded by the server or recorded.
|
|
226
|
+
if (state.demo) {
|
|
227
|
+
const kind = pending ? pending.kind : state.draft.other ? 'text' : 'choice'
|
|
228
|
+
const result = demoResult(kind, pending ? null : state.draft.picked)
|
|
229
|
+
await update($, quiz, (s: PanelState) => ({
|
|
230
|
+
...s,
|
|
231
|
+
step: result.phase === 'skipped' ? 'skipped' : 'feedback',
|
|
232
|
+
result,
|
|
233
|
+
pending: null,
|
|
234
|
+
message: null,
|
|
235
|
+
demo: s.demo && { ...s.demo, last: demoLast(`${kind} answer submitted`) },
|
|
236
|
+
}))
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
206
240
|
await update($, quiz, (s: PanelState) => ({ ...s, step: 'grading', message: null }))
|
|
207
241
|
const where = await whereAmI($)
|
|
208
242
|
if (epoch !== mine) return
|
|
@@ -284,6 +318,11 @@ export const register: Register = on => {
|
|
|
284
318
|
} catch {
|
|
285
319
|
/* Without the command the pane still opens by itself; only reopening is lost. */
|
|
286
320
|
}
|
|
321
|
+
try {
|
|
322
|
+
await $.command.register({ name: 'eklavya-panel-demo', description: 'Open the Eklavya quiz panel with a mock question, to try its clicks' })
|
|
323
|
+
} catch {
|
|
324
|
+
/* Only the test shortcut is lost. */
|
|
325
|
+
}
|
|
287
326
|
try {
|
|
288
327
|
await sync($)
|
|
289
328
|
} catch {
|
|
@@ -348,6 +387,8 @@ export const register: Register = on => {
|
|
|
348
387
|
on('command.run', { command: 'eklavya-panel' }, async $ => {
|
|
349
388
|
try {
|
|
350
389
|
openedFor = null
|
|
390
|
+
// A mock question on screen gives way to the real state.
|
|
391
|
+
if (await inDemo($)) await update($, quiz, () => EMPTY)
|
|
351
392
|
// The person typed this, so the pane is seated at any width. Off is inert:
|
|
352
393
|
// the server says so and nothing is opened.
|
|
353
394
|
const found = await sync($)
|
|
@@ -359,6 +400,30 @@ export const register: Register = on => {
|
|
|
359
400
|
}
|
|
360
401
|
})
|
|
361
402
|
|
|
403
|
+
// The mock question: opens at once, answered on this machine, so a card or a click can be tried with no real question waiting.
|
|
404
|
+
on('command.run', { command: 'eklavya-panel-demo' }, async $ => {
|
|
405
|
+
try {
|
|
406
|
+
await startDemo($, 'button')
|
|
407
|
+
return { text: STR.demoOpened }
|
|
408
|
+
} catch {
|
|
409
|
+
return { text: STR.demoFailed }
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
// A press on a whole-card `Client` (card.tsx) arrives here as data, never as a fact: only an id of the open question counts.
|
|
414
|
+
on('ui.message', { component: 'Pane' }, async ($, e, next) => {
|
|
415
|
+
if (e.requestId === PANE) {
|
|
416
|
+
try {
|
|
417
|
+
const id = (e.data as { pick?: unknown } | null)?.pick
|
|
418
|
+
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')
|
|
420
|
+
} catch {
|
|
421
|
+
/* Fail open: the card stays as it was. */
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return next(e)
|
|
425
|
+
})
|
|
426
|
+
|
|
362
427
|
// The mod's queued prompts are instructions for the model, not words for the
|
|
363
428
|
// learner: their transcript row shows one short line, as a tool call does.
|
|
364
429
|
// Only the drawing changes; the model still reads the whole message.
|
|
@@ -373,7 +438,7 @@ export const register: Register = on => {
|
|
|
373
438
|
on('ui.render', { component: 'Pane', requestId: 'eklavya-quiz' }, async ($, e, next) => {
|
|
374
439
|
// Only our pane: every other pane, and everything else, is the host's.
|
|
375
440
|
if (e.requestId !== PANE) return next(e)
|
|
376
|
-
const { Box, Text, Button, Input, Markdown } = $.ui.resolve(e) as any
|
|
441
|
+
const { Box, Text, Button, Input, Markdown, Client } = $.ui.resolve(e) as any
|
|
377
442
|
const s: PanelState = await read($, quiz)
|
|
378
443
|
const q = s.question
|
|
379
444
|
|
|
@@ -454,9 +519,24 @@ export const register: Register = on => {
|
|
|
454
519
|
{body}
|
|
455
520
|
</Box>
|
|
456
521
|
)
|
|
522
|
+
// The mock question's tester bar: what it is, the last click, and (desktop only) how the answer cards are drawn.
|
|
523
|
+
const demoBar = s.demo ? (
|
|
524
|
+
<Box key="demo-bar" flexDirection="column" gap={app ? 1 : 0}>
|
|
525
|
+
<Text color="warning">{STR.demoBanner}</Text>
|
|
526
|
+
<Text dimColor>{s.demo.last}</Text>
|
|
527
|
+
{app ? (
|
|
528
|
+
<Box flexDirection="row" gap={2} alignItems="center">
|
|
529
|
+
<Text dimColor>{STR.demoStyleLabel}</Text>
|
|
530
|
+
<Button key="style-button" label={STR.demoStyleButton} variant={s.demo.style === 'button' ? 'primary' : 'secondary'} onPress={() => setStyle($, 'button')} />
|
|
531
|
+
<Button key="style-client" label={STR.demoStyleClient} variant={s.demo.style === 'client' ? 'primary' : 'secondary'} onPress={() => setStyle($, 'client')} />
|
|
532
|
+
</Box>
|
|
533
|
+
) : null}
|
|
534
|
+
</Box>
|
|
535
|
+
) : null
|
|
457
536
|
const frame = (...body: unknown[]) => (
|
|
458
537
|
<Box flexDirection="column" paddingX={1} gap={1}>
|
|
459
538
|
{head(q ? topicLabel(q) : undefined)}
|
|
539
|
+
{demoBar}
|
|
460
540
|
{body}
|
|
461
541
|
{hint}
|
|
462
542
|
</Box>
|
|
@@ -520,8 +600,14 @@ export const register: Register = on => {
|
|
|
520
600
|
{note && !app ? <Text wrap="wrap" dimColor={!chosen} color={chosen ? PICKED.note : undefined}>{`\n ${note}`}</Text> : null}
|
|
521
601
|
</Button>
|
|
522
602
|
)
|
|
603
|
+
const wholeCard = app && s.demo?.style === 'client'
|
|
523
604
|
const options = q.options.map((o, i) => {
|
|
524
605
|
const chosen = !s.draft.other && s.draft.picked === o.id
|
|
606
|
+
// The whole card is one pressable region, so a click on its padding or note picks too (card.tsx).
|
|
607
|
+
if (wholeCard) {
|
|
608
|
+
const props: CardProps = { id: o.id, label: plainLabel(o.label), note: o.note ? plainLabel(o.note) : '', chosen, locked, brand: BRAND, fill: PICKED.bg, text: PICKED.text, noteText: PICKED.note }
|
|
609
|
+
return <Client key={`card-${o.id}`} module="./card.tsx" width="100%" props={props} />
|
|
610
|
+
}
|
|
525
611
|
return card(
|
|
526
612
|
o.id,
|
|
527
613
|
chosen,
|
|
@@ -591,9 +677,18 @@ export const register: Register = on => {
|
|
|
591
677
|
})
|
|
592
678
|
}
|
|
593
679
|
|
|
680
|
+
/** The demo's switch between the two ways of drawing an answer card on the desktop. */
|
|
681
|
+
async function setStyle($: EngineInterface, style: 'button' | 'client'): Promise<void> {
|
|
682
|
+
await update($, quiz, (s: PanelState) => (s.demo ? { ...s, demo: { ...s.demo, style, last: demoLast(`card style set to ${style}`) } } : s))
|
|
683
|
+
}
|
|
684
|
+
|
|
594
685
|
/** Selecting an option records a draft and nothing else: it never submits, grades or skips. */
|
|
595
|
-
async function pick($: EngineInterface, id: string): Promise<void> {
|
|
596
|
-
await update($, quiz, (s: PanelState) =>
|
|
686
|
+
async function pick($: EngineInterface, id: string, via = 'button'): Promise<void> {
|
|
687
|
+
await update($, quiz, (s: PanelState) =>
|
|
688
|
+
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})`) } } : {}) }
|
|
690
|
+
: s,
|
|
691
|
+
)
|
|
597
692
|
try {
|
|
598
693
|
// The host's focus mark follows the pick, so the highlighted answer is the chosen one even after a hotkey.
|
|
599
694
|
await $.ui.focus({ requestId: PANE, key: `opt-${id}` })
|
|
@@ -675,6 +770,9 @@ async function askNext($: EngineInterface): Promise<void> {
|
|
|
675
770
|
disarm()
|
|
676
771
|
try {
|
|
677
772
|
openedFor = null
|
|
773
|
+
const now: PanelState = (await $.state.get(REF)).value ?? EMPTY
|
|
774
|
+
// Next in the demo starts the mock again: it must not ask the model for a real question.
|
|
775
|
+
if (now.demo) return await startDemo($, now.demo.style)
|
|
678
776
|
await update($, quiz, () => ({ ...EMPTY, step: 'loading' }))
|
|
679
777
|
await $.prompt.submit({ text: NEXT_PROMPT })
|
|
680
778
|
} catch {
|
|
@@ -55,6 +55,11 @@ export type PanelState = {
|
|
|
55
55
|
result: PanelResult | null
|
|
56
56
|
/** Words for the learner: why the pane is in `error`, or a one-line notice in `none`. */
|
|
57
57
|
message: string | null
|
|
58
|
+
/**
|
|
59
|
+
* Set only by `/eklavya-panel-demo`: the question is a mock, answered on this machine and never sent to the
|
|
60
|
+
* server. `style` is how the desktop draws the answer cards; `last` is the latest click, for the tester.
|
|
61
|
+
*/
|
|
62
|
+
demo?: { style: 'button' | 'client'; last: string }
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
declare module 'claude-code' {
|
package/package.json
CHANGED