dsh-plugin-lookatstudy 0.20.0 → 0.21.0

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/lib/index.d.mts CHANGED
@@ -32,7 +32,7 @@ declare const Config: z<Config>;
32
32
  declare const name = "lookatstudy-plugin";
33
33
  declare const inject: string[];
34
34
  /**
35
- * Register the activation-gated study surface: the 25 `study_*` tools (kept
35
+ * Register the activation-gated study surface: the 31 `study_*` tools (kept
36
36
  * unregistered while dormant), the tutor persona (stable core + soul), the
37
37
  * dynamic learner-snapshot context, and the `/study` command — every prompt
38
38
  * text renders empty while inactive, and empty sections are dropped at
package/lib/index.mjs CHANGED
@@ -201,6 +201,9 @@ const ZH = {
201
201
  "note.retryRender": "重试渲染",
202
202
  "zoom.in": "放大面板字号",
203
203
  "zoom.out": "缩小面板字号",
204
+ "pane.rail.aria": "调整课程栏宽度",
205
+ "pane.chat.aria": "调整对话栏宽度",
206
+ "pane.tip": "拖拽调宽,双击重置",
204
207
  "palette.label": "命令面板",
205
208
  "palette.placeholder": "搜索课时/课程,或选择动作…",
206
209
  "palette.lessons": "课时",
@@ -544,6 +547,9 @@ const EN = {
544
547
  "note.retryRender": "Retry render",
545
548
  "zoom.in": "Increase panel font size",
546
549
  "zoom.out": "Decrease panel font size",
550
+ "pane.rail.aria": "Resize the course pane width",
551
+ "pane.chat.aria": "Resize the chat pane width",
552
+ "pane.tip": "Drag to resize, double-click to reset",
547
553
  "palette.label": "Command palette",
548
554
  "palette.placeholder": "Search lessons/courses or pick an action…",
549
555
  "palette.lessons": "Lesson",
@@ -1485,9 +1491,11 @@ function freshLesson(title, anchor, body, kind = "study") {
1485
1491
  * @param parsed - course tree from an importer.
1486
1492
  * @param source - import origin.
1487
1493
  * @param sourceRef - markdown/folder/repo reference for display.
1494
+ * @param languageTarget - the taught language (BCP-47) when the course IS a
1495
+ * language course (design-apply path only; markdown imports stay undefined).
1488
1496
  * @returns the imported (or pre-existing) course.
1489
1497
  */
1490
- function importCourse(state, parsed, source, sourceRef) {
1498
+ function importCourse(state, parsed, source, sourceRef, languageTarget) {
1491
1499
  const id = slugify(parsed.title);
1492
1500
  const existing = state.courses.find((c) => c.id === id);
1493
1501
  if (existing) return existing;
@@ -1497,6 +1505,7 @@ function importCourse(state, parsed, source, sourceRef) {
1497
1505
  source,
1498
1506
  sourceRef,
1499
1507
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1508
+ languageTarget: languageTarget ?? null,
1500
1509
  sections: parsed.sections.map((section) => {
1501
1510
  const lessons = section.lessons.map((lesson) => (() => {
1502
1511
  const st = freshLesson(lesson.title, lesson.anchor, lesson.body, lesson.world === "practice" ? "practice" : "study");
@@ -2626,11 +2635,13 @@ function sendJson(res, status, value) {
2626
2635
  /**
2627
2636
  * Read one JSON body, answering 400 on malformed or oversized input so the
2628
2637
  * handler never throws into the HTTP layer.
2638
+ * @param maxBytes - per-route body ceiling; defaults to the tight 64 kB every
2639
+ * small-json route wants (issue #4: only the attachment intake lifts it).
2629
2640
  * @returns the parsed value, or undefined when the response is already sent.
2630
2641
  */
2631
- async function readJsonBodySafe(req, res) {
2642
+ async function readJsonBodySafe(req, res, maxBytes = 65536) {
2632
2643
  try {
2633
- return await readJsonBody(req);
2644
+ return await readJsonBody(req, maxBytes);
2634
2645
  } catch (error) {
2635
2646
  sendJson(res, 400, {
2636
2647
  ok: false,
@@ -2639,13 +2650,13 @@ async function readJsonBodySafe(req, res) {
2639
2650
  return;
2640
2651
  }
2641
2652
  }
2642
- /** Read one JSON request body with a hard 64 kB cap; malformed bodies reject. */
2643
- function readJsonBody(req) {
2653
+ /** Read one JSON request body with a hard cap (64 kB unless the route lifts it); malformed bodies reject. */
2654
+ function readJsonBody(req, maxBytes = 65536) {
2644
2655
  return new Promise((resolve, reject) => {
2645
2656
  const chunks = [];
2646
2657
  req.on("data", (chunk) => {
2647
2658
  chunks.push(chunk);
2648
- if (chunks.reduce((n, c) => n + c.length, 0) > 65536) {
2659
+ if (chunks.reduce((n, c) => n + c.length, 0) > maxBytes) {
2649
2660
  reject(/* @__PURE__ */ new Error("request body too large"));
2650
2661
  return;
2651
2662
  }
@@ -3140,7 +3151,7 @@ function registerDashboard(webServer, deps) {
3140
3151
  return;
3141
3152
  }
3142
3153
  if (req.method === "POST" && pathname === "/lookatstudy/api/attachment") {
3143
- const body = await readJsonBodySafe(req, res);
3154
+ const body = await readJsonBodySafe(req, res, 29360128);
3144
3155
  if (body === void 0) return;
3145
3156
  const name = typeof body.name === "string" ? body.name.trim() : "";
3146
3157
  const data = typeof body.dataBase64 === "string" ? body.dataBase64 : "";
@@ -5043,8 +5054,13 @@ function renderDesignBrief(pending, part = 1) {
5043
5054
  lines.push("- if the directory layout is already clear (e.g. lessons/N-Topic/), keep its sections; do not over-reorganize.");
5044
5055
  lines.push(`- anchor is the full heading text used to slice the body; omit it for whole-file lessons.${shown.length > 80 ? " This part is large: design at file granularity (omit anchors, one lesson per file) to keep the JSON manageable." : ""}`);
5045
5056
  lines.push("");
5057
+ lines.push("### Language-course check (decide while designing)");
5058
+ lines.push("Is this course teaching a language ITSELF (vocabulary, grammar, reading/writing/listening of English/Japanese/Korean/Chinese/…), rather than a knowledge/tech course that merely happens to be written in some language? Typical tells: vocab lists, grammar-point explanations, dialogues/readings, sentence-translation pairs, level markers (N5/N1, TOEFL/IELTS, HSK, JLPT), pronunciation content.");
5059
+ lines.push("- Yes → add \"languageTarget\": \"<BCP-47 of the TAUGHT language>\" (teaching English → \"en\", Japanese → \"ja\"). The writing language is NOT the taught language — a Chinese-written English-textbook repo has languageTarget \"en\".");
5060
+ lines.push("- No, or unsure → omit it entirely (a normal course misflagged as a language course is worse than the reverse).");
5061
+ lines.push("");
5046
5062
  lines.push("Now design the course and call study_apply_design with:");
5047
- lines.push("{ \"sections\": [ { \"title\": \"...\", \"lessons\": [ { \"title\": \"...\", \"file\": \"<exact path from this brief>\", \"anchor\": \"<optional full heading text>\", \"world\": \"study\" | \"practice\" } ] } ] }");
5063
+ lines.push("{ \"languageTarget\": \"<only for language courses>\", \"sections\": [ { \"title\": \"...\", \"lessons\": [ { \"title\": \"...\", \"file\": \"<exact path from this brief>\", \"anchor\": \"<optional full heading text>\", \"world\": \"study\" | \"practice\" } ] } ] }");
5048
5064
  lines.push("Use ONLY file paths that appear in this brief — anything else is dropped. Apply directly, then walk the learner through the course map.");
5049
5065
  return lines.join("\n");
5050
5066
  }
@@ -6124,50 +6140,56 @@ function studyTools(store, deps = {}) {
6124
6140
  const applyDesign = defineTool({
6125
6141
  name: "study_apply_design",
6126
6142
  description: "Apply the tutor-designed course structure to the pending import (the one study_import_github or study_import_folder returned design_required for). Every lesson's file must come from the design brief — unknown paths are dropped (anti-hallucination); lesson bodies are sliced by their anchor heading and the course is imported. On a validation or fetch error the tutor fixes the design and simply calls again.",
6127
- parameters: { sections: {
6128
- type: "array",
6129
- required: true,
6130
- description: "Designed sections in learning order.",
6131
- items: {
6132
- type: "object",
6133
- additionalProperties: false,
6134
- properties: {
6135
- title: {
6136
- type: "string",
6137
- required: true,
6138
- description: "Section title (in the learner's language)."
6139
- },
6140
- lessons: {
6141
- type: "array",
6142
- required: true,
6143
- items: {
6144
- type: "object",
6145
- additionalProperties: false,
6146
- properties: {
6147
- title: {
6148
- type: "string",
6149
- required: true,
6150
- description: "Lesson title."
6151
- },
6152
- file: {
6153
- type: "string",
6154
- required: true,
6155
- description: "Exact file path from the design brief."
6156
- },
6157
- anchor: {
6158
- type: "string",
6159
- description: "Full H2/H3 heading text the lesson body starts at; omit for whole-file lessons."
6160
- },
6161
- world: {
6162
- type: "string",
6163
- description: "\"study\" (explanation) or \"practice\" (exercise/lab/notebook); anything else is treated as study."
6143
+ parameters: {
6144
+ languageTarget: {
6145
+ type: "string",
6146
+ description: "Set ONLY when this course teaches a language ITSELF (vocabulary, grammar, reading/writing of English/Japanese/Chinese/…) rather than being a knowledge/tech course that merely happens to be written in some language: the taught language as a BCP-47 tag (\"en\", \"ja\", \"zh-CN\", …). The writing language is NOT the taught language — a Chinese-written English-textbook repo has languageTarget \"en\". Typical tells: vocab lists, grammar points, dialogues/readings, sentence-translation pairs, level markers (N5/N1, TOEFL, HSK, JLPT), pronunciation content. When unsure, omit it — a normal course misflagged as a language course is worse than the reverse."
6147
+ },
6148
+ sections: {
6149
+ type: "array",
6150
+ required: true,
6151
+ description: "Designed sections in learning order.",
6152
+ items: {
6153
+ type: "object",
6154
+ additionalProperties: false,
6155
+ properties: {
6156
+ title: {
6157
+ type: "string",
6158
+ required: true,
6159
+ description: "Section title (in the learner's language)."
6160
+ },
6161
+ lessons: {
6162
+ type: "array",
6163
+ required: true,
6164
+ items: {
6165
+ type: "object",
6166
+ additionalProperties: false,
6167
+ properties: {
6168
+ title: {
6169
+ type: "string",
6170
+ required: true,
6171
+ description: "Lesson title."
6172
+ },
6173
+ file: {
6174
+ type: "string",
6175
+ required: true,
6176
+ description: "Exact file path from the design brief."
6177
+ },
6178
+ anchor: {
6179
+ type: "string",
6180
+ description: "Full H2/H3 heading text the lesson body starts at; omit for whole-file lessons."
6181
+ },
6182
+ world: {
6183
+ type: "string",
6184
+ description: "\"study\" (explanation) or \"practice\" (exercise/lab/notebook); anything else is treated as study."
6185
+ }
6164
6186
  }
6165
6187
  }
6166
6188
  }
6167
6189
  }
6168
6190
  }
6169
6191
  }
6170
- } },
6192
+ },
6171
6193
  output: {
6172
6194
  schema: {
6173
6195
  type: "object",
@@ -6200,12 +6222,16 @@ function studyTools(store, deps = {}) {
6200
6222
  droppedLessons: {
6201
6223
  type: "integer",
6202
6224
  required: true
6225
+ },
6226
+ languageTarget: {
6227
+ type: "string",
6228
+ description: "Present only when the course teaches a language itself (BCP-47); the tutor then keeps quiz material in the target language."
6203
6229
  }
6204
6230
  }
6205
6231
  },
6206
6232
  render: (_args, value) => [{
6207
6233
  type: "text",
6208
- text: `Imported designed course “${value.title}” (${value.sections} sections, ${value.lessons} lessons${value.droppedLessons > 0 ? `, ${value.droppedLessons} hallucinated lesson(s) dropped` : ""}). First lesson: “${value.firstLessonTitle}” (id ${value.firstLessonId}). Present the course map to the learner.`
6234
+ text: `Imported designed course “${value.title}” (${value.sections} sections, ${value.lessons} lessons${value.droppedLessons > 0 ? `, ${value.droppedLessons} hallucinated lesson(s) dropped` : ""})${typeof value.languageTarget === "string" ? ` — language course (${value.languageTarget}): keep reading passages, example sentences, and quiz language material in the target language` : ""}. First lesson: “${value.firstLessonTitle}” (id ${value.firstLessonId}). Present the course map to the learner.`
6209
6235
  }]
6210
6236
  },
6211
6237
  async execute(args, exec) {
@@ -6234,12 +6260,14 @@ function studyTools(store, deps = {}) {
6234
6260
  }
6235
6261
  const parsed = buildCourseFromDesign(pd.part !== void 0 && pd.partCount !== void 0 && pd.partCount > 1 ? `${pd.courseTitle} (part ${pd.part})` : pd.courseTitle, validated, contents, pd.translations);
6236
6262
  requireParsedLessons(parsed);
6237
- const value = mutate((state) => toImportValue(importCourse(state, parsed, pd.source, pd.url)));
6263
+ const languageTarget = typeof args.languageTarget === "string" && args.languageTarget.trim() !== "" ? args.languageTarget.trim().slice(0, 35) : null;
6264
+ const value = mutate((state) => toImportValue(importCourse(state, parsed, pd.source, pd.url, languageTarget)));
6238
6265
  pendingDesign = null;
6239
6266
  pendingPart = 1;
6240
6267
  return {
6241
6268
  ...value,
6242
- droppedLessons: validated.droppedLessons
6269
+ droppedLessons: validated.droppedLessons,
6270
+ ...languageTarget !== null ? { languageTarget } : {}
6243
6271
  };
6244
6272
  },
6245
6273
  timeoutMs: 18e4,
@@ -8688,6 +8716,45 @@ function studyTools(store, deps = {}) {
8688
8716
  ];
8689
8717
  }
8690
8718
  //#endregion
8719
+ //#region src/vendor/locale-names.ts
8720
+ /**
8721
+ * Vendored from LookatStudy shared/locales.ts (MIT License,
8722
+ * https://github.com/Kaiji-Z/LookatStudy), upstream v0.33.2 — only the
8723
+ * name-map + lookup function (the language-course round's BCP-47 → display
8724
+ * name need). Unmodified except this provenance header.
8725
+ *
8726
+ * locales —— BCP-47 → 人类可读语言名的共享映射(纯函数)。
8727
+ * 未知 locale 一律原样返回(如 "pt" → "pt"),不猜名字。
8728
+ */
8729
+ /** locale → 该语言的自称名(尽量用母语写法,语言学习者认得出) */
8730
+ const LOCALE_LANGUAGE_NAMES = {
8731
+ en: "English",
8732
+ "zh-CN": "中文",
8733
+ "zh-TW": "繁體中文",
8734
+ ja: "日本語",
8735
+ ko: "한국어",
8736
+ fr: "Français",
8737
+ de: "Deutsch",
8738
+ es: "Español",
8739
+ "pt-BR": "Português",
8740
+ ru: "Русский",
8741
+ it: "Italiano",
8742
+ ar: "العربية",
8743
+ hi: "हिन्दी",
8744
+ tr: "Türkçe",
8745
+ pl: "Polski",
8746
+ nl: "Nederlands",
8747
+ id: "Indonesia",
8748
+ vi: "Tiếng Việt",
8749
+ th: "ไทย",
8750
+ sv: "Svenska",
8751
+ fi: "Suomi"
8752
+ };
8753
+ /** BCP-47 → 语言名;未映射的 locale 原样返回 */
8754
+ function localeToLanguageName(locale) {
8755
+ return LOCALE_LANGUAGE_NAMES[locale] ?? locale;
8756
+ }
8757
+ //#endregion
8691
8758
  //#region src/surface.ts
8692
8759
  /**
8693
8760
  * Stable tutor core (ported from LookatStudy's BASE_AGENT_PROMPT plus its
@@ -8798,6 +8865,16 @@ function snapshotSectionText(state) {
8798
8865
  if (memory.length > 0) lines.push(`记忆: ${memory.join(" | ")}`);
8799
8866
  if (snap.dueCount > 0) lines.push(`今日待复习: ${snap.dueCount} 项`);
8800
8867
  if (snap.pendingProposal !== null) lines.push(`待决提案 ${snap.pendingProposal.id}: ${snap.pendingProposal.rationale}(等学习者表态)`);
8868
+ const focusCourse = state.courses.find((c) => c.id === snap.focus?.courseId);
8869
+ if (focusCourse?.languageTarget != null && focusCourse.languageTarget !== "") {
8870
+ const t = localeToLanguageName(focusCourse.languageTarget);
8871
+ lines.push("");
8872
+ lines.push(`【语言教学姿态】这门课程教的是${t}——教学语言(讲解、指令、反馈)跟随学习者的界面语言,${t}是学习对象:`);
8873
+ lines.push(`- 课文引用、例句、题目素材保持${t}原文,考察目标语言本身——不要把目标语言材料翻译成教学语言来讲解或出题(生词首次出现可附教学语言注释,注释是辅助不是替代);`);
8874
+ lines.push(`- 出题双轴:题干和选项中的语言素材(句子/单词/短语/语法点)用${t}原文,指令性文字和解析用教学语言;`);
8875
+ lines.push(`- 学习者用${t}造句或表达时,先肯定再纠正其中的语言错误(语法/用词/拼写),用教学语言讲清错在哪;`);
8876
+ lines.push(`- 随掌握度上升,渐进提高${t}素材的占比,把学习者往"直接用${t}读"带。`);
8877
+ }
8801
8878
  if (state.historyBudget === true) lines.push("历史预算已开启:当会话变长时,主动概括旧轮次的要点并基于概要继续,避免逐字复读历史。");
8802
8879
  return lines.join("\n");
8803
8880
  }
@@ -8843,7 +8920,7 @@ function createStudySurface(registry, store) {
8843
8920
  const name = "lookatstudy-plugin";
8844
8921
  const inject = ["tools", "systemPrompt"];
8845
8922
  /**
8846
- * Register the activation-gated study surface: the 25 `study_*` tools (kept
8923
+ * Register the activation-gated study surface: the 31 `study_*` tools (kept
8847
8924
  * unregistered while dormant), the tutor persona (stable core + soul), the
8848
8925
  * dynamic learner-snapshot context, and the `/study` command — every prompt
8849
8926
  * text renders empty while inactive, and empty sections are dropped at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-lookatstudy",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "packageManager": "pnpm@11.7.0",
5
5
  "description": "Turn any markdown, local folder, or GitHub learning repo into a guided course inside DeepSeek Harness: gated skill-tree progression, BKT mastery tracking, SM-2 spaced repetition.",
6
6
  "type": "module",