dsh-claude-move 0.2.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/CHANGELOG.md +73 -0
- package/LICENSE +201 -0
- package/NOTICE +23 -0
- package/README.es.md +291 -0
- package/README.hi.md +292 -0
- package/README.md +317 -0
- package/README.pt.md +291 -0
- package/README.zh.md +311 -0
- package/THIRD_PARTY_NOTICES.md +67 -0
- package/assets/social-card.png +0 -0
- package/client/client.js +451 -0
- package/cordis.patch.yml +5 -0
- package/index.mjs +2891 -0
- package/lib/agmd-section.mjs +144 -0
- package/lib/commands-migrate.mjs +85 -0
- package/lib/context.mjs +156 -0
- package/lib/convert.mjs +725 -0
- package/lib/discovery.mjs +619 -0
- package/lib/frontmatter.mjs +58 -0
- package/lib/handoff.mjs +136 -0
- package/lib/imports-store.mjs +64 -0
- package/lib/manifest.mjs +73 -0
- package/lib/persona.mjs +37 -0
- package/lib/report.mjs +63 -0
- package/lib/settings.mjs +147 -0
- package/lib/skill-migrate.mjs +128 -0
- package/lib/skills-provider.mjs +219 -0
- package/lib/sources/claude/mapper.mjs +102 -0
- package/lib/sources/claude/parser.mjs +190 -0
- package/lib/sources/codex/mapper.mjs +120 -0
- package/lib/sources/codex/parser.mjs +451 -0
- package/lib/sources/contract.mjs +145 -0
- package/lib/sources/hermes/mapper.mjs +61 -0
- package/lib/sources/hermes/parser.mjs +152 -0
- package/lib/sources/opencode/convert.mjs +236 -0
- package/lib/sources/opencode/mapper.mjs +102 -0
- package/lib/sources/opencode/parser.mjs +266 -0
- package/lib/wizard.mjs +329 -0
- package/package.json +66 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Third-Party Notices
|
|
2
|
+
|
|
3
|
+
`dsh-claude-move` is licensed under the **Apache License 2.0** (see
|
|
4
|
+
[LICENSE](LICENSE) and [NOTICE](NOTICE)). It reuses code and design
|
|
5
|
+
patterns from the following open-source projects, which remain under
|
|
6
|
+
their own licenses. Every reuse is attributed here and in the source files.
|
|
7
|
+
|
|
8
|
+
## Nwflower/dsh-chat-import — MIT
|
|
9
|
+
|
|
10
|
+
- Repository: https://github.com/Nwflower/dsh-chat-import
|
|
11
|
+
- License: MIT
|
|
12
|
+
- Reused: the JSONL → DSH SessionEvent conversion core (`lib/convert.mjs`,
|
|
13
|
+
vendored and extended in place), the import/attach orchestration pattern
|
|
14
|
+
(`sessionPersistence.create` + `append`, `workspaceRegistry.attachSession`,
|
|
15
|
+
idempotent batch import), and the `test/convert.test.mjs` suite plus its
|
|
16
|
+
synthetic fixtures.
|
|
17
|
+
- The MIT-licensed portions of `lib/convert.mjs` are retained under MIT;
|
|
18
|
+
the file header records the provenance.
|
|
19
|
+
|
|
20
|
+
## Demogorgon314/dsh-resume-plugin — MIT
|
|
21
|
+
|
|
22
|
+
- Repository: https://github.com/Demogorgon314/dsh-resume-plugin
|
|
23
|
+
- License: MIT
|
|
24
|
+
- Reused: the foreign-session safety model (treat transcripts as untrusted
|
|
25
|
+
inert history; exclude system/developer/reasoning from handoffs; verify
|
|
26
|
+
repository state before continuing; list candidates on ambiguity) and the
|
|
27
|
+
Claude Code discovery conventions (`$CLAUDE_CONFIG_DIR`, `projects/<slug>`
|
|
28
|
+
layout, `ai-title`/`custom-title`).
|
|
29
|
+
|
|
30
|
+
Note: upstream's bundled `session_reader.py` is adapted from Grok Build's
|
|
31
|
+
foreign-session reader under Apache-2.0. This project does not copy that
|
|
32
|
+
file; the discovery logic is reimplemented in Node. See
|
|
33
|
+
dsh-resume-plugin's THIRD_PARTY_NOTICES.md and LICENSES/Apache-2.0.txt for
|
|
34
|
+
the upstream attribution chain.
|
|
35
|
+
|
|
36
|
+
## YYTbit/dsh-plugin-claude-bridge — MIT
|
|
37
|
+
|
|
38
|
+
- Repository: https://github.com/YYTbit/dsh-plugin-claude-bridge
|
|
39
|
+
- License: MIT
|
|
40
|
+
- Reused: the zero-migration injection approach for Claude Code memory,
|
|
41
|
+
skills, and CLAUDE.md, the memory type priority order
|
|
42
|
+
(feedback > project > reference > user), and the frontmatter parsing shape
|
|
43
|
+
(`lib/frontmatter.mjs`).
|
|
44
|
+
|
|
45
|
+
## MIT license text (required by the MIT-licensed components above)
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
MIT License
|
|
49
|
+
|
|
50
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
51
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
52
|
+
in the Software without restriction, including without limitation the rights
|
|
53
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
54
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
55
|
+
furnished to do so, subject to the following conditions:
|
|
56
|
+
|
|
57
|
+
The above copyright notice and this permission notice shall be included in all
|
|
58
|
+
copies or substantial portions of the Software.
|
|
59
|
+
|
|
60
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
61
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
62
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
63
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
64
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
65
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
66
|
+
SOFTWARE.
|
|
67
|
+
```
|
|
Binary file
|
package/client/client.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// client/client.js — dsh-claude-move 浏览器迁移面板(F16)。
|
|
3
|
+
//
|
|
4
|
+
// 零依赖 vanilla 面板:host 端 dsh.client 扫描把本文件作为 classic script
|
|
5
|
+
// 注入 __DSH_BOOT__ 图,执行时经 window.__ModuleLoader__.load 注册工厂;
|
|
6
|
+
// 工厂闭包在首次 materialize 时运行,apply 挂载浮层面板(无注入服务,
|
|
7
|
+
// 只依赖 DOM + 本插件自注册的 /api/claude-move/* JSON 路由)。
|
|
8
|
+
// 面板自身不产生任何模型可见内容。文案按浏览器语言 zh/en 双语(D3)。
|
|
9
|
+
|
|
10
|
+
(function () {
|
|
11
|
+
'use strict'
|
|
12
|
+
if (typeof window === 'undefined' || !window.__ModuleLoader__ || !window.__ModuleLoader__.load) return
|
|
13
|
+
window.__ModuleLoader__.load({
|
|
14
|
+
id: 'dsh-claude-move',
|
|
15
|
+
factory: function () {
|
|
16
|
+
return {
|
|
17
|
+
name: 'claude-move-panel',
|
|
18
|
+
inject: [],
|
|
19
|
+
apply: function (ctx) { installPanel(ctx) },
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
})()
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 特性探测客户端 Cordis 上下文里的官方服务(B1)。老 shell 没有 sessions/
|
|
27
|
+
* workspaces 服务时不写入 inject(缺服务会导致 boot sweep FAILED),改在
|
|
28
|
+
* apply 里用 get 探测:有则免刷新更新会话列表、直接打开会话,无则回退整页刷新。
|
|
29
|
+
* @param ctx - 客户端 Cordis 上下文。
|
|
30
|
+
* @param name - 服务名。
|
|
31
|
+
* @returns 服务对象或 undefined。
|
|
32
|
+
*/
|
|
33
|
+
function safeService(ctx, name) {
|
|
34
|
+
try {
|
|
35
|
+
const svc = ctx && typeof ctx.get === 'function' ? ctx.get(name) : undefined
|
|
36
|
+
return svc ?? undefined
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 面板文案(D3):zh/en 双语,缺失键回退英文键名。 */
|
|
43
|
+
const PANEL_STRINGS = {
|
|
44
|
+
zh: {
|
|
45
|
+
panelTitle: 'Claude Code 迁移',
|
|
46
|
+
openButton: '🐳 Claude 迁移',
|
|
47
|
+
refresh: '刷新',
|
|
48
|
+
refreshTitle: '重新扫描',
|
|
49
|
+
reset: '重置缓存',
|
|
50
|
+
resetTitle: '重置扫描缓存与导入映射(保留已导入会话)',
|
|
51
|
+
close: '关闭',
|
|
52
|
+
filterPlaceholder: '关键词过滤(标题/会话)…',
|
|
53
|
+
importAll: '批量导入全部',
|
|
54
|
+
cancel: '取消',
|
|
55
|
+
cancelTitle: '取消当前导入',
|
|
56
|
+
scanning: '扫描中…',
|
|
57
|
+
scanned: '已扫描:{0} 个项目 / {1} 个会话',
|
|
58
|
+
panelDisabled: '面板路由未启用:enableWebPanel 为 false 或 Web 服务未加载,面板不可用',
|
|
59
|
+
panelDisabledImport: '面板路由未启用:enableWebPanel 为 false 或 Web 服务未加载,导入不可用',
|
|
60
|
+
scanFailed: '扫描失败:',
|
|
61
|
+
gitDirty: 'git 脏 ',
|
|
62
|
+
git: 'git ',
|
|
63
|
+
dirMissing: '目录不存在',
|
|
64
|
+
imported: '已导入',
|
|
65
|
+
importedNew: '已导入·有新增',
|
|
66
|
+
sourceMissing: '源缺失',
|
|
67
|
+
notImported: '未导入',
|
|
68
|
+
messagesTools: '{0} 消息 · {1} 工具',
|
|
69
|
+
showMore: '已显示 {0}/{1} 个会话,点击加载更多…',
|
|
70
|
+
empty: '(无数据,点「刷新」重新扫描)',
|
|
71
|
+
kvSession: '会话:',
|
|
72
|
+
kvLastActivity: '最近活动:',
|
|
73
|
+
messagesToolCalls: '{0} 消息 · {1} 工具调用',
|
|
74
|
+
kvDshSession: 'DSH 会话:',
|
|
75
|
+
kvDir: '目录:',
|
|
76
|
+
unknown: '未知',
|
|
77
|
+
dirNotExists: '(不存在)',
|
|
78
|
+
importContinue: '导入并继续',
|
|
79
|
+
openSession: '打开会话',
|
|
80
|
+
openSessionTitle: '在当前窗口打开已导入会话',
|
|
81
|
+
refreshSessions: '刷新会话列表',
|
|
82
|
+
refreshSessionsTitle: '导入后刷新会话列表以点开续聊',
|
|
83
|
+
cacheReset: '缓存已重置,正在重新扫描…',
|
|
84
|
+
resetFailed: '重置失败:',
|
|
85
|
+
submitting: '提交导入…',
|
|
86
|
+
importing: '导入中:{0}/{1} 新增,{2} 追加,{3} 失败',
|
|
87
|
+
importDone: '导入完成:新增 {0}、已存在 {1}、追加 {2}、跳过 {3}、失败 {4}。会话列表已自动刷新,无需重启 dsh。',
|
|
88
|
+
importCancelled: '导入已取消。',
|
|
89
|
+
importFailed: '导入失败:',
|
|
90
|
+
cancelling: '正在取消…',
|
|
91
|
+
cancelFailed: '取消失败:',
|
|
92
|
+
unknownError: '未知错误',
|
|
93
|
+
},
|
|
94
|
+
en: {
|
|
95
|
+
panelTitle: 'Claude Code Migration',
|
|
96
|
+
openButton: '🐳 Claude Migration',
|
|
97
|
+
refresh: 'Refresh',
|
|
98
|
+
refreshTitle: 'Rescan',
|
|
99
|
+
reset: 'Reset cache',
|
|
100
|
+
resetTitle: 'Reset scan cache and import map (imported sessions are kept)',
|
|
101
|
+
close: 'Close',
|
|
102
|
+
filterPlaceholder: 'Filter by keyword (title/session)…',
|
|
103
|
+
importAll: 'Import everything',
|
|
104
|
+
cancel: 'Cancel',
|
|
105
|
+
cancelTitle: 'Cancel current import',
|
|
106
|
+
scanning: 'Scanning…',
|
|
107
|
+
scanned: 'Scanned: {0} projects / {1} sessions',
|
|
108
|
+
panelDisabled: 'Panel routes disabled: enableWebPanel is false or the Web service is not loaded — the panel is unavailable',
|
|
109
|
+
panelDisabledImport: 'Panel routes disabled: enableWebPanel is false or the Web service is not loaded — import is unavailable',
|
|
110
|
+
scanFailed: 'Scan failed: ',
|
|
111
|
+
gitDirty: 'git dirty ',
|
|
112
|
+
git: 'git ',
|
|
113
|
+
dirMissing: 'directory missing',
|
|
114
|
+
imported: 'imported',
|
|
115
|
+
importedNew: 'imported · new turns',
|
|
116
|
+
sourceMissing: 'source missing',
|
|
117
|
+
notImported: 'not imported',
|
|
118
|
+
messagesTools: '{0} msgs · {1} tools',
|
|
119
|
+
showMore: 'Showing {0}/{1} sessions — click to load more…',
|
|
120
|
+
empty: '(No data — click "Refresh" to rescan)',
|
|
121
|
+
kvSession: 'Session: ',
|
|
122
|
+
kvLastActivity: 'Last activity: ',
|
|
123
|
+
messagesToolCalls: '{0} msgs · {1} tool calls',
|
|
124
|
+
kvDshSession: 'DSH session: ',
|
|
125
|
+
kvDir: 'Directory: ',
|
|
126
|
+
unknown: 'unknown',
|
|
127
|
+
dirNotExists: ' (missing)',
|
|
128
|
+
importContinue: 'Import & continue',
|
|
129
|
+
openSession: 'Open session',
|
|
130
|
+
openSessionTitle: 'Open the imported session in this window',
|
|
131
|
+
refreshSessions: 'Refresh session list',
|
|
132
|
+
refreshSessionsTitle: 'Refresh the session list after importing',
|
|
133
|
+
cacheReset: 'Cache reset — rescanning…',
|
|
134
|
+
resetFailed: 'Reset failed: ',
|
|
135
|
+
submitting: 'Submitting import…',
|
|
136
|
+
importing: 'Importing: {0}/{1} new, {2} appended, {3} failed',
|
|
137
|
+
importDone: 'Import done: {0} new, {1} existing, {2} appended, {3} skipped, {4} failed. The session list refreshed automatically — no DSH restart needed.',
|
|
138
|
+
importCancelled: 'Import cancelled.',
|
|
139
|
+
importFailed: 'Import failed: ',
|
|
140
|
+
cancelling: 'Cancelling…',
|
|
141
|
+
cancelFailed: 'Cancel failed: ',
|
|
142
|
+
unknownError: 'unknown error',
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 按浏览器语言取文案并代入 {0}/{1}/… 参数。 */
|
|
147
|
+
function panelText(key, ...args) {
|
|
148
|
+
const lang = typeof navigator !== 'undefined' && String(navigator.language || '').toLowerCase().startsWith('zh')
|
|
149
|
+
? 'zh'
|
|
150
|
+
: 'en'
|
|
151
|
+
let text = (PANEL_STRINGS[lang] ?? PANEL_STRINGS.en)[key] ?? key
|
|
152
|
+
for (let i = 0; i < args.length; i++) {
|
|
153
|
+
text = text.replace('{' + i + '}', String(args[i] ?? ''))
|
|
154
|
+
}
|
|
155
|
+
return text
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function installPanel(ctx) {
|
|
159
|
+
if (document.getElementById('claude-move-panel')) return
|
|
160
|
+
|
|
161
|
+
// B1:官方客户端服务(sessions.refresh/open、workspaces.refresh)——特性探测 + 回退。
|
|
162
|
+
const sessions = safeService(ctx, 'sessions')
|
|
163
|
+
const workspaces = safeService(ctx, 'workspaces')
|
|
164
|
+
|
|
165
|
+
const style = document.createElement('style')
|
|
166
|
+
style.textContent = `
|
|
167
|
+
#claude-move-panel { position: fixed; z-index: 2147483000; font: 13px/1.5 system-ui, "Segoe UI", sans-serif; }
|
|
168
|
+
#cm-open { position: fixed; right: 16px; bottom: 16px; z-index: 2147483000; padding: 8px 14px; border: 1px solid #3f6fae;
|
|
169
|
+
border-radius: 999px; background: #0d1526; color: #7db4ff; cursor: pointer; font: 13px system-ui; box-shadow: 0 4px 14px rgba(0,0,0,.35); }
|
|
170
|
+
#cm-open:hover { background: #16233c; }
|
|
171
|
+
#cm-drawer { position: fixed; right: 0; top: 0; bottom: 0; width: 460px; max-width: 92vw; display: flex; flex-direction: column;
|
|
172
|
+
background: #101827; color: #d7e2f2; border-left: 1px solid #24344d; box-shadow: -8px 0 24px rgba(0,0,0,.4); }
|
|
173
|
+
#cm-head { padding: 10px 12px; border-bottom: 1px solid #24344d; display: flex; gap: 8px; align-items: center; }
|
|
174
|
+
#cm-head b { flex: 1; }
|
|
175
|
+
#cm-head button, #cm-detail button, #cm-foot button { background: #1c2a42; color: #cfe1f7; border: 1px solid #2f4466; border-radius: 6px;
|
|
176
|
+
padding: 4px 10px; cursor: pointer; }
|
|
177
|
+
#cm-head button:hover, #cm-detail button:hover, #cm-foot button:hover { background: #27395a; }
|
|
178
|
+
#cm-filter { margin: 8px 12px; padding: 6px 8px; background: #0b1120; color: #d7e2f2; border: 1px solid #24344d; border-radius: 6px; }
|
|
179
|
+
#cm-body { flex: 1; overflow: auto; padding: 0 12px 12px; }
|
|
180
|
+
.cm-proj { margin: 10px 0 4px; color: #8fb4e8; }
|
|
181
|
+
.cm-proj .cm-badge { margin-left: 6px; }
|
|
182
|
+
.cm-sess { padding: 6px 8px; margin: 3px 0; border: 1px solid #24344d; border-radius: 8px; cursor: pointer; background: #131e33; }
|
|
183
|
+
.cm-sess:hover { background: #1a2944; }
|
|
184
|
+
.cm-sess .t { display: block; color: #e6eefb; }
|
|
185
|
+
.cm-badge { display: inline-block; padding: 0 6px; margin: 0 2px; border-radius: 999px; font-size: 11px; border: 1px solid #33476a; color: #9fb8da; }
|
|
186
|
+
.cm-badge.imported { border-color: #2f7d4f; color: #7fd6a2; }
|
|
187
|
+
.cm-badge.missing { border-color: #a4562f; color: #f0b98a; }
|
|
188
|
+
.cm-badge.dirty { border-color: #a08b2f; color: #ecd27e; }
|
|
189
|
+
#cm-detail { border-top: 1px solid #24344d; padding: 10px 12px; max-height: 40%; overflow: auto; background: #0d1424; }
|
|
190
|
+
#cm-detail h4 { margin: 0 0 6px; color: #9fc4f5; }
|
|
191
|
+
#cm-detail .kv { color: #9fb4d4; margin: 2px 0; }
|
|
192
|
+
#cm-foot { padding: 10px 12px; border-top: 1px solid #24344d; display: flex; gap: 8px; align-items: center; }
|
|
193
|
+
#cm-progress { height: 6px; flex: 1; background: #0b1120; border-radius: 3px; overflow: hidden; border: 1px solid #24344d; }
|
|
194
|
+
#cm-progress i { display: block; height: 100%; width: 0; background: #3f6fae; transition: width .3s; }
|
|
195
|
+
#cm-status { color: #8fa8cc; font-size: 12px; }
|
|
196
|
+
`
|
|
197
|
+
|
|
198
|
+
const openBtn = document.createElement('button')
|
|
199
|
+
openBtn.id = 'cm-open'
|
|
200
|
+
openBtn.textContent = panelText('openButton')
|
|
201
|
+
|
|
202
|
+
const root = document.createElement('div')
|
|
203
|
+
root.id = 'claude-move-panel'
|
|
204
|
+
const drawer = document.createElement('div')
|
|
205
|
+
drawer.id = 'cm-drawer'
|
|
206
|
+
drawer.style.display = 'none'
|
|
207
|
+
drawer.innerHTML = `
|
|
208
|
+
<div id="cm-head"><b>${panelText('panelTitle')}</b>
|
|
209
|
+
<button id="cm-refresh" title="${panelText('refreshTitle')}">${panelText('refresh')}</button>
|
|
210
|
+
<button id="cm-reset" title="${panelText('resetTitle')}">${panelText('reset')}</button>
|
|
211
|
+
<button id="cm-close" title="${panelText('close')}">✕</button>
|
|
212
|
+
</div>
|
|
213
|
+
<input id="cm-filter" placeholder="${panelText('filterPlaceholder')}" />
|
|
214
|
+
<div id="cm-body"></div>
|
|
215
|
+
<div id="cm-detail" style="display:none"></div>
|
|
216
|
+
<div id="cm-foot">
|
|
217
|
+
<button id="cm-import-all">${panelText('importAll')}</button>
|
|
218
|
+
<button id="cm-cancel" style="display:none" title="${panelText('cancelTitle')}">${panelText('cancel')}</button>
|
|
219
|
+
<div id="cm-progress"><i></i></div>
|
|
220
|
+
<span id="cm-status"></span>
|
|
221
|
+
</div>`
|
|
222
|
+
|
|
223
|
+
document.head.appendChild(style)
|
|
224
|
+
root.appendChild(drawer)
|
|
225
|
+
document.body.appendChild(openBtn)
|
|
226
|
+
document.body.appendChild(root)
|
|
227
|
+
|
|
228
|
+
let index = null
|
|
229
|
+
let disabled = false
|
|
230
|
+
const body = drawer.querySelector('#cm-body')
|
|
231
|
+
const detail = drawer.querySelector('#cm-detail')
|
|
232
|
+
const filter = drawer.querySelector('#cm-filter')
|
|
233
|
+
const status = drawer.querySelector('#cm-status')
|
|
234
|
+
const bar = drawer.querySelector('#cm-progress i')
|
|
235
|
+
|
|
236
|
+
const show = (open) => { drawer.style.display = open ? 'flex' : 'none'; if (open) refresh() }
|
|
237
|
+
openBtn.addEventListener('click', () => show(drawer.style.display === 'none'))
|
|
238
|
+
drawer.querySelector('#cm-close').addEventListener('click', () => show(false))
|
|
239
|
+
drawer.querySelector('#cm-refresh').addEventListener('click', () => refresh())
|
|
240
|
+
drawer.querySelector('#cm-reset').addEventListener('click', () => { void resetCache() })
|
|
241
|
+
drawer.querySelector('#cm-import-all').addEventListener('click', () => importJob('all'))
|
|
242
|
+
const cancelBtn = drawer.querySelector('#cm-cancel')
|
|
243
|
+
cancelBtn.addEventListener('click', () => { void cancelJob() })
|
|
244
|
+
filter.addEventListener('input', () => render())
|
|
245
|
+
|
|
246
|
+
async function resetCache() {
|
|
247
|
+
try {
|
|
248
|
+
await json('/api/claude-move/reset', { method: 'POST' })
|
|
249
|
+
status.textContent = panelText('cacheReset')
|
|
250
|
+
await refresh()
|
|
251
|
+
} catch (err) {
|
|
252
|
+
status.textContent = panelText('resetFailed') + String(err && err.message)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function json(url, options) {
|
|
257
|
+
const res = await fetch(url, options)
|
|
258
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
259
|
+
return res.json()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function refresh() {
|
|
263
|
+
status.textContent = panelText('scanning')
|
|
264
|
+
try {
|
|
265
|
+
index = await json('/api/claude-move/index')
|
|
266
|
+
disabled = false
|
|
267
|
+
visibleCount = PAGE_SIZE
|
|
268
|
+
status.textContent = panelText('scanned', index.projects?.length ?? 0, sessionCount(index))
|
|
269
|
+
render()
|
|
270
|
+
} catch (err) {
|
|
271
|
+
const msg = String(err && err.message)
|
|
272
|
+
if (msg === 'HTTP 404') {
|
|
273
|
+
disabled = true
|
|
274
|
+
status.textContent = panelText('panelDisabled')
|
|
275
|
+
} else {
|
|
276
|
+
status.textContent = panelText('scanFailed') + msg
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function sessionCount(idx) {
|
|
282
|
+
return (idx?.projects ?? []).reduce((n, p) => n + (p.sessions?.length ?? 0), 0)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function badge(klass, text) { return `<span class="cm-badge ${klass}">${text}</span>` }
|
|
286
|
+
|
|
287
|
+
// 大索引分页渲染(D4):每次最多渲染 PAGE_SIZE 行,点击「加载更多」增量追加。
|
|
288
|
+
const PAGE_SIZE = 150
|
|
289
|
+
let visibleCount = PAGE_SIZE
|
|
290
|
+
|
|
291
|
+
function render() {
|
|
292
|
+
const kw = filter.value.trim().toLowerCase()
|
|
293
|
+
const projects = (index?.projects ?? []).filter((p) => (
|
|
294
|
+
!kw || p.slug.toLowerCase().includes(kw)
|
|
295
|
+
|| (p.sessions ?? []).some((s) => (s.title ?? '').toLowerCase().includes(kw) || (s.sessionId ?? '').toLowerCase().includes(kw))
|
|
296
|
+
))
|
|
297
|
+
const total = projects.reduce((n, p) => n + (p.sessions ?? []).length, 0)
|
|
298
|
+
const parts = []
|
|
299
|
+
let shown = 0
|
|
300
|
+
outer: for (const p of projects) {
|
|
301
|
+
const badges = []
|
|
302
|
+
if (p.git?.isRepo) {
|
|
303
|
+
if (typeof p.git.dirtyCount === 'number' && p.git.dirtyCount > 0) badges.push(badge('dirty', panelText('gitDirty') + p.git.dirtyCount))
|
|
304
|
+
else badges.push(badge('', panelText('git') + (p.git.branch ?? '?')))
|
|
305
|
+
}
|
|
306
|
+
if (!p.dirExists) badges.push(badge('missing', panelText('dirMissing')))
|
|
307
|
+
parts.push(`<div class="cm-proj">📁 ${esc(p.slug)}${badges.join('')}</div>`)
|
|
308
|
+
for (const s of p.sessions ?? []) {
|
|
309
|
+
if (shown >= visibleCount) break outer
|
|
310
|
+
shown++
|
|
311
|
+
const st = s.import?.status ?? 'none'
|
|
312
|
+
const b = st === 'imported'
|
|
313
|
+
? badge('imported', s.import?.updatesPending ? panelText('importedNew') : panelText('imported'))
|
|
314
|
+
: st === 'source-missing' ? badge('missing', panelText('sourceMissing')) : badge('', panelText('notImported'))
|
|
315
|
+
parts.push(`<div class="cm-sess" data-file="${esc(s.file)}">
|
|
316
|
+
<span class="t">${esc(s.title ?? s.sessionId)}</span>
|
|
317
|
+
${b} <span class="cm-badge">${panelText('messagesTools', s.messages ?? 0, s.toolCalls ?? 0)}</span>
|
|
318
|
+
</div>`)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (total > shown) {
|
|
322
|
+
parts.push(`<div class="cm-sess" id="cm-more">${panelText('showMore', shown, total)}</div>`)
|
|
323
|
+
}
|
|
324
|
+
body.innerHTML = parts.length ? parts.join('') : `<div class="cm-proj">${panelText('empty')}</div>`
|
|
325
|
+
for (const el of body.querySelectorAll('.cm-sess')) {
|
|
326
|
+
if (el.id === 'cm-more') {
|
|
327
|
+
el.addEventListener('click', () => { visibleCount += PAGE_SIZE; render() })
|
|
328
|
+
} else {
|
|
329
|
+
el.addEventListener('click', () => showDetail(el.dataset.file))
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function findSession(file) {
|
|
335
|
+
for (const p of index?.projects ?? []) {
|
|
336
|
+
const s = (p.sessions ?? []).find((x) => x.file === file)
|
|
337
|
+
if (s) return { project: p, session: s }
|
|
338
|
+
}
|
|
339
|
+
return null
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function showDetail(file) {
|
|
343
|
+
const found = findSession(file)
|
|
344
|
+
if (!found) return
|
|
345
|
+
const { project, session } = found
|
|
346
|
+
const when = session.lastActivity ? new Date(session.lastActivity).toLocaleString() : panelText('unknown')
|
|
347
|
+
const dshId = session.import?.dshSessionId
|
|
348
|
+
const canOpen = typeof sessions?.open === 'function'
|
|
349
|
+
detail.style.display = 'block'
|
|
350
|
+
detail.innerHTML = `
|
|
351
|
+
<h4>${esc(session.title ?? session.sessionId)}</h4>
|
|
352
|
+
<div class="kv">${panelText('kvSession')}${esc(session.sessionId)}</div>
|
|
353
|
+
<div class="kv">${panelText('kvLastActivity')}${esc(when)} · ${panelText('messagesToolCalls', session.messages ?? 0, session.toolCalls ?? 0)}</div>
|
|
354
|
+
${dshId ? `<div class="kv">${panelText('kvDshSession')}${esc(dshId)}</div>` : ''}
|
|
355
|
+
<div class="kv">${panelText('kvDir')}${esc(project.cwd ?? panelText('unknown'))}${project.dirExists ? '' : panelText('dirNotExists')}</div>
|
|
356
|
+
<p style="margin-top:8px">
|
|
357
|
+
<button data-act="import">${panelText('importContinue')}</button>
|
|
358
|
+
${dshId && canOpen ? `<button data-act="open" title="${panelText('openSessionTitle')}">${panelText('openSession')}</button>` : ''}
|
|
359
|
+
<button data-act="reload" title="${panelText('refreshSessionsTitle')}">${panelText('refreshSessions')}</button>
|
|
360
|
+
</p>`
|
|
361
|
+
detail.querySelector('[data-act="import"]').addEventListener('click', () => importJob(session.file))
|
|
362
|
+
const openBtn = detail.querySelector('[data-act="open"]')
|
|
363
|
+
if (openBtn) openBtn.addEventListener('click', () => openSession(dshId))
|
|
364
|
+
detail.querySelector('[data-act="reload"]').addEventListener('click', () => { void refreshSessions() })
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** 官方服务可用时免刷新更新会话/工作区列表;否则回退整页刷新。 */
|
|
368
|
+
async function refreshSessions() {
|
|
369
|
+
try {
|
|
370
|
+
if (typeof sessions?.refresh === 'function') await sessions.refresh()
|
|
371
|
+
if (typeof workspaces?.refresh === 'function') await workspaces.refresh()
|
|
372
|
+
return
|
|
373
|
+
} catch {
|
|
374
|
+
// 服务异常:回退整页刷新,保证列表一定更新。
|
|
375
|
+
}
|
|
376
|
+
window.location.reload()
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** 官方服务可用时直接打开已导入会话。 */
|
|
380
|
+
function openSession(dshId) {
|
|
381
|
+
if (typeof sessions?.open === 'function') {
|
|
382
|
+
sessions.open(dshId)
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
window.location.reload()
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let currentJobId = null
|
|
389
|
+
|
|
390
|
+
async function cancelJob() {
|
|
391
|
+
if (!currentJobId) return
|
|
392
|
+
try {
|
|
393
|
+
await json('/api/claude-move/job?job=' + encodeURIComponent(currentJobId), { method: 'DELETE' })
|
|
394
|
+
status.textContent = panelText('cancelling')
|
|
395
|
+
} catch (err) {
|
|
396
|
+
status.textContent = panelText('cancelFailed') + String(err && err.message)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function importJob(target) {
|
|
401
|
+
if (disabled) {
|
|
402
|
+
status.textContent = panelText('panelDisabledImport')
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
status.textContent = panelText('submitting')
|
|
406
|
+
setBar(0)
|
|
407
|
+
cancelBtn.style.display = 'none'
|
|
408
|
+
currentJobId = null
|
|
409
|
+
try {
|
|
410
|
+
const { jobId } = await json('/api/claude-move/import', {
|
|
411
|
+
method: 'POST',
|
|
412
|
+
headers: { 'content-type': 'application/json' },
|
|
413
|
+
body: JSON.stringify(target === 'all' ? { path: 'all' } : { path: target }),
|
|
414
|
+
})
|
|
415
|
+
currentJobId = jobId
|
|
416
|
+
cancelBtn.style.display = ''
|
|
417
|
+
for (;;) {
|
|
418
|
+
await sleep(700)
|
|
419
|
+
const job = await json('/api/claude-move/progress?job=' + encodeURIComponent(jobId))
|
|
420
|
+
const total = job.total ?? 0
|
|
421
|
+
if (total > 0) setBar(((job.imported + job.alreadyImported + job.skipped + job.failed) / total) * 100)
|
|
422
|
+
status.textContent = panelText('importing', job.imported, total, job.appended ?? 0, job.failed)
|
|
423
|
+
if (job.status === 'done' || job.status === 'error' || job.status === 'cancelled') {
|
|
424
|
+
if (job.status === 'done') {
|
|
425
|
+
status.textContent = panelText('importDone', job.imported, job.alreadyImported, job.appended ?? 0, job.skipped, job.failed)
|
|
426
|
+
setBar(100)
|
|
427
|
+
await refreshSessions()
|
|
428
|
+
} else if (job.status === 'cancelled') {
|
|
429
|
+
status.textContent = panelText('importCancelled')
|
|
430
|
+
} else {
|
|
431
|
+
status.textContent = panelText('importFailed') + (job.error ?? panelText('unknownError'))
|
|
432
|
+
}
|
|
433
|
+
cancelBtn.style.display = 'none'
|
|
434
|
+
currentJobId = null
|
|
435
|
+
refresh()
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
} catch (err) {
|
|
440
|
+
status.textContent = panelText('importFailed') + String(err && err.message)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function setBar(pct) { bar.style.width = Math.max(0, Math.min(100, pct)) + '%' }
|
|
445
|
+
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) }
|
|
446
|
+
function esc(text) {
|
|
447
|
+
return String(text ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// 默认收起:只显示悬浮按钮,用户点击才展开抽屉(不抢占每次页面加载的注意力)。
|
|
451
|
+
}
|