dsh-nerd-mode 0.4.0 → 0.4.4
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/NOTICE.md +1 -1
- package/README.md +303 -215
- package/README.zh.md +303 -217
- package/assets/calibration.svg +2 -2
- package/assets/section.md +15 -0
- package/client.js +93 -77
- package/cordis.patch.yml +10 -14
- package/lib/index.js +223 -213
- package/package.json +1 -1
- package/skills/diagram/SKILL.md +0 -2
- package/skills/study/SKILL.md +3 -3
- package/skills/study-plus/SKILL.md +26 -19
- package/skills/study-plus/scripts/book-data.mjs +3 -3
- package/skills/study-plus/scripts/build-book.mjs +5 -5
- package/skills/teach/GLOSSARY-FORMAT.md +0 -35
- package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
- package/skills/teach/MISSION-FORMAT.md +0 -31
- package/skills/teach/RESOURCES-FORMAT.md +0 -32
- package/skills/teach/SKILL.md +0 -140
- package/skills/teach/agents/openai.yaml +0 -5
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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, '
|
|
860
|
+
React.createElement('span', null, 'Course'),
|
|
863
861
|
React.createElement('select', {
|
|
864
|
-
'aria-label': '
|
|
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
|
|
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 =
|
|
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
|
-
|
|
920
|
+
setTopics(found.topics);
|
|
923
921
|
setContent(found.content);
|
|
924
922
|
} catch (error) {
|
|
925
923
|
if (!live) return;
|
|
926
|
-
const message = error?.message ?? 'the
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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 `{
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
]
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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 [
|
|
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
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
|
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
|
-
|
|
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',
|
package/cordis.patch.yml
CHANGED
|
@@ -6,23 +6,19 @@
|
|
|
6
6
|
recallSessionEnabled: false
|
|
7
7
|
unseenQuestionCount: 3
|
|
8
8
|
unseenQuestionType: short-answer
|
|
9
|
-
section: |
|
|
10
|
-
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.
|
|
11
|
-
|
|
12
|
-
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 topic and supplied material, without a route question. Otherwise, load `study` by default for one concept or one learning task and use chat output. 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/<topic>/`. Do not grade, score, or log in `study`.
|
|
13
|
-
|
|
14
|
-
Every other 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.
|
|
15
|
-
|
|
16
|
-
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`.
|
|
17
|
-
|
|
18
|
-
Teaching level: run calibration once per new topic, 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 the current request and the supplied material do not already state them. 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 topic that already has a `graph.md`. A mid-topic change of depth lasts for this session only. Only an explicit "from now on" rewrites the `Teaching level` line.
|
|
19
|
-
|
|
20
|
-
Routing order: an explicit `/study-plus` loads `study-plus` directly, even for multiple independent concepts, and never asks a route question. Otherwise, for multiple independent concepts, ask exactly one question-tool choice before loading either skill. This includes default Nerd Mode and explicit `/study` entry. Offer only one-off complete Book, Chapter, and Section content in this conversation, or tracked study-plus with one concept per session. If the learner chooses one-off, load `study` with the current topic and supplied material. If the learner chooses tracked, load `study-plus` with the current topic and supplied material. Never ask again after the learner chooses. For one concept or one learning task without explicit `/study-plus`, load `study` by default.
|
|
21
|
-
|
|
22
|
-
`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`.
|
|
23
9
|
- id: study-plus-skills
|
|
24
10
|
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
25
11
|
config:
|
|
26
12
|
providerName: study-plus
|
|
27
13
|
includeDefaultRoots: false
|
|
28
14
|
bundledSkillDir: !!js process.getBuiltinModule('node:path').join(process.getBuiltinModule('node:path').dirname(process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('dsh-nerd-mode/package.json')), 'skills')
|
|
15
|
+
|
|
16
|
+
# Goal mode stays off. The built-in create_goal description tells the model to
|
|
17
|
+
# arm a goal for any long-running request, which turns one study answer into an
|
|
18
|
+
# auto-continuing run. Without the three packages below, no goal can be created.
|
|
19
|
+
- id: tool-goal
|
|
20
|
+
disabled: true
|
|
21
|
+
- id: command-goal
|
|
22
|
+
disabled: true
|
|
23
|
+
- id: goal-round-driver
|
|
24
|
+
disabled: true
|