feat(chats): add managed projectless chat sessions
Create projectless chat sessions under a managed, date-scoped Chats directory and clean abandoned or deleted session folders. Add Chats to sidebar state, startup cache, shared context, and Electron Mini Chat while keeping VS Code project-only. Resolve managed chat directories to one server-side memory owner and document the runtime contracts.
This commit is contained in:
@@ -504,14 +504,16 @@ const renderDraftTitle = (title: string, projectLabel: string | null): React.Rea
|
||||
|
||||
const DraftWelcome: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const draftTarget = useSessionUIStore((state) => state.newSessionDraft.target);
|
||||
const selectedProjectId = useSessionUIStore((state) => state.newSessionDraft.selectedProjectId ?? null);
|
||||
const projectLabel = useProjectsStore(React.useCallback((state) => {
|
||||
if (draftTarget === 'chat') return null;
|
||||
const projectId = selectedProjectId ?? state.activeProjectId;
|
||||
const project = (projectId
|
||||
? state.projects.find((candidate) => candidate.id === projectId)
|
||||
: null) ?? state.projects[0] ?? null;
|
||||
return project ? getProjectDisplayLabel(project) : null;
|
||||
}, [selectedProjectId]));
|
||||
}, [draftTarget, selectedProjectId]));
|
||||
|
||||
return (
|
||||
<div className="oc-draft-center flex min-h-0 flex-1 flex-col items-center justify-center px-6 text-center">
|
||||
|
||||
@@ -326,6 +326,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const setNewSessionDraftTarget = useSessionUIStore((s) => s.setNewSessionDraftTarget);
|
||||
const setDraftPermissionAutoAcceptEnabled = useSessionUIStore((s) => s.setDraftPermissionAutoAcceptEnabled);
|
||||
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
||||
const prepareChatDraftDirectory = useSessionUIStore((s) => s.prepareChatDraftDirectory);
|
||||
const abortPromptSessionId = useSessionUIStore((s) => s.abortPromptSessionId);
|
||||
const clearAbortPrompt = useSessionUIStore((s) => s.clearAbortPrompt);
|
||||
const attachedFiles = useInputStore((s) => s.attachedFiles);
|
||||
@@ -336,6 +337,11 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const pendingPresetSubmit = useInputStore((s) => s.pendingPresetSubmit);
|
||||
const setPendingInputText = useInputStore((s) => s.setPendingInputText);
|
||||
const pendingInputText = useInputStore((s) => s.pendingInputText);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!newSessionDraftOpen || newSessionDraft.target !== 'chat' || message.trim().length === 0) return;
|
||||
void prepareChatDraftDirectory();
|
||||
}, [message, newSessionDraft.target, newSessionDraftOpen, prepareChatDraftDirectory]);
|
||||
const consumePendingSyntheticParts = useInputStore((s) => s.consumePendingSyntheticParts);
|
||||
const acknowledgeSessionAbort = useSessionUIStore((s) => s.acknowledgeSessionAbort);
|
||||
const abortCurrentOperation = React.useCallback(
|
||||
@@ -2382,7 +2388,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!showDraftTargetSelectors || !selectedDraftProject || !selectedDraftDirectory) {
|
||||
if (!showDraftTargetSelectors || !selectedDraftProject || selectedDraftProject.kind === 'chat' || !selectedDraftDirectory) {
|
||||
return;
|
||||
}
|
||||
if (newSessionDraft?.pendingWorktreeRequestId || newSessionDraft?.bootstrapPendingDirectory || newSessionDraft?.preserveDirectoryOverride) {
|
||||
|
||||
@@ -23,6 +23,8 @@ import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { buildSessionTargetOptions } from '@/sync/session-worktree-contract';
|
||||
import { normalizePath } from '../attachments/filePaths';
|
||||
import { CHAT_DRAFT_PROJECT_ID } from '@/lib/chatDirectories';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
/** How long a cached branch list is served before it is refreshed. */
|
||||
const BRANCHES_SWR_TTL_MS = 30_000;
|
||||
@@ -35,6 +37,7 @@ export interface DraftTargetProject {
|
||||
color?: string | null;
|
||||
iconImage?: { mime: string; updatedAt: number; source: 'custom' | 'auto' } | null;
|
||||
iconBackground?: string | null;
|
||||
kind?: 'chat' | 'project';
|
||||
}
|
||||
|
||||
/** A project's display name, falling back to its directory name. */
|
||||
@@ -43,7 +46,15 @@ export function getProjectDisplayLabel(project: { label?: string; path: string }
|
||||
}
|
||||
|
||||
export function useDraftTarget(enabled: boolean) {
|
||||
const projects = useProjectsStore((state) => state.projects) as DraftTargetProject[];
|
||||
const configuredProjects: readonly DraftTargetProject[] = useProjectsStore((state) => state.projects);
|
||||
const { t } = useI18n();
|
||||
const chatProject = React.useMemo<DraftTargetProject>(() => ({
|
||||
id: CHAT_DRAFT_PROJECT_ID,
|
||||
path: '',
|
||||
label: t('layout.mainTab.chat'),
|
||||
kind: 'chat',
|
||||
}), [t]);
|
||||
const projects = React.useMemo(() => [chatProject, ...configuredProjects], [chatProject, configuredProjects]);
|
||||
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
|
||||
const setActiveProjectIdOnly = useProjectsStore((state) => state.setActiveProjectIdOnly);
|
||||
const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft);
|
||||
@@ -53,6 +64,7 @@ export function useDraftTarget(enabled: boolean) {
|
||||
const { git: runtimeGit } = useRuntimeAPIs();
|
||||
|
||||
const selectedDraftProject = React.useMemo(() => {
|
||||
if (newSessionDraft?.target === 'chat') return chatProject;
|
||||
const explicit = newSessionDraft?.selectedProjectId
|
||||
? projects.find((project) => project.id === newSessionDraft.selectedProjectId) ?? null
|
||||
: null;
|
||||
@@ -67,14 +79,16 @@ export function useDraftTarget(enabled: boolean) {
|
||||
return active;
|
||||
}
|
||||
|
||||
return projects[0] ?? null;
|
||||
}, [activeProjectId, newSessionDraft?.selectedProjectId, projects]);
|
||||
return configuredProjects[0] ?? chatProject;
|
||||
}, [activeProjectId, chatProject, configuredProjects, newSessionDraft?.selectedProjectId, newSessionDraft?.target, projects]);
|
||||
|
||||
const selectedDraftProjectPath = React.useMemo(
|
||||
() => normalizePath(selectedDraftProject?.path ?? null),
|
||||
[selectedDraftProject?.path],
|
||||
() => selectedDraftProject?.kind === 'chat' ? null : normalizePath(selectedDraftProject?.path ?? null),
|
||||
[selectedDraftProject?.kind, selectedDraftProject?.path],
|
||||
);
|
||||
const draftProjectLabel = selectedDraftProject ? getProjectDisplayLabel(selectedDraftProject) : null;
|
||||
const draftProjectLabel = selectedDraftProject && selectedDraftProject.kind !== 'chat'
|
||||
? getProjectDisplayLabel(selectedDraftProject)
|
||||
: null;
|
||||
|
||||
const selectedDraftProjectBranches = useGitBranches(selectedDraftProjectPath);
|
||||
const selectedDraftProjectBranchesFetchedAt = useGitStore(
|
||||
@@ -258,6 +272,10 @@ export function useDraftTarget(enabled: boolean) {
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
if (project.kind === 'chat') {
|
||||
setNewSessionDraftTarget({ projectId: CHAT_DRAFT_PROJECT_ID, directoryOverride: null }, { force: true });
|
||||
return;
|
||||
}
|
||||
if (activeProjectId !== projectId) {
|
||||
setActiveProjectIdOnly(projectId);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,9 @@ const getProjectIconColor = (projectColor?: string | null): string | undefined =
|
||||
function ProjectLabel({ project, theme }: { project: DraftTargetProject; theme: Theme }) {
|
||||
const projectIconName = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
|
||||
const iconColor = getProjectIconColor(project.color);
|
||||
const fallbackIcon = projectIconName ? (
|
||||
const fallbackIcon = project.kind === 'chat' ? (
|
||||
<Icon name="chat-4" className="h-3.5 w-3.5 shrink-0 text-muted-foreground/80" />
|
||||
) : projectIconName ? (
|
||||
<Icon name={projectIconName} className="h-3.5 w-3.5 shrink-0" style={iconColor ? { color: iconColor } : undefined} />
|
||||
) : (
|
||||
<Icon name="folder" className="h-3.5 w-3.5 shrink-0 text-muted-foreground/80" style={iconColor ? { color: iconColor } : undefined} />
|
||||
@@ -115,13 +117,15 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
|
||||
className="h-7 min-w-0 w-fit max-w-[42vw] sm:max-w-[18rem] border-transparent bg-transparent px-1.5 hover:bg-transparent data-[popup-open]:bg-transparent"
|
||||
>
|
||||
<SelectValue>
|
||||
{<ProjectLabel project={selectedProject} theme={theme} />}
|
||||
{selectedProject.kind === 'chat'
|
||||
? <span className="truncate">{t('chat.chatInput.chooseProject')}</span>
|
||||
: <ProjectLabel project={selectedProject} theme={theme} />}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent fitContent>
|
||||
{projects.map((project) => (
|
||||
<SelectItem key={project.id} value={project.id} className="max-w-[24rem] truncate">
|
||||
{<ProjectLabel project={project} theme={theme} />}
|
||||
<ProjectLabel project={project} theme={theme} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -195,7 +199,9 @@ export function MobileDraftTargetTriggers(
|
||||
className="inline-flex h-7 min-w-0 max-w-[42vw] flex-shrink cursor-pointer items-center gap-1 rounded-lg px-1.5 typography-micro font-medium text-foreground/80 hover:bg-[var(--interactive-hover)]"
|
||||
onClick={() => onOpenPicker('project')}
|
||||
>
|
||||
{<ProjectLabel project={selectedProject} theme={theme} />}
|
||||
{selectedProject.kind === 'chat'
|
||||
? <span className="truncate">{t('chat.chatInput.chooseProject')}</span>
|
||||
: <ProjectLabel project={selectedProject} theme={theme} />}
|
||||
<Icon name="arrow-down-s" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
{showBranchSelector ? (
|
||||
@@ -275,7 +281,7 @@ export function MobileDraftTargetSheets(
|
||||
onOpenPickerChange(null);
|
||||
}}
|
||||
>
|
||||
<span className="min-w-0 flex-1">{<ProjectLabel project={project} theme={theme} />}</span>
|
||||
<span className="min-w-0 flex-1"><ProjectLabel project={project} theme={theme} /></span>
|
||||
{project.id === selectedProject.id ? (
|
||||
<Icon name="check" className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user