claude-ws 0.2.0 → 0.3.0
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/bin/claudekanban.js +30 -7
- package/next.config.ts +4 -1
- package/package.json +5 -2
- package/public/docs/swagger/swagger.yaml +135 -0
- package/server.ts +67 -27
- package/src/app/{layout.tsx → [locale]/layout.tsx} +25 -9
- package/src/app/{page.tsx → [locale]/page.tsx} +15 -10
- package/src/app/api/agent-factory/discover/route.ts +123 -19
- 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/generate-message/route.ts +1 -1
- package/src/app/api/git/gitignore/route.ts +81 -0
- package/src/app/api/tasks/[id]/conversation/route.ts +20 -1
- package/src/app/globals.css +15 -4
- package/src/components/agent-factory/discovery-dialog.tsx +375 -142
- package/src/components/claude/message-block.tsx +98 -121
- package/src/components/claude/tool-use-block.tsx +24 -4
- package/src/components/editor/markdown-file-viewer.tsx +126 -0
- package/src/components/header.tsx +15 -48
- package/src/components/kanban/board.tsx +17 -3
- package/src/components/kanban/column.tsx +15 -10
- package/src/components/kanban/create-task-dialog.tsx +19 -17
- package/src/components/kanban/task-card.tsx +19 -21
- package/src/components/project-settings/project-settings-dialog.tsx +1 -0
- package/src/components/providers/socket-provider.tsx +7 -11
- package/src/components/right-sidebar.tsx +69 -16
- package/src/components/settings/settings-dialog.tsx +5 -68
- package/src/components/settings/settings-page.tsx +101 -0
- package/src/components/sidebar/file-browser/file-create-buttons.tsx +183 -0
- package/src/components/sidebar/file-browser/file-tab-content.tsx +49 -8
- package/src/components/sidebar/file-browser/file-tree-context-menu.tsx +390 -0
- package/src/components/sidebar/file-browser/file-tree-item.tsx +247 -58
- package/src/components/sidebar/file-browser/file-tree.tsx +74 -10
- package/src/components/sidebar/file-browser/unified-search.tsx +10 -8
- package/src/components/sidebar/file-browser/use-long-press.ts +48 -0
- package/src/components/sidebar/git-changes/diff-viewer.tsx +8 -1
- package/src/components/sidebar/git-changes/git-file-item.tsx +42 -12
- package/src/components/sidebar/git-changes/git-panel.tsx +64 -22
- package/src/components/sidebar/sidebar-panel.tsx +7 -5
- package/src/components/task/conversation-view.tsx +112 -81
- package/src/components/task/file-drop-zone.tsx +3 -1
- 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 +14 -12
- package/src/components/task/shell-log-view.tsx +1 -1
- package/src/components/task/task-detail-panel.tsx +59 -26
- package/src/components/ui/context-menu.tsx +252 -0
- package/src/components/ui/dialog.tsx +1 -1
- package/src/components/ui/language-switcher.tsx +69 -0
- package/src/hooks/use-attempt-stream.ts +189 -33
- package/src/hooks/use-touch-detection.ts +51 -0
- package/src/i18n/config.ts +31 -0
- package/src/i18n/request.ts +15 -0
- package/src/lib/agent-manager.ts +113 -13
- package/src/lib/i18n.ts +30 -0
- package/src/lib/inline-edit-manager.ts +23 -4
- package/src/lib/sdk-event-adapter.ts +9 -61
- package/src/lib/system-prompt.ts +10 -19
- package/src/proxy.ts +35 -20
- package/src/stores/agent-factory-store.ts +24 -3
- package/src/stores/locale-store.ts +40 -0
- package/src/stores/settings-ui-store.ts +13 -0
- package/src/stores/sidebar-store.ts +24 -3
- package/src/types/agent-factory.ts +10 -0
- package/src/types/index.ts +6 -6
- package/tsconfig.json +3 -0
- /package/src/app/{docs → [locale]/docs}/swagger/page.tsx +0 -0
- /package/src/app/{not-found.tsx → [locale]/not-found.tsx} +0 -0
package/bin/claudekanban.js
CHANGED
|
@@ -14,6 +14,26 @@ const path = require('path');
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const os = require('os');
|
|
16
16
|
|
|
17
|
+
const isWindows = process.platform === 'win32';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert Windows backslash paths to forward slashes for safe shell embedding.
|
|
21
|
+
* On Unix, this is a no-op since paths already use forward slashes.
|
|
22
|
+
*/
|
|
23
|
+
function toShellSafePath(p) {
|
|
24
|
+
return isWindows ? p.replace(/\\/g, '/') : p;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Cross-platform command existence check.
|
|
29
|
+
* Uses 'where' on Windows, 'which' on Unix.
|
|
30
|
+
*/
|
|
31
|
+
function whichCommand(cmd) {
|
|
32
|
+
const { execSync } = require('child_process');
|
|
33
|
+
const checker = isWindows ? 'where' : 'which';
|
|
34
|
+
execSync(`${checker} ${cmd}`, { stdio: 'ignore' });
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
// Load environment variables
|
|
18
38
|
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
|
19
39
|
|
|
@@ -80,8 +100,7 @@ async function runMigrations() {
|
|
|
80
100
|
if (!tsxCmd) {
|
|
81
101
|
// Try global tsx
|
|
82
102
|
try {
|
|
83
|
-
|
|
84
|
-
execSync('which tsx', { stdio: 'ignore' });
|
|
103
|
+
whichCommand('tsx');
|
|
85
104
|
tsxCmd = 'tsx';
|
|
86
105
|
} catch {
|
|
87
106
|
throw new Error('tsx not found - this should not happen after dependency installation');
|
|
@@ -89,9 +108,13 @@ async function runMigrations() {
|
|
|
89
108
|
}
|
|
90
109
|
|
|
91
110
|
const { execSync } = require('child_process');
|
|
92
|
-
|
|
111
|
+
// Use forward slashes in shell-embedded paths to avoid Windows backslash escape issues
|
|
112
|
+
const safeDbPath = toShellSafePath(dbPath);
|
|
113
|
+
const safeTsxCmd = toShellSafePath(tsxCmd);
|
|
114
|
+
execSync(`"${safeTsxCmd}" -e "require('${safeDbPath}'); console.log('[Claude Workspace] ✓ Database ready');"`, {
|
|
93
115
|
cwd: packageRoot,
|
|
94
116
|
stdio: 'inherit',
|
|
117
|
+
shell: true,
|
|
95
118
|
env: { ...process.env }
|
|
96
119
|
});
|
|
97
120
|
|
|
@@ -119,7 +142,7 @@ async function startServer() {
|
|
|
119
142
|
|
|
120
143
|
let installCmd = 'npm install --production=false';
|
|
121
144
|
try {
|
|
122
|
-
|
|
145
|
+
whichCommand('pnpm');
|
|
123
146
|
installCmd = 'pnpm install --no-frozen-lockfile';
|
|
124
147
|
} catch {
|
|
125
148
|
// pnpm not found, use npm
|
|
@@ -172,9 +195,10 @@ async function startServer() {
|
|
|
172
195
|
const { execSync } = require('child_process');
|
|
173
196
|
try {
|
|
174
197
|
const nextBin = path.join(packageRoot, 'node_modules', '.bin', 'next');
|
|
198
|
+
const safeNextBin = toShellSafePath(nextBin);
|
|
175
199
|
|
|
176
200
|
// Run next build using local binary directly
|
|
177
|
-
execSync(`"${
|
|
201
|
+
execSync(`"${safeNextBin}" build`, {
|
|
178
202
|
cwd: packageRoot,
|
|
179
203
|
stdio: 'inherit',
|
|
180
204
|
shell: true,
|
|
@@ -218,8 +242,7 @@ async function startServer() {
|
|
|
218
242
|
if (!tsxCmd) {
|
|
219
243
|
try {
|
|
220
244
|
// Try using tsx from global or local pnpm/npm
|
|
221
|
-
|
|
222
|
-
execSync('which tsx', { stdio: 'ignore' });
|
|
245
|
+
whichCommand('tsx');
|
|
223
246
|
tsxCmd = 'tsx';
|
|
224
247
|
} catch {
|
|
225
248
|
console.error('[Claude Workspace] Error: tsx not found');
|
package/next.config.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { NextConfig } from "next";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import createNextIntlPlugin from 'next-intl/plugin';
|
|
4
|
+
|
|
5
|
+
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
|
|
3
6
|
|
|
4
7
|
const nextConfig: NextConfig = {
|
|
5
8
|
outputFileTracingRoot: path.join(__dirname),
|
|
@@ -26,4 +29,4 @@ const nextConfig: NextConfig = {
|
|
|
26
29
|
},
|
|
27
30
|
};
|
|
28
31
|
|
|
29
|
-
export default nextConfig;
|
|
32
|
+
export default withNextIntl(nextConfig);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-ws",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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": [
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"prepublishOnly": "npm run build"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
70
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.20",
|
|
71
71
|
"@anthropic-ai/sdk": "^0.71.2",
|
|
72
72
|
"@codemirror/lang-cpp": "^6.0.3",
|
|
73
73
|
"@codemirror/lang-css": "^6.3.1",
|
|
@@ -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",
|
|
@@ -121,9 +122,11 @@
|
|
|
121
122
|
"eslint-config-next": "^16.1.3",
|
|
122
123
|
"highlight.js": "^11.11.1",
|
|
123
124
|
"js-yaml": "^4.1.1",
|
|
125
|
+
"lowlight": "^3.3.0",
|
|
124
126
|
"lucide-react": "^0.562.0",
|
|
125
127
|
"nanoid": "^5.1.6",
|
|
126
128
|
"next": "^16.1.3",
|
|
129
|
+
"next-intl": "^4.7.0",
|
|
127
130
|
"next-themes": "^0.4.6",
|
|
128
131
|
"react": "19.2.3",
|
|
129
132
|
"react-dom": "19.2.3",
|
|
@@ -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)
|
|
@@ -630,9 +635,50 @@ app.prepare().then(async () => {
|
|
|
630
635
|
});
|
|
631
636
|
if (!project) return;
|
|
632
637
|
|
|
633
|
-
//
|
|
634
|
-
|
|
638
|
+
// Extract port and find existing process PID
|
|
639
|
+
// Add delay to let nohup process bind to port before checking
|
|
640
|
+
const portMatch = shell.originalCommand?.match(/lsof\s+-ti\s+:(\d+)/);
|
|
641
|
+
if (portMatch) {
|
|
642
|
+
const port = portMatch[1];
|
|
643
|
+
console.log(`[Server] Waiting 6.6s for process to bind to port ${port}...`);
|
|
644
|
+
await new Promise(resolve => setTimeout(resolve, 6666));
|
|
645
|
+
try {
|
|
646
|
+
const { execSync } = require('child_process');
|
|
647
|
+
const pidOutput = execSync(`lsof -ti :${port} 2>/dev/null || true`, { encoding: 'utf-8' }).trim();
|
|
648
|
+
if (pidOutput) {
|
|
649
|
+
const pid = parseInt(pidOutput.split('\n')[0], 10);
|
|
650
|
+
if (pid) {
|
|
651
|
+
// Track existing process instead of respawning
|
|
652
|
+
console.log(`[Server] Found existing process on port ${port}: PID ${pid}`);
|
|
653
|
+
const shellId = shellManager.trackExternalProcess({
|
|
654
|
+
projectId: project.id,
|
|
655
|
+
attemptId,
|
|
656
|
+
pid,
|
|
657
|
+
command: shell.command,
|
|
658
|
+
cwd: project.path,
|
|
659
|
+
});
|
|
635
660
|
|
|
661
|
+
if (shellId) {
|
|
662
|
+
await db.insert(schema.shells).values({
|
|
663
|
+
id: shellId,
|
|
664
|
+
projectId: project.id,
|
|
665
|
+
attemptId,
|
|
666
|
+
command: shell.command,
|
|
667
|
+
cwd: project.path,
|
|
668
|
+
pid,
|
|
669
|
+
status: 'running',
|
|
670
|
+
});
|
|
671
|
+
console.log(`[Server] Tracking external process ${shellId} (PID ${pid})`);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
} catch {
|
|
677
|
+
// Fall through to spawn new shell
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// No existing process found, spawn new shell
|
|
636
682
|
const shellId = shellManager.spawn({
|
|
637
683
|
projectId: project.id,
|
|
638
684
|
attemptId,
|
|
@@ -658,8 +704,8 @@ app.prepare().then(async () => {
|
|
|
658
704
|
});
|
|
659
705
|
|
|
660
706
|
// Handle tracked process from BGPID pattern in bash output
|
|
661
|
-
//
|
|
662
|
-
agentManager.on('trackedProcess', async ({ attemptId, pid, command }) => {
|
|
707
|
+
// Track existing process instead of kill-and-respawn to avoid port conflicts
|
|
708
|
+
agentManager.on('trackedProcess', async ({ attemptId, pid, command, logFile: eventLogFile }) => {
|
|
663
709
|
console.log(`[Server] Tracked process detected for ${attemptId}: PID ${pid}`);
|
|
664
710
|
|
|
665
711
|
try {
|
|
@@ -690,49 +736,43 @@ app.prepare().then(async () => {
|
|
|
690
736
|
return;
|
|
691
737
|
}
|
|
692
738
|
|
|
693
|
-
//
|
|
694
|
-
try {
|
|
695
|
-
process.kill(pid, 'SIGTERM');
|
|
696
|
-
console.log(`[Server] Killed nohup'd process ${pid}`);
|
|
697
|
-
} catch {
|
|
698
|
-
console.log(`[Server] Process ${pid} already dead or not killable`);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// Extract actual command from nohup wrapper
|
|
702
|
-
// Pattern: "nohup <command> > /tmp/xxx.log 2>&1 & echo ..."
|
|
703
|
-
// or: "kill ...; sleep ...; nohup <command> > ..."
|
|
739
|
+
// Extract actual command from nohup wrapper, use eventLogFile if provided
|
|
704
740
|
let actualCommand = command;
|
|
705
|
-
|
|
741
|
+
let logFile = eventLogFile;
|
|
742
|
+
const nohupMatch = command.match(/nohup\s+(.+?)\s*>\s*(\/tmp\/[^\s]+\.log)/);
|
|
706
743
|
if (nohupMatch) {
|
|
707
744
|
actualCommand = nohupMatch[1].trim();
|
|
745
|
+
logFile = logFile || nohupMatch[2];
|
|
708
746
|
}
|
|
709
|
-
console.log(`[Server] Extracted command: ${actualCommand}`);
|
|
747
|
+
console.log(`[Server] Extracted command: ${actualCommand}, logFile: ${logFile}`);
|
|
710
748
|
|
|
711
|
-
//
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
// Spawn via ShellManager for realtime stdout/stderr capture
|
|
715
|
-
const shellId = shellManager.spawn({
|
|
749
|
+
// Track existing process via ShellManager (no kill-and-respawn)
|
|
750
|
+
const shellId = shellManager.trackExternalProcess({
|
|
716
751
|
projectId: project.id,
|
|
717
752
|
attemptId,
|
|
753
|
+
pid,
|
|
718
754
|
command: actualCommand,
|
|
719
755
|
cwd: project.path,
|
|
720
|
-
|
|
756
|
+
logFile,
|
|
721
757
|
});
|
|
722
758
|
|
|
759
|
+
if (!shellId) {
|
|
760
|
+
console.error(`[Server] Failed to track process: PID ${pid} not alive`);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
723
764
|
// Save to database for persistence
|
|
724
|
-
const shellInfo = shellManager.getShellInfo(shellId);
|
|
725
765
|
await db.insert(schema.shells).values({
|
|
726
766
|
id: shellId,
|
|
727
767
|
projectId: project.id,
|
|
728
768
|
attemptId,
|
|
729
769
|
command: actualCommand,
|
|
730
770
|
cwd: project.path,
|
|
731
|
-
pid
|
|
771
|
+
pid,
|
|
732
772
|
status: 'running',
|
|
733
773
|
});
|
|
734
774
|
|
|
735
|
-
console.log(`[Server]
|
|
775
|
+
console.log(`[Server] Tracking external process ${shellId} (PID ${pid}) for project ${project.id}`);
|
|
736
776
|
} catch (error) {
|
|
737
777
|
console.error(`[Server] Failed to track process:`, error);
|
|
738
778
|
}
|
|
@@ -3,7 +3,11 @@ import { Geist, Geist_Mono } from 'next/font/google';
|
|
|
3
3
|
import { Toaster } from 'sonner';
|
|
4
4
|
import { ThemeProvider } from '@/components/providers/theme-provider';
|
|
5
5
|
import { SocketProvider } from '@/components/providers/socket-provider';
|
|
6
|
-
import '
|
|
6
|
+
import { NextIntlClientProvider } from 'next-intl';
|
|
7
|
+
import { getMessages } from 'next-intl/server';
|
|
8
|
+
import { notFound } from 'next/navigation';
|
|
9
|
+
import { locales } from '@/i18n/config';
|
|
10
|
+
import '../globals.css';
|
|
7
11
|
|
|
8
12
|
// Force dynamic rendering to avoid Next.js 16 Turbopack bugs
|
|
9
13
|
export const dynamic = 'force-dynamic';
|
|
@@ -42,22 +46,34 @@ export const metadata: Metadata = {
|
|
|
42
46
|
},
|
|
43
47
|
};
|
|
44
48
|
|
|
45
|
-
export default function RootLayout({
|
|
49
|
+
export default async function RootLayout({
|
|
46
50
|
children,
|
|
51
|
+
params,
|
|
47
52
|
}: Readonly<{
|
|
48
53
|
children: React.ReactNode;
|
|
54
|
+
params: Promise<{ locale: string }>;
|
|
49
55
|
}>) {
|
|
56
|
+
const { locale } = await params;
|
|
57
|
+
|
|
58
|
+
if (!locales.includes(locale as any)) {
|
|
59
|
+
notFound();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const messages = await getMessages();
|
|
63
|
+
|
|
50
64
|
return (
|
|
51
|
-
<html lang=
|
|
65
|
+
<html lang={locale} suppressHydrationWarning>
|
|
52
66
|
<body
|
|
53
67
|
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
|
|
54
68
|
>
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
<NextIntlClientProvider messages={messages}>
|
|
70
|
+
<SocketProvider>
|
|
71
|
+
<ThemeProvider>
|
|
72
|
+
{children}
|
|
73
|
+
<Toaster position="top-right" richColors closeButton />
|
|
74
|
+
</ThemeProvider>
|
|
75
|
+
</SocketProvider>
|
|
76
|
+
</NextIntlClientProvider>
|
|
61
77
|
</body>
|
|
62
78
|
</html>
|
|
63
79
|
);
|
|
@@ -7,7 +7,7 @@ import { Header } from '@/components/header';
|
|
|
7
7
|
import { Board } from '@/components/kanban/board';
|
|
8
8
|
import { CreateTaskDialog } from '@/components/kanban/create-task-dialog';
|
|
9
9
|
import { TaskDetailPanel } from '@/components/task/task-detail-panel';
|
|
10
|
-
import {
|
|
10
|
+
import { SettingsPage } from '@/components/settings/settings-page';
|
|
11
11
|
import { SetupDialog } from '@/components/settings/setup-dialog';
|
|
12
12
|
import { SidebarPanel, FileTabsPanel, DiffTabsPanel } from '@/components/sidebar';
|
|
13
13
|
import { RightSidebar } from '@/components/right-sidebar';
|
|
@@ -18,16 +18,17 @@ import { useTaskStore } from '@/stores/task-store';
|
|
|
18
18
|
import { Task } from '@/types';
|
|
19
19
|
import { useSidebarStore } from '@/stores/sidebar-store';
|
|
20
20
|
import { useAgentFactoryUIStore } from '@/stores/agent-factory-ui-store';
|
|
21
|
+
import { useSettingsUIStore } from '@/stores/settings-ui-store';
|
|
21
22
|
|
|
22
23
|
function KanbanApp() {
|
|
23
24
|
const [createTaskOpen, setCreateTaskOpen] = useState(false);
|
|
24
|
-
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
25
25
|
const [setupOpen, setSetupOpen] = useState(false);
|
|
26
26
|
const [apiKeyRefresh, setApiKeyRefresh] = useState(0);
|
|
27
27
|
const [searchQuery, setSearchQuery] = useState('');
|
|
28
28
|
|
|
29
29
|
const { needsApiKey } = useApiKeyCheck(apiKeyRefresh);
|
|
30
30
|
const { open: agentFactoryOpen, setOpen: setAgentFactoryOpen } = useAgentFactoryUIStore();
|
|
31
|
+
const { open: settingsOpen, setOpen: setSettingsOpen } = useSettingsUIStore();
|
|
31
32
|
|
|
32
33
|
const { projects, selectedProjectIds, fetchProjects, loading: projectLoading } = useProjectStore();
|
|
33
34
|
const { selectedTask, fetchTasks, setSelectedTask, setPendingAutoStartTask } = useTaskStore();
|
|
@@ -48,8 +49,8 @@ function KanbanApp() {
|
|
|
48
49
|
// Rehydrate from localStorage and fetch projects on mount
|
|
49
50
|
useEffect(() => {
|
|
50
51
|
useProjectStore.persist.rehydrate();
|
|
51
|
-
fetchProjects();
|
|
52
|
-
}, [
|
|
52
|
+
useProjectStore.getState().fetchProjects();
|
|
53
|
+
}, []);
|
|
53
54
|
|
|
54
55
|
// Read project from URL and select it
|
|
55
56
|
useEffect(() => {
|
|
@@ -90,9 +91,9 @@ function KanbanApp() {
|
|
|
90
91
|
// Fetch tasks when selectedProjectIds changes
|
|
91
92
|
useEffect(() => {
|
|
92
93
|
if (!projectLoading) {
|
|
93
|
-
fetchTasks(selectedProjectIds);
|
|
94
|
+
useTaskStore.getState().fetchTasks(selectedProjectIds);
|
|
94
95
|
}
|
|
95
|
-
}, [selectedProjectIds, projectLoading
|
|
96
|
+
}, [selectedProjectIds, projectLoading]);
|
|
96
97
|
|
|
97
98
|
// Handle task created event - select task if startNow is true
|
|
98
99
|
const handleTaskCreated = (task: Task, startNow: boolean, processedPrompt?: string, fileIds?: string[]) => {
|
|
@@ -172,7 +173,7 @@ function KanbanApp() {
|
|
|
172
173
|
<div className="flex h-screen items-center justify-center">
|
|
173
174
|
<div className="flex items-center gap-3 text-muted-foreground">
|
|
174
175
|
<img src="/logo.svg" alt="Logo" className="h-8 w-8 animate-spin" />
|
|
175
|
-
<span>Loading to Claude
|
|
176
|
+
<span>Loading to Claude<span style={{ color: '#d87756' }}>.</span>WS</span>
|
|
176
177
|
</div>
|
|
177
178
|
</div>
|
|
178
179
|
);
|
|
@@ -182,7 +183,6 @@ function KanbanApp() {
|
|
|
182
183
|
<div className="flex h-screen flex-col">
|
|
183
184
|
<Header
|
|
184
185
|
onCreateTask={() => setCreateTaskOpen(true)}
|
|
185
|
-
onOpenSettings={() => setSettingsOpen(true)}
|
|
186
186
|
onAddProject={() => setSetupOpen(true)}
|
|
187
187
|
searchQuery={searchQuery}
|
|
188
188
|
onSearchChange={setSearchQuery}
|
|
@@ -227,7 +227,6 @@ function KanbanApp() {
|
|
|
227
227
|
onOpenChange={setCreateTaskOpen}
|
|
228
228
|
onTaskCreated={handleTaskCreated}
|
|
229
229
|
/>
|
|
230
|
-
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
|
|
231
230
|
<SetupDialog open={setupOpen || autoShowSetup} onOpenChange={setSetupOpen} />
|
|
232
231
|
<ApiKeyDialog
|
|
233
232
|
open={needsApiKey}
|
|
@@ -250,11 +249,17 @@ function KanbanApp() {
|
|
|
250
249
|
</div>
|
|
251
250
|
)}
|
|
252
251
|
|
|
252
|
+
{/* Settings Page */}
|
|
253
|
+
{settingsOpen && (
|
|
254
|
+
<div className="fixed inset-0 z-50 bg-background">
|
|
255
|
+
<SettingsPage />
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
|
|
253
259
|
{/* Right Sidebar - actions panel */}
|
|
254
260
|
<RightSidebar
|
|
255
261
|
projectId={selectedProjectIds[0]}
|
|
256
262
|
onCreateTask={() => setCreateTaskOpen(true)}
|
|
257
|
-
onOpenSettings={() => setSettingsOpen(true)}
|
|
258
263
|
/>
|
|
259
264
|
</div>
|
|
260
265
|
);
|