claude-ws 0.1.1 → 0.1.3
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/next.config.ts +5 -0
- package/package.json +18 -6
- package/src/app/api/tasks/[id]/stats/route.ts +19 -5
- package/src/app/globals.css +35 -6
- package/src/app/layout.tsx +9 -6
- package/src/components/claude/code-block.tsx +4 -4
- package/src/components/claude/diff-view.tsx +6 -6
- package/src/components/claude/message-block.tsx +4 -4
- package/src/components/claude/response-renderer.tsx +42 -3
- package/src/components/claude/tool-use-block.tsx +10 -10
- package/src/components/kanban/board.tsx +81 -66
- package/src/components/providers/socket-provider.tsx +2 -1
- package/src/components/sidebar/file-browser/file-icon.tsx +12 -98
- package/src/components/task/conversation-view.tsx +138 -21
- package/src/components/task/file-icon.tsx +65 -33
- package/src/components/task/prompt-input.tsx +12 -7
- package/src/components/task/task-detail-panel.tsx +209 -69
- package/src/components/ui/detachable-window.tsx +468 -0
- package/src/components/ui/file-extension-icon.tsx +110 -0
- package/src/components/ui/running-dots.tsx +43 -4
- package/src/components/ui/scroll-area.tsx +1 -1
package/next.config.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-ws",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A beautifully crafted workspace interface for Claude Code with real-time streaming and local SQLite database",
|
|
6
|
-
"keywords": [
|
|
6
|
+
"keywords": [
|
|
7
|
+
"claude",
|
|
8
|
+
"claude-code",
|
|
9
|
+
"workspace",
|
|
10
|
+
"task-management",
|
|
11
|
+
"ai",
|
|
12
|
+
"anthropic",
|
|
13
|
+
"nextjs",
|
|
14
|
+
"sqlite"
|
|
15
|
+
],
|
|
7
16
|
"license": "MIT",
|
|
8
17
|
"repository": {
|
|
9
18
|
"type": "git",
|
|
@@ -48,13 +57,15 @@
|
|
|
48
57
|
"scripts": {
|
|
49
58
|
"dev": "tsx server.ts",
|
|
50
59
|
"dev:next": "next dev",
|
|
51
|
-
"build": "next build",
|
|
60
|
+
"build": "NODE_ENV=production next build",
|
|
52
61
|
"start": "NODE_ENV=production tsx server.ts",
|
|
53
62
|
"lint": "eslint",
|
|
54
63
|
"db:generate": "drizzle-kit generate",
|
|
55
64
|
"db:migrate": "drizzle-kit migrate",
|
|
56
|
-
"
|
|
57
|
-
"
|
|
65
|
+
"version:patch": "npm version patch --no-git-tag-version",
|
|
66
|
+
"version:minor": "npm version minor --no-git-tag-version",
|
|
67
|
+
"version:major": "npm version major --no-git-tag-version",
|
|
68
|
+
"publish:npm": "pnpm run build && npm publish --access public"
|
|
58
69
|
},
|
|
59
70
|
"dependencies": {
|
|
60
71
|
"@anthropic-ai/claude-agent-sdk": "^0.2.5",
|
|
@@ -115,6 +126,7 @@
|
|
|
115
126
|
"tailwind-merge": "^3.4.0",
|
|
116
127
|
"tar": "^7.5.2",
|
|
117
128
|
"tsx": "^4.21.0",
|
|
129
|
+
"vscode-icons-js": "^11.6.1",
|
|
118
130
|
"zustand": "^5.0.9"
|
|
119
131
|
},
|
|
120
132
|
"devDependencies": {
|
|
@@ -132,4 +144,4 @@
|
|
|
132
144
|
"typescript": "^5"
|
|
133
145
|
},
|
|
134
146
|
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48"
|
|
135
|
-
}
|
|
147
|
+
}
|
|
@@ -25,12 +25,26 @@ export async function GET(
|
|
|
25
25
|
let totalDeletions = 0;
|
|
26
26
|
let filesChanged = 0;
|
|
27
27
|
|
|
28
|
-
// Context usage: Use LATEST attempt
|
|
29
|
-
//
|
|
28
|
+
// Context usage: Use LATEST attempt with actual context data
|
|
29
|
+
// When a new turn starts, the latest attempt may have 0 context (not yet updated)
|
|
30
|
+
// In that case, fall back to the previous completed attempt's context
|
|
30
31
|
const latestAttempt = attempts[0]; // Already ordered by createdAt DESC
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
|
|
33
|
+
// If latest attempt is running and has no context data yet, use previous attempt's context
|
|
34
|
+
let contextUsed = latestAttempt?.contextUsed || 0;
|
|
35
|
+
let contextLimit = latestAttempt?.contextLimit || 200000;
|
|
36
|
+
let contextPercentage = latestAttempt?.contextPercentage || 0;
|
|
37
|
+
|
|
38
|
+
// Fallback to previous attempt if current is running with no context data
|
|
39
|
+
if (latestAttempt?.status === 'running' && contextPercentage === 0 && attempts.length > 1) {
|
|
40
|
+
const previousAttempt = attempts[1];
|
|
41
|
+
if (previousAttempt?.contextPercentage && previousAttempt.contextPercentage > 0) {
|
|
42
|
+
contextUsed = previousAttempt.contextUsed || 0;
|
|
43
|
+
contextLimit = previousAttempt.contextLimit || 200000;
|
|
44
|
+
contextPercentage = previousAttempt.contextPercentage;
|
|
45
|
+
console.log(`[Stats] Using previous attempt context: ${contextPercentage}% (current attempt is running with no data)`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
34
48
|
|
|
35
49
|
// Calculate context health metrics (ClaudeKit formulas)
|
|
36
50
|
// Note: We approximate input/output split since DB only stores total contextUsed
|
package/src/app/globals.css
CHANGED
|
@@ -303,11 +303,40 @@
|
|
|
303
303
|
@layer base {
|
|
304
304
|
* {
|
|
305
305
|
@apply border-border outline-ring/50;
|
|
306
|
+
box-sizing: border-box;
|
|
306
307
|
}
|
|
307
308
|
|
|
308
309
|
body {
|
|
309
310
|
@apply bg-background text-foreground;
|
|
310
311
|
}
|
|
312
|
+
|
|
313
|
+
/* Prevent horizontal scrolling on mobile */
|
|
314
|
+
html, body {
|
|
315
|
+
overflow-x: hidden;
|
|
316
|
+
max-width: 100vw;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* Prevent iOS Safari zoom on input focus (inputs smaller than 16px trigger zoom) */
|
|
320
|
+
@media screen and (max-width: 767px) {
|
|
321
|
+
input, textarea, select {
|
|
322
|
+
font-size: 16px !important;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/* Ensure all fixed elements respect viewport bounds */
|
|
326
|
+
.fixed {
|
|
327
|
+
max-width: 100vw !important;
|
|
328
|
+
overflow-x: hidden !important;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* Force Radix ScrollArea to respect viewport width on mobile */
|
|
332
|
+
[data-slot="scroll-area"],
|
|
333
|
+
[data-slot="scroll-area-viewport"],
|
|
334
|
+
[data-slot="scroll-area-viewport"] > div {
|
|
335
|
+
max-width: 100vw !important;
|
|
336
|
+
width: 100% !important;
|
|
337
|
+
overflow-x: hidden !important;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
311
340
|
}
|
|
312
341
|
|
|
313
342
|
/* Custom scrollbar styling - minimal and modern */
|
|
@@ -390,21 +419,21 @@
|
|
|
390
419
|
animation: spin-border 2s linear infinite;
|
|
391
420
|
}
|
|
392
421
|
|
|
393
|
-
/*
|
|
394
|
-
@keyframes glow-
|
|
422
|
+
/* Warm glow animation for processing spinner */
|
|
423
|
+
@keyframes glow-warm {
|
|
395
424
|
|
|
396
425
|
0%,
|
|
397
426
|
100% {
|
|
398
|
-
filter: drop-shadow(0 0 2px #
|
|
427
|
+
filter: drop-shadow(0 0 2px #b9664a) drop-shadow(0 0 4px #b9664a);
|
|
399
428
|
}
|
|
400
429
|
|
|
401
430
|
50% {
|
|
402
|
-
filter: drop-shadow(0 0 4px #
|
|
431
|
+
filter: drop-shadow(0 0 4px #b9664a) drop-shadow(0 0 12px #b9664a) drop-shadow(0 0 16px #b9664a);
|
|
403
432
|
}
|
|
404
433
|
}
|
|
405
434
|
|
|
406
|
-
.animate-glow-
|
|
407
|
-
animation: glow-
|
|
435
|
+
.animate-glow-warm {
|
|
436
|
+
animation: glow-warm 2s ease-in-out infinite;
|
|
408
437
|
}
|
|
409
438
|
|
|
410
439
|
/* Inline Edit Diff Styling */
|
package/src/app/layout.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Metadata } from 'next';
|
|
1
|
+
import type { Metadata, Viewport } from 'next';
|
|
2
2
|
import { Geist, Geist_Mono } from 'next/font/google';
|
|
3
3
|
import { Toaster } from 'sonner';
|
|
4
4
|
import { ThemeProvider } from '@/components/providers/theme-provider';
|
|
@@ -15,14 +15,17 @@ const geistMono = Geist_Mono({
|
|
|
15
15
|
subsets: ['latin'],
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
export const viewport: Viewport = {
|
|
19
|
+
width: 'device-width',
|
|
20
|
+
initialScale: 1,
|
|
21
|
+
maximumScale: 1,
|
|
22
|
+
userScalable: false,
|
|
23
|
+
interactiveWidget: 'resizes-content',
|
|
24
|
+
};
|
|
25
|
+
|
|
18
26
|
export const metadata: Metadata = {
|
|
19
27
|
title: 'Claude Workspace',
|
|
20
28
|
description: 'Workspace powered by Claude Code CLI',
|
|
21
|
-
viewport: {
|
|
22
|
-
width: 'device-width',
|
|
23
|
-
initialScale: 1,
|
|
24
|
-
interactiveWidget: 'resizes-content',
|
|
25
|
-
},
|
|
26
29
|
icons: {
|
|
27
30
|
icon: '/favicon.ico',
|
|
28
31
|
apple: '/logo.png',
|
|
@@ -21,9 +21,9 @@ export function CodeBlock({ code, language, className }: CodeBlockProps) {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
|
-
<div className={cn('relative group rounded-md overflow-hidden border border-border', className)}>
|
|
24
|
+
<div className={cn('relative group rounded-md overflow-hidden border border-border w-full max-w-full', className)}>
|
|
25
25
|
{/* Header with language label and copy button */}
|
|
26
|
-
<div className="flex items-center justify-between px-3 py-1.5 bg-muted/50 border-b border-border">
|
|
26
|
+
<div className="flex items-center justify-between px-3 py-1.5 bg-muted/50 border-b border-border w-full">
|
|
27
27
|
<span className="text-xs font-mono text-muted-foreground">
|
|
28
28
|
{language || 'text'}
|
|
29
29
|
</span>
|
|
@@ -31,7 +31,7 @@ export function CodeBlock({ code, language, className }: CodeBlockProps) {
|
|
|
31
31
|
variant="ghost"
|
|
32
32
|
size="icon-sm"
|
|
33
33
|
onClick={handleCopy}
|
|
34
|
-
className="size-6 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
34
|
+
className="size-6 opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
|
|
35
35
|
>
|
|
36
36
|
{copied ? (
|
|
37
37
|
<Check className="size-3" />
|
|
@@ -40,7 +40,7 @@ export function CodeBlock({ code, language, className }: CodeBlockProps) {
|
|
|
40
40
|
)}
|
|
41
41
|
</Button>
|
|
42
42
|
</div>
|
|
43
|
-
<pre className="p-3 bg-muted/30 text-[13px] leading-relaxed whitespace-pre-wrap break-words font-mono">
|
|
43
|
+
<pre className="p-3 bg-muted/30 text-[13px] leading-relaxed whitespace-pre-wrap break-words font-mono w-full max-w-full overflow-x-auto">
|
|
44
44
|
<code className={language ? `language-${language}` : ''}>{code}</code>
|
|
45
45
|
</pre>
|
|
46
46
|
</div>
|
|
@@ -118,11 +118,11 @@ export function DiffView({ oldText, newText, filePath, className }: DiffViewProp
|
|
|
118
118
|
}, [diffLines]);
|
|
119
119
|
|
|
120
120
|
return (
|
|
121
|
-
<div className={cn('rounded-md border border-border overflow-hidden text-xs font-mono', className)}>
|
|
121
|
+
<div className={cn('rounded-md border border-border overflow-hidden text-xs font-mono w-full max-w-full', className)}>
|
|
122
122
|
{/* Header */}
|
|
123
|
-
<div className="flex items-center justify-between px-3 py-1.5 bg-muted/50 border-b border-border">
|
|
124
|
-
<span className="text-muted-foreground truncate">{filePath || 'changes'}</span>
|
|
125
|
-
<div className="flex items-center gap-2 text-[11px]">
|
|
123
|
+
<div className="flex items-center justify-between px-3 py-1.5 bg-muted/50 border-b border-border w-full">
|
|
124
|
+
<span className="text-muted-foreground truncate min-w-0 flex-1">{filePath || 'changes'}</span>
|
|
125
|
+
<div className="flex items-center gap-2 text-[11px] shrink-0">
|
|
126
126
|
{stats.added > 0 && (
|
|
127
127
|
<span className="text-green-600 dark:text-green-400">+{stats.added}</span>
|
|
128
128
|
)}
|
|
@@ -133,7 +133,7 @@ export function DiffView({ oldText, newText, filePath, className }: DiffViewProp
|
|
|
133
133
|
</div>
|
|
134
134
|
|
|
135
135
|
{/* Diff content */}
|
|
136
|
-
<div className="overflow-x-auto max-h-64">
|
|
136
|
+
<div className="overflow-x-auto max-h-64 w-full max-w-full">
|
|
137
137
|
<table className="w-full border-collapse">
|
|
138
138
|
<tbody>
|
|
139
139
|
{diffLines.map((line, idx) => (
|
|
@@ -163,7 +163,7 @@ export function DiffView({ oldText, newText, filePath, className }: DiffViewProp
|
|
|
163
163
|
|
|
164
164
|
{/* Content */}
|
|
165
165
|
<td className={cn(
|
|
166
|
-
'px-2 py-0 whitespace-pre',
|
|
166
|
+
'px-2 py-0 whitespace-pre-wrap break-all',
|
|
167
167
|
line.type === 'added' && 'text-green-700 dark:text-green-300',
|
|
168
168
|
line.type === 'removed' && 'text-red-700 dark:text-red-300'
|
|
169
169
|
)}>
|
|
@@ -85,8 +85,8 @@ export function MessageBlock({ content, isThinking = false, isStreaming = false,
|
|
|
85
85
|
) : (
|
|
86
86
|
<ChevronRight className="size-3" />
|
|
87
87
|
)}
|
|
88
|
-
<RunningDots
|
|
89
|
-
<span className="font-mono text-[14px]">Thinking...</span>
|
|
88
|
+
<RunningDots />
|
|
89
|
+
<span className="font-mono text-[14px]" style={{ color: '#b9664a' }}>Thinking...</span>
|
|
90
90
|
</button>
|
|
91
91
|
|
|
92
92
|
{isExpanded && (
|
|
@@ -99,7 +99,7 @@ export function MessageBlock({ content, isThinking = false, isStreaming = false,
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
return (
|
|
102
|
-
<div className={cn('text-[15px] leading-7 max-w-full overflow-hidden', className)}>
|
|
102
|
+
<div className={cn('text-[15px] leading-7 max-w-full w-full overflow-hidden', className)}>
|
|
103
103
|
<MarkdownContent content={displayContent} />
|
|
104
104
|
</div>
|
|
105
105
|
);
|
|
@@ -176,7 +176,7 @@ function MarkdownContent({ content }: { content: string }) {
|
|
|
176
176
|
},
|
|
177
177
|
// Pre blocks
|
|
178
178
|
pre: ({ children }) => (
|
|
179
|
-
<div className="my-2 overflow-x-auto">{children}</div>
|
|
179
|
+
<div className="my-2 w-full max-w-full overflow-x-auto">{children}</div>
|
|
180
180
|
),
|
|
181
181
|
// Strong/Bold
|
|
182
182
|
strong: ({ children }) => (
|
|
@@ -14,10 +14,49 @@ interface ResponseRendererProps {
|
|
|
14
14
|
|
|
15
15
|
export function ResponseRenderer({ messages, className }: ResponseRendererProps) {
|
|
16
16
|
const bottomRef = useRef<HTMLDivElement>(null);
|
|
17
|
+
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
const userScrollingRef = useRef(false);
|
|
19
|
+
const userScrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
20
|
+
|
|
21
|
+
// Check if user is near bottom
|
|
22
|
+
const isNearBottom = () => {
|
|
23
|
+
const viewport = scrollAreaRef.current?.querySelector('[data-slot="scroll-area-viewport"]');
|
|
24
|
+
if (!viewport) return true;
|
|
25
|
+
const threshold = 150;
|
|
26
|
+
return viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight < threshold;
|
|
27
|
+
};
|
|
17
28
|
|
|
18
|
-
//
|
|
29
|
+
// Detect user scroll to pause auto-scroll
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const viewport = scrollAreaRef.current?.querySelector('[data-slot="scroll-area-viewport"]');
|
|
32
|
+
if (!viewport) return;
|
|
33
|
+
|
|
34
|
+
const handleScroll = () => {
|
|
35
|
+
userScrollingRef.current = true;
|
|
36
|
+
if (userScrollTimeoutRef.current) {
|
|
37
|
+
clearTimeout(userScrollTimeoutRef.current);
|
|
38
|
+
}
|
|
39
|
+
userScrollTimeoutRef.current = setTimeout(() => {
|
|
40
|
+
if (isNearBottom()) {
|
|
41
|
+
userScrollingRef.current = false;
|
|
42
|
+
}
|
|
43
|
+
}, 150);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
viewport.addEventListener('scroll', handleScroll, { passive: true });
|
|
47
|
+
return () => {
|
|
48
|
+
viewport.removeEventListener('scroll', handleScroll);
|
|
49
|
+
if (userScrollTimeoutRef.current) {
|
|
50
|
+
clearTimeout(userScrollTimeoutRef.current);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
// Auto-scroll to bottom on new messages (only if not manually scrolling)
|
|
19
56
|
useEffect(() => {
|
|
20
|
-
|
|
57
|
+
if (!userScrollingRef.current && isNearBottom()) {
|
|
58
|
+
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
59
|
+
}
|
|
21
60
|
}, [messages]);
|
|
22
61
|
|
|
23
62
|
const renderContentBlock = (block: ClaudeContentBlock, index: number) => {
|
|
@@ -103,7 +142,7 @@ export function ResponseRenderer({ messages, className }: ResponseRendererProps)
|
|
|
103
142
|
}
|
|
104
143
|
|
|
105
144
|
return (
|
|
106
|
-
<ScrollArea className={cn('h-full', className)}>
|
|
145
|
+
<ScrollArea ref={scrollAreaRef} className={cn('h-full', className)}>
|
|
107
146
|
<div className="space-y-4 p-4">
|
|
108
147
|
{messages.map((message, index) => renderMessage(message, index))}
|
|
109
148
|
<div ref={bottomRef} />
|
|
@@ -161,17 +161,17 @@ function BashBlock({ command, output, isError }: { command: string; output?: str
|
|
|
161
161
|
const outputLines = output?.split('\n').length || 0;
|
|
162
162
|
|
|
163
163
|
return (
|
|
164
|
-
<div className="rounded-md border border-border overflow-hidden text-xs font-mono">
|
|
164
|
+
<div className="rounded-md border border-border overflow-hidden text-xs font-mono w-full max-w-full">
|
|
165
165
|
{/* Command header */}
|
|
166
166
|
<div
|
|
167
167
|
className={cn(
|
|
168
|
-
'flex items-center gap-2 px-3 py-2 bg-zinc-900 dark:bg-zinc-950',
|
|
168
|
+
'flex items-center gap-2 px-3 py-2 bg-zinc-900 dark:bg-zinc-950 w-full max-w-full',
|
|
169
169
|
hasOutput && 'cursor-pointer hover:bg-zinc-800 dark:hover:bg-zinc-900'
|
|
170
170
|
)}
|
|
171
171
|
onClick={() => hasOutput && setIsExpanded(!isExpanded)}
|
|
172
172
|
>
|
|
173
173
|
<Terminal className="size-3.5 text-zinc-400 shrink-0" />
|
|
174
|
-
<code className="text-zinc-100 flex-1 truncate">{command}</code>
|
|
174
|
+
<code className="text-zinc-100 flex-1 truncate min-w-0">{command}</code>
|
|
175
175
|
<div className="flex items-center gap-1">
|
|
176
176
|
<Button
|
|
177
177
|
variant="ghost"
|
|
@@ -249,11 +249,11 @@ export function ToolUseBlock({ name, input, result, isError, isStreaming, classN
|
|
|
249
249
|
const isCompleted = !isStreaming && result && !isError;
|
|
250
250
|
|
|
251
251
|
return (
|
|
252
|
-
<div className={cn('group max-w-full overflow-hidden my-2', className)}>
|
|
252
|
+
<div className={cn('group w-full max-w-full overflow-hidden my-2', className)}>
|
|
253
253
|
{/* Main status line */}
|
|
254
254
|
<div
|
|
255
255
|
className={cn(
|
|
256
|
-
'flex items-start gap-2.5 py-1.5 px-2 rounded-md transition-colors min-w-0 border border-transparent',
|
|
256
|
+
'flex items-start gap-2.5 py-1.5 px-2 rounded-md transition-colors min-w-0 w-full max-w-full border border-transparent',
|
|
257
257
|
isStreaming ? 'text-foreground bg-accent/30 border-accent/20' : 'text-muted-foreground hover:bg-accent/20',
|
|
258
258
|
hasOtherDetails && 'cursor-pointer'
|
|
259
259
|
)}
|
|
@@ -272,9 +272,9 @@ export function ToolUseBlock({ name, input, result, isError, isStreaming, classN
|
|
|
272
272
|
|
|
273
273
|
{/* Streaming spinner or icon */}
|
|
274
274
|
{isStreaming ? (
|
|
275
|
-
<RunningDots className="shrink-0
|
|
275
|
+
<RunningDots className="shrink-0" />
|
|
276
276
|
) : isCompleted ? null : (
|
|
277
|
-
<Icon className={cn('size-4 shrink-0
|
|
277
|
+
<Icon className={cn('size-4 shrink-0', isError && 'text-destructive')} />
|
|
278
278
|
)}
|
|
279
279
|
|
|
280
280
|
{/* Tool name and target - allow wrapping */}
|
|
@@ -310,7 +310,7 @@ export function ToolUseBlock({ name, input, result, isError, isStreaming, classN
|
|
|
310
310
|
|
|
311
311
|
{/* Special view for Bash */}
|
|
312
312
|
{isBash && Boolean(inputObj?.command) && (
|
|
313
|
-
<div className="mt-1.5 ml-5">
|
|
313
|
+
<div className="mt-1.5 ml-5 w-full max-w-full overflow-hidden pr-5">
|
|
314
314
|
<BashBlock
|
|
315
315
|
command={String(inputObj?.command)}
|
|
316
316
|
output={result}
|
|
@@ -321,14 +321,14 @@ export function ToolUseBlock({ name, input, result, isError, isStreaming, classN
|
|
|
321
321
|
|
|
322
322
|
{/* Special view for Edit with diff */}
|
|
323
323
|
{hasEditDiff && (
|
|
324
|
-
<div className="mt-1.5 ml-5">
|
|
324
|
+
<div className="mt-1.5 ml-5 w-full max-w-full overflow-hidden pr-5">
|
|
325
325
|
<EditBlock input={inputObj} result={result} isError={isError} />
|
|
326
326
|
</div>
|
|
327
327
|
)}
|
|
328
328
|
|
|
329
329
|
{/* Standard expandable details for other tools */}
|
|
330
330
|
{isExpanded && hasOtherDetails && (
|
|
331
|
-
<div className="ml-5 mt-1 pl-4 border-l border-border/50 text-[13px] text-muted-foreground space-y-2 max-w-full overflow-hidden">
|
|
331
|
+
<div className="ml-5 mt-1 pl-4 border-l border-border/50 text-[13px] text-muted-foreground space-y-2 w-full max-w-full overflow-hidden pr-5">
|
|
332
332
|
{inputObj && Object.keys(inputObj).length > 1 && (
|
|
333
333
|
<pre className="font-mono bg-muted/30 p-2 rounded overflow-x-auto max-h-32 whitespace-pre-wrap break-all">
|
|
334
334
|
{JSON.stringify(inputObj, null, 2)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { useEffect, useMemo, useRef, useState, useTransition } from 'react';
|
|
4
4
|
import {
|
|
5
5
|
DndContext,
|
|
6
6
|
DragEndEvent,
|
|
@@ -24,8 +24,22 @@ interface BoardProps {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function Board({ attempts = [], onCreateTask }: BoardProps) {
|
|
27
|
-
const { tasks, reorderTasks } = useTaskStore();
|
|
27
|
+
const { tasks, reorderTasks, selectTask, setPendingAutoStartTask } = useTaskStore();
|
|
28
28
|
const [activeTask, setActiveTask] = useState<Task | null>(null);
|
|
29
|
+
const [, startTransition] = useTransition();
|
|
30
|
+
const lastReorderRef = useRef<string>('');
|
|
31
|
+
const [pendingNewTaskStart, setPendingNewTaskStart] = useState<{ taskId: string; description: string } | null>(null);
|
|
32
|
+
|
|
33
|
+
// Handle auto-start for newly created tasks moved to In Progress
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (pendingNewTaskStart) {
|
|
36
|
+
const { taskId, description } = pendingNewTaskStart;
|
|
37
|
+
// Select the task and trigger auto-start
|
|
38
|
+
selectTask(taskId);
|
|
39
|
+
setPendingAutoStartTask(taskId, description);
|
|
40
|
+
setPendingNewTaskStart(null);
|
|
41
|
+
}
|
|
42
|
+
}, [pendingNewTaskStart, selectTask, setPendingAutoStartTask]);
|
|
29
43
|
|
|
30
44
|
const sensors = useSensors(
|
|
31
45
|
useSensor(PointerSensor, {
|
|
@@ -94,39 +108,11 @@ export function Board({ attempts = [], onCreateTask }: BoardProps) {
|
|
|
94
108
|
// Check if dropping over a column
|
|
95
109
|
const overColumn = KANBAN_COLUMNS.find((col) => col.id === overId);
|
|
96
110
|
if (overColumn) {
|
|
97
|
-
// Moving to a different column
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
reorderTasks(activeTask.id, overColumn.id, targetTasks.length);
|
|
101
|
-
}
|
|
102
|
-
} else {
|
|
103
|
-
// Dropping over another task
|
|
104
|
-
const overTask = tasks.find((t) => t.id === overId);
|
|
105
|
-
if (overTask) {
|
|
106
|
-
const targetColumn = overTask.status;
|
|
107
|
-
const columnTasks = tasksByStatus.get(targetColumn) || [];
|
|
108
|
-
|
|
109
|
-
// Find current position in the active task's current column
|
|
110
|
-
const oldIndex = columnTasks.findIndex((t) => t.id === activeId);
|
|
111
|
-
|
|
112
|
-
// Find position in target column
|
|
113
|
-
const newIndex = columnTasks.findIndex((t) => t.id === overId);
|
|
114
|
-
|
|
115
|
-
// If moving to different column or reordering within same column
|
|
116
|
-
if (activeTask.status !== targetColumn || oldIndex !== newIndex) {
|
|
117
|
-
// Handle the move in the target column
|
|
118
|
-
if (activeTask.status !== targetColumn) {
|
|
119
|
-
// Moving to different column - place at the position of overTask
|
|
120
|
-
reorderTasks(activeTask.id, targetColumn, newIndex);
|
|
121
|
-
} else if (oldIndex !== -1 && newIndex !== -1) {
|
|
122
|
-
// Reordering within same column
|
|
123
|
-
const reordered = arrayMove(columnTasks, oldIndex, newIndex);
|
|
124
|
-
const newPosition = reordered.findIndex((t) => t.id === activeId);
|
|
125
|
-
reorderTasks(activeTask.id, activeTask.status, newPosition);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
111
|
+
// Moving to a different column - don't reorder during drag, just for visual
|
|
112
|
+
// The actual reorder happens in handleDragEnd
|
|
113
|
+
return;
|
|
129
114
|
}
|
|
115
|
+
// Don't do anything during dragOver - let handleDragEnd handle the reordering
|
|
130
116
|
};
|
|
131
117
|
|
|
132
118
|
const handleDragEnd = (event: DragEndEvent) => {
|
|
@@ -143,41 +129,70 @@ export function Board({ attempts = [], onCreateTask }: BoardProps) {
|
|
|
143
129
|
const activeTask = tasks.find((t) => t.id === activeId);
|
|
144
130
|
if (!activeTask) return;
|
|
145
131
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
132
|
+
// Skip if we just processed this exact same reorder
|
|
133
|
+
if (lastReorderRef.current === `${activeId}-${overId}`) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Mark this reorder as in-progress
|
|
138
|
+
lastReorderRef.current = `${activeId}-${overId}`;
|
|
139
|
+
|
|
140
|
+
// Check if this is a newly created task moving to In Progress
|
|
141
|
+
const isNewTaskToInProgress = !activeTask.chatInit && activeTask.status === 'todo';
|
|
142
|
+
|
|
143
|
+
// Wrap in startTransition to avoid blocking the UI during reordering
|
|
144
|
+
startTransition(async () => {
|
|
145
|
+
// Check if dropping over a column
|
|
146
|
+
const overColumn = KANBAN_COLUMNS.find((col) => col.id === overId);
|
|
147
|
+
if (overColumn) {
|
|
148
|
+
if (activeTask.status !== overColumn.id) {
|
|
149
|
+
const targetTasks = tasksByStatus.get(overColumn.id) || [];
|
|
150
|
+
await reorderTasks(activeTask.id, overColumn.id, targetTasks.length);
|
|
151
|
+
|
|
152
|
+
// If this is a newly created task moving to In Progress, trigger auto-start
|
|
153
|
+
if (isNewTaskToInProgress && overColumn.id === 'in_progress' && activeTask.description) {
|
|
154
|
+
setPendingNewTaskStart({ taskId: activeTask.id, description: activeTask.description });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
// Dropping over another task
|
|
159
|
+
const overTask = tasks.find((t) => t.id === overId);
|
|
160
|
+
if (overTask) {
|
|
161
|
+
const targetColumn = overTask.status;
|
|
162
|
+
const columnTasks = tasksByStatus.get(targetColumn) || [];
|
|
163
|
+
|
|
164
|
+
// Find current position in the active task's current column
|
|
165
|
+
const oldIndex = columnTasks.findIndex((t) => t.id === activeId);
|
|
166
|
+
|
|
167
|
+
// Find position in target column
|
|
168
|
+
const newIndex = columnTasks.findIndex((t) => t.id === overId);
|
|
169
|
+
|
|
170
|
+
// If moving to different column or reordering within same column
|
|
171
|
+
if (activeTask.status !== targetColumn || oldIndex !== newIndex) {
|
|
172
|
+
// Handle the move in the target column
|
|
173
|
+
if (activeTask.status !== targetColumn) {
|
|
174
|
+
// Moving to different column - place at the position of overTask
|
|
175
|
+
await reorderTasks(activeTask.id, targetColumn, newIndex);
|
|
176
|
+
|
|
177
|
+
// If this is a newly created task moving to In Progress, trigger auto-start
|
|
178
|
+
if (isNewTaskToInProgress && targetColumn === 'in_progress' && activeTask.description) {
|
|
179
|
+
setPendingNewTaskStart({ taskId: activeTask.id, description: activeTask.description });
|
|
180
|
+
}
|
|
181
|
+
} else if (oldIndex !== -1 && newIndex !== -1) {
|
|
182
|
+
// Reordering within same column
|
|
183
|
+
const reordered = arrayMove(columnTasks, oldIndex, newIndex);
|
|
184
|
+
const newPosition = reordered.findIndex((t) => t.id === activeId);
|
|
185
|
+
await reorderTasks(activeTask.id, activeTask.status, newPosition);
|
|
186
|
+
}
|
|
177
187
|
}
|
|
178
188
|
}
|
|
179
189
|
}
|
|
180
|
-
|
|
190
|
+
|
|
191
|
+
// Reset the ref after a short delay to allow for rapid reordering of different tasks
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
lastReorderRef.current = '';
|
|
194
|
+
}, 100);
|
|
195
|
+
});
|
|
181
196
|
};
|
|
182
197
|
|
|
183
198
|
const handleDragCancel = () => {
|
|
@@ -20,7 +20,8 @@ export function SocketProvider({ children }: { children: React.ReactNode }) {
|
|
|
20
20
|
|
|
21
21
|
socketInstance.on('connect', () => {
|
|
22
22
|
console.log('[SocketProvider] Connected:', socketInstance.id);
|
|
23
|
-
setSocket
|
|
23
|
+
// Defer setSocket to avoid setState during render
|
|
24
|
+
Promise.resolve().then(() => setSocket(socketInstance));
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
socketInstance.on('disconnect', () => {
|