story-kit: merge 174_story_constrain_thinking_traces_in_chat_panel
This commit is contained in:
@@ -69,7 +69,9 @@ export type WsResponse =
|
||||
/** Heartbeat response confirming the connection is alive. */
|
||||
| { type: "pong" }
|
||||
/** Sent on connect when the project still needs onboarding (specs are placeholders). */
|
||||
| { type: "onboarding_status"; needs_onboarding: boolean };
|
||||
| { type: "onboarding_status"; needs_onboarding: boolean }
|
||||
/** Streaming thinking token from an extended-thinking block, separate from regular text. */
|
||||
| { type: "thinking_token"; content: string };
|
||||
|
||||
export interface ProviderConfig {
|
||||
provider: string;
|
||||
@@ -270,6 +272,7 @@ export class ChatWebSocket {
|
||||
private static refCount = 0;
|
||||
private socket?: WebSocket;
|
||||
private onToken?: (content: string) => void;
|
||||
private onThinkingToken?: (content: string) => void;
|
||||
private onUpdate?: (messages: Message[]) => void;
|
||||
private onSessionId?: (sessionId: string) => void;
|
||||
private onError?: (message: string) => void;
|
||||
@@ -339,6 +342,8 @@ export class ChatWebSocket {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as WsResponse;
|
||||
if (data.type === "token") this.onToken?.(data.content);
|
||||
if (data.type === "thinking_token")
|
||||
this.onThinkingToken?.(data.content);
|
||||
if (data.type === "update") this.onUpdate?.(data.messages);
|
||||
if (data.type === "session_id") this.onSessionId?.(data.session_id);
|
||||
if (data.type === "error") this.onError?.(data.message);
|
||||
@@ -401,6 +406,7 @@ export class ChatWebSocket {
|
||||
connect(
|
||||
handlers: {
|
||||
onToken?: (content: string) => void;
|
||||
onThinkingToken?: (content: string) => void;
|
||||
onUpdate?: (messages: Message[]) => void;
|
||||
onSessionId?: (sessionId: string) => void;
|
||||
onError?: (message: string) => void;
|
||||
@@ -423,6 +429,7 @@ export class ChatWebSocket {
|
||||
wsPath = DEFAULT_WS_PATH,
|
||||
) {
|
||||
this.onToken = handlers.onToken;
|
||||
this.onThinkingToken = handlers.onThinkingToken;
|
||||
this.onUpdate = handlers.onUpdate;
|
||||
this.onSessionId = handlers.onSessionId;
|
||||
this.onError = handlers.onError;
|
||||
|
||||
@@ -13,6 +13,56 @@ import { StagePanel } from "./StagePanel";
|
||||
|
||||
const { useCallback, useEffect, useRef, useState } = React;
|
||||
|
||||
/** Fixed-height thinking trace block that auto-scrolls to bottom as text arrives. */
|
||||
function ThinkingBlock({ text }: { text: string }) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (el) {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="chat-thinking-block"
|
||||
ref={scrollRef}
|
||||
style={{
|
||||
maxHeight: "96px",
|
||||
overflowY: "auto",
|
||||
background: "#161b22",
|
||||
border: "1px solid #2d333b",
|
||||
borderRadius: "6px",
|
||||
padding: "6px 10px",
|
||||
fontSize: "0.78em",
|
||||
fontFamily: "monospace",
|
||||
color: "#6e7681",
|
||||
fontStyle: "italic",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
lineHeight: "1.4",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: "0.8em",
|
||||
color: "#444c56",
|
||||
marginBottom: "4px",
|
||||
fontStyle: "normal",
|
||||
letterSpacing: "0.04em",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
thinking
|
||||
</span>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const NARROW_BREAKPOINT = 900;
|
||||
|
||||
function formatToolActivity(toolName: string): string {
|
||||
@@ -64,6 +114,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
const [claudeModels, setClaudeModels] = useState<string[]>([]);
|
||||
const [streamingContent, setStreamingContent] = useState("");
|
||||
const [streamingThinking, setStreamingThinking] = useState("");
|
||||
const [showApiKeyDialog, setShowApiKeyDialog] = useState(false);
|
||||
const [apiKeyInput, setApiKeyInput] = useState("");
|
||||
const [hasAnthropicKey, setHasAnthropicKey] = useState(false);
|
||||
@@ -208,9 +259,13 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
onToken: (content) => {
|
||||
setStreamingContent((prev: string) => prev + content);
|
||||
},
|
||||
onThinkingToken: (content) => {
|
||||
setStreamingThinking((prev: string) => prev + content);
|
||||
},
|
||||
onUpdate: (history) => {
|
||||
setMessages(history);
|
||||
setStreamingContent("");
|
||||
setStreamingThinking("");
|
||||
const last = history[history.length - 1];
|
||||
if (last?.role === "assistant" && !last.tool_calls) {
|
||||
setLoading(false);
|
||||
@@ -303,7 +358,8 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
lastScrollTopRef.current = currentScrollTop;
|
||||
};
|
||||
|
||||
const autoScrollKey = messages.length + streamingContent.length;
|
||||
const autoScrollKey =
|
||||
messages.length + streamingContent.length + streamingThinking.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -351,6 +407,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
setStreamingContent("");
|
||||
}
|
||||
|
||||
setStreamingThinking("");
|
||||
setLoading(false);
|
||||
setActivityStatus(null);
|
||||
} catch (e) {
|
||||
@@ -395,6 +452,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
}
|
||||
setLoading(true);
|
||||
setStreamingContent("");
|
||||
setStreamingThinking("");
|
||||
setActivityStatus(null);
|
||||
|
||||
try {
|
||||
@@ -471,6 +529,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
|
||||
clearMessages();
|
||||
setStreamingContent("");
|
||||
setStreamingThinking("");
|
||||
setLoading(false);
|
||||
setActivityStatus(null);
|
||||
setClaudeSessionId(null);
|
||||
@@ -761,6 +820,9 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{loading && streamingThinking && (
|
||||
<ThinkingBlock text={streamingThinking} />
|
||||
)}
|
||||
{loading && streamingContent && (
|
||||
<div
|
||||
style={{
|
||||
@@ -817,21 +879,23 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{loading && (activityStatus != null || !streamingContent) && (
|
||||
<div
|
||||
data-testid="activity-indicator"
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
color: "#888",
|
||||
fontSize: "0.9em",
|
||||
marginTop: "10px",
|
||||
}}
|
||||
>
|
||||
<span className="pulse">
|
||||
{activityStatus ?? "Thinking..."}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{loading &&
|
||||
(activityStatus != null ||
|
||||
(!streamingContent && !streamingThinking)) && (
|
||||
<div
|
||||
data-testid="activity-indicator"
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
color: "#888",
|
||||
fontSize: "0.9em",
|
||||
marginTop: "10px",
|
||||
}}
|
||||
>
|
||||
<span className="pulse">
|
||||
{activityStatus ?? "Thinking..."}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1075,7 +1139,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
stateVersion={agentStateVersion}
|
||||
/>
|
||||
|
||||
<StagePanel title="Done" items={pipeline.done} />
|
||||
<StagePanel title="Done" items={pipeline.done ?? []} />
|
||||
<StagePanel title="To Merge" items={pipeline.merge} />
|
||||
<StagePanel title="QA" items={pipeline.qa} />
|
||||
<StagePanel title="Current" items={pipeline.current} />
|
||||
|
||||
Reference in New Issue
Block a user