claude-ws 0.2.0 → 0.2.1
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/package.json +2 -1
- package/public/docs/swagger/swagger.yaml +135 -0
- package/server.ts +6 -1
- package/src/app/api/attempts/[id]/alive/route.ts +30 -0
- package/src/app/api/attempts/[id]/answer/route.ts +42 -0
- package/src/app/api/attempts/[id]/route.ts +57 -0
- package/src/app/api/files/operations/route.ts +415 -0
- package/src/app/api/git/gitignore/route.ts +81 -0
- package/src/app/api/tasks/[id]/conversation/route.ts +14 -1
- package/src/app/globals.css +4 -4
- package/src/app/page.tsx +1 -1
- package/src/components/claude/tool-use-block.tsx +21 -2
- package/src/components/header.tsx +4 -40
- package/src/components/kanban/board.tsx +10 -2
- package/src/components/kanban/task-card.tsx +68 -20
- package/src/components/right-sidebar.tsx +53 -9
- package/src/components/sidebar/file-browser/file-tree-context-menu.tsx +385 -0
- package/src/components/sidebar/file-browser/file-tree-item.tsx +242 -58
- package/src/components/sidebar/file-browser/file-tree.tsx +10 -1
- package/src/components/sidebar/file-browser/use-long-press.ts +48 -0
- package/src/components/sidebar/git-changes/git-file-item.tsx +42 -12
- package/src/components/sidebar/git-changes/git-panel.tsx +21 -0
- package/src/components/task/conversation-view.tsx +34 -4
- package/src/components/task/interactive-command/question-prompt.tsx +12 -1
- package/src/components/task/pending-question-indicator.tsx +57 -0
- package/src/components/task/prompt-input.tsx +1 -1
- package/src/components/task/task-detail-panel.tsx +47 -14
- package/src/components/ui/context-menu.tsx +252 -0
- package/src/hooks/use-attempt-stream.ts +188 -13
- package/src/lib/agent-manager.ts +4 -0
- package/src/stores/sidebar-store.ts +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-ws",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A beautifully crafted workspace interface for Claude Code with real-time streaming and local SQLite database",
|
|
6
6
|
"keywords": [
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
"@dnd-kit/sortable": "^10.0.0",
|
|
91
91
|
"@dnd-kit/utilities": "^3.2.2",
|
|
92
92
|
"@radix-ui/react-checkbox": "^1.3.3",
|
|
93
|
+
"@radix-ui/react-context-menu": "^2.2.16",
|
|
93
94
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
94
95
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
95
96
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -771,6 +771,141 @@ paths:
|
|
|
771
771
|
schema:
|
|
772
772
|
$ref: '#/components/schemas/Error'
|
|
773
773
|
|
|
774
|
+
/api/files/operations:
|
|
775
|
+
delete:
|
|
776
|
+
tags:
|
|
777
|
+
- Files
|
|
778
|
+
summary: Delete file or folder
|
|
779
|
+
description: |
|
|
780
|
+
Delete a file or folder recursively from the filesystem.
|
|
781
|
+
|
|
782
|
+
**Security:**
|
|
783
|
+
- Path traversal validation via `path.relative()` check
|
|
784
|
+
- File existence check before deletion
|
|
785
|
+
- Permission error handling (EACCES -> 403)
|
|
786
|
+
operationId: deleteFileOrFolder
|
|
787
|
+
requestBody:
|
|
788
|
+
required: true
|
|
789
|
+
content:
|
|
790
|
+
application/json:
|
|
791
|
+
schema:
|
|
792
|
+
type: object
|
|
793
|
+
required:
|
|
794
|
+
- path
|
|
795
|
+
- rootPath
|
|
796
|
+
properties:
|
|
797
|
+
path:
|
|
798
|
+
type: string
|
|
799
|
+
description: Absolute path to file/folder to delete
|
|
800
|
+
rootPath:
|
|
801
|
+
type: string
|
|
802
|
+
description: Root directory path for security validation
|
|
803
|
+
responses:
|
|
804
|
+
"200":
|
|
805
|
+
description: File/folder deleted successfully
|
|
806
|
+
content:
|
|
807
|
+
application/json:
|
|
808
|
+
schema:
|
|
809
|
+
type: object
|
|
810
|
+
properties:
|
|
811
|
+
success:
|
|
812
|
+
type: boolean
|
|
813
|
+
example: true
|
|
814
|
+
"400":
|
|
815
|
+
description: Bad request (missing required fields)
|
|
816
|
+
content:
|
|
817
|
+
application/json:
|
|
818
|
+
schema:
|
|
819
|
+
$ref: '#/components/schemas/Error'
|
|
820
|
+
"403":
|
|
821
|
+
description: Path traversal detected or permission denied
|
|
822
|
+
content:
|
|
823
|
+
application/json:
|
|
824
|
+
schema:
|
|
825
|
+
$ref: '#/components/schemas/Error'
|
|
826
|
+
"404":
|
|
827
|
+
description: Path not found
|
|
828
|
+
content:
|
|
829
|
+
application/json:
|
|
830
|
+
schema:
|
|
831
|
+
$ref: '#/components/schemas/Error'
|
|
832
|
+
"500":
|
|
833
|
+
description: Server error
|
|
834
|
+
content:
|
|
835
|
+
application/json:
|
|
836
|
+
schema:
|
|
837
|
+
$ref: '#/components/schemas/Error'
|
|
838
|
+
post:
|
|
839
|
+
tags:
|
|
840
|
+
- Files
|
|
841
|
+
summary: Download file or folder as ZIP
|
|
842
|
+
description: |
|
|
843
|
+
Create a ZIP archive of a file or folder for download.
|
|
844
|
+
|
|
845
|
+
**Security:**
|
|
846
|
+
- Path traversal validation via `path.relative()` check
|
|
847
|
+
- File existence check before zipping
|
|
848
|
+
- Only operations within provided rootPath
|
|
849
|
+
operationId: downloadAsZip
|
|
850
|
+
requestBody:
|
|
851
|
+
required: true
|
|
852
|
+
content:
|
|
853
|
+
application/json:
|
|
854
|
+
schema:
|
|
855
|
+
type: object
|
|
856
|
+
required:
|
|
857
|
+
- path
|
|
858
|
+
- rootPath
|
|
859
|
+
properties:
|
|
860
|
+
path:
|
|
861
|
+
type: string
|
|
862
|
+
description: Absolute path to file/folder to zip
|
|
863
|
+
rootPath:
|
|
864
|
+
type: string
|
|
865
|
+
description: Root directory path for security validation
|
|
866
|
+
responses:
|
|
867
|
+
"200":
|
|
868
|
+
description: ZIP archive created successfully
|
|
869
|
+
content:
|
|
870
|
+
application/zip:
|
|
871
|
+
schema:
|
|
872
|
+
type: string
|
|
873
|
+
format: binary
|
|
874
|
+
headers:
|
|
875
|
+
Content-Disposition:
|
|
876
|
+
description: Download filename
|
|
877
|
+
schema:
|
|
878
|
+
type: string
|
|
879
|
+
example: 'attachment; filename="folder.zip"'
|
|
880
|
+
Content-Length:
|
|
881
|
+
description: ZIP file size in bytes
|
|
882
|
+
schema:
|
|
883
|
+
type: integer
|
|
884
|
+
"400":
|
|
885
|
+
description: Bad request (missing required fields)
|
|
886
|
+
content:
|
|
887
|
+
application/json:
|
|
888
|
+
schema:
|
|
889
|
+
$ref: '#/components/schemas/Error'
|
|
890
|
+
"403":
|
|
891
|
+
description: Path traversal detected or permission denied
|
|
892
|
+
content:
|
|
893
|
+
application/json:
|
|
894
|
+
schema:
|
|
895
|
+
$ref: '#/components/schemas/Error'
|
|
896
|
+
"404":
|
|
897
|
+
description: Path not found
|
|
898
|
+
content:
|
|
899
|
+
application/json:
|
|
900
|
+
schema:
|
|
901
|
+
$ref: '#/components/schemas/Error'
|
|
902
|
+
"500":
|
|
903
|
+
description: Server error
|
|
904
|
+
content:
|
|
905
|
+
application/json:
|
|
906
|
+
schema:
|
|
907
|
+
$ref: '#/components/schemas/Error'
|
|
908
|
+
|
|
774
909
|
/api/git/discard:
|
|
775
910
|
post:
|
|
776
911
|
tags:
|
package/server.ts
CHANGED
|
@@ -599,12 +599,17 @@ app.prepare().then(async () => {
|
|
|
599
599
|
|
|
600
600
|
// Handle AskUserQuestion detection from AgentManager
|
|
601
601
|
agentManager.on('question', ({ attemptId, toolUseId, questions }) => {
|
|
602
|
-
console.log(`[Server] AskUserQuestion detected for ${attemptId}
|
|
602
|
+
console.log(`[Server] AskUserQuestion detected for ${attemptId}`, {
|
|
603
|
+
toolUseId,
|
|
604
|
+
questionCount: questions?.length,
|
|
605
|
+
questions: questions?.map((q: any) => ({ header: q.header, question: q.question?.substring(0, 50) }))
|
|
606
|
+
});
|
|
603
607
|
io.to(`attempt:${attemptId}`).emit('question:ask', {
|
|
604
608
|
attemptId,
|
|
605
609
|
toolUseId,
|
|
606
610
|
questions,
|
|
607
611
|
});
|
|
612
|
+
console.log(`[Server] Emitted question:ask to attempt:${attemptId}`);
|
|
608
613
|
});
|
|
609
614
|
|
|
610
615
|
// Handle background shell detection from AgentManager (Bash with run_in_background=true)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { agentManager } from '@/lib/agent-manager';
|
|
3
|
+
|
|
4
|
+
// GET /api/attempts/[id]/alive - Check if an attempt has an active agent process
|
|
5
|
+
export async function GET(
|
|
6
|
+
request: NextRequest,
|
|
7
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
8
|
+
) {
|
|
9
|
+
try {
|
|
10
|
+
const { id } = await params;
|
|
11
|
+
|
|
12
|
+
// Check if this attempt has an active agent or pending question
|
|
13
|
+
const hasAgent = agentManager.isRunning(id);
|
|
14
|
+
const hasPendingQuestion = agentManager.hasPendingQuestion(id);
|
|
15
|
+
const isAlive = hasAgent || hasPendingQuestion;
|
|
16
|
+
|
|
17
|
+
return NextResponse.json({
|
|
18
|
+
attemptId: id,
|
|
19
|
+
alive: isAlive,
|
|
20
|
+
hasAgent,
|
|
21
|
+
hasPendingQuestion
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Error checking attempt status:', error);
|
|
25
|
+
return NextResponse.json(
|
|
26
|
+
{ error: 'Failed to check attempt status' },
|
|
27
|
+
{ status: 500 }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { db, schema } from '@/lib/db';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
|
|
5
|
+
// POST /api/attempts/[id]/answer - Save a user's answer to database
|
|
6
|
+
export async function POST(
|
|
7
|
+
request: NextRequest,
|
|
8
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
+
) {
|
|
10
|
+
try {
|
|
11
|
+
const { id } = await params;
|
|
12
|
+
const body = await request.json();
|
|
13
|
+
const { questions, answers } = body as { questions: unknown[]; answers: Record<string, string> };
|
|
14
|
+
|
|
15
|
+
// Save the answer as an attempt log (with bold formatting for display)
|
|
16
|
+
const answerText = Object.entries(answers)
|
|
17
|
+
.map(([question, answer]) => `${question}: **${answer}**`)
|
|
18
|
+
.join('\n');
|
|
19
|
+
|
|
20
|
+
await db.insert(schema.attemptLogs).values({
|
|
21
|
+
attemptId: id,
|
|
22
|
+
type: 'json',
|
|
23
|
+
content: JSON.stringify({
|
|
24
|
+
type: 'user_answer',
|
|
25
|
+
questions,
|
|
26
|
+
answers,
|
|
27
|
+
displayText: `✓ You answered:\n${answerText}`
|
|
28
|
+
}),
|
|
29
|
+
createdAt: Date.now(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
console.log(`[answer] Saved answer for attempt ${id}`);
|
|
33
|
+
|
|
34
|
+
return NextResponse.json({ success: true });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Error saving answer:', error);
|
|
37
|
+
return NextResponse.json(
|
|
38
|
+
{ error: 'Failed to save answer' },
|
|
39
|
+
{ status: 500 }
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -117,3 +117,60 @@ export async function GET(
|
|
|
117
117
|
);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
// POST /api/attempts/[id] - Reactivate a completed/failed attempt
|
|
122
|
+
export async function POST(
|
|
123
|
+
request: NextRequest,
|
|
124
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
125
|
+
) {
|
|
126
|
+
try {
|
|
127
|
+
const { id } = await params;
|
|
128
|
+
|
|
129
|
+
// Get the attempt
|
|
130
|
+
const attempt = await db
|
|
131
|
+
.select()
|
|
132
|
+
.from(schema.attempts)
|
|
133
|
+
.where(eq(schema.attempts.id, id))
|
|
134
|
+
.limit(1);
|
|
135
|
+
|
|
136
|
+
if (attempt.length === 0) {
|
|
137
|
+
return NextResponse.json(
|
|
138
|
+
{ error: 'Attempt not found' },
|
|
139
|
+
{ status: 404 }
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const attemptData = attempt[0];
|
|
144
|
+
|
|
145
|
+
// Only reactivate attempts that are not already running
|
|
146
|
+
if (attemptData.status === 'running') {
|
|
147
|
+
return NextResponse.json({
|
|
148
|
+
success: true,
|
|
149
|
+
alreadyRunning: true,
|
|
150
|
+
attempt: { id: attemptData.id, status: attemptData.status }
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Reactivate the attempt
|
|
155
|
+
await db
|
|
156
|
+
.update(schema.attempts)
|
|
157
|
+
.set({
|
|
158
|
+
status: 'running',
|
|
159
|
+
completedAt: null, // Clear completion time
|
|
160
|
+
})
|
|
161
|
+
.where(eq(schema.attempts.id, id));
|
|
162
|
+
|
|
163
|
+
console.log(`[reactivate] Reactivated attempt ${id} for task ${attemptData.taskId}`);
|
|
164
|
+
|
|
165
|
+
return NextResponse.json({
|
|
166
|
+
success: true,
|
|
167
|
+
attempt: { id: attemptData.id, status: 'running' }
|
|
168
|
+
});
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('Error reactivating attempt:', error);
|
|
171
|
+
return NextResponse.json(
|
|
172
|
+
{ error: 'Failed to reactivate attempt' },
|
|
173
|
+
{ status: 500 }
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|