dsh-nerd-mode 0.4.0 → 0.4.3

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
@@ -12,18 +12,18 @@
12
12
  <a href="./README.zh.md"><img src="https://img.shields.io/badge/%E4%B8%AD%E6%96%87-switch-d0d7de" alt="中文"></a>
13
13
  </p>
14
14
 
15
- A chat lesson usually ends when the answer arrives. Nothing checks that you understood it, and nothing survives the tab closing. Nerd Mode makes dsh teach in steps, stop where the choice changes the result, and keep a dated record when a subject deserves one. A topic can then last weeks instead of one sitting.
15
+ A chat lesson usually ends when the answer arrives. Nothing checks that you understood it, and nothing survives the tab closing. Nerd Mode makes dsh teach in steps, stop where the choice changes the result, and keep a dated record when a subject deserves one. A Course can then last weeks instead of one sitting.
16
16
 
17
17
  ## Key features
18
18
 
19
19
  - One switch for the whole conversation. `/nerd-mode on` teaches every learning-shaped request and leaves feature work, searches, and commands alone.
20
- - Two ways to learn. `/study` answers one concept or one task in chat and keeps nothing. `/study-plus` runs one topic over weeks with a dated record.
20
+ - Two ways to learn. `/study` answers one concept or one task in chat and keeps nothing. `/study-plus` runs one Course over weeks with a dated record.
21
21
  - A map before the first lesson. A concept graph in prerequisite order, grouped into a Book of Chapters and Sections, approved by you before any file is written.
22
- - One calibration round per topic. It asks your goal, your background, and the concepts you can skip, then sets a teaching level that every lecture follows.
22
+ - One question round before teaching. It offers choices for your goal, your background, and whether you want one complete answer or a tracked Course; a tracked Course adds the concepts you can skip. The answers set a teaching level that every lecture follows.
23
23
  - Teaching that stops at the fork. The guide explains in steps and stops where the choice changes the result, instead of handing over a finished answer.
24
24
  - Evidence, not impressions. Unseen questions and a graded teach-back, with the rubric written before the question. Only an answer you produced changes the record.
25
25
  - A record that outlives the chat. The graph, the states, and the dated log survive restarts, and the HTML Book rebuilds after each batch and each lesson, with full-text search.
26
- - Lectures you can find again. The Lectures card lists every saved lecture in the workspace, grouped by topic.
26
+ - Lectures you can find again. The Lectures card lists every saved lecture in the workspace, grouped by Course.
27
27
 
28
28
  ## Requirements
29
29
 
@@ -80,15 +80,15 @@ Use it for a question you want to understand now and will probably not return to
80
80
 
81
81
  Type the command, or turn the switch on and just ask. With the switch on, a learning-shaped request gets this shape by default.
82
82
 
83
- An explicit `/study-plus` goes straight to tracked study, even when the request names several concepts. An explicit `/study`, or a request that arrives through Nerd Mode without a command, asks one question when it names two or more independent concepts: a one-off book-style answer in chat, or a tracked `/study-plus` session. The one-off answer uses a title, chapter headings, and section headings in plain Markdown.
83
+ An explicit `/study-plus` goes straight to tracked study, even when the request names several concepts. An explicit `/study` goes straight to one-off teaching. With the switch on and no command, a learning request gets one question round first: your goal and your background as single choices, and whether you want one complete answer in this conversation or a tracked `/study-plus` session. The one-off answer uses a title, chapter headings, and section headings in plain Markdown.
84
84
 
85
85
  You get a title, the central idea, numbered steps, one line of symbols above each formula, and a short table only when four or more symbols keep coming back. Then one worked example, the common mistake, and the single stop at the fork. For a task, you get the finished artifact with its reasoning.
86
86
 
87
- It does not score, keep a learner record, or continue a topic across sessions. A saved lecture or artifact goes only where you ask, at the path you name.
87
+ It does not score, keep a learner record, or continue a Course across sessions. A saved lecture or artifact goes only where you ask, at the path you name.
88
88
 
89
89
  ### Long lesson: `/study-plus`
90
90
 
91
- It organizes one topic as a Book of Chapters and Sections, with one concept per Section. It builds a concept graph in prerequisite order, groups the concepts into Chapters, and teaches and assesses one concept in each Concept cycle. The graph carries Chapter and Section columns, so one file records the reading order beside the prerequisite order. It keeps three tracker files and saves one Markdown file per concept under `study/<slug>/lectures/`. Once you approve the graph it prepares a complete draft for every concept, so a lesson starts without waiting. It builds an HTML book from the saved material and refreshes that page whenever the material changes.
91
+ It organizes one Course as a Book of Chapters and Sections, with one concept per Section. It builds a concept graph in prerequisite order, groups the concepts into Chapters, and teaches and assesses one concept in each Concept cycle. The graph carries Chapter and Section columns, so one file records the reading order beside the prerequisite order. It keeps three tracker files and saves one Markdown file per concept under `study/<course>/lectures/`. Once you approve the graph it prepares a complete draft for every concept, so a lesson starts without waiting. It builds an HTML book from the saved material and refreshes that page whenever the material changes.
92
92
 
93
93
  Use it for a subject you will still be learning next month.
94
94
 
@@ -98,11 +98,13 @@ Use it for a subject you will still be learning next month.
98
98
  - You want evidence of progress, not an impression of it.
99
99
  - You keep looking things up and forgetting them.
100
100
 
101
- An explicit `/study-plus` starts tracked study directly and does not ask the one-off book question, even when the topic covers several concepts.
101
+ An explicit `/study-plus` starts tracked study directly and does not ask the one-off book question, even when the request covers several concepts.
102
+
103
+ A Course is one folder under `study/`: one concept graph, one state file, one log, its lectures, its figures, and its Book. The first tracked study in a conversation settles it once. When the workspace holds one Course, `/study-plus` uses it and names it in one line, with no question. When it holds several, one question lists them and offers a new Course. A request about a second subject gets one question: add these concepts to the current Course, or start a new Course.
102
104
 
103
105
  ```text
104
106
  /study-plus Shampoo and Adam optimizers # first session builds the graph
105
- /study-plus # later sessions continue the topic
107
+ /study-plus # later sessions continue the Course
106
108
  /study-plus chapter 2 # read that chapter in Book order
107
109
  /study-plus the matrix square root # jump straight to one concept
108
110
  ```
@@ -110,7 +112,7 @@ An explicit `/study-plus` starts tracked study directly and does not ask the one
110
112
  The first session runs in a fixed order.
111
113
 
112
114
  ```text
113
- /study-plus <topic>
115
+ /study-plus <course>
114
116
  propose 15 to 30 concepts in prerequisite order, grouped into Chapters
115
117
  show the chapter list and the concept table together, then wait
116
118
  you approve, edit, or rebuild the graph
@@ -119,10 +121,10 @@ The first session runs in a fixed order.
119
121
  teach each concept: recall (optional) → lecture → configured unseen questions → teach-back → dated record
120
122
  ```
121
123
 
122
- Approval also runs one calibration round: what you want to do with the topic, what you have already read or used, and the concepts you can skip. The answers set `Teaching level` in `graph.md`, beside `Goal` and `Background`, and every lecture follows that level. It shapes the lecture only, never the questions, the teach-back, or the record. A mid-topic change lasts for that session unless you say to keep it.
124
+ Approval also asks the remaining calibration items: what you want to do with the Course, what you have already read or used, and the concepts you can skip. When the opening question already answered your goal and background, it asks only the concepts you can skip. The answers set `Teaching level` in `graph.md`, beside `Goal` and `Background`, and every lecture follows that level. It shapes the lecture only, never the questions, the teach-back, or the record. A mid-Course change lasts for that session unless you say to keep it.
123
125
 
124
126
  <p align="center">
125
- <img src="assets/calibration.svg" width="720" alt="The pre-course flow. You approve 15 to 30 concepts grouped by chapter, then answer one question round, once per topic: what you want to do with the topic, what you have read or used, and which concepts you can skip. The answers set a teaching level in graph.md, and every lecture follows it. A beginner lecture uses numbered steps, explains every term, and gives an example per step. A standard lecture gives the intuition first, then the formal detail, with examples where they help. An advanced lecture goes in one pass with no numbered steps, starts from the formal statement, and uses at most one example per idea.">
127
+ <img src="assets/calibration.svg" width="720" alt="The pre-course flow. You approve 15 to 30 concepts grouped by chapter, then answer one question round for the Course: what you want to do with it, what you have read or used, and which concepts you can skip. The answers set a teaching level in graph.md, and every lecture follows it. A beginner lecture uses numbered steps, explains every term, and gives an example per step. A standard lecture gives the intuition first, then the formal detail, with examples where they help. An advanced lecture goes in one pass with no numbered steps, starts from the formal statement, and uses at most one example per idea.">
126
128
  </p>
127
129
 
128
130
  Drafts count as preparation, not learning. Every lecture follows the `/study` writing standard and is saved before it is shown.
@@ -131,7 +133,7 @@ A concept the graph removes has its lecture file moved to `lectures/archive/`, n
131
133
 
132
134
  Later sessions read the tracker files first. You pick a Chapter or a Concept, and it names that Concept, its state, and the state of its prerequisites in one line. Nothing forces a prerequisite order.
133
135
 
134
- Profile defaults live under Settings -> Nerd Mode. They cover whether each tracked session opens with a Recall Session, and how many unseen questions follow a lecture, in which form (short answer, four-option multiple choice, or mixed). Save applies to the next session, Cancel discards a draft, and Restore defaults returns to recall off, 3 questions, short answer. A preference stated inside one `/study-plus` request wins for that session only.
136
+ Profile defaults live under Settings -> Nerd Mode. They cover whether each tracked session opens with a Recall Session, and how many unseen questions follow a lecture, in which form (short answer, four-option multiple choice, or mixed). A four-option question appears as a card with one correct choice. A short-answer question is asked in the conversation. Save applies to the next session, Cancel discards a draft, and Restore defaults returns to recall off, 3 questions, short answer. A preference stated inside one `/study-plus` request wins for that session only.
135
137
 
136
138
  You get a reading order and a prerequisite order, any concept you choose, a graded check for that concept, a saved lecture, and a record that survives restarts. The HTML book refreshes after each draft batch and each lesson, with one page per saved Section in its original order, full text search, and a draft label on unprepared reading.
137
139
 
@@ -141,14 +143,14 @@ It schedules no reviews and never guesses that you know something. Only an answe
141
143
 
142
144
  | | `/study`, quick lesson | `/study-plus`, long lesson |
143
145
  |---|---|---|
144
- | Writes files | saves a lecture or artifact at the path you name, and an SVG when a picture helps | `study/<topic>/`: `graph.md`, `state.md`, `log.md`, one file per concept, `book.html` refreshed automatically |
146
+ | Writes files | saves a lecture or artifact at the path you name, and an SVG when a picture helps | `study/<course>/`: `graph.md`, `state.md`, `log.md`, one file per concept, `book.html` refreshed automatically |
145
147
  | Reads files | no reads by default, but reads or writes a requested artifact path | the three tracker files, before the first line |
146
148
  | Remembered next session | no | yes, with dates |
147
149
  | Graded | no | each concept, rubric written first |
148
150
  | Organized as | one concept, one task, or a one-off book | Book, Chapter, Section |
149
151
  | Typical span | one sitting, one idea | 15 to 30 concepts over weeks |
150
152
 
151
- `/study` does not create a `study/` folder by itself. A saved lecture or artifact goes where you name it, including a path under `study/<topic>/lectures/`. A figure goes to `diagrams/<topic>/` unless you name another path. It must not read or write `graph.md`, `state.md`, `log.md`, or `plan.md`.
153
+ `/study` does not create a `study/` folder by itself. A saved lecture or artifact goes where you name it, including a path under `study/<course>/lectures/`. A figure goes to `diagrams/<name>.svg` when the guide runs alone, and to `study/<course>/diagrams/<name>.svg` inside a tracked Course. It must not read or write `graph.md`, `state.md`, `log.md`, or `plan.md`.
152
154
 
153
155
  ### Which one
154
156
 
@@ -169,7 +171,7 @@ One example. You are reading the Shampoo paper and hit the preconditioner. `/stu
169
171
  /nerd-mode on teaching posture for this conversation
170
172
  /nerd-mode off
171
173
  /study <concept or task>
172
- /study-plus <topic> recorded session with the three tracker files
174
+ /study-plus <course> recorded session with the three tracker files
173
175
  ```
174
176
 
175
177
  The book page is not a command. `/study-plus` refreshes it after each draft batch and each lesson, and keeps the previous copy as `book.html.bak`.
@@ -202,6 +204,7 @@ The pill in the composer bar sends `/nerd-mode`. It sits left of Full Throttle,
202
204
  - If you rename the package, update the `resolve('dsh-nerd-mode/package.json')` string in `cordis.patch.yml`.
203
205
  - Bundled third-party skills keep their own licenses. See [NOTICE.md](./NOTICE.md).
204
206
  - Design notes: [SPEC v3](./docs/specs/2026-10-28-study-plus-v3.md), [v2, historical](./docs/specs/2026-10-27-study-plus-v2.md), [session defaults spec](./docs/specs/2026-10-28-nerd-mode-settings.md), [GLOSSARY.md](./GLOSSARY.md), `docs/adr/`. Doc layout: [docs/README.md](./docs/README.md).
207
+ - Version history: [CHANGELOG.md](https://github.com/bocchii-the-code/dsh-nerd-mode/blob/main/CHANGELOG.md).
205
208
  - Tested with `@deepseek-ai/dsh` 0.2.1-rc.3 on Node 22.
206
209
 
207
210
  ## Uninstall
package/README.zh.md CHANGED
@@ -12,18 +12,18 @@
12
12
  <a href="./README.md"><img src="https://img.shields.io/badge/English-switch-d0d7de" alt="English"></a>
13
13
  </p>
14
14
 
15
- 聊天里的学习通常在答案到达时就结束了。没人检验你有没有弄懂,关掉窗口也什么都不剩。Nerd Mode 让 dsh 一步步地教,在换个选择就会改变结果的地方停下来,遇到值得跟的主题就留下带日期的记录。这样一个主题能撑过几周,而不只是一次对话。
15
+ 聊天里的学习通常在答案到达时就结束了。没人检验你有没有弄懂,关掉窗口也什么都不剩。Nerd Mode 让 dsh 一步步地教,在换个选择就会改变结果的地方停下来,遇到值得跟的科目就留下带日期的记录。一个 Course 能撑过几周,而不只是一次对话。
16
16
 
17
17
  ## 主要功能
18
18
 
19
19
  - 一个开关管整个会话。`/nerd-mode on` 之后,所有学习类请求都按教学方式回答,写代码、搜索和命令照常执行。
20
- - 两条学习路径。`/study` 在对话里讲一个概念或做一件事,不留下记录;`/study-plus` 把一个主题跟上几周,留下带日期的记录。
20
+ - 两条学习路径。`/study` 在对话里讲一个概念或做一件事,不留下记录;`/study-plus` 把一个 Course 跟上几周,留下带日期的记录。
21
21
  - 先有图,再上课。概念按先修顺序排好,分进 Book、Chapter、Section,等你批准之后才写文件。
22
- - 每个主题只问一轮开课校准。问你想用这个主题做什么、以前接触过什么、哪些概念可以跳过,据此定出教学深度,之后每份讲义都按它写。
22
+ - 开课前只问一轮。目标、以前接触过什么,以及在对话里一次讲完还是建一份跟下去的记录,都做成选项;走记录时再问哪些概念可以跳过。答案定出教学深度,之后每份讲义都按它写。
23
23
  - 讲到岔口就停。讲义一步一步地讲,在换个选择就会改变结果的地方停下来问你,不会直接把成品答案端上来。
24
24
  - 认证据,不认印象。没见过的题加上要评分的复述,评分标准在出题之前就写好。只有你自己答出来的内容才会改写记录。
25
25
  - 记录比对话活得久。概念图、状态和带日期的记录能跨重启保留,讲义一有变化,HTML 版的书就重建,并且支持全文搜索。
26
- - 讲义找得回来。Lectures 卡片按主题列出这个工作区里保存过的每一份讲义。
26
+ - 讲义找得回来。Lectures 卡片按 Course 列出这个工作区里保存过的每一份讲义。
27
27
 
28
28
  ## 运行环境
29
29
 
@@ -80,15 +80,15 @@ dsh plugin --profile <profile> add C:\path\to\dsh-nerd-mode
80
80
 
81
81
  敲命令,或者打开开关直接问。开关打开时,学习类的请求默认就是这个形状。
82
82
 
83
- 显式输入 `/study-plus` 会直接进入带记录的会话,即使请求里有多个概念。显式输入 `/study`,或者打开 Nerd Mode 后直接提问,如果请求里有多个互相独立的概念,会先问一次:要在聊天里一次性给出书式的完整讲解,还是走带记录的 `/study-plus`。前者用普通 Markdown,一级标题是书名或题目,二级标题是章,三级标题是节。
83
+ 显式输入 `/study-plus` 会直接进入带记录的会话,即使请求里有多个概念。显式输入 `/study` 会直接在对话里讲。打开开关、没敲命令直接提问时,学习类请求会先问一轮:目标选一项、以前接触过什么选一项,以及在对话里一次讲完还是走带记录的 `/study-plus`。前者用普通 Markdown,一级标题是书名或题目,二级标题是章,三级标题是节。
84
84
 
85
85
  你会拿到:标题、中心思想、编号步骤,每个公式上方一行符号说明。四个以上符号被多个公式反复用到时,开头再加一张速查表。然后是一个具体例子、常见错误,以及在分叉处停的那一次。任务是另外给你做完的东西和每一步的理由。
86
86
 
87
- 它不打分,不保存记录,也不能跨会话接着教同一个主题。讲稿或其他成品只在你要求时才保存,存到你指定的路径。
87
+ 它不打分,不保存记录,也不能跨会话接着教同一个 Course。讲稿或其他成品只在你要求时才保存,存到你指定的路径。
88
88
 
89
89
  ### 长课程:`/study-plus`
90
90
 
91
- 把一个主题组织成一本 Book,下面分 Chapter,每个 Chapter 下分 Section。每个 Section 只放一个概念。它按先修顺序建概念图,把概念分进各章,每次只教一个概念,也只考这个概念的掌握情况。概念图里同时带 Chapter 和 Section 两列,所以一张图里既有阅读顺序,也有先修顺序。它维护三个记录文件,并为每个概念保存一份 Markdown 文件,放在 `study/<slug>/lectures/`。你批准概念图后,它会为每个概念先写好一份完整讲稿,开讲不用等准备。它用已保存的材料生成一个 HTML 书页,材料有变化就自动刷新。
91
+ 把一个 Course 组织成一本 Book,下面分 Chapter,每个 Chapter 下分 Section。每个 Section 只放一个概念。它按先修顺序建概念图,把概念分进各章,每次只教一个概念,也只考这个概念的掌握情况。概念图里同时带 Chapter 和 Section 两列,所以一张图里既有阅读顺序,也有先修顺序。它维护三个记录文件,并为每个概念保存一份 Markdown 文件,放在 `study/<course>/lectures/`。你批准概念图后,它会为每个概念先写好一份完整讲稿,开讲不用等准备。它用已保存的材料生成一个 HTML 书页,材料有变化就自动刷新。
92
92
 
93
93
  适合下个月你还在学的东西。
94
94
 
@@ -98,11 +98,13 @@ dsh plugin --profile <profile> add C:\path\to\dsh-nerd-mode
98
98
  - 想要进度的证据,而不是感觉。
99
99
  - 你总在反复查同一批东西,查完就忘。
100
100
 
101
- 显式输入 `/study-plus` 会直接开始带记录的会话,即使主题里有多个概念,也不会再问一次性讲解还是带记录讲解。
101
+ 显式输入 `/study-plus` 会直接开始带记录的会话,即使请求里有多个概念,也不会再问一次性讲解还是带记录讲解。
102
+
103
+ 一个 Course 就是 `study/` 下的一个文件夹,里面有一份概念图、一个状态文件、一份日志,还有它的讲义、图和 Book。一次对话在第一次带记录的讲解时定下 Course,只定一次。工作区里只有一个 Course 时,`/study-plus` 直接用它,在一行里说明,不问。有多个 Course 时,用一个问题列出它们,并给出新建 Course 的选项。问到另一个科目时,只问一次:把这些概念加进当前 Course,还是新建一个 Course。
102
104
 
103
105
  ```text
104
106
  /study-plus 洗发水与 Adam 优化器 # 第一次建图
105
- /study-plus # 之后继续同一话题
107
+ /study-plus # 之后继续同一个 Course
106
108
  /study-plus 第 2 章 # 按书中顺序读这一章
107
109
  /study-plus 矩阵平方根 # 直接跳到某个概念
108
110
  ```
@@ -110,7 +112,7 @@ dsh plugin --profile <profile> add C:\path\to\dsh-nerd-mode
110
112
  第一次会话按固定顺序走。
111
113
 
112
114
  ```text
113
- /study-plus <主题>
115
+ /study-plus <course>
114
116
  按先修顺序给出 15 到 30 个概念,分进各章
115
117
  章节列表和概念表一起给你看,然后等你确认
116
118
  你批准、修改或重建概念图
@@ -119,10 +121,10 @@ dsh plugin --profile <profile> add C:\path\to\dsh-nerd-mode
119
121
  逐个概念教:课前回忆(可选)→ 讲义 → 设置数量的没见过的题 → 复述 → 带日期的记录
120
122
  ```
121
123
 
122
- 批准概念图后先问一轮开课校准:这个主题你想学到能做什么,以前接触过什么,概念图里哪些概念可以直接跳过。答案决定 `graph.md` 里的 `Teaching level` 一行,`Goal` 和 `Background` 也一并写入,之后每份讲义都按这个教学深度来写。深度只管讲义,不管题目、复述和记录。中途改的深度只对这次会话有效,说「以后都这样」才会改写那一行。
124
+ 批准概念图后再问剩下的开课问题:这个 Course 你想学到能做什么,以前接触过什么,概念图里哪些概念可以直接跳过。如果开头的提问已经问过目标和背景,这里只问哪些概念可以跳过。答案决定 `graph.md` 里的 `Teaching level` 一行,`Goal` 和 `Background` 也一并写入,之后每份讲义都按这个教学深度来写。深度只管讲义,不管题目、复述和记录。中途改的深度只对这次会话有效,说「以后都这样」才会改写那一行。
123
125
 
124
126
  <p align="center">
125
- <img src="assets/calibration.svg" width="720" alt="开课校准流程:你批准按章节分组的 15 到 30 个概念,再回答一轮提问,说明你想用这个主题做什么、以前读过或用过什么、哪些概念可以直接跳过。答案定出 graph.md 里的教学深度,之后每份讲义都按它来写。beginner 的讲义分成编号步骤,解释每个术语,每一步配一个例子。standard 先讲直觉,再讲形式细节,需要时给例子。advanced 一遍讲完,不编号,从形式陈述开始,每个想法最多一个例子。">
127
+ <img src="assets/calibration.svg" width="720" alt="开课校准流程:你批准按章节分组的 15 到 30 个概念,再回答一轮提问,说明你想用这个 Course 做什么、以前读过或用过什么、哪些概念可以直接跳过。答案定出 graph.md 里的教学深度,之后每份讲义都按它来写。beginner 的讲义分成编号步骤,解释每个术语,每一步配一个例子。standard 先讲直觉,再讲形式细节,需要时给例子。advanced 一遍讲完,不编号,从形式陈述开始,每个想法最多一个例子。">
126
128
  </p>
127
129
 
128
130
  讲稿只是准备,不算学过。每份讲义都按 `/study` 的写作规则写,并且先保存再显示。
@@ -131,7 +133,7 @@ dsh plugin --profile <profile> add C:\path\to\dsh-nerd-mode
131
133
 
132
134
  之后的会话先读记录文件。你挑一章或一个概念,它用一句说明这个概念的现状和它先修概念的现状。先修不会强制你按顺序走。
133
135
 
134
- Profile 默认值在 设置 -> Nerd Mode。它管的是记录型会话:每次开讲前是否先来一次课前回忆,一节课后问几道没见过的题,题型是简答、四选一还是混合。保存从下一节课开始生效;取消只丢草稿;恢复默认回到关闭、3 题、简答。在一次 `/study-plus` 请求里明确说出的偏好,只对那一次会话生效。
136
+ Profile 默认值在 设置 -> Nerd Mode。它管的是记录型会话:每次开讲前是否先来一次课前回忆,一节课后问几道没见过的题,题型是简答、四选一还是混合。四选一以卡片出现,四个选项里只有一个对;简答直接在对话里问。保存从下一节课开始生效;取消只丢草稿;恢复默认回到关闭、3 题、简答。在一次 `/study-plus` 请求里明确说出的偏好,只对那一次会话生效。
135
137
 
136
138
  你会拿到:一套阅读顺序和一套先修顺序、你想学的任何一个概念、只针对这个概念的批改、一份保存下来的讲义,以及重启也还在的记录。HTML 书页会在每次批量写稿和每节课后刷新,每节一页,按原来的节顺序排列,支持全文搜索,还没讲过的内容标成草稿。
137
139
 
@@ -143,14 +145,14 @@ HTML 书页保留每个 Section 在概念图里的原编号,也就是教材里
143
145
 
144
146
  | | `/study` 快速课程 | `/study-plus` 长课程 |
145
147
  |---|---|---|
146
- | 写文件 | 只在你指定路径时保存讲稿或成品,需要图时会画 SVG | `study/<主题>/` 下的概念图、状态、日志,每个概念一份文件,`book.html` 自动刷新 |
148
+ | 写文件 | 只在你指定路径时保存讲稿或成品,需要图时会画 SVG | `study/<course>/` 下的概念图、状态、日志,每个概念一份文件,`book.html` 自动刷新 |
147
149
  | 读文件 | 默认不读,但会读写你指定的成品路径 | 开口前先读那三个 |
148
150
  | 下次还记得 | 不记得 | 记得,带日期 |
149
151
  | 批改 | 不批 | 按概念批,先写评分标准 |
150
152
  | 组织方式 | 一个概念、一个任务,或一次性的书式讲解 | Book、Chapter、Section |
151
153
  | 典型长度 | 一次坐完,一个概念 | 15 到 30 个概念,跨几周 |
152
154
 
153
- `/study` 自己不会建 `study/` 文件夹。讲稿和成品只会存到你指定的路径,包括 `study/<主题>/lectures/` 下的普通文件。图默认放在 `diagrams/<topic>/`,除非你另指路径。它不得读写 `graph.md`、`state.md`、`log.md` 或 `plan.md`。
155
+ `/study` 自己不会建 `study/` 文件夹。讲稿和成品只会存到你指定的路径,包括 `study/<course>/lectures/` 下的普通文件。只跑讲解时,图放在 `diagrams/<name>.svg`;在 Course 里,图放在 `study/<course>/diagrams/<name>.svg`。它不得读写 `graph.md`、`state.md`、`log.md` 或 `plan.md`。
154
156
 
155
157
  ### 怎么选
156
158
 
@@ -171,7 +173,7 @@ HTML 书页保留每个 Section 在概念图里的原编号,也就是教材里
171
173
  /nerd-mode on 这次对话进入教学姿势
172
174
  /nerd-mode off
173
175
  /study <概念或任务>
174
- /study-plus <主题> 带三个记录文件的记录型会话
176
+ /study-plus <course> 带三个记录文件的记录型会话
175
177
  ```
176
178
 
177
179
  书页不是命令。`/study-plus` 每次批量写稿和每节课后自动刷新,上一版会留成 `book.html.bak`。
@@ -204,6 +206,7 @@ HTML 书页保留每个 Section 在概念图里的原编号,也就是教材里
204
206
  - 如果你改了包名,记得同步 `cordis.patch.yml` 里的 `resolve('dsh-nerd-mode/package.json')`。
205
207
  - 随包的第三方技能各自保留许可,见 [NOTICE.md](./NOTICE.md)。
206
208
  - 设计记录:[SPEC v3](./docs/specs/2026-10-28-study-plus-v3.md)、[v2,历史版本](./docs/specs/2026-10-27-study-plus-v2.md)、[会话默认值](./docs/specs/2026-10-28-nerd-mode-settings.md)、[GLOSSARY.md](./GLOSSARY.md)、`docs/adr/`。文档结构见 [docs/README.md](./docs/README.md)。
209
+ - 版本变更记录:[CHANGELOG.md](https://github.com/bocchii-the-code/dsh-nerd-mode/blob/main/CHANGELOG.md)。
207
210
  - 在 `@deepseek-ai/dsh` 0.2.1-rc.3 和 Node 22 上测过。
208
211
 
209
212
  ## 卸载
@@ -1,6 +1,6 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 640" role="img" aria-labelledby="title desc">
2
2
  <title id="title">Calibration sets one teaching level for every lecture</title>
3
- <desc id="desc">You approve the concept graph, then answer one question round, once per topic: what you want to do with it, what you have read or used, and which concepts you can skip. The answers fan out to three teaching levels. Beginner uses numbered steps, explains every term, and shows an example per step. Standard gives intuition first, then the formal detail, with examples where useful. Advanced takes one pass with no steps, gives the formal statement first, and uses at most one example. The level is written to graph.md, and every lecture follows it. The questions, the teach-back, and the record stay the same.</desc>
3
+ <desc id="desc">You approve the concept graph, then answer one question round, once per Course: what you want to do with it, what you have read or used, and which concepts you can skip. The answers fan out to three teaching levels. Beginner uses numbered steps, explains every term, and shows an example per step. Standard gives intuition first, then the formal detail, with examples where useful. Advanced takes one pass with no steps, gives the formal statement first, and uses at most one example. The level is written to graph.md, and every lecture follows it. The questions, the teach-back, and the record stay the same.</desc>
4
4
 
5
5
  <rect x="8" y="8" width="944" height="624" rx="20" fill="#FFFFFF" stroke="#D8D2C6" stroke-width="2"/>
6
6
 
@@ -37,7 +37,7 @@
37
37
  <text x="168" y="322" font-size="18">grouped by chapter</text>
38
38
 
39
39
  <text x="428" y="244" font-size="21" font-weight="700" letter-spacing="0.2">One question round</text>
40
- <text x="428" y="266" font-size="17">once per topic</text>
40
+ <text x="428" y="266" font-size="17">once per Course</text>
41
41
  <text x="428" y="296" font-size="18">what you want to do with it</text>
42
42
  <text x="428" y="326" font-size="18">what you have read or used</text>
43
43
  <text x="428" y="356" font-size="18">which concepts you can skip</text>
@@ -0,0 +1,15 @@
1
+ Nerd Mode is on for this conversation. These rules take precedence over any earlier persona, style, or answer-shape text in this prompt, including text that asks for brevity, for a direct answer, or for no follow-up question.
2
+
3
+ The `study` and `study-plus` skills are available through the skill tool. An explicit `/study-plus` means the user chose tracked study: load `study-plus` directly with the Course and supplied material, without a route question. An explicit `/study` means the user chose one-off teaching: load `study` directly. Otherwise, a learning request gets one opening question before either skill loads, and the answer chooses one-off `study` or tracked `study-plus`. Follow `study` for lecture-writing standards. Save other artifacts only when the user asks for a path. Do not let `study` read or write tracker files in `study/<course>/`. Do not grade, score, or log in `study`.
4
+
5
+ Every request that is not a learning request keeps the base mode's behavior: produce the artifact, run the command, make the edit. Answer in plain prose, without a teaching detour and without a closing question the learner did not ask for.
6
+
7
+ Study-plus loads `study` before Teach. It owns tracker updates, automatic lecture files for tracked sessions, and book assembly. Direct `study` may save a user-requested lecture or other artifact to the requested path. The four tracker files remain study-plus-only. Do not grade in `study`.
8
+
9
+ Teaching level: run calibration once per new Course, after graph approval and before any tracker file is written. Ask it as one question-tool call with three items: the goal, the prior exposure, and a multi-select skip list. Ask the goal and background items only when nothing already answers them: not the current request, not the opening question, not the supplied material. Derive `beginner`, `standard`, or `advanced`. Answer `beginner` when most prerequisites are unknown, or when the goal is to learn the topic from zero. Answer `standard` when some prerequisites are known and the goal is to understand the principles or pass an exam. Answer `advanced` when most prerequisites are known and the goal is review, reference, or use in a project. When the two answers clearly conflict, you may choose an adjacent level and state the reason in one line. Write `Goal`, `Background`, and `Teaching level` in `graph.md`. When the learner declines or gives no usable answer, write `Teaching level: standard` and omit the other two lines. Pass the lines that exist verbatim to `study`, without compressing or rewriting them. A missing `Teaching level` line means `standard`. Never ask for calibration on a Course that already has a `graph.md`. A mid-Course change of depth lasts for this session only. Only an explicit "from now on" rewrites the `Teaching level` line.
10
+
11
+ Routing order: an explicit `/study-plus` loads `study-plus` directly, even for several concepts, and never asks a path question. An explicit `/study` loads `study` directly. Otherwise, a learning request gets exactly one opening question-tool call before either skill loads. The call carries up to three single-choice items, in this order. Goal: pass an exam, understand the principles, use it in a project or at work, get a rough overview. Background: never studied it, read introductory material, took a course or systematic training, used it at work. The four background options run from least to most exposure, so the learner picks the highest one that applies. Path: one complete answer in this conversation with no Course record, one continue choice for each existing Course, start a new Course. Show a continue choice only for a Course that exists under `study/`. Call the only existing Course the current Course; with several, use each name. The path question text says that tracked study learns one concept per session. Write each label in the learner's language. Ask only the items that the request and the supplied material leave open. A bare topic, or "I want to learn X", states no goal and no background. When the host has no choice tool, ask the same items as a numbered text list. Pass the selected labels verbatim with the request as the brief. When the learner declines, or says to just teach, load `study` with `standard` and no goal or background. Never repeat an answered item.
12
+
13
+ Course: after an explicit `/study-plus`, settle the Course before the first tracked study and never ask again in that conversation. No Course asks for a name. Exactly one Course is used and named in one line, with no question. Several Courses get one question that lists them and offers a new Course. A named `/study-plus <course>` settles the Course with no question; when that Course does not exist, ask once whether to create it. The opening question, when it runs, carries the Course in its path item.
14
+
15
+ `study` defaults to chat output. Do not apply a blanket ban on files under `study/`: study-plus owns its tracker files and automatic tracked-session lectures. Direct `study` may save user-requested lectures, diagrams, and other artifacts. Only study-plus may read or write `graph.md`, `state.md`, `log.md`, and `plan.md`.
package/client.js CHANGED
@@ -571,8 +571,16 @@ window.__ModuleLoader__.load({
571
571
  const VIEW_LABEL = 'Study';
572
572
  const VIEW_ORDER = 100;
573
573
  const TRACKER_ROOT = 'study';
574
+ // One selection shared by the Study tab and the Lectures tab. This is a
575
+ // classic script, so a module-level binding is the whole mechanism.
576
+ let activeCourse = null;
574
577
  const DASH = '—';
575
578
 
579
+ /** Return the Course both tabs show: `activeCourse` while it exists, otherwise the first of the name-sorted `courses`. */
580
+ function pickCourse(courses) {
581
+ return activeCourse !== null && courses.includes(activeCourse) ? activeCourse : courses[0] ?? null;
582
+ }
583
+
576
584
  // --- Tracker parsing -----------------------------------------------------
577
585
  // Parsing lives here because the client bundle is a classic script: a
578
586
  // relative require throws and an export statement is invalid.
@@ -617,7 +625,8 @@ window.__ModuleLoader__.load({
617
625
  let inChapters = false;
618
626
  for (const line of lines) {
619
627
  const trimmed = line.trim();
620
- const topicMatch = /^#\s*Topic:\s*(.+)$/i.exec(trimmed);
628
+ // A new graph says Course and an older graph says Topic; both keep their title.
629
+ const topicMatch = /^#\s*(?:Course|Topic):\s*(.+)$/i.exec(trimmed);
621
630
  if (topicMatch) topic = topicMatch[1].trim();
622
631
  const bookMatch = /^Book:\s*(.+)$/i.exec(trimmed);
623
632
  if (bookMatch) book = bookMatch[1].trim();
@@ -696,11 +705,6 @@ window.__ModuleLoader__.load({
696
705
  return blocks.filter((block) => block.concept !== null || block.result !== null);
697
706
  }
698
707
 
699
- const latestEvidence = (text) => {
700
- const dates = [...parseState(text).states.values()].map((row) => row.date ?? '');
701
- return dates.sort().pop() ?? '';
702
- };
703
-
704
708
  function buildSnapshot(topic, graphText, stateText, logText) {
705
709
  const graph = parseGraph(graphText);
706
710
  const state = parseState(stateText);
@@ -780,25 +784,19 @@ window.__ModuleLoader__.load({
780
784
  return snapshot.total === 0 ? { status: 'empty', topic } : { status: 'ready', snapshot };
781
785
  }
782
786
 
783
- // "Changed most recently" is read from the newest dated Evidence, because
784
- // the file surface exposes no modification time.
787
+ // Both tabs open on the shared Course, then on the first Course by name.
785
788
  async function discoverTopics(files, sessionId) {
786
789
  const listing = await files.list(sessionId, TRACKER_ROOT);
787
790
  if (!listing.ok) {
788
791
  if (listing.error?.code === 'workspace-file/not-found') return { topics: [], content: { status: 'empty' } };
789
- throw new Error(listing.error?.message ?? 'the Tracker folder could not be read');
792
+ throw new Error(listing.error?.message ?? 'the Course folder could not be read');
790
793
  }
791
794
  const topics = (listing.value?.entries ?? [])
792
795
  .filter((entry) => entry.type === 'directory')
793
- .map((entry) => entry.name);
796
+ .map((entry) => entry.name)
797
+ .sort((left, right) => left.localeCompare(right));
794
798
  if (topics.length === 0) return { topics, content: { status: 'empty' } };
795
- const states = [];
796
- for (const topic of topics) {
797
- states.push({ topic, text: await readText(files, sessionId, `${TRACKER_ROOT}/${topic}/state.md`) });
798
- }
799
- states.sort((left, right) => latestEvidence(right.text).localeCompare(latestEvidence(left.text)));
800
- const chosen = states[0];
801
- return { topics, content: await loadTopic(files, sessionId, chosen.topic, chosen.text) };
799
+ return { topics, content: await loadTopic(files, sessionId, pickCourse(topics)) };
802
800
  }
803
801
 
804
802
  // --- Study view ----------------------------------------------------------
@@ -859,9 +857,9 @@ window.__ModuleLoader__.load({
859
857
  function topicPicker(topics, value, onTopic) {
860
858
  if (topics.length < 2) return null;
861
859
  return React.createElement('label', { style: panelFieldStyle },
862
- React.createElement('span', null, 'Topic'),
860
+ React.createElement('span', null, 'Course'),
863
861
  React.createElement('select', {
864
- 'aria-label': 'Topic',
862
+ 'aria-label': 'Course',
865
863
  value,
866
864
  onChange: (event) => onTopic(event.target.value),
867
865
  style: panelSelectStyle,
@@ -876,7 +874,7 @@ window.__ModuleLoader__.load({
876
874
  ];
877
875
  if (error) {
878
876
  nodes.push(React.createElement('p', { role: 'alert', style: panelMutedStyle },
879
- `The Tracker could not be read: ${error}.`));
877
+ `The Course could not be read: ${error}.`));
880
878
  }
881
879
  nodes.push(progressBlock(snapshot), stripBlock(snapshot));
882
880
 
@@ -910,20 +908,20 @@ window.__ModuleLoader__.load({
910
908
  const [topics, setTopics] = React.useState([]);
911
909
  const [selected, setSelected] = React.useState(null);
912
910
  const [content, setContent] = React.useState({ status: 'loading' });
911
+ // Read on every render so a selection made in the Lectures tab wins.
912
+ const shared = activeCourse;
913
913
 
914
914
  React.useEffect(() => {
915
915
  let live = true;
916
916
  const run = async () => {
917
917
  try {
918
- const found = selected === null
919
- ? await discoverTopics(workspaceFiles, sessionId)
920
- : { topics: null, content: await loadTopic(workspaceFiles, sessionId, selected) };
918
+ const found = await discoverTopics(workspaceFiles, sessionId);
921
919
  if (!live) return;
922
- if (found.topics !== null) setTopics(found.topics);
920
+ setTopics(found.topics);
923
921
  setContent(found.content);
924
922
  } catch (error) {
925
923
  if (!live) return;
926
- const message = error?.message ?? 'the Tracker could not be read';
924
+ const message = error?.message ?? 'the Course could not be read';
927
925
  // Keep the last good snapshot so a failed reload does not blank the page.
928
926
  setContent((previous) => previous.status === 'ready' || previous.status === 'empty'
929
927
  ? { ...previous, error: message }
@@ -932,22 +930,27 @@ window.__ModuleLoader__.load({
932
930
  };
933
931
  run();
934
932
  return () => { live = false; };
935
- }, [selected, sessionId, workspaceFiles]);
933
+ }, [selected, shared, sessionId, workspaceFiles]);
934
+
935
+ const choose = (topic) => {
936
+ activeCourse = topic;
937
+ setSelected(topic);
938
+ };
936
939
 
937
940
  let nodes;
938
941
  if (content.status === 'loading') {
939
- nodes = [React.createElement('p', { role: 'status', 'aria-busy': true, style: panelMutedStyle }, 'Loading the Tracker…')];
942
+ nodes = [React.createElement('p', { role: 'status', 'aria-busy': true, style: panelMutedStyle }, 'Loading the Course…')];
940
943
  } else if (content.status === 'failed') {
941
- nodes = [React.createElement('p', { role: 'alert', style: panelMutedStyle }, `The Tracker could not be read: ${content.error}.`)];
944
+ nodes = [React.createElement('p', { role: 'alert', style: panelMutedStyle }, `The Course could not be read: ${content.error}.`)];
942
945
  } else if (content.status === 'empty') {
943
946
  const searched = content.topic ? `${TRACKER_ROOT}/${content.topic}` : `${TRACKER_ROOT}/`;
944
- const picker = topicPicker(topics, content.topic ?? selected, setSelected);
947
+ const picker = topicPicker(topics, content.topic ?? selected, choose);
945
948
  nodes = [
946
949
  picker === null ? null : React.createElement('div', { style: panelHeaderStyle }, picker),
947
- React.createElement('p', { style: panelMutedStyle }, `No Tracker in ${searched}. Run /study-plus to create one.`),
950
+ React.createElement('p', { style: panelMutedStyle }, `No Course in ${searched}. Run /study-plus to create one.`),
948
951
  ];
949
952
  } else {
950
- nodes = readyBody(content.snapshot, content.error, topics, setSelected);
953
+ nodes = readyBody(content.snapshot, content.error, topics, choose);
951
954
  }
952
955
  return React.createElement('div', {
953
956
  role: 'region',
@@ -1107,57 +1110,57 @@ window.__ModuleLoader__.load({
1107
1110
  }
1108
1111
  }
1109
1112
 
1110
- /** Return `{ topics, error, rootFound }`. `rootFound` is false only when `study/` is missing, and `error` is the first read failure or null. */
1113
+ /** Return `{ courses, course, entries, error, rootFound }`. `rootFound` is false only when `study/` is missing, `course` is null when the workspace holds none, and `error` is the first read failure or null. */
1111
1114
  async function loadLectures(files, sessionId) {
1112
- const model = { topics: [], error: null, rootFound: false };
1115
+ const model = { courses: [], course: null, entries: [], error: null, rootFound: false };
1113
1116
  const root = await listFolder(files, sessionId, TRACKER_ROOT);
1114
1117
  model.rootFound = root.found;
1115
1118
  if (root.error !== null) {
1116
1119
  model.error = root.error;
1117
1120
  return model;
1118
1121
  }
1119
- const names = root.entries
1122
+ model.courses = root.entries
1120
1123
  .filter((entry) => entry.type === 'directory')
1121
1124
  .map((entry) => entry.name)
1122
1125
  .sort((left, right) => left.localeCompare(right));
1123
- for (const topic of names) {
1124
- const entries = [];
1125
- const seen = new Set();
1126
- const folders = [
1127
- [`${TRACKER_ROOT}/${topic}/lectures`, false],
1128
- [`${TRACKER_ROOT}/${topic}/lectures/archive`, true],
1129
- ];
1130
- for (const [folder, archived] of folders) {
1131
- const listing = await listFolder(files, sessionId, folder);
1132
- if (listing.error !== null && model.error === null) model.error = listing.error;
1133
- for (const item of listing.entries) {
1134
- if (item.type !== 'file') continue;
1135
- const match = /^(\d{2,})-(.+)\.md$/.exec(item.name);
1136
- // One lecture per number: a stale copy in the archive never repeats it.
1137
- if (match === null || seen.has(match[1])) continue;
1138
- let text;
1139
- try {
1140
- text = await readText(files, sessionId, `${folder}/${item.name}`);
1141
- } catch (error) {
1142
- if (model.error === null) model.error = error?.message ?? `${folder}/${item.name} could not be read`;
1143
- continue;
1144
- }
1145
- const parsed = parseLecture(text, match[2]);
1146
- entries.push({
1147
- number: match[1],
1148
- topic,
1149
- title: parsed.title,
1150
- concept: parsed.concept,
1151
- mark: archived ? 'archived' : parsed.draft ? 'draft' : parsed.date !== null ? 'dated' : null,
1152
- date: parsed.date,
1153
- path: `${folder}/${item.name}`,
1154
- });
1155
- seen.add(match[1]);
1126
+ model.course = pickCourse(model.courses);
1127
+ if (model.course === null) return model;
1128
+ const entries = [];
1129
+ const seen = new Set();
1130
+ const folders = [
1131
+ [`${TRACKER_ROOT}/${model.course}/lectures`, false],
1132
+ [`${TRACKER_ROOT}/${model.course}/lectures/archive`, true],
1133
+ ];
1134
+ for (const [folder, archived] of folders) {
1135
+ const listing = await listFolder(files, sessionId, folder);
1136
+ if (listing.error !== null && model.error === null) model.error = listing.error;
1137
+ for (const item of listing.entries) {
1138
+ if (item.type !== 'file') continue;
1139
+ const match = /^(\d{2,})-(.+)\.md$/.exec(item.name);
1140
+ // One lecture per number: a stale copy in the archive never repeats it.
1141
+ if (match === null || seen.has(match[1])) continue;
1142
+ let text;
1143
+ try {
1144
+ text = await readText(files, sessionId, `${folder}/${item.name}`);
1145
+ } catch (error) {
1146
+ if (model.error === null) model.error = error?.message ?? `${folder}/${item.name} could not be read`;
1147
+ continue;
1156
1148
  }
1149
+ const parsed = parseLecture(text, match[2]);
1150
+ entries.push({
1151
+ number: match[1],
1152
+ topic: model.course,
1153
+ title: parsed.title,
1154
+ concept: parsed.concept,
1155
+ mark: archived ? 'archived' : parsed.draft ? 'draft' : parsed.date !== null ? 'dated' : null,
1156
+ date: parsed.date,
1157
+ path: `${folder}/${item.name}`,
1158
+ });
1159
+ seen.add(match[1]);
1157
1160
  }
1158
- entries.sort((left, right) => Number(left.number) - Number(right.number));
1159
- model.topics.push({ topic, entries });
1160
1161
  }
1162
+ entries.sort((left, right) => Number(left.number) - Number(right.number));
1163
+ model.entries = entries;
1161
1164
  return model;
1162
1165
  }
1163
1166
 
@@ -1263,7 +1266,10 @@ window.__ModuleLoader__.load({
1263
1266
  function LecturesPage({ useTabInfo, sessionId, workspaceFiles }) {
1264
1267
  const { tab } = useTabInfo();
1265
1268
  const [content, setContent] = React.useState({ status: 'loading' });
1266
- const [openTopics, setOpenTopics] = React.useState({});
1269
+ const [selected, setSelected] = React.useState(null);
1270
+ const [foldOpen, setFoldOpen] = React.useState(false);
1271
+ // Read on every render so a selection made in the Study tab wins.
1272
+ const shared = activeCourse;
1267
1273
 
1268
1274
  React.useEffect(() => {
1269
1275
  let live = true;
@@ -1274,12 +1280,13 @@ window.__ModuleLoader__.load({
1274
1280
  };
1275
1281
  run();
1276
1282
  return () => { live = false; };
1277
- }, [sessionId, workspaceFiles]);
1283
+ }, [selected, shared, sessionId, workspaceFiles]);
1278
1284
 
1279
- const toggle = (topic) => setOpenTopics((previous) => ({
1280
- ...previous,
1281
- [topic]: previous[topic] !== true,
1282
- }));
1285
+ const choose = (course) => {
1286
+ activeCourse = course;
1287
+ setFoldOpen(false);
1288
+ setSelected(course);
1289
+ };
1283
1290
 
1284
1291
  const open = (entry) => tab.actions.openResource(
1285
1292
  sessionFileAddress(sessionId, entry.path),
@@ -1290,12 +1297,21 @@ window.__ModuleLoader__.load({
1290
1297
  if (content.status === 'loading') {
1291
1298
  nodes = [React.createElement('p', { role: 'status', 'aria-busy': true, style: panelMutedStyle }, 'Loading the lectures…')];
1292
1299
  } else if (content.error === null && !content.rootFound) {
1293
- nodes = [React.createElement('p', { style: panelMutedStyle }, `No Tracker in ${TRACKER_ROOT}/. Run /study-plus to create one.`)];
1300
+ nodes = [React.createElement('p', { style: panelMutedStyle }, `No Course in ${TRACKER_ROOT}/. Run /study-plus to create one.`)];
1294
1301
  } else {
1295
1302
  nodes = content.error === null
1296
1303
  ? []
1297
1304
  : [React.createElement('p', { role: 'alert', style: panelMutedStyle }, `The lecture library could not be read: ${content.error}.`)];
1298
- nodes.push(...content.topics.map((group) => lectureTopic(group, openTopics[group.topic] === true, toggle, open)));
1305
+ const picker = topicPicker(content.courses, content.course ?? selected, choose);
1306
+ if (picker !== null) nodes.push(React.createElement('div', { style: panelHeaderStyle }, picker));
1307
+ if (content.course !== null) {
1308
+ nodes.push(lectureTopic(
1309
+ { topic: content.course, entries: content.entries },
1310
+ foldOpen,
1311
+ () => setFoldOpen((previous) => !previous),
1312
+ open,
1313
+ ));
1314
+ }
1299
1315
  }
1300
1316
  return React.createElement('div', {
1301
1317
  role: 'region',