Przeglądaj źródła

fix(web): deduplicate reference submission paths

Yichen Jiang 2 miesięcy temu
rodzic
commit
e3d02f3c10

+ 27 - 19
packages/client/ui-conversation/src/client/chat/MessageItem.tsx

@@ -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>
       )
     }

+ 7 - 18
packages/client/ui-conversation/src/client/input/facade.ts

@@ -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
packages/host/apiproxy/src/api-proxy.ts

@@ -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()
   })