@changfenhuang/dsh-genui 0.11.3 → 0.12.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.
- package/CHANGELOG.md +44 -0
- package/README.md +21 -6
- package/README.zh-CN.md +21 -6
- package/SKILL.md +18 -6
- package/lib/assets/mermaid.js +325 -325
- package/lib/assets/standalone-runtime.js +10 -9
- package/lib/client.js +31 -33
- package/lib/index.js +1060 -371
- package/lib/types/client/blocks/forms.d.ts +5 -6
- package/lib/types/client/blocks/state.d.ts +2 -11
- package/lib/types/client/dom-fence.d.ts +2 -1
- package/lib/types/client/file-address.d.ts +4 -0
- package/lib/types/client/file-link-context.d.ts +41 -0
- package/lib/types/client/file-link.d.ts +17 -0
- package/lib/types/client/i18n/en.d.ts +5 -1
- package/lib/types/client/inline.d.ts +1 -1
- package/lib/types/client/mermaid-colors.d.ts +2 -0
- package/lib/types/client/panel.d.ts +6 -1
- package/lib/types/client/spec.d.ts +8 -11
- package/lib/types/client/submission-registry.d.ts +51 -0
- package/lib/types/client/table-details.d.ts +20 -0
- package/lib/types/client/walk-spec.d.ts +3 -0
- package/lib/types/plugin/fence-feedback.d.ts +55 -12
- package/lib/types/plugin/index.d.ts +7 -3
- package/lib/types/shared/fence-repair.d.ts +19 -7
- package/package.json +1 -12
- package/src/client/GenuiBlock.module.css +100 -4
- package/src/client/GenuiBlock.tsx +7 -14
- package/src/client/artifact/ArtifactExport.module.css +34 -3
- package/src/client/artifact/ExportableGenuiBlock.tsx +23 -2
- package/src/client/blocks/charts.tsx +63 -14
- package/src/client/blocks/forms.tsx +26 -48
- package/src/client/blocks/render-node.tsx +11 -8
- package/src/client/blocks/state.ts +2 -12
- package/src/client/dom-fence.tsx +190 -50
- package/src/client/file-address.ts +37 -0
- package/src/client/file-link-context.tsx +114 -0
- package/src/client/file-link.ts +46 -0
- package/src/client/genui-runtime/diagnostics.ts +20 -0
- package/src/client/genui-runtime/normalize.ts +28 -9
- package/src/client/guard.ts +98 -153
- package/src/client/i18n/en.ts +5 -1
- package/src/client/i18n/zh.ts +5 -1
- package/src/client/index.tsx +23 -5
- package/src/client/inline.ts +28 -10
- package/src/client/mermaid-colors.ts +39 -0
- package/src/client/mermaid-core.ts +7 -3
- package/src/client/panel.tsx +18 -5
- package/src/client/spec.ts +8 -11
- package/src/client/submission-registry.ts +150 -0
- package/src/client/table-details.ts +151 -0
- package/src/client/walk-spec.ts +43 -0
- package/src/plugin/fence-feedback.ts +384 -53
- package/src/plugin/index.ts +20 -9
- package/src/plugin/tool.ts +8 -2
- package/src/shared/fence-repair.ts +345 -128
- package/tsdown.config.ts +3 -7
- package/lib/invariant.js +0 -19
- package/lib/types/plugin/invariant.d.ts +0 -15
- package/src/plugin/invariant.ts +0 -30
|
@@ -9,6 +9,8 @@ import * as primitives from '../primitive-adapter.ts'
|
|
|
9
9
|
import css from '../GenuiBlock.module.css'
|
|
10
10
|
import { GENUI_LIMITS } from '../genui-runtime/index.ts'
|
|
11
11
|
import { renderInline } from '../inline.ts'
|
|
12
|
+
import { classifyLinkTarget } from '../file-link.ts'
|
|
13
|
+
import { FileLink } from '../file-link-context.tsx'
|
|
12
14
|
import { t } from '../i18n/index.ts'
|
|
13
15
|
import type { GenuiList, GenuiNode } from '../spec.ts'
|
|
14
16
|
import type { AnswersState, GenuiBlockProps } from './state.ts'
|
|
@@ -283,13 +285,14 @@ export function renderNode(
|
|
|
283
285
|
case 'select': return <SelectNode key={key} node={node} onAction={onAction} answers={answers} />
|
|
284
286
|
case 'checkbox': return <CheckboxNode key={key} node={node} onAction={onAction} answers={answers} />
|
|
285
287
|
case 'link': {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
288
|
+
const target = classifyLinkTarget(node.href)
|
|
289
|
+
if (target.type === 'external') {
|
|
290
|
+
return <a key={key} className={css.link} href={target.href} target="_blank" rel="noopener noreferrer">{renderInline(node.label, false)}</a>
|
|
291
|
+
}
|
|
292
|
+
if (target.type === 'file') {
|
|
293
|
+
return <FileLink key={key} path={target.file.path} label={node.label} {...(target.file.line === undefined ? {} : { line: target.file.line })}>{renderInline(node.label, false)}</FileLink>
|
|
294
|
+
}
|
|
295
|
+
return <span key={key} className={css.linkText}>{renderInline(node.label, false)}</span>
|
|
293
296
|
}
|
|
294
297
|
case 'image': return <ImageNode key={`${key}:${node.src}`} node={node} />
|
|
295
298
|
case 'audio': return <AudioNode key={`${key}:${node.src}`} node={node} />
|
|
@@ -299,7 +302,7 @@ export function renderNode(
|
|
|
299
302
|
return (
|
|
300
303
|
<span key={key} className={`${css.badge} ${css[tone] || ''}`}>
|
|
301
304
|
{node.icon !== undefined && <span aria-hidden>{node.icon} </span>}
|
|
302
|
-
{renderInline(node.label, false)}
|
|
305
|
+
<span className={css.badgeLabel}>{renderInline(node.label, false)}</span>
|
|
303
306
|
</span>
|
|
304
307
|
)
|
|
305
308
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { GenuiSpec } from '../spec.ts'
|
|
9
9
|
import type { BlockInteractionState } from '../interaction-store.ts'
|
|
10
|
+
import type { SubmissionRegistry } from '../submission-registry.ts'
|
|
10
11
|
|
|
11
12
|
export interface GenuiBlockProps {
|
|
12
13
|
/** Parsed spec to render. */
|
|
@@ -35,16 +36,6 @@ export interface GenuiBlockProps {
|
|
|
35
36
|
onStateSnapshot?: ((state: BlockInteractionState) => void) | undefined
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
/** Per-question metadata registered by grouped radios for local grading. */
|
|
39
|
-
export interface QuestionMeta {
|
|
40
|
-
label: string
|
|
41
|
-
options: string[]
|
|
42
|
-
/** Correct option: index (number) or label (string); absent = no local grading. */
|
|
43
|
-
answer?: number | string | undefined
|
|
44
|
-
/** Shown after local grading. */
|
|
45
|
-
explanation?: string | undefined
|
|
46
|
-
}
|
|
47
|
-
|
|
48
39
|
/** Block-wide aggregation state. Grouped radios store one selected label in
|
|
49
40
|
* `answers`; grouped checkboxes store a selected-label array in
|
|
50
41
|
* `multiAnswers`. Keeping the registries separate preserves the existing
|
|
@@ -60,7 +51,7 @@ export interface AnswersState {
|
|
|
60
51
|
fields: Record<string, string>
|
|
61
52
|
/** Field ids whose value must never be persisted or collected (secrets). */
|
|
62
53
|
secretFields: ReadonlySet<string>
|
|
63
|
-
|
|
54
|
+
registry: SubmissionRegistry
|
|
64
55
|
/** True after a local grading: questions are locked until 重新作答. */
|
|
65
56
|
locked: boolean
|
|
66
57
|
/** Bumped by every reset; radios use it as their remount key. */
|
|
@@ -69,7 +60,6 @@ export interface AnswersState {
|
|
|
69
60
|
setMultiAnswer: (group: string, choice: string, checked: boolean) => void
|
|
70
61
|
setField: (id: string, value: string) => void
|
|
71
62
|
registerSecretField: (id: string) => void
|
|
72
|
-
registerMeta: (group: string, meta: QuestionMeta) => void
|
|
73
63
|
clear: () => void
|
|
74
64
|
setLocked: (locked: boolean) => void
|
|
75
65
|
}
|
package/src/client/dom-fence.tsx
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Stock DSH renders every fenced code block through the shared CodeBlock
|
|
5
5
|
* surface (stable class `md-code-block`, language label rendered as the
|
|
6
6
|
* banner's childless label div). This channel observes the conversation DOM,
|
|
7
|
-
* finds blocks labelled `dsh-ui
|
|
8
|
-
* plugin's own React tree next to
|
|
7
|
+
* finds blocks labelled `dsh-ui` or complete `<dsh-ui>…</dsh-ui>` text spans,
|
|
8
|
+
* parses the raw fence body and mounts the plugin's own React tree next to
|
|
9
|
+
* the hidden host element:
|
|
9
10
|
*
|
|
10
11
|
* Fence discovery is **multi-surface** (issue #6): besides `md-code-block`,
|
|
11
12
|
* the channel also matches the deepsuite-style surfaces some host builds
|
|
@@ -13,8 +14,9 @@
|
|
|
13
14
|
* structural backstop — ANY element whose banner labels it `dsh-ui` and
|
|
14
15
|
* which contains a `<pre>` body. The only invariants are the language label
|
|
15
16
|
* (a leaf element with the exact text `dsh-ui`, outside the code body) and
|
|
16
|
-
* the `<pre
|
|
17
|
-
*
|
|
17
|
+
* the `<pre>`. Desktop 0.11.0 instead emits a plain-text tag span for the
|
|
18
|
+
* same fence; the DOM channel recognizes that exact wrapper in assistant
|
|
19
|
+
* messages:
|
|
18
20
|
*
|
|
19
21
|
* - **Streaming takeover**: the channel takes over a dsh-ui block as soon as
|
|
20
22
|
* ONE finished component parses (the partial parser), and re-renders the
|
|
@@ -46,7 +48,7 @@
|
|
|
46
48
|
*
|
|
47
49
|
* Security posture matches the registry channel: only code shipped in this
|
|
48
50
|
* plugin's browser bundle mounts React roots, the model can only author
|
|
49
|
-
* fence text, and unrepairable bodies
|
|
51
|
+
* fence text, and unrepairable bodies remain visible in their host form.
|
|
50
52
|
*/
|
|
51
53
|
import { Fragment, isValidElement, type Key, type ReactNode } from 'react'
|
|
52
54
|
import { createRoot, type Root } from 'react-dom/client'
|
|
@@ -56,8 +58,10 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
|
56
58
|
import type { ChatNode, ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client'
|
|
57
59
|
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
|
58
60
|
import { GenuiActionContext, type GenuiActionHandler } from './action-context.ts'
|
|
61
|
+
import { withFileLinkContext, type FileLinkAvailability, type FileLinkCordisContext } from './file-link-context.tsx'
|
|
59
62
|
import css from './GenuiBlock.module.css'
|
|
60
63
|
import { renderSvgFence } from './svg-fence.tsx'
|
|
64
|
+
import { repairFenceJson, trimToBalancedRoot } from '../shared/fence-repair.ts'
|
|
61
65
|
import { describeFenceFailure, FenceDiagnostic, renderResolvedFenceNode, type GenuiFenceContext } from './fence-render.tsx'
|
|
62
66
|
import { resolveViewedSessionId } from './session-resolver.ts'
|
|
63
67
|
import { validateCanonicalGenuiSpec } from './guard.ts'
|
|
@@ -65,13 +69,27 @@ import { diagnoseUnknownGenuiFields } from './genui-runtime/diagnostics.ts'
|
|
|
65
69
|
import { normalizeGenuiSpec } from './genui-runtime/normalize.ts'
|
|
66
70
|
import { sourceFencesOfAssistant, sourceLanguageAt } from './source-fence.ts'
|
|
67
71
|
|
|
68
|
-
/**
|
|
69
|
-
*
|
|
70
|
-
* (`.md-code-block`) and the deepsuite-style surfaces some host builds
|
|
71
|
-
* render instead (`.code-block` / `.code-block-small`). Surfaces with an
|
|
72
|
-
* unlisted class are still found by the structural backstop (label + `<pre>`),
|
|
73
|
-
* so this list is an optimization, not a hard contract. */
|
|
72
|
+
/** 优先用宿主代码块内容的语义标记定位包装节点;隐藏语言或改变类名不能阻断
|
|
73
|
+
* 原始消息的围栏识别。类名也用于仍以这些表面呈现的代码块。 */
|
|
74
74
|
const CODE_BLOCK_SELECTORS = '.md-code-block, .code-block, .code-block-small'
|
|
75
|
+
|
|
76
|
+
/** 在同一处收集代码块包装节点,供候选扫描和原始围栏序号共用。 */
|
|
77
|
+
function codeBlockSurfaces(scope: ParentNode): HTMLElement[] {
|
|
78
|
+
const surfaces = new Set(scope.querySelectorAll<HTMLElement>(CODE_BLOCK_SELECTORS))
|
|
79
|
+
for (const content of scope.querySelectorAll<HTMLElement>('[data-code-block-content]')) {
|
|
80
|
+
if (content.parentElement !== null) surfaces.add(content.parentElement)
|
|
81
|
+
}
|
|
82
|
+
return [...surfaces].sort((left, right) => left.compareDocumentPosition(right) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 返回拥有代码内容语义节点、或已知代码块类名的最近包装节点。 */
|
|
86
|
+
function closestCodeBlockSurface(element: Element | null): Element | null {
|
|
87
|
+
for (let current = element; current !== null; current = current.parentElement) {
|
|
88
|
+
if (current.matches(CODE_BLOCK_SELECTORS)
|
|
89
|
+
|| [...current.children].some(child => child.hasAttribute('data-code-block-content'))) return current
|
|
90
|
+
}
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
75
93
|
/** Marker attribute set on blocks this channel has taken over. */
|
|
76
94
|
const PROCESSED = 'data-genui-rendered'
|
|
77
95
|
/** The settled marker on AssistantMarkdown (absent = settled). */
|
|
@@ -83,6 +101,10 @@ const DIAGNOSTIC_CLASS = 'genui-dom-fence-diagnostic'
|
|
|
83
101
|
/** Slow sweep interval: the observer catches everything, this is the 1s
|
|
84
102
|
* belt-and-braces pass (history loads, missed attribute batches). */
|
|
85
103
|
const SWEEP_MS = 1000
|
|
104
|
+
const NO_FILE_LINK_AVAILABILITY: FileLinkAvailability = {
|
|
105
|
+
subscribe: () => () => {},
|
|
106
|
+
getSnapshot: () => false,
|
|
107
|
+
}
|
|
86
108
|
|
|
87
109
|
/** Max ancestors walked from a `<pre>` to its fence surface root (banner +
|
|
88
110
|
* pre holder). Most hosts put the pre directly under the surface; some wrap
|
|
@@ -161,6 +183,18 @@ function isTextNode(node: Node): node is Text {
|
|
|
161
183
|
return node.nodeType === Node.TEXT_NODE
|
|
162
184
|
}
|
|
163
185
|
|
|
186
|
+
/** 读取 DSH Desktop 0.11.0 将 dsh-ui 围栏呈现为纯文本标签时的正文。 */
|
|
187
|
+
function tagTextBodyOf(block: Element): string | null {
|
|
188
|
+
if (block.tagName !== 'SPAN' || block.childElementCount !== 0) return null
|
|
189
|
+
const flowKind = block.closest('[data-chat-flow-kind]')?.getAttribute('data-chat-flow-kind')
|
|
190
|
+
if (flowKind !== undefined && flowKind !== null && flowKind !== 'assistant-step') return null
|
|
191
|
+
const text = block.textContent?.trim() ?? ''
|
|
192
|
+
const opening = '<dsh-ui>'
|
|
193
|
+
const closing = '</dsh-ui>'
|
|
194
|
+
if (!text.startsWith(opening) || !text.endsWith(closing)) return null
|
|
195
|
+
return text.slice(opening.length, -closing.length).trim()
|
|
196
|
+
}
|
|
197
|
+
|
|
164
198
|
/** The banner's language label: a leaf element whose text is exactly the
|
|
165
199
|
* lang. CodeBlock renders the label as a childless div; deepsuite-style
|
|
166
200
|
* surfaces use a span; the ONLY structural invariants across hosts are "a
|
|
@@ -181,19 +215,46 @@ function infostringOf(block: Element): 'dsh-ui' | 'svg' | null {
|
|
|
181
215
|
// surface's banner, not `block`'s own banner. Only accept labels whose
|
|
182
216
|
// nearest known surface is `block` itself (or none — unknown surfaces
|
|
183
217
|
// stay supported by the structural backstop).
|
|
184
|
-
const owner = el
|
|
218
|
+
const owner = closestCodeBlockSurface(el)
|
|
185
219
|
if (owner !== null && owner !== block) continue
|
|
186
220
|
return lang
|
|
187
221
|
}
|
|
188
222
|
return null
|
|
189
223
|
}
|
|
190
224
|
|
|
191
|
-
|
|
225
|
+
/**
|
|
226
|
+
* 已知**真实语言标识符**的闭集:语言 id 有限、由本插件维护,白名单列它是对的。
|
|
227
|
+
* 反例——宿主各语言里通用代码块标题的叫法(`Code` / `代码块` / «Код» / Código /
|
|
228
|
+
* Codice / Kode …)——是**无界集合**,白名单永远列不全:旧实现 `GENERIC_CODE_LABELS`
|
|
229
|
+
* 只列了英/中两种,宿主 locale 为其它语言时本地化标题被当成真语言,围栏永远不被
|
|
230
|
+
* 接管(issue #258)。因此判定反转:标签命中本集合才视为「带语言」,其余(空标签、
|
|
231
|
+
* 本地化通用词)一律视为「无语言」,由 {@link isGenericGenuiFence} 的内容校验把关——
|
|
232
|
+
* 同一份正文的渲染结果不因宿主语言而不同。
|
|
233
|
+
*/
|
|
234
|
+
const KNOWN_LANGUAGE_LABELS = new Set([
|
|
235
|
+
// 插件自有语言
|
|
236
|
+
'dsh-ui', 'svg',
|
|
237
|
+
// 数据 / 标记 / 纯文本
|
|
238
|
+
'json', 'json5', 'yaml', 'yml', 'toml', 'ini', 'xml', 'html', 'css', 'scss', 'sass', 'less',
|
|
239
|
+
'markdown', 'md', 'text', 'txt', 'plain', 'plaintext', 'log', 'console', 'diff', 'patch',
|
|
240
|
+
// 助手回复里常见的编程语言
|
|
241
|
+
'js', 'jsx', 'javascript', 'ts', 'tsx', 'typescript', 'python', 'py', 'bash', 'sh', 'shell', 'zsh',
|
|
242
|
+
'sql', 'graphql', 'gql', 'rust', 'go', 'golang', 'java', 'kotlin', 'kt', 'swift', 'scala', 'dart',
|
|
243
|
+
'c', 'h', 'cpp', 'c++', 'cxx', 'cs', 'csharp', 'php', 'ruby', 'rb', 'perl', 'lua', 'r', 'matlab',
|
|
244
|
+
'haskell', 'hs', 'elixir', 'ex', 'erlang', 'clojure', 'clj', 'groovy', 'objc', 'objective-c',
|
|
245
|
+
'dockerfile', 'docker', 'makefile', 'make', 'cmake', 'protobuf', 'proto',
|
|
246
|
+
])
|
|
192
247
|
|
|
193
|
-
/**
|
|
248
|
+
/**
|
|
249
|
+
* Read a language that the host still exposes in its CodeBlock banner.
|
|
250
|
+
*
|
|
251
|
+
* 只有标签命中 {@link KNOWN_LANGUAGE_LABELS} 才返回语言;空标签与本地化通用词
|
|
252
|
+
* («Код» / Código / 代码块 …)是宿主的**呈现文案**、不是语义,一律返回 null——
|
|
253
|
+
* 此前它们会被当成真语言,连 ChatSnapshot 来源都被短路(issue #258)。
|
|
254
|
+
*/
|
|
194
255
|
function domLanguageOf(block: Element): string | null {
|
|
195
256
|
const label = labelTextOf(block)
|
|
196
|
-
return label
|
|
257
|
+
return label !== '' && KNOWN_LANGUAGE_LABELS.has(label.toLowerCase()) ? label : null
|
|
197
258
|
}
|
|
198
259
|
|
|
199
260
|
/** The banner label's raw text (empty while streaming — the host renders the
|
|
@@ -210,7 +271,18 @@ function labelTextOf(block: Element): string {
|
|
|
210
271
|
return ''
|
|
211
272
|
}
|
|
212
273
|
|
|
213
|
-
/**
|
|
274
|
+
/**
|
|
275
|
+
* 仅在通用 CodeBlock 的内容经过**与带标签路径相同的标点级修复**后仍是合规 GenUI
|
|
276
|
+
* 规范时,恢复丢失的围栏语言。
|
|
277
|
+
*
|
|
278
|
+
* 宿主会隐去它不认识的语言(高亮器不支持 `dsh-ui`),于是同一份围栏在 DOM 里表现为
|
|
279
|
+
* 一个通用标题的普通代码块(标题随宿主 locale 本地化:`Code` / `代码块` / «Код» …);
|
|
280
|
+
* ChatSnapshot 的语言来源在部分行上不可用
|
|
281
|
+
* 时,内容识别是唯一出路。此前这里要求 `JSON.parse(raw)` 直接通过,导致**正文只差一个
|
|
282
|
+
* 未转义引号(tier-1 能修)的围栏永远不会被接管**——用户看到一个能渲染却始终是代码块的
|
|
283
|
+
* 围栏(真实会话 seq 40530:正文经 tier-1 修 14 处后可渲染,界面却停在代码块)。
|
|
284
|
+
* 结构级修复(tier-2)刻意不参与:内容识别是兜底,不该接管只是"长得像 JSON"的普通代码。
|
|
285
|
+
|
|
214
286
|
*
|
|
215
287
|
* @param block - 宿主提供的代码块元素。
|
|
216
288
|
* @param raw - 未修改的围栏正文。
|
|
@@ -219,20 +291,33 @@ function labelTextOf(block: Element): string {
|
|
|
219
291
|
function isGenericGenuiFence(block: Element, raw: string): boolean {
|
|
220
292
|
const row = block.closest<HTMLElement>(ASSISTANT_FLOW_ROW)
|
|
221
293
|
if (row === null || row.dataset.chatGroupPart === 'reasoning') return false
|
|
294
|
+
// domLanguageOf 已反转(#258):banner 是已知真实语言 ⇒ 不是通用块;空标签或
|
|
295
|
+
// 本地化通用词 ⇒ null,进入内容校验。这里不再要求标题命中任何白名单。
|
|
222
296
|
if (domLanguageOf(block) !== null || !block.querySelector('[data-code-block-banner]')) return false
|
|
223
|
-
|
|
297
|
+
const repaired = repairFenceJson(raw)
|
|
298
|
+
const candidate = repaired === null ? raw : repaired.text
|
|
224
299
|
let value: unknown
|
|
225
300
|
try {
|
|
226
|
-
value = JSON.parse(
|
|
301
|
+
value = JSON.parse(candidate)
|
|
227
302
|
} catch {
|
|
228
|
-
|
|
303
|
+
// 「合法 JSON + 尾部杂字符」(真实样本:模型把工具调用模板泄漏在 JSON 之后,
|
|
304
|
+
// 而且围栏没闭合)同样要能认出来:裁到平衡根值再试一次。只裁剪、不补全结构。
|
|
305
|
+
const trimmed = trimToBalancedRoot(candidate)
|
|
306
|
+
if (trimmed === null) return false
|
|
307
|
+
try {
|
|
308
|
+
value = JSON.parse(trimmed)
|
|
309
|
+
} catch {
|
|
310
|
+
return false
|
|
311
|
+
}
|
|
229
312
|
}
|
|
230
313
|
if (!validateCanonicalGenuiSpec(value).ok || diagnoseUnknownGenuiFields(value).length > 0) return false
|
|
231
314
|
return JSON.stringify(normalizeGenuiSpec(value).value) === JSON.stringify(value)
|
|
232
315
|
}
|
|
233
316
|
|
|
234
|
-
/**
|
|
317
|
+
/** 读取宿主代码块或纯文本标签中的围栏正文。 */
|
|
235
318
|
function rawOf(block: Element): string {
|
|
319
|
+
const tagTextBody = tagTextBodyOf(block)
|
|
320
|
+
if (tagTextBody !== null) return tagTextBody
|
|
236
321
|
const pre = block.querySelector('pre')
|
|
237
322
|
if (pre === null) return ''
|
|
238
323
|
let text = ''
|
|
@@ -274,7 +359,7 @@ function implausibleLabeledAncestorOf(pre: HTMLElement, scope: ParentNode = docu
|
|
|
274
359
|
}
|
|
275
360
|
|
|
276
361
|
/**
|
|
277
|
-
* Every dsh-ui fence surface under `scope`,
|
|
362
|
+
* Every dsh-ui fence surface or complete tag-text span under `scope`, in DOM order.
|
|
278
363
|
* Known surface classes first (cheap, ordered), then a structural sweep —
|
|
279
364
|
* every `<pre>` whose banner labels it `dsh-ui` — so a host with an
|
|
280
365
|
* unlisted surface shape still renders. The label + `<pre>` gates make the
|
|
@@ -284,11 +369,11 @@ function implausibleLabeledAncestorOf(pre: HTMLElement, scope: ParentNode = docu
|
|
|
284
369
|
function findFenceCandidates(scope: ParentNode = document): HTMLElement[] {
|
|
285
370
|
const seen = new Set<HTMLElement>()
|
|
286
371
|
const out: HTMLElement[] = []
|
|
287
|
-
for (const el of scope
|
|
372
|
+
for (const el of codeBlockSurfaces(scope)) {
|
|
288
373
|
// Modifier classes can sit inside a surface (e.g. a `code-block-small`
|
|
289
374
|
// child of `code-block`): only the outermost matching element is a
|
|
290
375
|
// candidate, so a fence is never double-counted or taken over twice.
|
|
291
|
-
if (el.parentElement
|
|
376
|
+
if (closestCodeBlockSurface(el.parentElement) !== null) continue
|
|
292
377
|
if (el.closest(`.${CONTAINER_CLASS}, .${DIAGNOSTIC_CLASS}, [data-genui-svg-fence]`) !== null) continue
|
|
293
378
|
if (seen.has(el)) continue
|
|
294
379
|
// Message-level containers that happen to carry a surface class must
|
|
@@ -307,7 +392,8 @@ function findFenceCandidates(scope: ParentNode = document): HTMLElement[] {
|
|
|
307
392
|
// markdown root holding both a dsh-ui fence and a python block — and
|
|
308
393
|
// the backstop would mislabel that whole container as a fence, hiding
|
|
309
394
|
// every other code block with it (issue #13).
|
|
310
|
-
if (pre
|
|
395
|
+
if (closestCodeBlockSurface(pre) !== null
|
|
396
|
+
|| pre.closest(`.${CONTAINER_CLASS}, .${DIAGNOSTIC_CLASS}, [data-genui-svg-fence]`) !== null) continue
|
|
311
397
|
const surface = surfaceOf(pre, scope)
|
|
312
398
|
if (surface === null) {
|
|
313
399
|
// Diagnose the issue #19 guard: a labeled ancestor that is NOT a code
|
|
@@ -327,7 +413,13 @@ function findFenceCandidates(scope: ParentNode = document): HTMLElement[] {
|
|
|
327
413
|
out.push(surface)
|
|
328
414
|
seen.add(surface)
|
|
329
415
|
}
|
|
330
|
-
|
|
416
|
+
for (const span of scope.querySelectorAll<HTMLElement>('span')) {
|
|
417
|
+
if (closestCodeBlockSurface(span) !== null
|
|
418
|
+
|| span.closest(`.${CONTAINER_CLASS}, .${DIAGNOSTIC_CLASS}, [data-chat-group-part="reasoning"], [data-tool], [data-sidebar-chat], [data-sidebar-right-session], [data-sidebar-right-panel], [data-panel-conversation], [data-plugin-panel]`) !== null) continue
|
|
419
|
+
if (tagTextBodyOf(span) === null) continue
|
|
420
|
+
out.push(span)
|
|
421
|
+
}
|
|
422
|
+
return out.sort((left, right) => left.compareDocumentPosition(right) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1)
|
|
331
423
|
}
|
|
332
424
|
|
|
333
425
|
/** One-time-per-install drift diagnostic flag (reset per install, so tests
|
|
@@ -383,7 +475,7 @@ function fenceIndexOf(ctx: Context, row: Element, block: Element, sourceLanguage
|
|
|
383
475
|
let fallbackIndex = 0
|
|
384
476
|
for (const candidate of findFenceCandidates(scope)) {
|
|
385
477
|
if (candidate.closest(STREAMING) !== null) continue
|
|
386
|
-
const language = domLanguageOf(candidate) ?? sourceLanguage(candidate)
|
|
478
|
+
const language = tagTextBodyOf(candidate) !== null ? 'dsh-ui' : domLanguageOf(candidate) ?? sourceLanguage(candidate)
|
|
387
479
|
if (language !== 'dsh-ui' && !(language === undefined && isGenericGenuiFence(candidate, rawOf(candidate)))) continue
|
|
388
480
|
fallbackIndex += 1
|
|
389
481
|
if (candidate === block) return fallbackIndex
|
|
@@ -391,9 +483,11 @@ function fenceIndexOf(ctx: Context, row: Element, block: Element, sourceLanguage
|
|
|
391
483
|
return fallbackIndex + 1
|
|
392
484
|
}
|
|
393
485
|
let index = 0
|
|
394
|
-
for (const candidate of
|
|
486
|
+
for (const candidate of findFenceCandidates(row)) {
|
|
395
487
|
if (candidate.closest(STREAMING) !== null) continue
|
|
396
|
-
|
|
488
|
+
if (candidate.closest(ASSISTANT_FLOW_ROW) !== row) continue
|
|
489
|
+
if (candidate.closest('[data-chat-group-part="reasoning"]') !== null) continue
|
|
490
|
+
const language = tagTextBodyOf(candidate) !== null ? 'dsh-ui' : domLanguageOf(candidate) ?? sourceLanguage(candidate)
|
|
397
491
|
if (language !== 'dsh-ui' && !(language === undefined && isGenericGenuiFence(candidate, rawOf(candidate)))) continue
|
|
398
492
|
index += 1
|
|
399
493
|
if (candidate === block) return index
|
|
@@ -403,8 +497,8 @@ function fenceIndexOf(ctx: Context, row: Element, block: Element, sourceLanguage
|
|
|
403
497
|
|
|
404
498
|
/** Return outer host Markdown code surfaces in one assistant row's DOM order. */
|
|
405
499
|
function hostFenceBlocksOf(row: Element): HTMLElement[] {
|
|
406
|
-
return
|
|
407
|
-
if (candidate.parentElement
|
|
500
|
+
return codeBlockSurfaces(row).filter(candidate => {
|
|
501
|
+
if (closestCodeBlockSurface(candidate.parentElement) !== null) return false
|
|
408
502
|
if (candidate.closest(`.${CONTAINER_CLASS}, .${DIAGNOSTIC_CLASS}, [data-genui], [data-genui-svg-fence]`) !== null) return false
|
|
409
503
|
if (candidate.closest('[data-chat-group-part="reasoning"], [data-tool], [data-sidebar-chat], [data-sidebar-right-session], [data-sidebar-right-panel], [data-panel-conversation], [data-plugin-panel]') !== null) return false
|
|
410
504
|
if (candidate.closest(ASSISTANT_FLOW_ROW) !== row) return false
|
|
@@ -431,13 +525,18 @@ export function sourceLanguageOf(ctx: Context, block: Element): string | null |
|
|
|
431
525
|
}
|
|
432
526
|
|
|
433
527
|
/** Cache each row's Markdown parse for one sweep and retain confirmed opening-line languages per host block. */
|
|
434
|
-
function createSourceLanguageResolver(ctx: Context): {
|
|
528
|
+
function createSourceLanguageResolver(ctx: Context): {
|
|
529
|
+
beginSweep: () => void
|
|
530
|
+
get: (block: Element) => string | null | undefined
|
|
531
|
+
isSessionUnavailable: () => boolean
|
|
532
|
+
} {
|
|
435
533
|
const stableLanguages = new WeakMap<Element, { sessionId: SessionId; nodeKey: string; language: string | null }>()
|
|
436
534
|
let sweepLanguages = new Map<Element, string | null | undefined>()
|
|
437
535
|
let parsedNodes = new Map<string, ReturnType<typeof sourceFencesOfAssistant>>()
|
|
438
536
|
let sessionId: SessionId | undefined
|
|
439
537
|
let chat: ChatSnapshot | undefined
|
|
440
538
|
let chatRead = false
|
|
539
|
+
let sessionUnavailable = false
|
|
441
540
|
|
|
442
541
|
return {
|
|
443
542
|
beginSweep() {
|
|
@@ -446,13 +545,23 @@ function createSourceLanguageResolver(ctx: Context): { beginSweep: () => void; g
|
|
|
446
545
|
sessionId = undefined
|
|
447
546
|
chat = undefined
|
|
448
547
|
chatRead = false
|
|
548
|
+
sessionUnavailable = false
|
|
549
|
+
},
|
|
550
|
+
isSessionUnavailable() {
|
|
551
|
+
return sessionUnavailable
|
|
449
552
|
},
|
|
450
553
|
get(block) {
|
|
451
554
|
if (sweepLanguages.has(block)) return sweepLanguages.get(block)
|
|
452
555
|
const row = block.closest<HTMLElement>(`${ASSISTANT_FLOW_ROW}[data-chat-node-key]`)
|
|
453
556
|
if (row === null || row.dataset.chatGroupPart === 'reasoning') return undefined
|
|
454
557
|
const nodeKey = row.dataset.chatNodeKey
|
|
455
|
-
const
|
|
558
|
+
const sourceSession = sourceSessionOf(ctx)
|
|
559
|
+
if (!sourceSession.available) {
|
|
560
|
+
sessionUnavailable = true
|
|
561
|
+
sweepLanguages.set(block, undefined)
|
|
562
|
+
return undefined
|
|
563
|
+
}
|
|
564
|
+
const activeSessionId = sourceSession.sessionId
|
|
456
565
|
if (!nodeKey || activeSessionId === undefined) return undefined
|
|
457
566
|
|
|
458
567
|
const stable = stableLanguages.get(block)
|
|
@@ -497,9 +606,19 @@ export function hostFenceIndexOf(row: Element, block: Element): number {
|
|
|
497
606
|
return hostFenceBlocksOf(row).indexOf(block as HTMLElement)
|
|
498
607
|
}
|
|
499
608
|
|
|
609
|
+
/** Read the viewed session and report when Cordis rejects access to its Context. */
|
|
610
|
+
function sourceSessionOf(ctx: Context): { available: true; sessionId: SessionId | undefined } | { available: false } {
|
|
611
|
+
try {
|
|
612
|
+
return { available: true, sessionId: resolveViewedSessionId(ctx.sessions.list.getSnapshot()) }
|
|
613
|
+
} catch {
|
|
614
|
+
return { available: false }
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
500
618
|
/** Read the currently viewed session for source Markdown lookup. */
|
|
501
619
|
function sessionIdOfForSource(ctx: Context): SessionId | undefined {
|
|
502
|
-
|
|
620
|
+
const sourceSession = sourceSessionOf(ctx)
|
|
621
|
+
return sourceSession.available ? sourceSession.sessionId : undefined
|
|
503
622
|
}
|
|
504
623
|
|
|
505
624
|
/** Read uiConversation as an optional service so Cordis does not require a hard inject. */
|
|
@@ -577,6 +696,7 @@ function anchorSeqOf(row: Element): number {
|
|
|
577
696
|
export function installDomFenceRenderer(
|
|
578
697
|
ctx: Context,
|
|
579
698
|
sendAction: (sessionId: SessionId, action: string, payload: Record<string, unknown>) => void,
|
|
699
|
+
fileLinkAvailability: FileLinkAvailability = NO_FILE_LINK_AVAILABILITY,
|
|
580
700
|
): () => void {
|
|
581
701
|
if (typeof document === 'undefined') return () => {}
|
|
582
702
|
driftWarned = false
|
|
@@ -592,12 +712,27 @@ export function installDomFenceRenderer(
|
|
|
592
712
|
let unsubscribeChat: (() => void) | undefined
|
|
593
713
|
const sourceLanguages = createSourceLanguageResolver(ctx)
|
|
594
714
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
715
|
+
/** Apply both plugin contexts to every actual GenUI root render. */
|
|
716
|
+
function wrapFenceNode(node: ReactNode, context: GenuiFenceContext, handler: GenuiActionHandler, streaming: boolean): ReactNode {
|
|
717
|
+
const actionTree = <GenuiActionContext.Provider value={handler}>{node}</GenuiActionContext.Provider>
|
|
718
|
+
return withFileLinkContext(
|
|
719
|
+
ctx as unknown as FileLinkCordisContext,
|
|
720
|
+
fileLinkAvailability,
|
|
721
|
+
context.sessionId,
|
|
722
|
+
streaming,
|
|
723
|
+
actionTree,
|
|
724
|
+
)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const sessionIdOf = (): SessionId | undefined => sessionIdOfForSource(ctx)
|
|
728
|
+
let lastAvailableSessionId = sessionIdOf()
|
|
729
|
+
|
|
730
|
+
/** Keep a mounted fence's React identity stable while Cordis temporarily denies session access. */
|
|
731
|
+
function sessionIdForRender(preserveWhenUnavailable: boolean): SessionId | undefined {
|
|
732
|
+
const sourceSession = sourceSessionOf(ctx)
|
|
733
|
+
if (!sourceSession.available) return preserveWhenUnavailable ? lastAvailableSessionId : undefined
|
|
734
|
+
lastAvailableSessionId = sourceSession.sessionId
|
|
735
|
+
return sourceSession.sessionId
|
|
601
736
|
}
|
|
602
737
|
|
|
603
738
|
/** Retry ChatSnapshot subscription while the active session binding is unavailable. */
|
|
@@ -615,7 +750,7 @@ export function installDomFenceRenderer(
|
|
|
615
750
|
/** Render context for a block: session always; the stable source identity
|
|
616
751
|
* only once settled — streaming renders are identity-less (no panel
|
|
617
752
|
* publish, no durable state), mirroring the registry channel. */
|
|
618
|
-
function contextOf(row: Element, block: Element, settled: boolean): { key: Key; context: GenuiFenceContext } {
|
|
753
|
+
function contextOf(row: Element, block: Element, settled: boolean, preserveSessionIdentity = false): { key: Key; context: GenuiFenceContext } {
|
|
619
754
|
if (settled && row.getAttribute('data-chat-anchor-key') === null) {
|
|
620
755
|
// Safari / fallback render path: the host omitted the row anchor (the
|
|
621
756
|
// attribute is a React key that React drops when undefined). Fences
|
|
@@ -626,7 +761,7 @@ export function installDomFenceRenderer(
|
|
|
626
761
|
const fenceIndex = fenceIndexOf(ctx, row, block, sourceLanguages.get)
|
|
627
762
|
const anchorKey = row.getAttribute('data-chat-anchor-key') ?? 'unknown'
|
|
628
763
|
const key = `dom:${anchorKey}:${fenceIndex}` as Key
|
|
629
|
-
const sessionId =
|
|
764
|
+
const sessionId = sessionIdForRender(preserveSessionIdentity)
|
|
630
765
|
const context: GenuiFenceContext = {
|
|
631
766
|
...(sessionId === undefined ? {} : { sessionId }),
|
|
632
767
|
...(settled ? { source: { id: key as string, order: [anchorSeqOf(row), 0, fenceIndex] as const } } : {}),
|
|
@@ -744,7 +879,7 @@ export function installDomFenceRenderer(
|
|
|
744
879
|
if (block.hasAttribute(PROCESSED)) return
|
|
745
880
|
const row = rowOf(block)
|
|
746
881
|
const settled = isSettled(block)
|
|
747
|
-
const domLanguage = domLanguageOf(block)
|
|
882
|
+
const domLanguage = tagTextBodyOf(block) !== null ? 'dsh-ui' : domLanguageOf(block)
|
|
748
883
|
const sourceLanguage = domLanguage === null ? sourceLanguages.get(block) : undefined
|
|
749
884
|
const language = domLanguage ?? sourceLanguage
|
|
750
885
|
const raw = rawOf(block)
|
|
@@ -795,7 +930,7 @@ export function installDomFenceRenderer(
|
|
|
795
930
|
}
|
|
796
931
|
try {
|
|
797
932
|
const handler: GenuiActionHandler = (action, payload) => sendActionForBlock(block, action, payload)
|
|
798
|
-
root.render(
|
|
933
|
+
root.render(wrapFenceNode(payload, context, handler, !settled))
|
|
799
934
|
} catch (error) {
|
|
800
935
|
try {
|
|
801
936
|
root.unmount()
|
|
@@ -868,7 +1003,9 @@ export function installDomFenceRenderer(
|
|
|
868
1003
|
function sweep(): void {
|
|
869
1004
|
if (disposed) return
|
|
870
1005
|
sourceLanguages.beginSweep()
|
|
871
|
-
const
|
|
1006
|
+
const sourceSession = sourceSessionOf(ctx)
|
|
1007
|
+
const sessionId = sourceSession.available ? sourceSession.sessionId : undefined
|
|
1008
|
+
if (sourceSession.available) lastAvailableSessionId = sourceSession.sessionId
|
|
872
1009
|
syncChatSubscription(sessionId)
|
|
873
1010
|
for (const [block, mount] of mounts) {
|
|
874
1011
|
if (!block.isConnected) {
|
|
@@ -877,16 +1014,18 @@ export function installDomFenceRenderer(
|
|
|
877
1014
|
}
|
|
878
1015
|
const raw = rawOf(block)
|
|
879
1016
|
const settled = isSettled(block)
|
|
880
|
-
const domLanguage = domLanguageOf(block)
|
|
1017
|
+
const domLanguage = tagTextBodyOf(block) !== null ? 'dsh-ui' : domLanguageOf(block)
|
|
881
1018
|
const sourceLanguage = domLanguage === null ? sourceLanguages.get(block) : undefined
|
|
882
1019
|
const language = domLanguage ?? sourceLanguage
|
|
1020
|
+
const sourceUnavailable = domLanguage === null
|
|
1021
|
+
&& (!sourceSession.available || sourceLanguages.isSessionUnavailable())
|
|
883
1022
|
const validGenui = language === 'dsh-ui'
|
|
884
1023
|
|| (language === undefined && settled && isGenericGenuiFence(block, raw))
|
|
885
1024
|
if (mount.language === 'svg' && language !== 'svg') {
|
|
886
1025
|
unmountBlock(block)
|
|
887
1026
|
continue
|
|
888
1027
|
}
|
|
889
|
-
if (mount.language === 'dsh-ui' && !validGenui) {
|
|
1028
|
+
if (mount.language === 'dsh-ui' && !validGenui && !sourceUnavailable) {
|
|
890
1029
|
unmountBlock(block)
|
|
891
1030
|
continue
|
|
892
1031
|
}
|
|
@@ -897,7 +1036,7 @@ export function installDomFenceRenderer(
|
|
|
897
1036
|
const contentWiped = !isPanelRoot(mount.lastNode) && mount.container.childElementCount === 0
|
|
898
1037
|
if (mount.lastRaw !== raw || mount.lastSettled !== settled || contentWiped) {
|
|
899
1038
|
const anchor = rowOf(block)
|
|
900
|
-
const { key, context } = contextOf(anchor, block, settled)
|
|
1039
|
+
const { key, context } = contextOf(anchor, block, settled, true)
|
|
901
1040
|
const node = mount.language === 'svg' ? renderSvgFence(raw, key) : renderResolvedFenceNode(raw, key, context)
|
|
902
1041
|
if (node === null) {
|
|
903
1042
|
if (mount.skeleton && !settled) {
|
|
@@ -929,7 +1068,7 @@ export function installDomFenceRenderer(
|
|
|
929
1068
|
try {
|
|
930
1069
|
const freshRoot = domRootFactory(fresh)
|
|
931
1070
|
const handler: GenuiActionHandler = (action, payload) => sendActionForBlock(block, action, payload)
|
|
932
|
-
freshRoot.render(
|
|
1071
|
+
freshRoot.render(wrapFenceNode(node, context, handler, !settled))
|
|
933
1072
|
mount.root = freshRoot
|
|
934
1073
|
mount.container = fresh
|
|
935
1074
|
} catch (error) {
|
|
@@ -944,7 +1083,8 @@ export function installDomFenceRenderer(
|
|
|
944
1083
|
}
|
|
945
1084
|
} else {
|
|
946
1085
|
try {
|
|
947
|
-
|
|
1086
|
+
const handler: GenuiActionHandler = (action, payload) => sendActionForBlock(block, action, payload)
|
|
1087
|
+
mount.root.render(wrapFenceNode(node, context, handler, !settled))
|
|
948
1088
|
} catch (error) {
|
|
949
1089
|
// Never leave the stock block hidden behind a broken root: restore
|
|
950
1090
|
// the raw code block and drop the mount (issue #19).
|
|
@@ -972,7 +1112,7 @@ export function installDomFenceRenderer(
|
|
|
972
1112
|
// label is no longer dsh-ui is somebody else's fence, so our explanation
|
|
973
1113
|
// would be about the wrong block.
|
|
974
1114
|
if (isSettled(block)) {
|
|
975
|
-
const domLanguage = domLanguageOf(block)
|
|
1115
|
+
const domLanguage = tagTextBodyOf(block) !== null ? 'dsh-ui' : domLanguageOf(block)
|
|
976
1116
|
const sourceLanguage = domLanguage === null ? sourceLanguages.get(block) : undefined
|
|
977
1117
|
if ((domLanguage ?? sourceLanguage) !== 'dsh-ui') {
|
|
978
1118
|
clearDiagnostic(block)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const FILE_ADDRESS_PREFIX = 'dsh-resource://file/'
|
|
2
|
+
|
|
3
|
+
/** Encode one address segment while keeping a Windows drive colon readable. */
|
|
4
|
+
function encodeSegment(segment: string): string {
|
|
5
|
+
return encodeURIComponent(segment).replace(/%3A/gi, ':')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Encode each path segment independently so separators retain their structure. */
|
|
9
|
+
function encodePath(path: string): string {
|
|
10
|
+
return path.split('/').map(encodeSegment).join('/')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Check whether a path uses a Windows drive or UNC spelling. */
|
|
14
|
+
function isWindowsStylePath(value: string): boolean {
|
|
15
|
+
return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith('\\\\')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Check whether a path is absolute in a spelling accepted by the host. */
|
|
19
|
+
function isAbsoluteWorkspacePath(path: string): boolean {
|
|
20
|
+
return path.startsWith('/') || isWindowsStylePath(path)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Build a session resource address while preserving encoded path segments. */
|
|
24
|
+
export function sessionFileAddress(sessionId: string, path: string): string {
|
|
25
|
+
const normalized = path.replace(/\\/g, '/').replace(/^(?:\.\/)+/, '')
|
|
26
|
+
return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encodePath(normalized)}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resolve workspace absolute paths to session-relative resource addresses. */
|
|
30
|
+
export function fileAddressFor(sessionId: string, cwd: string | undefined, path: string): string {
|
|
31
|
+
const normalized = path.replace(/\\/g, '/')
|
|
32
|
+
if (!isAbsoluteWorkspacePath(normalized)) return sessionFileAddress(sessionId, normalized)
|
|
33
|
+
const root = cwd === undefined ? '' : cwd.replace(/\\/g, '/').replace(/\/+$/, '')
|
|
34
|
+
if (root !== '' && normalized === root) return sessionFileAddress(sessionId, '')
|
|
35
|
+
if (root !== '' && normalized.startsWith(`${root}/`)) return sessionFileAddress(sessionId, normalized.slice(root.length + 1))
|
|
36
|
+
return sessionFileAddress(sessionId, normalized)
|
|
37
|
+
}
|