Merge branch 'main' into johnhenry/env-claude-path

This commit is contained in:
John Henry
2025-09-15 14:30:59 -07:00
committed by GitHub
9 changed files with 197 additions and 48 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Claude Code UI</title>
<!-- PWA Manifest -->
@@ -12,7 +12,7 @@
<!-- iOS Safari PWA Meta Tags -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Claude UI" />
<!-- iOS Safari Icons -->

View File

@@ -506,7 +506,7 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
return res.status(404).json({ error: `Project path not found: ${actualPath}` });
}
const files = await getFileTree(actualPath, 3, 0, true);
const files = await getFileTree(actualPath, 10, 0, true);
const hiddenFiles = files.filter(f => f.name.startsWith('.'));
res.json(files);
} catch (error) {

View File

@@ -899,23 +899,17 @@ async function addProjectManually(projectPath, displayName = null) {
// Generate project name (encode path for use as directory name)
const projectName = absolutePath.replace(/\//g, '-');
// Check if project already exists in config or as a folder
// Check if project already exists in config
const config = await loadProjectConfig();
const projectDir = path.join(process.env.HOME, '.claude', 'projects', projectName);
try {
await fs.access(projectDir);
throw new Error(`Project already exists for path: ${absolutePath}`);
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
if (config[projectName]) {
throw new Error(`Project already configured for path: ${absolutePath}`);
}
// Allow adding projects even if the directory exists - this enables tracking
// existing Claude Code or Cursor projects in the UI
// Add to config as manually added project
config[projectName] = {
manuallyAdded: true,

View File

@@ -78,6 +78,37 @@ function AppContent() {
const { ws, sendMessage, messages } = useWebSocketContext();
// Detect if running as PWA
const [isPWA, setIsPWA] = useState(false);
useEffect(() => {
// Check if running in standalone mode (PWA)
const checkPWA = () => {
const isStandalone = window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone ||
document.referrer.includes('android-app://');
setIsPWA(isStandalone);
// Add class to html and body for CSS targeting
if (isStandalone) {
document.documentElement.classList.add('pwa-mode');
document.body.classList.add('pwa-mode');
} else {
document.documentElement.classList.remove('pwa-mode');
document.body.classList.remove('pwa-mode');
}
};
checkPWA();
// Listen for changes
window.matchMedia('(display-mode: standalone)').addEventListener('change', checkPWA);
return () => {
window.matchMedia('(display-mode: standalone)').removeEventListener('change', checkPWA);
};
}, []);
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
@@ -561,6 +592,8 @@ function AppContent() {
latestVersion={latestVersion}
currentVersion={currentVersion}
onShowVersionModal={() => setShowVersionModal(true)}
isPWA={isPWA}
isMobile={isMobile}
/>
</div>
</div>
@@ -607,6 +640,8 @@ function AppContent() {
latestVersion={latestVersion}
currentVersion={currentVersion}
onShowVersionModal={() => setShowVersionModal(true)}
isPWA={isPWA}
isMobile={isMobile}
/>
</div>
</div>
@@ -623,6 +658,7 @@ function AppContent() {
sendMessage={sendMessage}
messages={messages}
isMobile={isMobile}
isPWA={isPWA}
onMenuClick={() => setSidebarOpen(true)}
isLoading={isLoadingProjects}
onInputFocusChange={setIsInputFocused}

View File

@@ -0,0 +1,41 @@
import React from 'react';
function DiffViewer({ diff, fileName, isMobile, wrapText }) {
if (!diff) {
return (
<div className="p-4 text-center text-gray-500 dark:text-gray-400 text-sm">
No diff available
</div>
);
}
const renderDiffLine = (line, index) => {
const isAddition = line.startsWith('+') && !line.startsWith('+++');
const isDeletion = line.startsWith('-') && !line.startsWith('---');
const isHeader = line.startsWith('@@');
return (
<div
key={index}
className={`font-mono text-xs p-2 ${
isMobile && wrapText ? 'whitespace-pre-wrap break-all' : 'whitespace-pre overflow-x-auto'
} ${
isAddition ? 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300' :
isDeletion ? 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300' :
isHeader ? 'bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300' :
'text-gray-600 dark:text-gray-400'
}`}
>
{line}
</div>
);
};
return (
<div className="diff-viewer">
{diff.split('\n').map((line, index) => renderDiffLine(line, index))}
</div>
);
}
export default DiffViewer;

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import { GitBranch, GitCommit, Plus, Minus, RefreshCw, Check, X, ChevronDown, ChevronRight, Info, History, FileText, Mic, MicOff, Sparkles, Download, RotateCcw, Trash2, AlertTriangle, Upload } from 'lucide-react';
import { MicButton } from './MicButton.jsx';
import { authenticatedFetch } from '../utils/api';
import DiffViewer from './DiffViewer.jsx';
function GitPanel({ selectedProject, isMobile }) {
const [gitStatus, setGitStatus] = useState(null);
@@ -523,27 +524,6 @@ function GitPanel({ selectedProject, isMobile }) {
}
};
const renderDiffLine = (line, index) => {
const isAddition = line.startsWith('+') && !line.startsWith('+++');
const isDeletion = line.startsWith('-') && !line.startsWith('---');
const isHeader = line.startsWith('@@');
return (
<div
key={index}
className={`font-mono text-xs ${
isMobile && wrapText ? 'whitespace-pre-wrap break-all' : 'whitespace-pre overflow-x-auto'
} ${
isAddition ? 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300' :
isDeletion ? 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300' :
isHeader ? 'bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300' :
'text-gray-600 dark:text-gray-400'
}`}
>
{line}
</div>
);
};
const getStatusLabel = (status) => {
switch (status) {
@@ -590,7 +570,7 @@ function GitPanel({ selectedProject, isMobile }) {
<div className="text-xs font-mono text-gray-600 dark:text-gray-400 mb-2">
{commit.stats}
</div>
{diff.split('\n').map((line, index) => renderDiffLine(line, index))}
<DiffViewer diff={diff} fileName="commit" isMobile={isMobile} wrapText={wrapText} />
</div>
</div>
)}
@@ -705,8 +685,8 @@ function GitPanel({ selectedProject, isMobile }) {
</button>
)}
</div>
<div className="max-h-96 overflow-y-auto p-2">
{diff && diff.split('\n').map((line, index) => renderDiffLine(line, index))}
<div className="max-h-96 overflow-y-auto">
{diff && <DiffViewer diff={diff} fileName={filePath} isMobile={isMobile} wrapText={wrapText} />}
</div>
</div>
</div>

View File

@@ -37,6 +37,7 @@ function MainContent({
sendMessage,
messages,
isMobile,
isPWA,
onMenuClick,
isLoading,
onInputFocusChange,
@@ -152,10 +153,12 @@ function MainContent({
<div className="h-full flex flex-col">
{/* Header with menu button for mobile */}
{isMobile && (
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 flex-shrink-0">
<div
className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 pwa-header-safe flex-shrink-0"
>
<button
onClick={onMenuClick}
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 pwa-menu-button"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
@@ -188,10 +191,12 @@ function MainContent({
<div className="h-full flex flex-col">
{/* Header with menu button for mobile */}
{isMobile && (
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 flex-shrink-0">
<div
className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 pwa-header-safe flex-shrink-0"
>
<button
onClick={onMenuClick}
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 pwa-menu-button"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
@@ -224,7 +229,9 @@ function MainContent({
return (
<div className="h-full flex flex-col">
{/* Header with tabs */}
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 flex-shrink-0">
<div
className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 pwa-header-safe flex-shrink-0"
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 sm:space-x-3">
{isMobile && (
@@ -234,7 +241,7 @@ function MainContent({
e.preventDefault();
onMenuClick();
}}
className="p-2.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 touch-manipulation active:scale-95"
className="p-2.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 touch-manipulation active:scale-95 pwa-menu-button"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />

View File

@@ -54,7 +54,9 @@ function Sidebar({
updateAvailable,
latestVersion,
currentVersion,
onShowVersionModal
onShowVersionModal,
isPWA,
isMobile
}) {
const [expandedProjects, setExpandedProjects] = useState(new Set());
const [editingProject, setEditingProject] = useState(null);
@@ -562,7 +564,10 @@ function Sidebar({
};
return (
<div className="h-full flex flex-col bg-card md:select-none">
<div
className="h-full flex flex-col bg-card md:select-none"
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
>
{/* Header */}
<div className="md:p-4 md:border-b md:border-border">
{/* Desktop Header */}
@@ -607,7 +612,10 @@ function Sidebar({
</div>
{/* Mobile Header */}
<div className="md:hidden p-3 border-b border-border">
<div
className="md:hidden p-3 border-b border-border"
style={isPWA && isMobile ? { paddingTop: '16px' } : {}}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">

View File

@@ -43,6 +43,22 @@
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
/* Safe area CSS variables */
--safe-area-inset-top: env(safe-area-inset-top);
--safe-area-inset-right: env(safe-area-inset-right);
--safe-area-inset-bottom: env(safe-area-inset-bottom);
--safe-area-inset-left: env(safe-area-inset-left);
}
/* Fallback for older iOS versions */
@supports (padding-top: constant(safe-area-inset-top)) {
:root {
--safe-area-inset-top: constant(safe-area-inset-top);
--safe-area-inset-right: constant(safe-area-inset-right);
--safe-area-inset-bottom: constant(safe-area-inset-bottom);
--safe-area-inset-left: constant(safe-area-inset-left);
}
}
.dark {
@@ -82,12 +98,60 @@
padding: 0;
}
html, body, #root {
html, body {
height: 100%;
margin: 0;
padding: 0;
}
/* Root element with safe area padding for PWA */
#root {
min-height: 100vh;
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Apply safe area padding in standalone mode */
@supports (padding-top: env(safe-area-inset-top)) {
@media (display-mode: standalone) {
#root {
padding-top: var(--safe-area-inset-top);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
}
}
}
/* PWA mode detected by JavaScript - more reliable */
html.pwa-mode,
body.pwa-mode {
height: 100%;
overflow: hidden;
background-color: rgb(255 255 255); /* white - same as bg-white for safe area */
}
body.pwa-mode #root {
padding-top: calc(env(safe-area-inset-top, 0px) + 8px);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
height: 100vh;
overflow: hidden;
}
/* Adjust fixed inset positioning in PWA mode */
body.pwa-mode .fixed.inset-0 {
top: calc(env(safe-area-inset-top, 0px) + 8px);
left: env(safe-area-inset-left);
right: env(safe-area-inset-right);
bottom: 0;
}
/* Dark mode safe area background */
html.dark body.pwa-mode {
background-color: rgb(31 41 55); /* gray-800 - matches header color */
}
/* Global transition defaults */
button,
a,
@@ -577,6 +641,25 @@
padding-bottom: max(env(safe-area-inset-bottom), 12px);
}
/* PWA specific header adjustments for iOS */
.pwa-header-safe {
padding-top: 16px;
}
/* When PWA mode is detected by JavaScript */
body.pwa-mode .pwa-header-safe {
/* Reset padding since #root already handles safe area */
padding-top: 0px !important;
}
/* For mobile PWA, ensure proper header spacing */
@media screen and (max-width: 768px) {
body.pwa-mode .pwa-header-safe {
padding-top: 4px !important;
padding-bottom: 12px;
}
}
@media screen and (max-width: 768px) {
.chat-input-mobile {
padding-bottom: calc(60px + max(env(safe-area-inset-bottom), 12px));