dsh-plugin-bridge 0.2.11 → 0.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.
@@ -0,0 +1,487 @@
1
+ /** Pure wire-to-view helpers shared by the native WebUI card and Node tests. */
2
+ export const MAX_EDITED_SUMMARY_CHARS = 24_000;
3
+ const RUN_COMMAND = /\/bridge\s+([^\s]+)\s+--go(?:\s|$)/u;
4
+ const TEXT_SCHEMAS = {
5
+ zh: [
6
+ ['goal', '目标', 'text'],
7
+ ['currentState', '当前状态', 'text'],
8
+ ['keyDecisions', '关键决策与约定', 'list'],
9
+ ['keyFiles', '关键文件', 'list'],
10
+ ['nextStep', '下一步', 'text'],
11
+ ],
12
+ en: [
13
+ ['goal', 'Goal', 'text'],
14
+ ['currentState', 'Current state', 'text'],
15
+ ['keyDecisions', 'Key decisions & conventions', 'list'],
16
+ ['keyFiles', 'Key files', 'list'],
17
+ ['nextStep', 'Next step', 'text'],
18
+ ],
19
+ };
20
+ const TEXT_APPENDICES = {
21
+ zh: new Set(['视觉证据——原文搬运,未经二次摘要', '未解析图片']),
22
+ en: new Set(['Visual evidence — verbatim, not summarized', 'Unresolved images']),
23
+ };
24
+ function sourceLines(markdown, lineEnding) {
25
+ const lines = [];
26
+ let start = 0;
27
+ while (start <= markdown.length) {
28
+ const next = markdown.indexOf(lineEnding, start);
29
+ if (next < 0) {
30
+ lines.push({ start, contentEnd: markdown.length, end: markdown.length, text: markdown.slice(start) });
31
+ break;
32
+ }
33
+ lines.push({ start, contentEnd: next, end: next + lineEnding.length, text: markdown.slice(start, next) });
34
+ start = next + lineEnding.length;
35
+ if (start === markdown.length) {
36
+ lines.push({ start, contentEnd: start, end: start, text: '' });
37
+ break;
38
+ }
39
+ }
40
+ return lines;
41
+ }
42
+ function markdownHeadings(markdown, lineEnding) {
43
+ const headings = [];
44
+ let fence;
45
+ for (const line of sourceLines(markdown, lineEnding)) {
46
+ const fenceMatch = /^ {0,3}(`{3,}|~{3,})(.*)$/u.exec(line.text);
47
+ if (fence) {
48
+ if (fenceMatch && fenceMatch[1]?.[0] === fence.marker
49
+ && (fenceMatch[1]?.length ?? 0) >= fence.length
50
+ && (fenceMatch[2]?.trim() ?? '') === '') {
51
+ fence = undefined;
52
+ }
53
+ continue;
54
+ }
55
+ if (fenceMatch) {
56
+ const token = fenceMatch[1] ?? '';
57
+ fence = { marker: token[0], length: token.length };
58
+ continue;
59
+ }
60
+ if (!line.text.startsWith('##'))
61
+ continue;
62
+ const rest = line.text.slice(2);
63
+ if (rest[0] !== ' ' && rest[0] !== '\t')
64
+ continue;
65
+ let labelStart = 0;
66
+ while (labelStart < rest.length && (rest[labelStart] === ' ' || rest[labelStart] === '\t'))
67
+ labelStart += 1;
68
+ let labelEnd = rest.length;
69
+ while (labelEnd > labelStart && (rest[labelEnd - 1] === ' ' || rest[labelEnd - 1] === '\t'))
70
+ labelEnd -= 1;
71
+ if (labelEnd > labelStart)
72
+ headings.push({ label: rest.slice(labelStart, labelEnd), start: line.start, lineEnd: line.end });
73
+ }
74
+ return fence ? undefined : headings;
75
+ }
76
+ function documentLineEnding(markdown) {
77
+ const endings = markdown.match(/\r\n|\n|\r/gu);
78
+ if (!endings?.length)
79
+ return undefined;
80
+ const first = endings[0];
81
+ if ((first !== '\n' && first !== '\r\n') || endings.some((ending) => ending !== first))
82
+ return undefined;
83
+ return first;
84
+ }
85
+ const MARKDOWN_BLOCK_START = /^([\t ]{0,3})(#{1,6}(?:[\t ]|$)|>(?:[\t ]|$)|[-+*](?:[\t ]|$)|\d+[.)](?:[\t ]|$)|`{3}|~{3})/u;
86
+ const THEMATIC_OR_SETEXT = /^ {0,3}(?:(?:\*[\t ]*){3,}|(?:-[\t ]*){3,}|(?:_[\t ]*){3,}|=+[\t ]*)$/u;
87
+ const TABLE_DELIMITER = /^ {0,3}\|?[\t ]*:?-{3,}:?[\t ]*(?:\|[\t ]*:?-{3,}:?[\t ]*)+\|?[\t ]*$/u;
88
+ const LINK_DEFINITION = /^ {0,3}\[[^\]]+\]:[\t ]*\S/u;
89
+ const HTML_BLOCK_START = /^ {0,3}<[/!?A-Za-z]/u;
90
+ function hasUnsafeBlock(line) {
91
+ return MARKDOWN_BLOCK_START.test(line)
92
+ || THEMATIC_OR_SETEXT.test(line)
93
+ || TABLE_DELIMITER.test(line)
94
+ || LINK_DEFINITION.test(line)
95
+ || /^(?: {4}|\t)/u.test(line)
96
+ || line.includes('<!--')
97
+ || line.includes('-->')
98
+ || line.includes('--!>')
99
+ || HTML_BLOCK_START.test(line);
100
+ }
101
+ function parseListItems(markdownBody, lineEnding, bodyStart, allowPlain) {
102
+ if (!markdownBody)
103
+ return { items: [], style: 'bullet', text: '' };
104
+ const lines = sourceLines(markdownBody, lineEnding);
105
+ if (lines.some((line) => !line.text.trim()))
106
+ return undefined;
107
+ const bullet = /^- (.+)$/u.exec(lines[0]?.text ?? '');
108
+ const style = bullet ? 'bullet' : allowPlain ? 'plain' : undefined;
109
+ if (!style)
110
+ return undefined;
111
+ const items = [];
112
+ if (style === 'plain') {
113
+ for (const line of lines) {
114
+ if (hasUnsafeBlock(line.text) || /^[\t ]*(?:[-+*]|\d+[.)])[\t ]+/u.test(line.text))
115
+ return undefined;
116
+ items.push({
117
+ text: line.text,
118
+ contentStart: bodyStart + line.start,
119
+ contentEnd: bodyStart + line.contentEnd,
120
+ itemStart: bodyStart + line.start,
121
+ itemEnd: bodyStart + line.end,
122
+ });
123
+ }
124
+ }
125
+ else {
126
+ const starts = [];
127
+ for (const [index, line] of lines.entries()) {
128
+ if (/^- (.+)$/u.test(line.text)) {
129
+ starts.push(index);
130
+ continue;
131
+ }
132
+ if (!starts.length || hasUnsafeBlock(line.text) || /^[\t ]+(?:[-+*]|\d+[.)])[\t ]+/u.test(line.text)
133
+ || /^[+*][\t ]+/u.test(line.text) || /^\d+[.)][\t ]+/u.test(line.text))
134
+ return undefined;
135
+ }
136
+ for (const [itemIndex, lineIndex] of starts.entries()) {
137
+ const first = lines[lineIndex];
138
+ const nextLineIndex = starts[itemIndex + 1] ?? lines.length;
139
+ const last = lines[nextLineIndex - 1];
140
+ const firstText = /^- (.+)$/u.exec(first.text)?.[1];
141
+ if (!firstText)
142
+ return undefined;
143
+ const continuationLines = lines.slice(lineIndex + 1, nextLineIndex);
144
+ const continuationPrefixes = continuationLines.map((line) => /^[\t ]*/u.exec(line.text)?.[0] ?? '');
145
+ const continuation = continuationLines.map((line, index) => line.text.slice(continuationPrefixes[index]?.length ?? 0));
146
+ items.push({
147
+ text: [firstText, ...continuation].join(lineEnding),
148
+ contentStart: bodyStart + first.start + 2,
149
+ contentEnd: bodyStart + last.contentEnd,
150
+ itemStart: bodyStart + first.start,
151
+ itemEnd: bodyStart + (lines[nextLineIndex]?.start ?? markdownBody.length),
152
+ continuationPrefixes,
153
+ });
154
+ }
155
+ }
156
+ return { items, style, text: items.map((item) => item.text).join(lineEnding) };
157
+ }
158
+ function plainBlockText(markdownBody, lineEnding) {
159
+ return markdownBody
160
+ .split(lineEnding)
161
+ .map((line) => line.replace(/^([\t ]{0,3})\\(?=[#>*+`~_<\[|!=-])/u, '$1'))
162
+ .join(lineEnding);
163
+ }
164
+ function isPlainBlock(markdownBody, lineEnding) {
165
+ return sourceLines(markdownBody, lineEnding).every((line) => !hasUnsafeBlock(line.text));
166
+ }
167
+ function parseTextBlock(markdownBody, lineEnding) {
168
+ const lines = sourceLines(markdownBody, lineEnding);
169
+ if (lines.length && lines.every((line) => /^- (.+)$/u.test(line.text))) {
170
+ return { style: 'bullets', text: lines.map((line) => /^- (.+)$/u.exec(line.text)?.[1] ?? '').join(lineEnding) };
171
+ }
172
+ return isPlainBlock(markdownBody, lineEnding)
173
+ ? { style: 'plain', text: plainBlockText(markdownBody, lineEnding) }
174
+ : undefined;
175
+ }
176
+ function markdownSafePlainText(text, lineEnding) {
177
+ return text
178
+ .split(lineEnding)
179
+ .map((line) => {
180
+ if (/^(?: {4}|\t)/u.test(line))
181
+ throw new Error('Plain-text lines cannot start with four spaces or a tab');
182
+ if (!hasUnsafeBlock(line))
183
+ return line;
184
+ return line.replace(/^([\t ]{0,3})(?=\S)/u, '$1\\');
185
+ })
186
+ .join(lineEnding);
187
+ }
188
+ /**
189
+ * Project only Bridge's exact bilingual five-section schema into plain fields.
190
+ * Unknown structure fails closed; known visual appendices remain opaque Markdown.
191
+ */
192
+ export function parseBridgeTextProjection(markdown) {
193
+ const lineEnding = documentLineEnding(markdown);
194
+ if (!lineEnding)
195
+ return undefined;
196
+ const headings = markdownHeadings(markdown, lineEnding);
197
+ if (!headings?.length || headings[0]?.start !== 0)
198
+ return undefined;
199
+ const lang = headings[0]?.label === TEXT_SCHEMAS.zh[0][1]
200
+ ? 'zh'
201
+ : headings[0]?.label === TEXT_SCHEMAS.en[0][1]
202
+ ? 'en'
203
+ : undefined;
204
+ if (!lang)
205
+ return undefined;
206
+ const appendixIndex = headings.findIndex((heading) => TEXT_APPENDICES[lang].has(heading.label));
207
+ const schemaHeadings = appendixIndex < 0 ? headings : headings.slice(0, appendixIndex);
208
+ if (schemaHeadings.length !== TEXT_SCHEMAS[lang].length)
209
+ return undefined;
210
+ if (schemaHeadings.some((heading, index) => heading.label !== TEXT_SCHEMAS[lang][index]?.[1]))
211
+ return undefined;
212
+ const firstAppendix = appendixIndex < 0 ? undefined : headings[appendixIndex];
213
+ const editableMarkdown = markdown.slice(0, firstAppendix?.start ?? markdown.length);
214
+ if (editableMarkdown.includes('<!--') || editableMarkdown.includes('-->') || editableMarkdown.includes('--!>'))
215
+ return undefined;
216
+ const sections = [];
217
+ for (const [index, heading] of schemaHeadings.entries()) {
218
+ const [key, label, kind] = TEXT_SCHEMAS[lang][index];
219
+ const bodyStart = heading.lineEnd;
220
+ const regionEnd = schemaHeadings[index + 1]?.start ?? firstAppendix?.start ?? markdown.length;
221
+ let bodyEnd = regionEnd;
222
+ while (bodyEnd - lineEnding.length >= bodyStart
223
+ && markdown.slice(bodyEnd - lineEnding.length, bodyEnd) === lineEnding) {
224
+ bodyEnd -= lineEnding.length;
225
+ }
226
+ const body = markdown.slice(bodyStart, bodyEnd);
227
+ const list = kind === 'list' ? parseListItems(body, lineEnding, bodyStart, key === 'keyFiles') : undefined;
228
+ const textBlock = kind === 'text' ? parseTextBlock(body, lineEnding) : undefined;
229
+ if (kind === 'list' && !list)
230
+ return undefined;
231
+ if (kind === 'text' && !textBlock)
232
+ return undefined;
233
+ const text = list?.text ?? textBlock?.text ?? '';
234
+ sections.push({
235
+ key,
236
+ label,
237
+ kind,
238
+ text,
239
+ bodyStart,
240
+ bodyEnd,
241
+ ...(list ? { items: list.items, listStyle: list.style } : {}),
242
+ ...(textBlock ? { textStyle: textBlock.style } : {}),
243
+ });
244
+ }
245
+ return {
246
+ lang,
247
+ markdown,
248
+ lineEnding,
249
+ sections,
250
+ opaqueSuffix: firstAppendix ? markdown.slice(firstAppendix.start) : '',
251
+ };
252
+ }
253
+ function normalizeLineEndings(text, lineEnding) {
254
+ return text.replace(/\r\n|\n|\r/gu, lineEnding);
255
+ }
256
+ /** Replace one editable body while preserving every byte outside that section. */
257
+ export function replaceBridgeTextSection(projection, key, plainText) {
258
+ const section = projection.sections.find((candidate) => candidate.key === key);
259
+ if (!section)
260
+ throw new Error(`Unknown Bridge text section: ${key}`);
261
+ if (plainText === section.text)
262
+ return projection.markdown;
263
+ const normalized = normalizeLineEndings(plainText, projection.lineEnding);
264
+ let markdownBody;
265
+ if (section.kind === 'list') {
266
+ const values = normalized.split(projection.lineEnding);
267
+ if (!section.items || values.length !== section.items.length || section.items.some((item) => item.text.includes(projection.lineEnding))) {
268
+ throw new Error('Use Bridge list-item helpers for this section');
269
+ }
270
+ markdownBody = section.listStyle === 'plain'
271
+ ? values.join(projection.lineEnding)
272
+ : values.map((line) => `- ${line}`).join(projection.lineEnding);
273
+ }
274
+ else {
275
+ markdownBody = section.textStyle === 'bullets'
276
+ ? normalized.split(projection.lineEnding).map((line) => `- ${line}`).join(projection.lineEnding)
277
+ : markdownSafePlainText(normalized, projection.lineEnding);
278
+ }
279
+ return projection.markdown.slice(0, section.bodyStart)
280
+ + markdownBody
281
+ + projection.markdown.slice(section.bodyEnd);
282
+ }
283
+ function listSection(projection, key) {
284
+ const section = projection.sections.find((candidate) => candidate.key === key);
285
+ if (!section || section.kind !== 'list' || !section.items || !section.listStyle) {
286
+ throw new Error(`Unknown Bridge list section: ${key}`);
287
+ }
288
+ return section;
289
+ }
290
+ function editedListItem(text, style, lineEnding, original) {
291
+ const normalized = normalizeLineEndings(text, lineEnding);
292
+ if (!normalized.trim())
293
+ throw new Error('Bridge list item is empty');
294
+ const lines = normalized.split(lineEnding);
295
+ if (lines.some((line) => hasUnsafeBlock(line)))
296
+ throw new Error('Bridge list item contains Markdown block structure');
297
+ if (style === 'plain' && lines.length > 1)
298
+ throw new Error('Plain path rows must stay on one line');
299
+ return style === 'bullet'
300
+ ? [
301
+ lines[0],
302
+ ...lines.slice(1).map((line, index) => `${original?.continuationPrefixes?.[index] ?? ' '}${line}`),
303
+ ].join(lineEnding)
304
+ : normalized;
305
+ }
306
+ /** Replace one list item without rewriting siblings and while retaining wrapped-line indentation. */
307
+ export function replaceBridgeTextListItem(projection, key, index, plainText) {
308
+ const section = listSection(projection, key);
309
+ const item = section.items?.[index];
310
+ if (!item)
311
+ throw new Error(`Unknown Bridge list item: ${key}[${index}]`);
312
+ if (plainText === item.text)
313
+ return projection.markdown;
314
+ const edited = editedListItem(plainText, section.listStyle, projection.lineEnding, item);
315
+ return projection.markdown.slice(0, item.contentStart) + edited + projection.markdown.slice(item.contentEnd);
316
+ }
317
+ /** Remove exactly one original list-item span. */
318
+ export function removeBridgeTextListItem(projection, key, index) {
319
+ const section = listSection(projection, key);
320
+ const items = section.items ?? [];
321
+ const item = items[index];
322
+ if (!item)
323
+ throw new Error(`Unknown Bridge list item: ${key}[${index}]`);
324
+ const removeStart = index > 0 && index === items.length - 1
325
+ ? item.itemStart - projection.lineEnding.length
326
+ : item.itemStart;
327
+ return projection.markdown.slice(0, removeStart) + projection.markdown.slice(item.itemEnd);
328
+ }
329
+ /** Append one item using the section's existing bullet/plain convention. */
330
+ export function appendBridgeTextListItem(projection, key, plainText) {
331
+ const section = listSection(projection, key);
332
+ const edited = editedListItem(plainText, section.listStyle, projection.lineEnding);
333
+ const prefix = section.bodyStart === section.bodyEnd ? '' : projection.lineEnding;
334
+ const item = section.listStyle === 'bullet' ? `- ${edited}` : edited;
335
+ return projection.markdown.slice(0, section.bodyEnd) + prefix + item + projection.markdown.slice(section.bodyEnd);
336
+ }
337
+ function previewHeaderOf(line) {
338
+ const prefix = line.startsWith('─── Handoff · ')
339
+ ? { text: '─── Handoff · ', lang: 'en' }
340
+ : line.startsWith('─── 交接摘要 · ')
341
+ ? { text: '─── 交接摘要 · ', lang: 'zh' }
342
+ : undefined;
343
+ if (!prefix)
344
+ return undefined;
345
+ const route = line.slice(prefix.text.length);
346
+ const arrow = route.indexOf('→');
347
+ if (arrow < 1)
348
+ return undefined;
349
+ const sourcePreset = route.slice(0, arrow).trim();
350
+ const targetTail = route.slice(arrow + 1).trimStart();
351
+ let targetEnd = 0;
352
+ while (targetEnd < targetTail.length) {
353
+ const char = targetTail[targetEnd];
354
+ if (char === undefined || /\s/u.test(char) || char === '(' || char === '(' || char === '─')
355
+ break;
356
+ targetEnd += 1;
357
+ }
358
+ const targetPreset = targetTail.slice(0, targetEnd);
359
+ if (!sourcePreset || !targetPreset)
360
+ return undefined;
361
+ return { lang: prefix.lang, sourcePreset, targetPreset };
362
+ }
363
+ function isDivider(line) {
364
+ const trimmed = line.trim();
365
+ return trimmed.length >= 10 && [...trimmed].every((char) => char === '─');
366
+ }
367
+ function languageOf(text) {
368
+ return /[\u3400-\u9fff]/u.test(text) ? 'zh' : 'en';
369
+ }
370
+ /** Map the official WebUI document language onto Bridge's supported UI copy. */
371
+ export function uiLanguageOf(documentLang) {
372
+ return documentLang?.toLowerCase().startsWith('zh') === true ? 'zh' : 'en';
373
+ }
374
+ function parsePreview(text) {
375
+ const lines = text.split('\n');
376
+ const header = previewHeaderOf(lines[0] ?? '');
377
+ if (!header)
378
+ return undefined;
379
+ const divider = lines.findIndex((line, index) => index > 0 && isDivider(line));
380
+ if (divider < 2)
381
+ return undefined;
382
+ const command = RUN_COMMAND.exec(text);
383
+ const targetPreset = command?.[1] ?? header.targetPreset;
384
+ if (!targetPreset)
385
+ return undefined;
386
+ const tail = lines.slice(divider + 1);
387
+ const stats = tail.find((line) => line.trim() !== '' && !line.startsWith('⚠') && !RUN_COMMAND.test(line)) ?? '';
388
+ const warnings = tail
389
+ .filter((line) => line.startsWith('⚠'))
390
+ .map((line) => line.replace(/^⚠\s*/u, ''));
391
+ const previewIdLine = tail.find((line) => line.startsWith('Preview ID:') || line.startsWith('预览 ID:'));
392
+ const previewId = previewIdLine?.slice(previewIdLine.indexOf(previewIdLine.startsWith('Preview') ? ':' : ':') + 1).trim();
393
+ const fileLine = tail.find((line) => line.includes(' --file '));
394
+ const fileMarker = fileLine?.lastIndexOf(' --file ') ?? -1;
395
+ const summaryFile = fileMarker < 0 ? undefined : fileLine?.slice(fileMarker + ' --file '.length).trim().split(/\s/u)[0];
396
+ return {
397
+ phase: 'preview',
398
+ lang: header.lang,
399
+ sourcePreset: header.sourcePreset,
400
+ targetPreset,
401
+ ...(previewId ? { previewId } : {}),
402
+ summary: lines.slice(1, divider).join('\n').trim(),
403
+ ...(summaryFile ? { summaryFile } : {}),
404
+ stats,
405
+ warnings,
406
+ };
407
+ }
408
+ function parseMigrated(text) {
409
+ const lines = text.split('\n').map((line) => line.trim()).filter(Boolean);
410
+ const first = lines[0] ?? '';
411
+ const lang = first.startsWith('Created a new session') ? 'en' : 'zh';
412
+ const presetPrefix = lang === 'en' ? 'Created a new session in the ' : '已在 ';
413
+ const presetSuffix = lang === 'en' ? ' preset' : ' 模式下建好新会话';
414
+ const presetEnd = first.indexOf(presetSuffix, presetPrefix.length);
415
+ const preset = presetEnd < 0 ? undefined : first.slice(presetPrefix.length, presetEnd).trim();
416
+ if (!preset)
417
+ return undefined;
418
+ const targetIndex = lines.findIndex((line) => line.startsWith('Target session:') || line.startsWith('目标会话:'));
419
+ if (targetIndex < 0)
420
+ return undefined;
421
+ const targetLine = lines[targetIndex] ?? '';
422
+ const targetPrefix = targetLine.startsWith('Target session:') ? 'Target session:' : '目标会话:';
423
+ const targetPayload = targetLine.slice(targetPrefix.length).trim();
424
+ const targetSeparator = targetPayload.lastIndexOf(' · ');
425
+ if (targetSeparator < 1)
426
+ return undefined;
427
+ const title = targetPayload.slice(0, targetSeparator).trim();
428
+ const sessionId = targetPayload.slice(targetSeparator + 3).trim();
429
+ if (!title || !sessionId || /\s/u.test(sessionId))
430
+ return undefined;
431
+ const remaining = lines.filter((_, index) => index !== 0 && index !== targetIndex);
432
+ return {
433
+ phase: 'migrated',
434
+ lang,
435
+ targetPreset: preset,
436
+ title,
437
+ sessionId,
438
+ details: remaining.filter((line) => !line.startsWith('⚠')),
439
+ warnings: remaining.filter((line) => line.startsWith('⚠')).map((line) => line.replace(/^⚠\s*/u, '')),
440
+ };
441
+ }
442
+ /** Convert one durable `/bridge` outcome into the native card's view model. */
443
+ export function parseBridgeCard(outcome) {
444
+ if (outcome === null)
445
+ return { phase: 'running' };
446
+ const text = outcome.text?.trim() ?? '';
447
+ if (outcome.kind === 'error')
448
+ return { phase: 'error', text };
449
+ return parsePreview(text)
450
+ ?? parseMigrated(text)
451
+ ?? { phase: 'message', text, lang: languageOf(text) };
452
+ }
453
+ /** Return a value only when the complete editor document is valid JSON. */
454
+ export function parseJsonDocument(text) {
455
+ const trimmed = text.trim();
456
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('['))
457
+ return undefined;
458
+ try {
459
+ const parsed = JSON.parse(trimmed);
460
+ return typeof parsed === 'object' && parsed !== null ? parsed : undefined;
461
+ }
462
+ catch {
463
+ return undefined;
464
+ }
465
+ }
466
+ function encodeUtf8Base64Url(text) {
467
+ const bytes = new TextEncoder().encode(text);
468
+ let binary = '';
469
+ for (const byte of bytes)
470
+ binary += String.fromCharCode(byte);
471
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '');
472
+ }
473
+ /** Build the hidden-input-safe command used by the native editor confirmation. */
474
+ export function buildBridgeMigrationCommand(targetPreset, summary, lang, previewId) {
475
+ if (!/^[A-Za-z0-9._-]+$/u.test(targetPreset))
476
+ throw new Error('Unsupported target preset id');
477
+ if (lang !== 'zh' && lang !== 'en')
478
+ throw new Error('Unsupported Bridge language');
479
+ if (!/^[A-Za-z0-9-]{8,}$/u.test(previewId))
480
+ throw new Error('Unsupported Bridge preview ID');
481
+ if (!summary.trim())
482
+ throw new Error('The handoff summary is empty');
483
+ if (summary.length > MAX_EDITED_SUMMARY_CHARS) {
484
+ throw new Error(`The handoff summary exceeds ${MAX_EDITED_SUMMARY_CHARS} characters`);
485
+ }
486
+ return `/bridge ${targetPreset} --go --lang ${lang} --preview-id ${previewId} --summary64 ${encodeUtf8Base64Url(summary)}`;
487
+ }
@@ -0,0 +1,16 @@
1
+ /** Official WebUI half: one native `/bridge` command card, not a second WebUI. */
2
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
3
+ import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client';
4
+ import type { CommandRowProps } from '@deepseek-ai/dsh-client-ui-chat/client';
5
+ import { type BridgeOutcome } from './client-contract.ts';
6
+ interface BridgeInjected {
7
+ readonly execute: (sessionId: SessionId, line: string) => Promise<BridgeOutcome>;
8
+ readonly openSession: (sessionId: SessionId) => Promise<void>;
9
+ }
10
+ type BridgeCommandCardProps = CommandRowProps & BridgeInjected;
11
+ /** Rich renderer for the durable command lifecycle keyed by name and isolated from every other plugin. */
12
+ export declare function BridgeCommandCard(props: BridgeCommandCardProps): import("react").JSX.Element;
13
+ /** Client services are supplied by the official WebUI module table. */
14
+ export declare const inject: string[];
15
+ export declare function apply(ctx: ClientContext): void;
16
+ export {};