dsh-cmdk 0.3.1 → 0.3.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/CHANGELOG.md +23 -0
- package/README.md +14 -10
- package/client.js +82 -29
- package/index.js +7 -160
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.2 — 2026-11-08
|
|
4
|
+
|
|
5
|
+
### 修复
|
|
6
|
+
|
|
7
|
+
- 无标题会话不再显示 `undefined`,改为「无标题 / Untitled」。
|
|
8
|
+
- 英文界面的失败提示改用 `": "`,不再混入中文全角冒号。
|
|
9
|
+
- 导出改为调用宿主原生的 `/api/session.markdown?sessionId=`,不再依赖系统 `zstd` 命令,也不再自己解析会话日志。
|
|
10
|
+
|
|
11
|
+
### 改进
|
|
12
|
+
|
|
13
|
+
- 导出的 Markdown 现在包含工具调用、图片 / 文件附件和折叠的中间消息,与 README 的描述一致。
|
|
14
|
+
- 搜索分层:宿主一次下发完整会话目录,标题与拼音在浏览器内即时匹配;正文命中由宿主扫描后合并进来,输入不再等待网络。
|
|
15
|
+
- 正文搜索不再跳过无标题会话。
|
|
16
|
+
- 「最近使用」改为 frecency(使用次数 × 7 天半衰期的时间衰减),与 README 描述一致。
|
|
17
|
+
- 快捷键提示按平台显示 `⌘K` 或 `Ctrl+K`。
|
|
18
|
+
|
|
19
|
+
### 移除
|
|
20
|
+
|
|
21
|
+
- `GET /cmdk/export` 路由及其 zstd 解压 / Markdown 渲染代码(宿主减少约 150 行)。
|
|
22
|
+
|
|
23
|
+
更早版本见 git 历史。
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-cmdk — ⌘K 命令面板
|
|
2
2
|
|
|
3
|
-
当前版本 **0.3.
|
|
3
|
+
当前版本 **0.3.2**:会话 / 工作区 / 插件 / 动作 / 外观一个入口,插件可扩展。
|
|
4
4
|
|
|
5
5
|
一个快捷键跳到任何东西:会话、工作区、常用动作,还能对选中的会话直接做事。
|
|
6
6
|
装完即用,无配置,零依赖。
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
| 输入 | 结果 |
|
|
15
15
|
| --- | --- |
|
|
16
|
-
| `⌘K` / `Ctrl+K` | 打开 /
|
|
17
|
-
| 输入会话标题 |
|
|
16
|
+
| `⌘K` / `Ctrl+K` | 打开 / 关闭面板(侧栏底部也有同款按钮) |
|
|
17
|
+
| 输入会话标题 | 浏览器内即时模糊匹配全部会话(前缀 > 子串 > 拼音首字母 > 子序列),回车直达 |
|
|
18
18
|
| 输入中文首字母 | `cj` → 「插件…」,`cjgc` → 「插件广场」 |
|
|
19
|
-
| 输入消息内容 |
|
|
19
|
+
| 输入消息内容 | 标题先出结果;宿主扫描最近 12 个会话的日志文本,命中后带上下文片段补进来 |
|
|
20
20
|
| 输入工作区名 | 直接切换工作区 |
|
|
21
21
|
| `深色` / `浅色` / `跟随系统` | 换主题 |
|
|
22
22
|
| `中文` / `English` | 换界面语言 |
|
|
@@ -63,15 +63,19 @@ window.__DSH_CMDK__?.unregister('my-plugin.open-docs');
|
|
|
63
63
|
## 记忆
|
|
64
64
|
|
|
65
65
|
用过的动作会记在 `localStorage["dsh-cmdk/recent"]`(次数 + 时间,最多 60 条),
|
|
66
|
-
|
|
66
|
+
下次空查询时按 frecency(次数 × 7 天半衰期的时间衰减)排在最上面。纯本地,不上传。
|
|
67
67
|
|
|
68
68
|
## 设计取舍
|
|
69
69
|
|
|
70
70
|
- **跳转与动作走公开 Client 服务**:切会话、Fork、归档、重命名、换主题、换语言都用
|
|
71
71
|
`uiWorkspace` / `sessions` / `theme` / `locale`,不碰内部模块。
|
|
72
|
-
-
|
|
72
|
+
- **列表数据走宿主,过滤走本地**:浏览器侧拿不到"全部历史会话"和工作区清单,所以宿主读
|
|
73
73
|
`sessionQuery.listSessions()` + `readTitleSnapshots()`(标题)与
|
|
74
|
-
`~/.dsh/storages/workspace.json
|
|
74
|
+
`~/.dsh/storages/workspace.json`(工作区、已归档会话);宿主一次下发完整目录,
|
|
75
|
+
客户端在本地做模糊 + 拼音过滤,输入不等网络,正文命中回来后再合并。
|
|
76
|
+
- **导出复用宿主原生实现**:走同源的 `/api/session.markdown?sessionId=`,直接拿到
|
|
77
|
+
DSH 自带 `formatSessionMarkdown` 的结果(工具调用、图片附件、折叠段齐全),
|
|
78
|
+
不再自己解压 `zstd`、也不再依赖系统里有没有 `zstd` 命令。
|
|
75
79
|
- **内容搜索不依赖 SQLite 索引**:用 `filterEvents({kind:'text'})` 扫正文——实测有些
|
|
76
80
|
机器上原生「搜索会话」本身就是空的(全文索引没建起来),照抄索引会一起空。
|
|
77
81
|
代价是只扫最近 12 个会话,且只跳到会话顶部而不是那一条消息(API 不返回消息序号)。
|
|
@@ -88,9 +92,8 @@ window.__DSH_CMDK__?.unregister('my-plugin.open-docs');
|
|
|
88
92
|
| 方法 | 路径 | 作用 |
|
|
89
93
|
| --- | --- | --- |
|
|
90
94
|
| GET | `/cmdk/info` | home / profile / platform |
|
|
91
|
-
| GET | `/cmdk/sessions?q=&limit=` |
|
|
95
|
+
| GET | `/cmdk/sessions?q=&limit=` | 完整会话目录 + 正文命中片段(标题匹配在客户端做) |
|
|
92
96
|
| GET | `/cmdk/workspaces` | 工作区清单 + 已归档会话 ID |
|
|
93
|
-
| GET | `/cmdk/export?id=` | 把一段会话渲染成 Markdown |
|
|
94
97
|
| GET | `/cmdk/plugins` | 已安装插件(名称 / 版本 / 启用状态 / 目录) |
|
|
95
98
|
| POST | `/cmdk/plugin` | `{ name, enabled }` 启用或禁用某个插件 |
|
|
96
99
|
| POST | `/cmdk/open-profile` | 在访达/资源管理器打开目录(可选 `{path}`) |
|
|
@@ -110,7 +113,8 @@ dsh plugin add dsh-cmdk
|
|
|
110
113
|
```
|
|
111
114
|
package.json # dsh.bundle.patch + dsh.client(platform web, immediately)
|
|
112
115
|
cordis.patch.yml # insert: dsh-cmdk
|
|
113
|
-
|
|
116
|
+
CHANGELOG.md
|
|
117
|
+
index.js # Host:/cmdk/{info,sessions,workspaces,plugins,plugin,open-profile}
|
|
114
118
|
client.js # Client:shell.overlay 面板 + sidebar.footer.action 按钮
|
|
115
119
|
locale/zh.json # meta.title / meta.description
|
|
116
120
|
locale/en.json
|
package/client.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One overlay (shell.overlay) plus a small sidebar footer button. Navigation,
|
|
5
5
|
* renaming, archiving, forking and theming all go through public Client
|
|
6
|
-
* services; only the things a browser cannot do (reveal a directory
|
|
7
|
-
*
|
|
6
|
+
* services; only the things a browser cannot do (reveal a directory) come
|
|
7
|
+
* from the tiny Host route. Transcript export uses the native
|
|
8
|
+
* /api/session.markdown route.
|
|
8
9
|
*/
|
|
9
10
|
window.__ModuleLoader__.load({
|
|
10
11
|
id: 'dsh-cmdk',
|
|
@@ -21,6 +22,9 @@ window.__ModuleLoader__.load({
|
|
|
21
22
|
var API = '/cmdk';
|
|
22
23
|
var RECENT_KEY = 'dsh-cmdk/recent';
|
|
23
24
|
var MONO = 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace';
|
|
25
|
+
var IS_MAC = /Mac|iPhone|iPad|iPod/.test(String(navigator.platform || navigator.userAgent || ''));
|
|
26
|
+
var MOD_KEY = IS_MAC ? '⌘' : 'Ctrl';
|
|
27
|
+
var HOTKEY = MOD_KEY + 'K';
|
|
24
28
|
|
|
25
29
|
var C = {
|
|
26
30
|
text: 'var(--dsw-alias-label-primary)',
|
|
@@ -56,6 +60,7 @@ window.__ModuleLoader__.load({
|
|
|
56
60
|
searching: '正在搜索…',
|
|
57
61
|
action: '动作',
|
|
58
62
|
session: '会话',
|
|
63
|
+
untitled: '无标题',
|
|
59
64
|
workspace: '工作区',
|
|
60
65
|
theme: '外观',
|
|
61
66
|
system: '系统',
|
|
@@ -110,6 +115,7 @@ window.__ModuleLoader__.load({
|
|
|
110
115
|
copied: '已复制',
|
|
111
116
|
done: '完成',
|
|
112
117
|
failed: '失败',
|
|
118
|
+
failedSep: ':',
|
|
113
119
|
fontNow: '字号',
|
|
114
120
|
},
|
|
115
121
|
en: {
|
|
@@ -131,6 +137,7 @@ window.__ModuleLoader__.load({
|
|
|
131
137
|
searching: 'Searching…',
|
|
132
138
|
action: 'Actions',
|
|
133
139
|
session: 'Sessions',
|
|
140
|
+
untitled: 'Untitled',
|
|
134
141
|
workspace: 'Workspaces',
|
|
135
142
|
theme: 'Appearance',
|
|
136
143
|
system: 'System',
|
|
@@ -184,6 +191,7 @@ window.__ModuleLoader__.load({
|
|
|
184
191
|
copied: 'Copied',
|
|
185
192
|
done: 'Done',
|
|
186
193
|
failed: 'Failed',
|
|
194
|
+
failedSep: ': ',
|
|
187
195
|
fontNow: 'Font size',
|
|
188
196
|
},
|
|
189
197
|
};
|
|
@@ -415,6 +423,17 @@ window.__ModuleLoader__.load({
|
|
|
415
423
|
}
|
|
416
424
|
}
|
|
417
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Frecency: how often it was used, discounted by a one-week half-life.
|
|
428
|
+
* A brand-new action (count 1) beats something used twice a month ago.
|
|
429
|
+
*/
|
|
430
|
+
function frecency(hit, now) {
|
|
431
|
+
if (!hit || (!hit.at && !hit.count)) return 0;
|
|
432
|
+
var count = Math.max(1, Math.min(hit.count || 1, 40));
|
|
433
|
+
var ageDays = Math.max(0, ((now || Date.now()) - (hit.at || 0)) / 86400000);
|
|
434
|
+
return count * Math.pow(0.5, ageDays / 7);
|
|
435
|
+
}
|
|
436
|
+
|
|
418
437
|
// ---------------------------------------------------------------------
|
|
419
438
|
// item helpers
|
|
420
439
|
// ---------------------------------------------------------------------
|
|
@@ -532,12 +551,19 @@ window.__ModuleLoader__.load({
|
|
|
532
551
|
revealPath(cwd).then(() => ({ status: t.done })),
|
|
533
552
|
),
|
|
534
553
|
item('action:export', 'action', t.exportMd, t.exportMdHint, () =>
|
|
535
|
-
|
|
536
|
-
|
|
554
|
+
fetch('/api/session.markdown?sessionId=' + encodeURIComponent(id), {
|
|
555
|
+
headers: { Accept: 'text/markdown' },
|
|
556
|
+
}).then((response) => {
|
|
557
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
558
|
+
return response.text();
|
|
559
|
+
}).then((markdown) => {
|
|
560
|
+
var fallback = entry.title || id || 'session';
|
|
561
|
+
var filename = String(fallback).replace(/[\\/:*?"<>|]+/g, '-').slice(0, 80) + '.md';
|
|
562
|
+
var blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' });
|
|
537
563
|
var url = URL.createObjectURL(blob);
|
|
538
564
|
var anchor = document.createElement('a');
|
|
539
565
|
anchor.href = url;
|
|
540
|
-
anchor.download =
|
|
566
|
+
anchor.download = filename;
|
|
541
567
|
document.body.appendChild(anchor);
|
|
542
568
|
anchor.click();
|
|
543
569
|
anchor.remove();
|
|
@@ -718,23 +744,46 @@ window.__ModuleLoader__.load({
|
|
|
718
744
|
return t.recent;
|
|
719
745
|
}
|
|
720
746
|
|
|
721
|
-
function buildRoot(ctx, lang, query, catalog, workspaces, recent, plugins, ext, reloadPlugins) {
|
|
747
|
+
function buildRoot(ctx, lang, query, catalog, hits, workspaces, recent, plugins, ext, reloadPlugins) {
|
|
722
748
|
var items = [];
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
749
|
+
var text = query.trim();
|
|
750
|
+
var now = Date.now();
|
|
751
|
+
var seenSessions = new Set();
|
|
752
|
+
var sessionRows = [];
|
|
753
|
+
// Local-first: every catalogue row is fuzzy-matched in the browser, so
|
|
754
|
+
// keystrokes never wait for the Host. Content hits arrive separately.
|
|
755
|
+
var requested = text ? catalog : catalog.slice(0, 5);
|
|
756
|
+
requested.forEach(function (entry) {
|
|
757
|
+
var row = sessionItem(ctx, lang, entry);
|
|
758
|
+
if (text && scoreOf(query, row) <= 0) return;
|
|
759
|
+
seenSessions.add(entry.sessionId);
|
|
760
|
+
sessionRows.push(row);
|
|
761
|
+
});
|
|
762
|
+
if (text) {
|
|
763
|
+
(hits || []).forEach(function (entry) {
|
|
764
|
+
if (seenSessions.has(entry.sessionId)) return;
|
|
765
|
+
seenSessions.add(entry.sessionId);
|
|
766
|
+
var row = sessionItem(ctx, lang, entry);
|
|
767
|
+
row.force = true;
|
|
768
|
+
sessionRows.push(row);
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
sessionRows.forEach(function (row) {
|
|
772
|
+
items.push(row);
|
|
773
|
+
});
|
|
774
|
+
(plugins || []).slice(0, text ? plugins.length : 6).forEach((plugin) => items.push(pluginItem(ctx, lang, plugin, reloadPlugins)));
|
|
726
775
|
buildActions(ctx, lang).forEach((entry) => items.push(entry));
|
|
727
776
|
workspaces.forEach((workspace) => items.push(workspaceItem(ctx, lang, workspace)));
|
|
728
777
|
(ext || []).forEach((command) => items.push(extItem(ctx, lang, command)));
|
|
729
778
|
buildThemes(ctx, lang).forEach((entry) => items.push(entry));
|
|
730
779
|
buildLanguages(ctx, lang).forEach((entry) => items.push(entry));
|
|
731
780
|
|
|
732
|
-
if (!
|
|
781
|
+
if (!text) {
|
|
733
782
|
// a short "recent" band on top, then the natural groups
|
|
734
783
|
var ranked = items
|
|
735
784
|
.map((entry) => ({ entry: entry, hit: recent[entry.key] }))
|
|
736
|
-
.filter((row) => row.hit && row.hit.at)
|
|
737
|
-
.sort((a, b) => (b.hit
|
|
785
|
+
.filter((row) => row.hit && (row.hit.at || row.hit.count))
|
|
786
|
+
.sort((a, b) => frecency(b.hit, now) - frecency(a.hit, now))
|
|
738
787
|
.slice(0, 4)
|
|
739
788
|
.map((row) => row.entry);
|
|
740
789
|
var seen = new Set(ranked.map((entry) => entry.key));
|
|
@@ -745,12 +794,10 @@ window.__ModuleLoader__.load({
|
|
|
745
794
|
|
|
746
795
|
var scored = [];
|
|
747
796
|
items.forEach((entry) => {
|
|
748
|
-
var score = scoreOf(query, entry);
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
}
|
|
753
|
-
var bump = recent[entry.key] && recent[entry.key].at ? Math.min(recent[entry.key].count || 0, 6) * 0.02 : 0;
|
|
797
|
+
var score = entry.force ? Math.max(scoreOf(query, entry), 0.95) : scoreOf(query, entry);
|
|
798
|
+
if (score <= 0) return;
|
|
799
|
+
var hit = recent[entry.key];
|
|
800
|
+
var bump = hit ? Math.min(frecency(hit, now), 8) * 0.015 : 0;
|
|
754
801
|
scored.push({ entry: entry, score: score + bump });
|
|
755
802
|
});
|
|
756
803
|
// keep every group contiguous: a group's place comes from its best hit, the
|
|
@@ -1164,7 +1211,7 @@ window.__ModuleLoader__.load({
|
|
|
1164
1211
|
var t = copy(lang);
|
|
1165
1212
|
var open = useOpen();
|
|
1166
1213
|
var [query, setQuery] = useState('');
|
|
1167
|
-
var [catalog, setCatalog] = useState({ items: [], loading: true });
|
|
1214
|
+
var [catalog, setCatalog] = useState({ items: [], hits: [], hitsFor: '', loading: true });
|
|
1168
1215
|
var [index, setIndex] = useState(0);
|
|
1169
1216
|
var [stack, setStack] = useState([]);
|
|
1170
1217
|
var [input, setInput] = useState(null);
|
|
@@ -1253,16 +1300,17 @@ window.__ModuleLoader__.load({
|
|
|
1253
1300
|
setStatus(null);
|
|
1254
1301
|
var timer = setTimeout(
|
|
1255
1302
|
() => {
|
|
1256
|
-
setCatalog((prev) => (prev.items.length ? prev : { items: [], loading: true }));
|
|
1303
|
+
setCatalog((prev) => (prev.items.length ? prev : { items: [], hits: [], hitsFor: '', loading: true }));
|
|
1257
1304
|
getJson(API + '/sessions?limit=24' + (text ? '&q=' + encodeURIComponent(text) : ''))
|
|
1258
1305
|
.then((payload) => {
|
|
1259
1306
|
if (!alive) return;
|
|
1260
1307
|
var live = (payload.items || []).filter((entry) => !archived[entry.sessionId]);
|
|
1261
|
-
|
|
1308
|
+
var liveHits = (payload.hits || []).filter((entry) => !archived[entry.sessionId]);
|
|
1309
|
+
setCatalog({ items: live, hits: liveHits, hitsFor: text, loading: false });
|
|
1262
1310
|
})
|
|
1263
1311
|
.catch(() => {
|
|
1264
1312
|
if (!alive) return;
|
|
1265
|
-
setCatalog({ items: [], loading: false });
|
|
1313
|
+
setCatalog({ items: [], hits: [], hitsFor: text, loading: false });
|
|
1266
1314
|
});
|
|
1267
1315
|
},
|
|
1268
1316
|
text ? 180 : 0,
|
|
@@ -1311,7 +1359,12 @@ window.__ModuleLoader__.load({
|
|
|
1311
1359
|
for (var vi = 0; vi < catalog.items.length; vi += 1) {
|
|
1312
1360
|
if (!archived[catalog.items[vi].sessionId]) visible.push(catalog.items[vi]);
|
|
1313
1361
|
}
|
|
1314
|
-
var
|
|
1362
|
+
var visibleHits = [];
|
|
1363
|
+
var freshHits = catalog.hitsFor === query.trim() ? catalog.hits : [];
|
|
1364
|
+
for (var hi = 0; hi < freshHits.length; hi += 1) {
|
|
1365
|
+
if (!archived[freshHits[hi].sessionId]) visibleHits.push(freshHits[hi]);
|
|
1366
|
+
}
|
|
1367
|
+
var root = buildRoot(ctx, lang, query, visible, visibleHits, workspaces, recent, plugins, ext, reloadPlugins);
|
|
1315
1368
|
var current = stack.length ? stack[stack.length - 1] : null;
|
|
1316
1369
|
var items = input ? [] : current ? current.items : root;
|
|
1317
1370
|
var active = Math.min(index, Math.max(items.length - 1, 0));
|
|
@@ -1384,7 +1437,7 @@ window.__ModuleLoader__.load({
|
|
|
1384
1437
|
try {
|
|
1385
1438
|
result = entry.run();
|
|
1386
1439
|
} catch (error) {
|
|
1387
|
-
setStatus(t.failed +
|
|
1440
|
+
setStatus(t.failed + t.failedSep + String((error && error.message) || error));
|
|
1388
1441
|
return;
|
|
1389
1442
|
}
|
|
1390
1443
|
if (result && typeof result.then === 'function') {
|
|
@@ -1396,7 +1449,7 @@ window.__ModuleLoader__.load({
|
|
|
1396
1449
|
},
|
|
1397
1450
|
(error) => {
|
|
1398
1451
|
setBusy(false);
|
|
1399
|
-
setStatus(t.failed +
|
|
1452
|
+
setStatus(t.failed + t.failedSep + String((error && error.message) || error));
|
|
1400
1453
|
},
|
|
1401
1454
|
);
|
|
1402
1455
|
return;
|
|
@@ -1423,7 +1476,7 @@ window.__ModuleLoader__.load({
|
|
|
1423
1476
|
try {
|
|
1424
1477
|
pending = input.submit(value);
|
|
1425
1478
|
} catch (error) {
|
|
1426
|
-
setStatus(t.failed +
|
|
1479
|
+
setStatus(t.failed + t.failedSep + String((error && error.message) || error));
|
|
1427
1480
|
return;
|
|
1428
1481
|
}
|
|
1429
1482
|
setBusy(true);
|
|
@@ -1435,7 +1488,7 @@ window.__ModuleLoader__.load({
|
|
|
1435
1488
|
},
|
|
1436
1489
|
(error) => {
|
|
1437
1490
|
setBusy(false);
|
|
1438
|
-
setStatus(t.failed +
|
|
1491
|
+
setStatus(t.failed + t.failedSep + String((error && error.message) || error));
|
|
1439
1492
|
},
|
|
1440
1493
|
);
|
|
1441
1494
|
}
|
|
@@ -2117,7 +2170,7 @@ window.__ModuleLoader__.load({
|
|
|
2117
2170
|
{
|
|
2118
2171
|
type: 'button',
|
|
2119
2172
|
onClick: () => store.toggle(),
|
|
2120
|
-
title:
|
|
2173
|
+
title: HOTKEY,
|
|
2121
2174
|
onMouseEnter: () => setHover(true),
|
|
2122
2175
|
onMouseLeave: () => setHover(false),
|
|
2123
2176
|
style: {
|
|
@@ -2144,7 +2197,7 @@ window.__ModuleLoader__.load({
|
|
|
2144
2197
|
{ style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
|
|
2145
2198
|
t.footer || '搜索',
|
|
2146
2199
|
),
|
|
2147
|
-
h(Kbd, null,
|
|
2200
|
+
h(Kbd, null, HOTKEY),
|
|
2148
2201
|
);
|
|
2149
2202
|
}
|
|
2150
2203
|
|
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sandbox cannot reach:
|
|
7
7
|
*
|
|
8
8
|
* GET /cmdk/info → where this profile lives (home + profile dir)
|
|
9
|
-
* GET /cmdk/sessions?q= → session
|
|
9
|
+
* GET /cmdk/sessions?q= → full session catalogue + message-content hits
|
|
10
10
|
* POST /cmdk/open-profile → reveal the profile directory in Finder/Explorer
|
|
11
11
|
* GET /cmdk/plugins → installed bundles (name, version, enabled, dir)
|
|
12
12
|
* POST /cmdk/plugin → enable/disable one bundle via pluginManager
|
|
@@ -170,24 +170,6 @@ async function sessionCatalog(ctx) {
|
|
|
170
170
|
return items;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
/** Title hits from the warm catalogue, strongest first (prefix > substring). */
|
|
174
|
-
function matchTitles(items, text) {
|
|
175
|
-
const lower = text.toLowerCase();
|
|
176
|
-
const scored = [];
|
|
177
|
-
for (const item of items) {
|
|
178
|
-
if (!item.title) continue;
|
|
179
|
-
const title = item.title.toLowerCase();
|
|
180
|
-
let score = -1;
|
|
181
|
-
if (title === lower) score = 100;
|
|
182
|
-
else if (title.startsWith(lower)) score = 80;
|
|
183
|
-
else if (title.includes(lower)) score = 60;
|
|
184
|
-
else if (item.cwd && item.cwd.toLowerCase().includes(lower)) score = 20;
|
|
185
|
-
if (score > 0) scored.push({ item, score });
|
|
186
|
-
}
|
|
187
|
-
scored.sort((a, b) => b.score - a.score);
|
|
188
|
-
return scored.map((entry) => entry.item);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
173
|
/**
|
|
192
174
|
* Content hits. The SQLite full-text index can be empty in the wild (the
|
|
193
175
|
* native session search reports "no matches" on such machines), so this walks
|
|
@@ -202,7 +184,7 @@ async function matchContent(ctx, text, limit) {
|
|
|
202
184
|
const cached = contentCache.get(text);
|
|
203
185
|
if (cached && Date.now() - cached.at < CONTENT_TTL) return cached.items.slice(0, limit);
|
|
204
186
|
const catalog = await sessionCatalog(ctx);
|
|
205
|
-
const scan = catalog.
|
|
187
|
+
const scan = catalog.slice(0, CONTENT_SCAN_SIZE);
|
|
206
188
|
|
|
207
189
|
const hits = [];
|
|
208
190
|
for (const item of scan) {
|
|
@@ -240,11 +222,6 @@ function snippetAround(text, needle) {
|
|
|
240
222
|
}
|
|
241
223
|
|
|
242
224
|
|
|
243
|
-
// ---------------------------------------------------------------------------
|
|
244
|
-
// transcript export: the session log is a multi-frame zstd file, so it is
|
|
245
|
-
// decompressed through the zstd CLI (Node's zstd only reads the first frame).
|
|
246
|
-
// ---------------------------------------------------------------------------
|
|
247
|
-
|
|
248
225
|
function workspaceStorePath() {
|
|
249
226
|
return path.join(dshHome(), 'storages', 'workspace.json');
|
|
250
227
|
}
|
|
@@ -277,71 +254,6 @@ function readWorkspaces() {
|
|
|
277
254
|
};
|
|
278
255
|
}
|
|
279
256
|
|
|
280
|
-
function sessionDirOf(sessionId) {
|
|
281
|
-
const root = path.join(dshHome(), 'sessions');
|
|
282
|
-
let workspaces;
|
|
283
|
-
try {
|
|
284
|
-
workspaces = fs.readdirSync(root, { withFileTypes: true });
|
|
285
|
-
} catch {
|
|
286
|
-
return null;
|
|
287
|
-
}
|
|
288
|
-
const bare = sessionId.startsWith('session-') ? sessionId.slice('session-'.length) : sessionId;
|
|
289
|
-
const wanted = `session-${bare}`;
|
|
290
|
-
for (const workspace of workspaces) {
|
|
291
|
-
if (!workspace.isDirectory()) continue;
|
|
292
|
-
const candidate = path.join(root, workspace.name, wanted);
|
|
293
|
-
if (fs.existsSync(path.join(candidate, 'session.v4.jsonl.zstd'))) return candidate;
|
|
294
|
-
}
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function run(file, args) {
|
|
299
|
-
return new Promise((resolve) => {
|
|
300
|
-
execFile(file, args, { maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
301
|
-
resolve({ ok: !error, stdout: stdout || '', stderr: stderr || (error && error.message) || '' });
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function textOf(content) {
|
|
307
|
-
if (!Array.isArray(content)) return '';
|
|
308
|
-
const parts = [];
|
|
309
|
-
for (const block of content) {
|
|
310
|
-
if (block && block.type === 'text' && typeof block.text === 'string') parts.push(block.text);
|
|
311
|
-
}
|
|
312
|
-
return parts.join('\n').trim();
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function renderMarkdown(events, fallbackTitle) {
|
|
316
|
-
let title = fallbackTitle || '会话导出';
|
|
317
|
-
const lines = [];
|
|
318
|
-
for (const event of events) {
|
|
319
|
-
if (!event || typeof event !== 'object') continue;
|
|
320
|
-
if (event.type === 'session/title' && event.data && typeof event.data.title === 'string') {
|
|
321
|
-
title = event.data.title;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
lines.push(`# ${title}`, '');
|
|
325
|
-
let turns = 0;
|
|
326
|
-
for (const event of events) {
|
|
327
|
-
if (!event || typeof event !== 'object') continue;
|
|
328
|
-
const data = event.data || {};
|
|
329
|
-
if (event.type === 'user/message') {
|
|
330
|
-
if (!data.source || data.source.kind !== 'user') continue;
|
|
331
|
-
const text = textOf(data.content || (data.message && data.message.content));
|
|
332
|
-
if (!text) continue;
|
|
333
|
-
turns += 1;
|
|
334
|
-
lines.push(`## 你`, '', text, '');
|
|
335
|
-
} else if (event.type === 'assistant/message') {
|
|
336
|
-
const text = textOf(data.content || (data.message && data.message.content));
|
|
337
|
-
if (!text) continue;
|
|
338
|
-
lines.push(`## 助手`, '', text, '');
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
lines.push('---', `共 ${turns} 轮对话`);
|
|
342
|
-
return { title, markdown: lines.join('\n'), turns };
|
|
343
|
-
}
|
|
344
|
-
|
|
345
257
|
export function apply(ctx, config) {
|
|
346
258
|
const server = ctx.get('webServer') || ctx.webServer;
|
|
347
259
|
if (!server || typeof server.register !== 'function') {
|
|
@@ -388,24 +300,13 @@ export function apply(ctx, config) {
|
|
|
388
300
|
try {
|
|
389
301
|
const catalog = await sessionCatalog(ctx);
|
|
390
302
|
if (!text) {
|
|
391
|
-
|
|
392
|
-
sendJson(res, 200, { ok: true, items: titled, total: catalog.length });
|
|
303
|
+
sendJson(res, 200, { ok: true, items: catalog, hits: [], total: catalog.length });
|
|
393
304
|
return;
|
|
394
305
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
seen.add(item.sessionId);
|
|
400
|
-
merged.push(item);
|
|
401
|
-
}
|
|
402
|
-
for (const item of await matchContent(ctx, text, limit)) {
|
|
403
|
-
if (seen.has(item.sessionId)) continue;
|
|
404
|
-
seen.add(item.sessionId);
|
|
405
|
-
merged.push(item);
|
|
406
|
-
if (merged.length >= limit) break;
|
|
407
|
-
}
|
|
408
|
-
sendJson(res, 200, { ok: true, items: merged, total: catalog.length });
|
|
306
|
+
// The client filters titles locally (fuzzy + pinyin); the Host only
|
|
307
|
+
// contributes the expensive part: message-content hits.
|
|
308
|
+
const hits = await matchContent(ctx, text, limit);
|
|
309
|
+
sendJson(res, 200, { ok: true, items: catalog, hits, total: catalog.length });
|
|
409
310
|
} catch (error) {
|
|
410
311
|
sendJson(res, 500, { ok: false, error: String((error && error.message) || error) });
|
|
411
312
|
}
|
|
@@ -530,60 +431,6 @@ export function apply(ctx, config) {
|
|
|
530
431
|
}),
|
|
531
432
|
);
|
|
532
433
|
|
|
533
|
-
disposers.push(
|
|
534
|
-
server.register({
|
|
535
|
-
kind: 'exact',
|
|
536
|
-
path: ROUTE + '/export',
|
|
537
|
-
handler: async (req, res) => {
|
|
538
|
-
if (refused(ctx, req, res)) return;
|
|
539
|
-
let url;
|
|
540
|
-
try {
|
|
541
|
-
url = new URL(req.url || ROUTE, 'http://localhost');
|
|
542
|
-
} catch {
|
|
543
|
-
sendJson(res, 400, { ok: false, error: 'bad url' });
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
const sessionId = (url.searchParams.get('id') || '').trim();
|
|
547
|
-
if (!/^[\w-]{6,80}$/.test(sessionId)) {
|
|
548
|
-
sendJson(res, 400, { ok: false, error: 'bad session id' });
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
try {
|
|
552
|
-
const dir = sessionDirOf(sessionId);
|
|
553
|
-
if (!dir) {
|
|
554
|
-
sendJson(res, 404, { ok: false, error: 'session log not found' });
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
const zstd = await run('zstd', ['-d', '-c', path.join(dir, 'session.v4.jsonl.zstd')]);
|
|
558
|
-
if (!zstd.ok) {
|
|
559
|
-
sendJson(res, 500, { ok: false, error: 'zstd failed: ' + zstd.stderr.slice(0, 200) });
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
const events = [];
|
|
563
|
-
for (const line of zstd.stdout.split('\n')) {
|
|
564
|
-
const text = line.trim();
|
|
565
|
-
if (!text) continue;
|
|
566
|
-
try {
|
|
567
|
-
events.push(JSON.parse(text));
|
|
568
|
-
} catch {
|
|
569
|
-
/* skip a broken line rather than losing the export */
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
const rendered = renderMarkdown(events, sessionId);
|
|
573
|
-
sendJson(res, 200, {
|
|
574
|
-
ok: true,
|
|
575
|
-
title: rendered.title,
|
|
576
|
-
turns: rendered.turns,
|
|
577
|
-
filename: rendered.title.replace(/[\\/:*?"<>|]+/g, '-').slice(0, 80) + '.md',
|
|
578
|
-
markdown: rendered.markdown,
|
|
579
|
-
});
|
|
580
|
-
} catch (error) {
|
|
581
|
-
sendJson(res, 500, { ok: false, error: String((error && error.message) || error) });
|
|
582
|
-
}
|
|
583
|
-
},
|
|
584
|
-
}),
|
|
585
|
-
);
|
|
586
|
-
|
|
587
434
|
ctx.effect(() => () => {
|
|
588
435
|
for (const dispose of disposers) {
|
|
589
436
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cmdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "⌘K command palette for DeepSeek Harness: jump to sessions, search message content, switch workspaces, toggle plugins.",
|
|
6
6
|
"icon": "icon.svg",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"README.md",
|
|
22
|
+
"CHANGELOG.md",
|
|
22
23
|
"LICENSE",
|
|
23
24
|
"screenshot.png",
|
|
24
25
|
"screenshot-actions.png",
|