story-kit: merge 174_story_constrain_thinking_traces_in_chat_panel

This commit is contained in:
Dave
2026-02-25 09:32:48 +00:00
parent 4b161d7c87
commit 42c40209d2
5 changed files with 204 additions and 68 deletions

View File

@@ -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;

View File

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