Browse Source

fix(canvas-tui): Auto-find canvas dir, fix file selector, compact UI

- Search upward for .claude/canvas from any subdirectory
- Load most recent file from drafts/ on startup
- Add "no files" message when drafts/ is empty
- Compact status bar (filename only, shorter hints)
- Prevent accidental .claude/ creation in canvas-tui/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
0xDarkMatter 3 months ago
parent
commit
f8827aa4ab

+ 1 - 0
canvas-tui/.gitignore

@@ -2,3 +2,4 @@ node_modules/
 dist/
 *.log
 .DS_Store
+.claude/

+ 9 - 8
canvas-tui/src/app.tsx

@@ -211,23 +211,24 @@ export const App: React.FC<AppProps> = ({ watchPath, watchDir, enableMouse = tru
     }
   });
 
-  // Status message
+  // Status message - truncate path to just filename
+  const currentFileName = currentFilePath.split(/[/\\]/).pop() || 'file';
   const statusMessage = error
     ? `Error: ${error}`
     : syncStatus === 'waiting'
-    ? `Waiting for ${watchPath}...`
-    : `Updated: ${lastUpdate?.toLocaleTimeString() || ''}`;
+    ? `Waiting: ${currentFileName}`
+    : `Synced: ${currentFileName}`;
 
   const scrollHint = totalLines > contentHeight
-    ? ` | ${scrollOffset + 1}-${Math.min(scrollOffset + contentHeight, totalLines)}/${totalLines}`
+    ? ` ${scrollOffset + 1}-${Math.min(scrollOffset + contentHeight, totalLines)}/${totalLines}`
     : '';
 
-  const mouseHint = mouseEnabled ? 'mouse:on' : 'mouse:off';
+  const mouseHint = mouseEnabled ? 'on' : 'off';
   const hints = isEditing
-    ? 'Editing... save & quit editor to return'
+    ? 'Editing...'
     : isDropdownOpen
-    ? 'Tab:close | ↑↓:select | Enter:open'
-    : `Tab:files | e:edit | m:${mouseHint} | q:quit${scrollHint}`;
+    ? 'Tab:close ↑↓:nav Enter:open'
+    : `Tab:files e:edit m:${mouseHint} q:quit${scrollHint}`;
 
   return (
     <Box flexDirection="column" height={rows}>

+ 16 - 12
canvas-tui/src/components/FileSelector.tsx

@@ -40,18 +40,22 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
   lines.push(`${displayName} ▲${pad}`);
 
   // Separator
-  lines.push(`· · ·${pad}`);
-
-  // Other files
-  files.slice(0, 6).forEach((file, index) => {
-    const isSelected = index === selectedIndex;
-    const name = truncateFilename(file.name, MAX_DISPLAY_LENGTH);
-    const marker = isSelected ? '▸' : ' ';
-    lines.push(`${marker} ${name}${isSelected ? ' ◂' : '  '}${pad}`);
-  });
-
-  if (files.length > 6) {
-    lines.push(`  +${files.length - 6} more${pad}`);
+  lines.push(`─────────────────────${pad}`);
+
+  // Show files or "no files" message
+  if (files.length === 0) {
+    lines.push(`  (no files in drafts/)${pad}`);
+  } else {
+    files.slice(0, 6).forEach((file, index) => {
+      const isSelected = index === selectedIndex;
+      const name = truncateFilename(file.name, MAX_DISPLAY_LENGTH);
+      const marker = isSelected ? '▸' : ' ';
+      lines.push(`${marker} ${name}${isSelected ? ' ◂' : '  '}${pad}`);
+    });
+
+    if (files.length > 6) {
+      lines.push(`  +${files.length - 6} more${pad}`);
+    }
   }
 
   return <Text>{lines.join('\n')}</Text>;

+ 3 - 10
canvas-tui/src/hooks/useDirectoryFiles.ts

@@ -9,7 +9,7 @@ export interface FileInfo {
 }
 
 /**
- * Scans a directory for markdown and text files.
+ * Scans a directory's 'drafts' subdirectory for markdown and text files.
  * Returns sorted list (most recently modified first).
  */
 export function useDirectoryFiles(dirPath: string): FileInfo[] {
@@ -18,9 +18,7 @@ export function useDirectoryFiles(dirPath: string): FileInfo[] {
   useEffect(() => {
     const scanDirectory = () => {
       try {
-        // Resolve to absolute path, scan 'drafts' subdirectory
-        const absoluteDir = path.resolve(dirPath);
-        const draftsDir = path.join(absoluteDir, 'drafts');
+        const draftsDir = path.join(dirPath, 'drafts');
 
         // Create drafts dir if it doesn't exist
         if (!fs.existsSync(draftsDir)) {
@@ -49,18 +47,13 @@ export function useDirectoryFiles(dirPath: string): FileInfo[] {
         // Sort by modified time (most recent first)
         fileInfos.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
         setFiles(fileInfos);
-      } catch (err) {
-        // Directory might not exist yet
+      } catch {
         setFiles([]);
       }
     };
 
-    // Initial scan
     scanDirectory();
-
-    // Re-scan periodically (every 2 seconds) to catch new files
     const interval = setInterval(scanDirectory, 2000);
-
     return () => clearInterval(interval);
   }, [dirPath]);
 

+ 64 - 2
canvas-tui/src/index.tsx

@@ -1,5 +1,7 @@
 #!/usr/bin/env node
 import React from 'react';
+import fs from 'fs';
+import path from 'path';
 import { render } from 'ink';
 import meow from 'meow';
 import { App } from './app.js';
@@ -50,6 +52,66 @@ const cli = meow(`
   }
 });
 
-const watchPath = cli.flags.file || `${cli.flags.watch}/content.md`;
+// Find canvas directory by searching up from CWD
+function findCanvasDir(startDir: string = process.cwd()): string | null {
+  let current = startDir;
+  const root = path.parse(current).root;
 
-render(<App watchPath={watchPath} watchDir={cli.flags.watch} enableMouse={!cli.flags.noMouse} />);
+  while (current !== root) {
+    const candidate = path.join(current, '.claude', 'canvas');
+    if (fs.existsSync(candidate)) {
+      return candidate;
+    }
+    current = path.dirname(current);
+  }
+  return null;
+}
+
+// Determine watch directory - find it automatically or use explicit path
+function getWatchDir(explicitPath: string): string {
+  // If user specified absolute path, use it
+  if (path.isAbsolute(explicitPath)) {
+    return explicitPath;
+  }
+
+  // Try to find .claude/canvas by searching up
+  const found = findCanvasDir();
+  if (found) {
+    return found;
+  }
+
+  // Fall back to relative path from CWD
+  return path.resolve(explicitPath);
+}
+
+// Determine initial file to watch
+function getInitialFile(watchDir: string, specificFile?: string): string {
+  if (specificFile) return path.resolve(specificFile);
+
+  // Check drafts directory first
+  const draftsDir = path.join(watchDir, 'drafts');
+  try {
+    if (fs.existsSync(draftsDir)) {
+      const files = fs.readdirSync(draftsDir)
+        .filter(f => f.endsWith('.md') || f.endsWith('.txt'))
+        .sort((a, b) => {
+          const statA = fs.statSync(path.join(draftsDir, a));
+          const statB = fs.statSync(path.join(draftsDir, b));
+          return statB.mtime.getTime() - statA.mtime.getTime();
+        });
+      if (files.length > 0) {
+        return path.join(draftsDir, files[0]);
+      }
+    }
+  } catch {
+    // Fall through to default
+  }
+
+  // Fallback to content.md in watch dir
+  return path.join(watchDir, 'content.md');
+}
+
+const watchDir = getWatchDir(cli.flags.watch);
+const watchPath = getInitialFile(watchDir, cli.flags.file);
+
+render(<App watchPath={watchPath} watchDir={watchDir} enableMouse={!cli.flags.noMouse} />);