Skip to content

Commit 36fc601

Browse files
milispclaude
andcommitted
feat: implement real-time streaming message system
- Add comprehensive streaming infrastructure with StreamController and collectors - Implement agent_message_delta event handling for live response updates - Add StreamingMessage component with proper markdown rendering and cursor animation - Enable show_raw_agent_reasoning=true in codex client for streaming support - Update UI components to handle streaming state with proper text wrapping - Add streaming CSS animations for smooth line-by-line display - Update README to reflect streaming feature completion - Add release workflow for cross-platform builds 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 85a3110 commit 36fc601

File tree

17 files changed

+744
-20
lines changed

17 files changed

+744
-20
lines changed

.github/workflows/release.yml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Publish Cross-Platform
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
workflow_dispatch:
8+
9+
jobs:
10+
build:
11+
strategy:
12+
matrix:
13+
platform:
14+
- os: ubuntu-latest
15+
target: x86_64-unknown-linux-gnu
16+
name: Linux x86_64
17+
artifact-name: linux-x64
18+
artifact-path: |
19+
src-tauri/target/x86_64-unknown-linux-gnu/release/**/*.deb
20+
src-tauri/target/x86_64-unknown-linux-gnu/release/**/*.rpm
21+
src-tauri/target/x86_64-unknown-linux-gnu/release/**/*.AppImage
22+
- os: windows-latest
23+
target: x86_64-pc-windows-msvc
24+
name: Windows x86_64
25+
artifact-name: MCP-Linker-Windows
26+
artifact-path: |
27+
src-tauri/target/release/bundle/**/*.exe
28+
src-tauri/target/release/bundle/**/*.msi
29+
30+
runs-on: ${{ matrix.platform.os }}
31+
name: Build ${{ matrix.platform.name }}
32+
33+
steps:
34+
- name: Checkout code
35+
uses: actions/checkout@v4
36+
37+
- name: Install Linux dependencies
38+
if: runner.os == 'Linux'
39+
run: |
40+
sudo apt update
41+
sudo apt-get install -y \
42+
pkg-config \
43+
libwebkit2gtk-4.1-dev \
44+
libgtk-3-dev \
45+
libssl-dev \
46+
libayatana-appindicator3-dev \
47+
librsvg2-dev
48+
49+
- name: Install Rust toolchain
50+
uses: dtolnay/rust-toolchain@stable
51+
52+
- name: Setup Node.js
53+
uses: actions/setup-node@v4
54+
with:
55+
node-version: 20
56+
57+
- name: Install Bun
58+
uses: oven-sh/setup-bun@v1
59+
60+
- name: Install dependencies
61+
run: bun install
62+
63+
- name: Build Tauri App
64+
run: bun tauri build ${{ runner.os == 'Linux' && format('--target {0}', matrix.platform.target) || '' }}
65+
66+
- name: Upload build artifacts
67+
uses: actions/upload-artifact@v4
68+
with:
69+
name: ${{ matrix.platform.artifact-name }}
70+
path: ${{ matrix.platform.artifact-path }}
71+
72+
- name: Upload to GitHub Release
73+
uses: softprops/action-gh-release@v2
74+
if: startsWith(github.ref, 'refs/tags/v')
75+
with:
76+
files: ${{ matrix.platform.artifact-path }}
77+
env:
78+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ A modern, multi-session GUI application for the `Codex CLI` built with Tauri v2,
1919
- Switch between sessions without interrupting ongoing conversations
2020
- Persistent session storage with automatic restoration on app restart
2121

22-
### 💬 **Real-Time Streaming** - Todo
22+
### 💬 **Real-Time Streaming**
2323
- Live streaming responses for immediate feedback
2424
- Character-by-character message updates as AI generates responses
2525
- No more waiting for complete responses - see results as they appear
@@ -131,7 +131,7 @@ bun tauri build
131131
-**Command execution** with approval workflows
132132
-**Multiple AI providers** (OpenAI, OSS models via Ollama)
133133
-**Working directory context** for project-aware assistance
134-
- [ ] **Streaming responses** for real-time interaction
134+
- **Streaming responses** for real-time interaction - by config show_raw_agent_reasoning=true
135135

136136
## 🛠️ Development
137137

public/hello.txt

Whitespace-only changes.

src-tauri/src/codex_client.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ impl CodexClient {
134134
cmd.arg("-c").arg(sandbox_config);
135135
}
136136

137+
// Enable streaming by setting show_raw_agent_reasoning=true
138+
// This is required for agent_message_delta events to be generated
139+
cmd.arg("-c").arg("show_raw_agent_reasoning=true");
140+
137141
// Set working directory for the process
138142
if !config.working_directory.is_empty() {
139143
cmd.current_dir(&config.working_directory);

src/App.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,34 @@
117117
body {
118118
@apply bg-background text-foreground;
119119
}
120+
}
121+
122+
/* Streaming message styles */
123+
.streaming-message {
124+
position: relative;
125+
}
126+
127+
.streaming-cursor {
128+
animation: blink 1s infinite;
129+
}
130+
131+
@keyframes blink {
132+
0%, 50% { opacity: 1; }
133+
51%, 100% { opacity: 0; }
134+
}
135+
136+
/* Line-by-line animation */
137+
.streaming-line-enter {
138+
animation: slideIn 0.2s ease-out;
139+
}
140+
141+
@keyframes slideIn {
142+
from {
143+
opacity: 0;
144+
transform: translateY(10px);
145+
}
146+
to {
147+
opacity: 1;
148+
transform: translateY(0);
149+
}
120150
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React from 'react';
2+
import { ChatMessage } from '@/types/chat';
3+
import { cn } from '@/lib/utils';
4+
5+
interface StreamingMessageProps {
6+
message: ChatMessage;
7+
className?: string;
8+
}
9+
10+
export const StreamingMessage: React.FC<StreamingMessageProps> = ({
11+
message,
12+
className
13+
}) => {
14+
return (
15+
<div className={cn(
16+
"whitespace-pre-wrap break-words overflow-wrap-anywhere min-w-0",
17+
message.isStreaming && "streaming-message",
18+
className
19+
)}>
20+
{/* Simple markdown-like rendering */}
21+
{message.content.split('\n').map((line, index) => {
22+
// Handle headings
23+
if (line.startsWith('##')) {
24+
return (
25+
<h2 key={index} className="text-xl font-bold mt-4 mb-2 break-words">
26+
{line.replace(/^##\s*/, '')}
27+
</h2>
28+
);
29+
}
30+
if (line.startsWith('#')) {
31+
return (
32+
<h1 key={index} className="text-2xl font-bold mt-4 mb-2 break-words">
33+
{line.replace(/^#\s*/, '')}
34+
</h1>
35+
);
36+
}
37+
38+
// Handle code blocks
39+
if (line.startsWith('```')) {
40+
return (
41+
<div key={index} className="bg-gray-100 dark:bg-gray-800 p-2 rounded font-mono text-sm my-2 break-words overflow-x-auto">
42+
{line.replace(/^```/, '')}
43+
</div>
44+
);
45+
}
46+
47+
// Handle list items
48+
if (line.startsWith('- ')) {
49+
return (
50+
<li key={index} className="ml-4 list-disc break-words">
51+
{line.replace(/^-\s*/, '')}
52+
</li>
53+
);
54+
}
55+
56+
// Regular paragraphs
57+
if (line.trim()) {
58+
return (
59+
<p key={index} className="mb-2 break-words">
60+
{line}
61+
</p>
62+
);
63+
}
64+
65+
// Empty lines
66+
return <br key={index} />;
67+
})}
68+
69+
{message.isStreaming && (
70+
<span className="inline-block w-2 h-5 bg-blue-500 animate-pulse ml-1 streaming-cursor" />
71+
)}
72+
</div>
73+
);
74+
};

src/components/chat/ChatInput.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,15 @@ export const ChatInput: React.FC<ChatInputProps> = ({
117117

118118
if (directories.length > 0 && files.length === 0) {
119119
return directories.length === 1
120-
? `Read this folder: ${filePaths}`
121-
: `Read these folders: ${filePaths}`;
120+
? `${filePaths}`
121+
: `${filePaths}`;
122122
} else if (files.length > 0 && directories.length === 0) {
123123
return files.length === 1
124-
? `Read this file: ${filePaths}`
125-
: `Read these files: ${filePaths}`;
124+
? `${filePaths}`
125+
: `${filePaths}`;
126126
} else {
127127
// Mixed files and folders
128-
return `Read these files and folders: ${filePaths}`;
128+
return `${filePaths}`;
129129
}
130130
};
131131

@@ -290,4 +290,4 @@ export const ChatInput: React.FC<ChatInputProps> = ({
290290
</div>
291291
</div>
292292
);
293-
};
293+
};

src/components/chat/MarkdownRenderer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const MarkdownRenderer = memo<MarkdownRendererProps>(({
1515
className = ""
1616
}) => {
1717
return (
18-
<div className={`text-sm text-gray-800 leading-relaxed prose prose-sm max-w-none ${className}`}>
18+
<div className={`text-sm text-gray-800 leading-relaxed prose prose-sm max-w-none break-words ${className}`}>
1919
<ReactMarkdown
2020
remarkPlugins={[remarkGfm]}
2121
rehypePlugins={[rehypePrism]}

src/components/chat/Message.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { memo } from 'react';
22
import { Bot, User, Terminal } from 'lucide-react';
33
import { MessageNoteActions } from './MessageNoteActions';
44
import { MarkdownRenderer } from './MarkdownRenderer';
5+
import { StreamingMessage } from '../StreamingMessage';
56

67
interface NormalizedMessage {
78
id: string;
@@ -62,7 +63,7 @@ export const Message = memo<MessageProps>(({
6263
return (
6364
<div
6465
key={`${normalized.id}-${index}`}
65-
className={`group flex gap-3 p-4 ${isLastMessage ? 'mb-4' : ''}`}
66+
className={`group flex gap-3 p-4 min-w-0 ${isLastMessage ? 'mb-4' : ''}`}
6667
data-message-role={normalized.role}
6768
data-message-timestamp={normalized.timestamp}
6869
>
@@ -101,11 +102,22 @@ export const Message = memo<MessageProps>(({
101102
</div>
102103

103104
{/* Content */}
104-
<div className={`relative rounded-lg border p-3 ${getMessageStyle(normalized.role)}`}>
105-
<MarkdownRenderer content={normalized.content} />
106-
{normalized.isStreaming && (
107-
<span className="inline-block w-2 h-5 bg-current opacity-75 animate-pulse ml-1 align-text-bottom">|</span>
108-
)}
105+
<div className={`relative rounded-lg border p-3 w-full min-w-0 ${getMessageStyle(normalized.role)}`}>
106+
<div className="break-words overflow-wrap-anywhere min-w-0">
107+
{normalized.isStreaming ? (
108+
<StreamingMessage
109+
message={{
110+
id: normalized.id,
111+
role: normalized.role as "user" | "assistant" | "system",
112+
content: normalized.content,
113+
timestamp: normalized.timestamp,
114+
isStreaming: normalized.isStreaming
115+
}}
116+
/>
117+
) : (
118+
<MarkdownRenderer content={normalized.content} />
119+
)}
120+
</div>
109121
</div>
110122
</div>
111123
</div>

src/components/chat/MessageList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export function MessageList({ messages, className = "", isLoading = false, isPen
8888
<TextSelectionMenu />
8989
{/* Messages */}
9090
<div className="flex-1 overflow-y-auto px-4 py-2">
91-
<div className="max-w-4xl mx-auto">
91+
<div className="w-full max-w-full min-w-0">
9292
{normalizedMessages.map((normalizedMessage, index) => (
9393
<Message
9494
key={`${normalizedMessage.id}-${index}`}

0 commit comments

Comments
 (0)