feat(ui): context panel enhancements — resizable panels, drag-and-drop todo ordering, and persistent sizes (#1269)
* fix: remove max-h-80 cap on quick notes textarea so resized height is respected * feat: add drag & drop reordering to project todo items * feat: make todo panel resizable with density-aware sizing * feat: open plan import file picker at project root * feat: persist quick notes and todo panel sizes across sessions * refactor(ui): scale content height with padding in projectnotestodopanel * Update packages/ui/src/components/session/ProjectNotesTodoPanel.tsx Signed-off-by: Erman HAVUÇ <ermanhavuc@gmail.com> * fix(ui): harden context panel resizing and import --------- Signed-off-by: Erman HAVUÇ <ermanhavuc@gmail.com> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
631905764e
commit
6369cf76a7
@@ -2575,6 +2575,9 @@ ipcMain.handle('openchamber:dialog:open', async (event, options) => {
|
|||||||
const browserWindow = BrowserWindow.fromWebContents(event.sender);
|
const browserWindow = BrowserWindow.fromWebContents(event.sender);
|
||||||
const result = await dialog.showOpenDialog(browserWindow || undefined, {
|
const result = await dialog.showOpenDialog(browserWindow || undefined, {
|
||||||
title: typeof options?.title === 'string' ? options.title : undefined,
|
title: typeof options?.title === 'string' ? options.title : undefined,
|
||||||
|
defaultPath: typeof options?.defaultPath === 'string' && options.defaultPath.trim().length > 0
|
||||||
|
? options.defaultPath.trim()
|
||||||
|
: undefined,
|
||||||
filters: Array.isArray(options?.filters)
|
filters: Array.isArray(options?.filters)
|
||||||
? options.filters
|
? options.filters
|
||||||
.filter((filter) => filter && typeof filter === 'object')
|
.filter((filter) => filter && typeof filter === 'object')
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
PointerSensor,
|
||||||
|
closestCenter,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
type DragEndEvent,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
|
||||||
|
import { CSS as DndCSS } from '@dnd-kit/utilities';
|
||||||
import { toast } from '@/components/ui';
|
import { toast } from '@/components/ui';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +32,7 @@ import {
|
|||||||
type OpenChamberProjectTodoItem,
|
type OpenChamberProjectTodoItem,
|
||||||
type ProjectRef,
|
type ProjectRef,
|
||||||
} from '@/lib/openchamberConfig';
|
} from '@/lib/openchamberConfig';
|
||||||
|
import { requestFileAccess } from '@/lib/desktop';
|
||||||
import { generateBranchName } from '@/lib/git/branchNameGenerator';
|
import { generateBranchName } from '@/lib/git/branchNameGenerator';
|
||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
@@ -35,6 +46,25 @@ import { renderMagicPrompt } from '@/lib/magicPrompts';
|
|||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { TodoSendDialog, type TodoSendExecution } from './TodoSendDialog';
|
import { TodoSendDialog, type TodoSendExecution } from './TodoSendDialog';
|
||||||
|
|
||||||
|
const TODO_PANEL_MIN_ITEMS = 5;
|
||||||
|
const TODO_PANEL_MAX_ITEMS = 15;
|
||||||
|
|
||||||
|
const getEffectiveItemHeight = (padding: number) => {
|
||||||
|
const scale = Math.sqrt(padding / 100);
|
||||||
|
const paddingPx = 12 * scale;
|
||||||
|
const contentPx = 24 * scale; // h-6 uses --spacing-6 which also scales with --padding-scale
|
||||||
|
const borderPx = 1;
|
||||||
|
return Math.ceil(paddingPx + contentPx + borderPx);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPanelHeightForItems = (itemCount: number, padding: number) => {
|
||||||
|
const itemHeight = getEffectiveItemHeight(padding);
|
||||||
|
return Math.max(
|
||||||
|
itemHeight * TODO_PANEL_MIN_ITEMS,
|
||||||
|
Math.min(itemHeight * TODO_PANEL_MAX_ITEMS, itemHeight * itemCount)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface ProjectNotesTodoPanelProps {
|
interface ProjectNotesTodoPanelProps {
|
||||||
projectRef: ProjectRef | null;
|
projectRef: ProjectRef | null;
|
||||||
projectLabel?: string | null;
|
projectLabel?: string | null;
|
||||||
@@ -71,6 +101,41 @@ const createTodoId = (): string => {
|
|||||||
return `todo_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
return `todo_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SortableTodoHandleProps = {
|
||||||
|
attributes: ReturnType<typeof useSortable>['attributes'];
|
||||||
|
listeners: ReturnType<typeof useSortable>['listeners'];
|
||||||
|
setActivatorNodeRef: ReturnType<typeof useSortable>['setActivatorNodeRef'];
|
||||||
|
isDragging: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SortableTodoItem: React.FC<{
|
||||||
|
id: string;
|
||||||
|
children: (dragHandleProps: SortableTodoHandleProps) => React.ReactNode;
|
||||||
|
}> = ({ id, children }) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
setActivatorNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({ id });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={{
|
||||||
|
transform: DndCSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
}}
|
||||||
|
className={cn(isDragging && 'opacity-60')}
|
||||||
|
>
|
||||||
|
{children({ attributes, listeners, setActivatorNodeRef, isDragging })}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
||||||
projectRef,
|
projectRef,
|
||||||
projectLabel,
|
projectLabel,
|
||||||
@@ -91,6 +156,13 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
|||||||
const [contextReloadTick, setContextReloadTick] = React.useState(0);
|
const [contextReloadTick, setContextReloadTick] = React.useState(0);
|
||||||
const notesHydratedRef = React.useRef(false);
|
const notesHydratedRef = React.useRef(false);
|
||||||
const lastSavedNotesRef = React.useRef('');
|
const lastSavedNotesRef = React.useRef('');
|
||||||
|
const todoPanelHeight = useUIStore((state) => state.todoPanelHeight);
|
||||||
|
const setTodoPanelHeight = useUIStore((state) => state.setTodoPanelHeight);
|
||||||
|
const notesPanelHeight = useUIStore((state) => state.notesPanelHeight);
|
||||||
|
const setNotesPanelHeight = useUIStore((state) => state.setNotesPanelHeight);
|
||||||
|
const [isTodoPanelResizing, setIsTodoPanelResizing] = React.useState(false);
|
||||||
|
const todoPanelStartYRef = React.useRef(0);
|
||||||
|
const todoPanelStartHeightRef = React.useRef(todoPanelHeight);
|
||||||
|
|
||||||
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
||||||
const createSession = useSessionUIStore((state) => state.createSession);
|
const createSession = useSessionUIStore((state) => state.createSession);
|
||||||
@@ -102,6 +174,7 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
|||||||
const openContextPanelTab = useUIStore((state) => state.openContextPanelTab);
|
const openContextPanelTab = useUIStore((state) => state.openContextPanelTab);
|
||||||
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
||||||
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
|
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
|
||||||
|
const padding = useUIStore((state) => state.padding);
|
||||||
|
|
||||||
const persistProjectData = React.useCallback(
|
const persistProjectData = React.useCallback(
|
||||||
async (nextNotes: string, nextTodos: OpenChamberProjectTodoItem[]) => {
|
async (nextNotes: string, nextTodos: OpenChamberProjectTodoItem[]) => {
|
||||||
@@ -191,6 +264,53 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
|||||||
};
|
};
|
||||||
}, [projectRef]);
|
}, [projectRef]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (todos.length < 7) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetHeight = getPanelHeightForItems(todos.length, padding);
|
||||||
|
const minHeight = getEffectiveItemHeight(padding) * TODO_PANEL_MIN_ITEMS;
|
||||||
|
if (todoPanelHeight < minHeight || todoPanelHeight > targetHeight) {
|
||||||
|
setTodoPanelHeight(targetHeight);
|
||||||
|
}
|
||||||
|
}, [todos.length, padding, todoPanelHeight, setTodoPanelHeight]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isTodoPanelResizing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePointerMove = (event: PointerEvent) => {
|
||||||
|
const delta = event.clientY - todoPanelStartYRef.current;
|
||||||
|
const nextHeight = Math.min(
|
||||||
|
getEffectiveItemHeight(padding) * TODO_PANEL_MAX_ITEMS,
|
||||||
|
Math.max(getEffectiveItemHeight(padding) * TODO_PANEL_MIN_ITEMS, todoPanelStartHeightRef.current + delta)
|
||||||
|
);
|
||||||
|
setTodoPanelHeight(nextHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerEnd = () => {
|
||||||
|
setIsTodoPanelResizing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
|
window.addEventListener('pointerup', handlePointerEnd, { once: true });
|
||||||
|
window.addEventListener('pointercancel', handlePointerEnd, { once: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
|
window.removeEventListener('pointerup', handlePointerEnd);
|
||||||
|
window.removeEventListener('pointercancel', handlePointerEnd);
|
||||||
|
};
|
||||||
|
}, [isTodoPanelResizing, padding, setTodoPanelHeight]);
|
||||||
|
|
||||||
|
const handleTodoPanelResizeStart = React.useCallback((event: React.PointerEvent) => {
|
||||||
|
setIsTodoPanelResizing(true);
|
||||||
|
todoPanelStartYRef.current = event.clientY;
|
||||||
|
todoPanelStartHeightRef.current = todoPanelHeight;
|
||||||
|
event.preventDefault();
|
||||||
|
}, [todoPanelHeight]);
|
||||||
|
|
||||||
const handleNotesBlur = React.useCallback(() => {
|
const handleNotesBlur = React.useCallback(() => {
|
||||||
lastSavedNotesRef.current = notes;
|
lastSavedNotesRef.current = notes;
|
||||||
void persistProjectData(notes, todos);
|
void persistProjectData(notes, todos);
|
||||||
@@ -274,6 +394,28 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
|||||||
void persistProjectData(notes, nextTodos);
|
void persistProjectData(notes, nextTodos);
|
||||||
}, [notes, persistProjectData, todos]);
|
}, [notes, persistProjectData, todos]);
|
||||||
|
|
||||||
|
const handleTodoReorder = React.useCallback(
|
||||||
|
(event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
if (!over || active.id === over.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const oldIndex = todos.findIndex((todo) => todo.id === active.id);
|
||||||
|
const newIndex = todos.findIndex((todo) => todo.id === over.id);
|
||||||
|
if (oldIndex === -1 || newIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextTodos = arrayMove(todos, oldIndex, newIndex);
|
||||||
|
setTodos(nextTodos);
|
||||||
|
void persistProjectData(notes, nextTodos);
|
||||||
|
},
|
||||||
|
[notes, persistProjectData, todos]
|
||||||
|
);
|
||||||
|
|
||||||
|
const todoSensors = useSensors(
|
||||||
|
useSensor(PointerSensor, { activationConstraint: { distance: 8 } })
|
||||||
|
);
|
||||||
|
|
||||||
const todoInputValue = newTodoText.slice(0, OPENCHAMBER_PROJECT_TODO_TEXT_MAX_LENGTH);
|
const todoInputValue = newTodoText.slice(0, OPENCHAMBER_PROJECT_TODO_TEXT_MAX_LENGTH);
|
||||||
const completedTodoCount = todos.reduce((count, todo) => count + (todo.completed ? 1 : 0), 0);
|
const completedTodoCount = todos.reduce((count, todo) => count + (todo.completed ? 1 : 0), 0);
|
||||||
|
|
||||||
@@ -441,12 +583,55 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
|||||||
[deletingPlanId, projectRef, t]
|
[deletingPlanId, projectRef, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTriggerUploadPlan = React.useCallback(() => {
|
const handleTriggerUploadPlan = React.useCallback(async () => {
|
||||||
if (!projectRef || isImportingPlan) {
|
if (!projectRef || isImportingPlan) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
planFileInputRef.current?.click();
|
const result = await requestFileAccess({
|
||||||
}, [isImportingPlan, projectRef]);
|
defaultPath: projectRef.path,
|
||||||
|
filters: [
|
||||||
|
{ name: 'Plan files', extensions: ['md', 'markdown', 'txt'] },
|
||||||
|
{ name: 'All files', extensions: ['*'] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (result.success && result.path) {
|
||||||
|
setIsImportingPlan(true);
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
path: result.path,
|
||||||
|
allowOutsideWorkspace: 'true',
|
||||||
|
});
|
||||||
|
const response = await fetch(`/api/fs/read?${params.toString()}`, { cache: 'no-store' });
|
||||||
|
if (!response.ok) {
|
||||||
|
toast.error(t('rightSidebar.contextNotesTodo.toast.readPlanFileFailed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = await response.text();
|
||||||
|
if (!text.trim()) {
|
||||||
|
toast.error(t('rightSidebar.contextNotesTodo.toast.planFileEmpty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fallbackTitle = result.path.split('/').pop()?.replace(/\.(md|markdown|txt)$/i, '').trim() || '';
|
||||||
|
const created = await importProjectPlanFileFromContent(projectRef, text, fallbackTitle);
|
||||||
|
if (!created) {
|
||||||
|
toast.error(t('rightSidebar.contextNotesTodo.toast.importPlanFailed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.dispatchEvent(new CustomEvent('openchamber:project-plan-saved', {
|
||||||
|
detail: { projectId: projectRef.id },
|
||||||
|
}));
|
||||||
|
toast.success(t('rightSidebar.contextNotesTodo.toast.planImported'));
|
||||||
|
} catch (error) {
|
||||||
|
const description = error instanceof Error ? error.message : undefined;
|
||||||
|
toast.error(t('rightSidebar.contextNotesTodo.toast.readPlanFileFailed'), description ? { description } : undefined);
|
||||||
|
} finally {
|
||||||
|
setIsImportingPlan(false);
|
||||||
|
}
|
||||||
|
} else if (result.error === 'Native file picker not available') {
|
||||||
|
// Fall back to HTML file input for web/non-desktop runtimes
|
||||||
|
planFileInputRef.current?.click();
|
||||||
|
}
|
||||||
|
}, [isImportingPlan, projectRef, t]);
|
||||||
|
|
||||||
const handleUploadPlanFile = React.useCallback(
|
const handleUploadPlanFile = React.useCallback(
|
||||||
async (file: File | null) => {
|
async (file: File | null) => {
|
||||||
@@ -523,7 +708,8 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
|||||||
onChange={(event) => setNotes(event.target.value.slice(0, OPENCHAMBER_PROJECT_NOTES_MAX_LENGTH))}
|
onChange={(event) => setNotes(event.target.value.slice(0, OPENCHAMBER_PROJECT_NOTES_MAX_LENGTH))}
|
||||||
onBlur={handleNotesBlur}
|
onBlur={handleNotesBlur}
|
||||||
placeholder={t('rightSidebar.contextNotesTodo.notes.placeholder')}
|
placeholder={t('rightSidebar.contextNotesTodo.notes.placeholder')}
|
||||||
className="min-h-28 max-h-80 resize-none"
|
resizedHeight={notesPanelHeight}
|
||||||
|
onResizeHeightChange={setNotesPanelHeight}
|
||||||
useScrollShadow
|
useScrollShadow
|
||||||
scrollShadowSize={56}
|
scrollShadowSize={56}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@@ -579,86 +765,141 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-h-56 overflow-y-auto rounded-lg border border-border/60 bg-background/40">
|
<div
|
||||||
|
className={cn(
|
||||||
|
'overflow-y-auto rounded-lg border border-border/60 bg-background/40',
|
||||||
|
isTodoPanelResizing && 'transition-none'
|
||||||
|
)}
|
||||||
|
style={
|
||||||
|
todos.length < 7
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
height: `${todoPanelHeight}px`,
|
||||||
|
maxHeight: `${todoPanelHeight}px`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
{todos.length === 0 ? (
|
{todos.length === 0 ? (
|
||||||
<p className="px-3 py-3 typography-meta text-muted-foreground">
|
<p className="px-3 py-3 typography-meta text-muted-foreground">
|
||||||
{t('rightSidebar.contextNotesTodo.todo.empty')}
|
{t('rightSidebar.contextNotesTodo.todo.empty')}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="divide-y divide-border/50">
|
<DndContext
|
||||||
{todos.map((todo) => {
|
sensors={todoSensors}
|
||||||
const isExpandedTodo = expandedTodoIds.has(todo.id);
|
collisionDetection={closestCenter}
|
||||||
return (
|
onDragEnd={handleTodoReorder}
|
||||||
<li key={todo.id} className="flex items-start gap-1.5 px-2.5 py-1.5">
|
>
|
||||||
<div className="flex h-6 items-center">
|
<SortableContext
|
||||||
<Checkbox
|
items={todos.map((todo) => todo.id)}
|
||||||
checked={todo.completed}
|
strategy={verticalListSortingStrategy}
|
||||||
onChange={(checked) => handleToggleTodo(todo.id, checked)}
|
>
|
||||||
ariaLabel={t('rightSidebar.contextNotesTodo.todo.actions.markComplete', { text: todo.text })}
|
<ul className="divide-y divide-border/50">
|
||||||
/>
|
{todos.map((todo) => {
|
||||||
</div>
|
const isExpandedTodo = expandedTodoIds.has(todo.id);
|
||||||
<button
|
return (
|
||||||
type="button"
|
<SortableTodoItem key={todo.id} id={todo.id}>
|
||||||
onClick={() => handleToggleTodoExpanded(todo.id)}
|
{(dragHandleProps) => (
|
||||||
className={cn(
|
<div className="flex items-start gap-1.5 px-2.5 py-1.5">
|
||||||
'block min-h-6 min-w-0 flex-1 bg-transparent p-0 text-left typography-ui-label leading-6 text-foreground',
|
<button
|
||||||
isExpandedTodo ? 'whitespace-normal break-words' : 'overflow-hidden text-ellipsis whitespace-nowrap',
|
type="button"
|
||||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
ref={dragHandleProps.setActivatorNodeRef}
|
||||||
todo.completed && 'text-muted-foreground line-through'
|
{...dragHandleProps.attributes}
|
||||||
)}
|
{...dragHandleProps.listeners}
|
||||||
title={isExpandedTodo ? undefined : todo.text}
|
onClick={(event) => {
|
||||||
aria-label={
|
event.preventDefault();
|
||||||
isExpandedTodo
|
event.stopPropagation();
|
||||||
? t('rightSidebar.contextNotesTodo.todo.actions.collapse', { text: todo.text })
|
}}
|
||||||
: t('rightSidebar.contextNotesTodo.todo.actions.expand', { text: todo.text })
|
className="flex h-6 w-4 flex-shrink-0 touch-none items-center justify-center text-muted-foreground hover:text-foreground"
|
||||||
}
|
aria-label={t('rightSidebar.contextNotesTodo.todo.actions.reorder', { text: todo.text })}
|
||||||
>
|
title={t('rightSidebar.contextNotesTodo.todo.actions.reorder', { text: todo.text })}
|
||||||
{todo.text}
|
>
|
||||||
</button>
|
<Icon name="draggable" className="h-3.5 w-3.5" />
|
||||||
<div className="flex h-6 items-center gap-0.5">
|
</button>
|
||||||
<button
|
<div className="flex h-6 items-center">
|
||||||
type="button"
|
<Checkbox
|
||||||
onClick={() => handleDeleteTodo(todo.id)}
|
checked={todo.completed}
|
||||||
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
onChange={(checked) => handleToggleTodo(todo.id, checked)}
|
||||||
aria-label={t('rightSidebar.contextNotesTodo.todo.actions.delete', { text: todo.text })}
|
ariaLabel={t('rightSidebar.contextNotesTodo.todo.actions.markComplete', { text: todo.text })}
|
||||||
title={t('rightSidebar.contextNotesTodo.todo.actions.delete', { text: todo.text })}
|
/>
|
||||||
>
|
</div>
|
||||||
<Icon name="delete-bin" className="h-3.5 w-3.5" />
|
<button
|
||||||
</button>
|
type="button"
|
||||||
<DropdownMenu>
|
onClick={() => handleToggleTodoExpanded(todo.id)}
|
||||||
<DropdownMenuTrigger asChild>
|
className={cn(
|
||||||
<button
|
'block min-h-6 min-w-0 flex-1 bg-transparent p-0 text-left typography-ui-label leading-6 text-foreground',
|
||||||
type="button"
|
isExpandedTodo ? 'whitespace-normal break-words' : 'overflow-hidden text-ellipsis whitespace-nowrap',
|
||||||
disabled={sendingTodoId === todo.id}
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
||||||
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 disabled:cursor-not-allowed disabled:opacity-50"
|
todo.completed && 'text-muted-foreground line-through'
|
||||||
aria-label={t('rightSidebar.contextNotesTodo.todo.actions.send', { text: todo.text })}
|
)}
|
||||||
title={t('rightSidebar.contextNotesTodo.todo.actions.send', { text: todo.text })}
|
title={isExpandedTodo ? undefined : todo.text}
|
||||||
>
|
aria-label={
|
||||||
<Icon name="send-plane" className="h-3.5 w-3.5" />
|
isExpandedTodo
|
||||||
</button>
|
? t('rightSidebar.contextNotesTodo.todo.actions.collapse', { text: todo.text })
|
||||||
</DropdownMenuTrigger>
|
: t('rightSidebar.contextNotesTodo.todo.actions.expand', { text: todo.text })
|
||||||
<DropdownMenuContent align="end" className="w-56">
|
}
|
||||||
<DropdownMenuItem onClick={() => handleSendToCurrentSession(todo.text)}>
|
>
|
||||||
{t('rightSidebar.contextNotesTodo.todo.sendMenu.currentSession')}
|
{todo.text}
|
||||||
</DropdownMenuItem>
|
</button>
|
||||||
<DropdownMenuItem onClick={() => handleSendToNewSession(todo.id, todo.text)}>
|
<div className="flex h-6 items-center gap-0.5">
|
||||||
{t('rightSidebar.contextNotesTodo.todo.sendMenu.newSession')}
|
<button
|
||||||
</DropdownMenuItem>
|
type="button"
|
||||||
<DropdownMenuItem
|
onClick={() => handleDeleteTodo(todo.id)}
|
||||||
onClick={() => void handleSendToNewWorktreeSession(todo.id, todo.text)}
|
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||||
disabled={!canCreateWorktree}
|
aria-label={t('rightSidebar.contextNotesTodo.todo.actions.delete', { text: todo.text })}
|
||||||
>
|
title={t('rightSidebar.contextNotesTodo.todo.actions.delete', { text: todo.text })}
|
||||||
{t('rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession')}
|
>
|
||||||
</DropdownMenuItem>
|
<Icon name="delete-bin" className="h-3.5 w-3.5" />
|
||||||
</DropdownMenuContent>
|
</button>
|
||||||
</DropdownMenu>
|
<DropdownMenu>
|
||||||
</div>
|
<DropdownMenuTrigger asChild>
|
||||||
</li>
|
<button
|
||||||
);
|
type="button"
|
||||||
})}
|
disabled={sendingTodoId === todo.id}
|
||||||
</ul>
|
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
aria-label={t('rightSidebar.contextNotesTodo.todo.actions.send', { text: todo.text })}
|
||||||
|
title={t('rightSidebar.contextNotesTodo.todo.actions.send', { text: todo.text })}
|
||||||
|
>
|
||||||
|
<Icon name="send-plane" className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-56">
|
||||||
|
<DropdownMenuItem onClick={() => handleSendToCurrentSession(todo.text)}>
|
||||||
|
{t('rightSidebar.contextNotesTodo.todo.sendMenu.currentSession')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleSendToNewSession(todo.id, todo.text)}>
|
||||||
|
{t('rightSidebar.contextNotesTodo.todo.sendMenu.newSession')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => void handleSendToNewWorktreeSession(todo.id, todo.text)}
|
||||||
|
disabled={!canCreateWorktree}
|
||||||
|
>
|
||||||
|
{t('rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SortableTodoItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{todos.length >= 7 && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'h-[3px] w-full cursor-row-resize hover:bg-[var(--interactive-border)]/80 transition-colors',
|
||||||
|
isTodoPanelResizing && 'bg-[var(--interactive-border)]'
|
||||||
|
)}
|
||||||
|
onPointerDown={handleTodoPanelResizeStart}
|
||||||
|
role="separator"
|
||||||
|
aria-orientation="horizontal"
|
||||||
|
aria-label={t('rightSidebar.contextNotesTodo.todo.resizeAria')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ type TextareaProps = React.ComponentProps<"textarea"> & {
|
|||||||
useScrollShadow?: boolean;
|
useScrollShadow?: boolean;
|
||||||
scrollShadowSize?: number;
|
scrollShadowSize?: number;
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
|
resizedHeight?: number | null;
|
||||||
|
onResizeHeightChange?: (height: number) => void;
|
||||||
/**
|
/**
|
||||||
* AlignUI "simple" mode: render a bare textarea (no compound wrapper).
|
* AlignUI "simple" mode: render a bare textarea (no compound wrapper).
|
||||||
* Used for chat composer or anywhere the textarea is embedded inside an
|
* Used for chat composer or anywhere the textarea is embedded inside an
|
||||||
@@ -68,6 +70,8 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
useScrollShadow = false,
|
useScrollShadow = false,
|
||||||
scrollShadowSize,
|
scrollShadowSize,
|
||||||
hasError,
|
hasError,
|
||||||
|
resizedHeight: controlledResizedHeight,
|
||||||
|
onResizeHeightChange,
|
||||||
disabled,
|
disabled,
|
||||||
simple,
|
simple,
|
||||||
endSlot,
|
endSlot,
|
||||||
@@ -79,6 +83,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
const wrapperRef = React.useRef<HTMLDivElement>(null);
|
const wrapperRef = React.useRef<HTMLDivElement>(null);
|
||||||
const dragStateRef = React.useRef<{ startY: number; startHeight: number } | null>(null);
|
const dragStateRef = React.useRef<{ startY: number; startHeight: number } | null>(null);
|
||||||
const [resizedHeight, setResizedHeight] = React.useState<number | null>(null);
|
const [resizedHeight, setResizedHeight] = React.useState<number | null>(null);
|
||||||
|
const effectiveResizedHeight = controlledResizedHeight ?? resizedHeight;
|
||||||
|
|
||||||
const handleResizeStart = React.useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
const handleResizeStart = React.useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
const wrapper = wrapperRef.current;
|
const wrapper = wrapperRef.current;
|
||||||
@@ -94,7 +99,12 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
const state = dragStateRef.current;
|
const state = dragStateRef.current;
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
const next = state.startHeight + (moveEvent.clientY - state.startY);
|
const next = state.startHeight + (moveEvent.clientY - state.startY);
|
||||||
setResizedHeight(Math.max(82, next));
|
const nextHeight = Math.max(82, next);
|
||||||
|
if (onResizeHeightChange) {
|
||||||
|
onResizeHeightChange(nextHeight);
|
||||||
|
} else {
|
||||||
|
setResizedHeight(nextHeight);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const onUp = () => {
|
const onUp = () => {
|
||||||
dragStateRef.current = null;
|
dragStateRef.current = null;
|
||||||
@@ -106,7 +116,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
target.addEventListener('pointerup', onUp);
|
target.addEventListener('pointerup', onUp);
|
||||||
target.addEventListener('pointercancel', onUp);
|
target.addEventListener('pointercancel', onUp);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}, []);
|
}, [onResizeHeightChange]);
|
||||||
|
|
||||||
const focusInnerTextarea = React.useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
const focusInnerTextarea = React.useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
// Clicking the wrapper chrome (below/around the textarea) should focus it.
|
// Clicking the wrapper chrome (below/around the textarea) should focus it.
|
||||||
@@ -152,7 +162,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
<div
|
<div
|
||||||
ref={wrapperRef}
|
ref={wrapperRef}
|
||||||
onPointerDown={focusInnerTextarea}
|
onPointerDown={focusInnerTextarea}
|
||||||
style={resizedHeight !== null ? { height: `${resizedHeight}px` } : undefined}
|
style={effectiveResizedHeight !== null ? { height: `${effectiveResizedHeight}px` } : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/textarea relative flex w-full flex-col rounded-[var(--radius-xl)] bg-[var(--surface-elevated)] pb-2.5",
|
"group/textarea relative flex w-full flex-col rounded-[var(--radius-xl)] bg-[var(--surface-elevated)] pb-2.5",
|
||||||
"ring-1 ring-inset ring-border/60 transition duration-200 ease-out",
|
"ring-1 ring-inset ring-border/60 transition duration-200 ease-out",
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ export const requestDirectoryAccess = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const requestFileAccess = async (
|
export const requestFileAccess = async (
|
||||||
options?: { filters?: Array<{ name: string; extensions: string[] }> }
|
options?: { filters?: Array<{ name: string; extensions: string[] }>; defaultPath?: string }
|
||||||
): Promise<{ success: boolean; path?: string; error?: string }> => {
|
): Promise<{ success: boolean; path?: string; error?: string }> => {
|
||||||
if (isTauriShell() && isDesktopLocalOriginActive()) {
|
if (isTauriShell() && isDesktopLocalOriginActive()) {
|
||||||
try {
|
try {
|
||||||
@@ -379,6 +379,7 @@ export const requestFileAccess = async (
|
|||||||
multiple: false,
|
multiple: false,
|
||||||
title: 'Select File',
|
title: 'Select File',
|
||||||
...(options?.filters ? { filters: options.filters } : {}),
|
...(options?.filters ? { filters: options.filters } : {}),
|
||||||
|
...(options?.defaultPath ? { defaultPath: options.defaultPath } : {}),
|
||||||
});
|
});
|
||||||
if (!selected || typeof selected !== 'string') {
|
if (!selected || typeof selected !== 'string') {
|
||||||
return { success: false, error: 'File selection cancelled' };
|
return { success: false, error: 'File selection cancelled' };
|
||||||
|
|||||||
@@ -1034,6 +1034,8 @@ export const dict = {
|
|||||||
'rightSidebar.contextNotesTodo.todo.actions.expand': 'Expand todo "{text}"',
|
'rightSidebar.contextNotesTodo.todo.actions.expand': 'Expand todo "{text}"',
|
||||||
'rightSidebar.contextNotesTodo.todo.actions.delete': 'Delete "{text}"',
|
'rightSidebar.contextNotesTodo.todo.actions.delete': 'Delete "{text}"',
|
||||||
'rightSidebar.contextNotesTodo.todo.actions.send': 'Send "{text}"',
|
'rightSidebar.contextNotesTodo.todo.actions.send': 'Send "{text}"',
|
||||||
|
'rightSidebar.contextNotesTodo.todo.actions.reorder': 'Reorder "{text}"',
|
||||||
|
'rightSidebar.contextNotesTodo.todo.resizeAria': 'Resize todo list',
|
||||||
'rightSidebar.contextNotesTodo.todo.sendMenu.currentSession': 'Send to current session',
|
'rightSidebar.contextNotesTodo.todo.sendMenu.currentSession': 'Send to current session',
|
||||||
'rightSidebar.contextNotesTodo.todo.sendMenu.newSession': 'Send to new session',
|
'rightSidebar.contextNotesTodo.todo.sendMenu.newSession': 'Send to new session',
|
||||||
'rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession': 'Send to new worktree session',
|
'rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession': 'Send to new worktree session',
|
||||||
|
|||||||
@@ -1000,6 +1000,8 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"rightSidebar.contextNotesTodo.todo.actions.expand": "Expandir tarea pendiente \"{text}\"",
|
"rightSidebar.contextNotesTodo.todo.actions.expand": "Expandir tarea pendiente \"{text}\"",
|
||||||
"rightSidebar.contextNotesTodo.todo.actions.delete": "Eliminar \"{text}\"",
|
"rightSidebar.contextNotesTodo.todo.actions.delete": "Eliminar \"{text}\"",
|
||||||
"rightSidebar.contextNotesTodo.todo.actions.send": "Enviar \"{text}\"",
|
"rightSidebar.contextNotesTodo.todo.actions.send": "Enviar \"{text}\"",
|
||||||
|
"rightSidebar.contextNotesTodo.todo.actions.reorder": "Reordenar \"{text}\"",
|
||||||
|
"rightSidebar.contextNotesTodo.todo.resizeAria": "Redimensionar lista de tareas",
|
||||||
"rightSidebar.contextNotesTodo.todo.sendMenu.currentSession": "Enviar a la sesión actual",
|
"rightSidebar.contextNotesTodo.todo.sendMenu.currentSession": "Enviar a la sesión actual",
|
||||||
"rightSidebar.contextNotesTodo.todo.sendMenu.newSession": "Enviar a una nueva sesión",
|
"rightSidebar.contextNotesTodo.todo.sendMenu.newSession": "Enviar a una nueva sesión",
|
||||||
"rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession": "Enviar a una nueva sesión de worktree",
|
"rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession": "Enviar a una nueva sesión de worktree",
|
||||||
|
|||||||
@@ -1037,6 +1037,8 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'rightSidebar.contextNotesTodo.todo.actions.expand': '펼치기 todo "{text}"',
|
'rightSidebar.contextNotesTodo.todo.actions.expand': '펼치기 todo "{text}"',
|
||||||
'rightSidebar.contextNotesTodo.todo.actions.delete': '"{text}" 삭제',
|
'rightSidebar.contextNotesTodo.todo.actions.delete': '"{text}" 삭제',
|
||||||
'rightSidebar.contextNotesTodo.todo.actions.send': '보내기 "{text}"',
|
'rightSidebar.contextNotesTodo.todo.actions.send': '보내기 "{text}"',
|
||||||
|
'rightSidebar.contextNotesTodo.todo.actions.reorder': '재정렬 "{text}"',
|
||||||
|
'rightSidebar.contextNotesTodo.todo.resizeAria': '할 일 목록 크기 조정',
|
||||||
'rightSidebar.contextNotesTodo.todo.sendMenu.currentSession': '현재 세션으로 보내기',
|
'rightSidebar.contextNotesTodo.todo.sendMenu.currentSession': '현재 세션으로 보내기',
|
||||||
'rightSidebar.contextNotesTodo.todo.sendMenu.newSession': '새 세션으로 보내기',
|
'rightSidebar.contextNotesTodo.todo.sendMenu.newSession': '새 세션으로 보내기',
|
||||||
'rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession': '새 워크트리 세션으로 보내기',
|
'rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession': '새 워크트리 세션으로 보내기',
|
||||||
|
|||||||
@@ -1930,6 +1930,8 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'rightSidebar.contextNotesTodo.todo.actions.expand': 'Rozwiń zadanie „{text}”',
|
'rightSidebar.contextNotesTodo.todo.actions.expand': 'Rozwiń zadanie „{text}”',
|
||||||
'rightSidebar.contextNotesTodo.todo.actions.markComplete': 'Oznacz „{text}” jako ukończone',
|
'rightSidebar.contextNotesTodo.todo.actions.markComplete': 'Oznacz „{text}” jako ukończone',
|
||||||
'rightSidebar.contextNotesTodo.todo.actions.send': 'Wyślij „{text}”',
|
'rightSidebar.contextNotesTodo.todo.actions.send': 'Wyślij „{text}”',
|
||||||
|
'rightSidebar.contextNotesTodo.todo.actions.reorder': 'Zmień kolejność "{text}"',
|
||||||
|
'rightSidebar.contextNotesTodo.todo.resizeAria': 'Zmień rozmiar listy zadań',
|
||||||
'rightSidebar.contextNotesTodo.todo.addAria': 'Dodaj zadanie',
|
'rightSidebar.contextNotesTodo.todo.addAria': 'Dodaj zadanie',
|
||||||
'rightSidebar.contextNotesTodo.todo.clearCompleted': 'Wyczyść ukończone',
|
'rightSidebar.contextNotesTodo.todo.clearCompleted': 'Wyczyść ukończone',
|
||||||
'rightSidebar.contextNotesTodo.todo.empty': 'Brak zadań. Dodaj krótką checklistę dla tego projektu.',
|
'rightSidebar.contextNotesTodo.todo.empty': 'Brak zadań. Dodaj krótką checklistę dla tego projektu.',
|
||||||
|
|||||||
@@ -1000,6 +1000,8 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"rightSidebar.contextNotesTodo.todo.actions.expand": "Expandir tarefa pendente \"{text}\"",
|
"rightSidebar.contextNotesTodo.todo.actions.expand": "Expandir tarefa pendente \"{text}\"",
|
||||||
"rightSidebar.contextNotesTodo.todo.actions.delete": "Excluir \"{text}\"",
|
"rightSidebar.contextNotesTodo.todo.actions.delete": "Excluir \"{text}\"",
|
||||||
"rightSidebar.contextNotesTodo.todo.actions.send": "Enviar \"{text}\"",
|
"rightSidebar.contextNotesTodo.todo.actions.send": "Enviar \"{text}\"",
|
||||||
|
"rightSidebar.contextNotesTodo.todo.actions.reorder": "Reordenar \"{text}\"",
|
||||||
|
"rightSidebar.contextNotesTodo.todo.resizeAria": "Redimensionar lista de tarefas",
|
||||||
"rightSidebar.contextNotesTodo.todo.sendMenu.currentSession": "Enviar à sessão atual",
|
"rightSidebar.contextNotesTodo.todo.sendMenu.currentSession": "Enviar à sessão atual",
|
||||||
"rightSidebar.contextNotesTodo.todo.sendMenu.newSession": "Enviar a uma nova sessão",
|
"rightSidebar.contextNotesTodo.todo.sendMenu.newSession": "Enviar a uma nova sessão",
|
||||||
"rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession": "Enviar a uma nova sessão de worktree",
|
"rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession": "Enviar a uma nova sessão de worktree",
|
||||||
|
|||||||
@@ -1000,6 +1000,8 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"rightSidebar.contextNotesTodo.todo.actions.expand": "Розгорнути завдання \"{text}\"",
|
"rightSidebar.contextNotesTodo.todo.actions.expand": "Розгорнути завдання \"{text}\"",
|
||||||
"rightSidebar.contextNotesTodo.todo.actions.delete": "Видалити \"{text}\"",
|
"rightSidebar.contextNotesTodo.todo.actions.delete": "Видалити \"{text}\"",
|
||||||
"rightSidebar.contextNotesTodo.todo.actions.send": "Надіслати \"{text}\"",
|
"rightSidebar.contextNotesTodo.todo.actions.send": "Надіслати \"{text}\"",
|
||||||
|
"rightSidebar.contextNotesTodo.todo.actions.reorder": "Змінити порядок \"{text}\"",
|
||||||
|
"rightSidebar.contextNotesTodo.todo.resizeAria": "Змінити розмір списку завдань",
|
||||||
"rightSidebar.contextNotesTodo.todo.sendMenu.currentSession": "Надіслати до поточної сесії",
|
"rightSidebar.contextNotesTodo.todo.sendMenu.currentSession": "Надіслати до поточної сесії",
|
||||||
"rightSidebar.contextNotesTodo.todo.sendMenu.newSession": "Надіслати до нової сесії",
|
"rightSidebar.contextNotesTodo.todo.sendMenu.newSession": "Надіслати до нової сесії",
|
||||||
"rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession": "Надіслати до нової сесії в worktree",
|
"rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession": "Надіслати до нової сесії в worktree",
|
||||||
|
|||||||
@@ -1000,6 +1000,8 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'rightSidebar.contextNotesTodo.todo.actions.expand': '展开待办“{text}”',
|
'rightSidebar.contextNotesTodo.todo.actions.expand': '展开待办“{text}”',
|
||||||
'rightSidebar.contextNotesTodo.todo.actions.delete': '删除“{text}”',
|
'rightSidebar.contextNotesTodo.todo.actions.delete': '删除“{text}”',
|
||||||
'rightSidebar.contextNotesTodo.todo.actions.send': '发送“{text}”',
|
'rightSidebar.contextNotesTodo.todo.actions.send': '发送“{text}”',
|
||||||
|
'rightSidebar.contextNotesTodo.todo.actions.reorder': '重新排序"{text}"',
|
||||||
|
'rightSidebar.contextNotesTodo.todo.resizeAria': '调整待办列表大小',
|
||||||
'rightSidebar.contextNotesTodo.todo.sendMenu.currentSession': '发送到当前会话',
|
'rightSidebar.contextNotesTodo.todo.sendMenu.currentSession': '发送到当前会话',
|
||||||
'rightSidebar.contextNotesTodo.todo.sendMenu.newSession': '发送到新会话',
|
'rightSidebar.contextNotesTodo.todo.sendMenu.newSession': '发送到新会话',
|
||||||
'rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession': '发送到新 worktree 会话',
|
'rightSidebar.contextNotesTodo.todo.sendMenu.newWorktreeSession': '发送到新 worktree 会话',
|
||||||
|
|||||||
@@ -496,6 +496,8 @@ interface UIStore {
|
|||||||
isBottomTerminalExpanded: boolean;
|
isBottomTerminalExpanded: boolean;
|
||||||
bottomTerminalHeight: number;
|
bottomTerminalHeight: number;
|
||||||
hasManuallyResizedBottomTerminal: boolean;
|
hasManuallyResizedBottomTerminal: boolean;
|
||||||
|
notesPanelHeight: number;
|
||||||
|
todoPanelHeight: number;
|
||||||
isSessionSwitcherOpen: boolean;
|
isSessionSwitcherOpen: boolean;
|
||||||
isSessionDropdownOpen: boolean;
|
isSessionDropdownOpen: boolean;
|
||||||
activeMainTab: MainTab;
|
activeMainTab: MainTab;
|
||||||
@@ -627,6 +629,8 @@ interface UIStore {
|
|||||||
setBottomTerminalOpen: (open: boolean) => void;
|
setBottomTerminalOpen: (open: boolean) => void;
|
||||||
setBottomTerminalExpanded: (expanded: boolean) => void;
|
setBottomTerminalExpanded: (expanded: boolean) => void;
|
||||||
setBottomTerminalHeight: (height: number) => void;
|
setBottomTerminalHeight: (height: number) => void;
|
||||||
|
setNotesPanelHeight: (height: number) => void;
|
||||||
|
setTodoPanelHeight: (height: number) => void;
|
||||||
setSessionSwitcherOpen: (open: boolean) => void;
|
setSessionSwitcherOpen: (open: boolean) => void;
|
||||||
setSessionDropdownOpen: (open: boolean) => void;
|
setSessionDropdownOpen: (open: boolean) => void;
|
||||||
setActiveMainTab: (tab: MainTab) => void;
|
setActiveMainTab: (tab: MainTab) => void;
|
||||||
@@ -760,6 +764,8 @@ export const useUIStore = create<UIStore>()(
|
|||||||
isBottomTerminalExpanded: false,
|
isBottomTerminalExpanded: false,
|
||||||
bottomTerminalHeight: 300,
|
bottomTerminalHeight: 300,
|
||||||
hasManuallyResizedBottomTerminal: false,
|
hasManuallyResizedBottomTerminal: false,
|
||||||
|
notesPanelHeight: 112,
|
||||||
|
todoPanelHeight: 259,
|
||||||
isSessionSwitcherOpen: false,
|
isSessionSwitcherOpen: false,
|
||||||
isSessionDropdownOpen: false,
|
isSessionDropdownOpen: false,
|
||||||
activeMainTab: 'chat',
|
activeMainTab: 'chat',
|
||||||
@@ -1291,6 +1297,14 @@ export const useUIStore = create<UIStore>()(
|
|||||||
set({ bottomTerminalHeight: height, hasManuallyResizedBottomTerminal: true });
|
set({ bottomTerminalHeight: height, hasManuallyResizedBottomTerminal: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setNotesPanelHeight: (height) => {
|
||||||
|
set({ notesPanelHeight: height });
|
||||||
|
},
|
||||||
|
|
||||||
|
setTodoPanelHeight: (height) => {
|
||||||
|
set({ todoPanelHeight: height });
|
||||||
|
},
|
||||||
|
|
||||||
setSessionSwitcherOpen: (open) => {
|
setSessionSwitcherOpen: (open) => {
|
||||||
set({ isSessionSwitcherOpen: open });
|
set({ isSessionSwitcherOpen: open });
|
||||||
},
|
},
|
||||||
@@ -1955,13 +1969,23 @@ export const useUIStore = create<UIStore>()(
|
|||||||
{
|
{
|
||||||
name: 'ui-store',
|
name: 'ui-store',
|
||||||
storage: createJSONStorage(() => getSafeStorage()),
|
storage: createJSONStorage(() => getSafeStorage()),
|
||||||
version: 8,
|
version: 9,
|
||||||
migrate: (persistedState, version) => {
|
migrate: (persistedState, version) => {
|
||||||
if (!persistedState || typeof persistedState !== 'object') {
|
if (!persistedState || typeof persistedState !== 'object') {
|
||||||
return persistedState;
|
return persistedState;
|
||||||
}
|
}
|
||||||
const state = persistedState as Record<string, unknown>;
|
const state = persistedState as Record<string, unknown>;
|
||||||
|
|
||||||
|
// v8 -> v9: initialize notes/todo panel height fields
|
||||||
|
if (version < 9) {
|
||||||
|
if (typeof state.notesPanelHeight !== 'number' || !Number.isFinite(state.notesPanelHeight)) {
|
||||||
|
state.notesPanelHeight = 112;
|
||||||
|
}
|
||||||
|
if (typeof state.todoPanelHeight !== 'number' || !Number.isFinite(state.todoPanelHeight)) {
|
||||||
|
state.todoPanelHeight = 259;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// v0 -> v1: reset legacy notification templates
|
// v0 -> v1: reset legacy notification templates
|
||||||
if (version < 1) {
|
if (version < 1) {
|
||||||
if (isLegacyDefaultTemplates(state.notificationTemplates)) {
|
if (isLegacyDefaultTemplates(state.notificationTemplates)) {
|
||||||
@@ -2046,6 +2070,8 @@ export const useUIStore = create<UIStore>()(
|
|||||||
isBottomTerminalOpen: state.isBottomTerminalOpen,
|
isBottomTerminalOpen: state.isBottomTerminalOpen,
|
||||||
isBottomTerminalExpanded: state.isBottomTerminalExpanded,
|
isBottomTerminalExpanded: state.isBottomTerminalExpanded,
|
||||||
bottomTerminalHeight: state.bottomTerminalHeight,
|
bottomTerminalHeight: state.bottomTerminalHeight,
|
||||||
|
notesPanelHeight: state.notesPanelHeight,
|
||||||
|
todoPanelHeight: state.todoPanelHeight,
|
||||||
isSessionSwitcherOpen: state.isSessionSwitcherOpen,
|
isSessionSwitcherOpen: state.isSessionSwitcherOpen,
|
||||||
activeMainTab: state.activeMainTab,
|
activeMainTab: state.activeMainTab,
|
||||||
sidebarSection: state.sidebarSection,
|
sidebarSection: state.sidebarSection,
|
||||||
|
|||||||
Reference in New Issue
Block a user