fix(web): deduplicate reference submission paths

This commit is contained in:
Yichen Jiang committed 2026-07-30 10:43:35 +08:00
1 parent 04aed3fb5a
commit e3d02f3c10
3 files changed
+58 -48

No files matched your search

@@ -81,21 +81,38 @@ function projectUserText(text: string, sessionLabels: readonly string[] = []): R
return <>{parts}</>
}
function UserBubble({
text,
rest,
sessionLabels,
steering,
}: {
text: string
rest: readonly unknown[]
sessionLabels: readonly string[]
steering?: boolean
}): ReactNode {
return (
<div className={css.userStack}>
<div className={css.bubble}>
{steering === true ? <span className={css.badge}>插话</span> : null}
{projectUserText(text, sessionLabels)}
{rest.map((block, i) => <JsonBlock key={i} label="附加内容块" payload={block} />)}
</div>
{sessionLabels.length > 0
? <div className={css.referenceSummary}>引用会话 · {sessionLabels.join(', ')}</div>
: null}
</div>
)
}
export const MessageItem = memo(function MessageItem({ node, sessionLabels = [] }: MessageItemProps) {
switch (node.kind) {
case 'user': {
const { text, rest } = contentText(node.content)
return (
<div className={css.userRow}>
<div className={css.userStack}>
<div className={css.bubble}>
{projectUserText(text, sessionLabels)}
{rest.map((block, i) => <JsonBlock key={i} label="附加内容块" payload={block} />)}
</div>
{sessionLabels.length > 0
? <div className={css.referenceSummary}>引用会话 · {sessionLabels.join(', ')}</div>
: null}
</div>
<UserBubble text={text} rest={rest} sessionLabels={sessionLabels} />
<MessageIconActions
text={text}
time={node.time}
@@ -110,16 +127,7 @@ export const MessageItem = memo(function MessageItem({ node, sessionLabels = []
const { text, rest } = contentText(node.content)
return (
<div className={css.userRow}>
<div className={css.userStack}>
<div className={css.bubble}>
<span className={css.badge}>插话</span>
{projectUserText(text, sessionLabels)}
{rest.map((block, i) => <JsonBlock key={i} label="附加内容块" payload={block} />)}
</div>
{sessionLabels.length > 0
? <div className={css.referenceSummary}>引用会话 · {sessionLabels.join(', ')}</div>
: null}
</div>
<UserBubble text={text} rest={rest} sessionLabels={sessionLabels} steering />
</div>
)
}
@@ -348,7 +348,7 @@ export class SessionInputShell implements SessionInput {
private sinkSerialized(attempt: SubmitAttempt, draft: string, mode: 'queue' | 'steer'): void {
const occurrences = this.core.state.occurrences
if (occurrences.length === 0) {
this.settleDefault(attempt, this.deps.defaultSink(draft.trim(), mode, attempt.signal))
this.settleSubmit(attempt, this.deps.defaultSink(draft.trim(), mode, attempt.signal))
return
}
const slash = this.deps.slash?.()
@@ -368,7 +368,7 @@ export class SessionInputShell implements SessionInput {
cursor = part.offset + 1
}
out += draft.slice(cursor)
this.settleDefault(attempt, this.deps.defaultSink(out.trim(), mode, attempt.signal))
this.settleSubmit(attempt, this.deps.defaultSink(out.trim(), mode, attempt.signal))
},
(error: unknown) => {
controller.abort()
@@ -384,7 +384,7 @@ export class SessionInputShell implements SessionInput {
)
}
private settleDefault(
private settleSubmit(
attempt: SubmitAttempt,
pending: Promise<SubmitOutcome>,
): void {
@@ -433,21 +433,10 @@ export class SessionInputShell implements SessionInput {
/** The submit transaction: claim.submit against the session scope; ok maps from the outcome kind. */
private beginSubmit(attempt: SubmitAttempt, claim: CommandClaim, args: string): void {
Promise.resolve()
.then(() => claim.submit(args, this.deps.actx))
.then(
(outcome) => {
if (this.dead(attempt)) return
this.run(this.core.dispatch({
type: 'submit-settled', attempt, ok: outcome.kind === 'success', outcome,
}))
},
(error: unknown) => {
if (this.dead(attempt)) return
const message = error instanceof Error ? error.message : String(error)
this.run(this.core.dispatch({ type: 'submit-settled', attempt, ok: false, message }))
},
)
this.settleSubmit(
attempt,
Promise.resolve().then(() => claim.submit(args, this.deps.actx)),
)
}
/** Late-settlement guard: superseded attempts and disposed facades drop silently. */
+24 -11
View File
@@ -9,7 +9,7 @@ import { join } from 'node:path'
import type { Context } from 'cordis'
import { installAgentLlmTarget } from '@deepseek-ai/dsh-agent'
import type {
Agent, AgentLlmTarget, AgentLlmTargetRef, AgentStatus, InboxPlacement,
Agent, AgentLlmTarget, AgentLlmTargetRef, AgentStatus, InboxPlacement, PromptDecision,
} from '@deepseek-ai/dsh-agent'
import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import { errorChain } from '@deepseek-ai/dsh-llm'
@@ -458,6 +458,22 @@ function changedWorkspaceView(workspaceId: string, value: unknown): WorkspaceVie
}
}
async function appendPreparedPromptContext(
matchesPrompt: boolean,
additionalContext: UserMessage,
cleanup: () => void,
next: () => Promise<PromptDecision>,
): Promise<PromptDecision> {
if (!matchesPrompt) return next()
cleanup()
const decision = await next()
if (decision.kind !== 'allow') return decision
return {
...decision,
additionalContexts: [...decision.additionalContexts ?? [], additionalContext],
}
}
/**
* Deliver a prepared browser prompt while preserving session-reference
* admission ownership under the context-injection contract.
@@ -490,16 +506,13 @@ function deliverPrompt(
detachSubmit()
detachDiscard()
}
detachSubmit = ctx.on('agent/prompt-submit', async (subject, submitted, _signal, next) => {
if (subject !== agent || submitted.id !== message.id) return next()
cleanup()
const decision = await next()
if (decision.kind !== 'allow') return decision
return {
...decision,
additionalContexts: [...decision.additionalContexts ?? [], additionalContext],
}
}, { prepend: true })
detachSubmit = ctx.on('agent/prompt-submit', (subject, submitted, _signal, next) =>
appendPreparedPromptContext(
subject === agent && submitted.id === message.id,
additionalContext,
cleanup,
next,
), { prepend: true })
detachDiscard = ctx.on('agent/inbox/discard', (subject, messages) => {
if (subject === agent && messages.some(discarded => discarded.id === message.id)) cleanup()
})