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
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@modelcontextprotocol/sdk": "^1.27.1",
"@supabase/supabase-js": "^2.90.1",
"@tauri-apps/api": "^2.10.1",
"@tauri-apps/plugin-dialog": "^2.7.0",
"@types/dagre": "^0.7.53",
"@types/papaparse": "^5.5.2",
"@ui-tars/sdk": "^1.2.3",
Expand Down
67 changes: 67 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ reqwest = { version = "0.12", default-features = false, features = ["blocking",
ctrlc = "3.4"
tauri-plugin-global-shortcut = "2"
tauri-plugin-single-instance = "2"
tauri-plugin-dialog = "2"

[profile.release]
panic = "abort"
Expand Down
6 changes: 5 additions & 1 deletion src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
"identifier": "default",
"description": "Default capabilities for Code Agent",
"windows": ["main"],
"remote": {
"urls": ["http://localhost:8180/*"]
},
"permissions": [
"core:default",
"updater:default",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister"
"global-shortcut:allow-unregister",
"dialog:allow-open"
]
}
1 change: 1 addition & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ fn main() {
}))
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_dialog::init())
.manage(AppState::default())
.manage(NativeDesktopState::default())
.invoke_handler(tauri::generate_handler![
Expand Down
11 changes: 7 additions & 4 deletions src/renderer/components/TaskPanel/WorkingFolder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useComposerStore } from '../../stores/composerStore';
import { useSessionStore } from '../../stores/sessionStore';
import { useI18n } from '../../hooks/useI18n';
import { IPC_CHANNELS } from '@shared/ipc';
import { isWebMode } from '../../utils/platform';
import { isWebMode, isTauriMode } from '../../utils/platform';
import ipcService from '../../services/ipcService';

interface FileInfo {
Expand Down Expand Up @@ -92,9 +92,12 @@ export const WorkingFolder: React.FC = () => {
}
return;
}
const result = await ipcService.invoke(IPC_CHANNELS.WORKSPACE_SELECT_DIRECTORY);
if (result) {
setWorkingDirectory(result);
if (isTauriMode()) {
const { open } = await import('@tauri-apps/plugin-dialog');
const result = await open({ directory: true, multiple: false, title: '选择工作目录' });
if (typeof result === 'string') {
setWorkingDirectory(result);
}
}
} catch (error) {
console.error('Failed to select directory:', error);
Expand Down
10 changes: 5 additions & 5 deletions src/renderer/components/TitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import { useAppStore } from '../stores/appStore';
import { useComposerStore } from '../stores/composerStore';
import { useDisclosure } from '../hooks/useDisclosure';
import { PanelLeftClose, PanelLeft, PanelRightClose, PanelRight, FolderOpen, FolderTree, GitBranch, FlaskConical, Monitor, Clock3, Sparkles } from 'lucide-react';
import { isWebMode } from '../utils/platform';
import { IPC_CHANNELS } from '@shared/ipc';
import ipcService from '../services/ipcService';
import { isWebMode, isTauriMode } from '../utils/platform';
import { IconButton } from './primitives';
// 奶酪图标组件
const CheeseIcon: React.FC<{ className?: string }> = ({ className }) => (
Expand Down Expand Up @@ -63,8 +61,10 @@ export const TitleBar: React.FC = () => {
let selectedPath: string | null = null;
if (isWebMode()) {
selectedPath = window.prompt('输入工作目录路径', effectiveWorkingDirectory || '')?.trim() || null;
} else {
selectedPath = await ipcService.invoke(IPC_CHANNELS.WORKSPACE_SELECT_DIRECTORY);
} else if (isTauriMode()) {
const { open } = await import('@tauri-apps/plugin-dialog');
const result = await open({ directory: true, multiple: false, title: '选择工作目录' });
selectedPath = typeof result === 'string' ? result : null;
}
if (selectedPath) {
setComposerWorkingDirectory(selectedPath);
Expand Down
22 changes: 10 additions & 12 deletions src/renderer/components/features/explorer/FileExplorerPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ const FileTreeNode: React.FC<{
// ── TabBar ──

const TabBar: React.FC = () => {
const { tabs, activeTabId, setActiveTab, closeTab, addTab } = useExplorerStore();
const { tabs, activeTabId, setActiveTab, closeTab, openOrFocusTab } = useExplorerStore();
const workingDirectory = useAppStore((s) => s.workingDirectory);

const handleAddTab = useCallback(async () => {
Expand All @@ -379,16 +379,16 @@ const TabBar: React.FC = () => {
if (response?.success && response.data) {
const dirPath = response.data;
const label = dirPath.split('/').pop() || dirPath;
addTab(dirPath, label);
openOrFocusTab(dirPath, label);
}
} catch {
// Fallback: use working directory
if (workingDirectory) {
const label = workingDirectory.split('/').pop() || 'Root';
addTab(workingDirectory, label);
openOrFocusTab(workingDirectory, label);
}
}
}, [addTab, workingDirectory]);
}, [openOrFocusTab, workingDirectory]);

return (
<div className="flex items-center gap-0.5 px-1 overflow-x-auto scrollbar-none">
Expand Down Expand Up @@ -432,18 +432,16 @@ interface FileExplorerPanelProps {
export const FileExplorerPanel: React.FC<FileExplorerPanelProps> = ({ onClose }) => {
const {
tabs, activeTabId, dirContents, pendingCreate,
addTab, setDirContents, setLoading, startCreate, cancelCreate,
openOrFocusTab, setDirContents, setLoading, startCreate, cancelCreate,
} = useExplorerStore();
const workingDirectory = useAppStore((s) => s.workingDirectory);
const initRef = useRef(false);

// Auto-add working directory tab on first mount
// Sync active tab with session workingDirectory: open it or focus if already open.
useEffect(() => {
if (initRef.current || tabs.length > 0 || !workingDirectory) return;
initRef.current = true;
if (!workingDirectory) return;
const label = workingDirectory.split('/').pop() || 'Project';
addTab(workingDirectory, label);
}, [workingDirectory, tabs.length, addTab]);
openOrFocusTab(workingDirectory, label);
}, [workingDirectory, openOrFocusTab]);

// Load root directory contents when active tab changes
const activeTab = tabs.find((t) => t.id === activeTabId);
Expand Down Expand Up @@ -532,7 +530,7 @@ export const FileExplorerPanel: React.FC<FileExplorerPanelProps> = ({ onClose })
onClick={() => {
if (workingDirectory) {
const label = workingDirectory.split('/').pop() || 'Project';
addTab(workingDirectory, label);
openOrFocusTab(workingDirectory, label);
}
}}
className="text-xs text-primary-400 hover:text-primary-300"
Expand Down
15 changes: 9 additions & 6 deletions src/renderer/components/features/lab/gpt1/RealModePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
XCircle,
} from 'lucide-react';
import { IPC_CHANNELS } from '../../../../../shared/ipc';
import { isWebMode } from '../../../../utils/platform';
import { isWebMode, isTauriMode } from '../../../../utils/platform';
import ipcService from '../../../../services/ipcService';
import type {
PythonEnvStatus,
Expand Down Expand Up @@ -198,11 +198,14 @@ export const RealModePanel: React.FC = () => {
addLog('info', `已选择项目目录: ${manualPath.trim()}`);
return;
}
const selectedPath = await ipcService.invoke(IPC_CHANNELS.WORKSPACE_SELECT_DIRECTORY);
if (selectedPath) {
setProjectPath(selectedPath);
setProjectUIStatus('downloaded');
addLog('info', `已选择项目目录: ${selectedPath}`);
if (isTauriMode()) {
const { open } = await import('@tauri-apps/plugin-dialog');
const result = await open({ directory: true, multiple: false, title: '选择项目目录' });
if (typeof result === 'string') {
setProjectPath(result);
setProjectUIStatus('downloaded');
addLog('info', `已选择项目目录: ${result}`);
}
}
} catch (error) {
addLog('error', `选择目录失败: ${error instanceof Error ? error.message : String(error)}`);
Expand Down
18 changes: 18 additions & 0 deletions src/renderer/stores/explorerStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface ExplorerState {

// Actions
addTab: (rootPath: string, label: string) => void;
openOrFocusTab: (rootPath: string, label: string) => void;
closeTab: (id: string) => void;
setActiveTab: (id: string) => void;
setDirContents: (path: string, contents: FileInfo[]) => void;
Expand Down Expand Up @@ -61,6 +62,23 @@ export const useExplorerStore = create<ExplorerState>((set) => ({
}));
},

openOrFocusTab: (rootPath, label) => {
set((state) => {
const existing = state.tabs.find((t) => t.rootPath === rootPath);
if (existing) {
return state.activeTabId === existing.id
? state
: { ...state, activeTabId: existing.id };
}
const id = `tab-${++tabCounter}`;
return {
...state,
tabs: [...state.tabs, { id, rootPath, label }],
activeTabId: id,
};
});
},

closeTab: (id) => {
set((state) => {
const newTabs = state.tabs.filter((t) => t.id !== id);
Expand Down
56 changes: 56 additions & 0 deletions tests/unit/renderer/explorerStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { useExplorerStore } from '../../../src/renderer/stores/explorerStore';

describe('explorerStore.openOrFocusTab', () => {
beforeEach(() => {
useExplorerStore.getState().reset();
});

it('creates a new tab when rootPath is not open', () => {
const { openOrFocusTab } = useExplorerStore.getState();
openOrFocusTab('/tmp/a', 'a');

const { tabs, activeTabId } = useExplorerStore.getState();
expect(tabs).toHaveLength(1);
expect(tabs[0].rootPath).toBe('/tmp/a');
expect(tabs[0].label).toBe('a');
expect(activeTabId).toBe(tabs[0].id);
});

it('activates existing tab without duplicating when rootPath matches', () => {
const { openOrFocusTab } = useExplorerStore.getState();
openOrFocusTab('/tmp/a', 'a');
openOrFocusTab('/tmp/b', 'b');
const tabAId = useExplorerStore.getState().tabs[0].id;

// Switch back to /tmp/a — should focus existing, not add a third tab
openOrFocusTab('/tmp/a', 'a');

const { tabs, activeTabId } = useExplorerStore.getState();
expect(tabs).toHaveLength(2);
expect(activeTabId).toBe(tabAId);
});

it('is a no-op when rootPath already matches the active tab', () => {
const { openOrFocusTab } = useExplorerStore.getState();
openOrFocusTab('/tmp/a', 'a');
const stateBefore = useExplorerStore.getState();

openOrFocusTab('/tmp/a', 'a');

const stateAfter = useExplorerStore.getState();
expect(stateAfter.tabs).toBe(stateBefore.tabs);
expect(stateAfter.activeTabId).toBe(stateBefore.activeTabId);
});

it('keeps previously opened tabs when adding a new one', () => {
const { openOrFocusTab } = useExplorerStore.getState();
openOrFocusTab('/tmp/a', 'a');
openOrFocusTab('/tmp/b', 'b');
openOrFocusTab('/tmp/c', 'c');

const { tabs, activeTabId } = useExplorerStore.getState();
expect(tabs.map((t) => t.rootPath)).toEqual(['/tmp/a', '/tmp/b', '/tmp/c']);
expect(activeTabId).toBe(tabs[2].id);
});
});
Loading