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
|
|
1654
|
+
const rows = [];
|
|
1637
1655
|
if (group.entries.length === 0) {
|
|
1638
|
-
|
|
1656
|
+
rows.push(React.createElement('p', { key: 'empty', style: panelMutedStyle }, 'No lectures yet.'));
|
|
1639
1657
|
} else if (main.length > 0) {
|
|
1640
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1670
|
+
rows.push(React.createElement('ul', { key: 'archive', style: panelListStyle }, ...archived.map((entry) => lectureRow(entry, onOpen))));
|
|
1653
1671
|
}
|
|
1654
1672
|
}
|
|
1655
|
-
|
|
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
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1731
|
+
style: lecturesPanelStyle,
|
|
1712
1732
|
}, React.createElement('style', null, LECTURES_CSS), ...nodes);
|
|
1713
1733
|
}
|
|
1714
1734
|
|
package/package.json
CHANGED
package/skills/study/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
60
|
-
|
|
61
|
-
| Most prerequisites are unknown, or the goal is to learn the topic from zero
|
|
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
|
|
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
|
|
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',
|
|
17
|
-
: { search: '搜索全书', toc: '目录', none: '没有匹配', hits: ' 处', chapter: '第',
|
|
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
|
|
37
|
-
return
|
|
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
|
|
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(
|
|
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}
|
|
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
|
-
|
|
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');
|