1
0
Эх сурвалжийг харах

refactor(ui-chat): fold local-path media mapping into AssistantMarkdown

A single-consumer pure helper does not earn its own file: the mapping
now lives beside the prose vocabulary it serves (same shape as the
deliverables file-mention logic), and its unit cases merged into the
component spec.
_Kerman 1 сар өмнө
parent
commit
ead14339bc

+ 16 - 1
packages/client/ui-chat/src/client/chat/AssistantMarkdown.tsx

@@ -6,10 +6,25 @@ import type { ChatNodeOwnerProps, ChatViewSlotProps } from '../contract/slots.ts
 import type { AssistantBlock } from '../contract/snapshot.ts'
 import { markdownLabels } from '../markdown-labels.ts'
 import { ReasoningRow } from './ReasoningRow.tsx'
-import { localPathMediaUrl } from './local-path-media.ts'
 import { useSearchableHidden } from './searchable-hidden.ts'
 import css from './AssistantMarkdown.module.css'
 
+/**
+ * Map one authored media destination to the same-origin workspace-file URL.
+ * @param protocol - `window.location.protocol` at render time.
+ * @param origin - `window.location.origin` at render time.
+ * @param value - The authored markdown destination, exactly as written.
+ * @returns The API URL for an absolute POSIX path on an HTTP(S) page, or
+ * undefined when the destination cannot be a Host-served local file
+ * (non-HTTP transport such as Electron `file://`, protocol-relative or
+ * relative destinations).
+ */
+export function localPathMediaUrl(protocol: string, origin: string, value: string): string | undefined {
+  if (protocol !== 'http:' && protocol !== 'https:') return undefined
+  if (value.length === 0 || !value.startsWith('/') || value.startsWith('//')) return undefined
+  return `${origin}/api/file?path=${encodeURIComponent(value)}`
+}
+
 export interface AssistantMarkdownProps {
   blocks: readonly AssistantBlock[]
   streaming: boolean

+ 0 - 24
packages/client/ui-chat/src/client/chat/local-path-media.ts

@@ -1,24 +0,0 @@
-/**
- * Pure local-path → same-origin workspace-file mapping for closing prose. The
- * renderer seam consumes this vocabulary; the served endpoint re-validates
- * every request, so this side only decides whether a destination *looks*
- * like an absolute local path the Host could serve. One endpoint covers every
- * served media type (images today; video/audio the same way), so consumers
- * rewrite the path without knowing what kind of media it names.
- */
-
-/**
- * Map one authored media destination to the same-origin workspace-file URL.
- * @param protocol - `window.location.protocol` at render time.
- * @param origin - `window.location.origin` at render time.
- * @param value - The authored markdown destination, exactly as written.
- * @returns The API URL for an absolute POSIX path on an HTTP(S) page, or
- * undefined when the destination cannot be a Host-served local file
- * (non-HTTP transport such as Electron `file://`, protocol-relative or
- * relative destinations).
- */
-export function localPathMediaUrl(protocol: string, origin: string, value: string): string | undefined {
-  if (protocol !== 'http:' && protocol !== 'https:') return undefined
-  if (value.length === 0 || !value.startsWith('/') || value.startsWith('//')) return undefined
-  return `${origin}/api/file?path=${encodeURIComponent(value)}`
-}

+ 29 - 1
packages/client/ui-chat/tests/assistant-markdown-path-images.client.spec.tsx

@@ -1,7 +1,7 @@
 // @vitest-environment jsdom
 import { cleanup, render } from '@testing-library/react'
 import { afterEach, describe, expect, it } from 'vitest'
-import { AssistantMarkdown } from '../src/client/chat/AssistantMarkdown.tsx'
+import { AssistantMarkdown, localPathMediaUrl } from '../src/client/chat/AssistantMarkdown.tsx'
 import type { ChatNodeOwnerProps, ChatViewSlotProps } from '../src/client/contract/slots.ts'
 import type { AssistantBlock } from '../src/client/contract/snapshot.ts'
 
@@ -14,6 +14,34 @@ function textBlock(text: string): AssistantBlock {
   return { kind: 'text', text }
 }
 
+const ORIGIN = 'http://127.0.0.1:3080'
+
+describe('localPathMediaUrl', () => {
+  it('maps an absolute POSIX path on an HTTP page to the file API', () => {
+    expect(localPathMediaUrl('http:', ORIGIN, '/tmp/graph.png'))
+      .toBe(`${ORIGIN}/api/file?path=${encodeURIComponent('/tmp/graph.png')}`)
+    expect(localPathMediaUrl('https:', 'https://127.0.0.1:3080', '/tmp/graph.png'))
+      .toBe(`https://127.0.0.1:3080/api/file?path=${encodeURIComponent('/tmp/graph.png')}`)
+  })
+
+  it('keeps non-HTTP transports inert', () => {
+    expect(localPathMediaUrl('file:', 'file:///app', '/tmp/graph.png')).toBeUndefined()
+    expect(localPathMediaUrl('ws:', ORIGIN, '/tmp/graph.png')).toBeUndefined()
+  })
+
+  it('keeps destinations that cannot be Host-served local files inert', () => {
+    expect(localPathMediaUrl('http:', ORIGIN, '')).toBeUndefined()
+    expect(localPathMediaUrl('http:', ORIGIN, '//cdn.example.com/x.png')).toBeUndefined()
+    expect(localPathMediaUrl('http:', ORIGIN, 'relative.png')).toBeUndefined()
+    expect(localPathMediaUrl('http:', ORIGIN, 'C:\\tmp\\x.png')).toBeUndefined()
+  })
+
+  it('encodes the full path including spaces', () => {
+    expect(localPathMediaUrl('http:', ORIGIN, '/tmp/my graph.png'))
+      .toBe(`${ORIGIN}/api/file?path=${encodeURIComponent('/tmp/my graph.png')}`)
+  })
+})
+
 describe('AssistantMarkdown local-path images', () => {
   it('renders a local image path in closing prose through the same-origin API', () => {
     const { container } = render(

+ 0 - 30
packages/client/ui-chat/tests/local-path-media.client.spec.ts

@@ -1,30 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import { localPathMediaUrl } from '../src/client/chat/local-path-media.ts'
-
-const ORIGIN = 'http://127.0.0.1:3080'
-
-describe('localPathMediaUrl', () => {
-  it('maps an absolute POSIX path on an HTTP page to the image API', () => {
-    expect(localPathMediaUrl('http:', ORIGIN, '/tmp/graph.png'))
-      .toBe(`${ORIGIN}/api/file?path=${encodeURIComponent('/tmp/graph.png')}`)
-    expect(localPathMediaUrl('https:', 'https://127.0.0.1:3080', '/tmp/graph.png'))
-      .toBe(`https://127.0.0.1:3080/api/file?path=${encodeURIComponent('/tmp/graph.png')}`)
-  })
-
-  it('keeps non-HTTP transports inert', () => {
-    expect(localPathMediaUrl('file:', 'file:///app', '/tmp/graph.png')).toBeUndefined()
-    expect(localPathMediaUrl('ws:', ORIGIN, '/tmp/graph.png')).toBeUndefined()
-  })
-
-  it('keeps destinations that cannot be Host-served local files inert', () => {
-    expect(localPathMediaUrl('http:', ORIGIN, '')).toBeUndefined()
-    expect(localPathMediaUrl('http:', ORIGIN, '//cdn.example.com/x.png')).toBeUndefined()
-    expect(localPathMediaUrl('http:', ORIGIN, 'relative.png')).toBeUndefined()
-    expect(localPathMediaUrl('http:', ORIGIN, 'C:\\tmp\\x.png')).toBeUndefined()
-  })
-
-  it('encodes the full path including spaces', () => {
-    expect(localPathMediaUrl('http:', ORIGIN, '/tmp/my graph.png'))
-      .toBe(`${ORIGIN}/api/file?path=${encodeURIComponent('/tmp/my graph.png')}`)
-  })
-})