This commit is contained in:
xinsngx 2026-05-31 01:23:31 -04:00 committed by GitHub
commit e506b8626d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 190 additions and 11 deletions

View file

@ -771,10 +771,11 @@ interface Props {
isDeck?: boolean;
onExportAsPptx?: ((fileName: string) => void) | undefined;
streaming?: boolean;
commentSendDisabled?: boolean;
previewComments?: PreviewComment[];
onSavePreviewComment?: (target: PreviewCommentTarget, note: string, attachAfterSave: boolean) => Promise<PreviewComment | null>;
onRemovePreviewComment?: (commentId: string) => Promise<void>;
onSendBoardCommentAttachments?: (attachments: ChatCommentAttachment[]) => Promise<void> | void;
onSendBoardCommentAttachments?: (attachments: ChatCommentAttachment[]) => Promise<boolean | void> | boolean | void;
onFileSaved?: () => Promise<void> | void;
// Open `openName` as a tab (focusing it) and close `closeName` in one
// atomic tab-state update. The React module pointer uses this to jump to the
@ -793,6 +794,7 @@ export function FileViewer({
isDeck,
onExportAsPptx,
streaming,
commentSendDisabled = false,
previewComments = [],
onSavePreviewComment,
onRemovePreviewComment,
@ -833,6 +835,7 @@ export function FileViewer({
isDeck={rendererMatch.renderer.id === 'deck-html'}
onExportAsPptx={onExportAsPptx}
streaming={Boolean(streaming)}
commentSendDisabled={commentSendDisabled}
previewComments={previewComments}
onSavePreviewComment={onSavePreviewComment}
onRemovePreviewComment={onRemovePreviewComment}
@ -4012,6 +4015,7 @@ function HtmlViewer({
isDeck,
onExportAsPptx,
streaming,
commentSendDisabled,
previewComments = [],
onSavePreviewComment,
onRemovePreviewComment,
@ -4028,10 +4032,11 @@ function HtmlViewer({
isDeck: boolean;
onExportAsPptx?: ((fileName: string) => void) | undefined;
streaming: boolean;
commentSendDisabled: boolean;
previewComments?: PreviewComment[];
onSavePreviewComment?: (target: PreviewCommentTarget, note: string, attachAfterSave: boolean) => Promise<PreviewComment | null>;
onRemovePreviewComment?: (commentId: string) => Promise<void>;
onSendBoardCommentAttachments?: (attachments: ChatCommentAttachment[]) => Promise<void> | void;
onSendBoardCommentAttachments?: (attachments: ChatCommentAttachment[]) => Promise<boolean | void> | boolean | void;
onFileSaved?: () => Promise<void> | void;
commentPortalId?: string;
onCommentModeChange?: (active: boolean) => void;
@ -6339,12 +6344,13 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
if (nextNotes.length === 0) return;
setSendingBoardBatch(true);
try {
await onSendBoardCommentAttachments(
const accepted = await onSendBoardCommentAttachments(
buildBoardCommentAttachments({
target: targetFromSnapshot(activeCommentTarget),
notes: nextNotes,
}),
);
if (accepted === false) return;
clearBoardComposer();
} finally {
setSendingBoardBatch(false);
@ -6749,7 +6755,8 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
});
setActivePreviewCommentId((current) => (current === commentId ? null : current));
} : undefined}
sending={sendingBoardBatch || streaming}
sending={sendingBoardBatch || commentSendDisabled}
t={t}
scale={overlayPreviewScale}
offset={{ x: overlayPreviewTransform.offsetX, y: overlayPreviewTransform.offsetY }}
@ -6813,14 +6820,14 @@ const [manualEditTargets, setManualEditTargets] = useState<ManualEditTarget[]>([
fireCommentPopoverClick('send_to_chat');
setSendingBoardBatch(true);
try {
await onSendBoardCommentAttachments(commentsToAttachments(selected));
setSelectedSideCommentIds(new Set());
const accepted = await onSendBoardCommentAttachments(commentsToAttachments(selected));
if (accepted !== false) setSelectedSideCommentIds(new Set());
} finally {
setSendingBoardBatch(false);
}
}}
onCreateComment={savePanelComment}
sending={sendingBoardBatch || streaming}
sending={sendingBoardBatch || commentSendDisabled}
t={t}
composer={null}
/>

View file

@ -72,6 +72,7 @@ interface Props {
isDeck: boolean;
onExportAsPptx?: ((fileName: string) => void) | undefined;
streaming?: boolean;
commentSendDisabled?: boolean;
openRequest?: { name: string; nonce: number } | null;
liveArtifactEvents?: LiveArtifactEventItem[];
designSystemActivityEvents?: AgentEvent[];
@ -82,7 +83,7 @@ interface Props {
previewComments?: PreviewComment[];
onSavePreviewComment?: (target: PreviewCommentTarget, note: string, attachAfterSave: boolean) => Promise<PreviewComment | null>;
onRemovePreviewComment?: (commentId: string) => Promise<void>;
onSendBoardCommentAttachments?: (attachments: ChatCommentAttachment[]) => Promise<void> | void;
onSendBoardCommentAttachments?: (attachments: ChatCommentAttachment[]) => Promise<boolean | void> | boolean | void;
onPluginFolderAgentAction?: (
relativePath: string,
action: PluginFolderAgentAction,
@ -207,6 +208,7 @@ export function FileWorkspace({
isDeck,
onExportAsPptx,
streaming,
commentSendDisabled = false,
openRequest,
liveArtifactEvents = [],
designSystemActivityEvents = [],
@ -1098,6 +1100,7 @@ export function FileWorkspace({
isDeck={isDeck}
onExportAsPptx={onExportAsPptx}
streaming={streaming}
commentSendDisabled={commentSendDisabled}
previewComments={previewComments.filter((comment) => comment.filePath === activeFile.name)}
onSavePreviewComment={onSavePreviewComment}
onRemovePreviewComment={onRemovePreviewComment}

View file

@ -743,6 +743,8 @@ export function ProjectView({
|| failedMessagesConversationId === activeConversationId
|| currentConversationAwaitingActiveRunAttach;
const currentConversationActionDisabled = currentConversationBusy || currentConversationSendDisabled;
const currentConversationQueueDisabled = currentConversationLoading
|| failedMessagesConversationId === activeConversationId;
const currentConversationQueuedItems = activeConversationId
? queuedChatSends
.filter((item) => item.conversationId === activeConversationId)
@ -3064,12 +3066,13 @@ export function ProjectView({
const handleSendBoardCommentAttachments = useCallback(
async (commentAttachments: ChatCommentAttachment[]) => {
if (currentConversationActionDisabled || commentAttachments.length === 0) return;
if (currentConversationQueueDisabled || commentAttachments.length === 0) return false;
setWorkspaceFocused(false);
setCommentInspectorActive(false);
await handleSend('', [], commentAttachments);
return true;
},
[handleSend, currentConversationActionDisabled],
[handleSend, currentConversationQueueDisabled],
);
const handleContinueRemainingTasks = useCallback(
@ -4556,6 +4559,7 @@ export function ProjectView({
isDeck={isDeck}
onExportAsPptx={handleExportAsPptx}
streaming={currentConversationActionDisabled}
commentSendDisabled={currentConversationQueueDisabled}
openRequest={openRequest}
liveArtifactEvents={liveArtifactEvents}
designSystemActivityEvents={designSystemActivityEvents}

View file

@ -2573,6 +2573,88 @@ describe('FileViewer tweaks toolbar', () => {
expect(activeItem?.getAttribute('aria-current')).toBe('true');
});
it('lets element comments queue to chat while a task is running', async () => {
const onSendBoardCommentAttachments = vi.fn().mockResolvedValue(undefined);
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={htmlPreviewFile()}
liveHtml='<html><body><main data-od-id="hero">Hero</main></body></html>'
streaming
onSendBoardCommentAttachments={onSendBoardCommentAttachments}
/>,
);
const frame = screen.getByTestId('artifact-preview-frame') as HTMLIFrameElement;
fireEvent.click(screen.getByTestId('board-mode-toggle'));
window.dispatchEvent(new MessageEvent('message', {
source: frame.contentWindow,
data: {
type: 'od:comment-target',
elementId: 'hero',
selector: '[data-od-id="hero"]',
label: 'Hero',
text: 'Hero',
position: { x: 8, y: 12, width: 120, height: 48 },
htmlHint: '<main data-od-id="hero">Hero</main>',
},
}));
const input = await screen.findByTestId('comment-popover-input');
fireEvent.change(input, { target: { value: '加大字号' } });
const send = screen.getByTestId('comment-add-send') as HTMLButtonElement;
expect(send.disabled).toBe(false);
fireEvent.click(send);
await waitFor(() => expect(onSendBoardCommentAttachments).toHaveBeenCalledTimes(1));
expect(onSendBoardCommentAttachments.mock.calls[0]?.[0]?.[0]).toMatchObject({
filePath: 'preview.html',
elementId: 'hero',
comment: '加大字号',
source: 'board-batch',
});
await waitFor(() => expect(screen.queryByTestId('comment-popover')).toBeNull());
});
it('keeps the comment draft when chat queueing declines the send', async () => {
const onSendBoardCommentAttachments = vi.fn().mockResolvedValue(false);
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={htmlPreviewFile()}
liveHtml='<html><body><main data-od-id="hero">Hero</main></body></html>'
onSendBoardCommentAttachments={onSendBoardCommentAttachments}
/>,
);
const frame = screen.getByTestId('artifact-preview-frame') as HTMLIFrameElement;
fireEvent.click(screen.getByTestId('board-mode-toggle'));
window.dispatchEvent(new MessageEvent('message', {
source: frame.contentWindow,
data: {
type: 'od:comment-target',
elementId: 'hero',
selector: '[data-od-id="hero"]',
label: 'Hero',
text: 'Hero',
position: { x: 8, y: 12, width: 120, height: 48 },
htmlHint: '<main data-od-id="hero">Hero</main>',
},
}));
const input = await screen.findByTestId('comment-popover-input') as HTMLTextAreaElement;
fireEvent.change(input, { target: { value: '保留这个评论' } });
fireEvent.click(screen.getByTestId('comment-add-send'));
await waitFor(() => expect(onSendBoardCommentAttachments).toHaveBeenCalledTimes(1));
expect(screen.getByTestId('comment-popover')).toBeTruthy();
expect((screen.getByTestId('comment-popover-input') as HTMLTextAreaElement).value)
.toBe('保留这个评论');
});
it('returns to element picking from the Comment button while another annotation tool is active', async () => {
render(
<FileViewer

View file

@ -153,8 +153,12 @@ vi.mock('../../src/components/ChatPane', () => ({
},
}));
const fileWorkspaceSpy = vi.fn();
vi.mock('../../src/components/FileWorkspace', () => ({
FileWorkspace: () => null,
FileWorkspace: (props: Record<string, unknown>) => {
fileWorkspaceSpy(props);
return null;
},
}));
vi.mock('../../src/components/Loading', () => ({
@ -702,6 +706,85 @@ describe('ProjectView daemon cleanup', () => {
}
});
it('queues board comment attachments while the current daemon run is still busy', async () => {
listConversations.mockResolvedValue([{ id: 'conv-1', title: 'Conversation' }]);
listMessages.mockResolvedValue([]);
fetchPreviewComments.mockResolvedValue([]);
loadTabs.mockResolvedValue({ tabs: [], activeTabId: null });
fetchProjectFiles.mockResolvedValue([]);
fetchLiveArtifacts.mockResolvedValue([]);
fetchSkill.mockResolvedValue(null);
fetchDesignSystem.mockResolvedValue(null);
getTemplate.mockResolvedValue(null);
listActiveChatRuns.mockResolvedValue([]);
streamViaDaemon.mockImplementation(async () => new Promise<void>(() => {}));
render(
<ProjectView
project={{
id: 'project-comments',
name: 'Project',
skillId: null,
designSystemId: null,
} as never}
routeFileName={null}
config={{ mode: 'daemon', agentId: 'agent-1', notifications: undefined, agentModels: {} } as never}
agents={[{ id: 'agent-1', name: 'OpenCode', models: [] } as never]}
skills={[]}
designTemplates={[]}
designSystems={[]}
daemonLive
onModeChange={() => {}}
onAgentChange={() => {}}
onAgentModelChange={() => {}}
onRefreshAgents={() => {}}
onOpenSettings={() => {}}
onBack={() => {}}
onClearPendingPrompt={() => {}}
onTouchProject={() => {}}
onProjectChange={() => {}}
onProjectsRefresh={() => {}}
/>,
);
const chatProps = await waitForReadyChatPaneProps();
await chatProps.onSend!('keep running', [], []);
await waitFor(() => expect(streamViaDaemon).toHaveBeenCalledTimes(1));
await waitFor(() => {
expect(chatPaneSpy.mock.calls.at(-1)?.[0]?.streaming).toBe(true);
});
const workspaceProps = fileWorkspaceSpy.mock.calls.at(-1)?.[0] as {
onSendBoardCommentAttachments?: (attachments: unknown[]) => Promise<boolean | void>;
};
expect(workspaceProps.onSendBoardCommentAttachments).toBeTruthy();
await workspaceProps.onSendBoardCommentAttachments!([
{
id: 'hero-board-1',
order: 1,
filePath: 'preview.html',
elementId: 'hero',
selector: '[data-od-id="hero"]',
label: 'Hero',
comment: 'Use a warmer accent',
currentText: 'Hero',
pagePosition: { x: 10, y: 20, width: 100, height: 40 },
htmlHint: '<main data-od-id="hero">',
source: 'board-batch',
},
]);
await waitFor(() => {
const latest = chatPaneSpy.mock.calls.at(-1)?.[0] as {
queuedItems?: Array<{ commentAttachments?: Array<{ comment: string }> }>;
};
expect(latest.queuedItems).toHaveLength(1);
expect(latest.queuedItems?.[0]?.commentAttachments?.[0]?.comment).toBe('Use a warmer accent');
});
expect(streamViaDaemon).toHaveBeenCalledTimes(1);
});
it('audits design-system workspace output after first auto-send and seeds a bounded repair prompt', async () => {
listConversations.mockResolvedValue([{ id: 'conv-1', title: 'Conversation' }]);
listMessages.mockResolvedValue([]);