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.
Files changed (31) hide show
  1. package/package.json +2 -1
  2. package/public/docs/swagger/swagger.yaml +135 -0
  3. package/server.ts +6 -1
  4. package/src/app/api/attempts/[id]/alive/route.ts +30 -0
  5. package/src/app/api/attempts/[id]/answer/route.ts +42 -0
  6. package/src/app/api/attempts/[id]/route.ts +57 -0
  7. package/src/app/api/files/operations/route.ts +415 -0
  8. package/src/app/api/git/gitignore/route.ts +81 -0
  9. package/src/app/api/tasks/[id]/conversation/route.ts +14 -1
  10. package/src/app/globals.css +4 -4
  11. package/src/app/page.tsx +1 -1
  12. package/src/components/claude/tool-use-block.tsx +21 -2
  13. package/src/components/header.tsx +4 -40
  14. package/src/components/kanban/board.tsx +10 -2
  15. package/src/components/kanban/task-card.tsx +68 -20
  16. package/src/components/right-sidebar.tsx +53 -9
  17. package/src/components/sidebar/file-browser/file-tree-context-menu.tsx +385 -0
  18. package/src/components/sidebar/file-browser/file-tree-item.tsx +242 -58
  19. package/src/components/sidebar/file-browser/file-tree.tsx +10 -1
  20. package/src/components/sidebar/file-browser/use-long-press.ts +48 -0
  21. package/src/components/sidebar/git-changes/git-file-item.tsx +42 -12
  22. package/src/components/sidebar/git-changes/git-panel.tsx +21 -0
  23. package/src/components/task/conversation-view.tsx +34 -4
  24. package/src/components/task/interactive-command/question-prompt.tsx +12 -1
  25. package/src/components/task/pending-question-indicator.tsx +57 -0
  26. package/src/components/task/prompt-input.tsx +1 -1
  27. package/src/components/task/task-detail-panel.tsx +47 -14
  28. package/src/components/ui/context-menu.tsx +252 -0
  29. package/src/hooks/use-attempt-stream.ts +188 -13
  30. package/src/lib/agent-manager.ts +4 -0
  31. 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.0",
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
+ }