dsh-nerd-mode 0.4.5 → 0.4.6

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/client.js CHANGED
@@ -1556,6 +1556,24 @@ window.__ModuleLoader__.load({
1556
1556
  return model;
1557
1557
  }
1558
1558
 
1559
+ // The page fills the dock body so the picker and course name stay in view
1560
+ // while only the lecture rows scroll. The scrollport owns the side padding,
1561
+ // so its bar sits at the page edge rather than inside the text.
1562
+ const lecturesPanelStyle = { ...panelStyle, height: '100%', padding: '24px 0 0' };
1563
+ const lecturesHeaderStyle = { ...panelHeaderStyle, padding: '0 16px' };
1564
+ const lecturesMessageStyle = { ...panelMutedStyle, padding: '0 16px' };
1565
+ const lecturesHeadingStyle = { ...panelHeadingStyle, padding: '0 16px' };
1566
+ const lecturesSectionStyle = { ...panelSectionStyle, flex: 1, minHeight: 0 };
1567
+ const lecturesScrollStyle = {
1568
+ display: 'flex',
1569
+ flex: 1,
1570
+ flexDirection: 'column',
1571
+ gap: 10,
1572
+ minHeight: 0,
1573
+ overflowY: 'auto',
1574
+ padding: '0 16px 40px',
1575
+ };
1576
+
1559
1577
  // The rows and the fold are the only elements the host cannot style for us.
1560
1578
  const LECTURES_CSS = '.lectures-entry:focus-visible,.lectures-fold:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary, currentColor);outline-offset:2px}';
1561
1579
  const lectureRowStyle = {
@@ -1633,14 +1651,14 @@ window.__ModuleLoader__.load({
1633
1651
  function lectureTopic(group, open, onToggle, onOpen) {
1634
1652
  const main = group.entries.filter((entry) => entry.mark !== 'archived');
1635
1653
  const archived = group.entries.filter((entry) => entry.mark === 'archived');
1636
- const nodes = [React.createElement('h2', { key: 'heading', style: panelHeadingStyle }, group.topic)];
1654
+ const rows = [];
1637
1655
  if (group.entries.length === 0) {
1638
- nodes.push(React.createElement('p', { key: 'empty', style: panelMutedStyle }, 'No lectures yet.'));
1656
+ rows.push(React.createElement('p', { key: 'empty', style: panelMutedStyle }, 'No lectures yet.'));
1639
1657
  } else if (main.length > 0) {
1640
- nodes.push(React.createElement('ul', { key: 'main', style: panelListStyle }, ...main.map((entry) => lectureRow(entry, onOpen))));
1658
+ rows.push(React.createElement('ul', { key: 'main', style: panelListStyle }, ...main.map((entry) => lectureRow(entry, onOpen))));
1641
1659
  }
1642
1660
  if (archived.length > 0) {
1643
- nodes.push(React.createElement('button', {
1661
+ rows.push(React.createElement('button', {
1644
1662
  key: 'fold',
1645
1663
  type: 'button',
1646
1664
  className: 'lectures-fold',
@@ -1649,10 +1667,13 @@ window.__ModuleLoader__.load({
1649
1667
  style: lectureFoldStyle,
1650
1668
  }, `Archived lectures (${archived.length})`));
1651
1669
  if (open) {
1652
- nodes.push(React.createElement('ul', { key: 'archive', style: panelListStyle }, ...archived.map((entry) => lectureRow(entry, onOpen))));
1670
+ rows.push(React.createElement('ul', { key: 'archive', style: panelListStyle }, ...archived.map((entry) => lectureRow(entry, onOpen))));
1653
1671
  }
1654
1672
  }
1655
- return React.createElement('section', { key: group.topic, style: panelSectionStyle }, ...nodes);
1673
+ // The Course key remounts the section on a switch, so its scrollport starts at the top.
1674
+ return React.createElement('section', { key: group.topic, style: lecturesSectionStyle },
1675
+ React.createElement('h2', { key: 'heading', style: lecturesHeadingStyle }, group.topic),
1676
+ React.createElement('div', { style: lecturesScrollStyle }, ...rows));
1656
1677
  }
1657
1678
 
1658
1679
  function LecturesPage({ useTabInfo, sessionId, workspaceFiles }) {
@@ -1680,22 +1701,21 @@ window.__ModuleLoader__.load({
1680
1701
  setSelected(course);
1681
1702
  };
1682
1703
 
1683
- const open = (entry) => tab.actions.openResource(
1684
- sessionFileAddress(sessionId, entry.path),
1685
- { preferNewPane: true },
1686
- );
1704
+ // No placement option: the preview opens as a tab in this pane and the
1705
+ // host focuses it when that lecture is already open.
1706
+ const open = (entry) => tab.actions.openResource(sessionFileAddress(sessionId, entry.path));
1687
1707
 
1688
1708
  let nodes;
1689
1709
  if (content.status === 'loading') {
1690
- nodes = [React.createElement('p', { role: 'status', 'aria-busy': true, style: panelMutedStyle }, 'Loading the lectures…')];
1710
+ nodes = [React.createElement('p', { role: 'status', 'aria-busy': true, style: lecturesMessageStyle }, 'Loading the lectures…')];
1691
1711
  } else if (content.error === null && !content.rootFound) {
1692
- nodes = [React.createElement('p', { style: panelMutedStyle }, `No Course in ${TRACKER_ROOT}/. Run /study-plus to create one.`)];
1712
+ nodes = [React.createElement('p', { style: lecturesMessageStyle }, `No Course in ${TRACKER_ROOT}/. Run /study-plus to create one.`)];
1693
1713
  } else {
1694
1714
  nodes = content.error === null
1695
1715
  ? []
1696
- : [React.createElement('p', { role: 'alert', style: panelMutedStyle }, `The lecture library could not be read: ${content.error}.`)];
1716
+ : [React.createElement('p', { role: 'alert', style: lecturesMessageStyle }, `The lecture library could not be read: ${content.error}.`)];
1697
1717
  const picker = topicPicker(content.courses, content.course ?? selected, choose);
1698
- if (picker !== null) nodes.push(React.createElement('div', { style: panelHeaderStyle }, picker));
1718
+ if (picker !== null) nodes.push(React.createElement('div', { style: lecturesHeaderStyle }, picker));
1699
1719
  if (content.course !== null) {
1700
1720
  nodes.push(lectureTopic(
1701
1721
  { topic: content.course, entries: content.entries },
@@ -1708,7 +1728,7 @@ window.__ModuleLoader__.load({
1708
1728
  return React.createElement('div', {
1709
1729
  role: 'region',
1710
1730
  'aria-label': LECTURES_LABEL,
1711
- style: panelStyle,
1731
+ style: lecturesPanelStyle,
1712
1732
  }, React.createElement('style', null, LECTURES_CSS), ...nodes);
1713
1733
  }
1714
1734
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-nerd-mode",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "A study switch and study skills for DeepSeek Harness.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: study
3
- description: Teach one concept or help complete one task, using the supplied material as scope and defaulting to chat output.
3
+ description: "[v0.4.6] Teach one concept or help complete one task, using the supplied material as scope and defaulting to chat output."
4
4
  argument-hint: "<concept or task>"
5
5
  ---
6
6
  # Study
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: study-plus
3
- description: Run one tracked session against a dated learning record. Load for an explicit command or after the user chooses tracked study in the upper router.
3
+ description: "[v0.4.6] Run one tracked session against a dated learning record. Load for an explicit command or after the user chooses tracked study in the upper router."
4
4
  argument-hint: "<course-slug>, or empty to continue the current Course"
5
5
  ---
6
6
  # Study Plus
@@ -52,15 +52,15 @@ For a new graph:
52
52
  2. Make a concept table in prerequisite order. Each row has a one-line definition and prerequisite numbers. Keep prerequisite order separate from textbook reading order.
53
53
  3. Use the source's book and chapter hierarchy when present. Otherwise, propose logical chapter groupings.
54
54
  4. Show the full graph and chapter layout in the reply before the approval question, as a Markdown table. Keep the question text short, because it renders as one paragraph: ask whether to approve, edit, or rebuild the graph shown above, and state that approval starts draft generation for every concept, including any concept the learner then skips, and that drafts are not teaching. Do not put the table in the question text. Do not add another approval step.
55
- 5. After approval, run calibration as one `question` call with up to three items: what the learner wants to be able to do after this Course, what they have already read, studied, or used in this area in their own words, and which concepts in the approved graph they know well enough to skip, as a multi-select list. Items 1 and 2 are free text. Ask the goal and background items only when nothing already answers them: not the current request, not the opening question, not the supplied material. Reuse an opening-question answer verbatim. Derive the level. Then write `graph.md`, create `state.md` with every concept `new` and undated except the concepts selected to skip, which are `skipped`, create an empty `log.md`, and generate one draft per concept (section 3). Do not create `plan.md`.
55
+ 5. After approval, run calibration as one question-tool call with up to three items: what the learner wants to be able to do after this Course, what they have already read, studied, or used in this area in their own words, and which concepts in the approved graph they know well enough to skip, as a multi-select list. Items 1 and 2 are free text. Ask the goal and background items only when nothing already answers them: not the current request, not the opening question, not the supplied material. Reuse an opening-question answer verbatim. Derive the level. Then write `graph.md`, create `state.md` with every concept `new` and undated except the concepts selected to skip, which are `skipped`, create an empty `log.md`, and generate one draft per concept (section 3). Do not create `plan.md`.
56
56
 
57
57
  Calibration writes `Goal`, `Background`, and `Teaching level` to `graph.md`. Derive the level from the two answers:
58
58
 
59
- | Answers | Level |
60
- |---|---|
61
- | Most prerequisites are unknown, or the goal is to learn the topic from zero | `beginner` |
59
+ | Answers | Level |
60
+ | ------------------------------------------------------------------------------------------ | ------------ |
61
+ | Most prerequisites are unknown, or the goal is to learn the topic from zero | `beginner` |
62
62
  | Some prerequisites are known, and the goal is to understand the principles or pass an exam | `standard` |
63
- | Most prerequisites are known, and the goal is review, reference, or use in a project | `advanced` |
63
+ | Most prerequisites are known, and the goal is review, reference, or use in a project | `advanced` |
64
64
 
65
65
  When the two answers clearly conflict, you may choose an adjacent level and state the reason in one line. When the learner declines or gives no usable answer, write `Teaching level: standard` and omit the `Goal` and `Background` lines. Each selected concept becomes `skipped`, with no date and no passes.
66
66
 
@@ -136,7 +136,7 @@ If the row was already `in_process`, say that the last session stopped before th
136
136
 
137
137
  **Ask unseen questions.** Use the explicit count or type preference in the current `/study-plus` request when present. Otherwise, use the profile defaults in the top-level Nerd Mode instructions. Plan the configured number of unseen question slots, from 0 to 10. A hint, re-ask, or similar question stays in its current slot and does not add a slot. For each slot, write down the expected answer and criteria for a good answer. Keep both private. A draft never contains them. The criteria test the conclusion and whether each step taken is valid, not the route you had in mind. Ask one question at a time, then wait. One call asks one question. Outside `study-plus`, ignore these preferences.
138
138
 
139
- For `short-answer`, ask the question in the conversation. For `multiple-choice`, ask it as one `question` call with four options and exactly one correct answer; choosing it is sufficient. Give the labels alone, with no description, and shuffle the four positions for each slot. A host without a `question` tool gets the same four options as a numbered text list. A typed answer that names or repeats one option counts as choosing it. Any other typed answer is graded like a short answer. For `mixed`, choose short-answer or multiple-choice independently at random for each planned slot. Do not add question-type details to the session log. When the configured count is zero, skip unseen questions. A passed Teach-back alone is sufficient to pass. With one or more slots, require a passed Teach-back and `ceil(2 × slot count / 3)` correct slots.
139
+ For `short-answer`, ask the question in the conversation. For `multiple-choice`, ask it as one question-tool call with four options and exactly one correct answer; choosing it is sufficient. Give the labels alone, with no description, and shuffle the four positions for each slot. A host without a question tool gets the same four options as a numbered text list. A typed answer that names or repeats one option counts as choosing it. Any other typed answer is graded like a short answer. For `mixed`, choose short-answer or multiple-choice independently at random for each planned slot. Do not add question-type details to the session log. When the configured count is zero, skip unseen questions. A passed Teach-back alone is sufficient to pass. With one or more slots, require a passed Teach-back and `ceil(2 × slot count / 3)` correct slots.
140
140
 
141
141
  A question exists to find the gap, not to stop the session. Nothing in this step blocks the learner.
142
142
 
@@ -13,8 +13,8 @@
13
13
  if (!panels.length) return;
14
14
 
15
15
  var L = book.lang === 'en'
16
- ? { search: 'Search the book', toc: 'Contents', none: 'No match', hits: ' hits', chapter: 'Chapter', section: 'Section', sections: 'sections', menu: 'Chapters', draft: 'Draft' }
17
- : { search: '搜索全书', toc: '目录', none: '没有匹配', hits: ' 处', chapter: '第', section: '第', sections: '节', menu: '章节', draft: '草稿' };
16
+ ? { search: 'Search the book', toc: 'Contents', none: 'No match', hits: ' hits', chapter: 'Chapter', sections: 'sections', menu: 'Chapters', draft: 'Draft' }
17
+ : { search: '搜索全书', toc: '目录', none: '没有匹配', hits: ' 处', chapter: '第', sections: '节', menu: '章节', draft: '草稿' };
18
18
 
19
19
  var layout = document.querySelector('.am-doc-layout');
20
20
  var body = document.querySelector('.am-doc-body');
@@ -33,10 +33,8 @@
33
33
  : L.chapter + chapter.n + '章 ' + chapter.name;
34
34
  }
35
35
 
36
- function sectionLabel(unit) {
37
- return book.lang === 'en'
38
- ? L.section + ' ' + unit.section
39
- : L.section + unit.section + '节';
36
+ function sectionNumber(unit) {
37
+ return unit.chapter + '.' + unit.section;
40
38
  }
41
39
 
42
40
  function chapterCount(chapter) {
@@ -46,9 +44,7 @@
46
44
  }
47
45
 
48
46
  function sectionTitle(unit) {
49
- return book.lang === 'en'
50
- ? L.section + ' ' + unit.section + '. ' + unit.title
51
- : L.section + unit.section + '节 ' + unit.title;
47
+ return sectionNumber(unit) + ' ' + unit.title;
52
48
  }
53
49
 
54
50
  var style = document.createElement('style');
@@ -70,6 +66,8 @@
70
66
  '.am-doc-body .am-md h3 { font-size: 18px; font-weight: 650; margin-top: 28px; }',
71
67
  '.am-doc-body .am-md h3:first-child { margin-top: 0; }',
72
68
  '.am-doc-body .am-md h4 { font-size: 16px; font-weight: 600; margin-top: 20px; }',
69
+ // am's overflow-x:auto makes overflow-y auto too; a 1px overshoot draws scrollbar arrows on Windows.
70
+ '.am-math-block { overflow-y: hidden; }',
73
71
  '.bk-context { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin: 0 0 8px; font-size: 13px; }',
74
72
  '.bk-context-chapter { color: var(--ink-2); font-weight: 600; }',
75
73
  '.bk-context-section { color: var(--ink-3); white-space: nowrap; }',
@@ -151,7 +149,7 @@
151
149
  chapter.textContent = chapterTitle(g.chapter);
152
150
  var section = document.createElement('span');
153
151
  section.className = 'bk-context-section';
154
- section.innerHTML = esc(sectionLabel(u)) + draftTag(u);
152
+ section.innerHTML = esc(sectionNumber(u)) + draftTag(u);
155
153
  context.appendChild(chapter);
156
154
  context.appendChild(section);
157
155
  var heading = panel.querySelector('.am-panel-head');
@@ -82,9 +82,13 @@ function build() {
82
82
  ...(book.goal ? [`subtitle: ${JSON.stringify(book.goal)}`] : []),
83
83
  '---',
84
84
  '',
85
- book.units.map((unit) => `## ${unit.section}. ${unit.title}\n\n${unit.body}`).join('\n\n'),
85
+ book.units.map((unit) => `## ${unit.chapter}.${unit.section} ${unit.title}\n\n${unit.body}`).join('\n\n'),
86
86
  '',
87
- ].join('\n');
87
+ ].join('\n')
88
+ // am reads $ and $$ math only: a LaTeX display delimiter on its own line
89
+ // would reach the page as a literal bracket, so turn it into a $$ block.
90
+ .replace(/^ {0,3}\\\[\s*$/gm, () => '$$')
91
+ .replace(/^ {0,3}\\\]\s*$/gm, () => '$$');
88
92
  const outPath = join(dir, 'book.html');
89
93
  const backupPath = join(dir, 'book.html.bak');
90
94
  const guardPath = (path, what) => {
@@ -123,7 +127,9 @@ function build() {
123
127
  units: book.units.map(({ concept, title, chapter, section, index, status }) => ({ concept, title, chapter, section, index, status })),
124
128
  };
125
129
  const json = JSON.stringify(payload).replace(/</g, '\\u003c');
126
- const client = readFileSync(CLIENT, 'utf8');
130
+ // HTML parsing turns CRLF into LF before the browser checks the CSP hash,
131
+ // so a Windows checkout would otherwise be blocked as the wrong hash.
132
+ const client = readFileSync(CLIENT, 'utf8').replace(/\r\n?/g, '\n');
127
133
  const html = readFileSync(tmpPath, 'utf8');
128
134
  const scriptText = `\n${client}\n`;
129
135
  const hash = createHash('sha256').update(scriptText, 'utf8').digest('base64');