dsh-side-memo 1.3.0 → 1.3.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/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  A lightweight, toggleable **right-side notes panel** for [DeepSeek Harness](https://deepseek.com/harness). Jot memos while you chat — text, inline images, and labels — copy them with one click, star favorites to keep them pinned at the top, and move everything in or out as an Excel workbook. When a conversation is worth keeping, one click turns it into a memo.
10
10
 
11
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
- ![Version](https://img.shields.io/badge/version-1.3.0-4D6BFE)
12
+ ![Version](https://img.shields.io/badge/version-1.3.1-4D6BFE)
13
13
  [![npm](https://img.shields.io/npm/v/dsh-side-memo?label=npm&color=4D6BFE)](https://www.npmjs.com/package/dsh-side-memo)
14
14
  [![CI](https://github.com/r48n34/dsh-side-memo/actions/workflows/ci.yml/badge.svg)](https://github.com/r48n34/dsh-side-memo/actions/workflows/ci.yml)
15
15
  ![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-0.2.x-4D6BFE)
@@ -53,6 +53,7 @@ To remove it, use **Settings → Plugins** in DSH (or the equivalent `dsh plugin
53
53
  - **Copy and transfer:** Copy notes in one click; import or export notes as Excel workbooks.
54
54
  - **Right-click actions:** Right-click a memo — or press `Shift`+`F10` — for Open, Copy, Duplicate, favorite and Delete, without leaving the list. Delete asks for a second click, like the editor.
55
55
  - **Save locally:** Notes and images stay in your browser and are available across Sessions.
56
+ - **Stays fast:** image data is read only when the notes panel or the pre-Session drawer first opens, never at plugin startup, and off-screen memo rows skip layout and paint when the chat column resizes. A 300-memo library stays responsive even with tens of megabytes of images.
56
57
 
57
58
  ## Usage
58
59
 
@@ -91,6 +92,7 @@ Imports add new notes without overwriting existing ones. Images remain local and
91
92
  - Chat-to-memo captures plain text only: the visible user prompts and the assistant's text replies (or, when you pick the prompts-only entry, only what you sent). Reasoning, tool output and images are not included, the 8,000-character body limit drops the oldest turns first, and the memo title is taken from the first prompt (with a generic fallback title when there is none). The button in a message's action row saves only that one prompt's text; it appears for user messages whose visible text is non-empty.
92
93
  - Text is plain text (no Markdown or rich-text formatting); images are the only rich content. Images are downscaled before storage and are not exported to Excel — the `Images` column only holds a count.
93
94
  - Image support needs IndexedDB and Canvas. If the browser blocks IndexedDB, the text memos keep working and image insertion is disabled.
95
+ - Images are loaded the first time the notes panel or the pre-Session drawer opens in a page load. The text list appears immediately, and a very large image library can take a moment before images — and the editor's image controls — are ready.
94
96
  - Excel import reads the first worksheet only; formulas import as their cached values, and rich-text cell formatting is ignored.
95
97
  - Limits: 300 memos, 120-character titles, 8,000-character bodies, 12 labels per memo (24 characters each), 20 images per memo (source images up to 24 MB, downscaled to at most 1,400 px at quality 0.82 before storage; display sizes 24–1,400 px), 8 MB `.xlsx` imports (up to 32 MB expanded across at most 64 entries), and a 60 MB total image budget for the browser.
96
98
  - Labels are flat tags — no nesting, per-label colors, or separate tag manager — and each label is stored per memo, so renaming one means editing every memo that carries it.
package/README.zh-CN.md CHANGED
@@ -9,7 +9,7 @@
9
9
  一个轻量、可开关的 **右侧便签面板**,为 [DeepSeek Harness](https://deepseek.com/harness) 打造。聊天时随手记下内容——文字、内嵌图片和标签——一键复制,收藏常用便签让它始终置顶,还能把所有便签整体导入或导出为 Excel 工作簿。对话值得保留时,一次点击就能把它存成便签。
10
10
 
11
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
- ![Version](https://img.shields.io/badge/version-1.3.0-4D6BFE)
12
+ ![Version](https://img.shields.io/badge/version-1.3.1-4D6BFE)
13
13
  [![npm](https://img.shields.io/npm/v/dsh-side-memo?label=npm&color=4D6BFE)](https://www.npmjs.com/package/dsh-side-memo)
14
14
  [![CI](https://github.com/r48n34/dsh-side-memo/actions/workflows/ci.yml/badge.svg)](https://github.com/r48n34/dsh-side-memo/actions/workflows/ci.yml)
15
15
  ![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-0.2.x-4D6BFE)
@@ -55,6 +55,7 @@ dsh plugin --profile desktop add dsh-side-memo
55
55
  - **单条提问存为便签** —— 把鼠标移到你发送的消息上,点击操作行(**复制** 按钮旁)的便签按钮,即可只把这条提问存成新便签,无需打开标题栏菜单。
56
56
  - **Excel 导入 / 导出** —— 导出全部便签到 `.xlsx`,或追加导入便签,不覆盖现有内容。图片本身不会导出,`Images` 列仅记录数量。
57
57
  - **本地保存** —— 便签和图片引用存于浏览器的 `localStorage`,图片数据存于 IndexedDB;刷新后保留,并在会话间共享。
58
+ - **保持轻快** —— 图片数据只在首次打开便签面板或会话前的浮动抽屉时读取,不会在插件启动时加载;聊天区缩放时,屏幕外的便签行会跳过布局与绘制。即使有 300 条便签、几十 MB 图片,面板也能保持流畅。
58
59
 
59
60
  ## 使用方法
60
61
 
@@ -92,6 +93,7 @@ dsh plugin --profile desktop add dsh-side-memo
92
93
  - 对话存为便签只收录纯文本:可见的用户提问和助手正文(选择“只保存我的提问”时只保留你发送的内容)。思考过程、工具输出和图片不会包含;8,000 字符的正文上限会先从最早的轮次开始丢弃;便签标题取自第一个提问,没有提问时使用通用的默认标题。消息操作行里的按钮只保存该条提问的文字,且只对可见文字非空的用户消息出现。
93
94
  - 文本是纯文本(不支持 Markdown 或富文本格式);图片是唯一的富内容。图片在保存前会被缩小,也不会导出到 Excel——`Images` 列只记录数量。所选显示尺寸随便签保存,刷新后仍会保留;它只改变图片的显示方式,不会重新编码或替换已保存的图片。
94
95
  - 图片功能依赖 IndexedDB 和 Canvas。如果浏览器禁用了 IndexedDB,文字便签仍可正常使用,但插入图片会被禁用。
96
+ - 图片数据会在本次页面首次打开便签面板或会话前的浮动抽屉时加载。文字列表会立即出现;如果图片很多,图片以及编辑器中的图片操作会稍等片刻才就绪。
95
97
  - Excel 导入只读取第一个工作表;公式按其缓存值导入,富文本单元格格式会被忽略。
96
98
  - 上限:300 条便签、标题 120 字符、正文 8,000 字符、每条便签 12 个标签(每个 24 字符)、每条便签 20 张图片(单张源图片不超过 24 MB,保存前会缩小到最长边 1,400 像素、质量 0.82,显示尺寸限制在 24–1,400 像素)、`.xlsx` 文件 8 MB(解压后总量不超过 32 MB,压缩包内最多 64 个条目),以及浏览器图片总量 60 MB。
97
99
  - 标签是扁平标签——不支持层级、按标签着色或独立的标签管理器——而且每个标签都按便签单独存储,所以重命名一个标签意味着要编辑所有带有它的便签。
package/README.zh-TW.md CHANGED
@@ -9,7 +9,7 @@
9
9
  一個輕量、可開關的 **右側便箋面板**,為 [DeepSeek Harness](https://deepseek.com/harness) 打造。聊天時隨手記下內容——文字、內嵌圖片與標籤——一鍵複製,收藏常用便箋讓它始終置頂,還能把所有便箋整批匯入或匯出為 Excel 活頁簿。對話值得保留時,點一下就能把它存成便箋。
10
10
 
11
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
- ![Version](https://img.shields.io/badge/version-1.3.0-4D6BFE)
12
+ ![Version](https://img.shields.io/badge/version-1.3.1-4D6BFE)
13
13
  [![npm](https://img.shields.io/npm/v/dsh-side-memo?label=npm&color=4D6BFE)](https://www.npmjs.com/package/dsh-side-memo)
14
14
  [![CI](https://github.com/r48n34/dsh-side-memo/actions/workflows/ci.yml/badge.svg)](https://github.com/r48n34/dsh-side-memo/actions/workflows/ci.yml)
15
15
  ![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-0.2.x-4D6BFE)
@@ -54,6 +54,7 @@ dsh plugin --profile desktop add dsh-side-memo
54
54
  - **標籤與搜尋** —— 搜尋標題、內文和標籤;每則最多 12 個標籤、每個最多 24 字元,重複標籤不分大小寫。可篩選同時符合多個標籤的便箋,或依標籤分組。
55
55
  - **Excel 匯入 / 匯出** —— 匯出所有便箋為 `.xlsx`,或附加匯入新便箋,不覆寫現有內容。圖片不會匯出,`Images` 欄僅記錄數量。
56
56
  - **本機保存** —— 便箋及圖片參照存於瀏覽器的 `localStorage`,圖片資料存於 IndexedDB;重新載入後保留,並在工作階段間共用。
57
+ - **保持輕快** —— 圖片資料只在首次開啟便箋面板或工作階段前的浮動抽屜時讀取,不會在外掛啟動時載入;聊天區縮放時,畫面外的便箋列會跳過版面配置與繪製。即使有 300 則便箋、數十 MB 圖片,面板也能保持順暢。
57
58
 
58
59
  ## 使用方式
59
60
 
@@ -93,6 +94,7 @@ dsh plugin --profile desktop add dsh-side-memo
93
94
  - 對話存為便箋只收錄純文字:可見的使用者提問與助理正文(選擇「只儲存我的提問」時只保留你傳送的內容)。思考過程、工具輸出與圖片不會包含;8,000 字元的正文上限會先從最早的輪次開始捨棄;便箋標題取自第一個提問,沒有提問時使用通用的預設標題。訊息操作列中的按鈕只會儲存該則提問的文字,且只對可見文字非空的使用者訊息出現。
94
95
  - 文字為純文字(不支援 Markdown 或富文字格式);圖片是唯一的豐富內容。圖片在儲存前會先縮小,也不會匯出到 Excel——`Images` 欄只記錄數量。所選顯示尺寸會隨便箋儲存,重新載入後仍會保留;它只改變圖片的顯示方式,不會重新編碼或取代已儲存的圖片。
95
96
  - 圖片功能需要 IndexedDB 與 Canvas。若瀏覽器封鎖 IndexedDB,文字便箋仍可正常運作,但插入圖片會被停用。
97
+ - 圖片資料會在本次頁面首次開啟便箋面板或工作階段前的浮動抽屜時載入。文字清單會立即出現;如果圖片很多,圖片以及編輯器中的圖片操作會稍待片刻才就緒。
96
98
  - Excel 匯入只讀取第一個工作表;公式會以其快取值匯入,儲存格中的富文字格式會被忽略。
97
99
  - 上限:300 則便箋、標題 120 個字元、內文 8,000 個字元、每則便箋 12 個標籤(每個 24 個字元)、每則便箋 20 張圖片(單張來源圖片不超過 24 MB,儲存前會縮小到最長邊 1,400 像素、品質 0.82,顯示尺寸限制在 24–1,400 像素)、`.xlsx` 檔 8 MB(解壓後總量不超過 32 MB,壓縮檔內最多 64 個項目),以及瀏覽器圖片總量 60 MB。
98
100
  - 標籤是扁平標籤——不支援階層、個別標籤顏色或獨立的標籤管理員——而且每個標籤都以個別便箋為單位儲存,所以重新命名一個標籤必須編輯所有帶有它的便箋。
package/client.js CHANGED
@@ -350,6 +350,10 @@ window.__ModuleLoader__.load({
350
350
  '.dsm-row[draggable="true"]{cursor:grab}',
351
351
  '.dsm-row[draggable="true"]:active{cursor:grabbing}',
352
352
  '.dsm-row[data-dragging="true"]{opacity:.55;background:var(--dsw-alias-bg-layer-2)}',
353
+ // Rows far outside the panel viewport skip layout and paint; the intrinsic
354
+ // size keeps the scrollbar stable until a row has been rendered once. This
355
+ // is what keeps a 300-memo library cheap when the chat column re-layouts.
356
+ '.dsm-row{content-visibility:auto;contain-intrinsic-size:auto 78px}',
353
357
  '.dsm-menu{position:absolute;z-index:20;min-width:172px;max-width:240px;padding:4px;box-sizing:border-box;',
354
358
  'display:flex;flex-direction:column;gap:1px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;',
355
359
  'background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);',
@@ -1451,20 +1455,35 @@ window.__ModuleLoader__.load({
1451
1455
  });
1452
1456
  }
1453
1457
  function addImage(record) {
1454
- if (!imagesAvailable)
1455
- return Promise.resolve({ ok: false, reason: 'unavailable' });
1456
- var clean = normalizeImageRecord(record);
1457
- if (clean === undefined)
1458
- return Promise.resolve({ ok: false, reason: 'unsupported' });
1459
- var image = clean;
1460
- if (imageBytes() + image.size <= MAX_TOTAL_IMAGE_BYTES)
1461
- return persistImage(image);
1462
- return collectOrphans().then(function () {
1463
- if (imageBytes() + image.size > MAX_TOTAL_IMAGE_BYTES)
1464
- return { ok: false, reason: 'budget' };
1465
- return persistImage(image);
1458
+ // The image cache is lazy, but an insert still needs the open database
1459
+ // handle and the in-memory budget, so force hydration before writing.
1460
+ return ensureImages().then(function () {
1461
+ if (!imagesAvailable)
1462
+ return { ok: false, reason: 'unavailable' };
1463
+ var clean = normalizeImageRecord(record);
1464
+ if (clean === undefined)
1465
+ return { ok: false, reason: 'unsupported' };
1466
+ var image = clean;
1467
+ if (imageBytes() + image.size <= MAX_TOTAL_IMAGE_BYTES)
1468
+ return persistImage(image);
1469
+ return collectOrphans().then(function () {
1470
+ if (imageBytes() + image.size > MAX_TOTAL_IMAGE_BYTES)
1471
+ return { ok: false, reason: 'budget' };
1472
+ return persistImage(image);
1473
+ });
1466
1474
  });
1467
1475
  }
1476
+ var imagesRequest = null;
1477
+ /**
1478
+ * Open the image database on first use instead of at plugin boot. Records
1479
+ * can be tens of megabytes, and most Sessions never open the notes surface,
1480
+ * so this work belongs to the panel mount rather than to `apply()`.
1481
+ */
1482
+ function ensureImages() {
1483
+ if (imagesRequest === null)
1484
+ imagesRequest = loadImages();
1485
+ return imagesRequest;
1486
+ }
1468
1487
  function loadImages() {
1469
1488
  return openImageDb().then(function (db) {
1470
1489
  imageDb = db;
@@ -1524,7 +1543,7 @@ window.__ModuleLoader__.load({
1524
1543
  notes = [];
1525
1544
  }
1526
1545
  emit();
1527
- loadImages();
1546
+ // Images hydrate on first panel/drawer mount (see ensureImages), not here.
1528
1547
  return {
1529
1548
  subscribe: function (listener) {
1530
1549
  listeners.add(listener);
@@ -1729,6 +1748,7 @@ window.__ModuleLoader__.load({
1729
1748
  write();
1730
1749
  },
1731
1750
  addImage: addImage,
1751
+ ensureImages: ensureImages,
1732
1752
  collectOrphans: collectOrphans,
1733
1753
  limits: { maxNotes: MAX_NOTES, maxTitle: MAX_TITLE, maxBody: MAX_BODY, maxImages: MAX_IMAGES }
1734
1754
  };
@@ -3293,6 +3313,11 @@ window.__ModuleLoader__.load({
3293
3313
  var menuReturnFocusRef = React.useRef(null);
3294
3314
  var openRequest = props.openRequest === undefined ? nullOpenRequest : props.openRequest;
3295
3315
  var requestedNoteId = useSyncExternalStore(openRequest.subscribe, openRequest.getSnapshot, openRequest.getSnapshot);
3316
+ // Inline images live in IndexedDB and can be large. Hydrate them when this
3317
+ // surface mounts (panel tab or pre-Session drawer), not at plugin boot.
3318
+ React.useEffect(function () {
3319
+ notes.ensureImages();
3320
+ }, [notes]);
3296
3321
  // A memo created from the chat header asks to be opened here. The panel can
3297
3322
  // mount after the request was filed, so the store is read on mount as well;
3298
3323
  // the request is consumed exactly once.
@@ -5116,17 +5141,65 @@ window.__ModuleLoader__.load({
5116
5141
  return null;
5117
5142
  return h(NotesChatMemoButton, props);
5118
5143
  }
5144
+ function chatVisibleNodes(chat) {
5145
+ var list = [];
5146
+ if (chat === null || chat === undefined)
5147
+ return list;
5148
+ var order = Array.isArray(chat.order) ? chat.order : [];
5149
+ var nodes = chat.nodes;
5150
+ if (nodes === null || nodes === undefined || typeof nodes.get !== 'function')
5151
+ return list;
5152
+ for (var index = 0; index < order.length; index += 1) {
5153
+ var node = nodes.get(order[index]);
5154
+ if (node !== null && node !== undefined)
5155
+ list.push(node);
5156
+ }
5157
+ return list;
5158
+ }
5159
+ /**
5160
+ * Menu body for "Save chat as memo". The chat subscription lives here so it
5161
+ * exists only while the menu is open; the closed header button subscribes to
5162
+ * nothing and therefore never re-renders when the conversation changes.
5163
+ */
5164
+ function ChatMemoMenu(props) {
5165
+ var t = props.t;
5166
+ var chat = props.useChat(function (snapshot) {
5167
+ return snapshot;
5168
+ });
5169
+ var labels = {
5170
+ you: t('chat.you'),
5171
+ assistant: t('chat.assistant'),
5172
+ fallbackTitle: t('chat.fallbackTitle')
5173
+ };
5174
+ function pick(mode) {
5175
+ props.onPick(mode, chatMemoDraft(chatVisibleNodes(chat), labels, mode));
5176
+ }
5177
+ var menuStyle = props.anchor === null ? undefined : { left: props.anchor.left + 'px', top: props.anchor.top + 'px' };
5178
+ return h('div', {
5179
+ className: 'dsm-chat-memo-menu',
5180
+ role: 'menu',
5181
+ 'aria-label': t('chat.menu.label'),
5182
+ style: menuStyle
5183
+ }, h('button', {
5184
+ type: 'button',
5185
+ role: 'menuitem',
5186
+ className: 'dsm-chat-memo-item',
5187
+ onClick: function () {
5188
+ pick('chat');
5189
+ }
5190
+ }, h('span', null, t('chat.menu.chat')), h('span', { className: 'dsm-chat-memo-note' }, t('chat.menu.chatNote'))), h('button', {
5191
+ type: 'button',
5192
+ role: 'menuitem',
5193
+ className: 'dsm-chat-memo-item',
5194
+ onClick: function () {
5195
+ pick('prompts');
5196
+ }
5197
+ }, h('span', null, t('chat.menu.prompts')), h('span', { className: 'dsm-chat-memo-note' }, t('chat.menu.promptsNote'))));
5198
+ }
5119
5199
  function NotesChatMemoButton(props) {
5120
5200
  var t = translateFor(props);
5121
5201
  var notes = props.notes;
5122
5202
  var openRequest = props.openRequest;
5123
- var useChat = props.useChat;
5124
- // Subscribe to the whole (identity-stable) snapshot; the transcript is
5125
- // reduced only when the user picks a menu item, so this never allocates per
5126
- // render and stays a safe useSyncExternalStore consumer.
5127
- var chat = useChat(function (snapshot) {
5128
- return snapshot;
5129
- });
5130
5203
  var feedbackState = React.useState('');
5131
5204
  var feedback = feedbackState[0];
5132
5205
  var setFeedback = feedbackState[1];
@@ -5178,21 +5251,6 @@ window.__ModuleLoader__.load({
5178
5251
  window.removeEventListener('scroll', close, true);
5179
5252
  };
5180
5253
  }, [menuOpen]);
5181
- function visibleNodes() {
5182
- var list = [];
5183
- if (chat === null || chat === undefined)
5184
- return list;
5185
- var order = Array.isArray(chat.order) ? chat.order : [];
5186
- var nodes = chat.nodes;
5187
- if (nodes === null || nodes === undefined || typeof nodes.get !== 'function')
5188
- return list;
5189
- for (var index = 0; index < order.length; index += 1) {
5190
- var node = nodes.get(order[index]);
5191
- if (node !== null && node !== undefined)
5192
- list.push(node);
5193
- }
5194
- return list;
5195
- }
5196
5254
  function openMenu() {
5197
5255
  var button = buttonRef.current;
5198
5256
  if (button !== null && typeof button.getBoundingClientRect === 'function') {
@@ -5204,13 +5262,8 @@ window.__ModuleLoader__.load({
5204
5262
  }
5205
5263
  setMenuOpen(true);
5206
5264
  }
5207
- function save(mode) {
5265
+ function take(mode, draft) {
5208
5266
  setMenuOpen(false);
5209
- var draft = chatMemoDraft(visibleNodes(), {
5210
- you: t('chat.you'),
5211
- assistant: t('chat.assistant'),
5212
- fallbackTitle: t('chat.fallbackTitle')
5213
- }, mode);
5214
5267
  if (draft === null) {
5215
5268
  setFeedback(mode === 'prompts' ? 'empty-prompts' : 'empty');
5216
5269
  return;
@@ -5238,7 +5291,6 @@ window.__ModuleLoader__.load({
5238
5291
  : feedback === 'empty'
5239
5292
  ? t('chat.empty')
5240
5293
  : t('chat.create');
5241
- var menuStyle = anchor === null ? undefined : { left: anchor.left + 'px', top: anchor.top + 'px' };
5242
5294
  return h('div', { className: 'dsm-chat-memo', ref: rootRef }, h('button', {
5243
5295
  type: 'button',
5244
5296
  className: 'dsm-toggle',
@@ -5257,26 +5309,12 @@ window.__ModuleLoader__.load({
5257
5309
  openMenu();
5258
5310
  }
5259
5311
  }, saved ? h(IconCheck, { size: 16 }) : h(IconChatMemo, { size: 16 })), menuOpen
5260
- ? h('div', {
5261
- className: 'dsm-chat-memo-menu',
5262
- role: 'menu',
5263
- 'aria-label': t('chat.menu.label'),
5264
- style: menuStyle
5265
- }, h('button', {
5266
- type: 'button',
5267
- role: 'menuitem',
5268
- className: 'dsm-chat-memo-item',
5269
- onClick: function () {
5270
- save('chat');
5271
- }
5272
- }, h('span', null, t('chat.menu.chat')), h('span', { className: 'dsm-chat-memo-note' }, t('chat.menu.chatNote'))), h('button', {
5273
- type: 'button',
5274
- role: 'menuitem',
5275
- className: 'dsm-chat-memo-item',
5276
- onClick: function () {
5277
- save('prompts');
5278
- }
5279
- }, h('span', null, t('chat.menu.prompts')), h('span', { className: 'dsm-chat-memo-note' }, t('chat.menu.promptsNote'))))
5312
+ ? h(ChatMemoMenu, {
5313
+ t: t,
5314
+ useChat: props.useChat,
5315
+ anchor: anchor,
5316
+ onPick: take
5317
+ })
5280
5318
  : null, h('span', { className: 'dsm-sr-only', role: 'status', 'aria-live': 'polite' }, saved ? t('chat.saved') : ''));
5281
5319
  }
5282
5320
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-side-memo",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Toggleable Side Memo notes panel for DeepSeek Harness: labels/tags, resizable inline images, favorites pinned to the top, quick copy, drag a memo into the chat box to paste it, save a conversation or a single prompt as a memo, right-click memo actions, and Excel (.xlsx) import/export.",
5
5
  "keywords": [
6
6
  "dsh",