Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions src/crates/core/src/infrastructure/filesystem/file_operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,16 +303,21 @@ impl FileOperationService {
}

pub async fn copy_file(&self, from: &str, to: &str) -> BitFunResult<u64> {
let from_path = Path::new(from);
let to_path = Path::new(to);
let from_trim = from.trim();
let to_trim = to.trim();
let from_path = Path::new(from_trim);
let to_path = Path::new(to_trim);

self.validate_file_access(from_path, false).await?;
self.validate_file_access(to_path, true).await?;

if !from_path.exists() {
// Use symlink_metadata (do not follow symlinks). `Path::exists()` follows links and
// returns false for broken symlinks and some reparse-point / cloud placeholder edge cases
// even though the name is listed in the directory.
if fs::symlink_metadata(from_path).await.is_err() {
return Err(BitFunError::service(format!(
"Source file does not exist: {}",
from
from_trim
)));
}

Expand All @@ -336,16 +341,18 @@ impl FileOperationService {
}

pub async fn move_file(&self, from: &str, to: &str) -> BitFunResult<()> {
let from_path = Path::new(from);
let to_path = Path::new(to);
let from_trim = from.trim();
let to_trim = to.trim();
let from_path = Path::new(from_trim);
let to_path = Path::new(to_trim);

self.validate_file_access(from_path, true).await?;
self.validate_file_access(to_path, true).await?;

if !from_path.exists() {
if fs::symlink_metadata(from_path).await.is_err() {
return Err(BitFunError::service(format!(
"Source file does not exist: {}",
from
from_trim
)));
}

Expand Down
97 changes: 64 additions & 33 deletions src/web-ui/src/app/components/SceneBar/SceneBar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -96,26 +96,43 @@ $_tab-v-margin: 6px; // symmetric top/bottom gap inside SceneBar
}
}

&:only-child,
&:only-child#{&}--active {
cursor: default;
}

// Active state
// Active state — same fill as .bitfun-nav-panel__item.is-active (no left rail on horizontal tabs)
&--active {
color: var(--color-text-primary);
font-weight: 500;
background: var(--element-bg-base);
background: var(--element-bg-soft);

.bitfun-scene-tab__icon {
color: var(--color-primary);
color: var(--color-text-primary);
opacity: 1;
}

&:hover .bitfun-scene-tab__close {
opacity: 1;
}
}

// Single tab: no pill background (same whether active or not).
&:only-child {
cursor: default;
background: transparent;
box-shadow: none;

&:hover {
background: transparent;
color: var(--color-text-secondary);
}

&.bitfun-scene-tab--active {
background: transparent;
box-shadow: none;

&:hover {
background: transparent;
}
}
}

&:focus-visible {
outline: 2px solid var(--color-accent-500);
outline-offset: -2px;
Expand Down Expand Up @@ -226,47 +243,61 @@ $_tab-v-margin: 6px; // symmetric top/bottom gap inside SceneBar
opacity: 0.94;
}

// Light themes: inactive tabs use tiered bg.* tokens; active is a gentle step brighter
// (not full scene white + cast shadow) so it does not read as a separate floating card.
// Light themes: inactive = former active (soft tint on chrome); active = former inactive (lifted pill + edge).
:root[data-theme-type='light'] .bitfun-scene-tab {
background: var(--color-bg-tertiary);
color: var(--color-text-secondary);
background: var(--element-bg-soft);
color: var(--color-text-primary);
box-shadow: none;

&:hover {
background: var(--color-bg-quaternary);
color: var(--color-text-secondary);
background: var(--element-bg-medium);
color: var(--color-text-primary);
box-shadow: none;
}

.bitfun-scene-tab__icon {
opacity: 1;
}

&--active {
color: var(--color-text-primary);
// Softer than full scene white + drop shadow: stays in the same family as idle tabs
background: color-mix(in srgb, var(--color-bg-scene) 72%, var(--color-bg-tertiary));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--border-subtle) 42%, transparent);
background: var(--color-bg-secondary);
color: color-mix(in srgb, var(--color-text-primary) 86%, var(--color-text-muted));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--border-subtle) 38%, transparent);

&:hover {
background: color-mix(in srgb, var(--color-bg-scene) 76%, var(--color-bg-tertiary));
background: color-mix(in srgb, var(--color-bg-secondary) 82%, var(--color-bg-quaternary));
color: color-mix(in srgb, var(--color-text-primary) 90%, var(--color-text-muted));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--border-subtle) 48%, transparent);
}
}

// Single tab: still a visible chip when inactive; when active, align with scene card.
&:only-child:not(.bitfun-scene-tab--active) {
background: var(--color-bg-tertiary);
box-shadow: none;
color: var(--color-text-secondary);
.bitfun-scene-tab__icon {
color: inherit;
}

.bitfun-scene-tab__subtitle {
color: var(--color-text-muted);
}
}

&:only-child.bitfun-scene-tab--active {
background: color-mix(in srgb, var(--color-bg-scene) 72%, var(--color-bg-tertiary));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--border-subtle) 42%, transparent);
color: var(--color-text-primary);
&:only-child {
background: transparent;
box-shadow: none;
color: color-mix(in srgb, var(--color-text-primary) 86%, var(--color-text-muted));

&:hover {
background: color-mix(in srgb, var(--color-bg-scene) 76%, var(--color-bg-tertiary));
background: transparent;
color: color-mix(in srgb, var(--color-text-primary) 90%, var(--color-text-muted));
}
}

&:only-child:not(.bitfun-scene-tab--active):hover {
background: var(--color-bg-quaternary);
&.bitfun-scene-tab--active {
background: transparent;
box-shadow: none;
color: var(--color-text-primary);

&:hover {
background: transparent;
}
}
}
}

Expand Down
42 changes: 11 additions & 31 deletions src/web-ui/src/app/components/panels/FilesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
type FileExplorerToolbarHandlers,
} from '@/tools/file-system';
import { Search, IconButton, Tooltip } from '@/component-library';
import { useFileTreeGitSync } from '@/tools/file-system/hooks/useFileTreeGitSync';
import { FileSearchResults } from '@/tools/file-system/components/FileSearchResults';
import { useFileSearch } from '@/hooks';
import { workspaceAPI } from '@/infrastructure/api';
Expand All @@ -24,6 +23,7 @@ import { InputDialog, CubeLoading } from '@/component-library';
import { openFileInBestTarget } from '@/shared/utils/tabUtils';
import { PanelHeader } from './base';
import { createLogger } from '@/shared/utils/logger';
import { basenamePath, dirnameAbsolutePath, normalizeLocalPathForRename, replaceBasename } from '@/shared/utils/pathUtils';
import { workspaceManager } from '@/infrastructure/services/business/workspaceManager';
import { isRemoteWorkspace } from '@/shared/types';
import {
Expand Down Expand Up @@ -108,7 +108,6 @@ const FilesPanel: React.FC<FilesPanelProps> = ({
selectFile,
expandFolder,
expandFolderLazy,
setFileTree
} = useFileSystem({
rootPath: workspacePath,
autoLoad: true,
Expand All @@ -117,22 +116,10 @@ const FilesPanel: React.FC<FilesPanelProps> = ({
enableAutoWatch: true,
enableLazyLoad: true
});
const handleTreeUpdate = useCallback((updatedTree: FileSystemNode[]) => {
log.debug('File tree updated', { nodeCount: updatedTree.length });
setFileTree(updatedTree);
}, [setFileTree]);

const handleNodeExpandLazy = useCallback((path: string) => {
expandFolderLazy(path);
}, [expandFolderLazy]);

useFileTreeGitSync({
workspacePath,
fileTree,
onTreeUpdate: handleTreeUpdate,
autoRefresh: true
});

const prevWorkspacePathRef = useRef<string | undefined>(workspacePath);
useEffect(() => {
if (prevWorkspacePathRef.current !== undefined && prevWorkspacePathRef.current !== workspacePath) {
Expand Down Expand Up @@ -235,22 +222,19 @@ const FilesPanel: React.FC<FilesPanelProps> = ({
}, []);

const handleExecuteRename = useCallback(async (oldPath: string, newName: string) => {
const isWindows = oldPath.includes('\\');
const separator = isWindows ? '\\' : '/';
const pathParts = oldPath.split(separator);
const oldName = pathParts[pathParts.length - 1];

if (newName === oldName) {
const normalizedOld = normalizeLocalPathForRename(oldPath);
const oldName = basenamePath(normalizedOld);

if (newName.trim() === oldName) {
setRenamingPath(null);
return;
}

pathParts[pathParts.length - 1] = newName;
const newPath = pathParts.join(separator);


const newPath = replaceBasename(normalizedOld, newName.trim());

try {
await workspaceAPI.renameFile(oldPath, newPath);
log.info('File renamed', { oldPath, newPath });
await workspaceAPI.renameFile(normalizedOld, newPath);
log.info('File renamed', { oldPath: normalizedOld, newPath });
setRenamingPath(null);
loadFileTree(workspacePath || '', true);
} catch (error) {
Expand Down Expand Up @@ -367,11 +351,7 @@ const FilesPanel: React.FC<FilesPanelProps> = ({
}, [workspacePath, expandFolder, expandedFolders]);

const getParentDirectory = useCallback((filePath: string): string => {
const isWindows = filePath.includes('\\');
const separator = isWindows ? '\\' : '/';
const parts = filePath.split(separator);
parts.pop();
return parts.join(separator);
return dirnameAbsolutePath(filePath);
}, []);

const findNode = useCallback((nodes: FileSystemNode[], path: string): FileSystemNode | null => {
Expand Down
21 changes: 17 additions & 4 deletions src/web-ui/src/flow_chat/components/FileMentionPicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -247,16 +247,29 @@
}

&__item--selected {
background: color-mix(in srgb, var(--color-accent-primary) 14%, white 86%);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-accent-primary) 30%, transparent);
// Fallback when color-mix is unsupported; keeps keyboard highlight visible on light surfaces
background: var(--element-bg-medium);
background: color-mix(
in srgb,
var(--color-accent-primary, var(--color-accent-500)) 22%,
var(--color-bg-elevated) 78%
);

.file-mention-picker__item-name {
color: color-mix(in srgb, var(--color-accent-primary) 78%, var(--color-text-primary));
color: color-mix(
in srgb,
var(--color-accent-primary, var(--color-accent-500)) 72%,
var(--color-text-primary)
);
}

.file-mention-picker__icon,
.file-mention-picker__expand-icon {
color: color-mix(in srgb, var(--color-accent-primary) 70%, var(--color-text-primary));
color: color-mix(
in srgb,
var(--color-accent-primary, var(--color-accent-500)) 65%,
var(--color-text-primary)
);
opacity: 0.92;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/web-ui/src/infrastructure/theme/core/ThemeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ export class ThemeService {
root.style.setProperty('--color-primary', primaryAccent);
root.style.setProperty('--color-primary-hover', primaryHover);
root.style.setProperty('--color-accent', primaryAccent);
root.style.setProperty('--color-accent-primary', primaryAccent);
const primaryRgb = accentColorToRgbChannels(primaryAccent);
if (primaryRgb) {
root.style.setProperty('--color-primary-rgb', primaryRgb);
Expand Down
3 changes: 2 additions & 1 deletion src/web-ui/src/locales/en-US/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,8 @@
"renameSymbol": "Rename Symbol",
"quickFix": "Quick Fix...",
"goToSymbol": "Go to Symbol...",
"highlightAllOccurrences": "Highlight All Occurrences"
"highlightAllOccurrences": "Highlight All Occurrences",
"addToChat": "Add to Chat Context"
},
"document": {
"delete": "Delete Document"
Expand Down
3 changes: 2 additions & 1 deletion src/web-ui/src/locales/zh-CN/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,8 @@
"renameSymbol": "重命名符号",
"quickFix": "快速修复...",
"goToSymbol": "转到符号...",
"highlightAllOccurrences": "高亮所有出现位置"
"highlightAllOccurrences": "高亮所有出现位置",
"addToChat": "添加到聊天上下文"
},
"document": {
"delete": "删除文档"
Expand Down
27 changes: 23 additions & 4 deletions src/web-ui/src/shared/context-menu-system/core/ContextResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export class ContextResolver {


let cursorPosition: { line: number; column: number } | undefined;
let selectionRange: EditorContext['selectionRange'];

try {
const monacoGlobal = (window as any).monaco;
Expand Down Expand Up @@ -308,15 +309,32 @@ export class ContextResolver {
};
} else {

const selection = targetEditor.getSelection?.();
if (selection) {
const monacoSelection = targetEditor.getSelection?.();
if (monacoSelection) {
cursorPosition = {
line: selection.startLineNumber,
column: selection.startColumn
line: monacoSelection.startLineNumber,
column: monacoSelection.startColumn
};
}
}
}

const monacoSelection = targetEditor.getSelection?.();
if (monacoSelection) {
const isEmpty =
typeof monacoSelection.isEmpty === 'function'
? monacoSelection.isEmpty()
: monacoSelection.startLineNumber === monacoSelection.endLineNumber &&
monacoSelection.startColumn === monacoSelection.endColumn;
if (!isEmpty) {
selectionRange = {
startLine: monacoSelection.startLineNumber,
endLine: monacoSelection.endLineNumber,
startColumn: monacoSelection.startColumn,
endColumn: monacoSelection.endColumn
};
}
}
}
}

Expand All @@ -342,6 +360,7 @@ export class ContextResolver {
filePath,
cursorPosition,
selectedText,
selectionRange,
isReadOnly
};
}
Expand Down
Loading
Loading