From bf0278e586336ff1326f5008646ce50a7f78e0a0 Mon Sep 17 00:00:00 2001 From: MT-Mint <798521692@qq.com> Date: Wed, 15 Apr 2026 13:21:35 +0800 Subject: [PATCH] feat(input): align phase-06 reference contract and state docs --- .planning/ROADMAP.md | 4 +- .planning/STATE.md | 6 +- .../src/components/workspace/input-box.tsx | 137 ++++++++++++++---- frontend/src/core/threads/hooks.ts | 4 +- 4 files changed, 118 insertions(+), 33 deletions(-) diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index c42aa173..e8e821a9 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -58,12 +58,14 @@ **Goal:** 在当前线程聊天输入框中实现 `@` 文件引用(artifacts + uploads),并通过 `additional_kwargs.files` 稳定提交且具备回归测试。 **Requirements**: ATREF-01, ATREF-02, ATREF-03, ATREF-04 **Depends on:** Phase 5 -**Plans:** 3 plans +**Plans:** 4 executable plans + 1 archived revision record Plans: - [x] 06-01-PLAN.md — 锁定引用提交契约与软失败链路(additional_kwargs.files) - [x] 06-02-PLAN.md — 实现 @ 候选 dropdown、chip 交互与上限控制 - [x] 06-03-PLAN.md — 补齐自动化验证并产出 style/logic/tests/docs 提交分组计划 +- [x] 06-04-ARCHIVED.md — 修订归档:原 gap-closure 计划与锁定决策 D-08(上限 10)冲突,保留追踪但不再执行 +- [ ] 06-05-PLAN.md — 关闭 verification 缺口:恢复 10 个上限/类型去歧义,并稳定 DF-INPUT-008/009 回归 --- *Next command:* `/gsd-verify-work` diff --git a/.planning/STATE.md b/.planning/STATE.md index eb9ea92f..cab3ea59 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,12 +3,12 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: Executing Phase 06 -last_updated: "2026-04-15T03:07:32.408Z" +last_updated: "2026-04-15T05:06:28.041Z" progress: total_phases: 6 completed_phases: 6 total_plans: 10 - completed_plans: 10 + completed_plans: 12 percent: 100 --- @@ -19,7 +19,7 @@ progress: See: .planning/PROJECT.md (updated 2026-04-07) **Core value:** Keep the frontend visually familiar while preserving and hardening new-system behavior end to end. -**Current focus:** Phase 06 — @引用文件与附件 +**Current focus:** Phase 06 — phase-06 ## Workflow State diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx index 9fe6c7ef..97c29d19 100644 --- a/frontend/src/components/workspace/input-box.tsx +++ b/frontend/src/components/workspace/input-box.tsx @@ -57,9 +57,13 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { + DropdownMenu, + DropdownMenuContent, DropdownMenuGroup, + DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tag } from "@/components/ui/tag"; import { useI18n } from "@/core/i18n/hooks"; @@ -89,7 +93,12 @@ import { Tooltip } from "./tooltip"; import { useThread } from "./messages/context"; import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime"; -const MAX_REFERENCES_PER_MESSAGE = 6; +const MAX_REFERENCES_PER_MESSAGE = 10; + +const REFERENCE_SOURCE_LABELS = { + artifact: "生成文件", + upload: "上传附件", +} as const; type MentionCandidate = { key: string; @@ -98,6 +107,7 @@ type MentionCandidate = { pathTail: string; ref_source: "artifact" | "upload"; ref_kind: "mention"; + typeLabel: string; }; function getPathTail(path: string | undefined): string { @@ -172,6 +182,7 @@ export function InputBox({ const promptRootRef = useRef(null); const textareaRef = useRef(null); const containerRef = useRef(null); + const mentionTriggerRef = useRef(null); const [followups, setFollowups] = useState([]); const [followupsHidden, setFollowupsHidden] = useState(false); const [followupsLoading, setFollowupsLoading] = useState(false); @@ -240,6 +251,7 @@ export function InputBox({ pathTail: getPathTail(path), ref_source: "artifact" as const, ref_kind: "mention" as const, + typeLabel: REFERENCE_SOURCE_LABELS.artifact, }; }); @@ -251,6 +263,7 @@ export function InputBox({ pathTail: getPathTail(file.virtual_path), ref_source: "upload" as const, ref_kind: "mention" as const, + typeLabel: REFERENCE_SOURCE_LABELS.upload, })) ?? []; const deduped = new Map(); @@ -266,7 +279,9 @@ export function InputBox({ return mentionCandidates; } return mentionCandidates.filter((candidate) => - `${candidate.filename} ${candidate.pathTail}`.toLowerCase().includes(query), + `${candidate.filename} ${candidate.typeLabel} ${candidate.pathTail}` + .toLowerCase() + .includes(query), ); }, [mentionCandidates, mentionQuery]); const handleModelSelect = useCallback( @@ -331,7 +346,7 @@ export function InputBox({ return prev; } if (prev.length >= MAX_REFERENCES_PER_MESSAGE) { - toast.error("单条消息最多引用 6 个文件"); + toast.error("单条消息最多引用 10 个文件"); return prev; } return prev.concat({ @@ -467,6 +482,12 @@ export function InputBox({ setTimeout(() => requestFormSubmit(), 0); }, [pendingSuggestion, requestFormSubmit, textInput]); + useEffect(() => { + if (mentionOpen) { + mentionTriggerRef.current?.focus(); + } + }, [mentionOpen]); + useEffect(() => { /* // 暂时禁用 suggestions 接口(/api/threads/{thread_id}/suggestions) @@ -544,9 +565,13 @@ export function InputBox({ data-testid="reference-inline-preview" > {references.map((reference) => { - const label = reference.path - ? `${reference.filename} · ${getPathTail(reference.path)}` - : reference.filename; + const typeLabel = REFERENCE_SOURCE_LABELS[reference.ref_source]; + const labelParts = [ + reference.filename, + typeLabel, + reference.path ? getPathTail(reference.path) : "", + ].filter(Boolean); + const label = labelParts.join(" · "); const referenceUrl = threadId && reference.path ? urlOfArtifact({ @@ -561,6 +586,7 @@ export function InputBox({
{isImageReference && referenceUrl ? (
)} - - {label} - +
+ + {reference.filename} + + + {typeLabel} + {reference.path ? ` · ${getPathTail(reference.path)}` : ""} + +
+
+ + {candidate.filename} + + + {detail} + +
+ ); - })} - - )} + })} + + + {!effectiveIsFocused && (
0) { - toast.error("部分引用已失效,已自动移除"); + toast.error("部分引用文件已失效,已自动移除并继续发送。"); } await thread.submit( @@ -757,7 +757,7 @@ export function useSubmitThread({ normalizedReferences, ); if (staleCount > 0) { - toast.error("部分引用已失效,已自动移除"); + toast.error("部分引用文件已失效,已自动移除并继续发送。"); } await thread.submit(