feat(UI): Introduce new UI components for file attachments and related views (#191)
Add file management API and UI components Implement directory listing, search and CRUD operations in desktop backend Expose new Files API on frontend to list, search, and modify files
This commit is contained in:
committed by
GitHub
parent
1f23b63c0b
commit
d97181bcd2
Generated
+1
-1
@@ -2977,7 +2977,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openchamber-desktop"
|
name = "openchamber-desktop"
|
||||||
version = "1.5.2"
|
version = "1.5.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
|||||||
@@ -53,6 +53,19 @@ pub struct CreateDirectoryResponse {
|
|||||||
path: String,
|
path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DeletePathResponse {
|
||||||
|
success: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RenamePathResponse {
|
||||||
|
success: bool,
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FileSearchHit {
|
pub struct FileSearchHit {
|
||||||
@@ -121,6 +134,34 @@ impl FsCommandError {
|
|||||||
FsCommandError::NotFound => "Parent directory not found".to_string(),
|
FsCommandError::NotFound => "Parent directory not found".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_delete_message(&self) -> String {
|
||||||
|
match self {
|
||||||
|
FsCommandError::NotFound => "File or directory not found".to_string(),
|
||||||
|
FsCommandError::AccessDenied | FsCommandError::OutsideWorkspace => {
|
||||||
|
"Access to path denied".to_string()
|
||||||
|
}
|
||||||
|
FsCommandError::NotDirectory => "Specified path is not a directory".to_string(),
|
||||||
|
FsCommandError::Other(message) => {
|
||||||
|
let _ = message;
|
||||||
|
"Failed to delete path".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_rename_message(&self) -> String {
|
||||||
|
match self {
|
||||||
|
FsCommandError::NotFound => "Source path not found".to_string(),
|
||||||
|
FsCommandError::AccessDenied | FsCommandError::OutsideWorkspace => {
|
||||||
|
"Access to path denied".to_string()
|
||||||
|
}
|
||||||
|
FsCommandError::NotDirectory => "Parent path must be a directory".to_string(),
|
||||||
|
FsCommandError::Other(message) => {
|
||||||
|
let _ = message;
|
||||||
|
"Failed to rename path".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::io::Error> for FsCommandError {
|
impl From<std::io::Error> for FsCommandError {
|
||||||
@@ -455,6 +496,71 @@ pub async fn create_directory(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_path(
|
||||||
|
path: String,
|
||||||
|
state: tauri::State<'_, DesktopRuntime>,
|
||||||
|
) -> Result<DeletePathResponse, String> {
|
||||||
|
let trimmed = path.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return Err("Path is required".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (workspace_roots, default_root) = resolve_workspace_roots(state.settings()).await;
|
||||||
|
let resolved_path = resolve_sandboxed_path(Some(trimmed.to_string()), &workspace_roots, default_root.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.to_delete_message())?;
|
||||||
|
|
||||||
|
let metadata = fs::metadata(&resolved_path)
|
||||||
|
.await
|
||||||
|
.map_err(|err| FsCommandError::from(err).to_delete_message())?;
|
||||||
|
|
||||||
|
if metadata.is_dir() {
|
||||||
|
fs::remove_dir_all(&resolved_path)
|
||||||
|
.await
|
||||||
|
.map_err(|err| FsCommandError::from(err).to_delete_message())?;
|
||||||
|
} else {
|
||||||
|
fs::remove_file(&resolved_path)
|
||||||
|
.await
|
||||||
|
.map_err(|err| FsCommandError::from(err).to_delete_message())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DeletePathResponse { success: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn rename_path(
|
||||||
|
old_path: String,
|
||||||
|
new_path: String,
|
||||||
|
state: tauri::State<'_, DesktopRuntime>,
|
||||||
|
) -> Result<RenamePathResponse, String> {
|
||||||
|
let trimmed_old = old_path.trim();
|
||||||
|
if trimmed_old.is_empty() {
|
||||||
|
return Err("oldPath is required".to_string());
|
||||||
|
}
|
||||||
|
let trimmed_new = new_path.trim();
|
||||||
|
if trimmed_new.is_empty() {
|
||||||
|
return Err("newPath is required".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (workspace_roots, default_root) = resolve_workspace_roots(state.settings()).await;
|
||||||
|
let resolved_old = resolve_sandboxed_path(Some(trimmed_old.to_string()), &workspace_roots, default_root.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.to_rename_message())?;
|
||||||
|
let resolved_new = resolve_creatable_path(trimmed_new, &workspace_roots, default_root.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.to_rename_message())?;
|
||||||
|
|
||||||
|
fs::rename(&resolved_old, &resolved_new)
|
||||||
|
.await
|
||||||
|
.map_err(|err| FsCommandError::from(err).to_rename_message())?;
|
||||||
|
|
||||||
|
Ok(RenamePathResponse {
|
||||||
|
success: true,
|
||||||
|
path: normalize_path(&resolved_new),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn resolve_sandboxed_path(
|
async fn resolve_sandboxed_path(
|
||||||
path: Option<String>,
|
path: Option<String>,
|
||||||
workspace_roots: &[PathBuf],
|
workspace_roots: &[PathBuf],
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ use axum::{
|
|||||||
routing::{any, get, post},
|
routing::{any, get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use commands::files::{create_directory, exec_commands, list_directory, read_file, read_file_binary, search_files, write_file};
|
use commands::files::{create_directory, delete_path, exec_commands, list_directory, read_file, read_file_binary, rename_path, search_files, write_file};
|
||||||
use commands::git::{
|
use commands::git::{
|
||||||
add_git_worktree, check_is_git_repository, checkout_branch, create_branch, create_git_commit, rename_branch,
|
add_git_worktree, check_is_git_repository, checkout_branch, create_branch, create_git_commit, rename_branch,
|
||||||
create_git_identity, delete_git_branch, delete_git_identity, delete_remote_branch,
|
create_git_identity, delete_git_branch, delete_git_identity, delete_remote_branch,
|
||||||
@@ -835,6 +835,8 @@ fn main() {
|
|||||||
list_directory,
|
list_directory,
|
||||||
search_files,
|
search_files,
|
||||||
create_directory,
|
create_directory,
|
||||||
|
delete_path,
|
||||||
|
rename_path,
|
||||||
read_file,
|
read_file,
|
||||||
read_file_binary,
|
read_file_binary,
|
||||||
write_file,
|
write_file,
|
||||||
|
|||||||
@@ -192,6 +192,49 @@ export const createDesktopFilesAPI = (): FilesAPI => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async delete(path: string): Promise<{ success: boolean }> {
|
||||||
|
try {
|
||||||
|
const normalizedPath = normalizePath(path);
|
||||||
|
const result = await safeInvoke<{ success: boolean }>('delete_path', {
|
||||||
|
path: normalizedPath,
|
||||||
|
}, {
|
||||||
|
timeout: 10000,
|
||||||
|
onCancel: () => {
|
||||||
|
console.warn('[FilesAPI] Delete operation timed out');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: Boolean(result?.success),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
throw new Error(message || 'Failed to delete path');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async rename(oldPath: string, newPath: string): Promise<{ success: boolean; path: string }> {
|
||||||
|
try {
|
||||||
|
const result = await safeInvoke<{ success: boolean; path: string }>('rename_path', {
|
||||||
|
oldPath: normalizePath(oldPath),
|
||||||
|
newPath: normalizePath(newPath),
|
||||||
|
}, {
|
||||||
|
timeout: 10000,
|
||||||
|
onCancel: () => {
|
||||||
|
console.warn('[FilesAPI] Rename operation timed out');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: Boolean(result?.success),
|
||||||
|
path: result?.path ? normalizePath(result.path) : normalizePath(newPath),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
throw new Error(message || 'Failed to rename path');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async execCommands(commands: string[], cwd: string): Promise<{
|
async execCommands(commands: string[], cwd: string): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
results: Array<{
|
results: Array<{
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ export const ChatContainer: React.FC = () => {
|
|||||||
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
||||||
>
|
>
|
||||||
<div className="flex-1 overflow-y-auto p-4 bg-background">
|
<div className="flex-1 overflow-y-auto p-4 bg-background">
|
||||||
<div className="chat-column space-y-4">
|
<div className="chat-message-column space-y-4">
|
||||||
{[1, 2, 3].map((i) => (
|
{[1, 2, 3].map((i) => (
|
||||||
<div key={i} className="flex gap-3 p-4">
|
<div key={i} className="flex gap-3 p-4">
|
||||||
<Skeleton className="h-8 w-8 rounded-full" />
|
<Skeleton className="h-8 w-8 rounded-full" />
|
||||||
|
|||||||
@@ -827,17 +827,17 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'group w-full',
|
'group w-full',
|
||||||
shouldShowHeader ? 'pt-2' : 'pt-0',
|
shouldShowHeader ? 'pt-6' : 'pt-0',
|
||||||
isUser ? 'pb-2' : isFollowedByAssistant ? 'pb-0' : 'pb-2'
|
isUser ? 'pb-4' : isFollowedByAssistant ? 'pb-0' : 'pb-8'
|
||||||
)}
|
)}
|
||||||
data-message-id={message.info.id}
|
data-message-id={message.info.id}
|
||||||
ref={messageContainerRef}
|
ref={messageContainerRef}
|
||||||
>
|
>
|
||||||
<div className="chat-column">
|
<div className="chat-message-column relative">
|
||||||
{isUser ? (
|
{isUser ? (
|
||||||
<FadeInOnReveal>
|
<FadeInOnReveal>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<div className="max-w-[85%] rounded-xl rounded-br-xs bg-primary/10 dark:bg-primary/8 px-3.5 pt-2.5 pb-1.5">
|
<div className="max-w-[85%] rounded-2xl rounded-br-sm bg-primary/10 dark:bg-primary/10 px-5 py-3 shadow-sm border border-primary/5">
|
||||||
<MessageBody
|
<MessageBody
|
||||||
messageId={message.info.id}
|
messageId={message.info.id}
|
||||||
parts={visibleParts}
|
parts={visibleParts}
|
||||||
@@ -870,7 +870,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</FadeInOnReveal>
|
</FadeInOnReveal>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div className="relative pl-4 ml-1">
|
||||||
{shouldShowHeader && (
|
{shouldShowHeader && (
|
||||||
<MessageHeader
|
<MessageHeader
|
||||||
isUser={isUser}
|
isUser={isUser}
|
||||||
|
|||||||
@@ -168,9 +168,11 @@ const FileChip = memo(({ file, onRemove }: FileChipProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{getFileIcon()}
|
{getFileIcon()}
|
||||||
<span title={file.serverPath || displayName}>
|
<div className="overflow-hidden max-w-[200px]">
|
||||||
{displayName}
|
<span className="marquee-text" title={file.serverPath || displayName}>
|
||||||
</span>
|
{displayName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<span className="text-muted-foreground flex-shrink-0">
|
<span className="text-muted-foreground flex-shrink-0">
|
||||||
({formatFileSize(file.size)})
|
({formatFileSize(file.size)})
|
||||||
</span>
|
</span>
|
||||||
@@ -289,9 +291,11 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup }: MessageFilesDis
|
|||||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-muted/30 border border-border/30 rounded-xl typography-meta"
|
className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-muted/30 border border-border/30 rounded-xl typography-meta"
|
||||||
>
|
>
|
||||||
{getFileIcon(file.mime)}
|
{getFileIcon(file.mime)}
|
||||||
<span>
|
<div className="overflow-hidden max-w-[200px]">
|
||||||
{extractFilename(file.filename)}
|
<span className="marquee-text">
|
||||||
</span>
|
{extractFilename(file.filename)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2049,13 +2049,15 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
|||||||
<span
|
<span
|
||||||
key={`${currentProviderId}-${currentModelId}`}
|
key={`${currentProviderId}-${currentModelId}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'model-controls__model-label',
|
'model-controls__model-label overflow-hidden',
|
||||||
controlTextSize,
|
controlTextSize,
|
||||||
'font-medium whitespace-nowrap text-foreground truncate min-w-0',
|
'font-medium whitespace-nowrap text-foreground min-w-0',
|
||||||
'max-w-[260px]'
|
'max-w-[260px]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getCurrentModelDisplayName()}
|
<span className="marquee-text">
|
||||||
|
{getCurrentModelDisplayName()}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -2169,11 +2171,13 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
|||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'model-controls__model-label typography-micro font-medium truncate min-w-0',
|
'model-controls__model-label typography-micro font-medium overflow-hidden min-w-0',
|
||||||
isMobile ? 'max-w-[120px]' : 'max-w-[220px]',
|
isMobile ? 'max-w-[120px]' : 'max-w-[220px]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getCurrentModelDisplayName()}
|
<span className="marquee-text">
|
||||||
|
{getCurrentModelDisplayName()}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ const UserMessageBody: React.FC<{
|
|||||||
style={{ contain: 'layout', transform: 'translateZ(0)' }}
|
style={{ contain: 'layout', transform: 'translateZ(0)' }}
|
||||||
onTouchStart={isTouchContext && canCopyMessage && hasCopyableText ? revealCopyHint : undefined}
|
onTouchStart={isTouchContext && canCopyMessage && hasCopyableText ? revealCopyHint : undefined}
|
||||||
>
|
>
|
||||||
<div className="leading-normal overflow-hidden text-foreground/85">
|
<div className="leading-relaxed overflow-hidden text-foreground/90 text-base">
|
||||||
{textParts.map((part, index) => {
|
{textParts.map((part, index) => {
|
||||||
let mentionForPart: AgentMentionInfo | undefined;
|
let mentionForPart: AgentMentionInfo | undefined;
|
||||||
if (agentMention && mentionToken && !mentionInjected) {
|
if (agentMention && mentionToken && !mentionInjected) {
|
||||||
@@ -180,8 +180,7 @@ const UserMessageBody: React.FC<{
|
|||||||
<MessageFilesDisplay files={parts} onShowPopup={onShowPopup} />
|
<MessageFilesDisplay files={parts} onShowPopup={onShowPopup} />
|
||||||
{(canCopyMessage && hasCopyableText) || onRevert || onFork ? (
|
{(canCopyMessage && hasCopyableText) || onRevert || onFork ? (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"mt-1 flex items-center justify-end gap-2 opacity-0 pointer-events-none transition-opacity duration-150 group-hover/message:opacity-100 group-hover/message:pointer-events-auto focus-within:opacity-100 focus-within:pointer-events-auto",
|
"mt-1 flex items-center justify-end gap-2"
|
||||||
copyHintVisible && "opacity-100 pointer-events-auto"
|
|
||||||
)}>
|
)}>
|
||||||
{onRevert && (
|
{onRevert && (
|
||||||
<Tooltip delayDuration={1000}>
|
<Tooltip delayDuration={1000}>
|
||||||
@@ -284,7 +283,6 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
void _streamPhase;
|
|
||||||
void _allowAnimation;
|
void _allowAnimation;
|
||||||
const [copyHintVisible, setCopyHintVisible] = React.useState(false);
|
const [copyHintVisible, setCopyHintVisible] = React.useState(false);
|
||||||
const copyHintTimeoutRef = React.useRef<number | null>(null);
|
const copyHintTimeoutRef = React.useRef<number | null>(null);
|
||||||
@@ -899,11 +897,9 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
|||||||
}}
|
}}
|
||||||
onTouchStart={isTouchContext && canCopyMessage && hasCopyableText ? revealCopyHint : undefined}
|
onTouchStart={isTouchContext && canCopyMessage && hasCopyableText ? revealCopyHint : undefined}
|
||||||
>
|
>
|
||||||
<div
|
<div>
|
||||||
className="px-3"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="leading-normal overflow-hidden text-foreground/90 [&_p:last-child]:mb-0 [&_ul:last-child]:mb-0 [&_ol:last-child]:mb-0"
|
className="leading-relaxed overflow-hidden text-foreground/90 [&_p:last-child]:mb-0 [&_ul:last-child]:mb-0 [&_ol:last-child]:mb-0"
|
||||||
>
|
>
|
||||||
{renderedParts}
|
{renderedParts}
|
||||||
{showErrorMessage && (
|
{showErrorMessage && (
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const MessageHeader: React.FC<MessageHeaderProps> = ({ isUser, providerID, agent
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FadeInOnReveal>
|
<FadeInOnReveal>
|
||||||
<div className={cn('pl-3', 'mb-2')}>
|
<div className={cn('mb-2')}>
|
||||||
<div className={cn('flex items-center justify-between gap-2')}>
|
<div className={cn('flex items-center justify-between gap-2')}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const FixedSessionsButton: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [isMobile, setSessionSwitcherOpen, toggleSidebar]);
|
}, [isMobile, setSessionSwitcherOpen, toggleSidebar]);
|
||||||
|
|
||||||
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground';
|
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center rounded-md gap-2 p-2 typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-secondary/50 transition-colors';
|
||||||
|
|
||||||
if (isMobile || !isDesktopApp || !isMacPlatform) {
|
if (isMobile || !isDesktopApp || !isMacPlatform) {
|
||||||
return null;
|
return null;
|
||||||
@@ -89,7 +89,6 @@ export const Header: React.FC = () => {
|
|||||||
const toggleCommandPalette = useUIStore((state) => state.toggleCommandPalette);
|
const toggleCommandPalette = useUIStore((state) => state.toggleCommandPalette);
|
||||||
const toggleHelpDialog = useUIStore((state) => state.toggleHelpDialog);
|
const toggleHelpDialog = useUIStore((state) => state.toggleHelpDialog);
|
||||||
const isSidebarOpen = useUIStore((state) => state.isSidebarOpen);
|
const isSidebarOpen = useUIStore((state) => state.isSidebarOpen);
|
||||||
const sidebarWidth = useUIStore((state) => state.sidebarWidth);
|
|
||||||
const activeMainTab = useUIStore((state) => state.activeMainTab);
|
const activeMainTab = useUIStore((state) => state.activeMainTab);
|
||||||
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
||||||
|
|
||||||
@@ -168,12 +167,11 @@ export const Header: React.FC = () => {
|
|||||||
setSettingsDialogOpen(true);
|
setSettingsDialogOpen(true);
|
||||||
}, [blurActiveElement, isMobile, setSessionSwitcherOpen, setSettingsDialogOpen]);
|
}, [blurActiveElement, isMobile, setSessionSwitcherOpen, setSettingsDialogOpen]);
|
||||||
|
|
||||||
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground';
|
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 rounded-md typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-secondary/50 transition-colors';
|
||||||
|
|
||||||
const desktopPaddingClass = React.useMemo(() => {
|
const desktopPaddingClass = React.useMemo(() => {
|
||||||
if (isDesktopApp && isMacPlatform) {
|
if (isDesktopApp && isMacPlatform) {
|
||||||
|
return isSidebarOpen ? 'pl-2' : 'pl-[8.0rem]';
|
||||||
return isSidebarOpen ? 'pl-0' : 'pl-[8.0rem]';
|
|
||||||
}
|
}
|
||||||
return 'pl-3';
|
return 'pl-3';
|
||||||
}, [isDesktopApp, isMacPlatform, isSidebarOpen]);
|
}, [isDesktopApp, isMacPlatform, isSidebarOpen]);
|
||||||
@@ -221,15 +219,12 @@ export const Header: React.FC = () => {
|
|||||||
}, [updateHeaderHeight, isMobile]);
|
}, [updateHeaderHeight, isMobile]);
|
||||||
|
|
||||||
const handleDragStart = React.useCallback(async (e: React.MouseEvent) => {
|
const handleDragStart = React.useCallback(async (e: React.MouseEvent) => {
|
||||||
|
|
||||||
if ((e.target as HTMLElement).closest('button, a, input, select, textarea')) {
|
if ((e.target as HTMLElement).closest('button, a, input, select, textarea')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.button !== 0) {
|
if (e.button !== 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDesktopApp) {
|
if (isDesktopApp) {
|
||||||
try {
|
try {
|
||||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||||
@@ -242,11 +237,9 @@ export const Header: React.FC = () => {
|
|||||||
}, [isDesktopApp]);
|
}, [isDesktopApp]);
|
||||||
|
|
||||||
const handleActiveTabDragStart = React.useCallback(async (e: React.MouseEvent) => {
|
const handleActiveTabDragStart = React.useCallback(async (e: React.MouseEvent) => {
|
||||||
|
|
||||||
if (e.button !== 0) {
|
if (e.button !== 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDesktopApp) {
|
if (isDesktopApp) {
|
||||||
try {
|
try {
|
||||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||||
@@ -278,7 +271,6 @@ export const Header: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
|
||||||
if (hasModifier(e) && !e.shiftKey && !e.altKey) {
|
if (hasModifier(e) && !e.shiftKey && !e.altKey) {
|
||||||
const num = parseInt(e.key, 10);
|
const num = parseInt(e.key, 10);
|
||||||
if (num >= 1 && num <= tabs.length) {
|
if (num >= 1 && num <= tabs.length) {
|
||||||
@@ -287,82 +279,58 @@ export const Header: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [tabs, setActiveMainTab]);
|
}, [tabs, setActiveMainTab]);
|
||||||
|
|
||||||
const renderTab = (tab: TabConfig, isLast: boolean) => {
|
const renderTab = (tab: TabConfig) => {
|
||||||
const isActive = activeMainTab === tab.id;
|
const isActive = activeMainTab === tab.id;
|
||||||
const Icon = tab.icon;
|
const Icon = tab.icon;
|
||||||
const isChatTab = tab.id === 'chat';
|
const isChatTab = tab.id === 'chat';
|
||||||
const isGitTab = tab.id === 'git';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={tab.id}>
|
<button
|
||||||
<button
|
key={tab.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActiveMainTab(tab.id)}
|
onClick={() => setActiveMainTab(tab.id)}
|
||||||
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
|
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex h-full items-center gap-2 px-4 typography-ui-label font-medium transition-colors',
|
'relative flex h-8 items-center gap-2 px-3 rounded-md typography-ui-label font-medium transition-colors',
|
||||||
isActive ? 'app-region-drag' : 'app-region-no-drag',
|
isActive ? 'app-region-drag bg-secondary text-foreground shadow-sm' : 'app-region-no-drag text-muted-foreground hover:bg-secondary/50 hover:text-foreground',
|
||||||
'hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary',
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||||
isActive ? 'text-foreground' : 'text-muted-foreground',
|
isChatTab && !isMobile && 'min-w-[100px] justify-center'
|
||||||
|
|
||||||
isActive && 'after:absolute after:bottom-[-1px] after:left-0 after:right-0 after:h-[2px] after:bg-background',
|
|
||||||
|
|
||||||
isActive && isChatTab && isSidebarOpen && 'before:absolute before:bottom-[-1px] before:right-full before:h-px before:bg-[var(--interactive-border)] before:w-[var(--sidebar-w)]',
|
|
||||||
|
|
||||||
isChatTab && !(isDesktopApp && isMacPlatform && isSidebarOpen) && 'border-l',
|
|
||||||
|
|
||||||
isGitTab && 'border-r',
|
|
||||||
|
|
||||||
isChatTab && !isMobile && 'min-w-[165px] max-[1024px]:min-w-0'
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
...(isActive && isChatTab && isSidebarOpen ? {
|
|
||||||
|
|
||||||
['--sidebar-w' as string]: (isDesktopApp && isMacPlatform) ? `${sidebarWidth}px` : '64px'
|
|
||||||
} : {}),
|
|
||||||
...((isChatTab && !(isDesktopApp && isMacPlatform && isSidebarOpen)) || isGitTab ? { borderColor: 'var(--interactive-border)' } : {}),
|
|
||||||
}}
|
|
||||||
aria-label={tab.label}
|
|
||||||
aria-selected={isActive}
|
|
||||||
role="tab"
|
|
||||||
>
|
|
||||||
{isMobile ? (
|
|
||||||
<Icon size={20} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon size={16} />
|
|
||||||
<span className="header-tab-label">{tab.label}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{}
|
|
||||||
{isChatTab && !isMobile && contextUsage && contextUsage.totalTokens > 0 && (
|
|
||||||
<span className="ml-1">
|
|
||||||
<ContextUsageDisplay
|
|
||||||
totalTokens={contextUsage.totalTokens}
|
|
||||||
percentage={contextUsage.percentage}
|
|
||||||
contextLimit={contextUsage.contextLimit}
|
|
||||||
outputLimit={contextUsage.outputLimit ?? 0}
|
|
||||||
size="compact"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{}
|
|
||||||
{tab.badge !== undefined && tab.badge > 0 && (
|
|
||||||
<span className="text-xs font-semibold text-primary">
|
|
||||||
{tab.badge}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
{}
|
|
||||||
{!isLast && (
|
|
||||||
<div className="h-full w-px bg-border" aria-hidden="true" />
|
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
aria-label={tab.label}
|
||||||
|
aria-selected={isActive}
|
||||||
|
role="tab"
|
||||||
|
>
|
||||||
|
{isMobile ? (
|
||||||
|
<Icon size={20} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon size={16} />
|
||||||
|
<span className="header-tab-label">{tab.label}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isChatTab && !isMobile && contextUsage && contextUsage.totalTokens > 0 && (
|
||||||
|
<span className="ml-1">
|
||||||
|
<ContextUsageDisplay
|
||||||
|
totalTokens={contextUsage.totalTokens}
|
||||||
|
percentage={contextUsage.percentage}
|
||||||
|
contextLimit={contextUsage.contextLimit}
|
||||||
|
outputLimit={contextUsage.outputLimit ?? 0}
|
||||||
|
size="compact"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab.badge !== undefined && tab.badge > 0 && (
|
||||||
|
<span className="ml-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary/10 px-1 text-[10px] font-bold text-primary">
|
||||||
|
{tab.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -376,29 +344,25 @@ export const Header: React.FC = () => {
|
|||||||
role="tablist"
|
role="tablist"
|
||||||
aria-label="Main navigation"
|
aria-label="Main navigation"
|
||||||
>
|
>
|
||||||
{}
|
|
||||||
{!(isDesktopApp && isMacPlatform) && (
|
{!(isDesktopApp && isMacPlatform) && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleOpenSessionSwitcher}
|
onClick={handleOpenSessionSwitcher}
|
||||||
aria-label="Open sessions"
|
aria-label="Open sessions"
|
||||||
className={`${headerIconButtonClass} mr-2.5`}
|
className={`${headerIconButtonClass} mr-2`}
|
||||||
>
|
>
|
||||||
<RiLayoutLeftLine className="h-5 w-5" />
|
<RiLayoutLeftLine className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{}
|
<div className="flex items-center gap-1 p-1 bg-background/50 rounded-lg">
|
||||||
<div className="flex h-full items-center">
|
{tabs.map((tab) => renderTab(tab))}
|
||||||
{tabs.map((tab, index) => renderTab(tab, index === tabs.length - 1))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="flex items-center gap-1 pr-3">
|
<div className="flex items-center gap-1 pr-3">
|
||||||
<Tooltip delayDuration={500}>
|
<Tooltip delayDuration={500}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -442,7 +406,7 @@ export const Header: React.FC = () => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleOpenSessionSwitcher}
|
onClick={handleOpenSessionSwitcher}
|
||||||
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md active:bg-secondary"
|
||||||
aria-label="Open sessions"
|
aria-label="Open sessions"
|
||||||
>
|
>
|
||||||
<RiPlayListAddLine className="h-5 w-5" />
|
<RiPlayListAddLine className="h-5 w-5" />
|
||||||
@@ -459,9 +423,10 @@ export const Header: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="app-region-no-drag flex items-center gap-1.5">
|
<div className="app-region-no-drag flex items-center gap-1">
|
||||||
|
|
||||||
|
<div className="flex items-center gap-0.5" role="tablist" aria-label="Main navigation">
|
||||||
|
|
||||||
<div className="flex items-center" role="tablist" aria-label="Main navigation">
|
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const isActive = activeMainTab === tab.id;
|
const isActive = activeMainTab === tab.id;
|
||||||
const Icon = tab.icon;
|
const Icon = tab.icon;
|
||||||
@@ -482,7 +447,7 @@ export const Header: React.FC = () => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
headerIconButtonClass,
|
headerIconButtonClass,
|
||||||
'relative',
|
'relative',
|
||||||
isActive && 'text-foreground'
|
isActive && 'text-foreground bg-secondary'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="h-5 w-5" />
|
<Icon className="h-5 w-5" />
|
||||||
@@ -535,7 +500,7 @@ export const Header: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const headerClassName = cn(
|
const headerClassName = cn(
|
||||||
'header-safe-area border-b relative z-10',
|
'header-safe-area border-b border-border/50 relative z-10',
|
||||||
isDesktopApp ? 'bg-background' : 'bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80'
|
isDesktopApp ? 'bg-background' : 'bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -543,7 +508,7 @@ export const Header: React.FC = () => {
|
|||||||
<header
|
<header
|
||||||
ref={headerRef}
|
ref={headerRef}
|
||||||
className={headerClassName}
|
className={headerClassName}
|
||||||
style={{ borderColor: 'var(--interactive-border)', ['--padding-scale' as string]: '1' } as React.CSSProperties}
|
style={{ ['--padding-scale' as string]: '1' } as React.CSSProperties}
|
||||||
>
|
>
|
||||||
{isMobile ? renderMobile() : renderDesktop()}
|
{isMobile ? renderMobile() : renderDesktop()}
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -168,19 +168,17 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
|||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex h-full overflow-hidden border-r',
|
'relative flex h-full overflow-hidden border-r border-border',
|
||||||
isDesktopApp
|
isDesktopApp
|
||||||
? 'bg-[color:var(--sidebar-overlay-strong)] backdrop-blur supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
|
? 'bg-sidebar/95 backdrop-blur supports-[backdrop-filter]:bg-sidebar/80'
|
||||||
: 'bg-sidebar',
|
: 'bg-sidebar',
|
||||||
isResizing ? 'transition-none' : '',
|
isResizing ? 'transition-none' : 'transition-[width] duration-300 ease-in-out',
|
||||||
!isOpen && 'border-r-0'
|
!isOpen && 'border-r-0'
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
width: `${appliedWidth}px`,
|
width: `${appliedWidth}px`,
|
||||||
minWidth: `${appliedWidth}px`,
|
minWidth: `${appliedWidth}px`,
|
||||||
maxWidth: `${appliedWidth}px`,
|
maxWidth: `${appliedWidth}px`,
|
||||||
pointerEvents: !isOpen ? 'none' : undefined,
|
|
||||||
borderColor: 'var(--interactive-border)',
|
|
||||||
overflowX: 'clip',
|
overflowX: 'clip',
|
||||||
}}
|
}}
|
||||||
aria-hidden={!isOpen || appliedWidth === 0}
|
aria-hidden={!isOpen || appliedWidth === 0}
|
||||||
@@ -188,8 +186,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
|||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute right-0 top-0 z-20 h-full w-[6px] -mr-[3px] cursor-col-resize',
|
'absolute right-0 top-0 z-20 h-full w-[4px] cursor-col-resize hover:bg-primary/50 transition-colors',
|
||||||
isResizing ? 'bg-primary/30' : 'bg-transparent hover:bg-primary/20'
|
isResizing && 'bg-primary'
|
||||||
)}
|
)}
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
role="separator"
|
role="separator"
|
||||||
@@ -199,7 +197,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
|||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-10 flex h-full flex-col transition-opacity duration-200 ease-in-out',
|
'relative z-10 flex h-full flex-col transition-opacity duration-300 ease-in-out',
|
||||||
!isOpen && 'pointer-events-none select-none opacity-0'
|
!isOpen && 'pointer-events-none select-none opacity-0'
|
||||||
)}
|
)}
|
||||||
style={{ width: `${appliedWidth}px`, overflowX: 'hidden' }}
|
style={{ width: `${appliedWidth}px`, overflowX: 'hidden' }}
|
||||||
@@ -216,44 +214,45 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
|||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<ErrorBoundary>{children}</ErrorBoundary>
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 border-t border-border p-2">
|
<div className="flex-shrink-0 border-t border-border h-12 px-2 bg-sidebar-accent/10">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex h-full items-center justify-between gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSettingsDialogOpen(true)}
|
onClick={() => setSettingsDialogOpen(true)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 rounded-md px-3 py-2',
|
'flex h-8 items-center gap-2 rounded-md px-2',
|
||||||
'text-sm font-semibold text-muted-foreground',
|
'text-sm font-semibold text-sidebar-foreground/90',
|
||||||
'hover:text-foreground',
|
'hover:text-sidebar-foreground hover:bg-sidebar-accent',
|
||||||
'transition-colors'
|
'transition-all duration-200'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<RiSettings3Line className="h-4 w-4" />
|
<RiSettings3Line className="h-4 w-4" />
|
||||||
<span>Settings</span>
|
<span>Settings</span>
|
||||||
</button>
|
</button>
|
||||||
{(available || downloaded) ? (
|
{(available || downloaded) ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => setUpdateDialogOpen(true)}
|
onClick={() => setUpdateDialogOpen(true)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-1.5 rounded-md px-2.5 py-1.5',
|
'flex items-center gap-1.5 rounded-md px-2 py-1',
|
||||||
'text-xs font-medium',
|
'text-xs font-semibold',
|
||||||
'bg-primary/10 text-primary',
|
'bg-primary/10 text-primary',
|
||||||
'hover:bg-primary/20',
|
'hover:bg-primary/20',
|
||||||
'transition-colors'
|
'transition-colors'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<RiDownloadLine className="h-3.5 w-3.5" />
|
<RiDownloadLine className="h-3.5 w-3.5" />
|
||||||
<span>Update</span>
|
<span>Update</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
) : !isDesktopApp && (
|
) : !isDesktopApp && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
onClick={() => setAboutDialogOpen(true)}
|
onClick={() => setAboutDialogOpen(true)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center rounded-md p-1.5',
|
'flex h-8 w-8 items-center justify-center rounded-md',
|
||||||
'text-muted-foreground',
|
'text-sidebar-foreground/70',
|
||||||
'hover:text-foreground hover:bg-muted/50',
|
'hover:text-sidebar-foreground hover:bg-sidebar-accent',
|
||||||
'transition-colors'
|
'transition-all duration-200'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<RiInformationLine className="h-4 w-4" />
|
<RiInformationLine className="h-4 w-4" />
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
|
|||||||
onTouchStart={isMobile ? handleLongPressStart : undefined}
|
onTouchStart={isMobile ? handleLongPressStart : undefined}
|
||||||
onTouchEnd={isMobile ? handleLongPressEnd : undefined}
|
onTouchEnd={isMobile ? handleLongPressEnd : undefined}
|
||||||
>
|
>
|
||||||
<RiDonutChartLine className="h-4 w-4 flex-shrink-0" />
|
{!isMobile && <RiDonutChartLine className="h-4 w-4 flex-shrink-0" />}
|
||||||
<span className={cn(getPercentageColor(percentage), 'font-medium')}>
|
<span className={cn(getPercentageColor(percentage), 'font-medium')}>
|
||||||
{Math.min(percentage, 999).toFixed(1)}%
|
{Math.min(percentage, 999).toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -16,9 +16,22 @@ import {
|
|||||||
RiSave3Line,
|
RiSave3Line,
|
||||||
RiSendPlane2Line,
|
RiSendPlane2Line,
|
||||||
RiTextWrap,
|
RiTextWrap,
|
||||||
|
RiMore2Fill,
|
||||||
|
RiFileAddLine,
|
||||||
|
RiFolderAddLine,
|
||||||
|
RiDeleteBinLine,
|
||||||
|
RiEditLine,
|
||||||
|
RiFileCopyLine,
|
||||||
} from '@remixicon/react';
|
} from '@remixicon/react';
|
||||||
import { toast } from '@/components/ui';
|
import { toast } from '@/components/ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -273,6 +286,24 @@ export const FilesView: React.FC = () => {
|
|||||||
const pendingTabRef = React.useRef<import('@/stores/useUIStore').MainTab | null>(null);
|
const pendingTabRef = React.useRef<import('@/stores/useUIStore').MainTab | null>(null);
|
||||||
const skipDirtyOnceRef = React.useRef(false);
|
const skipDirtyOnceRef = React.useRef(false);
|
||||||
|
|
||||||
|
const [activeDialog, setActiveDialog] = React.useState<'createFile' | 'createFolder' | 'rename' | 'delete' | null>(null);
|
||||||
|
const [dialogData, setDialogData] = React.useState<{ path: string; name?: string; type?: 'file' | 'directory' } | null>(null);
|
||||||
|
const [dialogInputValue, setDialogInputValue] = React.useState('');
|
||||||
|
const [isDialogSubmitting, setIsDialogSubmitting] = React.useState(false);
|
||||||
|
const [contextMenuPath, setContextMenuPath] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
const canCreateFile = Boolean(files.writeFile);
|
||||||
|
const canCreateFolder = Boolean(files.createDirectory);
|
||||||
|
const canRename = Boolean(files.rename);
|
||||||
|
const canDelete = Boolean(files.delete);
|
||||||
|
|
||||||
|
const handleOpenDialog = React.useCallback((type: 'createFile' | 'createFolder' | 'rename' | 'delete', data: { path: string; name?: string; type?: 'file' | 'directory' }) => {
|
||||||
|
setActiveDialog(type);
|
||||||
|
setDialogData(data);
|
||||||
|
setDialogInputValue(type === 'rename' ? data.name || '' : '');
|
||||||
|
setIsDialogSubmitting(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Line selection state for commenting
|
// Line selection state for commenting
|
||||||
const [lineSelection, setLineSelection] = React.useState<SelectedLineRange | null>(null);
|
const [lineSelection, setLineSelection] = React.useState<SelectedLineRange | null>(null);
|
||||||
const [commentText, setCommentText] = React.useState('');
|
const [commentText, setCommentText] = React.useState('');
|
||||||
@@ -509,6 +540,78 @@ export const FilesView: React.FC = () => {
|
|||||||
void refreshRoot();
|
void refreshRoot();
|
||||||
}, [currentDirectory, refreshRoot, showGitignored]);
|
}, [currentDirectory, refreshRoot, showGitignored]);
|
||||||
|
|
||||||
|
const handleDialogSubmit = React.useCallback(async (e?: React.FormEvent) => {
|
||||||
|
e?.preventDefault();
|
||||||
|
if (!dialogData || !activeDialog) return;
|
||||||
|
|
||||||
|
setIsDialogSubmitting(true);
|
||||||
|
try {
|
||||||
|
if (activeDialog === 'createFile') {
|
||||||
|
if (!dialogInputValue.trim()) throw new Error('Filename is required');
|
||||||
|
const parentPath = dialogData.path;
|
||||||
|
// Handle root path or empty path
|
||||||
|
const prefix = parentPath ? `${parentPath}/` : '';
|
||||||
|
const newPath = normalizePath(`${prefix}${dialogInputValue.trim()}`);
|
||||||
|
|
||||||
|
if (!files.writeFile) throw new Error('Write not supported');
|
||||||
|
const result = await files.writeFile(newPath, '');
|
||||||
|
if (result.success) {
|
||||||
|
toast.success('File created');
|
||||||
|
await refreshRoot();
|
||||||
|
}
|
||||||
|
} else if (activeDialog === 'createFolder') {
|
||||||
|
if (!dialogInputValue.trim()) throw new Error('Folder name is required');
|
||||||
|
const parentPath = dialogData.path;
|
||||||
|
const prefix = parentPath ? `${parentPath}/` : '';
|
||||||
|
const newPath = normalizePath(`${prefix}${dialogInputValue.trim()}`);
|
||||||
|
|
||||||
|
const result = await files.createDirectory(newPath);
|
||||||
|
if (result.success) {
|
||||||
|
toast.success('Folder created');
|
||||||
|
await refreshRoot();
|
||||||
|
}
|
||||||
|
} else if (activeDialog === 'rename') {
|
||||||
|
if (!dialogInputValue.trim()) throw new Error('Name is required');
|
||||||
|
const oldPath = dialogData.path;
|
||||||
|
const parentDir = oldPath.split('/').slice(0, -1).join('/');
|
||||||
|
const prefix = parentDir ? `${parentDir}/` : '';
|
||||||
|
const newPath = normalizePath(`${prefix}${dialogInputValue.trim()}`);
|
||||||
|
|
||||||
|
if (files.rename) {
|
||||||
|
const result = await files.rename(oldPath, newPath);
|
||||||
|
if (result.success) {
|
||||||
|
toast.success('Renamed successfully');
|
||||||
|
await refreshRoot();
|
||||||
|
if (selectedFile?.path === oldPath) {
|
||||||
|
setSelectedFile(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error("Rename not supported");
|
||||||
|
}
|
||||||
|
} else if (activeDialog === 'delete') {
|
||||||
|
if (files.delete) {
|
||||||
|
const result = await files.delete(dialogData.path);
|
||||||
|
if (result.success) {
|
||||||
|
toast.success('Deleted successfully');
|
||||||
|
await refreshRoot();
|
||||||
|
if (selectedFile?.path === dialogData.path || selectedFile?.path.startsWith(dialogData.path + '/')) {
|
||||||
|
setSelectedFile(null);
|
||||||
|
setFileContent('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error("Delete not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setActiveDialog(null);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Operation failed');
|
||||||
|
} finally {
|
||||||
|
setIsDialogSubmitting(false);
|
||||||
|
}
|
||||||
|
}, [activeDialog, dialogData, dialogInputValue, files, refreshRoot, selectedFile]);
|
||||||
|
|
||||||
const fuzzyScore = React.useCallback((query: string, candidate: string): number | null => {
|
const fuzzyScore = React.useCallback((query: string, candidate: string): number | null => {
|
||||||
const q = query.trim().toLowerCase();
|
const q = query.trim().toLowerCase();
|
||||||
if (!q) {
|
if (!q) {
|
||||||
@@ -853,56 +956,129 @@ export const FilesView: React.FC = () => {
|
|||||||
const renderTree = React.useCallback((dirPath: string, depth: number): React.ReactNode => {
|
const renderTree = React.useCallback((dirPath: string, depth: number): React.ReactNode => {
|
||||||
const nodes = childrenByDir[dirPath] ?? [];
|
const nodes = childrenByDir[dirPath] ?? [];
|
||||||
|
|
||||||
return nodes.map((node) => {
|
return nodes.map((node, index) => {
|
||||||
const isDir = node.type === 'directory';
|
const isDir = node.type === 'directory';
|
||||||
const isExpanded = isDir && expandedDirs.has(node.path);
|
const isExpanded = isDir && expandedDirs.has(node.path);
|
||||||
const isActive = selectedFile?.path === node.path;
|
const isActive = selectedFile?.path === node.path;
|
||||||
const isLoading = isDir && inFlightDirsRef.current.has(node.path);
|
const isLoading = isDir && inFlightDirsRef.current.has(node.path);
|
||||||
|
const isLast = index === nodes.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={node.path}>
|
<li key={node.path} className="relative">
|
||||||
<button
|
{depth > 0 && (
|
||||||
type="button"
|
<>
|
||||||
onClick={() => {
|
<span className="absolute top-3.5 left-[-12px] w-3 h-px bg-border/40" />
|
||||||
if (isDir) {
|
{isLast && (
|
||||||
void toggleDirectory(node.path);
|
<span className="absolute top-3.5 bottom-0 left-[-13px] w-[2px] bg-background" />
|
||||||
} else {
|
)}
|
||||||
void handleSelectFile(node);
|
</>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="group relative flex items-center"
|
||||||
|
onContextMenu={(event) => {
|
||||||
|
if (!canRename && !canCreateFile && !canCreateFolder && !canDelete) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
setContextMenuPath(node.path);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
|
||||||
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors',
|
|
||||||
isActive ? 'bg-accent/70' : 'hover:bg-accent/40'
|
|
||||||
)}
|
|
||||||
style={{ paddingLeft: `${8 + depth * 12}px` }}
|
|
||||||
>
|
>
|
||||||
{isDir ? (
|
<button
|
||||||
isLoading ? (
|
type="button"
|
||||||
<RiLoader4Line className="h-4 w-4 flex-shrink-0 animate-spin" />
|
onClick={() => {
|
||||||
) : isExpanded ? (
|
if (isDir) {
|
||||||
<RiFolderOpenFill className="h-4 w-4 flex-shrink-0 text-primary/60" />
|
void toggleDirectory(node.path);
|
||||||
) : (
|
} else {
|
||||||
<RiFolder3Fill className="h-4 w-4 flex-shrink-0 text-primary/60" />
|
void handleSelectFile(node);
|
||||||
)
|
}
|
||||||
) : (
|
}}
|
||||||
getFileIcon(node.extension)
|
className={cn(
|
||||||
)}
|
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors pr-8 select-none',
|
||||||
<span
|
isActive ? 'bg-accent/70' : 'hover:bg-accent/40'
|
||||||
className="min-w-0 flex-1 truncate typography-meta"
|
)}
|
||||||
title={node.path}
|
|
||||||
>
|
>
|
||||||
{node.name}
|
{isDir ? (
|
||||||
</span>
|
isLoading ? (
|
||||||
</button>
|
<RiLoader4Line className="h-4 w-4 flex-shrink-0 animate-spin" />
|
||||||
|
) : isExpanded ? (
|
||||||
|
<RiFolderOpenFill className="h-4 w-4 flex-shrink-0 text-primary/60" />
|
||||||
|
) : (
|
||||||
|
<RiFolder3Fill className="h-4 w-4 flex-shrink-0 text-primary/60" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
getFileIcon(node.extension)
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className="min-w-0 flex-1 truncate typography-meta"
|
||||||
|
title={node.path}
|
||||||
|
>
|
||||||
|
{node.name}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{(canRename || canCreateFile || canCreateFolder || canDelete) && (
|
||||||
|
<div className="absolute right-1 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 focus-within:opacity-100">
|
||||||
|
<DropdownMenu
|
||||||
|
open={contextMenuPath === node.path}
|
||||||
|
onOpenChange={(open) => setContextMenuPath(open ? node.path : null)}
|
||||||
|
>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-6 w-6">
|
||||||
|
<RiMore2Fill className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" onCloseAutoFocus={() => setContextMenuPath(null)}>
|
||||||
|
{canRename && (
|
||||||
|
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleOpenDialog('rename', node); }}>
|
||||||
|
<RiEditLine className="mr-2 h-4 w-4" /> Rename
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItem onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
void navigator.clipboard.writeText(node.path);
|
||||||
|
toast.success('Path copied');
|
||||||
|
}}>
|
||||||
|
<RiFileCopyLine className="mr-2 h-4 w-4" /> Copy Path
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{isDir && (canCreateFile || canCreateFolder) && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{canCreateFile && (
|
||||||
|
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleOpenDialog('createFile', node); }}>
|
||||||
|
<RiFileAddLine className="mr-2 h-4 w-4" /> New File
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{canCreateFolder && (
|
||||||
|
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleOpenDialog('createFolder', node); }}>
|
||||||
|
<RiFolderAddLine className="mr-2 h-4 w-4" /> New Folder
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{canDelete && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleOpenDialog('delete', node); }}
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<RiDeleteBinLine className="mr-2 h-4 w-4" /> Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{isDir && isExpanded && (
|
{isDir && isExpanded && (
|
||||||
<ul className="flex flex-col gap-1">
|
<ul className="flex flex-col gap-1 ml-3 pl-3 border-l border-border/40 relative">
|
||||||
{renderTree(node.path, depth + 1)}
|
{renderTree(node.path, depth + 1)}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, [childrenByDir, expandedDirs, handleSelectFile, selectedFile?.path, toggleDirectory]);
|
}, [childrenByDir, expandedDirs, handleSelectFile, selectedFile?.path, toggleDirectory, handleOpenDialog, canCreateFile, canCreateFolder, canRename, canDelete, contextMenuPath, setContextMenuPath]);
|
||||||
|
|
||||||
const isSelectedImage = Boolean(selectedFile?.path && isImageFile(selectedFile.path));
|
const isSelectedImage = Boolean(selectedFile?.path && isImageFile(selectedFile.path));
|
||||||
const isSelectedSvg = Boolean(selectedFile?.path && selectedFile.path.toLowerCase().endsWith('.svg'));
|
const isSelectedSvg = Boolean(selectedFile?.path && selectedFile.path.toLowerCase().endsWith('.svg'));
|
||||||
@@ -1000,6 +1176,58 @@ export const FilesView: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, [files, isSelectedImage, isSelectedSvg, runtime.isDesktop, selectedFile?.path]);
|
}, [files, isSelectedImage, isSelectedSvg, runtime.isDesktop, selectedFile?.path]);
|
||||||
|
|
||||||
|
const renderDialogs = () => (
|
||||||
|
<Dialog open={!!activeDialog} onOpenChange={(open) => !open && setActiveDialog(null)}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{activeDialog === 'createFile' && 'Create File'}
|
||||||
|
{activeDialog === 'createFolder' && 'Create Folder'}
|
||||||
|
{activeDialog === 'rename' && 'Rename'}
|
||||||
|
{activeDialog === 'delete' && 'Delete'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{activeDialog === 'createFile' && `Create a new file in ${dialogData?.path ?? 'root'}`}
|
||||||
|
{activeDialog === 'createFolder' && `Create a new folder in ${dialogData?.path ?? 'root'}`}
|
||||||
|
{activeDialog === 'rename' && `Rename ${dialogData?.name}`}
|
||||||
|
{activeDialog === 'delete' && `Are you sure you want to delete ${dialogData?.name}? This action cannot be undone.`}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{activeDialog !== 'delete' && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Input
|
||||||
|
value={dialogInputValue}
|
||||||
|
onChange={(e) => setDialogInputValue(e.target.value)}
|
||||||
|
placeholder={activeDialog === 'rename' ? 'New name' : 'Name'}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
void handleDialogSubmit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setActiveDialog(null)} disabled={isDialogSubmitting}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={activeDialog === 'delete' ? 'destructive' : 'default'}
|
||||||
|
onClick={() => void handleDialogSubmit()}
|
||||||
|
disabled={isDialogSubmitting || (activeDialog !== 'delete' && !dialogInputValue.trim())}
|
||||||
|
>
|
||||||
|
{isDialogSubmitting ? <RiLoader4Line className="animate-spin" /> : (
|
||||||
|
activeDialog === 'delete' ? 'Delete' : 'Confirm'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
|
||||||
// Comment UI component
|
// Comment UI component
|
||||||
const renderCommentUI = () => {
|
const renderCommentUI = () => {
|
||||||
if (!lineSelection || !selectedFile) return null;
|
if (!lineSelection || !selectedFile) return null;
|
||||||
@@ -1369,6 +1597,24 @@ export const FilesView: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleOpenDialog('createFile', { path: currentDirectory, type: 'directory' })}
|
||||||
|
className="h-8 w-8 p-0 flex-shrink-0"
|
||||||
|
title="New File"
|
||||||
|
>
|
||||||
|
<RiFileAddLine className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleOpenDialog('createFolder', { path: currentDirectory, type: 'directory' })}
|
||||||
|
className="h-8 w-8 p-0 flex-shrink-0"
|
||||||
|
title="New Folder"
|
||||||
|
>
|
||||||
|
<RiFolderAddLine className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
<Button variant="ghost" size="sm" onClick={() => void refreshRoot()} className="h-8 w-8 p-0 flex-shrink-0">
|
<Button variant="ghost" size="sm" onClick={() => void refreshRoot()} className="h-8 w-8 p-0 flex-shrink-0">
|
||||||
<RiRefreshLine className="h-4 w-4" />
|
<RiRefreshLine className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1419,6 +1665,7 @@ export const FilesView: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 overflow-hidden bg-background">
|
<div className="flex h-full min-h-0 overflow-hidden bg-background">
|
||||||
|
{renderDialogs()}
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
showMobilePageContent ? (
|
showMobilePageContent ? (
|
||||||
fileViewer
|
fileViewer
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react'
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { FileDiff } from '@pierre/diffs/react';
|
import { FileDiff } from '@pierre/diffs/react';
|
||||||
import { parseDiffFromFile, type FileContents, type FileDiffMetadata, type SelectedLineRange } from '@pierre/diffs';
|
import { parseDiffFromFile, type FileContents, type FileDiffMetadata, type SelectedLineRange } from '@pierre/diffs';
|
||||||
import { RiArrowDownSLine, RiEyeLine, RiSendPlane2Line } from '@remixicon/react';
|
import { RiSendPlane2Line } from '@remixicon/react';
|
||||||
|
|
||||||
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
@@ -141,7 +141,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
|
|
||||||
const [selection, setSelection] = useState<SelectedLineRange | null>(null);
|
const [selection, setSelection] = useState<SelectedLineRange | null>(null);
|
||||||
const [commentText, setCommentText] = useState('');
|
const [commentText, setCommentText] = useState('');
|
||||||
|
const commentContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Calculate initial center synchronously to avoid flicker
|
// Calculate initial center synchronously to avoid flicker
|
||||||
const getMainContentCenter = useCallback(() => {
|
const getMainContentCenter = useCallback(() => {
|
||||||
@@ -212,8 +212,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
|
|
||||||
// Check if click is inside the comment UI portal
|
// Check if click is inside the comment UI portal
|
||||||
const commentUI = document.querySelector('[data-comment-ui]');
|
if (commentContainerRef.current?.contains(target)) return;
|
||||||
if (commentUI?.contains(target)) return;
|
|
||||||
|
|
||||||
// Check if click is inside toast (sonner)
|
// Check if click is inside toast (sonner)
|
||||||
if (target.closest('[data-sonner-toast]') || target.closest('[data-sonner-toaster]')) return;
|
if (target.closest('[data-sonner-toast]') || target.closest('[data-sonner-toaster]')) return;
|
||||||
@@ -302,21 +301,8 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
fileDiff: FileDiffMetadata;
|
fileDiff: FileDiffMetadata;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
// Threshold for lazy loading (total lines > 1500 or content size > 150KB)
|
// Pre-parse the diff with cacheKey for worker pool caching
|
||||||
const isLargeDiff = useMemo(() => {
|
|
||||||
const totalLines = (original || '').split('\n').length + (modified || '').split('\n').length;
|
|
||||||
const totalSize = (original?.length || 0) + (modified?.length || 0);
|
|
||||||
return totalLines > 1500 || totalSize > 150 * 1024;
|
|
||||||
}, [original, modified]);
|
|
||||||
|
|
||||||
// State for large diff loading
|
|
||||||
const [shouldLoad, setShouldLoad] = useState(false);
|
|
||||||
|
|
||||||
// Parse diff when loaded (for large diffs) or always (for small diffs)
|
|
||||||
const fileDiff = useMemo(() => {
|
const fileDiff = useMemo(() => {
|
||||||
// For large diffs, only parse if manually triggered
|
|
||||||
if (isLargeDiff && !shouldLoad) return null;
|
|
||||||
|
|
||||||
const cacheKey = getCacheKey(fileName, original, modified);
|
const cacheKey = getCacheKey(fileName, original, modified);
|
||||||
|
|
||||||
// Return cached diff if inputs haven't changed
|
// Return cached diff if inputs haven't changed
|
||||||
@@ -344,7 +330,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
diffCacheRef.current = { key: cacheKey, fileDiff: diff };
|
diffCacheRef.current = { key: cacheKey, fileDiff: diff };
|
||||||
|
|
||||||
return diff;
|
return diff;
|
||||||
}, [fileName, original, modified, language, isLargeDiff, shouldLoad]);
|
}, [fileName, original, modified, language]);
|
||||||
|
|
||||||
const options = useMemo(() => ({
|
const options = useMemo(() => ({
|
||||||
theme: {
|
theme: {
|
||||||
@@ -363,45 +349,11 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
onLineSelected: handleSelectionChange,
|
onLineSelected: handleSelectionChange,
|
||||||
unsafeCSS: WEBKIT_SCROLL_FIX_CSS,
|
unsafeCSS: WEBKIT_SCROLL_FIX_CSS,
|
||||||
}), [isDark, renderSideBySide, wrapLines, handleSelectionChange]);
|
}), [isDark, renderSideBySide, wrapLines, handleSelectionChange]);
|
||||||
|
|
||||||
// Show placeholder for large diffs
|
|
||||||
if (isLargeDiff && !fileDiff) {
|
|
||||||
const originalLines = (original || '').split('\n').length;
|
|
||||||
const modifiedLines = (modified || '').split('\n').length;
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center h-full min-h-[200px] p-6">
|
|
||||||
<div className="flex flex-col items-center gap-3 text-center">
|
|
||||||
<div className="p-3 rounded-full bg-muted/50">
|
|
||||||
<RiEyeLine className="size-6 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="typography-ui-header font-medium text-foreground">Large Diff</div>
|
|
||||||
<div className="typography-meta text-muted-foreground mt-1">
|
|
||||||
{originalLines + modifiedLines} lines
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShouldLoad(true)}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 mt-2 rounded-lg bg-primary/10 text-primary hover:bg-primary/20 transition-colors typography-meta font-medium"
|
|
||||||
>
|
|
||||||
<RiArrowDownSLine className="size-4" />
|
|
||||||
Load Diff
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fileDiff should not be null here (handled by large diff placeholder above)
|
|
||||||
if (!fileDiff) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extracted Comment Interface Content for reuse in Portal or In-Flow
|
// Extracted Comment Interface Content for reuse in Portal or In-Flow
|
||||||
const renderCommentContent = () => {
|
const renderCommentContent = () => {
|
||||||
if (!selection) return null;
|
if (!selection) return null;
|
||||||
@@ -521,6 +473,8 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
: '16px'
|
: '16px'
|
||||||
}}
|
}}
|
||||||
data-keyboard-avoid="true"
|
data-keyboard-avoid="true"
|
||||||
|
data-comment-ui="true"
|
||||||
|
ref={commentContainerRef}
|
||||||
>
|
>
|
||||||
{commentContent}
|
{commentContent}
|
||||||
</div>
|
</div>
|
||||||
@@ -566,6 +520,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
}}
|
}}
|
||||||
data-keyboard-avoid="true"
|
data-keyboard-avoid="true"
|
||||||
data-comment-ui="true"
|
data-comment-ui="true"
|
||||||
|
ref={commentContainerRef}
|
||||||
>
|
>
|
||||||
{commentContent}
|
{commentContent}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -325,7 +325,6 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
return '';
|
return '';
|
||||||
}, [isDesktopApp, isMacPlatform]);
|
}, [isDesktopApp, isMacPlatform]);
|
||||||
|
|
||||||
const showLeadingDivider = isDesktopApp && isMacPlatform;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className={cn('flex h-full flex-col overflow-hidden', isDesktopApp ? 'bg-transparent' : 'bg-background')}>
|
<div ref={containerRef} className={cn('flex h-full flex-col overflow-hidden', isDesktopApp ? 'bg-transparent' : 'bg-background')}>
|
||||||
@@ -356,48 +355,46 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
{isMobile && <div className="flex-1" />}
|
{isMobile && <div className="flex-1" />}
|
||||||
|
|
||||||
<div className={cn('flex items-center', isMobile ? 'gap-1' : 'h-full')}>
|
<div className={cn('flex items-center', isMobile ? 'gap-1' : 'h-full')}>
|
||||||
{/* Leading divider before first tab - only on Mac desktop */}
|
<div className={cn('flex items-center gap-1', !isMobile && 'p-1 bg-background/50 rounded-lg')}>
|
||||||
{!isMobile && showLeadingDivider && <div className="h-full w-px bg-border" aria-hidden="true" />}
|
{settingsSections.map(({ id, label, icon: Icon }) => {
|
||||||
{settingsSections.map(({ id, label, icon: Icon }) => {
|
const isActive = activeTab === id;
|
||||||
const isActive = activeTab === id;
|
const PhosphorIcon = Icon as React.ComponentType<{ className?: string; weight?: string }>;
|
||||||
const PhosphorIcon = Icon as React.ComponentType<{ className?: string; weight?: string }>;
|
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
// Mobile: icon-only buttons
|
// Mobile: icon-only buttons
|
||||||
|
return (
|
||||||
|
<Tooltip key={id} delayDuration={500}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={() => handleTabChange(id)}
|
||||||
|
className={cn(
|
||||||
|
'relative flex h-9 w-9 items-center justify-center rounded-md transition-colors',
|
||||||
|
'hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||||
|
isActive ? 'bg-secondary text-foreground shadow-sm' : 'text-muted-foreground'
|
||||||
|
)}
|
||||||
|
aria-pressed={isActive}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
<PhosphorIcon className="h-5 w-5" weight="regular" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{label}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop: pill tabs like main header
|
||||||
return (
|
return (
|
||||||
<Tooltip key={id} delayDuration={500}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
onClick={() => handleTabChange(id)}
|
|
||||||
className={cn(
|
|
||||||
'relative flex h-9 w-9 items-center justify-center rounded-md transition-colors',
|
|
||||||
'hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
|
||||||
isActive ? 'text-foreground' : 'text-muted-foreground'
|
|
||||||
)}
|
|
||||||
aria-pressed={isActive}
|
|
||||||
aria-label={label}
|
|
||||||
>
|
|
||||||
<PhosphorIcon className="h-5 w-5" weight="regular" />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{label}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Desktop: full tabs with text and dividers
|
|
||||||
return (
|
|
||||||
<React.Fragment key={id}>
|
|
||||||
<button
|
<button
|
||||||
|
key={id}
|
||||||
onClick={() => handleTabChange(id)}
|
onClick={() => handleTabChange(id)}
|
||||||
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
|
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex h-full items-center gap-2 px-4 typography-ui-label font-medium transition-colors',
|
'relative flex h-8 items-center gap-2 px-3 rounded-md typography-ui-label font-medium transition-colors',
|
||||||
isActive ? 'app-region-drag' : 'app-region-no-drag',
|
isActive ? 'app-region-drag bg-secondary text-foreground shadow-sm' : 'app-region-no-drag text-muted-foreground hover:bg-secondary/50 hover:text-foreground',
|
||||||
'hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary',
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary'
|
||||||
isActive ? 'text-foreground' : 'text-muted-foreground'
|
|
||||||
)}
|
)}
|
||||||
aria-pressed={isActive}
|
aria-pressed={isActive}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
@@ -405,11 +402,9 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
<PhosphorIcon className="h-4 w-4" weight="regular" />
|
<PhosphorIcon className="h-4 w-4" weight="regular" />
|
||||||
{showTabLabels && <span>{label}</span>}
|
{showTabLabels && <span>{label}</span>}
|
||||||
</button>
|
</button>
|
||||||
{/* Vertical divider after each tab */}
|
);
|
||||||
<div className="h-full w-px bg-border" aria-hidden="true" />
|
})}
|
||||||
</React.Fragment>
|
</div>
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(onClose || showProjectSwitcher) && (
|
{(onClose || showProjectSwitcher) && (
|
||||||
|
|||||||
+20
-943
@@ -1,33 +1,10 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
@import "./styles/design-system.css";
|
||||||
|
@import "./styles/typography.css";
|
||||||
|
@import "./styles/mobile.css";
|
||||||
@source "./**/*.{ts,tsx,css}";
|
@source "./**/*.{ts,tsx,css}";
|
||||||
|
|
||||||
:root {
|
|
||||||
color-scheme: light;
|
|
||||||
--oc-safe-area-top: 0px;
|
|
||||||
--oc-safe-area-right: 0px;
|
|
||||||
--oc-safe-area-bottom: 0px;
|
|
||||||
--oc-safe-area-bottom-visual: 0px;
|
|
||||||
--oc-safe-area-left: 0px;
|
|
||||||
--oc-header-height: 56px;
|
|
||||||
--oc-visual-viewport-offset-top: 0px;
|
|
||||||
--oc-keyboard-inset: 0px;
|
|
||||||
--oc-keyboard-avoid-offset: 0px;
|
|
||||||
--oc-keyboard-home-indicator: 0px;
|
|
||||||
--ui-regular-font-weight: 400;
|
|
||||||
--oc-scrollbar-thumb: oklch(0.32 0.03 50 / 0.4);
|
|
||||||
--oc-scrollbar-thumb-hover: oklch(0.32 0.03 50 / 0.6);
|
|
||||||
--padding-scale: 1;
|
|
||||||
|
|
||||||
/* Semantic typography defaults (must match SEMANTIC_TYPOGRAPHY) */
|
|
||||||
--text-markdown: 0.9375rem;
|
|
||||||
--text-code: 0.9063rem;
|
|
||||||
--text-ui-header: 0.9375rem;
|
|
||||||
--text-ui-label: 0.8750rem;
|
|
||||||
--text-meta: 0.875rem;
|
|
||||||
--text-micro: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
/* Suppress WebKit-specific hover/focus adornments on the chat textarea */
|
/* Suppress WebKit-specific hover/focus adornments on the chat textarea */
|
||||||
@@ -240,399 +217,8 @@ svg.animate-spin {
|
|||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Semantic Typography System Classes */
|
.animate-spin-once {
|
||||||
@layer components {
|
animation: spin-once 0.5s linear forwards;
|
||||||
/* Semantic typography classes - font-size only */
|
|
||||||
.typography-markdown {
|
|
||||||
font-size: var(--text-markdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-code {
|
|
||||||
font-size: var(--text-code);
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-ui-header {
|
|
||||||
font-size: var(--text-ui-header);
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-ui-label {
|
|
||||||
font-size: var(--text-ui-label);
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-meta {
|
|
||||||
font-size: var(--text-meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-micro {
|
|
||||||
font-size: var(--text-micro);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-column {
|
|
||||||
width: min(100%, 56rem);
|
|
||||||
margin-inline: auto;
|
|
||||||
padding-inline: calc(clamp(1rem, 3vw, 1.5rem) * var(--padding-scale, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.chat-column {
|
|
||||||
padding-inline: calc(clamp(1.25rem, 2.5vw, 2rem) * var(--padding-scale, 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced focus indicators for small interactive elements */
|
|
||||||
.model-favorite-button {
|
|
||||||
position: relative;
|
|
||||||
transition: transform 0.1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-favorite-button:focus-visible {
|
|
||||||
outline: 2px solid var(--primary);
|
|
||||||
outline-offset: 2px;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-favorite-button:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* FadeInOnReveal handles all animations - no custom CSS needed */
|
|
||||||
|
|
||||||
/* Heading typography - all use markdown size, differentiated by weight/color */
|
|
||||||
|
|
||||||
.typography-h1 {
|
|
||||||
font-size: var(--text-markdown);
|
|
||||||
line-height: var(--h1-line-height);
|
|
||||||
letter-spacing: var(--h1-letter-spacing);
|
|
||||||
font-weight: var(--h1-font-weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-h2 {
|
|
||||||
font-size: var(--text-markdown);
|
|
||||||
line-height: var(--h2-line-height);
|
|
||||||
letter-spacing: var(--h2-letter-spacing);
|
|
||||||
font-weight: var(--h2-font-weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-h3 {
|
|
||||||
font-size: var(--text-markdown);
|
|
||||||
line-height: var(--h3-line-height);
|
|
||||||
letter-spacing: var(--h3-letter-spacing);
|
|
||||||
font-weight: var(--h3-font-weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* UI element typography - mapped to semantic variables */
|
|
||||||
.typography-ui-button {
|
|
||||||
font-size: var(--text-ui-label);
|
|
||||||
line-height: var(--ui-button-line-height);
|
|
||||||
letter-spacing: var(--ui-button-letter-spacing);
|
|
||||||
font-weight: var(--ui-button-font-weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-ui-caption {
|
|
||||||
font-size: var(--text-micro);
|
|
||||||
line-height: var(--ui-caption-line-height);
|
|
||||||
letter-spacing: var(--ui-caption-letter-spacing);
|
|
||||||
font-weight: var(--ui-caption-font-weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Markdown typography - all use semantic markdown size */
|
|
||||||
.typography-markdown-body {
|
|
||||||
font-size: var(--text-markdown);
|
|
||||||
line-height: var(--markdown-body-line-height);
|
|
||||||
letter-spacing: var(--markdown-body-letter-spacing);
|
|
||||||
font-weight: var(--markdown-body-font-weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-markdown-h1 {
|
|
||||||
font-size: var(--text-markdown);
|
|
||||||
line-height: var(--markdown-h1-line-height);
|
|
||||||
letter-spacing: var(--markdown-h1-letter-spacing);
|
|
||||||
font-weight: var(--markdown-h1-font-weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-markdown-h2 {
|
|
||||||
font-size: var(--text-markdown);
|
|
||||||
line-height: var(--markdown-h2-line-height);
|
|
||||||
letter-spacing: var(--markdown-h2-letter-spacing);
|
|
||||||
font-weight: var(--markdown-h2-font-weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-markdown-h3 {
|
|
||||||
font-size: var(--text-markdown);
|
|
||||||
line-height: var(--markdown-h3-line-height);
|
|
||||||
letter-spacing: var(--markdown-h3-letter-spacing);
|
|
||||||
font-weight: var(--markdown-h3-font-weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-markdown-h4 {
|
|
||||||
font-size: var(--text-markdown);
|
|
||||||
line-height: var(--markdown-h4-line-height);
|
|
||||||
letter-spacing: var(--markdown-h4-letter-spacing);
|
|
||||||
font-weight: var(--markdown-h4-font-weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-spin-once {
|
|
||||||
animation: spin-once 0.5s linear forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/* Default Light Theme */
|
|
||||||
--background: oklch(0.97 0.02 85); /* Warm sand background */
|
|
||||||
--foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
|
||||||
--card: oklch(0.99 0.01 90); /* Bright sand card */
|
|
||||||
--card-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
|
||||||
--popover: oklch(0.99 0.01 90); /* Bright sand popover */
|
|
||||||
--popover-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
|
||||||
--primary: oklch(0.65 0.2 55); /* Primary accent - orange */
|
|
||||||
--primary-foreground: oklch(0.99 0.01 90); /* Light text on primary */
|
|
||||||
--secondary: oklch(0.92 0.02 80); /* Light sand */
|
|
||||||
--secondary-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
|
||||||
--muted: oklch(0.9 0.015 75); /* Muted sand */
|
|
||||||
--muted-foreground: oklch(0.45 0.02 50); /* Medium warm text */
|
|
||||||
--accent: oklch(0.92 0.02 80); /* Light sand accent */
|
|
||||||
--accent-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
|
||||||
--destructive: oklch(0.55 0.25 25); /* Warm red */
|
|
||||||
--destructive-foreground: oklch(0.99 0.01 90); /* Light text */
|
|
||||||
--border: oklch(0.85 0.02 70); /* Warm border */
|
|
||||||
--input: oklch(0.88 0.02 75); /* Input background */
|
|
||||||
--ring: oklch(0.65 0.2 55); /* Focus ring color */
|
|
||||||
--radius: 0.625rem;
|
|
||||||
--chart-1: oklch(0.58 0.15 230); /* Blue */
|
|
||||||
--chart-2: oklch(0.58 0.15 145); /* Oasis green */
|
|
||||||
--chart-3: oklch(0.65 0.2 55); /* Orange */
|
|
||||||
--chart-4: oklch(0.55 0.18 30); /* Rose */
|
|
||||||
--chart-5: oklch(0.6 0.16 85); /* Golden sand */
|
|
||||||
--sidebar: oklch(0.95 0.02 80); /* Slightly darker sand */
|
|
||||||
--sidebar-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
|
||||||
--sidebar-primary: oklch(0.65 0.2 55); /* Primary accent */
|
|
||||||
--sidebar-primary-foreground: oklch(0.99 0.01 90); /* Light text */
|
|
||||||
--sidebar-accent: oklch(0.9 0.02 75); /* Light sand accent */
|
|
||||||
--sidebar-accent-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
|
||||||
--sidebar-border: oklch(0.85 0.02 70); /* Warm border */
|
|
||||||
--sidebar-ring: oklch(0.65 0.2 55); /* Focus ring */
|
|
||||||
--sidebar-stuck-bg: #DDDDD3; /* Desktop sidebar sticky header background */
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
/* Default Dark Theme - using OKLCH color space */
|
|
||||||
--background: oklch(0.16 0.01 30); /* #151313 */
|
|
||||||
--foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
|
||||||
--card: oklch(0.19 0.01 40); /* #1C1B1A */
|
|
||||||
--card-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
|
||||||
--popover: oklch(0.24 0.01 40); /* #282726 */
|
|
||||||
--popover-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
|
||||||
--primary: oklch(0.77 0.17 85); /* #edb449 - golden sand */
|
|
||||||
--primary-foreground: oklch(0.16 0.01 30); /* #151313 */
|
|
||||||
--secondary: oklch(0.29 0.01 40); /* #343331 */
|
|
||||||
--secondary-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
|
||||||
--muted: oklch(0.33 0.01 40); /* #403E3C */
|
|
||||||
--muted-foreground: oklch(0.75 0.02 80); /* #b6b4ab */
|
|
||||||
--accent: oklch(0.29 0.01 40); /* #343331 */
|
|
||||||
--accent-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
|
||||||
--destructive: oklch(0.65 0.15 30); /* #d98678 */
|
|
||||||
--destructive-foreground: oklch(0.9 0.02 80); /* #e1dcd0 */
|
|
||||||
--border: oklch(0.31 0.01 35); /* #393836 */
|
|
||||||
--input: oklch(0.33 0.01 40); /* #403E3C */
|
|
||||||
--ring: oklch(0.77 0.17 85); /* #edb449 */
|
|
||||||
--chart-1: oklch(0.68 0.12 230); /* #5aa9d9 - blue */
|
|
||||||
--chart-2: oklch(0.68 0.12 145); /* #81af6c - green */
|
|
||||||
--chart-3: oklch(0.7 0.13 95); /* #c2974d - yellow */
|
|
||||||
--chart-4: oklch(0.65 0.14 45); /* #d8886d - coral */
|
|
||||||
--chart-5: oklch(0.68 0.12 55); /* #d29470 - orange */
|
|
||||||
--sidebar: oklch(0.16 0.01 30); /* #151313 */
|
|
||||||
--sidebar-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
|
||||||
--sidebar-primary: oklch(0.77 0.17 85); /* #edb449 */
|
|
||||||
--sidebar-primary-foreground: oklch(0.16 0.01 30); /* #151313 */
|
|
||||||
--sidebar-accent: oklch(0.24 0.01 40); /* #282726 */
|
|
||||||
--sidebar-accent-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
|
||||||
--sidebar-border: oklch(0.31 0.01 35); /* #393836 */
|
|
||||||
--sidebar-ring: oklch(0.77 0.17 85); /* #edb449 */
|
|
||||||
--sidebar-stuck-bg: #1F1F1D; /* Desktop sidebar sticky header background */
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
border-color: hsl(var(--border));
|
|
||||||
}
|
|
||||||
|
|
||||||
:root * {
|
|
||||||
border-color: rgba(194, 151, 77, 0.2); /* Warm golden sand borders for light mode */
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark * {
|
|
||||||
border-color: rgba(57, 56, 54, 0.5); /* #393836 at 50% opacity */
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: hsl(var(--background));
|
|
||||||
color: hsl(var(--foreground));
|
|
||||||
}
|
|
||||||
|
|
||||||
@theme inline {
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
|
||||||
|
|
||||||
/* Override Tailwind spacing scale with custom properties that respect --padding-scale */
|
|
||||||
--spacing-0: 0;
|
|
||||||
--spacing-0\.5: calc(0.125rem * var(--padding-scale, 1));
|
|
||||||
--spacing-1: calc(0.25rem * var(--padding-scale, 1));
|
|
||||||
--spacing-1\.5: calc(0.375rem * var(--padding-scale, 1));
|
|
||||||
--spacing-2: calc(0.5rem * var(--padding-scale, 1));
|
|
||||||
--spacing-2\.5: calc(0.625rem * var(--padding-scale, 1));
|
|
||||||
--spacing-3: calc(0.75rem * var(--padding-scale, 1));
|
|
||||||
--spacing-3\.5: calc(0.875rem * var(--padding-scale, 1));
|
|
||||||
--spacing-4: calc(1rem * var(--padding-scale, 1));
|
|
||||||
--spacing-5: calc(1.25rem * var(--padding-scale, 1));
|
|
||||||
--spacing-6: calc(1.5rem * var(--padding-scale, 1));
|
|
||||||
--spacing-7: calc(1.75rem * var(--padding-scale, 1));
|
|
||||||
--spacing-8: calc(2rem * var(--padding-scale, 1));
|
|
||||||
--spacing-9: calc(2.25rem * var(--padding-scale, 1));
|
|
||||||
--spacing-10: calc(2.5rem * var(--padding-scale, 1));
|
|
||||||
--spacing-11: calc(2.75rem * var(--padding-scale, 1));
|
|
||||||
--spacing-12: calc(3rem * var(--padding-scale, 1));
|
|
||||||
--spacing-14: calc(3.5rem * var(--padding-scale, 1));
|
|
||||||
--spacing-16: calc(4rem * var(--padding-scale, 1));
|
|
||||||
--spacing-20: calc(5rem * var(--padding-scale, 1));
|
|
||||||
--spacing-24: calc(6rem * var(--padding-scale, 1));
|
|
||||||
--spacing-28: calc(7rem * var(--padding-scale, 1));
|
|
||||||
--spacing-32: calc(8rem * var(--padding-scale, 1));
|
|
||||||
--spacing-36: calc(9rem * var(--padding-scale, 1));
|
|
||||||
--spacing-40: calc(10rem * var(--padding-scale, 1));
|
|
||||||
--spacing-44: calc(11rem * var(--padding-scale, 1));
|
|
||||||
--spacing-48: calc(12rem * var(--padding-scale, 1));
|
|
||||||
--spacing-52: calc(13rem * var(--padding-scale, 1));
|
|
||||||
--spacing-56: calc(14rem * var(--padding-scale, 1));
|
|
||||||
--spacing-60: calc(15rem * var(--padding-scale, 1));
|
|
||||||
--spacing-64: calc(16rem * var(--padding-scale, 1));
|
|
||||||
--spacing-72: calc(18rem * var(--padding-scale, 1));
|
|
||||||
--spacing-80: calc(20rem * var(--padding-scale, 1));
|
|
||||||
--spacing-96: calc(24rem * var(--padding-scale, 1));
|
|
||||||
|
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--color-card: var(--card);
|
|
||||||
--color-card-foreground: var(--card-foreground);
|
|
||||||
--color-popover: var(--popover);
|
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-border: var(--border);
|
|
||||||
--color-input: var(--input);
|
|
||||||
--color-ring: var(--ring);
|
|
||||||
--color-chart-1: var(--chart-1);
|
|
||||||
--color-chart-2: var(--chart-2);
|
|
||||||
--color-chart-3: var(--chart-3);
|
|
||||||
--color-chart-4: var(--chart-4);
|
|
||||||
--color-chart-5: var(--chart-5);
|
|
||||||
--color-sidebar: var(--sidebar);
|
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
||||||
--color-status-success: var(--status-success);
|
|
||||||
--color-status-warning: var(--status-warning);
|
|
||||||
--color-status-error: var(--status-error);
|
|
||||||
--color-status-info: var(--status-info);
|
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
|
||||||
|
|
||||||
/* Markdown typography variables */
|
|
||||||
--markdown-body-font-size: var(--text-markdown);
|
|
||||||
--markdown-body-line-height: var(--markdown-body-line-height);
|
|
||||||
--markdown-h1-font-size: var(--text-markdown);
|
|
||||||
--markdown-h1-line-height: var(--markdown-h1-line-height);
|
|
||||||
--markdown-h2-font-size: var(--text-markdown);
|
|
||||||
--markdown-h2-line-height: var(--markdown-h2-line-height);
|
|
||||||
--markdown-h3-font-size: var(--text-markdown);
|
|
||||||
--markdown-h3-line-height: var(--markdown-h3-line-height);
|
|
||||||
--markdown-h4-font-size: var(--text-markdown);
|
|
||||||
--markdown-h4-line-height: var(--markdown-h4-line-height);
|
|
||||||
--markdown-h5-font-size: var(--text-markdown);
|
|
||||||
--markdown-h5-line-height: var(--markdown-h5-line-height);
|
|
||||||
--markdown-h6-font-size: var(--text-markdown);
|
|
||||||
--markdown-h6-line-height: var(--markdown-h6-line-height);
|
|
||||||
--markdown-list-font-size: var(--text-markdown);
|
|
||||||
--markdown-list-line-height: var(--markdown-list-line-height);
|
|
||||||
--markdown-code-block-font-size: var(--text-code);
|
|
||||||
--markdown-code-block-line-height: var(--markdown-code-block-line-height);
|
|
||||||
|
|
||||||
/* Markdown color variables */
|
|
||||||
--markdown-heading1: var(--markdown-heading1);
|
|
||||||
--markdown-heading2: var(--markdown-heading2);
|
|
||||||
--markdown-heading3: var(--markdown-heading3);
|
|
||||||
--markdown-heading4: var(--markdown-heading4);
|
|
||||||
--markdown-link: var(--markdown-link);
|
|
||||||
--markdown-link-hover: var(--markdown-link-hover);
|
|
||||||
--markdown-list-marker: var(--markdown-list-marker);
|
|
||||||
--markdown-inline-code: var(--markdown-inline-code);
|
|
||||||
--markdown-inline-code-bg: var(--markdown-inline-code-bg);
|
|
||||||
--markdown-blockquote: var(--markdown-blockquote);
|
|
||||||
--markdown-blockquote-border: var(--markdown-blockquote-border);
|
|
||||||
|
|
||||||
/* Markdown spacing defaults */
|
|
||||||
--markdown-paragraph-spacing: 0.35rem;
|
|
||||||
--markdown-heading-primary-top: 0.75rem;
|
|
||||||
--markdown-heading-primary-bottom: 0.35rem;
|
|
||||||
--markdown-heading-secondary-top: 0.6rem;
|
|
||||||
--markdown-heading-secondary-bottom: 0.3rem;
|
|
||||||
--markdown-blockquote-spacing: 0.6rem;
|
|
||||||
--markdown-blockquote-padding: 0.75rem;
|
|
||||||
--markdown-divider-spacing: 0.85rem;
|
|
||||||
--markdown-table-spacing: 0.9rem;
|
|
||||||
--markdown-heading1-size: var(--text-markdown);
|
|
||||||
--markdown-heading2-size: var(--text-markdown);
|
|
||||||
--markdown-heading3-size: var(--text-markdown);
|
|
||||||
--markdown-heading4-size: var(--text-markdown);
|
|
||||||
--markdown-heading5-size: var(--text-markdown);
|
|
||||||
--markdown-heading6-size: var(--text-markdown);
|
|
||||||
--font-sans: "IBM Plex Mono", "JetBrains Mono", "Fira Code", "SFMono-Regular", "Menlo", monospace;
|
|
||||||
--font-heading: var(--font-sans);
|
|
||||||
--font-mono: "IBM Plex Mono", "JetBrains Mono", "Fira Code", "SFMono-Regular", "Menlo", monospace;
|
|
||||||
--font-family-sans: var(--font-sans);
|
|
||||||
--font-family-mono: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
@apply border-border outline-none;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
@apply bg-background text-foreground;
|
|
||||||
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
|
|
||||||
font-weight: var(--ui-regular-font-weight, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-sans {
|
|
||||||
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-mono,
|
|
||||||
code,
|
|
||||||
pre {
|
|
||||||
font-family: var(--font-mono, ui-monospace, SFMono-Regular, 'Liberation Mono', Menlo, monospace) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove focus rings globally */
|
|
||||||
*:focus {
|
|
||||||
outline: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
*:focus-visible {
|
|
||||||
outline: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar styles */
|
/* Custom scrollbar styles */
|
||||||
@@ -843,82 +429,6 @@ html:not(.dark) .chat-scroll {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Agent color system using theme variables */
|
|
||||||
@layer components {
|
|
||||||
/* Agent color variants - each maps to theme colors */
|
|
||||||
.agent-primary {
|
|
||||||
--agent-color: var(--primary);
|
|
||||||
--agent-color-bg: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-info {
|
|
||||||
--agent-color: var(--status-info);
|
|
||||||
--agent-color-bg: var(--status-info);
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-keyword {
|
|
||||||
--agent-color: var(--syntax-keyword);
|
|
||||||
--agent-color-bg: var(--syntax-keyword);
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-success {
|
|
||||||
--agent-color: var(--status-success);
|
|
||||||
--agent-color-bg: var(--status-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-function {
|
|
||||||
--agent-color: var(--syntax-function);
|
|
||||||
--agent-color-bg: var(--syntax-function);
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-number {
|
|
||||||
--agent-color: var(--syntax-number);
|
|
||||||
--agent-color-bg: var(--syntax-number);
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-type {
|
|
||||||
--agent-color: var(--syntax-type);
|
|
||||||
--agent-color-bg: var(--syntax-type);
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-warning {
|
|
||||||
--agent-color: var(--status-warning);
|
|
||||||
--agent-color-bg: var(--status-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-variable {
|
|
||||||
--agent-color: var(--syntax-variable);
|
|
||||||
--agent-color-bg: var(--syntax-variable);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Apply colors with opacity for badges */
|
|
||||||
.agent-badge {
|
|
||||||
background: rgb(from var(--agent-color-bg) r g b / 0.1);
|
|
||||||
border: 1px solid rgb(from var(--agent-color) r g b / 0.2);
|
|
||||||
color: var(--agent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-badge:hover {
|
|
||||||
background: rgb(from var(--agent-color-bg) r g b / 0.15);
|
|
||||||
border-color: rgb(from var(--agent-color) r g b / 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Agent dot indicator */
|
|
||||||
.agent-dot {
|
|
||||||
background: var(--agent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Agent list item */
|
|
||||||
.agent-list-item:hover {
|
|
||||||
background: rgb(from var(--agent-color-bg) r g b / 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-list-item.active {
|
|
||||||
background: rgb(from var(--agent-color-bg) r g b / 0.1);
|
|
||||||
border-color: rgb(from var(--agent-color) r g b / 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header tab responsiveness */
|
/* Header tab responsiveness */
|
||||||
:root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-label {
|
:root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-label {
|
||||||
display: inline;
|
display: inline;
|
||||||
@@ -930,454 +440,6 @@ html:not(.dark) .chat-scroll {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile Adaptations - All Devices */
|
|
||||||
|
|
||||||
/* Set global device type variables */
|
|
||||||
:root {
|
|
||||||
--is-mobile: 0;
|
|
||||||
--device-type: 'desktop';
|
|
||||||
--font-scale: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) {
|
|
||||||
--is-mobile: 1;
|
|
||||||
--device-type: 'mobile';
|
|
||||||
--font-scale: 0.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Utility classes for device-specific styling */
|
|
||||||
.desktop-only {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-only {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .desktop-only {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.desktop-runtime .mobile-only {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .mobile-only {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* General mobile improvements for all mobile devices */
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
/* Override CSS custom properties for mobile */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) {
|
|
||||||
--text-markdown: 1rem;
|
|
||||||
--text-code: 0.875rem;
|
|
||||||
--text-ui-header: 0.9375rem;
|
|
||||||
--text-ui-label: 0.875rem;
|
|
||||||
--text-meta: 0.875rem;
|
|
||||||
--text-micro: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force override with higher specificity */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) * {
|
|
||||||
--text-markdown: 1rem !important;
|
|
||||||
--text-code: 0.875rem !important;
|
|
||||||
--text-ui-header: 0.9375rem !important;
|
|
||||||
--text-ui-label: 0.875rem !important;
|
|
||||||
--text-meta: 0.875rem !important;
|
|
||||||
--text-micro: 0.8125rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Additional override for elements using typography classes */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .typography-markdown,
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .typography-code,
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .typography-ui-header,
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .typography-ui-label,
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .typography-meta,
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .typography-micro {
|
|
||||||
font-size: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix font size for tool displays */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) [class*="tool"],
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) [class*="code"] {
|
|
||||||
--text-code: 0.875rem !important;
|
|
||||||
font-size: var(--text-code) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Improve touch targets for mobile */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) button,
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .btn,
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) [role="button"] {
|
|
||||||
min-height: 36px;
|
|
||||||
min-width: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Improve input field touch targets */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) input,
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) textarea,
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) select {
|
|
||||||
min-height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix mobile text inputs */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) input[type="text"],
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) input[type="search"],
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) input[type="email"],
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) input[type="password"],
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) textarea {
|
|
||||||
font-size: 16px !important; /* Prevents iOS zoom */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prevent keyboard on non-input elements */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime)
|
|
||||||
button:not([type="submit"]):not([type="button"]):not([type="reset"]),
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .btn,
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) [role="button"] {
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Specific fix for session buttons */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) button[inputmode="none"] {
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Improve mobile spacing */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .px-4 {
|
|
||||||
padding-left: 1rem !important;
|
|
||||||
padding-right: 1rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .py-2 {
|
|
||||||
padding-top: 0.75rem !important;
|
|
||||||
padding-bottom: 0.75rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix mobile scroll containers */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .overflow-hidden {
|
|
||||||
overflow-x: hidden !important;
|
|
||||||
overflow-y: auto !important;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix mobile viewport height */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime),
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) body {
|
|
||||||
height: 100%;
|
|
||||||
height: -webkit-fill-available;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prevent iOS keyboard from scrolling the page */
|
|
||||||
:root.device-mobile:not(.desktop-runtime) body,
|
|
||||||
:root.device-mobile:not(.desktop-runtime) #root {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
overscroll-behavior-y: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix main layout container */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-screen {
|
|
||||||
height: 100vh;
|
|
||||||
height: -webkit-fill-available;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix header positioning */
|
|
||||||
:root.device-mobile:not(.desktop-runtime) .header-safe-area {
|
|
||||||
position: fixed;
|
|
||||||
top: var(--oc-visual-viewport-offset-top, 0px);
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix main content area */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .flex-1.overflow-hidden {
|
|
||||||
min-height: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix chat container */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-full {
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure proper flex behavior */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile sidebar positioning */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .mobile-sidebar-top {
|
|
||||||
top: var(--header-height, 3rem);
|
|
||||||
height: calc(100vh - var(--header-height, 3rem));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Set header height variable */
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
|
||||||
--header-height: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For mobile devices with larger header */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
|
||||||
--header-height: 3.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For tablet devices */
|
|
||||||
@media (min-width: 769px) and (max-width: 1024px) {
|
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
|
||||||
--header-height: 3.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* iOS PWA Adaptations */
|
|
||||||
|
|
||||||
/* Phase 1: iOS PWA safe area handling - Enhanced positioning approach */
|
|
||||||
@media (display-mode: standalone) {
|
|
||||||
/* iOS-specific safe area handling with CSS variable support */
|
|
||||||
@supports (-webkit-touch-callout: none) {
|
|
||||||
/* Safe area handling for fixed positioned elements */
|
|
||||||
:root {
|
|
||||||
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
|
||||||
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
|
||||||
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
|
||||||
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
|
|
||||||
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-safe-area {
|
|
||||||
padding-top: var(--oc-safe-area-top);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content-safe-area {
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
padding-left: var(--oc-safe-area-left);
|
|
||||||
padding-right: var(--oc-safe-area-right);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Safe area for bottom fixed/bottom-0 elements */
|
|
||||||
.bottom-safe-area {
|
|
||||||
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* iOS keyboard home indicator safe area - used when keyboard is open */
|
|
||||||
.ios-keyboard-safe-area {
|
|
||||||
padding-bottom: calc(var(--oc-keyboard-home-indicator, 34px) + var(--oc-safe-area-bottom-visual, 0px)) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prevent keyboard-induced page scroll on iOS PWA */
|
|
||||||
body {
|
|
||||||
min-height: 100vh;
|
|
||||||
min-height: -webkit-fill-available;
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
overscroll-behavior-y: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix iOS viewport issues */
|
|
||||||
.flex.flex-col.h-screen {
|
|
||||||
min-height: 100vh;
|
|
||||||
min-height: -webkit-fill-available;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prevent content overlap in iOS */
|
|
||||||
.flex-1.overflow-hidden {
|
|
||||||
position: relative;
|
|
||||||
overflow-y: auto;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Update header height for iOS */
|
|
||||||
.header-safe-area {
|
|
||||||
--header-height: 4.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-sidebar-top {
|
|
||||||
--header-height: 4.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile typography improvements */
|
|
||||||
.typography-markdown {
|
|
||||||
font-size: 1rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-code {
|
|
||||||
font-size: 0.875rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-ui-header {
|
|
||||||
font-size: 0.9375rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-ui-label {
|
|
||||||
font-size: 0.875rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-meta {
|
|
||||||
font-size: 0.875rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.typography-micro {
|
|
||||||
font-size: 0.8125rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix font size for tool displays */
|
|
||||||
[class*="tool"], [class*="code"] {
|
|
||||||
font-size: 0.875rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fallback for non-iOS standalone mode */
|
|
||||||
@supports not (-webkit-touch-callout: none) {
|
|
||||||
:root {
|
|
||||||
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
|
||||||
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
|
||||||
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
|
||||||
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
|
|
||||||
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-safe-area {
|
|
||||||
padding-top: var(--oc-safe-area-top);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content-safe-area {
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
padding-left: var(--oc-safe-area-left);
|
|
||||||
padding-right: var(--oc-safe-area-right);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-safe-area {
|
|
||||||
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Phase 6: Home indicator blur gradient fix */
|
|
||||||
@media (display-mode: standalone) and (max-width: 768px) {
|
|
||||||
.pwa-dialog-content {
|
|
||||||
--pwa-dialog-padding: clamp(0.75rem, 3vw, 1.25rem);
|
|
||||||
top: calc(50% + (var(--oc-safe-area-top, 0px) * 0.22));
|
|
||||||
max-height: calc(100vh - var(--oc-safe-area-top, 0px) - var(--oc-safe-area-bottom-visual, 0px) - 20px);
|
|
||||||
padding: var(--pwa-dialog-padding);
|
|
||||||
padding-top: calc(var(--pwa-dialog-padding) + (var(--oc-safe-area-top, 0px) * 0.35));
|
|
||||||
--tw-translate-y: calc(-50% + (var(--oc-safe-area-top, 0px) * 0.6));
|
|
||||||
row-gap: clamp(0.75rem, 2vw, 1.25rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pwa-dialog-content.pwa-compact {
|
|
||||||
--pwa-dialog-padding: clamp(0.5rem, 2vw, 0.85rem);
|
|
||||||
row-gap: clamp(0.45rem, 1.5vw, 0.8rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pwa-dialog-content [data-slot="dialog-close"] {
|
|
||||||
top: calc(0.25rem + (var(--oc-safe-area-top, 0px) * 0.35));
|
|
||||||
right: var(--pwa-dialog-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pwa-dialog-content.pwa-compact [data-slot="dialog-close"] {
|
|
||||||
top: calc(0.2rem + (var(--oc-safe-area-top, 0px) * 0.3));
|
|
||||||
}
|
|
||||||
|
|
||||||
.pwa-dialog-content .settings-page-body {
|
|
||||||
max-width: 100%;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
row-gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pwa-dialog-content .settings-page-body > :not([hidden]) ~ :not([hidden]) {
|
|
||||||
margin-top: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pwa-dialog-content .directory-dialog-body {
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pwa-dialog-content .directory-dialog-body .directory-grid {
|
|
||||||
gap: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pwa-overlay-panel {
|
|
||||||
margin-bottom: max(var(--oc-safe-area-bottom-visual, 0px), 12px);
|
|
||||||
padding-bottom: calc(var(--oc-safe-area-bottom-visual, 0px) * 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pwa-overlay-panel .pwa-overlay-scroll {
|
|
||||||
padding-bottom: calc(var(--oc-safe-area-bottom-visual, 0px) + 12px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (display-mode: standalone) {
|
|
||||||
/* Home indicator blur overlay */
|
|
||||||
body::after {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: var(--oc-safe-area-bottom-visual, var(--oc-safe-area-bottom, env(safe-area-inset-bottom, 0)));
|
|
||||||
background: linear-gradient(
|
|
||||||
to top,
|
|
||||||
var(--background) 0%,
|
|
||||||
var(--background) 30%,
|
|
||||||
rgba(from var(--background) r g b / 0.8) 60%,
|
|
||||||
rgba(from var(--background) r g b / 0.3) 85%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
z-index: 1000;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fallback for browsers without rgba(from ...) support */
|
|
||||||
@supports not (color: rgba(from white r g b / 0.5)) {
|
|
||||||
body::after {
|
|
||||||
background: linear-gradient(
|
|
||||||
to top,
|
|
||||||
var(--background) 0%,
|
|
||||||
var(--background) 30%,
|
|
||||||
rgba(21, 19, 19, 0.8) 60%, /* Dark theme fallback */
|
|
||||||
rgba(21, 19, 19, 0.3) 85%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Light theme fallback */
|
|
||||||
:root body::after {
|
|
||||||
background: linear-gradient(
|
|
||||||
to top,
|
|
||||||
var(--background) 0%,
|
|
||||||
var(--background) 30%,
|
|
||||||
rgba(248, 247, 243, 0.8) 60%, /* Light theme fallback */
|
|
||||||
rgba(248, 247, 243, 0.3) 85%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
Streamdown Styles
|
Streamdown Styles
|
||||||
Minimal overrides - fonts only, using Streamdown defaults
|
Minimal overrides - fonts only, using Streamdown defaults
|
||||||
@@ -1565,3 +627,18 @@ html:not(.dark) .chat-scroll {
|
|||||||
.animate-grid-pulse {
|
.animate-grid-pulse {
|
||||||
animation: grid-pulse 1.2s ease-in-out infinite;
|
animation: grid-pulse 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes marquee-scroll {
|
||||||
|
0% { transform: translateX(0); }
|
||||||
|
100% { transform: translateX(-100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.marquee-text {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group:hover .marquee-text,
|
||||||
|
.marquee-text:hover {
|
||||||
|
animation: marquee-scroll 5s linear infinite alternate;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,357 @@
|
|||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--oc-safe-area-top: 0px;
|
||||||
|
--oc-safe-area-right: 0px;
|
||||||
|
--oc-safe-area-bottom: 0px;
|
||||||
|
--oc-safe-area-bottom-visual: 0px;
|
||||||
|
--oc-safe-area-left: 0px;
|
||||||
|
--oc-header-height: 56px;
|
||||||
|
--oc-visual-viewport-offset-top: 0px;
|
||||||
|
--oc-keyboard-inset: 0px;
|
||||||
|
--oc-keyboard-avoid-offset: 0px;
|
||||||
|
--oc-keyboard-home-indicator: 0px;
|
||||||
|
--ui-regular-font-weight: 400;
|
||||||
|
--oc-scrollbar-thumb: oklch(0.32 0.03 50 / 0.4);
|
||||||
|
--oc-scrollbar-thumb-hover: oklch(0.32 0.03 50 / 0.6);
|
||||||
|
--padding-scale: 1;
|
||||||
|
|
||||||
|
/* Semantic typography defaults (must match SEMANTIC_TYPOGRAPHY) */
|
||||||
|
--text-markdown: 0.9375rem;
|
||||||
|
--text-code: 0.9063rem;
|
||||||
|
--text-ui-header: 0.9375rem;
|
||||||
|
--text-ui-label: 0.8750rem;
|
||||||
|
--text-meta: 0.875rem;
|
||||||
|
--text-micro: 0.875rem;
|
||||||
|
|
||||||
|
/* Default Light Theme */
|
||||||
|
--background: oklch(0.97 0.02 85); /* Warm sand background */
|
||||||
|
--foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||||
|
--card: oklch(0.99 0.01 90); /* Bright sand card */
|
||||||
|
--card-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||||
|
--popover: oklch(0.99 0.01 90); /* Bright sand popover */
|
||||||
|
--popover-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||||
|
--primary: oklch(0.65 0.2 55); /* Primary accent - orange */
|
||||||
|
--primary-foreground: oklch(0.99 0.01 90); /* Light text on primary */
|
||||||
|
--secondary: oklch(0.92 0.02 80); /* Light sand */
|
||||||
|
--secondary-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||||
|
--muted: oklch(0.9 0.015 75); /* Muted sand */
|
||||||
|
--muted-foreground: oklch(0.45 0.02 50); /* Medium warm text */
|
||||||
|
--accent: oklch(0.92 0.02 80); /* Light sand accent */
|
||||||
|
--accent-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||||
|
--destructive: oklch(0.55 0.25 25); /* Warm red */
|
||||||
|
--destructive-foreground: oklch(0.99 0.01 90); /* Light text */
|
||||||
|
--border: oklch(0.85 0.02 70); /* Warm border */
|
||||||
|
--input: oklch(0.88 0.02 75); /* Input background */
|
||||||
|
--ring: oklch(0.65 0.2 55); /* Focus ring color */
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--chart-1: oklch(0.58 0.15 230); /* Blue */
|
||||||
|
--chart-2: oklch(0.58 0.15 145); /* Oasis green */
|
||||||
|
--chart-3: oklch(0.65 0.2 55); /* Orange */
|
||||||
|
--chart-4: oklch(0.55 0.18 30); /* Rose */
|
||||||
|
--chart-5: oklch(0.6 0.16 85); /* Golden sand */
|
||||||
|
--sidebar: oklch(0.95 0.02 80); /* Slightly darker sand */
|
||||||
|
--sidebar-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||||
|
--sidebar-primary: oklch(0.65 0.2 55); /* Primary accent */
|
||||||
|
--sidebar-primary-foreground: oklch(0.99 0.01 90); /* Light text */
|
||||||
|
--sidebar-accent: oklch(0.9 0.02 75); /* Light sand accent */
|
||||||
|
--sidebar-accent-foreground: oklch(0.25 0.02 40); /* Dark warm text */
|
||||||
|
--sidebar-border: oklch(0.85 0.02 70); /* Warm border */
|
||||||
|
--sidebar-ring: oklch(0.65 0.2 55); /* Focus ring */
|
||||||
|
--sidebar-stuck-bg: #DDDDD3; /* Desktop sidebar sticky header background */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
/* Default Dark Theme - using OKLCH color space */
|
||||||
|
--background: oklch(0.16 0.01 30); /* #151313 */
|
||||||
|
--foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||||
|
--card: oklch(0.19 0.01 40); /* #1C1B1A */
|
||||||
|
--card-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||||
|
--popover: oklch(0.24 0.01 40); /* #282726 */
|
||||||
|
--popover-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||||
|
--primary: oklch(0.77 0.17 85); /* #edb449 - golden sand */
|
||||||
|
--primary-foreground: oklch(0.16 0.01 30); /* #151313 */
|
||||||
|
--secondary: oklch(0.29 0.01 40); /* #343331 */
|
||||||
|
--secondary-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||||
|
--muted: oklch(0.33 0.01 40); /* #403E3C */
|
||||||
|
--muted-foreground: oklch(0.75 0.02 80); /* #b6b4ab */
|
||||||
|
--accent: oklch(0.29 0.01 40); /* #343331 */
|
||||||
|
--accent-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||||
|
--destructive: oklch(0.65 0.15 30); /* #d98678 */
|
||||||
|
--destructive-foreground: oklch(0.9 0.02 80); /* #e1dcd0 */
|
||||||
|
--border: oklch(0.31 0.01 35); /* #393836 */
|
||||||
|
--input: oklch(0.33 0.01 40); /* #403E3C */
|
||||||
|
--ring: oklch(0.77 0.17 85); /* #edb449 */
|
||||||
|
--chart-1: oklch(0.68 0.12 230); /* #5aa9d9 - blue */
|
||||||
|
--chart-2: oklch(0.68 0.12 145); /* #81af6c - green */
|
||||||
|
--chart-3: oklch(0.7 0.13 95); /* #c2974d - yellow */
|
||||||
|
--chart-4: oklch(0.65 0.14 45); /* #d8886d - coral */
|
||||||
|
--chart-5: oklch(0.68 0.12 55); /* #d29470 - orange */
|
||||||
|
--sidebar: oklch(0.16 0.01 30); /* #151313 */
|
||||||
|
--sidebar-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||||
|
--sidebar-primary: oklch(0.77 0.17 85); /* #edb449 */
|
||||||
|
--sidebar-primary-foreground: oklch(0.16 0.01 30); /* #151313 */
|
||||||
|
--sidebar-accent: oklch(0.24 0.01 40); /* #282726 */
|
||||||
|
--sidebar-accent-foreground: oklch(0.85 0.02 90); /* #cdccc3 */
|
||||||
|
--sidebar-border: oklch(0.31 0.01 35); /* #393836 */
|
||||||
|
--sidebar-ring: oklch(0.77 0.17 85); /* #edb449 */
|
||||||
|
--sidebar-stuck-bg: #1F1F1D; /* Desktop sidebar sticky header background */
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
border-color: hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
:root * {
|
||||||
|
border-color: rgba(194, 151, 77, 0.2); /* Warm golden sand borders for light mode */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark * {
|
||||||
|
border-color: rgba(57, 56, 54, 0.5); /* #393836 at 50% opacity */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: hsl(var(--background));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
@apply border-border outline-none;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
|
||||||
|
font-weight: var(--ui-regular-font-weight, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-sans {
|
||||||
|
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-mono,
|
||||||
|
code,
|
||||||
|
pre {
|
||||||
|
font-family: var(--font-mono, ui-monospace, SFMono-Regular, 'Liberation Mono', Menlo, monospace) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove focus rings globally */
|
||||||
|
*:focus {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
*:focus-visible {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
|
||||||
|
/* Override Tailwind spacing scale with custom properties that respect --padding-scale */
|
||||||
|
--spacing-0: 0;
|
||||||
|
--spacing-0\.5: calc(0.125rem * var(--padding-scale, 1));
|
||||||
|
--spacing-1: calc(0.25rem * var(--padding-scale, 1));
|
||||||
|
--spacing-1\.5: calc(0.375rem * var(--padding-scale, 1));
|
||||||
|
--spacing-2: calc(0.5rem * var(--padding-scale, 1));
|
||||||
|
--spacing-2\.5: calc(0.625rem * var(--padding-scale, 1));
|
||||||
|
--spacing-3: calc(0.75rem * var(--padding-scale, 1));
|
||||||
|
--spacing-3\.5: calc(0.875rem * var(--padding-scale, 1));
|
||||||
|
--spacing-4: calc(1rem * var(--padding-scale, 1));
|
||||||
|
--spacing-5: calc(1.25rem * var(--padding-scale, 1));
|
||||||
|
--spacing-6: calc(1.5rem * var(--padding-scale, 1));
|
||||||
|
--spacing-7: calc(1.75rem * var(--padding-scale, 1));
|
||||||
|
--spacing-8: calc(2rem * var(--padding-scale, 1));
|
||||||
|
--spacing-9: calc(2.25rem * var(--padding-scale, 1));
|
||||||
|
--spacing-10: calc(2.5rem * var(--padding-scale, 1));
|
||||||
|
--spacing-11: calc(2.75rem * var(--padding-scale, 1));
|
||||||
|
--spacing-12: calc(3rem * var(--padding-scale, 1));
|
||||||
|
--spacing-14: calc(3.5rem * var(--padding-scale, 1));
|
||||||
|
--spacing-16: calc(4rem * var(--padding-scale, 1));
|
||||||
|
--spacing-20: calc(5rem * var(--padding-scale, 1));
|
||||||
|
--spacing-24: calc(6rem * var(--padding-scale, 1));
|
||||||
|
--spacing-28: calc(7rem * var(--padding-scale, 1));
|
||||||
|
--spacing-32: calc(8rem * var(--padding-scale, 1));
|
||||||
|
--spacing-36: calc(9rem * var(--padding-scale, 1));
|
||||||
|
--spacing-40: calc(10rem * var(--padding-scale, 1));
|
||||||
|
--spacing-44: calc(11rem * var(--padding-scale, 1));
|
||||||
|
--spacing-48: calc(12rem * var(--padding-scale, 1));
|
||||||
|
--spacing-52: calc(13rem * var(--padding-scale, 1));
|
||||||
|
--spacing-56: calc(14rem * var(--padding-scale, 1));
|
||||||
|
--spacing-60: calc(15rem * var(--padding-scale, 1));
|
||||||
|
--spacing-64: calc(16rem * var(--padding-scale, 1));
|
||||||
|
--spacing-72: calc(18rem * var(--padding-scale, 1));
|
||||||
|
--spacing-80: calc(20rem * var(--padding-scale, 1));
|
||||||
|
--spacing-96: calc(24rem * var(--padding-scale, 1));
|
||||||
|
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-status-success: var(--status-success);
|
||||||
|
--color-status-warning: var(--status-warning);
|
||||||
|
--color-status-error: var(--status-error);
|
||||||
|
--color-status-info: var(--status-info);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
/* Markdown typography variables */
|
||||||
|
--markdown-body-font-size: var(--text-markdown);
|
||||||
|
--markdown-body-line-height: var(--markdown-body-line-height);
|
||||||
|
--markdown-h1-font-size: var(--text-markdown);
|
||||||
|
--markdown-h1-line-height: var(--markdown-h1-line-height);
|
||||||
|
--markdown-h2-font-size: var(--text-markdown);
|
||||||
|
--markdown-h2-line-height: var(--markdown-h2-line-height);
|
||||||
|
--markdown-h3-font-size: var(--text-markdown);
|
||||||
|
--markdown-h3-line-height: var(--markdown-h3-line-height);
|
||||||
|
--markdown-h4-font-size: var(--text-markdown);
|
||||||
|
--markdown-h4-line-height: var(--markdown-h4-line-height);
|
||||||
|
--markdown-h5-font-size: var(--text-markdown);
|
||||||
|
--markdown-h5-line-height: var(--markdown-h5-line-height);
|
||||||
|
--markdown-h6-font-size: var(--text-markdown);
|
||||||
|
--markdown-h6-line-height: var(--markdown-h6-line-height);
|
||||||
|
--markdown-list-font-size: var(--text-markdown);
|
||||||
|
--markdown-list-line-height: var(--markdown-list-line-height);
|
||||||
|
--markdown-code-block-font-size: var(--text-code);
|
||||||
|
--markdown-code-block-line-height: var(--markdown-code-block-line-height);
|
||||||
|
|
||||||
|
/* Markdown color variables */
|
||||||
|
--markdown-heading1: var(--markdown-heading1);
|
||||||
|
--markdown-heading2: var(--markdown-heading2);
|
||||||
|
--markdown-heading3: var(--markdown-heading3);
|
||||||
|
--markdown-heading4: var(--markdown-heading4);
|
||||||
|
--markdown-link: var(--markdown-link);
|
||||||
|
--markdown-link-hover: var(--markdown-link-hover);
|
||||||
|
--markdown-list-marker: var(--markdown-list-marker);
|
||||||
|
--markdown-inline-code: var(--markdown-inline-code);
|
||||||
|
--markdown-inline-code-bg: var(--markdown-inline-code-bg);
|
||||||
|
--markdown-blockquote: var(--markdown-blockquote);
|
||||||
|
--markdown-blockquote-border: var(--markdown-blockquote-border);
|
||||||
|
|
||||||
|
/* Markdown spacing defaults */
|
||||||
|
--markdown-paragraph-spacing: 0.35rem;
|
||||||
|
--markdown-heading-primary-top: 0.75rem;
|
||||||
|
--markdown-heading-primary-bottom: 0.35rem;
|
||||||
|
--markdown-heading-secondary-top: 0.6rem;
|
||||||
|
--markdown-heading-secondary-bottom: 0.3rem;
|
||||||
|
--markdown-blockquote-spacing: 0.6rem;
|
||||||
|
--markdown-blockquote-padding: 0.75rem;
|
||||||
|
--markdown-divider-spacing: 0.85rem;
|
||||||
|
--markdown-table-spacing: 0.9rem;
|
||||||
|
--markdown-heading1-size: var(--text-markdown);
|
||||||
|
--markdown-heading2-size: var(--text-markdown);
|
||||||
|
--markdown-heading3-size: var(--text-markdown);
|
||||||
|
--markdown-heading4-size: var(--text-markdown);
|
||||||
|
--markdown-heading5-size: var(--text-markdown);
|
||||||
|
--markdown-heading6-size: var(--text-markdown);
|
||||||
|
--font-sans: "IBM Plex Mono", "JetBrains Mono", "Fira Code", "SFMono-Regular", "Menlo", monospace;
|
||||||
|
--font-heading: var(--font-sans);
|
||||||
|
--font-mono: "IBM Plex Mono", "JetBrains Mono", "Fira Code", "SFMono-Regular", "Menlo", monospace;
|
||||||
|
--font-family-sans: var(--font-sans);
|
||||||
|
--font-family-mono: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Agent color system using theme variables */
|
||||||
|
@layer components {
|
||||||
|
/* Agent color variants - each maps to theme colors */
|
||||||
|
.agent-primary {
|
||||||
|
--agent-color: var(--primary);
|
||||||
|
--agent-color-bg: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-info {
|
||||||
|
--agent-color: var(--status-info);
|
||||||
|
--agent-color-bg: var(--status-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-keyword {
|
||||||
|
--agent-color: var(--syntax-keyword);
|
||||||
|
--agent-color-bg: var(--syntax-keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-success {
|
||||||
|
--agent-color: var(--status-success);
|
||||||
|
--agent-color-bg: var(--status-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-function {
|
||||||
|
--agent-color: var(--syntax-function);
|
||||||
|
--agent-color-bg: var(--syntax-function);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-number {
|
||||||
|
--agent-color: var(--syntax-number);
|
||||||
|
--agent-color-bg: var(--syntax-number);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-type {
|
||||||
|
--agent-color: var(--syntax-type);
|
||||||
|
--agent-color-bg: var(--syntax-type);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-warning {
|
||||||
|
--agent-color: var(--status-warning);
|
||||||
|
--agent-color-bg: var(--status-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-variable {
|
||||||
|
--agent-color: var(--syntax-variable);
|
||||||
|
--agent-color-bg: var(--syntax-variable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply colors with opacity for badges */
|
||||||
|
.agent-badge {
|
||||||
|
background: rgb(from var(--agent-color-bg) r g b / 0.1);
|
||||||
|
border: 1px solid rgb(from var(--agent-color) r g b / 0.2);
|
||||||
|
color: var(--agent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-badge:hover {
|
||||||
|
background: rgb(from var(--agent-color-bg) r g b / 0.15);
|
||||||
|
border-color: rgb(from var(--agent-color) r g b / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Agent dot indicator */
|
||||||
|
.agent-dot {
|
||||||
|
background: var(--agent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Agent list item */
|
||||||
|
.agent-list-item:hover {
|
||||||
|
background: rgb(from var(--agent-color-bg) r g b / 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-list-item.active {
|
||||||
|
background: rgb(from var(--agent-color-bg) r g b / 0.1);
|
||||||
|
border-color: rgb(from var(--agent-color) r g b / 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
/* Mobile Adaptations - All Devices */
|
||||||
|
|
||||||
|
/* Set global device type variables */
|
||||||
|
:root {
|
||||||
|
--is-mobile: 0;
|
||||||
|
--device-type: 'desktop';
|
||||||
|
--font-scale: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) {
|
||||||
|
--is-mobile: 1;
|
||||||
|
--device-type: 'mobile';
|
||||||
|
--font-scale: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility classes for device-specific styling */
|
||||||
|
.desktop-only {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .desktop-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.desktop-runtime .mobile-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .mobile-only {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* General mobile improvements for all mobile devices */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
/* Override CSS custom properties for mobile */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) {
|
||||||
|
--text-markdown: 1rem;
|
||||||
|
--text-code: 0.875rem;
|
||||||
|
--text-ui-header: 0.9375rem;
|
||||||
|
--text-ui-label: 0.875rem;
|
||||||
|
--text-meta: 0.875rem;
|
||||||
|
--text-micro: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force override with higher specificity */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) * {
|
||||||
|
--text-markdown: 1rem !important;
|
||||||
|
--text-code: 0.875rem !important;
|
||||||
|
--text-ui-header: 0.9375rem !important;
|
||||||
|
--text-ui-label: 0.875rem !important;
|
||||||
|
--text-meta: 0.875rem !important;
|
||||||
|
--text-micro: 0.8125rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional override for elements using typography classes */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .typography-markdown,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .typography-code,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .typography-ui-header,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .typography-ui-label,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .typography-meta,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .typography-micro {
|
||||||
|
font-size: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix font size for tool displays */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) [class*="tool"],
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) [class*="code"] {
|
||||||
|
--text-code: 0.875rem !important;
|
||||||
|
font-size: var(--text-code) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improve touch targets for mobile */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) button,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .btn,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) [role="button"] {
|
||||||
|
min-height: 36px;
|
||||||
|
min-width: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improve input field touch targets */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) input,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) textarea,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) select {
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix mobile text inputs */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) input[type="text"],
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) input[type="search"],
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) input[type="email"],
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) input[type="password"],
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) textarea {
|
||||||
|
font-size: 16px !important; /* Prevents iOS zoom */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent keyboard on non-input elements */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime)
|
||||||
|
button:not([type="submit"]):not([type="button"]):not([type="reset"]),
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .btn,
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) [role="button"] {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific fix for session buttons */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) button[inputmode="none"] {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improve mobile spacing */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .px-4 {
|
||||||
|
padding-left: 1rem !important;
|
||||||
|
padding-right: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .py-2 {
|
||||||
|
padding-top: 0.75rem !important;
|
||||||
|
padding-bottom: 0.75rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix mobile scroll containers */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .overflow-hidden {
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix mobile viewport height */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime),
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) body {
|
||||||
|
height: 100%;
|
||||||
|
height: -webkit-fill-available;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent iOS keyboard from scrolling the page */
|
||||||
|
:root.device-mobile:not(.desktop-runtime) body,
|
||||||
|
:root.device-mobile:not(.desktop-runtime) #root {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
overscroll-behavior-y: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix main layout container */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-screen {
|
||||||
|
height: 100vh;
|
||||||
|
height: -webkit-fill-available;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix header positioning */
|
||||||
|
:root.device-mobile:not(.desktop-runtime) .header-safe-area {
|
||||||
|
position: fixed;
|
||||||
|
top: var(--oc-visual-viewport-offset-top, 0px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix main content area */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .flex-1.overflow-hidden {
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix chat container */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col.h-full {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper flex behavior */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .flex.flex-col {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile sidebar positioning */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .mobile-sidebar-top {
|
||||||
|
top: var(--header-height, 3rem);
|
||||||
|
height: calc(100vh - var(--header-height, 3rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set header height variable */
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
||||||
|
--header-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For mobile devices with larger header */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
||||||
|
--header-height: 3.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For tablet devices */
|
||||||
|
@media (min-width: 769px) and (max-width: 1024px) {
|
||||||
|
:root.mobile-pointer:not(.desktop-runtime) .header-safe-area {
|
||||||
|
--header-height: 3.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* iOS PWA Adaptations */
|
||||||
|
|
||||||
|
/* Phase 1: iOS PWA safe area handling - Enhanced positioning approach */
|
||||||
|
@media (display-mode: standalone) {
|
||||||
|
/* iOS-specific safe area handling with CSS variable support */
|
||||||
|
@supports (-webkit-touch-callout: none) {
|
||||||
|
/* Safe area handling for fixed positioned elements */
|
||||||
|
:root {
|
||||||
|
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
||||||
|
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
||||||
|
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
||||||
|
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
|
||||||
|
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-safe-area {
|
||||||
|
padding-top: var(--oc-safe-area-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content-safe-area {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-left: var(--oc-safe-area-left);
|
||||||
|
padding-right: var(--oc-safe-area-right);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Safe area for bottom fixed/bottom-0 elements */
|
||||||
|
.bottom-safe-area {
|
||||||
|
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* iOS keyboard home indicator safe area - used when keyboard is open */
|
||||||
|
.ios-keyboard-safe-area {
|
||||||
|
padding-bottom: calc(var(--oc-keyboard-home-indicator, 34px) + var(--oc-safe-area-bottom-visual, 0px)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent keyboard-induced page scroll on iOS PWA */
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: -webkit-fill-available;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
overscroll-behavior-y: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix iOS viewport issues */
|
||||||
|
.flex.flex-col.h-screen {
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: -webkit-fill-available;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent content overlap in iOS */
|
||||||
|
.flex-1.overflow-hidden {
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update header height for iOS */
|
||||||
|
.header-safe-area {
|
||||||
|
--header-height: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar-top {
|
||||||
|
--header-height: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile typography improvements */
|
||||||
|
.typography-markdown {
|
||||||
|
font-size: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-code {
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-ui-header {
|
||||||
|
font-size: 0.9375rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-ui-label {
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-meta {
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-micro {
|
||||||
|
font-size: 0.8125rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix font size for tool displays */
|
||||||
|
[class*="tool"], [class*="code"] {
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback for non-iOS standalone mode */
|
||||||
|
@supports not (-webkit-touch-callout: none) {
|
||||||
|
:root {
|
||||||
|
--oc-safe-area-top: env(safe-area-inset-top, 0);
|
||||||
|
--oc-safe-area-right: env(safe-area-inset-right, 0);
|
||||||
|
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
|
||||||
|
--oc-safe-area-bottom-visual: clamp(0px, calc(var(--oc-safe-area-bottom) * 0.05), 4px);
|
||||||
|
--oc-safe-area-left: env(safe-area-inset-left, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-safe-area {
|
||||||
|
padding-top: var(--oc-safe-area-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content-safe-area {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-left: var(--oc-safe-area-left);
|
||||||
|
padding-right: var(--oc-safe-area-right);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-safe-area {
|
||||||
|
padding-bottom: var(--oc-safe-area-bottom-visual) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Phase 6: Home indicator blur gradient fix */
|
||||||
|
@media (display-mode: standalone) and (max-width: 768px) {
|
||||||
|
.pwa-dialog-content {
|
||||||
|
--pwa-dialog-padding: clamp(0.75rem, 3vw, 1.25rem);
|
||||||
|
top: calc(50% + (var(--oc-safe-area-top, 0px) * 0.22));
|
||||||
|
max-height: calc(100vh - var(--oc-safe-area-top, 0px) - var(--oc-safe-area-bottom-visual, 0px) - 20px);
|
||||||
|
padding: var(--pwa-dialog-padding);
|
||||||
|
padding-top: calc(var(--pwa-dialog-padding) + (var(--oc-safe-area-top, 0px) * 0.35));
|
||||||
|
--tw-translate-y: calc(-50% + (var(--oc-safe-area-top, 0px) * 0.6));
|
||||||
|
row-gap: clamp(0.75rem, 2vw, 1.25rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-dialog-content.pwa-compact {
|
||||||
|
--pwa-dialog-padding: clamp(0.5rem, 2vw, 0.85rem);
|
||||||
|
row-gap: clamp(0.45rem, 1.5vw, 0.8rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-dialog-content [data-slot="dialog-close"] {
|
||||||
|
top: calc(0.25rem + (var(--oc-safe-area-top, 0px) * 0.35));
|
||||||
|
right: var(--pwa-dialog-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-dialog-content.pwa-compact [data-slot="dialog-close"] {
|
||||||
|
top: calc(0.2rem + (var(--oc-safe-area-top, 0px) * 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-dialog-content .settings-page-body {
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
row-gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-dialog-content .settings-page-body > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
margin-top: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-dialog-content .directory-dialog-body {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-dialog-content .directory-dialog-body .directory-grid {
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-overlay-panel {
|
||||||
|
margin-bottom: max(var(--oc-safe-area-bottom-visual, 0px), 12px);
|
||||||
|
padding-bottom: calc(var(--oc-safe-area-bottom-visual, 0px) * 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-overlay-panel .pwa-overlay-scroll {
|
||||||
|
padding-bottom: calc(var(--oc-safe-area-bottom-visual, 0px) + 12px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (display-mode: standalone) {
|
||||||
|
/* Home indicator blur overlay */
|
||||||
|
body::after {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: var(--oc-safe-area-bottom-visual, var(--oc-safe-area-bottom, env(safe-area-inset-bottom, 0)));
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
var(--background) 0%,
|
||||||
|
var(--background) 30%,
|
||||||
|
rgba(from var(--background) r g b / 0.8) 60%,
|
||||||
|
rgba(from var(--background) r g b / 0.3) 85%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback for browsers without rgba(from ...) support */
|
||||||
|
@supports not (color: rgba(from white r g b / 0.5)) {
|
||||||
|
body::after {
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
var(--background) 0%,
|
||||||
|
var(--background) 30%,
|
||||||
|
rgba(21, 19, 19, 0.8) 60%, /* Dark theme fallback */
|
||||||
|
rgba(21, 19, 19, 0.3) 85%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light theme fallback */
|
||||||
|
:root body::after {
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
var(--background) 0%,
|
||||||
|
var(--background) 30%,
|
||||||
|
rgba(248, 247, 243, 0.8) 60%, /* Light theme fallback */
|
||||||
|
rgba(248, 247, 243, 0.3) 85%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
@layer components {
|
||||||
|
/* Semantic typography classes - font-size only */
|
||||||
|
.typography-markdown {
|
||||||
|
font-size: var(--text-markdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-code {
|
||||||
|
font-size: var(--text-code);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-ui-header {
|
||||||
|
font-size: var(--text-ui-header);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-ui-label {
|
||||||
|
font-size: var(--text-ui-label);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-meta {
|
||||||
|
font-size: var(--text-meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-micro {
|
||||||
|
font-size: var(--text-micro);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-column {
|
||||||
|
width: min(100%, 56rem);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: calc(clamp(1rem, 3vw, 1.5rem) * var(--padding-scale, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-column {
|
||||||
|
width: min(100%, 56rem);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: calc(clamp(0.75rem, 2.5vw, 1rem) * var(--padding-scale, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.chat-column {
|
||||||
|
padding-inline: calc(clamp(1.25rem, 2.5vw, 2rem) * var(--padding-scale, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-column {
|
||||||
|
padding-inline: calc(clamp(1rem, 2.5vw, 1.5rem) * var(--padding-scale, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced focus indicators for small interactive elements */
|
||||||
|
.model-favorite-button {
|
||||||
|
position: relative;
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-favorite-button:focus-visible {
|
||||||
|
outline: 2px solid var(--primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-favorite-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Heading typography - all use markdown size, differentiated by weight/color */
|
||||||
|
|
||||||
|
.typography-h1 {
|
||||||
|
font-size: var(--text-markdown);
|
||||||
|
line-height: var(--h1-line-height);
|
||||||
|
letter-spacing: var(--h1-letter-spacing);
|
||||||
|
font-weight: var(--h1-font-weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-h2 {
|
||||||
|
font-size: var(--text-markdown);
|
||||||
|
line-height: var(--h2-line-height);
|
||||||
|
letter-spacing: var(--h2-letter-spacing);
|
||||||
|
font-weight: var(--h2-font-weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-h3 {
|
||||||
|
font-size: var(--text-markdown);
|
||||||
|
line-height: var(--h3-line-height);
|
||||||
|
letter-spacing: var(--h3-letter-spacing);
|
||||||
|
font-weight: var(--h3-font-weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* UI element typography - mapped to semantic variables */
|
||||||
|
.typography-ui-button {
|
||||||
|
font-size: var(--text-ui-label);
|
||||||
|
line-height: var(--ui-button-line-height);
|
||||||
|
letter-spacing: var(--ui-button-letter-spacing);
|
||||||
|
font-weight: var(--ui-button-font-weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-ui-caption {
|
||||||
|
font-size: var(--text-micro);
|
||||||
|
line-height: var(--ui-caption-line-height);
|
||||||
|
letter-spacing: var(--ui-caption-letter-spacing);
|
||||||
|
font-weight: var(--ui-caption-font-weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown typography - all use semantic markdown size */
|
||||||
|
.typography-markdown-body {
|
||||||
|
font-size: var(--text-markdown);
|
||||||
|
line-height: var(--markdown-body-line-height);
|
||||||
|
letter-spacing: var(--markdown-body-letter-spacing);
|
||||||
|
font-weight: var(--markdown-body-font-weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-markdown-h1 {
|
||||||
|
font-size: var(--text-markdown);
|
||||||
|
line-height: var(--markdown-h1-line-height);
|
||||||
|
letter-spacing: var(--markdown-h1-letter-spacing);
|
||||||
|
font-weight: var(--markdown-h1-font-weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-markdown-h2 {
|
||||||
|
font-size: var(--text-markdown);
|
||||||
|
line-height: var(--markdown-h2-line-height);
|
||||||
|
letter-spacing: var(--markdown-h2-letter-spacing);
|
||||||
|
font-weight: var(--markdown-h2-font-weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-markdown-h3 {
|
||||||
|
font-size: var(--text-markdown);
|
||||||
|
line-height: var(--markdown-h3-line-height);
|
||||||
|
letter-spacing: var(--markdown-h3-letter-spacing);
|
||||||
|
font-weight: var(--markdown-h3-font-weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typography-markdown-h4 {
|
||||||
|
font-size: var(--text-markdown);
|
||||||
|
line-height: var(--markdown-h4-line-height);
|
||||||
|
letter-spacing: var(--markdown-h4-letter-spacing);
|
||||||
|
font-weight: var(--markdown-h4-font-weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -686,6 +686,45 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'api:fs:delete': {
|
||||||
|
const targetPath = (payload as { path: string })?.path;
|
||||||
|
if (!targetPath) {
|
||||||
|
return { id, type, success: false, error: 'Path is required' };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || os.homedir();
|
||||||
|
const resolvedPath = resolveUserPath(targetPath, workspaceRoot);
|
||||||
|
const uri = vscode.Uri.file(resolvedPath);
|
||||||
|
await vscode.workspace.fs.delete(uri, { recursive: true, useTrash: false });
|
||||||
|
return { id, type, success: true, data: { success: true } };
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to delete file';
|
||||||
|
return { id, type, success: false, error: message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'api:fs:rename': {
|
||||||
|
const { oldPath, newPath } = (payload as { oldPath: string; newPath: string }) || {};
|
||||||
|
if (!oldPath) {
|
||||||
|
return { id, type, success: false, error: 'oldPath is required' };
|
||||||
|
}
|
||||||
|
if (!newPath) {
|
||||||
|
return { id, type, success: false, error: 'newPath is required' };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || os.homedir();
|
||||||
|
const resolvedOld = resolveUserPath(oldPath, workspaceRoot);
|
||||||
|
const resolvedNew = resolveUserPath(newPath, workspaceRoot);
|
||||||
|
const oldUri = vscode.Uri.file(resolvedOld);
|
||||||
|
const newUri = vscode.Uri.file(resolvedNew);
|
||||||
|
await vscode.workspace.fs.rename(oldUri, newUri, { overwrite: false });
|
||||||
|
return { id, type, success: true, data: { success: true, path: normalizeFsPath(resolvedNew) } };
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to rename file';
|
||||||
|
return { id, type, success: false, error: message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case 'api:fs:exec': {
|
case 'api:fs:exec': {
|
||||||
const { commands, cwd } = (payload as { commands: string[]; cwd: string }) || {};
|
const { commands, cwd } = (payload as { commands: string[]; cwd: string }) || {};
|
||||||
if (!Array.isArray(commands) || commands.length === 0) {
|
if (!Array.isArray(commands) || commands.length === 0) {
|
||||||
|
|||||||
@@ -61,6 +61,20 @@ export const createVSCodeFilesAPI = (): FilesAPI => ({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async delete(path: string): Promise<{ success: boolean }> {
|
||||||
|
const target = normalizePath(path);
|
||||||
|
const data = await sendBridgeMessage<{ success: boolean }>('api:fs:delete', { path: target });
|
||||||
|
return { success: Boolean(data?.success) };
|
||||||
|
},
|
||||||
|
|
||||||
|
async rename(oldPath: string, newPath: string): Promise<{ success: boolean; path: string }> {
|
||||||
|
const data = await sendBridgeMessage<{ success: boolean; path: string }>('api:fs:rename', { oldPath, newPath });
|
||||||
|
return {
|
||||||
|
success: Boolean(data?.success),
|
||||||
|
path: typeof data?.path === 'string' ? normalizePath(data.path) : newPath,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
async readFile(path: string): Promise<{ content: string; path: string }> {
|
async readFile(path: string): Promise<{ content: string; path: string }> {
|
||||||
const target = normalizePath(path);
|
const target = normalizePath(path);
|
||||||
const data = await sendBridgeMessage<{ content: string; path: string }>('api:fs:read', { path: target });
|
const data = await sendBridgeMessage<{ content: string; path: string }>('api:fs:read', { path: target });
|
||||||
|
|||||||
Reference in New Issue
Block a user