mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
feat(desktop): route assistant message links to in-app preview surfaces
Wire L1 link routing into assistant message rendering. AssistantMessage now accepts an optional sessionId and, when present, builds an onLinkClick that runs handlePreviewLink against the running local server base (getServerBaseUrl) and the browser/workspace panel stores, preventing default on handled links. MessageList passes sessionId through in the assistant_text case. Adds getServerBaseUrl() to desktopRuntime as a synchronous wrapper over the api client's cached getBaseUrl(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
13222b375d
commit
d9ebf4c2f1
@ -0,0 +1,24 @@
|
||||
import '@testing-library/jest-dom'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { openBrowser } = vi.hoisted(() => ({ openBrowser: vi.fn() }))
|
||||
vi.mock('../../stores/browserPanelStore', () => ({
|
||||
useBrowserPanelStore: { getState: () => ({ open: openBrowser }) },
|
||||
}))
|
||||
vi.mock('../../lib/desktopRuntime', async (orig) => ({
|
||||
...(await orig<Record<string, unknown>>()),
|
||||
getServerBaseUrl: () => 'http://127.0.0.1:4321',
|
||||
}))
|
||||
|
||||
import { AssistantMessage } from './AssistantMessage'
|
||||
|
||||
afterEach(() => openBrowser.mockReset())
|
||||
|
||||
describe('AssistantMessage link routing', () => {
|
||||
it('opens a localhost link in the in-app browser', () => {
|
||||
render(<AssistantMessage sessionId="s1" content={'打开 [预览](http://localhost:5173/)'} />)
|
||||
fireEvent.click(screen.getByText('预览'))
|
||||
expect(openBrowser).toHaveBeenCalledWith('s1', 'http://localhost:5173/')
|
||||
})
|
||||
})
|
||||
@ -1,15 +1,43 @@
|
||||
import { memo } from 'react'
|
||||
import { memo, useCallback } from 'react'
|
||||
import type { MouseEvent as ReactMouseEvent } from 'react'
|
||||
import { MarkdownRenderer } from '../markdown/MarkdownRenderer'
|
||||
import { MessageActionBar, type MessageBranchAction } from './MessageActionBar'
|
||||
import { InlineImageGallery } from './InlineImageGallery'
|
||||
import { handlePreviewLink } from '../../lib/handlePreviewLink'
|
||||
import { getServerBaseUrl } from '../../lib/desktopRuntime'
|
||||
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
|
||||
import { useWorkspacePanelStore } from '../../stores/workspacePanelStore'
|
||||
|
||||
type Props = {
|
||||
content: string
|
||||
isStreaming?: boolean
|
||||
branchAction?: MessageBranchAction
|
||||
sessionId?: string
|
||||
}
|
||||
|
||||
export const AssistantMessage = memo(function AssistantMessage({ content, isStreaming, branchAction }: Props) {
|
||||
export const AssistantMessage = memo(function AssistantMessage({ content, isStreaming, branchAction, sessionId }: Props) {
|
||||
const handleLinkClick = useCallback(
|
||||
(href: string, event: ReactMouseEvent<HTMLDivElement>): boolean => {
|
||||
if (!sessionId) return false
|
||||
const handled = handlePreviewLink(href, {
|
||||
sessionId,
|
||||
serverBaseUrl: getServerBaseUrl(),
|
||||
openBrowser: (id, url) => useBrowserPanelStore.getState().open(id, url),
|
||||
openFilePreview: (id, path) => {
|
||||
void useWorkspacePanelStore.getState().openPreview(id, path, 'file')
|
||||
},
|
||||
openExternal: (url) => {
|
||||
void import('@tauri-apps/plugin-shell')
|
||||
.then((m) => m.open(url))
|
||||
.catch(() => window.open(url, '_blank'))
|
||||
},
|
||||
})
|
||||
if (handled) event.preventDefault()
|
||||
return handled
|
||||
},
|
||||
[sessionId],
|
||||
)
|
||||
|
||||
if (!content.trim()) return null
|
||||
|
||||
const documentLayout = shouldUseDocumentLayout(content)
|
||||
@ -32,6 +60,7 @@ export const AssistantMessage = memo(function AssistantMessage({ content, isStre
|
||||
content={content}
|
||||
variant={documentLayout ? 'document' : 'default'}
|
||||
streaming={isStreaming}
|
||||
onLinkClick={sessionId ? handleLinkClick : undefined}
|
||||
/>
|
||||
{!isStreaming && <InlineImageGallery text={content} />}
|
||||
{isStreaming && (
|
||||
|
||||
@ -1960,7 +1960,7 @@ export const MessageBlock = memo(function MessageBlock({
|
||||
role="assistant"
|
||||
content={message.content}
|
||||
>
|
||||
<AssistantMessage content={message.content} branchAction={branchAction} />
|
||||
<AssistantMessage content={message.content} branchAction={branchAction} sessionId={sessionId ?? undefined} />
|
||||
</SelectableChatMessage>
|
||||
)
|
||||
case 'thinking':
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
api,
|
||||
getBaseUrl,
|
||||
getDefaultBaseUrl,
|
||||
hasExplicitDefaultBaseUrl,
|
||||
setAuthToken,
|
||||
@ -41,6 +42,19 @@ export function isBrowserH5Runtime() {
|
||||
return typeof window !== 'undefined' && !isTauriRuntime()
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously return the running local server's base URL (e.g.
|
||||
* `http://127.0.0.1:<port>`).
|
||||
*
|
||||
* The api client caches the resolved base after startup: `initializeDesktopServerUrl`
|
||||
* calls `invoke('get_server_url')` (desktop) or resolves a browser/H5 URL, then
|
||||
* `setBaseUrl(...)`. Until that runs, `getBaseUrl()` returns the default
|
||||
* (`http://127.0.0.1:3456` or `VITE_DESKTOP_SERVER_URL`).
|
||||
*/
|
||||
export function getServerBaseUrl(): string {
|
||||
return getBaseUrl()
|
||||
}
|
||||
|
||||
export function readStoredH5Connection(): StoredH5Connection {
|
||||
if (typeof window === 'undefined') {
|
||||
return { serverUrl: null, token: null }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user