dsh-plugin-lookatstudy 0.8.1 → 0.9.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # dsh-plugin-lookatstudy
2
2
 
3
- Turn any markdown document, local folder, or GitHub learning repository into a guided course inside [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) — your dsh agent becomes a full AI tutor with the interaction design of [LookatStudy](https://github.com/kaiji/LookatStudy): per-concept knowledge tracking, mastery-driven progression, spaced repetition, mastery proposals, friction awareness, learner memory, a Cornell notebook, and an in-chat proposal card. Learning engine modules are vendored from LookatStudy (MIT).
3
+ Turn any markdown document, local folder, or GitHub learning repository into a guided course inside [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) — your dsh agent becomes a full AI tutor with the interaction design of [LookatStudy](https://github.com/Kaiji-Z/LookatStudy): per-concept knowledge tracking, mastery-driven progression, spaced repetition, mastery proposals, friction awareness, learner memory, a Cornell notebook, and an in-chat proposal card. Learning engine modules are vendored from LookatStudy (MIT).
4
4
 
5
5
  ## Install
6
6
 
@@ -65,8 +65,8 @@ LookatStudy's Electron-native experiences have no host surface in dsh: persisten
65
65
  pnpm exec tsdown # build lib/ (host + client entries, peers external)
66
66
  pnpm test # 48 node:test cases over the real source (no key needed)
67
67
 
68
- # iterate against a live dsh (from a deepseek-harness checkout):
69
- pnpm dsh web --patch ./lookatstudy-plugin/cordis.dev.yml
68
+ # iterate against a live dsh (this repo lives beside a deepseek-harness checkout):
69
+ pnpm dsh web --patch ../dsh-plugin-lookatstudy/cordis.dev.yml # run from the harness checkout
70
70
  # then open http://127.0.0.1:3080/ and switch to the 学习 tab
71
71
  ```
72
72
 
@@ -0,0 +1,47 @@
1
+ //#region src/vendor/adoc-parser.ts
2
+ function parseAdoc(adocText) {
3
+ const lines = adocText.split("\n");
4
+ const output = [];
5
+ let inSourceBlock = false;
6
+ let pendingLang = "";
7
+ for (let i = 0; i < lines.length; i++) {
8
+ const line = lines[i] ?? "";
9
+ const sourceMatch = line.match(/^\[source,\s*(\w*)\s*\]$/);
10
+ if (sourceMatch && !inSourceBlock) {
11
+ pendingLang = sourceMatch[1] ?? "";
12
+ if ((lines[i + 1] ?? "").trim() === "----") {
13
+ inSourceBlock = true;
14
+ i++;
15
+ output.push(`\`\`\`${pendingLang}`);
16
+ continue;
17
+ }
18
+ }
19
+ if (inSourceBlock && line.trim() === "----") {
20
+ inSourceBlock = false;
21
+ output.push("```");
22
+ continue;
23
+ }
24
+ if (inSourceBlock) {
25
+ output.push(line);
26
+ continue;
27
+ }
28
+ let processed = line;
29
+ processed = processed.replace(/^(=+)\s+/, (_match, eqs) => {
30
+ if (eqs.length > 6) return "#".repeat(6) + " ";
31
+ return "#".repeat(eqs.length) + " ";
32
+ });
33
+ processed = processed.replace(/image::(\S+?)\[([^\]]*)\]/g, (_m, path, alt) => {
34
+ return alt ? `![${alt}](${path})` : `![](${path})`;
35
+ });
36
+ processed = processed.replace(/image:(\S+?)\[([^\]]*)\]/g, (_m, path, alt) => {
37
+ return alt ? `![${alt}](${path})` : `![](${path})`;
38
+ });
39
+ processed = processed.replace(/link:(\S+?)\[([^\]]*)\]/g, "[$2]($1)");
40
+ processed = processed.replace(/(\s|^)\*([^\s*][^*]*?)\*(?=\s|[.,;:!?)])/gm, "$1**$2**");
41
+ processed = processed.replace(/(\s|^)_([^\s_][^_]*?)_(?=\s|[.,;:!?)])/gm, "$1*$2*");
42
+ output.push(processed);
43
+ }
44
+ return { markdown: output.join("\n") };
45
+ }
46
+ //#endregion
47
+ export { parseAdoc };
package/lib/client.js CHANGED
@@ -102,6 +102,7 @@ window.__ModuleLoader__.load({
102
102
  .lks-pct{font-size:12px;color:var(--dsw-alias-label-tertiary);flex:none;width:32px;text-align:right}
103
103
 
104
104
  .lks-empty{color:var(--dsw-alias-label-tertiary);text-align:center;padding:48px 8px;font-size:14px;line-height:2}
105
+ .lks-actbar{display:flex;flex-direction:row;justify-content:flex-end;flex:none;padding-bottom:8px}
105
106
 
106
107
  /* ── middle column: the tutor ── */
107
108
  .lks-transcript{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:10px;padding:4px 2px}
@@ -325,6 +326,18 @@ window.__ModuleLoader__.load({
325
326
  });
326
327
  this.refresh();
327
328
  }
329
+ /**
330
+ * Flip learning mode. The host syncs its tool registry before responding,
331
+ * so awaiting this guarantees the `study_*` tools exist for the next prompt.
332
+ */
333
+ async activate(active) {
334
+ await fetchJson("/lookatstudy/api/active", {
335
+ method: "POST",
336
+ headers: { "content-type": "application/json" },
337
+ body: JSON.stringify({ active })
338
+ });
339
+ this.refresh();
340
+ }
328
341
  /** Point the tutor's focus at one lesson. */
329
342
  async setFocus(lessonId) {
330
343
  await fetchJson("/lookatstudy/api/focus", {
@@ -362,6 +375,7 @@ window.__ModuleLoader__.load({
362
375
  function useStudy() {
363
376
  return {
364
377
  data: (0, react.useSyncExternalStore)(studyStore.subscribe, studyStore.getSnapshot, studyStore.getSnapshot),
378
+ activate: studyStore.activate.bind(studyStore),
365
379
  setMode: studyStore.setMode.bind(studyStore),
366
380
  setFocus: studyStore.setFocus.bind(studyStore),
367
381
  deleteCourse: studyStore.deleteCourse.bind(studyStore),
@@ -707,14 +721,11 @@ window.__ModuleLoader__.load({
707
721
  call: node.call
708
722
  });
709
723
  break;
710
- case "turn-error":
711
- rows.push({
712
- key: `e${node.seq}`,
713
- role: "error",
714
- text: node.message
715
- });
716
- break;
717
- default: break;
724
+ case "turn-error": rows.push({
725
+ key: `e${node.seq}`,
726
+ role: "error",
727
+ text: node.message
728
+ });
718
729
  }
719
730
  if (partial !== null) {
720
731
  const text = assistantText(partial.blocks);
@@ -762,18 +773,27 @@ window.__ModuleLoader__.load({
762
773
  }
763
774
  /** Tab body: the factory-bound ctx carries workspaces/sessions for the per-lesson session jumps. */
764
775
  function StudyTab({ useSession, inputActions, ctx }) {
765
- const { data, setMode, setFocus, deleteCourse, bindLessonSession } = useStudy();
776
+ const { data, activate, setMode, setFocus, deleteCourse, bindLessonSession } = useStudy();
766
777
  const snapshot = useSession((s) => s);
767
778
  const [pane, setPane] = (0, react.useState)(storedPane);
768
779
  const send = (text) => {
769
- inputActions.setDraft(text);
770
- inputActions.submit();
780
+ (async () => {
781
+ if (data?.active !== true) await activate(true);
782
+ inputActions.setDraft(text);
783
+ inputActions.submit();
784
+ })();
771
785
  };
772
786
  return (0, react.createElement)("div", {
773
787
  className: "lks-root lks-study",
774
788
  "data-conversation-composer-overlay": "",
775
789
  "data-pane": pane
776
- }, (0, react.createElement)("div", { className: "lks-body" }, (0, react.createElement)("div", { className: "lks-switch" }, ...[
790
+ }, (0, react.createElement)("div", { className: "lks-actbar" }, data === null ? null : (0, react.createElement)("button", {
791
+ className: `lks-btn ${data.active ? "ghost" : "primary"}`,
792
+ title: data.active ? "注销 study 工具与导师人格;学习进度保留,可随时重新开启" : "注册 study 工具并载入导师人格,之后普通对话和现在一样",
793
+ onClick: () => {
794
+ activate(!data.active);
795
+ }
796
+ }, data.active ? "⏻ 退出学习模式" : "▶ 开始学习")), (0, react.createElement)("div", { className: "lks-body" }, (0, react.createElement)("div", { className: "lks-switch" }, ...[
777
797
  {
778
798
  id: "rail",
779
799
  label: "课程"
@@ -797,6 +817,7 @@ window.__ModuleLoader__.load({
797
817
  }
798
818
  }, p.label))), (0, react.createElement)(CourseRail, {
799
819
  data,
820
+ activate,
800
821
  setFocus,
801
822
  deleteCourse,
802
823
  bindLessonSession,
@@ -811,7 +832,7 @@ window.__ModuleLoader__.load({
811
832
  }), (0, react.createElement)(BlackboardColumn, { data })));
812
833
  }
813
834
  /** Left column: course management (pick/delete/search/import), lesson tree, due box. */
814
- function CourseRail({ data, setFocus, deleteCourse, bindLessonSession, send, ctx, currentSessionId }) {
835
+ function CourseRail({ data, activate, setFocus, deleteCourse, bindLessonSession, send, ctx, currentSessionId }) {
815
836
  const [jumping, setJumping] = (0, react.useState)(null);
816
837
  /** Open (or mint) the lesson's own session — the simplified thread system. */
817
838
  const openLessonThread = (lesson) => {
@@ -828,6 +849,7 @@ window.__ModuleLoader__.load({
828
849
  }
829
850
  setJumping(lesson.id);
830
851
  setFocus(lesson.id).then(() => (async () => {
852
+ if (data?.active !== true) await activate(true);
831
853
  const area = await fetch("/lookatstudy/api/study-workspace");
832
854
  if (!area.ok) throw new Error(`study area unavailable (HTTP ${area.status})`);
833
855
  const { path } = await area.json();
@@ -1129,6 +1151,7 @@ window.__ModuleLoader__.load({
1129
1151
  }
1130
1152
  /** Button body in its own component so hook order stays constant. */
1131
1153
  function Inner({ ctx }) {
1154
+ const { data, activate } = useStudy();
1132
1155
  const [busy, setBusy] = (0, react.useState)(false);
1133
1156
  const [error, setError] = (0, react.useState)(null);
1134
1157
  const start = () => {
@@ -1137,6 +1160,7 @@ window.__ModuleLoader__.load({
1137
1160
  setError(null);
1138
1161
  (async () => {
1139
1162
  try {
1163
+ if (data?.active !== true) await activate(true);
1140
1164
  const area = await fetch("/lookatstudy/api/study-workspace");
1141
1165
  if (!area.ok) throw new Error(`study area unavailable (HTTP ${area.status})`);
1142
1166
  const { path } = await area.json();
@@ -1169,7 +1193,7 @@ window.__ModuleLoader__.load({
1169
1193
  title: "一键准备学习:建立学习工作区、开启会话并让导师就位",
1170
1194
  disabled: busy,
1171
1195
  onClick: start
1172
- }, busy ? (0, react.createElement)(IconLoadingOutline16, { className: "lks-spin" }) : (0, react.createElement)(IconThinkOutline16, null), busy ? "正在准备学习区…" : "开始学习"), error !== null ? (0, react.createElement)("span", {
1196
+ }, busy ? (0, react.createElement)(IconLoadingOutline16, { className: "lks-spin" }) : (0, react.createElement)(IconThinkOutline16, null), busy ? "正在准备学习区…" : data?.active === true ? "进入学习" : "开始学习"), error !== null ? (0, react.createElement)("span", {
1173
1197
  className: "lks-propcard-err",
1174
1198
  style: { marginTop: 0 }
1175
1199
  }, error) : null);