a screen can hear when a chat turn finished
ChatPanelWrapper already refreshed the file tree and the preview when a turn ran tools. it kept that to itself, so a screen whose own panels show something a turn can change had no way to know — which is every plugin with a chat beside a list. so onTurnComplete is a prop now, composed with the platform's own handling rather than replacing it. called for EVERY turn, outside the hadToolCalls guard, because "did anything I care about change" is a question about the caller's screen and not about this one — it gets the flag and decides. read through a ref: a caller passing an inline arrow would otherwise rebuild the callback every render, and it goes into EmbeddableChat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { useWorkspace } from '../../components/Workspace';
|
||||
import { useActiveChatSession, useFilesRefresh, usePreviewRefresh } from '../../channels';
|
||||
@@ -164,9 +164,26 @@ type ChatPanelWrapperProps = {
|
||||
* platform mounts, not a caller that could pass this.
|
||||
*/
|
||||
chatContext?: { context: string; contextId?: string };
|
||||
/**
|
||||
* Called when a turn ends, with whether it ran any tools.
|
||||
*
|
||||
* The platform's own refreshes — the file tree, the preview — always happen; this is composed on top for
|
||||
* a screen whose panels show something a turn can change. A plugin's lists are the case: an agent that
|
||||
* deletes a proxy host leaves every panel beside it describing a world that no longer exists, and the
|
||||
* only signal a panel gets is this one.
|
||||
*
|
||||
* `hadToolCalls` is the whole discrimination worth making. A turn that only talked changed nothing, and
|
||||
* re-reading after every sentence would put a request behind every reply.
|
||||
*/
|
||||
onTurnComplete?: (hadToolCalls: boolean) => void;
|
||||
};
|
||||
|
||||
export const ChatPanelWrapper = ({ panelId, promptPrefix, chatContext: declared }: ChatPanelWrapperProps) => {
|
||||
export const ChatPanelWrapper = ({
|
||||
panelId,
|
||||
promptPrefix,
|
||||
chatContext: declared,
|
||||
onTurnComplete: alsoOnTurnComplete,
|
||||
}: ChatPanelWrapperProps) => {
|
||||
const { workspace, cwd, root } = useWorkspace();
|
||||
const scoped = cwd !== '~';
|
||||
|
||||
@@ -198,12 +215,20 @@ export const ChatPanelWrapper = ({ panelId, promptPrefix, chatContext: declared
|
||||
const [, setPreviewRefresh] = usePreviewRefresh();
|
||||
const [, bumpFilesRefresh] = useFilesRefresh();
|
||||
|
||||
// The caller's handler is read through a ref rather than named as a dependency: a plugin passing an
|
||||
// inline arrow would otherwise rebuild this callback every render, and it goes into EmbeddableChat.
|
||||
const alsoRef = useRef(alsoOnTurnComplete);
|
||||
alsoRef.current = alsoOnTurnComplete;
|
||||
|
||||
const onTurnComplete = useCallback(
|
||||
(hadToolCalls: boolean) => {
|
||||
if (hadToolCalls) {
|
||||
setPreviewRefresh(Date.now());
|
||||
bumpFilesRefresh();
|
||||
}
|
||||
// Outside the `hadToolCalls` guard deliberately — the caller is told about every turn and decides
|
||||
// for itself, because "did anything change" is a question about that screen, not about this one.
|
||||
alsoRef.current?.(hadToolCalls);
|
||||
},
|
||||
[setPreviewRefresh, bumpFilesRefresh],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user