claudeship 0.2.18 → 0.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/server/package.json +1 -1
- package/apps/web/.next/BUILD_ID +1 -1
- package/apps/web/.next/app-build-manifest.json +6 -6
- package/apps/web/.next/app-path-routes-manifest.json +1 -1
- package/apps/web/.next/build-manifest.json +2 -2
- package/apps/web/.next/cache/.previewinfo +1 -1
- package/apps/web/.next/cache/.rscinfo +1 -1
- package/apps/web/.next/cache/.tsbuildinfo +1 -1
- package/apps/web/.next/cache/config.json +3 -3
- package/apps/web/.next/cache/eslint/.cache_j3uhuz +1 -1
- package/apps/web/.next/cache/webpack/client-production/0.pack +0 -0
- package/apps/web/.next/cache/webpack/client-production/index.pack +0 -0
- package/apps/web/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/apps/web/.next/cache/webpack/server-production/0.pack +0 -0
- package/apps/web/.next/cache/webpack/server-production/index.pack +0 -0
- package/apps/web/.next/prerender-manifest.json +9 -9
- package/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/apps/web/.next/server/app/_not-found.html +1 -1
- package/apps/web/.next/server/app/_not-found.rsc +2 -2
- package/apps/web/.next/server/app/index.html +1 -1
- package/apps/web/.next/server/app/index.rsc +3 -3
- package/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/apps/web/.next/server/app/project/[id]/page.js +1 -1
- package/apps/web/.next/server/app/project/[id]/page_client-reference-manifest.js +1 -1
- package/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/apps/web/.next/server/app/settings.html +1 -1
- package/apps/web/.next/server/app/settings.rsc +2 -2
- package/apps/web/.next/server/app-paths-manifest.json +1 -1
- package/apps/web/.next/server/pages/404.html +1 -1
- package/apps/web/.next/server/pages/500.html +1 -1
- package/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/apps/web/.next/static/chunks/87-e65fb39b36fc5ac8.js +1 -0
- package/apps/web/.next/static/chunks/app/page-8310956d8eae9762.js +1 -0
- package/apps/web/.next/static/chunks/app/project/[id]/page-3d9d2622b2801ab0.js +1 -0
- package/apps/web/.next/static/css/b92103813bcb2a3c.css +3 -0
- package/apps/web/.next/trace +18 -18
- package/apps/web/package.json +1 -1
- package/apps/web/src/components/chat/MessageInput.tsx +5 -8
- package/apps/web/src/components/chat/QueuePreview.tsx +98 -0
- package/apps/web/src/components/chat/StreamingMessage.tsx +126 -20
- package/apps/web/src/stores/useChatStore.ts +26 -6
- package/package.json +1 -1
- package/apps/web/.next/static/chunks/574-8fbf7d67ac55f996.js +0 -1
- package/apps/web/.next/static/chunks/app/page-637614a00e18faa8.js +0 -1
- package/apps/web/.next/static/chunks/app/project/[id]/page-c28098a9b8a94336.js +0 -1
- package/apps/web/.next/static/css/0a24552d9794f8c8.css +0 -3
- /package/apps/web/.next/static/{_uY1B78_jnG7JywlmeEpm → 91tvQbwE6MrVEkEolpLDW}/_buildManifest.js +0 -0
- /package/apps/web/.next/static/{_uY1B78_jnG7JywlmeEpm → 91tvQbwE6MrVEkEolpLDW}/_ssgManifest.js +0 -0
package/apps/web/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
|
|
6
6
|
import { useTranslation } from "@/lib/i18n";
|
|
7
7
|
import { ModeToggle } from "./ModeToggle";
|
|
8
8
|
import { FilePreview } from "./FilePreview";
|
|
9
|
+
import { QueuePreview } from "./QueuePreview";
|
|
9
10
|
import { useChatStore } from "@/stores/useChatStore";
|
|
10
11
|
|
|
11
12
|
interface MessageInputProps {
|
|
@@ -34,7 +35,7 @@ const MAX_FILES = 5;
|
|
|
34
35
|
|
|
35
36
|
export function MessageInput({ onSend, projectId, disabled, isStreaming, queueCount = 0 }: MessageInputProps) {
|
|
36
37
|
const { t } = useTranslation();
|
|
37
|
-
const { mode, attachedFiles, addFiles, removeFile, uploadFiles, isUploading } = useChatStore();
|
|
38
|
+
const { mode, attachedFiles, addFiles, removeFile, uploadFiles, isUploading, messageQueue, deleteFromQueue } = useChatStore();
|
|
38
39
|
const [content, setContent] = useState("");
|
|
39
40
|
const [isDragging, setIsDragging] = useState(false);
|
|
40
41
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
@@ -173,7 +174,7 @@ export function MessageInput({ onSend, projectId, disabled, isStreaming, queueCo
|
|
|
173
174
|
</Button>
|
|
174
175
|
</div>
|
|
175
176
|
{(isStreaming || isUploading) && (
|
|
176
|
-
<div className="px-4 pb-
|
|
177
|
+
<div className="px-4 pb-2 flex items-center gap-2 text-xs text-muted-foreground">
|
|
177
178
|
{isUploading ? (
|
|
178
179
|
<span className="flex items-center gap-1">
|
|
179
180
|
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-blue-500" />
|
|
@@ -185,14 +186,10 @@ export function MessageInput({ onSend, projectId, disabled, isStreaming, queueCo
|
|
|
185
186
|
{t("chat.thinking")}
|
|
186
187
|
</span>
|
|
187
188
|
)}
|
|
188
|
-
{queueCount > 0 && (
|
|
189
|
-
<span className="flex items-center gap-1 text-blue-500">
|
|
190
|
-
<Clock className="h-3 w-3" />
|
|
191
|
-
{t("chat.queueCount", { count: queueCount })}
|
|
192
|
-
</span>
|
|
193
|
-
)}
|
|
194
189
|
</div>
|
|
195
190
|
)}
|
|
191
|
+
{/* Queue Preview */}
|
|
192
|
+
<QueuePreview items={messageQueue} onDelete={deleteFromQueue} />
|
|
196
193
|
</div>
|
|
197
194
|
);
|
|
198
195
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ChevronDown, ChevronUp, Trash2, Play } from "lucide-react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { useChatStore, type QueuedMessage } from "@/stores/useChatStore";
|
|
7
|
+
|
|
8
|
+
interface QueuePreviewProps {
|
|
9
|
+
items: QueuedMessage[];
|
|
10
|
+
onDelete?: (id: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function QueuePreview({ items, onDelete }: QueuePreviewProps) {
|
|
14
|
+
const [isExpanded, setIsExpanded] = useState(true);
|
|
15
|
+
|
|
16
|
+
if (items.length === 0) return null;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="border-t bg-muted/30">
|
|
20
|
+
{/* Header */}
|
|
21
|
+
<button
|
|
22
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
23
|
+
className="flex items-center justify-between w-full px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
|
24
|
+
>
|
|
25
|
+
<div className="flex items-center gap-2">
|
|
26
|
+
{isExpanded ? (
|
|
27
|
+
<ChevronUp className="h-4 w-4" />
|
|
28
|
+
) : (
|
|
29
|
+
<ChevronDown className="h-4 w-4" />
|
|
30
|
+
)}
|
|
31
|
+
<span>Queue ({items.length})</span>
|
|
32
|
+
</div>
|
|
33
|
+
</button>
|
|
34
|
+
|
|
35
|
+
{/* Queue Items */}
|
|
36
|
+
{isExpanded && (
|
|
37
|
+
<div className="px-4 pb-3 space-y-2">
|
|
38
|
+
{items.map((item, index) => (
|
|
39
|
+
<QueueItem
|
|
40
|
+
key={item.id}
|
|
41
|
+
item={item}
|
|
42
|
+
isNext={index === 0}
|
|
43
|
+
onDelete={onDelete}
|
|
44
|
+
/>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface QueueItemProps {
|
|
53
|
+
item: QueuedMessage;
|
|
54
|
+
isNext: boolean;
|
|
55
|
+
onDelete?: (id: string) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function QueueItem({ item, isNext, onDelete }: QueueItemProps) {
|
|
59
|
+
const isProcessing = item.status === "processing";
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
className={`relative flex items-start gap-2 p-3 rounded-md border ${
|
|
64
|
+
isProcessing
|
|
65
|
+
? "bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800"
|
|
66
|
+
: "bg-background border-border"
|
|
67
|
+
}`}
|
|
68
|
+
>
|
|
69
|
+
<div className="flex-1 min-w-0">
|
|
70
|
+
<p className="text-sm line-clamp-2 break-words">
|
|
71
|
+
{item.content}
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
75
|
+
{isNext && !isProcessing && (
|
|
76
|
+
<span className="text-xs font-medium text-blue-600 dark:text-blue-400 px-1.5 py-0.5 bg-blue-100 dark:bg-blue-900 rounded">
|
|
77
|
+
Next
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
{isProcessing && (
|
|
81
|
+
<span className="text-xs font-medium text-blue-600 dark:text-blue-400 px-1.5 py-0.5 bg-blue-100 dark:bg-blue-900 rounded animate-pulse">
|
|
82
|
+
Processing
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
{onDelete && !isProcessing && (
|
|
86
|
+
<Button
|
|
87
|
+
variant="ghost"
|
|
88
|
+
size="icon"
|
|
89
|
+
className="h-6 w-6 text-muted-foreground hover:text-destructive"
|
|
90
|
+
onClick={() => onDelete(item.id)}
|
|
91
|
+
>
|
|
92
|
+
<Trash2 className="h-3 w-3" />
|
|
93
|
+
</Button>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useState } from "react";
|
|
3
4
|
import { useChatStore, type StreamingBlock } from "@/stores/useChatStore";
|
|
4
5
|
import {
|
|
5
6
|
FileText,
|
|
@@ -12,6 +13,8 @@ import {
|
|
|
12
13
|
Loader2,
|
|
13
14
|
ListTodo,
|
|
14
15
|
Bot,
|
|
16
|
+
ChevronDown,
|
|
17
|
+
ChevronUp,
|
|
15
18
|
} from "lucide-react";
|
|
16
19
|
import { MarkdownRenderer } from "./MarkdownRenderer";
|
|
17
20
|
import { AskUserQuestionBlock } from "./AskUserQuestionBlock";
|
|
@@ -22,6 +25,9 @@ interface StreamingMessageProps {
|
|
|
22
25
|
projectId: string;
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
const COLLAPSE_THRESHOLD = 5; // Number of tool blocks before collapsing
|
|
29
|
+
const VISIBLE_WHEN_COLLAPSED = 2; // Number of items to show at start and end when collapsed
|
|
30
|
+
|
|
25
31
|
const toolIcons: Record<string, React.ReactNode> = {
|
|
26
32
|
Read: <FileText className="h-4 w-4" />,
|
|
27
33
|
Glob: <FolderSearch className="h-4 w-4" />,
|
|
@@ -89,6 +95,13 @@ function TextBlock({ content }: { content: string }) {
|
|
|
89
95
|
return <MarkdownRenderer content={content} />;
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
function formatDuration(ms: number): string {
|
|
99
|
+
if (ms < 1000) {
|
|
100
|
+
return `${ms}ms`;
|
|
101
|
+
}
|
|
102
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
function ToolUseBlock({ block }: { block: StreamingBlock }) {
|
|
93
106
|
const isRunning = block.status === "running";
|
|
94
107
|
const toolName = block.tool?.name || "Unknown";
|
|
@@ -113,17 +126,106 @@ function ToolUseBlock({ block }: { block: StreamingBlock }) {
|
|
|
113
126
|
{getToolDisplayName(toolName)}
|
|
114
127
|
</span>
|
|
115
128
|
{getToolDescription(block) && (
|
|
116
|
-
<span className={`truncate ${isRunning ? "text-blue-600 dark:text-blue-400" : "text-muted-foreground"}`}>
|
|
129
|
+
<span className={`truncate flex-1 ${isRunning ? "text-blue-600 dark:text-blue-400" : "text-muted-foreground"}`}>
|
|
117
130
|
{getToolDescription(block)}
|
|
118
131
|
</span>
|
|
119
132
|
)}
|
|
133
|
+
{block.duration !== undefined && (
|
|
134
|
+
<span className="text-xs text-muted-foreground ml-auto tabular-nums">
|
|
135
|
+
({formatDuration(block.duration)})
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
120
138
|
</div>
|
|
121
139
|
);
|
|
122
140
|
}
|
|
123
141
|
|
|
142
|
+
interface ToolBlockGroupProps {
|
|
143
|
+
blocks: StreamingBlock[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function ToolBlockGroup({ blocks }: ToolBlockGroupProps) {
|
|
147
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
148
|
+
const shouldCollapse = blocks.length > COLLAPSE_THRESHOLD;
|
|
149
|
+
const hiddenCount = blocks.length - (VISIBLE_WHEN_COLLAPSED * 2);
|
|
150
|
+
|
|
151
|
+
if (!shouldCollapse || isExpanded) {
|
|
152
|
+
return (
|
|
153
|
+
<div className="space-y-1">
|
|
154
|
+
{blocks.map((block) => (
|
|
155
|
+
<ToolUseBlock key={block.id} block={block} />
|
|
156
|
+
))}
|
|
157
|
+
{shouldCollapse && isExpanded && (
|
|
158
|
+
<button
|
|
159
|
+
onClick={() => setIsExpanded(false)}
|
|
160
|
+
className="flex items-center gap-1 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
161
|
+
>
|
|
162
|
+
<ChevronUp className="h-4 w-4" />
|
|
163
|
+
<span>접기</span>
|
|
164
|
+
</button>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Show first few, collapse button, then last few
|
|
171
|
+
const firstBlocks = blocks.slice(0, VISIBLE_WHEN_COLLAPSED);
|
|
172
|
+
const lastBlocks = blocks.slice(-VISIBLE_WHEN_COLLAPSED);
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div className="space-y-1">
|
|
176
|
+
{firstBlocks.map((block) => (
|
|
177
|
+
<ToolUseBlock key={block.id} block={block} />
|
|
178
|
+
))}
|
|
179
|
+
<button
|
|
180
|
+
onClick={() => setIsExpanded(true)}
|
|
181
|
+
className="flex items-center gap-1 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors w-full justify-center border border-dashed border-border rounded-md hover:bg-muted/50"
|
|
182
|
+
>
|
|
183
|
+
<ChevronDown className="h-4 w-4" />
|
|
184
|
+
<span>{hiddenCount}개 더 보기</span>
|
|
185
|
+
</button>
|
|
186
|
+
{lastBlocks.map((block) => (
|
|
187
|
+
<ToolUseBlock key={block.id} block={block} />
|
|
188
|
+
))}
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Group consecutive blocks by type
|
|
194
|
+
interface BlockGroup {
|
|
195
|
+
type: "tool_group" | "other";
|
|
196
|
+
blocks: StreamingBlock[];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function groupBlocks(blocks: StreamingBlock[]): BlockGroup[] {
|
|
200
|
+
const groups: BlockGroup[] = [];
|
|
201
|
+
let currentToolGroup: StreamingBlock[] = [];
|
|
202
|
+
|
|
203
|
+
for (const block of blocks) {
|
|
204
|
+
if (block.type === "tool_use") {
|
|
205
|
+
currentToolGroup.push(block);
|
|
206
|
+
} else {
|
|
207
|
+
// Flush current tool group if exists
|
|
208
|
+
if (currentToolGroup.length > 0) {
|
|
209
|
+
groups.push({ type: "tool_group", blocks: currentToolGroup });
|
|
210
|
+
currentToolGroup = [];
|
|
211
|
+
}
|
|
212
|
+
// Add non-tool block as single item
|
|
213
|
+
groups.push({ type: "other", blocks: [block] });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Flush remaining tool group
|
|
218
|
+
if (currentToolGroup.length > 0) {
|
|
219
|
+
groups.push({ type: "tool_group", blocks: currentToolGroup });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return groups;
|
|
223
|
+
}
|
|
224
|
+
|
|
124
225
|
export function StreamingMessage({ blocks, isStreaming = true, projectId }: StreamingMessageProps) {
|
|
125
226
|
const hasBlocks = blocks.length > 0;
|
|
126
|
-
const { respondToQuestion
|
|
227
|
+
const { respondToQuestion } = useChatStore();
|
|
228
|
+
const blockGroups = groupBlocks(blocks);
|
|
127
229
|
|
|
128
230
|
const handleQuestionSubmit = (answers: Record<string, string>) => {
|
|
129
231
|
respondToQuestion(projectId, answers);
|
|
@@ -135,25 +237,29 @@ export function StreamingMessage({ blocks, isStreaming = true, projectId }: Stre
|
|
|
135
237
|
AI
|
|
136
238
|
</div>
|
|
137
239
|
<div className="flex-1 space-y-2 overflow-hidden">
|
|
138
|
-
{/* Render
|
|
139
|
-
{
|
|
140
|
-
if (
|
|
141
|
-
return <
|
|
142
|
-
}
|
|
143
|
-
if (block.type === "tool_use") {
|
|
144
|
-
return <ToolUseBlock key={block.id} block={block} />;
|
|
240
|
+
{/* Render block groups */}
|
|
241
|
+
{blockGroups.map((group, groupIndex) => {
|
|
242
|
+
if (group.type === "tool_group") {
|
|
243
|
+
return <ToolBlockGroup key={`group-${groupIndex}`} blocks={group.blocks} />;
|
|
145
244
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
245
|
+
|
|
246
|
+
// Render other blocks individually
|
|
247
|
+
return group.blocks.map((block) => {
|
|
248
|
+
if (block.type === "text") {
|
|
249
|
+
return <TextBlock key={block.id} content={block.content || ""} />;
|
|
250
|
+
}
|
|
251
|
+
if (block.type === "ask_user_question" && block.askUserQuestion) {
|
|
252
|
+
return (
|
|
253
|
+
<AskUserQuestionBlock
|
|
254
|
+
key={block.id}
|
|
255
|
+
data={block.askUserQuestion}
|
|
256
|
+
isWaiting={block.status === "waiting"}
|
|
257
|
+
onSubmit={handleQuestionSubmit}
|
|
258
|
+
/>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
});
|
|
157
263
|
})}
|
|
158
264
|
|
|
159
265
|
{/* Show thinking state when streaming but no blocks yet */}
|
|
@@ -28,6 +28,8 @@ export interface StreamingBlock {
|
|
|
28
28
|
askUserQuestion?: AskUserQuestionData;
|
|
29
29
|
status?: "running" | "completed" | "error" | "waiting";
|
|
30
30
|
result?: string;
|
|
31
|
+
timestamp?: number; // Unix timestamp in milliseconds
|
|
32
|
+
duration?: number; // Duration in milliseconds
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export interface QueuedMessage {
|
|
@@ -60,6 +62,7 @@ interface ChatState {
|
|
|
60
62
|
fetchActiveSession: (projectId: string) => Promise<void>;
|
|
61
63
|
sendMessage: (projectId: string, content: string, fromQueue?: boolean) => Promise<void>;
|
|
62
64
|
queueMessage: (projectId: string, content: string) => void;
|
|
65
|
+
deleteFromQueue: (id: string) => void;
|
|
63
66
|
processQueue: (projectId: string) => Promise<void>;
|
|
64
67
|
respondToQuestion: (projectId: string, answers: Record<string, string>) => Promise<void>;
|
|
65
68
|
addMessage: (message: ChatMessage) => void;
|
|
@@ -263,19 +266,24 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
263
266
|
input: data.tool.input,
|
|
264
267
|
},
|
|
265
268
|
status: "running",
|
|
269
|
+
timestamp: Date.now(),
|
|
266
270
|
};
|
|
267
271
|
set((state) => ({
|
|
268
272
|
streamingBlocks: [...state.streamingBlocks, toolBlock],
|
|
269
273
|
}));
|
|
270
274
|
} else if (data.type === "tool_result") {
|
|
275
|
+
const now = Date.now();
|
|
271
276
|
set((state) => {
|
|
272
277
|
const blocks = [...state.streamingBlocks];
|
|
273
278
|
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
274
|
-
|
|
279
|
+
const block = blocks[i];
|
|
280
|
+
if (block.type === "tool_use" && block.status === "running") {
|
|
281
|
+
const duration = block.timestamp ? now - block.timestamp : undefined;
|
|
275
282
|
blocks[i] = {
|
|
276
|
-
...
|
|
283
|
+
...block,
|
|
277
284
|
status: "completed",
|
|
278
285
|
result: data.content,
|
|
286
|
+
duration,
|
|
279
287
|
};
|
|
280
288
|
break;
|
|
281
289
|
}
|
|
@@ -341,6 +349,13 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
341
349
|
}));
|
|
342
350
|
},
|
|
343
351
|
|
|
352
|
+
// Delete a message from the queue
|
|
353
|
+
deleteFromQueue: (id: string) => {
|
|
354
|
+
set((state) => ({
|
|
355
|
+
messageQueue: state.messageQueue.filter((m) => m.id !== id),
|
|
356
|
+
}));
|
|
357
|
+
},
|
|
358
|
+
|
|
344
359
|
// Process the message queue sequentially
|
|
345
360
|
processQueue: async (projectId: string) => {
|
|
346
361
|
const state = get();
|
|
@@ -487,7 +502,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
487
502
|
}));
|
|
488
503
|
}
|
|
489
504
|
} else if (data.type === "tool_use" && data.tool) {
|
|
490
|
-
// Add tool_use block
|
|
505
|
+
// Add tool_use block with timestamp
|
|
491
506
|
const toolBlock: StreamingBlock = {
|
|
492
507
|
id: `tool-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
493
508
|
type: "tool_use",
|
|
@@ -496,21 +511,26 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
496
511
|
input: data.tool.input,
|
|
497
512
|
},
|
|
498
513
|
status: "running",
|
|
514
|
+
timestamp: Date.now(),
|
|
499
515
|
};
|
|
500
516
|
set((state) => ({
|
|
501
517
|
streamingBlocks: [...state.streamingBlocks, toolBlock],
|
|
502
518
|
}));
|
|
503
519
|
} else if (data.type === "tool_result") {
|
|
504
|
-
// Update the last running tool_use block with result
|
|
520
|
+
// Update the last running tool_use block with result and duration
|
|
521
|
+
const now = Date.now();
|
|
505
522
|
set((state) => {
|
|
506
523
|
const blocks = [...state.streamingBlocks];
|
|
507
524
|
// Find the last running tool_use block
|
|
508
525
|
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
509
|
-
|
|
526
|
+
const block = blocks[i];
|
|
527
|
+
if (block.type === "tool_use" && block.status === "running") {
|
|
528
|
+
const duration = block.timestamp ? now - block.timestamp : undefined;
|
|
510
529
|
blocks[i] = {
|
|
511
|
-
...
|
|
530
|
+
...block,
|
|
512
531
|
status: "completed",
|
|
513
532
|
result: data.content,
|
|
533
|
+
duration,
|
|
514
534
|
};
|
|
515
535
|
break;
|
|
516
536
|
}
|