@bojackduy/opencode-learn 1.4.1 → 1.4.2

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/tui.js CHANGED
@@ -2677,8 +2677,27 @@ var tui = async (api) => {
2677
2677
  return;
2678
2678
  let current = currentBySession.get(curSid);
2679
2679
  if (current) {
2680
- refreshPopupClaim(current.id);
2681
- return;
2680
+ const pendingStillExists = (() => {
2681
+ try {
2682
+ return fs.readdirSync(pendingDir).some((f) => f === `quiz-${current.id}.json` || f === `quiz_batch-${current.id}.json`);
2683
+ } catch {
2684
+ return true;
2685
+ }
2686
+ })();
2687
+ if (!pendingStillExists) {
2688
+ tlog("processPending stale current cleared (pending gone)", current.id);
2689
+ releasePopupClaim(current.id);
2690
+ currentBySession.delete(curSid);
2691
+ current = undefined;
2692
+ } else if (api.ui.dialog.open) {
2693
+ refreshPopupClaim(current.id);
2694
+ return;
2695
+ } else {
2696
+ tlog("processPending stale current cleared (dialog no longer open)", current.id);
2697
+ releasePopupClaim(current.id);
2698
+ currentBySession.delete(curSid);
2699
+ current = undefined;
2700
+ }
2682
2701
  }
2683
2702
  if (api.ui.dialog.open)
2684
2703
  return;
@@ -2699,7 +2718,8 @@ var tui = async (api) => {
2699
2718
  return null;
2700
2719
  }
2701
2720
  }).filter(Boolean).filter((x) => !hasAnswerArtifact(x.j.id));
2702
- const pick = matching.find((x) => x.j.sessionID === curSid) || matching.find((x) => !x.j.sessionID);
2721
+ const byNewest = [...matching].sort((a, b) => (b.j?.timestamp || 0) - (a.j?.timestamp || 0));
2722
+ const pick = byNewest.find((x) => x.j.sessionID === curSid) || byNewest.find((x) => !x.j.sessionID);
2703
2723
  if (!pick)
2704
2724
  return;
2705
2725
  const file = pick.f;
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.4.1",
4
+ "version": "1.4.2",
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": "AGPL-3.0-or-later",
@@ -812,14 +812,36 @@ export const tui: TuiPlugin = async (api) => {
812
812
  const curSid = getCurrentSessionID()
813
813
  if (!curSid) return
814
814
  let current = currentBySession.get(curSid) as { id: string; type: string } | undefined
815
- if (current) { refreshPopupClaim(current.id); return }
815
+ if (current) {
816
+ // Self-heal: the tracked dialog may have been dismissed through a path other than
817
+ // done()/cancel() (session/tab switch, TUI reconnect, dialog replaced externally),
818
+ // or its pending file may have been consumed externally. A stuck entry would block
819
+ // ALL future quizzes for this session forever — release it and rediscover below.
820
+ const pendingStillExists = (() => { try { return fs.readdirSync(pendingDir).some((f) => f === `quiz-${current!.id}.json` || f === `quiz_batch-${current!.id}.json`) } catch { return true } })()
821
+ if (!pendingStillExists) {
822
+ tlog("processPending stale current cleared (pending gone)", current.id)
823
+ releasePopupClaim(current.id)
824
+ currentBySession.delete(curSid)
825
+ current = undefined
826
+ } else if (api.ui.dialog.open) { refreshPopupClaim(current.id); return }
827
+ else {
828
+ tlog("processPending stale current cleared (dialog no longer open)", current.id)
829
+ releasePopupClaim(current.id)
830
+ currentBySession.delete(curSid)
831
+ current = undefined
832
+ }
833
+ }
816
834
  if (api.ui.dialog.open) return
817
835
  let files: string[] = []
818
836
  try { files = fs.readdirSync(pendingDir).filter(f => f.endsWith(".json") && !f.startsWith("response-") && !f.startsWith(".") && !f.startsWith("classify")).sort() } catch { return }
819
837
  // Session-distinct: only show pending for current session.
820
838
  // Skip answered-pending (a response file exists, server is consuming): prevents re-popup after answer.
821
839
  const matching = files.map(f => { try { const j = JSON.parse(fs.readFileSync(path.join(pendingDir, f), "utf8")) as any; return { f, j } } catch { return null } }).filter(Boolean).filter(x => !hasAnswerArtifact(x!.j.id)) as Array<{f: string, j: any}>
822
- const pick = matching.find(x => x.j.sessionID === curSid) || matching.find(x => !x.j.sessionID)
840
+ // Newest-first: when several quizzes stack up for the same session (e.g. earlier ones
841
+ // answered manually in chat after a popup failure), the LATEST quiz is the live one the
842
+ // agent is waiting on. Alphabetical order would keep re-showing the oldest stuck quiz.
843
+ const byNewest = [...matching].sort((a, b) => (((b as any).j?.timestamp || 0) as number) - (((a as any).j?.timestamp || 0) as number))
844
+ const pick = byNewest.find(x => x.j.sessionID === curSid) || byNewest.find(x => !x.j.sessionID)
823
845
  if (!pick) return
824
846
  const file = pick.f
825
847
  const full = path.join(pendingDir, file)