fix(web): deduplicate reference submission paths
This commit is contained in:
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. */
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user