@bojackduy/opencode-learn 1.0.0 → 1.1.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@bojackduy/opencode-learn",
4
- "version": "1.0.0",
4
+ "version": "1.1.1",
5
5
  "description": "Pi learn system for OpenCode — Socratic teaching, graded quiz, Obsidian md_log, and visual makers. Port of amosblomqvist/learn (video: How I Use AI to Learn Things) to OpenCode.",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -99,8 +99,14 @@ function QuizDialog(props: {
99
99
  const theme = () => props.api.theme.current
100
100
  const syntax = () => syntaxStyle(theme())
101
101
  const dims = useTerminalDimensions()
102
- const popupWidth = () => Math.max(68, Math.min(dims().width - 4, 92))
103
- const popupHeight = () => Math.max(4, Math.min(24, dims().height - 2))
102
+ const popupWidth = () => {
103
+ const w = dims().width
104
+ return Math.max(62, Math.min(w - 8, Math.floor(w * 0.80), 92))
105
+ }
106
+ const popupHeight = () => {
107
+ const h = dims().height
108
+ return Math.max(14, Math.min(h - 6, Math.floor(h * 0.62), 26))
109
+ }
104
110
  const options = () => props.request.options
105
111
  const correctSet = new Set(props.request.correctIndices)
106
112
  const isMulti = () => !!props.request.multiSelect
@@ -280,9 +286,9 @@ function QuizDialog(props: {
280
286
  }
281
287
  // Note focused: handle Tab/Esc/Enter to exit note, otherwise let input handle typing
282
288
  if (focused() === "note") {
283
- if (key === "tab" || seq === "\t") { prevent(evt); setFocused("options"); return }
284
- if (key === "escape" || key === "esc") { prevent(evt); setFocused("options"); return }
285
- if (key === "enter" && (evt.ctrl || evt.meta)) { prevent(evt); setFocused("options"); return }
289
+ if (lower === "tab" || seq === "\t") { prevent(evt); setFocused("options"); return }
290
+ if (lower === "escape" || lower === "esc") { prevent(evt); setFocused("options"); return }
291
+ if (lower === "enter" && (evt.ctrl || evt.meta)) { prevent(evt); setFocused("options"); return }
286
292
  // Allow typing to go to input; don't prevent
287
293
  return
288
294
  }
@@ -297,11 +303,11 @@ function QuizDialog(props: {
297
303
  if (note().trim() && !selected().size && !dontKnow()) return dontKnowIdx() + 1
298
304
  return dontKnowIdx()
299
305
  }
300
- if (key === "up" || key === "k" || seq === "\x1b[A") { prevent(evt); setOptionIndex(i => Math.max(0, i - 1)); return }
301
- if (key === "down" || key === "j" || seq === "\x1b[B") { prevent(evt); setOptionIndex(i => Math.min(maxIdx(), i + 1)); return }
302
- if (key === "tab" || seq === "\t") { prevent(evt); setFocused("note"); return }
303
- if (key === "escape" || key === "esc") { prevent(evt); props.onCancel(); return }
304
- if (key === "space" || seq === " ") {
306
+ if (lower === "up" || isPlainKey(evt,"k") || seq === "\x1b[A") { prevent(evt); setOptionIndex(i => Math.max(0, i - 1)); return }
307
+ if (lower === "down" || isPlainKey(evt,"j") || seq === "\x1b[B") { prevent(evt); setOptionIndex(i => Math.min(maxIdx(), i + 1)); return }
308
+ if (lower === "tab" || seq === "\t") { prevent(evt); setFocused("note"); return }
309
+ if (lower === "escape" || lower === "esc") { prevent(evt); props.onCancel(); return }
310
+ if (lower === "space" || seq === " ") {
305
311
  prevent(evt)
306
312
  const idx = optionIndex()
307
313
  if (idx === dontKnowIdx()) handleDontKnow()
@@ -315,7 +321,7 @@ function QuizDialog(props: {
315
321
  }
316
322
  return
317
323
  }
318
- if (key === "enter" || seq === "\r") {
324
+ if (lower === "enter" || seq === "\r") {
319
325
  prevent(evt)
320
326
  const idx = optionIndex()
321
327
  if (idx === dontKnowIdx()) handleDontKnow()
@@ -327,7 +333,7 @@ function QuizDialog(props: {
327
333
  }
328
334
  return
329
335
  }
330
- if (seq === "ctrl+j" || (key === "enter" && (evt as any).ctrl)) {
336
+ if (seq === "ctrl+j" || (lower === "enter" && (evt as any).ctrl)) {
331
337
  prevent(evt); submitSelect(); return
332
338
  }
333
339
  })
@@ -339,6 +345,14 @@ function QuizDialog(props: {
339
345
  <text fg={theme().background} bold>{phase() === "feedback" ? (feedback()?.correct ? "✓ CORRECT" : dontKnow() ? "○ I DON'T KNOW" : "✗ INCORRECT") : isMulti() ? "☑ QUIZ · MULTI-SELECT" : "● QUIZ · SINGLE" }</text>
340
346
  <text fg={theme().background} dim>learn</text>
341
347
  </box>
348
+ {/* Pinned scroll cue — header-anchored, high contrast so user instantly knows explanation is below */}
349
+ <Show when={phase()==="feedback" && (canScrollUp() || canScrollDown())}>
350
+ <box justifyContent="center" height={1} backgroundColor={canScrollDown() ? theme().warning : theme().accent} paddingLeft={1} paddingRight={1}>
351
+ <text fg={theme().background} bold>
352
+ {canScrollUp() && canScrollDown() ? "▲ more above · ▼ more below — d / u to scroll" : canScrollDown() ? "▼ more below — press d to see explanation" : "▲ more above — press u to scroll up"}
353
+ </text>
354
+ </box>
355
+ </Show>
342
356
 
343
357
  <scrollbox ref={(el:any)=> scrollRef = el} flexGrow={1} verticalScrollbarOptions={{ visible: true, trackOptions: { backgroundColor: theme().background, foregroundColor: theme().borderActive } }}>
344
358
  {/* Question — use opencode markdown render so ```python blocks get syntax coloring like native messages */}
@@ -459,14 +473,10 @@ function QuizDialog(props: {
459
473
  <box height={1} justifyContent="center">
460
474
  <text fg={theme().textMuted} wrapMode="wrap">
461
475
  {phase() === "feedback"
462
- ? (canScrollUp() && canScrollDown() ? "▲ more above · ▼ more below — d/u to scroll · Enter to continue"
463
- : canScrollDown() ? "▼ more below — d to scroll · Enter to continue"
464
- : canScrollUp() ? "▲ more above — u to scroll · Enter to continue"
465
- : "↵ Enter / Esc to continue → next probe")
476
+ ? (canScrollUp() && canScrollDown() ? <><span style={{fg: theme.warning, bold: true}}>▲ more above · ▼ more below</span><span style={{fg: theme().textMuted}}> — d/u to scroll · Enter to continue</span></> : canScrollDown() ? <><span style={{fg: theme.warning, bold: true}}>▼ more below</span><span style={{fg: theme().textMuted}}> — d to scroll · Enter to continue</span></> : canScrollUp() ? <><span style={{fg: theme.accent, bold: true}}>▲ more above</span><span style={{fg: theme().textMuted}}> — u to scroll · Enter to continue</span></> : "↵ Enter / Esc to continue → next probe")
466
477
  : phase() === "classifying" ? "Classifying your note..."
467
478
  : focused() === "note" ? "Enter submit note → classify · Tab/Esc back"
468
- : (canScrollUp() || canScrollDown()) ? "j/k or ↑↓ move · Space toggle · Tab note · Enter submit · Esc cancel · d/u scroll"
469
- : "j/k or ↑↓ move · Space toggle · Tab note · Enter submit · Esc cancel"}
479
+ : (canScrollUp() || canScrollDown()) ? <><span style={{fg: theme().textMuted}}>j/k or ↑↓ move · Space toggle · Tab note · Enter submit · Esc cancel</span><span style={{fg: theme.warning, bold: true}}> · d/u scroll</span></> : "j/k or ↑↓ move · Space toggle · Tab note · Enter submit · Esc cancel"}
470
480
  </text>
471
481
  </box>
472
482
  </box>
@@ -483,8 +493,14 @@ function QuizBatchDialog(props: {
483
493
  const theme = () => props.api.theme.current
484
494
  const syntax = () => syntaxStyle(theme())
485
495
  const dims = useTerminalDimensions()
486
- const popupWidth = () => Math.max(68, Math.min(dims().width - 4, 96))
487
- const popupHeight = () => Math.max(4, Math.min(24, dims().height - 2))
496
+ const popupWidth = () => {
497
+ const w = dims().width
498
+ return Math.max(64, Math.min(w - 8, Math.floor(w * 0.82), 96))
499
+ }
500
+ const popupHeight = () => {
501
+ const h = dims().height
502
+ return Math.max(14, Math.min(h - 6, Math.floor(h * 0.64), 28))
503
+ }
488
504
  const [idx, setIdx] = createSignal(0)
489
505
  // Guard: if no quizzes, cancel
490
506
  if (!props.request.quizzes || props.request.quizzes.length === 0) {
@@ -638,16 +654,16 @@ function QuizBatchDialog(props: {
638
654
  if (lower==="pageup"||seq==="\x1b[5~"){ prevent(evt); try{scrollRefBatch?.scrollBy(-scrollAmountBatch()); setTimeout(updateScrollBatch,30)}catch{} return }
639
655
  if (lower==="pagedown"||seq==="\x1b[6~"){ prevent(evt); try{scrollRefBatch?.scrollBy(scrollAmountBatch()); setTimeout(updateScrollBatch,30)}catch{} return }
640
656
  if(lower==="enter"||seq==="\r"||lower==="escape"||lower==="esc"){ prevent(evt); goNext() } return }
641
- if(focused()==="note"){ if(k==="tab"||seq==="\t"){prevent(evt); setFocused("options"); return} if(k==="escape"){prevent(evt); setFocused("options"); return} return }
657
+ if(focused()==="note"){ if(lower==="tab"||seq==="\t"){prevent(evt); setFocused("options"); return} if(lower==="escape"||lower==="esc"){prevent(evt); setFocused("options"); return} return }
642
658
  if(phase()==="select" && (isPlainKeyBatch(evt,"d")||seq==="\x04")){ prevent(evt); try{scrollRefBatch?.scrollBy(scrollAmountBatch()); setTimeout(updateScrollBatch,30); setTimeout(updateScrollBatch,120)}catch{} return }
643
659
  if(phase()==="select" && (isPlainKeyBatch(evt,"u")||seq==="\x15")){ prevent(evt); try{scrollRefBatch?.scrollBy(-scrollAmountBatch()); setTimeout(updateScrollBatch,30); setTimeout(updateScrollBatch,120)}catch{} return }
644
- if(k==="up"||k==="k"||seq==="\x1b[A"){prevent(evt); setOptionIndex(i=>Math.max(0,i-1)); return}
645
- if(k==="down"||k==="j"||seq==="\x1b[B"){prevent(evt); setOptionIndex(i=>Math.min(isMulti()?submitIdx():dontKnowIdx(),i+1)); return}
646
- if(k==="tab"||seq==="\t"){prevent(evt); setFocused("note"); return}
647
- if(k==="escape"||k==="esc"){prevent(evt); props.onCancel(); return}
648
- if(k==="space"||seq===" "){prevent(evt); const i=optionIndex(); if(i===dontKnowIdx()){ const willBe=!dontKnow(); setDontKnow(willBe); if(willBe) setSelected(new Map()); } else if(isMulti() && i===submitIdx()) submitSelect(); else if(isMulti()) toggle(i); else { const o=cur().options[i]; if(o){setSelected(new Map([[`opt:${i}`,{label:o.label,value:o.value,index:i+1}]])); setDontKnow(false); submitSelect()} } return}
649
- if(k==="enter"||seq==="\r"){prevent(evt); const i=optionIndex(); if(i===dontKnowIdx()){ const willBe=!dontKnow(); setDontKnow(willBe); if(willBe) setSelected(new Map()); } else if(isMulti()) submitSelect(); else { const o=cur().options[i]; if(o){setSelected(new Map([[`opt:${i}`,{label:o.label,value:o.value,index:i+1}]])); setDontKnow(false); submitSelect()} } return}
650
- if(seq==="ctrl+j" || (k==="enter" && evt.ctrl)){prevent(evt); submitSelect(); return}
660
+ if(lower==="up"||isPlainKeyBatch(evt,"k")||seq==="\x1b[A"){prevent(evt); setOptionIndex(i=>Math.max(0,i-1)); return}
661
+ if(lower==="down"||isPlainKeyBatch(evt,"j")||seq==="\x1b[B"){prevent(evt); setOptionIndex(i=>Math.min(isMulti()?submitIdx():dontKnowIdx(),i+1)); return}
662
+ if(lower==="tab"||seq==="\t"){prevent(evt); setFocused("note"); return}
663
+ if(lower==="escape"||lower==="esc"){prevent(evt); props.onCancel(); return}
664
+ if(lower==="space"||seq===" "){prevent(evt); const i=optionIndex(); if(i===dontKnowIdx()){ const willBe=!dontKnow(); setDontKnow(willBe); if(willBe) setSelected(new Map()); } else if(isMulti() && i===submitIdx()) submitSelect(); else if(isMulti()) toggle(i); else { const o=cur().options[i]; if(o){setSelected(new Map([[`opt:${i}`,{label:o.label,value:o.value,index:i+1}]])); setDontKnow(false); submitSelect()} } return}
665
+ if(lower==="enter"||seq==="\r"){prevent(evt); const i=optionIndex(); if(i===dontKnowIdx()){ const willBe=!dontKnow(); setDontKnow(willBe); if(willBe) setSelected(new Map()); } else if(isMulti()) submitSelect(); else { const o=cur().options[i]; if(o){setSelected(new Map([[`opt:${i}`,{label:o.label,value:o.value,index:i+1}]])); setDontKnow(false); submitSelect()} } return}
666
+ if(seq==="ctrl+j" || (lower==="enter" && evt.ctrl)){prevent(evt); submitSelect(); return}
651
667
  } catch(e){ tlog("useKeyboard batch failed", String(e)) }
652
668
  })
653
669
  return (
@@ -656,6 +672,14 @@ function QuizBatchDialog(props: {
656
672
  <text fg={theme().background} bold> decks.quiz batch {idx()+1}/{props.request.quizzes.length} {phase()==="feedback"?(feedback()?.correct?"✓":"✗"):""}</text>
657
673
  <text fg={theme().background} dim>learn</text>
658
674
  </box>
675
+ {/* Pinned scroll cue — header-anchored, high contrast */}
676
+ <Show when={phase()==="feedback" && (canScrollUpBatch() || canScrollDownBatch())}>
677
+ <box justifyContent="center" height={1} backgroundColor={canScrollDownBatch() ? theme().warning : theme().accent} paddingLeft={1} paddingRight={1}>
678
+ <text fg={theme().background} bold>
679
+ {canScrollUpBatch() && canScrollDownBatch() ? "▲ more above · ▼ more below — d / u to scroll" : canScrollDownBatch() ? "▼ more below — press d to see explanation" : "▲ more above — press u to scroll up"}
680
+ </text>
681
+ </box>
682
+ </Show>
659
683
  <scrollbox ref={(el:any)=> scrollRefBatch = el} flexGrow={1} verticalScrollbarOptions={{ visible: true, trackOptions: { backgroundColor: theme().background, foregroundColor: theme().borderActive } }}>
660
684
  <markdown syntaxStyle={syntax()} content={decodeQuizText(cur().question)} fg={theme().text} bg={theme().backgroundPanel} />
661
685
  <Show when={cur().details}><markdown syntaxStyle={syntax()} content={decodeQuizText(cur().details)} fg={theme().textMuted} bg={theme().backgroundPanel} /></Show>
@@ -687,7 +711,7 @@ function QuizBatchDialog(props: {
687
711
  </scrollbox>
688
712
  <box height={1} justifyContent="center">
689
713
  <text fg={theme().textMuted} wrapMode="wrap">
690
- {phase()==="feedback" ? (canScrollUpBatch() && canScrollDownBatch() ? `▲ more above · ▼ more below — d/u to scroll · Enter → next (${idx()+1}/${props.request.quizzes.length})` : canScrollDownBatch() ? `▼ more below — d to scroll · Enter → next (${idx()+1}/${props.request.quizzes.length})` : canScrollUpBatch() ? `▲ more above — u to scroll · Enter → next (${idx()+1}/${props.request.quizzes.length})` : `Enter → next (${idx()+1}/${props.request.quizzes.length})`) : phase()==="classifying" ? "Classifying your note..." : focused()==="note" ? "Enter submit note → classify · Tab/Esc back" : (canScrollUpBatch() || canScrollDownBatch()) ? "j/k or ↑↓ move · Space toggle · Tab note · Enter submit · Esc cancel · d/u scroll" : "j/k or ↑↓ move · Space toggle · Tab note · Enter submit · Esc cancel"}
714
+ {phase()==="feedback" ? (canScrollUpBatch() && canScrollDownBatch() ? <><span style={{fg: theme.warning, bold: true}}>▲ more above · ▼ more below</span><span style={{fg: theme().textMuted}}> — d/u to scroll · Enter → next ({idx()+1}/{props.request.quizzes.length})</span></> : canScrollDownBatch() ? <><span style={{fg: theme.warning, bold: true}}>▼ more below</span><span style={{fg: theme().textMuted}}> — d to scroll · Enter → next ({idx()+1}/{props.request.quizzes.length})</span></> : canScrollUpBatch() ? <><span style={{fg: theme.accent, bold: true}}>▲ more above</span><span style={{fg: theme().textMuted}}> — u to scroll · Enter → next ({idx()+1}/{props.request.quizzes.length})</span></> : `Enter → next (${idx()+1}/${props.request.quizzes.length})`) : phase()==="classifying" ? "Classifying your note..." : focused()==="note" ? "Enter submit note → classify · Tab/Esc back" : (canScrollUpBatch() || canScrollDownBatch()) ? <><span style={{fg: theme().textMuted}}>j/k or ↑↓ move · Space toggle · Tab note · Enter submit · Esc cancel</span><span style={{fg: theme.warning, bold: true}}> · d/u scroll</span></> : "j/k or ↑↓ move · Space toggle · Tab note · Enter submit · Esc cancel"}
691
715
  </text>
692
716
  </box>
693
717
  </box>