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.
Files changed (70) hide show
  1. package/bin/claudekanban.js +30 -7
  2. package/next.config.ts +4 -1
  3. package/package.json +5 -2
  4. package/public/docs/swagger/swagger.yaml +135 -0
  5. package/server.ts +67 -27
  6. package/src/app/{layout.tsx → [locale]/layout.tsx} +25 -9
  7. package/src/app/{page.tsx → [locale]/page.tsx} +15 -10
  8. package/src/app/api/agent-factory/discover/route.ts +123 -19
  9. package/src/app/api/attempts/[id]/alive/route.ts +30 -0
  10. package/src/app/api/attempts/[id]/answer/route.ts +42 -0
  11. package/src/app/api/attempts/[id]/route.ts +57 -0
  12. package/src/app/api/files/operations/route.ts +415 -0
  13. package/src/app/api/git/generate-message/route.ts +1 -1
  14. package/src/app/api/git/gitignore/route.ts +81 -0
  15. package/src/app/api/tasks/[id]/conversation/route.ts +20 -1
  16. package/src/app/globals.css +15 -4
  17. package/src/components/agent-factory/discovery-dialog.tsx +375 -142
  18. package/src/components/claude/message-block.tsx +98 -121
  19. package/src/components/claude/tool-use-block.tsx +24 -4
  20. package/src/components/editor/markdown-file-viewer.tsx +126 -0
  21. package/src/components/header.tsx +15 -48
  22. package/src/components/kanban/board.tsx +17 -3
  23. package/src/components/kanban/column.tsx +15 -10
  24. package/src/components/kanban/create-task-dialog.tsx +19 -17
  25. package/src/components/kanban/task-card.tsx +19 -21
  26. package/src/components/project-settings/project-settings-dialog.tsx +1 -0
  27. package/src/components/providers/socket-provider.tsx +7 -11
  28. package/src/components/right-sidebar.tsx +69 -16
  29. package/src/components/settings/settings-dialog.tsx +5 -68
  30. package/src/components/settings/settings-page.tsx +101 -0
  31. package/src/components/sidebar/file-browser/file-create-buttons.tsx +183 -0
  32. package/src/components/sidebar/file-browser/file-tab-content.tsx +49 -8
  33. package/src/components/sidebar/file-browser/file-tree-context-menu.tsx +390 -0
  34. package/src/components/sidebar/file-browser/file-tree-item.tsx +247 -58
  35. package/src/components/sidebar/file-browser/file-tree.tsx +74 -10
  36. package/src/components/sidebar/file-browser/unified-search.tsx +10 -8
  37. package/src/components/sidebar/file-browser/use-long-press.ts +48 -0
  38. package/src/components/sidebar/git-changes/diff-viewer.tsx +8 -1
  39. package/src/components/sidebar/git-changes/git-file-item.tsx +42 -12
  40. package/src/components/sidebar/git-changes/git-panel.tsx +64 -22
  41. package/src/components/sidebar/sidebar-panel.tsx +7 -5
  42. package/src/components/task/conversation-view.tsx +112 -81
  43. package/src/components/task/file-drop-zone.tsx +3 -1
  44. package/src/components/task/interactive-command/question-prompt.tsx +12 -1
  45. package/src/components/task/pending-question-indicator.tsx +57 -0
  46. package/src/components/task/prompt-input.tsx +14 -12
  47. package/src/components/task/shell-log-view.tsx +1 -1
  48. package/src/components/task/task-detail-panel.tsx +59 -26
  49. package/src/components/ui/context-menu.tsx +252 -0
  50. package/src/components/ui/dialog.tsx +1 -1
  51. package/src/components/ui/language-switcher.tsx +69 -0
  52. package/src/hooks/use-attempt-stream.ts +189 -33
  53. package/src/hooks/use-touch-detection.ts +51 -0
  54. package/src/i18n/config.ts +31 -0
  55. package/src/i18n/request.ts +15 -0
  56. package/src/lib/agent-manager.ts +113 -13
  57. package/src/lib/i18n.ts +30 -0
  58. package/src/lib/inline-edit-manager.ts +23 -4
  59. package/src/lib/sdk-event-adapter.ts +9 -61
  60. package/src/lib/system-prompt.ts +10 -19
  61. package/src/proxy.ts +35 -20
  62. package/src/stores/agent-factory-store.ts +24 -3
  63. package/src/stores/locale-store.ts +40 -0
  64. package/src/stores/settings-ui-store.ts +13 -0
  65. package/src/stores/sidebar-store.ts +24 -3
  66. package/src/types/agent-factory.ts +10 -0
  67. package/src/types/index.ts +6 -6
  68. package/tsconfig.json +3 -0
  69. /package/src/app/{docs → [locale]/docs}/swagger/page.tsx +0 -0
  70. /package/src/app/{not-found.tsx → [locale]/not-found.tsx} +0 -0
@@ -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
- const { execSync } = require('child_process');
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
- execSync(`"${tsxCmd}" -e "require('${dbPath}'); console.log('[Claude Workspace] Database ready');"`, {
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
- execSync('which pnpm', { stdio: 'ignore' });
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(`"${nextBin}" build`, {
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
- const { execSync } = require('child_process');
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.2.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.5",
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
- // Add delay to let SDK's process start first, then our kill command takes over
634
- await new Promise(resolve => setTimeout(resolve, 2000));
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
- // Kill the nohup'd process and respawn via ShellManager for realtime streaming
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
- // Kill the nohup'd process - we'll respawn via ShellManager for realtime streaming
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
- const nohupMatch = command.match(/nohup\s+(.+?)\s*>\s*\/tmp\//);
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
- // Wait for port to be released
712
- await new Promise(resolve => setTimeout(resolve, 1000));
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
- description: `Background: ${actualCommand.substring(0, 50)}`,
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: shellInfo?.pid || null,
771
+ pid,
732
772
  status: 'running',
733
773
  });
734
774
 
735
- console.log(`[Server] Tracked external process ${shellId} (PID ${pid}) for project ${project.id}`);
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 './globals.css';
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="en" suppressHydrationWarning>
65
+ <html lang={locale} suppressHydrationWarning>
52
66
  <body
53
67
  className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
54
68
  >
55
- <SocketProvider>
56
- <ThemeProvider>
57
- {children}
58
- <Toaster position="top-right" richColors closeButton />
59
- </ThemeProvider>
60
- </SocketProvider>
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 { SettingsDialog } from '@/components/settings/settings-dialog';
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
- }, [fetchProjects]);
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, fetchTasks]);
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 Workspace</span>
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
  );