dsh-deja 0.18.0 → 0.18.2

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
@@ -12,6 +12,8 @@ disk. Nothing has to have been recorded ahead of time: the history is already
12
12
  there, including the months before deja was installed. If you moved to dsh last
13
13
  week, everything you did before last week is still reachable from inside it.
14
14
 
15
+ <p align="center"><img src="assets/panel.png" width="820" alt="The history strip above the DSH composer: one row per past session, with the agent it came from, the project, the date and the match count"></p>
16
+
15
17
  ## Install
16
18
 
17
19
  ```sh
@@ -34,6 +36,10 @@ Three tools the model can call:
34
36
 
35
37
  One command: `/deja <what to look for>`.
36
38
 
39
+ A strip above the composer, so it is there without asking the model for it:
40
+ type a query, get one row per past session — the agent it came from, the
41
+ project, the date, the match count — and click a row to open the rest.
42
+
37
43
  And, unless you turn it off, automatic recall: before each step the plugin asks
38
44
  deja whether this machine's history answers the prompt, and adds the answer to
39
45
  the runtime context. Silence is the common case — it speaks only when there is
package/README.zh.md CHANGED
@@ -6,6 +6,8 @@ DeepSeek Harness 自带的 `session-query` 已经可以检索它自己的会话
6
6
 
7
7
  deja 索引 Claude Code、Codex、Cursor、opencode、Antigravity、Grok Build、Kimi、Cline、Zed 等二十个智能体本来就写在磁盘上的会话文件。不需要提前录制:历史已经在那里,包括安装 deja 之前的那些月份。如果你上周才转到 dsh,上周之前做过的事仍然可以在 dsh 里查到。
8
8
 
9
+ <p align="center"><img src="assets/panel.png" width="820" alt="DSH 输入框上方的历史条:每个历史会话一行,标出来自哪个智能体、项目、日期和命中数"></p>
10
+
9
11
  ## 安装
10
12
 
11
13
  ```sh
@@ -26,6 +28,8 @@ dsh plugin --profile web add dsh-deja
26
28
 
27
29
  一个命令:`/deja <要查什么>`。
28
30
 
31
+ 以及输入框上方的历史条,不必让模型代劳:输入查询,每个历史会话一行——来自哪个智能体、哪个项目、什么时间、命中多少——点开一行看全文。
32
+
29
33
  以及默认开启的自动召回:每一步之前,插件会问 deja 这台机器的历史能否回答当前提问,并把结果加入运行时上下文。多数情况下它保持沉默,只有确实有内容时才开口。
30
34
 
31
35
  ```yaml
package/client.js ADDED
@@ -0,0 +1,281 @@
1
+ // The browser half of dsh-deja: a strip above the composer that searches the
2
+ // history the other agents on this machine already wrote, and shows what it
3
+ // found without leaving the conversation.
4
+ //
5
+ // The web server serves exactly one file per plugin, so this is one CJS bundle
6
+ // inside the ModuleLoader handshake. React and every @deepseek-ai/* package
7
+ // come from the app's own module system through `require` — nothing is bundled
8
+ // here, which is why this file is plain JavaScript with no build step.
9
+ //
10
+ // Searching needs the index, which lives behind a binary the browser cannot
11
+ // run. Rather than mount a Remote namespace of its own, this calls the command
12
+ // the host half already registers: ctx.remote.commands.execute runs "/deja
13
+ // <query>" in the current session and hands back its text.
14
+ window.__ModuleLoader__.load({
15
+ id: "dsh-deja",
16
+ factory: (require) => {
17
+ var module = { exports: {} };
18
+ var exports = module.exports;
19
+
20
+ const React = require("react");
21
+ const { useCallback, useEffect, useRef, useState } = React;
22
+ const h = React.createElement;
23
+
24
+ const STRIP = {
25
+ boxSizing: "border-box",
26
+ width: "100%",
27
+ display: "flex",
28
+ flexDirection: "column",
29
+ gap: "6px",
30
+ margin: "0 auto",
31
+ padding: "6px 10px",
32
+ borderRadius: "12px",
33
+ border: "1px solid var(--dsw-alias-border-l1)",
34
+ background: "var(--dsw-specific-tip)",
35
+ };
36
+ const ROW = { display: "flex", alignItems: "center", gap: "8px" };
37
+ const LABEL = {
38
+ flex: "none",
39
+ fontSize: "13px",
40
+ fontWeight: 500,
41
+ lineHeight: "24px",
42
+ color: "var(--dsw-alias-label-primary)",
43
+ };
44
+ const INPUT = {
45
+ flex: 1,
46
+ minWidth: 0,
47
+ height: "26px",
48
+ padding: "0 8px",
49
+ fontSize: "13px",
50
+ lineHeight: "20px",
51
+ color: "var(--dsw-alias-label-primary)",
52
+ background: "var(--dsw-alias-bg-base)",
53
+ border: "1px solid var(--dsw-alias-border-l2)",
54
+ borderRadius: "6px",
55
+ outline: "none",
56
+ };
57
+ const BUTTON = {
58
+ flex: "none",
59
+ height: "26px",
60
+ padding: "0 10px",
61
+ fontSize: "13px",
62
+ color: "var(--dsw-alias-label-secondary)",
63
+ background: "transparent",
64
+ border: "1px solid var(--dsw-alias-border-l2)",
65
+ borderRadius: "6px",
66
+ cursor: "pointer",
67
+ };
68
+ const NOTE = {
69
+ fontSize: "12px",
70
+ lineHeight: "18px",
71
+ color: "var(--dsw-alias-label-tertiary)",
72
+ };
73
+ const OUTPUT = {
74
+ maxHeight: "220px",
75
+ overflow: "auto",
76
+ margin: 0,
77
+ padding: "8px",
78
+ fontSize: "12px",
79
+ lineHeight: "18px",
80
+ whiteSpace: "pre-wrap",
81
+ wordBreak: "break-word",
82
+ color: "var(--dsw-alias-label-primary-dimmed)",
83
+ background: "var(--dsw-alias-bg-base)",
84
+ border: "1px solid var(--dsw-alias-border-l2)",
85
+ borderRadius: "8px",
86
+ };
87
+
88
+ const ROWS = { display: "flex", flexDirection: "column", gap: "6px", maxHeight: "260px", overflow: "auto" };
89
+ const ROW_BOX = {
90
+ padding: "6px 8px",
91
+ borderRadius: "8px",
92
+ background: "var(--dsw-alias-bg-base)",
93
+ border: "1px solid var(--dsw-alias-border-l2)",
94
+ cursor: "pointer",
95
+ };
96
+ const HEAD = { display: "flex", alignItems: "center", gap: "8px", fontSize: "12px", lineHeight: "18px" };
97
+ const BADGE = {
98
+ flex: "none",
99
+ padding: "1px 6px",
100
+ borderRadius: "999px",
101
+ fontSize: "11px",
102
+ color: "var(--dsw-alias-label-secondary)",
103
+ background: "var(--dsw-alias-interactive-bg-hover)",
104
+ };
105
+ const META = { color: "var(--dsw-alias-label-tertiary)" };
106
+ const SNIPPET = {
107
+ margin: "4px 0 0",
108
+ fontSize: "12px",
109
+ lineHeight: "17px",
110
+ color: "var(--dsw-alias-label-primary-dimmed)",
111
+ whiteSpace: "pre-wrap",
112
+ wordBreak: "break-word",
113
+ };
114
+
115
+ // parse turns what deja printed into rows. A header line names the agent,
116
+ // the project, the date and the match count; the lines indented under it
117
+ // are that session's own text. Anything matching neither shape — the notes
118
+ // deja writes about word forms and result caps — is dropped rather than
119
+ // shown as if it were a result.
120
+ function parse(text) {
121
+ const rows = [];
122
+ for (const line of String(text).split("\n")) {
123
+ const header = /^\[([^\]]+)\]\s+(\S+)\s+·\s+([^·]+?)\s+·\s+\S+\s+—\s+(.+)$/.exec(line);
124
+ if (header) {
125
+ rows.push({
126
+ agent: header[1].trim(),
127
+ project: header[2].trim(),
128
+ when: header[3].trim(),
129
+ count: header[4].trim(),
130
+ body: [],
131
+ });
132
+ continue;
133
+ }
134
+ if (rows.length > 0 && /^\s+\S/.test(line)) rows[rows.length - 1].body.push(line.trim());
135
+ }
136
+ return rows;
137
+ }
138
+
139
+ /** One past session. Collapsed it shows two lines; a click opens the rest. */
140
+ function Row({ row }) {
141
+ const [open, setOpen] = useState(false);
142
+ const body = open ? row.body : row.body.slice(0, 2);
143
+ return h(
144
+ "div",
145
+ { style: ROW_BOX, onClick: () => setOpen(!open) },
146
+ h(
147
+ "div",
148
+ { style: HEAD },
149
+ h("span", { style: BADGE }, row.agent),
150
+ h("span", { style: META }, row.project + " · " + row.when + " · " + row.count),
151
+ ),
152
+ body.length > 0 ? h("p", { style: SNIPPET }, body.join("\n")) : null,
153
+ );
154
+ }
155
+
156
+ /**
157
+ * HistoryStrip: collapsed it is one button, because a search box nobody
158
+ * asked for is noise above the composer. Open, it searches on Enter and
159
+ * shows one row per past session it found.
160
+ */
161
+ function HistoryStrip({ onSearch }) {
162
+ const [open, setOpen] = useState(false);
163
+ const [query, setQuery] = useState("");
164
+ const [state, setState] = useState({ kind: "idle" });
165
+ const inputRef = useRef(null);
166
+ const generation = useRef(0);
167
+
168
+ useEffect(() => {
169
+ if (open && inputRef.current) inputRef.current.focus();
170
+ }, [open]);
171
+
172
+ const run = useCallback(async () => {
173
+ const text = query.trim();
174
+ if (!text) return;
175
+ const mine = ++generation.current;
176
+ setState({ kind: "searching" });
177
+ try {
178
+ const answer = await onSearch(text);
179
+ // A late answer to an abandoned query must not overwrite a newer one.
180
+ if (mine !== generation.current) return;
181
+ setState({ kind: "done", text: answer, rows: parse(answer) });
182
+ } catch (error) {
183
+ if (mine !== generation.current) return;
184
+ setState({ kind: "failed", text: String((error && error.message) || error) });
185
+ }
186
+ }, [onSearch, query]);
187
+
188
+ if (!open) {
189
+ return h(
190
+ "div",
191
+ { style: { ...STRIP, padding: "4px 10px" } },
192
+ h(
193
+ "div",
194
+ { style: ROW },
195
+ h(
196
+ "button",
197
+ {
198
+ type: "button",
199
+ style: BUTTON,
200
+ onClick: () => setOpen(true),
201
+ },
202
+ "Search your other agents' history",
203
+ ),
204
+ ),
205
+ );
206
+ }
207
+
208
+ return h(
209
+ "div",
210
+ { style: STRIP },
211
+ h(
212
+ "div",
213
+ { style: ROW },
214
+ h("span", { style: LABEL }, "History"),
215
+ h("input", {
216
+ ref: inputRef,
217
+ style: INPUT,
218
+ value: query,
219
+ placeholder: "an error string, a file path, a flag",
220
+ onChange: (event) => setQuery(event.target.value),
221
+ onKeyDown: (event) => {
222
+ if (event.key === "Enter") {
223
+ event.preventDefault();
224
+ void run();
225
+ }
226
+ if (event.key === "Escape") setOpen(false);
227
+ },
228
+ }),
229
+ h("button", { type: "button", style: BUTTON, onClick: () => void run() }, "Search"),
230
+ h("button", { type: "button", style: BUTTON, onClick: () => setOpen(false) }, "Close"),
231
+ ),
232
+ state.kind === "searching" ? h("div", { style: NOTE }, "searching this machine's history…") : null,
233
+ state.kind === "failed" ? h("div", { style: NOTE }, "search failed: " + state.text) : null,
234
+ state.kind === "done"
235
+ ? state.rows.length > 0
236
+ ? h("div", { style: ROWS }, state.rows.map((row, i) => h(Row, { key: i, row })))
237
+ : h("div", { style: NOTE }, state.text)
238
+ : null,
239
+ );
240
+ }
241
+
242
+ // The dotted entry is not decoration: a plugin that injects only "remote"
243
+ // is refused the namespace with 'cannot get property "remote.commands"
244
+ // without inject' the first time it searches.
245
+ const inject = ["slots", "remote", "remote.commands"];
246
+
247
+ function apply(ctx) {
248
+ ctx.slots.inject("conversation.input.dock", () =>
249
+ ctx.slots.register(
250
+ {
251
+ name: "conversation.input.dock",
252
+ id: "deja-history",
253
+ order: 30,
254
+ inject: (sessionId) => ({
255
+ onSearch: async (query) => {
256
+ const result = await ctx.remote.commands.execute(sessionId, "/deja " + query, []);
257
+ if (!result || !result.ok) {
258
+ const error = result && result.error;
259
+ throw new Error(error ? error.code + ": " + error.message : "no answer from the host");
260
+ }
261
+ // The envelope is { value: { result: { kind, text } } }: the
262
+ // command's own answer sits one level in, and reading `text`
263
+ // off the outer object prints "[object Object]".
264
+ const value = result.value;
265
+ const answer = (value && value.result) || value;
266
+ const text = answer && typeof answer.text === "string" ? answer.text.trim() : "";
267
+ return text || "Nothing in this machine's history matches that.";
268
+ },
269
+ }),
270
+ },
271
+ HistoryStrip,
272
+ ),
273
+ );
274
+ }
275
+
276
+ exports.HistoryStrip = HistoryStrip;
277
+ exports.apply = apply;
278
+ exports.inject = inject;
279
+ return module.exports;
280
+ },
281
+ });
package/index.js CHANGED
@@ -14,13 +14,13 @@ const require = createRequire(import.meta.url);
14
14
  // missing peer takes the whole profile down with it, so both imports degrade:
15
15
  // without them the command and automatic recall still work, only the
16
16
  // model-facing tools are skipped.
17
+ // The tool registry lives in the host. A plugin that throws on a missing peer
18
+ // takes the whole profile down with it, so this degrades: without dsh-tools the
19
+ // command and automatic recall still work, only the model-facing tools are
20
+ // skipped.
17
21
  let defineTool = null;
18
- let z = null;
19
- let createUserMessage = null;
20
22
  try {
21
23
  ({ defineTool } = await import("@deepseek-ai/dsh-tools"));
22
- z = (await import("@deepseek-ai/schemastery")).default;
23
- ({ createUserMessage } = await import("@deepseek-ai/dsh-llm"));
24
24
  } catch {}
25
25
 
26
26
  const PLATFORM = process.platform === "win32" ? "windows" : process.platform;
@@ -59,12 +59,14 @@ function run(args, input) {
59
59
  const NOTHING = "Nothing in this machine's history matches that.";
60
60
 
61
61
  function tools(ctx) {
62
- if (!defineTool || !z) return;
62
+ if (!defineTool) return;
63
63
 
64
64
  // Every tool here answers with the text deja printed, so one output
65
- // declaration serves all three.
65
+ // declaration serves all three. The schema is plain JSON Schema — a
66
+ // schemastery instance is rejected as "schema must be a value schema object",
67
+ // because the host validates that this is an ordinary JSON record.
66
68
  const TEXT_OUTPUT = {
67
- schema: z.string(),
69
+ schema: { type: "string" },
68
70
  render: (_args, value) => [{ type: "text", text: String(value) }],
69
71
  };
70
72
 
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "dsh-deja",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "Brings the session history of nineteen other coding agents into DeepSeek Harness: recall, session digest and per-file history tools over a local index, plus optional automatic recall before each step.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
8
8
  ".": "./index.js",
9
+ "./client": "./client.js",
9
10
  "./package.json": "./package.json"
10
11
  },
11
12
  "files": [
12
13
  "index.js",
14
+ "client.js",
13
15
  "cordis.patch.yml",
14
16
  "README.md",
15
17
  "README.zh.md",
@@ -40,12 +42,18 @@
40
42
  "dsh": {
41
43
  "bundle": {
42
44
  "patch": "./cordis.patch.yml"
45
+ },
46
+ "client": {
47
+ "platform": "web",
48
+ "inject": [
49
+ "@deepseek-ai/dsh-client-runtime",
50
+ "@deepseek-ai/dsh-api-remotes",
51
+ "@deepseek-ai/dsh-client-ui-commands",
52
+ "@deepseek-ai/dsh-client-ui-conversation"
53
+ ]
43
54
  }
44
55
  },
45
56
  "dependencies": {
46
57
  "@vshulcz/deja-vu": "0.18.0"
47
- },
48
- "peerDependencies": {
49
- "@deepseek-ai/dsh-tools": "^0.1.0"
50
58
  }
51
59
  }