feat: introduce useEffectiveDirectory hook and adopt across views (#226)

Add a centralized useEffectiveDirectory hook to resolve the active directory
Use the hook across DiffView, FilesView, GitView, and TerminalView
Fix list styling by rendering ul/ol bullets outside
This commit is contained in:
Bohdan Triapitsyn
2026-01-27 11:57:56 +02:00
committed by GitHub
parent 8b515346bc
commit b861e4abf1
8 changed files with 70 additions and 79 deletions
+1 -1
View File
@@ -2994,7 +2994,7 @@ dependencies = [
[[package]] [[package]]
name = "openchamber-desktop" name = "openchamber-desktop"
version = "1.5.7" version = "1.5.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@@ -53,6 +53,7 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMenti
return null; return null;
} }
// Render content with optional agent mention link
const renderContent = () => { const renderContent = () => {
if (!agentMention?.token || !textContent.includes(agentMention.token)) { if (!agentMention?.token || !textContent.includes(agentMention.token)) {
return textContent; return textContent;
@@ -67,7 +68,8 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMenti
href={buildMentionUrl(agentMention.name)} href={buildMentionUrl(agentMention.name)}
className="text-primary hover:underline" className="text-primary hover:underline"
target="_blank" target="_blank"
rel="noreferrer" rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
> >
{agentMention.token} {agentMention.token}
</a> </a>
@@ -79,7 +81,7 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMenti
return ( return (
<div <div
className={cn( className={cn(
"font-sans typography-markdown whitespace-pre-wrap", "break-words whitespace-pre-wrap font-sans typography-markdown",
!isExpanded && "line-clamp-3", !isExpanded && "line-clamp-3",
(isTruncated || isExpanded) && "cursor-pointer" (isTruncated || isExpanded) && "cursor-pointer"
)} )}
@@ -87,9 +89,7 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMenti
onClick={handleClick} onClick={handleClick}
key={part.id || `${messageId}-user-text`} key={part.id || `${messageId}-user-text`}
> >
<span className="text-foreground/90"> {renderContent()}
{renderContent()}
</span>
</div> </div>
); );
}; };
+2 -20
View File
@@ -1,9 +1,8 @@
import React from 'react'; import React from 'react';
import { RiArrowDownSLine, RiArrowRightSLine, RiGitCommitLine, RiLoader4Line, RiTextWrap } from '@remixicon/react'; import { RiArrowDownSLine, RiArrowRightSLine, RiGitCommitLine, RiLoader4Line, RiTextWrap } from '@remixicon/react';
import { useSessionStore } from '@/stores/useSessionStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { useGitStore, useGitStatus, useIsGitRepo, useGitFileCount } from '@/stores/useGitStore'; import { useGitStore, useGitStatus, useIsGitRepo, useGitFileCount } from '@/stores/useGitStore';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { GitStatus } from '@/lib/api/types'; import type { GitStatus } from '@/lib/api/types';
@@ -793,17 +792,6 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
); );
}); });
const useEffectiveDirectory = () => {
const { currentSessionId, sessions, worktreeMetadata: worktreeMap } = useSessionStore();
const { currentDirectory: fallbackDirectory } = useDirectoryStore();
const worktreeMetadata = currentSessionId ? worktreeMap.get(currentSessionId) ?? undefined : undefined;
const currentSession = sessions.find((session) => session.id === currentSessionId);
const sessionDirectory = (currentSession as Record<string, unknown>)?.directory as string | undefined;
return worktreeMetadata?.path ?? sessionDirectory ?? fallbackDirectory ?? undefined;
};
export const DiffView: React.FC = () => { export const DiffView: React.FC = () => {
const { git } = useRuntimeAPIs(); const { git } = useRuntimeAPIs();
const effectiveDirectory = useEffectiveDirectory(); const effectiveDirectory = useEffectiveDirectory();
@@ -1246,13 +1234,7 @@ export const DiffView: React.FC = () => {
// eslint-disable-next-line react-refresh/only-export-components // eslint-disable-next-line react-refresh/only-export-components
export const useDiffFileCount = (): number => { export const useDiffFileCount = (): number => {
const { git } = useRuntimeAPIs(); const { git } = useRuntimeAPIs();
const { currentSessionId, sessions, worktreeMetadata: worktreeMap } = useSessionStore(); const effectiveDirectory = useEffectiveDirectory();
const { currentDirectory: fallbackDirectory } = useDirectoryStore();
const worktreeMetadata = currentSessionId ? worktreeMap.get(currentSessionId) ?? undefined : undefined;
const currentSession = sessions.find((session) => session.id === currentSessionId);
const sessionDirectory = (currentSession as Record<string, unknown>)?.directory as string | undefined;
const effectiveDirectory = worktreeMetadata?.path ?? sessionDirectory ?? fallbackDirectory ?? undefined;
const { setActiveDirectory, fetchStatus } = useGitStore(); const { setActiveDirectory, fetchStatus } = useGitStore();
const fileCount = useGitFileCount(effectiveDirectory ?? null); const fileCount = useGitFileCount(effectiveDirectory ?? null);
+2 -14
View File
@@ -63,11 +63,11 @@ import { useSessionStore } from '@/stores/useSessionStore';
import { useConfigStore } from '@/stores/useConfigStore'; import { useConfigStore } from '@/stores/useConfigStore';
import { useContextStore } from '@/stores/contextStore'; import { useContextStore } from '@/stores/contextStore';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { opencodeClient } from '@/lib/opencode/client'; import { opencodeClient } from '@/lib/opencode/client';
import { useDirectoryShowHidden } from '@/lib/directoryShowHidden'; import { useDirectoryShowHidden } from '@/lib/directoryShowHidden';
import { useFilesViewShowGitignored } from '@/lib/filesViewShowGitignored'; import { useFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
type FileNode = { type FileNode = {
name: string; name: string;
@@ -101,18 +101,6 @@ const shouldIgnorePath = (path: string): boolean => {
return normalized === 'node_modules' || normalized.endsWith('/node_modules') || normalized.includes('/node_modules/'); return normalized === 'node_modules' || normalized.endsWith('/node_modules') || normalized.includes('/node_modules/');
}; };
const useEffectiveDirectory = () => {
const { currentSessionId, sessions, worktreeMetadata: worktreeMap } = useSessionStore();
const { currentDirectory: fallbackDirectory } = useDirectoryStore();
const worktreeMetadata = currentSessionId ? worktreeMap.get(currentSessionId) ?? undefined : undefined;
const currentSession = sessions.find((session) => session.id === currentSessionId);
type SessionWithDirectory = { directory?: string };
const sessionDirectory = (currentSession as unknown as SessionWithDirectory | undefined)?.directory;
return worktreeMetadata?.path ?? sessionDirectory ?? fallbackDirectory ?? '';
};
const MAX_VIEW_CHARS = 200_000; const MAX_VIEW_CHARS = 200_000;
const CODE_EXTENSIONS = new Set([ const CODE_EXTENSIONS = new Set([
@@ -263,7 +251,7 @@ export const FilesView: React.FC = () => {
const showHidden = useDirectoryShowHidden(); const showHidden = useDirectoryShowHidden();
const showGitignored = useFilesViewShowGitignored(); const showGitignored = useFilesViewShowGitignored();
const currentDirectory = useEffectiveDirectory(); const currentDirectory = useEffectiveDirectory() ?? '';
const root = normalizePath(currentDirectory); const root = normalizePath(currentDirectory);
const searchFiles = useFileSearchStore((state) => state.searchFiles); const searchFiles = useFileSearchStore((state) => state.searchFiles);
+2 -18
View File
@@ -4,8 +4,8 @@ import { useConfigStore } from '@/stores/useConfigStore';
import { useFireworksCelebration } from '@/contexts/FireworksContext'; import { useFireworksCelebration } from '@/contexts/FireworksContext';
import type { GitIdentityProfile, CommitFileEntry } from '@/lib/api/types'; import type { GitIdentityProfile, CommitFileEntry } from '@/lib/api/types';
import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore'; import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useProjectsStore } from '@/stores/useProjectsStore'; import { useProjectsStore } from '@/stores/useProjectsStore';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { import {
useGitStore, useGitStore,
useGitStatus, useGitStatus,
@@ -31,7 +31,7 @@ import {
CommandItem, CommandItem,
CommandList, CommandList,
} from '@/components/ui/command'; } from '@/components/ui/command';
import type { Session } from '@opencode-ai/sdk/v2';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
@@ -182,22 +182,6 @@ const matchGitmojiFromSubject = (subject: string, gitmojis: GitmojiEntry[]): Git
const gitViewSnapshots = new Map<string, GitViewSnapshot>(); const gitViewSnapshots = new Map<string, GitViewSnapshot>();
const useEffectiveDirectory = () => {
const { currentSessionId, sessions, worktreeMetadata: worktreeMap } = useSessionStore();
const { currentDirectory: fallbackDirectory } = useDirectoryStore();
const worktreeMetadata = currentSessionId
? worktreeMap.get(currentSessionId) ?? undefined
: undefined;
const currentSession = sessions.find((session) => session.id === currentSessionId);
type SessionWithDirectory = Session & { directory?: string };
const sessionDirectory: string | undefined = (
currentSession as SessionWithDirectory | undefined
)?.directory;
return worktreeMetadata?.path ?? sessionDirectory ?? fallbackDirectory ?? undefined;
};
export const GitView: React.FC = () => { export const GitView: React.FC = () => {
const { git } = useRuntimeAPIs(); const { git } = useRuntimeAPIs();
const currentDirectory = useEffectiveDirectory(); const currentDirectory = useEffectiveDirectory();
@@ -4,6 +4,7 @@ import { RiAlertLine, RiArrowDownLine, RiArrowGoBackLine, RiArrowLeftLine, RiArr
import { useSessionStore } from '@/stores/useSessionStore'; import { useSessionStore } from '@/stores/useSessionStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useTerminalStore } from '@/stores/useTerminalStore'; import { useTerminalStore } from '@/stores/useTerminalStore';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { type TerminalStreamEvent } from '@/lib/api/types'; import { type TerminalStreamEvent } from '@/lib/api/types';
import { useThemeSystem } from '@/contexts/useThemeSystem'; import { useThemeSystem } from '@/contexts/useThemeSystem';
import { useFontPreferences } from '@/hooks/useFontPreferences'; import { useFontPreferences } from '@/hooks/useFontPreferences';
@@ -79,23 +80,11 @@ export const TerminalView: React.FC = () => {
const { monoFont } = useFontPreferences(); const { monoFont } = useFontPreferences();
const { isMobile, hasTouchInput } = useDeviceInfo(); const { isMobile, hasTouchInput } = useDeviceInfo();
const { currentSessionId, sessions, worktreeMetadata: worktreeMap } = useSessionStore(); const { currentSessionId, newSessionDraft } = useSessionStore();
const worktreeMetadata = currentSessionId ? worktreeMap.get(currentSessionId) ?? undefined : undefined; const hasActiveContext = currentSessionId !== null || newSessionDraft?.open === true;
const sessionDirectory = React.useMemo(() => { const effectiveDirectory = useEffectiveDirectory() ?? null;
if (worktreeMetadata?.path) { const { homeDirectory } = useDirectoryStore();
return worktreeMetadata.path;
}
if (!currentSessionId) return null;
const entry = sessions.find((session) => session.id === currentSessionId);
const directory = typeof (entry as { directory?: string } | undefined)?.directory === 'string'
? (entry as { directory?: string }).directory
: null;
return directory && directory.length > 0 ? directory : null;
}, [currentSessionId, sessions, worktreeMetadata]);
const { currentDirectory: fallbackDirectory, homeDirectory } = useDirectoryStore();
const effectiveDirectory = sessionDirectory || fallbackDirectory || null;
const displayDirectory = React.useMemo(() => { const displayDirectory = React.useMemo(() => {
if (!effectiveDirectory) return ''; if (!effectiveDirectory) return '';
@@ -269,7 +258,7 @@ export const TerminalView: React.FC = () => {
if (!effectiveDirectory) { if (!effectiveDirectory) {
setConnectionError( setConnectionError(
currentSessionId hasActiveContext
? 'No working directory available for terminal.' ? 'No working directory available for terminal.'
: 'Select a session to open the terminal.' : 'Select a session to open the terminal.'
); );
@@ -328,7 +317,7 @@ export const TerminalView: React.FC = () => {
disconnectStream(); disconnectStream();
}; };
}, [ }, [
currentSessionId, hasActiveContext,
effectiveDirectory, effectiveDirectory,
terminalSessionId, terminalSessionId,
removeTerminalSession, removeTerminalSession,
@@ -645,7 +634,7 @@ export const TerminalView: React.FC = () => {
? <RiCircleLine size={20} className="text-amber-400 animate-pulse" /> ? <RiCircleLine size={20} className="text-amber-400 animate-pulse" />
: <RiCircleLine size={20} className="text-muted-foreground" />; : <RiCircleLine size={20} className="text-muted-foreground" />;
if (!currentSessionId) { if (!hasActiveContext) {
return ( return (
<div className="flex h-full items-center justify-center p-4 text-center text-sm text-muted-foreground"> <div className="flex h-full items-center justify-center p-4 text-center text-sm text-muted-foreground">
Select a session to open the terminal Select a session to open the terminal
@@ -0,0 +1,48 @@
import { useSessionStore } from '@/stores/useSessionStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import type { Session } from '@opencode-ai/sdk/v2';
/**
* Hook that resolves the effective working directory for tabs (Git, Diff, Files, Terminal).
*
* Priority order:
* 1. Worktree metadata path (for worktree sessions)
* 2. Session directory (for active sessions)
* 3. Draft session directoryOverride (when creating a new session)
* 4. Fallback directory from DirectoryStore
*
* This ensures that tabs show content from the correct project directory
* even when a draft session is being created.
*/
export const useEffectiveDirectory = (): string | undefined => {
const {
currentSessionId,
sessions,
worktreeMetadata: worktreeMap,
newSessionDraft,
} = useSessionStore();
const { currentDirectory: fallbackDirectory } = useDirectoryStore();
// If we have an active session, use its directory
if (currentSessionId) {
const worktreeMetadata = worktreeMap.get(currentSessionId);
if (worktreeMetadata?.path) {
return worktreeMetadata.path;
}
const currentSession = sessions.find((session) => session.id === currentSessionId);
type SessionWithDirectory = Session & { directory?: string };
const sessionDirectory = (currentSession as SessionWithDirectory | undefined)?.directory;
if (sessionDirectory) {
return sessionDirectory;
}
}
// If a draft session is open, use its directoryOverride
if (newSessionDraft?.open && newSessionDraft.directoryOverride) {
return newSessionDraft.directoryOverride;
}
// Fall back to the global directory
return fallbackDirectory ?? undefined;
};
+2 -2
View File
@@ -572,13 +572,13 @@ html:not(.dark) .chat-scroll {
/* Fix list styling - override Tailwind reset */ /* Fix list styling - override Tailwind reset */
.streamdown-content ul { .streamdown-content ul {
list-style-type: disc; list-style-type: disc;
list-style-position: inside; list-style-position: outside;
padding-left: 1.5em; padding-left: 1.5em;
} }
.streamdown-content ol { .streamdown-content ol {
list-style-type: decimal; list-style-type: decimal;
list-style-position: inside; list-style-position: outside;
padding-left: 1.5em; padding-left: 1.5em;
} }