dsh-codinput 0.3.9 → 0.4.0

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/NOTICE +37 -4
  3. package/README.en.md +26 -12
  4. package/README.md +26 -12
  5. package/TESTING.md +86 -6
  6. package/assets/donate-qr.jpg +0 -0
  7. package/lib/client.js +2875 -98
  8. package/package.json +5 -2
  9. package/src/client/cm.ts +19 -1
  10. package/src/client/command.ts +39 -8
  11. package/src/client/components/CodinputSurface.tsx +80 -27
  12. package/src/client/components/EditorPane.tsx +3 -1
  13. package/src/client/components/InputSlotSeat.tsx +56 -1
  14. package/src/client/components/SlashMenu.tsx +294 -41
  15. package/src/client/components/StatsRow.tsx +40 -23
  16. package/src/client/components/Toolbar.tsx +284 -48
  17. package/src/client/components/UserMessageMarkdown.tsx +158 -115
  18. package/src/client/components/VoiceActivity.tsx +30 -69
  19. package/src/client/components/slotEntry.tsx +37 -0
  20. package/src/client/host-types.ts +11 -3
  21. package/src/client/hotkey.ts +38 -0
  22. package/src/client/i18n.ts +18 -4
  23. package/src/client/index.tsx +9 -0
  24. package/src/client/markdown/code.ts +281 -0
  25. package/src/client/markdown/extensions.ts +576 -0
  26. package/src/client/markdown/heavy-assets.d.ts +11 -0
  27. package/src/client/markdown/heavy-shared.ts +50 -0
  28. package/src/client/markdown/heavy.ts +159 -0
  29. package/src/client/markdown/index.ts +289 -0
  30. package/src/client/markdown/math.ts +134 -0
  31. package/src/client/markdown/mermaid.ts +358 -0
  32. package/src/client/markdown/sanitize.ts +103 -0
  33. package/src/client/markdown/styles.ts +171 -0
  34. package/src/client/prefs.ts +206 -23
  35. package/src/client/settings.tsx +7 -0
  36. package/src/client/stats.ts +92 -20
  37. package/src/client/styles.ts +55 -30
@@ -27,7 +27,11 @@ const ZH: Dict = {
27
27
  'editor.placeholder': '描述你想要构建的内容,/ 调用指令,@ 文件或对话',
28
28
  'preview.empty': '暂无可预览内容',
29
29
  'tool.command': '指令(/)',
30
- 'tool.send': '发送',
30
+ 'tool.send': '发送消息',
31
+ // 官方 0.2.2 input.* 按钮文案(conversation 词典逐值):sending / queue / steer。
32
+ 'tool.sending': '正在发送消息',
33
+ 'tool.sendQueue': '排队发送',
34
+ 'tool.sendSteer': '插话发送',
31
35
  'tool.model': '选择模型,当前 {name}',
32
36
  'tool.modelPlain': '选择模型',
33
37
  'tool.context': '上下文已用 {percent}%',
@@ -103,6 +107,9 @@ const ZH: Dict = {
103
107
  'umsg.image': '图片',
104
108
  'set.lineNumbers': '显示行号',
105
109
  'set.lineNumbersDesc': '当前行与行号高亮常开',
110
+ 'set.markdownMono': 'Markdown 等宽连字',
111
+ 'set.markdownMonoDesc':
112
+ '预览与用户消息正文改用 JetBrains Mono 等宽连字字体:-> 靠字体连字显示为箭头(文本仍是原文,编辑器输入框始终按字面显示);关闭后正文恢复普通阅读字体,代码块不受影响',
106
113
  'set.font': '编辑器字体',
107
114
  'set.fontDesc': 'CSS font-family,留空恢复默认',
108
115
  'set.sendKey': '发送快捷键',
@@ -121,7 +128,7 @@ const ZH: Dict = {
121
128
  'side.guideDesc': '代码编辑器风格的输入面板;开启期间主输入框由本面板接管(共用同一台输入机)',
122
129
  'side.disabled': 'Codinput 接管已关闭:主输入在原输入位(官方输入框)。输入 /codinput 或到 设置 → Codinput 重新开启。',
123
130
  'tool.attach': '文件',
124
- 'tool.stop': '停止',
131
+ 'tool.stop': '停止生成',
125
132
  'image.preview': '原图预览',
126
133
  'image.closePreview': '关闭原图预览',
127
134
  'image.openOriginal': '查看原图',
@@ -140,7 +147,11 @@ const EN: Dict = {
140
147
  'editor.placeholder': 'Describe what you want to build — / for commands, @ for files or chats',
141
148
  'preview.empty': 'Nothing to preview',
142
149
  'tool.command': 'Commands (/)',
143
- 'tool.send': 'Send',
150
+ 'tool.send': 'Send message',
151
+ // Official 0.2.2 input.* copy (conversation dictionary, verbatim).
152
+ 'tool.sending': 'Sending message',
153
+ 'tool.sendQueue': 'Queue message',
154
+ 'tool.sendSteer': 'Steer message',
144
155
  'tool.model': 'Select model, current {name}',
145
156
  'tool.modelPlain': 'Select model',
146
157
  'tool.context': 'Context used {percent}%',
@@ -216,6 +227,9 @@ const EN: Dict = {
216
227
  'umsg.image': 'Image',
217
228
  'set.lineNumbers': 'Show line numbers',
218
229
  'set.lineNumbersDesc': 'The current line and its gutter number stay highlighted',
230
+ 'set.markdownMono': 'Monospace ligatures in Markdown',
231
+ 'set.markdownMonoDesc':
232
+ 'Render previews and your own messages in the JetBrains Mono ligature font, so -> shows as an arrow while the text itself stays unchanged (the editor always shows literal characters); turn it off to restore the reading font. Code blocks are unaffected',
219
233
  'set.font': 'Editor font',
220
234
  'set.fontDesc': 'CSS font-family; leave empty for the default',
221
235
  'set.sendKey': 'Send shortcut',
@@ -234,7 +248,7 @@ const EN: Dict = {
234
248
  'side.guideDesc': 'Code-editor style input panel. While enabled it takes over the composer (sharing the same input machine).',
235
249
  'side.disabled': 'Codinput takeover is off: the composer is back at its original place (official input). Type /codinput or open Settings → Codinput to re-enable.',
236
250
  'tool.attach': 'File',
237
- 'tool.stop': 'Stop',
251
+ 'tool.stop': 'Stop generating',
238
252
  'image.preview': 'Original image preview',
239
253
  'image.closePreview': 'Close original image preview',
240
254
  'image.openOriginal': 'View original',
@@ -18,6 +18,7 @@ import { loadPrefs, subscribePrefs } from './prefs';
18
18
  import { isAnySidebarInputCarried, subscribeSidebarInput } from './sideinput';
19
19
  import { barRegistration, CodinputBar } from './components/CodinputBar';
20
20
  import { userMarkdownRegistrations } from './components/UserMessageMarkdown';
21
+ import { registerHeavyMarkdown } from './markdown/heavy';
21
22
  import { sidebarBodyRegistration, sidebarTabDefinition, sidebarTitleRegistration } from './sidebar';
22
23
  import { settingsRegistration } from './settings';
23
24
  import { registerCodinputCommand } from './command';
@@ -47,6 +48,14 @@ export function apply(ctx: ClientContext): void {
47
48
  injectStyles();
48
49
  installServices(ctx);
49
50
 
51
+ // ---- Markdown 扩展渲染 heavy 扩展:代码高亮 / KaTeX / Mermaid(全内置离线)----
52
+ // 只注册渲染扩展;同步/异步激活由 基础渲染的 activateMarkdown 在 innerHTML
53
+ // 落地后统一调度(组件侧无感)。注册幂等,热更新重复 apply 安全。
54
+ runGuarded('md.heavy', () => {
55
+ registerHeavyMarkdown();
56
+ diagWindow().__dshCodinputShell['md.heavy'] = 'registered';
57
+ });
58
+
50
59
  // 诊断:插件加载期即接管 console.error(应用首渲染在插件加载之后),
51
60
  // 抓取槽位边界的原始错误对象。
52
61
  runGuarded('console.hook', () => {
@@ -0,0 +1,281 @@
1
+ /**
2
+ * 扩展渲染:代码块高亮(highlight.js 常用语言子集,全部打进单文件、离线可用)。
3
+ *
4
+ * 产出(经 基础渲染的 marked renderer 覆盖,再交给 DOMPurify 白名单):
5
+ * <div class="dci-md-codeblock">
6
+ * <div class="dci-md-codebar"><span class="dci-md-codelang">js</span>
7
+ * <button class="dci-md-copy" …>…</button></div>
8
+ * <pre class="dci-md-pre"><code class="hljs language-js">…</code></pre>
9
+ * </div>
10
+ * 输出只有 class 与文本(hljs 的 span 只带 class),DOMPurify 白名单无需新增
11
+ * 标签;复制按钮的点击在 `activateCodeBlocks` 里用事件委托接(返回 cleanup)。
12
+ *
13
+ * `mermaid` 语言块不在本模块渲染,交给 `mermaid.ts` 的占位元素(异步渲染)。
14
+ */
15
+
16
+ import hljs from 'highlight.js/lib/core';
17
+ import type { LanguageFn } from 'highlight.js';
18
+ import bash from 'highlight.js/lib/languages/bash';
19
+ import c from 'highlight.js/lib/languages/c';
20
+ import clojure from 'highlight.js/lib/languages/clojure';
21
+ import cmake from 'highlight.js/lib/languages/cmake';
22
+ import cpp from 'highlight.js/lib/languages/cpp';
23
+ import csharp from 'highlight.js/lib/languages/csharp';
24
+ import css from 'highlight.js/lib/languages/css';
25
+ import dart from 'highlight.js/lib/languages/dart';
26
+ import diff from 'highlight.js/lib/languages/diff';
27
+ import dockerfile from 'highlight.js/lib/languages/dockerfile';
28
+ import elixir from 'highlight.js/lib/languages/elixir';
29
+ import go from 'highlight.js/lib/languages/go';
30
+ import gradle from 'highlight.js/lib/languages/gradle';
31
+ import graphql from 'highlight.js/lib/languages/graphql';
32
+ import groovy from 'highlight.js/lib/languages/groovy';
33
+ import haskell from 'highlight.js/lib/languages/haskell';
34
+ import ini from 'highlight.js/lib/languages/ini';
35
+ import java from 'highlight.js/lib/languages/java';
36
+ import javascript from 'highlight.js/lib/languages/javascript';
37
+ import json from 'highlight.js/lib/languages/json';
38
+ import julia from 'highlight.js/lib/languages/julia';
39
+ import kotlin from 'highlight.js/lib/languages/kotlin';
40
+ import less from 'highlight.js/lib/languages/less';
41
+ import lua from 'highlight.js/lib/languages/lua';
42
+ import makefile from 'highlight.js/lib/languages/makefile';
43
+ import markdown from 'highlight.js/lib/languages/markdown';
44
+ import matlab from 'highlight.js/lib/languages/matlab';
45
+ import nginx from 'highlight.js/lib/languages/nginx';
46
+ import perl from 'highlight.js/lib/languages/perl';
47
+ import php from 'highlight.js/lib/languages/php';
48
+ import plaintext from 'highlight.js/lib/languages/plaintext';
49
+ import powershell from 'highlight.js/lib/languages/powershell';
50
+ import protobuf from 'highlight.js/lib/languages/protobuf';
51
+ import python from 'highlight.js/lib/languages/python';
52
+ import r from 'highlight.js/lib/languages/r';
53
+ import ruby from 'highlight.js/lib/languages/ruby';
54
+ import rust from 'highlight.js/lib/languages/rust';
55
+ import scala from 'highlight.js/lib/languages/scala';
56
+ import scss from 'highlight.js/lib/languages/scss';
57
+ import shell from 'highlight.js/lib/languages/shell';
58
+ import sql from 'highlight.js/lib/languages/sql';
59
+ import swift from 'highlight.js/lib/languages/swift';
60
+ import typescript from 'highlight.js/lib/languages/typescript';
61
+ import vim from 'highlight.js/lib/languages/vim';
62
+ import wasm from 'highlight.js/lib/languages/wasm';
63
+ import xml from 'highlight.js/lib/languages/xml';
64
+ import yaml from 'highlight.js/lib/languages/yaml';
65
+ import type { Tokens } from 'marked';
66
+ import { copyTextToClipboard } from './index';
67
+ import { escapeHtml, safeLanguageClass } from './heavy-shared';
68
+ import { renderMermaidBlock } from './mermaid';
69
+
70
+ /** 内置语言表(46 种常用;只注册一次)。 */
71
+ const LANGUAGES: ReadonlyArray<readonly [string, LanguageFn]> = [
72
+ ['xml', xml],
73
+ ['css', css],
74
+ ['scss', scss],
75
+ ['less', less],
76
+ ['javascript', javascript],
77
+ ['typescript', typescript],
78
+ ['json', json],
79
+ ['yaml', yaml],
80
+ ['bash', bash],
81
+ ['shell', shell],
82
+ ['powershell', powershell],
83
+ ['python', python],
84
+ ['java', java],
85
+ ['c', c],
86
+ ['cpp', cpp],
87
+ ['csharp', csharp],
88
+ ['go', go],
89
+ ['rust', rust],
90
+ ['kotlin', kotlin],
91
+ ['swift', swift],
92
+ ['ruby', ruby],
93
+ ['php', php],
94
+ ['sql', sql],
95
+ ['markdown', markdown],
96
+ ['diff', diff],
97
+ ['dockerfile', dockerfile],
98
+ ['ini', ini],
99
+ ['makefile', makefile],
100
+ ['lua', lua],
101
+ ['r', r],
102
+ ['perl', perl],
103
+ ['graphql', graphql],
104
+ ['protobuf', protobuf],
105
+ ['plaintext', plaintext],
106
+ ['dart', dart],
107
+ ['scala', scala],
108
+ ['elixir', elixir],
109
+ ['haskell', haskell],
110
+ ['clojure', clojure],
111
+ ['groovy', groovy],
112
+ ['julia', julia],
113
+ ['matlab', matlab],
114
+ ['nginx', nginx],
115
+ ['vim', vim],
116
+ ['cmake', cmake],
117
+ ['gradle', gradle],
118
+ ['wasm', wasm],
119
+ ];
120
+
121
+ for (const [name, definition] of LANGUAGES) {
122
+ try {
123
+ hljs.registerLanguage(name, definition);
124
+ } catch {
125
+ // 单个语言注册失败不应拖垮整块高亮(未知语言回退纯文本)。
126
+ }
127
+ }
128
+
129
+ /** 代码块别名 → hljs 语言名(hljs 自身的 aliases 之外再补一批常见写法)。 */
130
+ const LANGUAGE_ALIASES: Readonly<Record<string, string>> = {
131
+ html: 'xml',
132
+ htm: 'xml',
133
+ svg: 'xml',
134
+ vue: 'xml',
135
+ md: 'markdown',
136
+ yml: 'yaml',
137
+ sh: 'bash',
138
+ zsh: 'bash',
139
+ ps1: 'powershell',
140
+ pwsh: 'powershell',
141
+ docker: 'dockerfile',
142
+ make: 'makefile',
143
+ 'c++': 'cpp',
144
+ 'c#': 'csharp',
145
+ cs: 'csharp',
146
+ rs: 'rust',
147
+ py: 'python',
148
+ rb: 'ruby',
149
+ kt: 'kotlin',
150
+ jsx: 'javascript',
151
+ mjs: 'javascript',
152
+ cjs: 'javascript',
153
+ tsx: 'typescript',
154
+ mts: 'typescript',
155
+ cts: 'typescript',
156
+ jsonc: 'json',
157
+ text: 'plaintext',
158
+ txt: 'plaintext',
159
+ plain: 'plaintext',
160
+ 'objective-c': 'objectivec',
161
+ };
162
+
163
+ /** 解析代码块语言:返回 hljs 可高亮的语言名;未知返回空串。 */
164
+ export function resolveHighlightLanguage(raw: string): string {
165
+ const normalized = raw.trim().toLowerCase();
166
+ if (normalized === '') return '';
167
+ const candidates = [normalized, LANGUAGE_ALIASES[normalized] ?? ''];
168
+ for (const candidate of candidates) {
169
+ if (candidate === '') continue;
170
+ try {
171
+ if (hljs.getLanguage(candidate)) return candidate;
172
+ } catch {
173
+ // getLanguage 对奇怪名字可能抛错:当作未知继续。
174
+ }
175
+ }
176
+ return '';
177
+ }
178
+
179
+ export interface CodeHighlightResult {
180
+ /** 已在内部转义过的 HTML(hljs span / 纯文本)。 */
181
+ html: string;
182
+ /** 实际使用的 hljs 语言名(未知为空串)。 */
183
+ language: string;
184
+ /** 是否真的走了高亮(未知语言为 false)。 */
185
+ known: boolean;
186
+ }
187
+
188
+ /** 高亮一段源码;未知语言 / 高亮异常一律回退「转义纯文本」。 */
189
+ export function highlightCode(source: string, rawLanguage: string): CodeHighlightResult {
190
+ const language = resolveHighlightLanguage(rawLanguage);
191
+ if (language === '') return { html: escapeHtml(source), language: '', known: false };
192
+ try {
193
+ const result = hljs.highlight(source, { language, ignoreIllegals: true });
194
+ return { html: result.value, language, known: true };
195
+ } catch {
196
+ return { html: escapeHtml(source), language, known: false };
197
+ }
198
+ }
199
+
200
+ /** 复制按钮文案:宿主语言是 en 时用英文,其余中文(插件默认中文环境)。 */
201
+ function copyLabels(): { copy: string; copied: string } {
202
+ let language = 'zh';
203
+ if (typeof document !== 'undefined') {
204
+ language = (document.documentElement.getAttribute('lang') ?? '').toLowerCase();
205
+ }
206
+ if (language === '') {
207
+ try {
208
+ language = (navigator.language ?? 'zh').toLowerCase();
209
+ } catch {
210
+ language = 'zh';
211
+ }
212
+ }
213
+ return language.startsWith('en') ? { copy: 'Copy', copied: 'Copied' } : { copy: '复制', copied: '已复制' };
214
+ }
215
+
216
+ /** marked renderer.code 的实现:chrome(语言标签 + 复制按钮)+ 高亮代码。 */
217
+ export function renderCodeBlock(token: Tokens.Code): string {
218
+ const source = typeof token.text === 'string' ? token.text : '';
219
+ const info = typeof token.lang === 'string' ? token.lang.trim() : '';
220
+ const requested = info === '' ? '' : (info.split(/\s+/u)[0] ?? '');
221
+ if (requested.toLowerCase() === 'mermaid') return renderMermaidBlock(source);
222
+
223
+ const result = highlightCode(source, requested);
224
+ const { copy } = copyLabels();
225
+ const label = requested === '' ? 'text' : requested;
226
+ const hljsClass = result.known ? 'hljs ' : '';
227
+ const languageClass = result.language === '' ? '' : `language-${safeLanguageClass(result.language)}`;
228
+ return [
229
+ '<div class="dci-md-codeblock">',
230
+ '<div class="dci-md-codebar">',
231
+ `<span class="dci-md-codelang">${escapeHtml(label)}</span>`,
232
+ `<button type="button" class="dci-md-copy" title="复制代码 / Copy code"><span class="dci-md-copy-glyph" aria-hidden="true"></span><span class="dci-md-copy-text">${escapeHtml(copy)}</span></button>`,
233
+ '</div>',
234
+ `<pre class="dci-md-pre"><code class="${hljsClass}${languageClass}">${result.html}</code></pre>`,
235
+ '</div>\n',
236
+ ].join('');
237
+ }
238
+
239
+ /* ---------- 激活期:复制按钮(事件委托,返回 cleanup) ---------- */
240
+
241
+ const COPY_FEEDBACK_MS = 1200;
242
+
243
+ function flashCopied(button: HTMLButtonElement, timers: Set<number>): void {
244
+ const { copy, copied } = copyLabels();
245
+ button.dataset.dciCopied = 'true';
246
+ const text = button.querySelector('.dci-md-copy-text');
247
+ if (text) text.textContent = copied;
248
+ const timer = window.setTimeout(() => {
249
+ timers.delete(timer);
250
+ if (!button.isConnected) return;
251
+ delete button.dataset.dciCopied;
252
+ if (text) text.textContent = copy;
253
+ }, COPY_FEEDBACK_MS);
254
+ timers.add(timer);
255
+ }
256
+
257
+ /**
258
+ * innerHTML 落地后的复制按钮激活(基础渲染 `activateMarkdown` 调用)。
259
+ * 没有代码块时零开销;返回 cleanup 摘掉监听与计时器。
260
+ */
261
+ export function activateCodeBlocks(root: HTMLElement): (() => void) | undefined {
262
+ if (!root.querySelector('.dci-md-copy')) return undefined;
263
+ const timers = new Set<number>();
264
+ const onClick = (event: Event): void => {
265
+ const target = event.target instanceof Element ? event.target.closest('.dci-md-copy') : null;
266
+ if (!(target instanceof HTMLButtonElement) || !root.contains(target)) return;
267
+ const block = target.closest('.dci-md-codeblock');
268
+ const code = block ? block.querySelector('code') : null;
269
+ if (!code) return;
270
+ const text = code.textContent ?? '';
271
+ void copyTextToClipboard(text).then((ok) => {
272
+ if (ok) flashCopied(target, timers);
273
+ });
274
+ };
275
+ root.addEventListener('click', onClick);
276
+ return () => {
277
+ root.removeEventListener('click', onClick);
278
+ for (const timer of timers) window.clearTimeout(timer);
279
+ timers.clear();
280
+ };
281
+ }