claude-ws 0.2.1 → 0.3.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 (63) hide show
  1. package/bin/claudekanban.js +30 -7
  2. package/next.config.ts +4 -1
  3. package/package.json +5 -2
  4. package/server.ts +80 -26
  5. package/src/app/{layout.tsx → [locale]/layout.tsx} +25 -9
  6. package/src/app/{page.tsx → [locale]/page.tsx} +15 -25
  7. package/src/app/api/agent-factory/discover/route.ts +123 -19
  8. package/src/app/api/git/generate-message/route.ts +1 -1
  9. package/src/app/api/tasks/[id]/conversation/route.ts +7 -1
  10. package/src/app/api/tasks/[id]/route.ts +20 -1
  11. package/src/app/globals.css +11 -0
  12. package/src/components/agent-factory/discovery-dialog.tsx +375 -142
  13. package/src/components/auth/api-key-dialog.tsx +68 -10
  14. package/src/components/claude/message-block.tsx +98 -121
  15. package/src/components/claude/tool-use-block.tsx +4 -3
  16. package/src/components/editor/markdown-file-viewer.tsx +234 -0
  17. package/src/components/header.tsx +11 -8
  18. package/src/components/kanban/board.tsx +7 -1
  19. package/src/components/kanban/column.tsx +15 -10
  20. package/src/components/kanban/create-task-dialog.tsx +19 -17
  21. package/src/components/kanban/task-card.tsx +2 -52
  22. package/src/components/project-settings/project-settings-dialog.tsx +1 -0
  23. package/src/components/providers/socket-provider.tsx +7 -11
  24. package/src/components/right-sidebar.tsx +74 -9
  25. package/src/components/settings/settings-dialog.tsx +5 -68
  26. package/src/components/settings/settings-page.tsx +101 -0
  27. package/src/components/sidebar/file-browser/file-create-buttons.tsx +183 -0
  28. package/src/components/sidebar/file-browser/file-tab-content.tsx +120 -10
  29. package/src/components/sidebar/file-browser/file-tree-context-menu.tsx +26 -21
  30. package/src/components/sidebar/file-browser/file-tree-item.tsx +11 -5
  31. package/src/components/sidebar/file-browser/file-tree.tsx +64 -9
  32. package/src/components/sidebar/file-browser/unified-search.tsx +10 -8
  33. package/src/components/sidebar/git-changes/diff-viewer.tsx +8 -1
  34. package/src/components/sidebar/git-changes/git-panel.tsx +44 -23
  35. package/src/components/sidebar/sidebar-panel.tsx +7 -5
  36. package/src/components/task/conversation-view.tsx +78 -77
  37. package/src/components/task/file-drop-zone.tsx +3 -1
  38. package/src/components/task/prompt-input.tsx +13 -11
  39. package/src/components/task/shell-log-view.tsx +1 -1
  40. package/src/components/task/task-detail-panel.tsx +16 -16
  41. package/src/components/ui/dialog.tsx +1 -1
  42. package/src/components/ui/language-switcher.tsx +69 -0
  43. package/src/hooks/use-attempt-stream.ts +58 -77
  44. package/src/hooks/use-touch-detection.ts +51 -0
  45. package/src/i18n/config.ts +31 -0
  46. package/src/i18n/request.ts +15 -0
  47. package/src/lib/agent-manager.ts +109 -13
  48. package/src/lib/i18n.ts +30 -0
  49. package/src/lib/inline-edit-manager.ts +23 -4
  50. package/src/lib/sdk-event-adapter.ts +9 -61
  51. package/src/lib/session-manager.ts +7 -4
  52. package/src/lib/system-prompt.ts +10 -19
  53. package/src/lib/utils.ts +39 -0
  54. package/src/proxy.ts +35 -20
  55. package/src/stores/agent-factory-store.ts +24 -3
  56. package/src/stores/locale-store.ts +40 -0
  57. package/src/stores/settings-ui-store.ts +13 -0
  58. package/src/stores/sidebar-store.ts +7 -3
  59. package/src/types/agent-factory.ts +10 -0
  60. package/src/types/index.ts +6 -6
  61. package/tsconfig.json +3 -0
  62. /package/src/app/{docs → [locale]/docs}/swagger/page.tsx +0 -0
  63. /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.1",
3
+ "version": "0.3.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": [
@@ -16,6 +16,7 @@
16
16
  "sqlite"
17
17
  ],
18
18
  "license": "MIT",
19
+ "author": "Claude Workspace",
19
20
  "repository": {
20
21
  "type": "git",
21
22
  "url": "https://github.com/Claude-Workspace/claude-ws.git"
@@ -67,7 +68,7 @@
67
68
  "prepublishOnly": "npm run build"
68
69
  },
69
70
  "dependencies": {
70
- "@anthropic-ai/claude-agent-sdk": "^0.2.5",
71
+ "@anthropic-ai/claude-agent-sdk": "^0.2.20",
71
72
  "@anthropic-ai/sdk": "^0.71.2",
72
73
  "@codemirror/lang-cpp": "^6.0.3",
73
74
  "@codemirror/lang-css": "^6.3.1",
@@ -122,9 +123,11 @@
122
123
  "eslint-config-next": "^16.1.3",
123
124
  "highlight.js": "^11.11.1",
124
125
  "js-yaml": "^4.1.1",
126
+ "lowlight": "^3.3.0",
125
127
  "lucide-react": "^0.562.0",
126
128
  "nanoid": "^5.1.6",
127
129
  "next": "^16.1.3",
130
+ "next-intl": "^4.7.0",
128
131
  "next-themes": "^0.4.6",
129
132
  "react": "19.2.3",
130
133
  "react-dom": "19.2.3",
package/server.ts CHANGED
@@ -26,12 +26,31 @@ const dev = process.env.NODE_ENV !== 'production';
26
26
  const hostname = 'localhost';
27
27
  const port = parseInt(process.env.PORT || '8556', 10);
28
28
 
29
+ // API authentication key (optional)
30
+ const API_ACCESS_KEY = process.env.API_ACCESS_KEY;
31
+
29
32
  const app = next({ dev, hostname, port, turbopack: false });
30
33
  const handle = app.getRequestHandler();
31
34
 
32
35
  app.prepare().then(async () => {
33
36
  const httpServer = createServer((req, res) => {
34
37
  const parsedUrl = parse(req.url!, true);
38
+ const pathname = parsedUrl.pathname || '';
39
+
40
+ // API authentication check
41
+ const isApiRoute = pathname.startsWith('/api/');
42
+ const isVerifyEndpoint = pathname === '/api/auth/verify';
43
+
44
+ if (isApiRoute && !isVerifyEndpoint && API_ACCESS_KEY && API_ACCESS_KEY.length > 0) {
45
+ const providedKey = req.headers['x-api-key'];
46
+
47
+ if (!providedKey || providedKey !== API_ACCESS_KEY) {
48
+ res.writeHead(401, { 'Content-Type': 'application/json' });
49
+ res.end(JSON.stringify({ error: 'Unauthorized', message: 'Valid API key required' }));
50
+ return;
51
+ }
52
+ }
53
+
35
54
  handle(req, res, parsedUrl);
36
55
  });
37
56
 
@@ -635,9 +654,50 @@ app.prepare().then(async () => {
635
654
  });
636
655
  if (!project) return;
637
656
 
638
- // Add delay to let SDK's process start first, then our kill command takes over
639
- await new Promise(resolve => setTimeout(resolve, 2000));
657
+ // Extract port and find existing process PID
658
+ // Add delay to let nohup process bind to port before checking
659
+ const portMatch = shell.originalCommand?.match(/lsof\s+-ti\s+:(\d+)/);
660
+ if (portMatch) {
661
+ const port = portMatch[1];
662
+ console.log(`[Server] Waiting 6.6s for process to bind to port ${port}...`);
663
+ await new Promise(resolve => setTimeout(resolve, 6666));
664
+ try {
665
+ const { execSync } = require('child_process');
666
+ const pidOutput = execSync(`lsof -ti :${port} 2>/dev/null || true`, { encoding: 'utf-8' }).trim();
667
+ if (pidOutput) {
668
+ const pid = parseInt(pidOutput.split('\n')[0], 10);
669
+ if (pid) {
670
+ // Track existing process instead of respawning
671
+ console.log(`[Server] Found existing process on port ${port}: PID ${pid}`);
672
+ const shellId = shellManager.trackExternalProcess({
673
+ projectId: project.id,
674
+ attemptId,
675
+ pid,
676
+ command: shell.command,
677
+ cwd: project.path,
678
+ });
640
679
 
680
+ if (shellId) {
681
+ await db.insert(schema.shells).values({
682
+ id: shellId,
683
+ projectId: project.id,
684
+ attemptId,
685
+ command: shell.command,
686
+ cwd: project.path,
687
+ pid,
688
+ status: 'running',
689
+ });
690
+ console.log(`[Server] Tracking external process ${shellId} (PID ${pid})`);
691
+ return;
692
+ }
693
+ }
694
+ }
695
+ } catch {
696
+ // Fall through to spawn new shell
697
+ }
698
+ }
699
+
700
+ // No existing process found, spawn new shell
641
701
  const shellId = shellManager.spawn({
642
702
  projectId: project.id,
643
703
  attemptId,
@@ -663,8 +723,8 @@ app.prepare().then(async () => {
663
723
  });
664
724
 
665
725
  // Handle tracked process from BGPID pattern in bash output
666
- // Kill the nohup'd process and respawn via ShellManager for realtime streaming
667
- agentManager.on('trackedProcess', async ({ attemptId, pid, command }) => {
726
+ // Track existing process instead of kill-and-respawn to avoid port conflicts
727
+ agentManager.on('trackedProcess', async ({ attemptId, pid, command, logFile: eventLogFile }) => {
668
728
  console.log(`[Server] Tracked process detected for ${attemptId}: PID ${pid}`);
669
729
 
670
730
  try {
@@ -695,49 +755,43 @@ app.prepare().then(async () => {
695
755
  return;
696
756
  }
697
757
 
698
- // Kill the nohup'd process - we'll respawn via ShellManager for realtime streaming
699
- try {
700
- process.kill(pid, 'SIGTERM');
701
- console.log(`[Server] Killed nohup'd process ${pid}`);
702
- } catch {
703
- console.log(`[Server] Process ${pid} already dead or not killable`);
704
- }
705
-
706
- // Extract actual command from nohup wrapper
707
- // Pattern: "nohup <command> > /tmp/xxx.log 2>&1 & echo ..."
708
- // or: "kill ...; sleep ...; nohup <command> > ..."
758
+ // Extract actual command from nohup wrapper, use eventLogFile if provided
709
759
  let actualCommand = command;
710
- const nohupMatch = command.match(/nohup\s+(.+?)\s*>\s*\/tmp\//);
760
+ let logFile = eventLogFile;
761
+ const nohupMatch = command.match(/nohup\s+(.+?)\s*>\s*(\/tmp\/[^\s]+\.log)/);
711
762
  if (nohupMatch) {
712
763
  actualCommand = nohupMatch[1].trim();
764
+ logFile = logFile || nohupMatch[2];
713
765
  }
714
- console.log(`[Server] Extracted command: ${actualCommand}`);
715
-
716
- // Wait for port to be released
717
- await new Promise(resolve => setTimeout(resolve, 1000));
766
+ console.log(`[Server] Extracted command: ${actualCommand}, logFile: ${logFile}`);
718
767
 
719
- // Spawn via ShellManager for realtime stdout/stderr capture
720
- const shellId = shellManager.spawn({
768
+ // Track existing process via ShellManager (no kill-and-respawn)
769
+ const shellId = shellManager.trackExternalProcess({
721
770
  projectId: project.id,
722
771
  attemptId,
772
+ pid,
723
773
  command: actualCommand,
724
774
  cwd: project.path,
725
- description: `Background: ${actualCommand.substring(0, 50)}`,
775
+ logFile,
726
776
  });
727
777
 
778
+ if (!shellId) {
779
+ console.error(`[Server] Failed to track process: PID ${pid} not alive`);
780
+ return;
781
+ }
782
+
728
783
  // Save to database for persistence
729
- const shellInfo = shellManager.getShellInfo(shellId);
730
784
  await db.insert(schema.shells).values({
731
785
  id: shellId,
732
786
  projectId: project.id,
733
787
  attemptId,
734
788
  command: actualCommand,
735
789
  cwd: project.path,
736
- pid: shellInfo?.pid || null,
790
+ pid,
737
791
  status: 'running',
738
792
  });
739
793
 
740
- console.log(`[Server] Tracked external process ${shellId} (PID ${pid}) for project ${project.id}`);
794
+ console.log(`[Server] Tracking external process ${shellId} (PID ${pid}) for project ${project.id}`);
741
795
  } catch (error) {
742
796
  console.error(`[Server] Failed to track process:`, error);
743
797
  }
@@ -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,27 +7,26 @@ 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';
14
- import { ApiKeyProvider, ApiKeyDialog, useApiKeyCheck } from '@/components/auth/api-key-dialog';
14
+ import { ApiKeyProvider } from '@/components/auth/api-key-dialog';
15
15
  import { PluginList } from '@/components/agent-factory/plugin-list';
16
16
  import { useProjectStore } from '@/stores/project-store';
17
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
- const [apiKeyRefresh, setApiKeyRefresh] = useState(0);
27
26
  const [searchQuery, setSearchQuery] = useState('');
28
27
 
29
- const { needsApiKey } = useApiKeyCheck(apiKeyRefresh);
30
28
  const { open: agentFactoryOpen, setOpen: setAgentFactoryOpen } = useAgentFactoryUIStore();
29
+ const { open: settingsOpen, setOpen: setSettingsOpen } = useSettingsUIStore();
31
30
 
32
31
  const { projects, selectedProjectIds, fetchProjects, loading: projectLoading } = useProjectStore();
33
32
  const { selectedTask, fetchTasks, setSelectedTask, setPendingAutoStartTask } = useTaskStore();
@@ -48,8 +47,8 @@ function KanbanApp() {
48
47
  // Rehydrate from localStorage and fetch projects on mount
49
48
  useEffect(() => {
50
49
  useProjectStore.persist.rehydrate();
51
- fetchProjects();
52
- }, [fetchProjects]);
50
+ useProjectStore.getState().fetchProjects();
51
+ }, []);
53
52
 
54
53
  // Read project from URL and select it
55
54
  useEffect(() => {
@@ -90,9 +89,9 @@ function KanbanApp() {
90
89
  // Fetch tasks when selectedProjectIds changes
91
90
  useEffect(() => {
92
91
  if (!projectLoading) {
93
- fetchTasks(selectedProjectIds);
92
+ useTaskStore.getState().fetchTasks(selectedProjectIds);
94
93
  }
95
- }, [selectedProjectIds, projectLoading, fetchTasks]);
94
+ }, [selectedProjectIds, projectLoading]);
96
95
 
97
96
  // Handle task created event - select task if startNow is true
98
97
  const handleTaskCreated = (task: Task, startNow: boolean, processedPrompt?: string, fileIds?: string[]) => {
@@ -182,7 +181,6 @@ function KanbanApp() {
182
181
  <div className="flex h-screen flex-col">
183
182
  <Header
184
183
  onCreateTask={() => setCreateTaskOpen(true)}
185
- onOpenSettings={() => setSettingsOpen(true)}
186
184
  onAddProject={() => setSetupOpen(true)}
187
185
  searchQuery={searchQuery}
188
186
  onSearchChange={setSearchQuery}
@@ -227,21 +225,7 @@ function KanbanApp() {
227
225
  onOpenChange={setCreateTaskOpen}
228
226
  onTaskCreated={handleTaskCreated}
229
227
  />
230
- <SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
231
228
  <SetupDialog open={setupOpen || autoShowSetup} onOpenChange={setSetupOpen} />
232
- <ApiKeyDialog
233
- open={needsApiKey}
234
- onOpenChange={(open) => {
235
- // Allow closing only if not needed
236
- if (!open && !needsApiKey) return;
237
- }}
238
- onSuccess={() => {
239
- // Trigger refresh to re-check API key status
240
- setApiKeyRefresh(prev => prev + 1);
241
- // Refetch projects after API key is set
242
- fetchProjects();
243
- }}
244
- />
245
229
 
246
230
  {/* Agent Factory Dialog */}
247
231
  {agentFactoryOpen && (
@@ -250,11 +234,17 @@ function KanbanApp() {
250
234
  </div>
251
235
  )}
252
236
 
237
+ {/* Settings Page */}
238
+ {settingsOpen && (
239
+ <div className="fixed inset-0 z-50 bg-background">
240
+ <SettingsPage />
241
+ </div>
242
+ )}
243
+
253
244
  {/* Right Sidebar - actions panel */}
254
245
  <RightSidebar
255
246
  projectId={selectedProjectIds[0]}
256
247
  onCreateTask={() => setCreateTaskOpen(true)}
257
- onOpenSettings={() => setSettingsOpen(true)}
258
248
  />
259
249
  </div>
260
250
  );