feat(web): a prose mention of a produced file opens it
The chatFileMentions service (provided by ui-deliverables beside its turn-tail entry, reached via ctx.get) resolves inline-code tokens in the closing message against the turn's produced locations: exact path or unique basename links, ambiguity and unknowns stay inert. MarkdownText gains the optional fileMentions seam — settled renders only, never inside anchors.
This commit is contained in:
31 files changed
+584
-43
No files matched your search
@@ -313,6 +313,7 @@ export function apply(ctx: Context): void {
|
||||
actions.select(target)
|
||||
layout.openDetails()
|
||||
},
|
||||
fileMentions: owner => ctx.get('chatFileMentions')?.forClosing(owner),
|
||||
openFile: (path) => {
|
||||
const cwd = sessions.list.getSnapshot().byId[sessionId]?.cwd
|
||||
void workspaces.openPath(resolveToolPath(cwd, path)).catch(() => {
|
||||
|
||||
@@ -15,7 +15,8 @@ import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import {
|
||||
IconThinkOutline14, JsonBlock, MarkdownText,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ChatViewSlotProps, TurnTailOwnerProps } from '../contract/slots.ts'
|
||||
import type { MarkdownFileMentions } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ChatViewSlotProps, ChatViewInjected, TurnTailOwnerProps } from '../contract/slots.ts'
|
||||
import { hasContentText } from './chat-flow.ts'
|
||||
import { MessageIconActions } from './MessageIconActions.tsx'
|
||||
import { ToolRow } from './ToolRow.tsx'
|
||||
@@ -43,6 +44,8 @@ export interface AssistantMarkdownProps {
|
||||
onFork?: ((seq: number) => void) | undefined
|
||||
/** Turn-tail slot dispatch share and owner currency; omitted for a mid-turn assistant. */
|
||||
turnTail?: (Pick<PropsRenderSlots<'conversation.chat.turnTail'>, 'renderSlotChain'> & { owner: TurnTailOwnerProps }) | undefined
|
||||
/** Prose file-mention factory (the injected face); omitted wherever `turnTail` is. */
|
||||
fileMentions?: ChatViewInjected['fileMentions'] | undefined
|
||||
/** The message is not the transcript tail of a completed turn. */
|
||||
forkUnavailable?: boolean | undefined
|
||||
/** The owning view's locale seat, passed down as a plain prop. */
|
||||
@@ -86,11 +89,25 @@ function ThinkRow({ text, running, t }: { text: string; running: boolean; t: Ass
|
||||
}
|
||||
|
||||
export const AssistantMarkdown = memo(function AssistantMarkdown({
|
||||
blocks, streaming, interrupted, time, runMs, ttftMs, tokensPerSecond, seq, onFork, forkUnavailable, turnTail, t,
|
||||
blocks, streaming, interrupted, time, runMs, ttftMs, tokensPerSecond, seq, onFork, forkUnavailable, turnTail,
|
||||
fileMentions, t,
|
||||
}: AssistantMarkdownProps) {
|
||||
// Stable per locale revision (t identity changes on switch): a fresh object
|
||||
// per render would rebuild MarkdownText's component table every chunk.
|
||||
const codeLabels = useMemo(() => ({ copyLabel: t('copy'), copiedLabel: t('copied') }), [t])
|
||||
// Mention vocabulary for the closing prose. Keyed on the anchor seq, not the
|
||||
// growing transcript: a settled turn's produced files are final, and a
|
||||
// fresh identity per append would discard MarkdownText's cached parse for
|
||||
// every settled closing message on every stream chunk. The window-prepend
|
||||
// edge (a mid-turn window start later gaining earlier same-turn writes)
|
||||
// leaves a mention unlinked until remount — never a wrong link.
|
||||
const owner = turnTail?.owner
|
||||
const mentions: MarkdownFileMentions | undefined = useMemo(
|
||||
() => (owner === undefined ? undefined : fileMentions?.(owner)),
|
||||
// Deliberately not `owner`: its identity changes per append while the
|
||||
// seq-addressed vocabulary it yields does not.
|
||||
[fileMentions, owner?.seq],
|
||||
)
|
||||
const last = blocks.length - 1
|
||||
// Tool-call heads render as tool rows in the chat view's grouping pass, so
|
||||
// a node that is only those heads (or empty) would paint an empty root
|
||||
@@ -107,7 +124,13 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({
|
||||
{blocks.map((block, i) => {
|
||||
switch (block.kind) {
|
||||
case 'text': return (
|
||||
<MarkdownText key={i} text={block.text} streaming={streaming} codeLabels={codeLabels} />
|
||||
<MarkdownText
|
||||
key={i}
|
||||
text={block.text}
|
||||
streaming={streaming}
|
||||
codeLabels={codeLabels}
|
||||
fileMentions={mentions}
|
||||
/>
|
||||
)
|
||||
case 'reasoning': return <ThinkRow key={i} text={block.text} running={streaming && i === last} t={t} />
|
||||
// Grouped into tool rows by ChatView; hasVisible above skips an empty shell.
|
||||
|
||||
@@ -335,7 +335,8 @@ function StreamingTail({ useSession, t }: {
|
||||
* render through the declared keyed hole's renderSlot share).
|
||||
*/
|
||||
export function ChatView({
|
||||
useSession, useSessions, useStore, renderSlot, renderSlotChain, sessionId, openFile, loadOlder, inspectCall, chatScroll, forkAt, t,
|
||||
useSession, useSessions, useStore, renderSlot, renderSlotChain, sessionId, openFile, loadOlder, inspectCall, chatScroll, forkAt,
|
||||
fileMentions, t,
|
||||
}: ChatViewSlotProps) {
|
||||
const nodes = useSession(s => s.nodes)
|
||||
const turnTimings = useSession(s => s.turnTimings)
|
||||
@@ -603,6 +604,7 @@ export function ChatView({
|
||||
turnTail={actionSeqs.has(node.seq)
|
||||
? { renderSlotChain, owner: { nodes, seq: node.seq, openFile } }
|
||||
: undefined}
|
||||
fileMentions={actionSeqs.has(node.seq) ? fileMentions : undefined}
|
||||
t={t}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
InjectFace, MaybeSnapshotSelectorHook, PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore, SnapshotSelectorHook,
|
||||
} from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { CommandNode, ConversationNode, ConversationSnapshot, ObservableSnapshot, PendingInteraction, PendingWait, SessionId, ToolCallBlock, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { MarkdownFileMentions } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
import type { ComposerKeyboard, EditSelection, InputActions, InputNotice, InputState } from '../input/contract.ts'
|
||||
import type { createChatStore } from '../stores.ts'
|
||||
@@ -158,6 +159,30 @@ export interface ConvViewOwnerProps {
|
||||
onInspectDone?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional prose file-mention provider, consumed via `ctx.get('chatFileMentions')`
|
||||
* (optional-service convention): the chat view asks it for a closing message's
|
||||
* inline-code vocabulary and threads the result into MarkdownText. Absent
|
||||
* service — the providing plugin composed out of cordis.yml — turns the
|
||||
* surface off; the prose renders inert code.
|
||||
*/
|
||||
export interface ChatFileMentions {
|
||||
/**
|
||||
* Mention vocabulary for the closing message the owner currency names.
|
||||
* @param owner - Turn-tail owner currency (nodes, closing seq, opener).
|
||||
* @returns The resolver MarkdownText consumes, or undefined when the turn
|
||||
* produced nothing worth linking.
|
||||
*/
|
||||
forClosing(owner: TurnTailOwnerProps): MarkdownFileMentions | undefined
|
||||
}
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
/** Prose file-mention provider (ui-deliverables); reach via ctx.get — optional. */
|
||||
chatFileMentions: ChatFileMentions
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner currency of the chat view's turn-tail hole: the finalized snapshot
|
||||
* and the closing assistant's anchor. Registrants derive their own facts
|
||||
@@ -502,6 +527,13 @@ export interface ChatViewInjected {
|
||||
}
|
||||
/** Fork through the completed turn ending at the eligible message `seq`, then open the child. */
|
||||
forkAt: (seq: number) => void
|
||||
/**
|
||||
* Prose file-mention vocabulary for one closing message, from the optional
|
||||
* {@link ChatFileMentions} service (resolved lazily per call, so composing
|
||||
* the provider in or out takes effect live). Undefined when the service is
|
||||
* absent or the turn produced nothing worth linking.
|
||||
*/
|
||||
fileMentions: (owner: TurnTailOwnerProps) => MarkdownFileMentions | undefined
|
||||
}
|
||||
|
||||
/** Full chat-view component props: runtime & the declared toolview/commandview holes' render share & store & injected & locale seat. */
|
||||
|
||||
@@ -13,6 +13,7 @@ export type {
|
||||
export type { ToolCallBlock } from './contract/tool-call-model.ts'
|
||||
export type { ConversationKey } from './locales.ts'
|
||||
export type {
|
||||
ChatFileMentions,
|
||||
ChatStore, ChatViewInjected, ChatViewSlotProps, CommandRowOwnerProps, CommandRowProps, ComposerBarInjected,
|
||||
ComposerChainProps, ConversationInjected,
|
||||
ConversationSessionHeaderInjected, ConversationSessionInjected, ConversationSlotProps,
|
||||
|
||||
Reference in New Issue
Block a user