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/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',
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