dsh-reports-panel 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bayan1377
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,58 @@
1
- # Temporary Holding Version
1
+ # dsh-reports-panel
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Панель «Отчёты» в левом сайдбаре DeepSeek Harness: файлы из папки `Отчёты` рабочей
4
+ папки сессии — под рукой, без переключения на файловый менеджер.
5
+
6
+ ## Что делает
7
+
8
+ - Читает список файлов из папки `Отчёты` текущей рабочей папки сессии через Remote
9
+ `workspaceFiles` — то есть показывает ровно то, что агент положил рядом с работой.
10
+ - Рисует ленту в левой панели, клик открывает файл.
11
+ - Есть кнопка «Обновить», состояния «Загружаю…», «Отчётов пока нет. Появятся после
12
+ обработки файлов.» и понятные ошибки: «Нет активной сессии — откройте чат и
13
+ обновите.», «Не удалось прочитать отчёты.».
14
+ - На хосте — только строка в Loader: ни сервисов, ни конфигурации, ни своих маршрутов.
15
+
16
+ ## Установка
17
+
18
+ Нужны Node.js 20+ и `pnpm` (DSH ставит плагины через него):
19
+
20
+ ```powershell
21
+ dsh plugin --profile desktop add dsh-reports-panel
22
+ ```
23
+
24
+ Обновление — двумя шагами, `add` поверх стоящего пакета DSH отклонит:
25
+
26
+ ```powershell
27
+ dsh plugin --profile desktop remove dsh-reports-panel
28
+ dsh plugin --profile desktop add dsh-reports-panel
29
+ ```
30
+
31
+ Перезапустите DSH после установки: плагин добавляет строку в сайдбар через
32
+ `@deepseek-ai/dsh-client-ui-sidebar`.
33
+
34
+ ## Настройка
35
+
36
+ Настройки нет. Папка берётся по имени `Отчёты` в рабочей папке сессии — если её нет,
37
+ панель показывает пустое состояние, а не ошибку.
38
+
39
+ ## Устройство
40
+
41
+ - `lib/index.js` — хост: пустой `apply()`, нужен только для строки в Loader.
42
+ - `lib/client.js` — браузерная половина: сайдбар, чтение `workspaceFiles`, рендер.
43
+ - `cordis.patch.yml` — вставка плагина в стек слоёв профиля.
44
+
45
+ ## Выключить
46
+
47
+ ```powershell
48
+ dsh plugin --profile desktop remove dsh-reports-panel
49
+ ```
50
+
51
+ ## Лицензия
52
+
53
+ MIT.
54
+
55
+ ---
56
+
57
+ English: reports feed in the DSH left sidebar — lists files from the `Отчёты` folder of
58
+ the session workspace through the `workspaceFiles` remote, with refresh and empty states.
@@ -0,0 +1,4 @@
1
+ # dsh bundle patch: вставляет плагин «Отчёты» в стек слоёв профиля.
2
+ - insert:
3
+ - id: reports-panel
4
+ name: 'dsh-reports-panel'
package/lib/client.js ADDED
@@ -0,0 +1,407 @@
1
+ /* Reports panel — левая панель «Отчёты».
2
+ *
3
+ * Читает папку «Отчёты» рабочей папки сессии через Remote `workspaceFiles`
4
+ * и показывает список карточек отчётов. Клик по карточке раскрывает текст
5
+ * отчёта прямо под этой карточкой, повторный клик — сворачивает и оставляет
6
+ * только карточки. Открытым может быть один отчёт; текст кешируется, чтобы
7
+ * повторное раскрытие не ходило в диск.
8
+ *
9
+ * Как и automation-log, панель defensive: отсутствие слота, сессии или Remote
10
+ * не должно валить загрузку клиента.
11
+ */
12
+ window.__ModuleLoader__.load({
13
+ id: "dsh-reports-panel",
14
+ factory: (require) => {
15
+ var module = { exports: {} };
16
+ var exports = module.exports;
17
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
18
+ const react = require("react");
19
+
20
+ const PANEL_ID = "reports";
21
+ const FOLDER = "Отчёты";
22
+ const LABEL = "Отчёты";
23
+ const SUBTITLE = "Отчёты по обработанным файлам. Нажмите на карточку, чтобы прочитать.";
24
+ const EMPTY = "Отчётов пока нет.";
25
+ const EMPTY_HINT = "Появятся после обработки файлов.";
26
+ const RETRY = "Обновить";
27
+ const LOADING = "Загружаю…";
28
+ const FAILED = "Не удалось прочитать отчёты.";
29
+ const NO_SESSION = "Нет активной сессии — откройте чат и обновите.";
30
+ const POLL_MS = 20000;
31
+
32
+ const COLOR = {
33
+ primary: "var(--dsw-alias-label-primary, #e6e6e6)",
34
+ secondary: "var(--dsw-alias-label-secondary, #9aa0a6)",
35
+ tertiary: "var(--dsw-alias-label-tertiary, #7c8288)",
36
+ border: "var(--dsw-alias-border-l1, rgba(255,255,255,0.10))",
37
+ hover: "var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06))",
38
+ accent: "var(--dsw-alias-brand-primary, #4d6bfe)"
39
+ };
40
+
41
+ /** Подпись отчёта: имя файла без .md. */
42
+ function titleOf(name) {
43
+ return String(name || "").replace(/\.md$/i, "");
44
+ }
45
+
46
+ /** Кеш текстов: имя отчёта → уже прочитанный текст. */
47
+ const texts = new Map();
48
+
49
+ const store = {
50
+ state: { phase: "loading", rows: [], open: "", text: "", loading: false, error: null },
51
+ listeners: new Set(),
52
+ set(next) {
53
+ store.state = next;
54
+ for (const listener of store.listeners) {
55
+ try {
56
+ listener();
57
+ } catch (error) {
58
+ console.warn("[reports-panel] listener failed", error);
59
+ }
60
+ }
61
+ },
62
+ subscribe(fn) {
63
+ store.listeners.add(fn);
64
+ return () => store.listeners.delete(fn);
65
+ }
66
+ };
67
+
68
+ let inFlight = null;
69
+ let panelContext = null;
70
+
71
+ /** Сервис сессий: ctx.sessions либо ctx.get('sessions'). */
72
+ function sessionsService(ctx) {
73
+ try {
74
+ if (ctx && ctx.sessions) return ctx.sessions;
75
+ } catch (error) { }
76
+ try {
77
+ if (ctx && typeof ctx.get === "function") {
78
+ const service = ctx.get("sessions");
79
+ if (service) return service;
80
+ }
81
+ } catch (error) { }
82
+ return null;
83
+ }
84
+
85
+ /** id сессии, которую держит mainView (текущая вкладка). */
86
+ function pickSessionId(ctx) {
87
+ const service = sessionsService(ctx);
88
+ if (!service) return null;
89
+ let snapshot = null;
90
+ try {
91
+ if (service.list && typeof service.list.getSnapshot === "function") {
92
+ snapshot = service.list.getSnapshot();
93
+ }
94
+ } catch (error) { }
95
+ if (!snapshot) {
96
+ try {
97
+ if (typeof service.getSnapshot === "function") snapshot = service.getSnapshot();
98
+ } catch (error) { }
99
+ }
100
+ if (!snapshot) return null;
101
+ const byId = snapshot.byId || (snapshot.sessions && snapshot.sessions.byId);
102
+ if (!byId) return null;
103
+ const rows = Object.values(byId);
104
+ const main = rows.find((row) => row && row.retainedBy && row.retainedBy.mainView > 0);
105
+ if (main && main.id) return main.id;
106
+ return rows.length && rows[0].id ? rows[0].id : null;
107
+ }
108
+
109
+ /** Список markdown-файлов в папке отчётов. */
110
+ function listFolder(ctx) {
111
+ const sessionId = pickSessionId(ctx);
112
+ if (!sessionId) throw new Error(NO_SESSION);
113
+ return Promise.resolve()
114
+ .then(() => ctx.remote.workspaceFiles.list(sessionId, FOLDER, undefined))
115
+ .then((raw) => {
116
+ const value = raw && raw.ok === true ? raw.value : raw;
117
+ if (!value || !Array.isArray(value.entries)) throw new Error("список не получен");
118
+ return value.entries
119
+ .filter((entry) => entry && typeof entry.name === "string" && /\.md$/i.test(entry.name))
120
+ .map((entry) => ({ name: entry.name, path: entry.path || (FOLDER + "/" + entry.name) }))
121
+ .sort((a, b) => b.name.localeCompare(a.name));
122
+ });
123
+ }
124
+
125
+ /** Клик по карточке: раскрыть отчёт под ней или свернуть открытый. */
126
+ function toggleReport(ctx, row) {
127
+ const current = store.state;
128
+ if (current.open === row.name) {
129
+ store.set({ ...current, open: "", text: "", loading: false, error: null });
130
+ return;
131
+ }
132
+ const cached = texts.get(row.name);
133
+ if (typeof cached === "string") {
134
+ store.set({ ...current, open: row.name, text: cached, loading: false, error: null });
135
+ return;
136
+ }
137
+ store.set({ ...current, open: row.name, text: "", loading: true, error: null });
138
+ loadText(ctx, row);
139
+ }
140
+
141
+ /** Чтение одного отчёта (первая страница текста) с кешированием. */
142
+ function loadText(ctx, row) {
143
+ const sessionId = pickSessionId(ctx);
144
+ if (!sessionId) {
145
+ store.set({ ...store.state, open: "", text: "", loading: false, error: NO_SESSION });
146
+ return;
147
+ }
148
+ Promise.resolve()
149
+ .then(() => ctx.remote.workspaceFiles.read(sessionId, row.path, { offset: 1, limit: 160 }, undefined))
150
+ .then((raw) => {
151
+ const value = raw && raw.ok === true ? raw.value : raw;
152
+ if (!value || typeof value.text !== "string") throw new Error("текст не получен");
153
+ texts.set(row.name, value.text);
154
+ if (store.state.open === row.name) {
155
+ store.set({ ...store.state, text: value.text, loading: false, error: null });
156
+ }
157
+ })
158
+ .catch((error) => {
159
+ const message = "Не удалось прочитать: " + String(error && error.message || error);
160
+ if (store.state.open === row.name) {
161
+ store.set({ ...store.state, text: message, loading: false });
162
+ }
163
+ });
164
+ }
165
+
166
+ /** Обновить список (одно чтение на всех). */
167
+ function refresh(ctx) {
168
+ if (inFlight) return inFlight;
169
+ inFlight = Promise.resolve()
170
+ .then(() => listFolder(ctx))
171
+ .then((rows) => {
172
+ const current = store.state;
173
+ const openStillExists = Boolean(current.open) && rows.some((row) => row.name === current.open);
174
+ store.set({
175
+ phase: "ready",
176
+ rows,
177
+ open: openStillExists ? current.open : "",
178
+ text: openStillExists ? current.text : "",
179
+ loading: openStillExists ? current.loading : false,
180
+ error: null
181
+ });
182
+ })
183
+ .catch((error) => {
184
+ console.warn("[reports-panel] list failed", error);
185
+ store.set({
186
+ phase: "error",
187
+ rows: store.state.rows,
188
+ open: store.state.open,
189
+ text: store.state.text,
190
+ loading: store.state.loading,
191
+ error: String(error && error.message || error)
192
+ });
193
+ })
194
+ .finally(() => {
195
+ inFlight = null;
196
+ });
197
+ return inFlight;
198
+ }
199
+
200
+ function useReports(ctx) {
201
+ const [, setTick] = react.useState(0);
202
+ react.useEffect(() => store.subscribe(() => setTick((value) => value + 1)), []);
203
+ react.useEffect(() => {
204
+ refresh(ctx);
205
+ const timer = setInterval(() => refresh(ctx), POLL_MS);
206
+ const onVisible = () => {
207
+ if (document.visibilityState === "visible") refresh(ctx);
208
+ };
209
+ document.addEventListener("visibilitychange", onVisible);
210
+ return () => {
211
+ clearInterval(timer);
212
+ document.removeEventListener("visibilitychange", onVisible);
213
+ };
214
+ }, []);
215
+ return store.state;
216
+ }
217
+
218
+ const CHEVRON = { width: 12, fontSize: 10, lineHeight: "20px", color: COLOR.secondary, flex: "0 0 auto", textAlign: "center" };
219
+
220
+ /** Карточка отчёта: заголовок-кнопка и, если открыта, текст под ней. */
221
+ function ReportRow({ row, active, loading, text, onToggle }) {
222
+ const [hover, setHover] = react.useState(false);
223
+ return react.createElement("div", {
224
+ "data-report": row.name,
225
+ "data-open": active ? "true" : "false",
226
+ style: {
227
+ marginBottom: 8,
228
+ border: "1px solid " + (hover || active ? COLOR.accent : COLOR.border),
229
+ borderRadius: 10,
230
+ background: hover && !active ? COLOR.hover : "transparent",
231
+ overflow: "hidden"
232
+ }
233
+ }, [
234
+ react.createElement("div", {
235
+ key: "head",
236
+ role: "button",
237
+ tabIndex: 0,
238
+ "aria-expanded": active,
239
+ onClick: () => onToggle(row),
240
+ onKeyDown: (event) => {
241
+ if (event.key === "Enter" || event.key === " ") {
242
+ event.preventDefault();
243
+ onToggle(row);
244
+ }
245
+ },
246
+ onMouseEnter: () => setHover(true),
247
+ onMouseLeave: () => setHover(false),
248
+ style: {
249
+ display: "flex",
250
+ alignItems: "center",
251
+ gap: 6,
252
+ padding: "10px 12px",
253
+ textAlign: "left",
254
+ cursor: "pointer",
255
+ outline: "none",
256
+ color: COLOR.primary,
257
+ font: "inherit"
258
+ }
259
+ }, [
260
+ react.createElement("span", { key: "chevron", "aria-hidden": "true", style: CHEVRON }, active ? "▾" : "▸"),
261
+ react.createElement("span", {
262
+ key: "title",
263
+ style: { fontSize: 14, lineHeight: "20px", fontWeight: 550 }
264
+ }, titleOf(row.name))
265
+ ]),
266
+ active ? react.createElement("div", {
267
+ key: "body",
268
+ "data-report-body": row.name,
269
+ style: { borderTop: "1px solid " + COLOR.border }
270
+ }, loading ? react.createElement("div", {
271
+ style: { padding: "12px 14px", color: COLOR.secondary, fontSize: 12 }
272
+ }, LOADING) : react.createElement("pre", {
273
+ style: {
274
+ margin: 0,
275
+ padding: "12px 16px 14px",
276
+ color: COLOR.primary,
277
+ font: "12px/1.55 ui-monospace, Consolas, monospace",
278
+ whiteSpace: "pre-wrap",
279
+ wordBreak: "break-word",
280
+ maxHeight: 480,
281
+ overflowY: "auto"
282
+ }
283
+ }, text)) : null
284
+ ]);
285
+ }
286
+
287
+ function ReportsPanel(props) {
288
+ const ctx = props && props.ctx ? props.ctx : panelContext;
289
+ const state = useReports(ctx);
290
+ return react.createElement("div", {
291
+ style: {
292
+ height: "100%",
293
+ overflowY: "auto",
294
+ boxSizing: "border-box",
295
+ padding: "calc(var(--dsh-frame-top-clearance, 0px) + 28px) 24px 32px"
296
+ }
297
+ }, react.createElement("div", {
298
+ style: { maxWidth: 820, margin: "0 auto" }
299
+ }, [
300
+ react.createElement("div", {
301
+ key: "header",
302
+ style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 16, marginBottom: 18 }
303
+ }, [
304
+ react.createElement("div", { key: "text" }, [
305
+ react.createElement("h2", {
306
+ key: "title",
307
+ style: { margin: 0, fontSize: 20, lineHeight: "26px", fontWeight: 600, color: COLOR.primary }
308
+ }, LABEL),
309
+ react.createElement("div", {
310
+ key: "sub",
311
+ style: { marginTop: 4, fontSize: 13, lineHeight: "18px", color: COLOR.secondary }
312
+ }, SUBTITLE)
313
+ ]),
314
+ react.createElement("button", {
315
+ key: "refresh",
316
+ type: "button",
317
+ onClick: () => refresh(ctx),
318
+ style: {
319
+ padding: "6px 12px", borderRadius: 8, border: "1px solid " + COLOR.border,
320
+ background: "transparent", color: COLOR.secondary, font: "inherit", fontSize: 12, cursor: "pointer"
321
+ }
322
+ }, RETRY)
323
+ ]),
324
+ state.phase === "loading" && state.rows.length === 0 ? react.createElement("div", {
325
+ key: "loading", style: { color: COLOR.secondary, fontSize: 13 }
326
+ }, LOADING) : null,
327
+ state.phase === "error" ? react.createElement("div", {
328
+ key: "error",
329
+ style: {
330
+ marginBottom: 12, padding: "10px 12px", borderRadius: 10,
331
+ border: "1px solid " + COLOR.border, color: COLOR.secondary, fontSize: 12
332
+ }
333
+ }, FAILED + " " + (state.error || "")) : null,
334
+ state.rows.length === 0 && state.phase !== "loading" ? react.createElement("div", {
335
+ key: "empty",
336
+ style: { padding: "24px 12px", color: COLOR.secondary, fontSize: 13, textAlign: "center" }
337
+ }, [react.createElement("div", { key: "a" }, EMPTY), react.createElement("div", {
338
+ key: "b", style: { marginTop: 4, color: COLOR.tertiary, fontSize: 12 }
339
+ }, EMPTY_HINT)]) : null,
340
+ react.createElement("div", { key: "rows" }, state.rows.map((row) => react.createElement(ReportRow, {
341
+ key: row.name,
342
+ row,
343
+ active: state.open === row.name,
344
+ loading: state.loading,
345
+ text: state.open === row.name ? state.text : "",
346
+ onToggle: (item) => toggleReport(ctx, item)
347
+ })))
348
+ ]));
349
+ }
350
+
351
+ function ReportsIcon({ size }) {
352
+ const px = typeof size === "number" && size > 0 ? size : 16;
353
+ return react.createElement("svg", {
354
+ width: px, height: px, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true"
355
+ }, [
356
+ react.createElement("path", {
357
+ key: "a", d: "M4 1.8h5.2L13 5.6v8.6H4z", stroke: "currentColor", strokeWidth: 1.3, strokeLinejoin: "round"
358
+ }),
359
+ react.createElement("path", {
360
+ key: "b", d: "M9 1.9v4h3.9", stroke: "currentColor", strokeWidth: 1.3, strokeLinejoin: "round"
361
+ }),
362
+ react.createElement("path", {
363
+ key: "c", d: "M6 8.4h5M6 10.8h3.4", stroke: "currentColor", strokeWidth: 1.3, strokeLinecap: "round"
364
+ })
365
+ ]);
366
+ }
367
+
368
+ function place(ctx, attempt = 0) {
369
+ try {
370
+ ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
371
+ name: "sidebar.panellist",
372
+ id: PANEL_ID,
373
+ order: 30,
374
+ label: () => LABEL
375
+ }, ReportsIcon));
376
+ ctx.slots.inject("main", () => ctx.slots.register({
377
+ name: "main",
378
+ key: PANEL_ID
379
+ }, ReportsPanel));
380
+ return true;
381
+ } catch (error) {
382
+ if (attempt < 20) {
383
+ setTimeout(() => place(ctx, attempt + 1), 1000);
384
+ } else {
385
+ console.warn("[reports-panel] could not place the panel", error);
386
+ }
387
+ return false;
388
+ }
389
+ }
390
+
391
+ const inject = ["slots", "remote", "remote.workspaceFiles"];
392
+
393
+ function apply(ctx) {
394
+ try {
395
+ panelContext = ctx;
396
+ refresh(ctx);
397
+ place(ctx);
398
+ } catch (error) {
399
+ console.warn("[reports-panel] apply failed", error);
400
+ }
401
+ }
402
+
403
+ exports.apply = apply;
404
+ exports.inject = inject;
405
+ return module.exports;
406
+ }
407
+ });
package/lib/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /** Host half of dsh-reports-panel.
2
+ *
3
+ * Browser-only плагин: клиентская часть читает папку «Отчёты» через Remote
4
+ * `workspaceFiles` и рисует ленту в левой панели. Хосту нужна только строка
5
+ * в Loader — сервисов и конфигурации нет.
6
+ */
7
+
8
+ /** Loader display name. */
9
+ export const name = 'reports-panel'
10
+
11
+ /** No host service or configuration is required. */
12
+ export function apply() {}
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "dsh-reports-panel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Reports feed in the DSH left sidebar: files from the «Отчёты» folder of the session workspace, opened with one click",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./cordis.patch.yml": "./cordis.patch.yml",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "dsh": {
14
+ "bundle": {
15
+ "patch": "./cordis.patch.yml"
16
+ },
17
+ "client": {
18
+ "platform": "web",
19
+ "inject": [
20
+ "@deepseek-ai/dsh-client-ui-sidebar"
21
+ ]
22
+ }
23
+ },
24
+ "files": [
25
+ "lib",
26
+ "cordis.patch.yml",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "keywords": [
34
+ "dsh-plugin",
35
+ "dsh",
36
+ "deepseek-harness",
37
+ "deepseek",
38
+ "harness",
39
+ "reports",
40
+ "documents",
41
+ "sidebar",
42
+ "workspace",
43
+ "feed"
44
+ ],
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/Bayan1377/dsh-reports-panel.git"
48
+ },
49
+ "homepage": "https://github.com/Bayan1377/dsh-reports-panel#readme",
50
+ "bugs": {
51
+ "url": "https://github.com/Bayan1377/dsh-reports-panel/issues"
52
+ },
53
+ "author": "bayan1377",
54
+ "license": "MIT"
55
+ }