Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/electron/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@proma/electron",
"version": "0.17.39",
"version": "0.17.40",
"description": "Proma next gen ai software with general agents - Electron App",
"main": "dist/main.cjs",
"author": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,15 @@ export function isAbsoluteFilePath(text: string): boolean {
return false
}

/**
* 判断 Markdown inline code 当前是否可以转换为文件 chip。
* 流式期间必须保留原始代码文本,等语法和内容稳定后再缩成 chip,避免视觉回退。
*/
export function shouldRenderFilePathChip(text: string, isStreaming: boolean, hasBasePaths: boolean): boolean {
if (isStreaming) return false
return isAbsoluteFilePath(text) || (hasBasePaths && isRelativeFilePath(text))
}

/**
* 检测文本是否为相对文件路径(需要 basePath 才有意义)
*
Expand Down
67 changes: 41 additions & 26 deletions apps/electron/src/renderer/components/ai-elements/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
import { LoadingIndicator } from '@/components/ui/loading-indicator'
import { CodeBlock, MermaidBlock } from '@proma/ui'
import { detectLanguage } from '@proma/core'
import { FilePathChip, isAbsoluteFilePath, isImageFilePath, isRelativeFilePath } from './file-path-chip'
import { FilePathChip, isAbsoluteFilePath, isImageFilePath, isRelativeFilePath, shouldRenderFilePathChip } from './file-path-chip'
import { buildAgentHistoryQuoteLabel, parseAgentHistoryQuoteMention } from '@/lib/quoted-selection'
import { useAgentBrowserLink } from '@/components/browser/AgentBrowserLinkProvider'
import type { HTMLAttributes, ComponentProps, ReactNode } from 'react'
Expand Down Expand Up @@ -543,6 +543,15 @@ const MarkdownLink = React.memo(function MarkdownLink({
...linkProps
}: React.AnchorHTMLAttributes<HTMLAnchorElement>): React.ReactElement {
const agentBrowserLink = useAgentBrowserLink()
const isStreamingMarkdown = React.useContext(MarkdownStreamingContext)
// 流式期间保持原始链接文本,避免闭合链接后立刻缩成更短的文件 chip。
if (isStreamingMarkdown && href) {
const mentionMatch = MENTION_URL_RE.exec(href)
const filePath = safeDecode(href)
if (mentionMatch || isAbsoluteFilePath(filePath)) {
return <span>{linkChildren}</span>
}
}
// mention:// 协议 → 渲染为 MentionChip
if (href) {
const mentionMatch = MENTION_URL_RE.exec(href)
Expand Down Expand Up @@ -645,6 +654,7 @@ const MarkdownInlineCode = React.memo(function MarkdownInlineCode({
}: React.HTMLAttributes<HTMLElement> & { basePath?: string; basePaths?: string[] }): React.ReactElement {
// 兜底:从 context 读附加 basePaths(避免穿透 SDKMessageRenderer / ContentBlock 等中间层)
const ctxBasePaths = React.useContext(BasePathsContext)
const isStreamingMarkdown = React.useContext(MarkdownStreamingContext)
// 本轮「文件名 → 绝对路径」映射:命中时把内联裸文件名补全为绝对路径
const turnFileMap = React.useContext(TurnFileMapContext)
if (codeClassName) {
Expand All @@ -654,34 +664,39 @@ const MarkdownInlineCode = React.memo(function MarkdownInlineCode({
const text = typeof codeChildren === 'string' ? codeChildren : ''

if (text) {
// 合并 basePath(主 cwd)+ basePaths(props 或 context 提供的附加目录)作为候选
const merged: string[] = []
if (basePath) merged.push(basePath)
const allExtra = basePaths || ctxBasePaths
if (allExtra) {
for (const p of allExtra) {
if (p && !merged.includes(p)) merged.push(p)
// 流式期间不把 Markdown inline code 转换成文件 chip,避免同一段文本在
// 反引号闭合后由完整路径缩成文件名,造成视觉上的内容回退。
const hasBasePaths = Boolean(basePath || (basePaths || ctxBasePaths)?.some(Boolean))
if (shouldRenderFilePathChip(text, isStreamingMarkdown, hasBasePaths)) {
// 合并 basePath(主 cwd)+ basePaths(props 或 context 提供的附加目录)作为候选
const merged: string[] = []
if (basePath) merged.push(basePath)
const allExtra = basePaths || ctxBasePaths
if (allExtra) {
for (const p of allExtra) {
if (p && !merged.includes(p)) merged.push(p)
}
}
}
if (isAbsoluteFilePath(text)) {
return <FilePathChip filePath={text.trim()} basePaths={merged.length > 0 ? merged : undefined} />
}
if (merged.length > 0 && isRelativeFilePath(text)) {
// 命中本轮实际触及文件的映射时,用绝对路径替换裸文件名(保留行号后缀),
// 使内联引用与 footer chip 走同一条可靠解析;未命中则维持原样降级。
const trimmed = text.trim()
if (turnFileMap && turnFileMap.size > 0) {
const lineColMatch = trimmed.match(/^(.+?)(:\d+(?::\d+)?)$/)
const hasLineCol = !!lineColMatch && !lineColMatch[1]!.endsWith(':')
const pathPart = hasLineCol ? lineColMatch![1]! : trimmed
const suffix = hasLineCol ? lineColMatch![2]! : ''
const baseName = pathPart.split(/[\\/]/).pop() || pathPart
const abs = turnFileMap.get(baseName)
if (abs) {
return <FilePathChip filePath={abs + suffix} basePaths={merged} />
if (isAbsoluteFilePath(text)) {
return <FilePathChip filePath={text.trim()} basePaths={merged.length > 0 ? merged : undefined} />
}
if (merged.length > 0 && isRelativeFilePath(text)) {
// 命中本轮实际触及文件的映射时,用绝对路径替换裸文件名(保留行号后缀),
// 使内联引用与 footer chip 走同一条可靠解析;未命中则维持原样降级。
const trimmed = text.trim()
if (turnFileMap && turnFileMap.size > 0) {
const lineColMatch = trimmed.match(/^(.+?)(:\d+(?::\d+)?)$/)
const hasLineCol = !!lineColMatch && !lineColMatch[1]!.endsWith(':')
const pathPart = hasLineCol ? lineColMatch![1]! : trimmed
const suffix = hasLineCol ? lineColMatch![2]! : ''
const baseName = pathPart.split(/[\\/]/).pop() || pathPart
const abs = turnFileMap.get(baseName)
if (abs) {
return <FilePathChip filePath={abs + suffix} basePaths={merged} />
}
}
return <FilePathChip filePath={trimmed} basePaths={merged} />
}
return <FilePathChip filePath={trimmed} basePaths={merged} />
}
}

Expand Down
6 changes: 5 additions & 1 deletion apps/electron/src/renderer/components/chat/ChatMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
MessageContent,
MessageLoading,
MessageResponse,
MarkdownStreamingContext,
StreamingIndicator,
} from '@/components/ai-elements/message'
import {
Expand Down Expand Up @@ -343,6 +344,7 @@ export function ChatMessages({
streaming={streaming}
streamingContent={smoothContent}
streamingReasoning={smoothReasoning}
markdownStreaming={streaming || smoothContent !== streamingContent}
startedAt={startedAt}
contextDividers={contextDividers}
onDeleteDivider={onDeleteDivider}
Expand Down Expand Up @@ -429,7 +431,9 @@ export function ChatMessages({
{/* 流式内容(经过平滑处理) */}
{smoothContent ? (
<>
<MessageResponse>{smoothContent}</MessageResponse>
<MarkdownStreamingContext.Provider value={streaming || smoothContent !== streamingContent}>
<MessageResponse>{smoothContent}</MessageResponse>
</MarkdownStreamingContext.Provider>
{streaming && <StreamingIndicator />}
</>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
MessageContent,
MessageLoading,
MessageResponse,
MarkdownStreamingContext,
StreamingIndicator,
} from '@/components/ai-elements/message'
import {
Expand Down Expand Up @@ -56,6 +57,8 @@ interface ParallelChatMessagesProps {
onSubmitInlineEdit?: (message: ChatMessage, payload: InlineEditSubmitPayload) => Promise<void>
onCancelInlineEdit?: () => void
inlineEditingMessageId?: string | null
/** Markdown 解析是否仍处于流式排空阶段(与 UI streaming 状态分离) */
markdownStreaming?: boolean
/** 是否正在加载更多历史消息 */
loadingMore?: boolean
}
Expand Down Expand Up @@ -139,6 +142,8 @@ interface MessageColumnProps {
streaming?: boolean
streamingContent?: string
streamingReasoning?: string
/** Markdown 解析是否仍处于流式排空阶段 */
markdownStreaming?: boolean
startedAt?: number
}

Expand All @@ -157,6 +162,7 @@ function MessageColumn({
streamingContent = '',
streamingReasoning = '',
startedAt,
markdownStreaming = streaming,
}: MessageColumnProps): React.ReactElement {
const streamingModel = useAtomValue(streamingModelAtom)
const channels = useAtomValue(channelsAtom)
Expand Down Expand Up @@ -224,7 +230,9 @@ function MessageColumn({
)}
{streamingContent ? (
<>
<MessageResponse>{streamingContent}</MessageResponse>
<MarkdownStreamingContext.Provider value={markdownStreaming}>
<MessageResponse>{streamingContent}</MessageResponse>
</MarkdownStreamingContext.Provider>
{streaming && <StreamingIndicator />}
</>
) : (
Expand Down Expand Up @@ -254,6 +262,7 @@ export function ParallelChatMessages({
onCancelInlineEdit,
inlineEditingMessageId,
loadingMore = false,
markdownStreaming = streaming,
}: ParallelChatMessagesProps): React.ReactElement {
// 分段消息
const segments = useMemo(
Expand Down Expand Up @@ -326,6 +335,7 @@ export function ParallelChatMessages({
streamingContent={streamingContent}
streamingReasoning={streamingReasoning}
startedAt={startedAt}
markdownStreaming={markdownStreaming}
/>
</div>
</div>
Expand Down Expand Up @@ -397,6 +407,7 @@ export function ParallelChatMessages({
streamingContent={index === segments.length - 1 ? streamingContent : ''}
streamingReasoning={index === segments.length - 1 ? streamingReasoning : ''}
startedAt={index === segments.length - 1 ? startedAt : undefined}
markdownStreaming={index === segments.length - 1 ? markdownStreaming : false}
/>
</div>
</div>
Expand Down