fix: AI loop prevention (no tools on last iteration), calendar button first, mail filter in toolbar, AI folder rename query invalidation
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
@@ -451,9 +451,12 @@ async def stream_chat(
|
|||||||
# Agent loop: LLM → tool calls → execute → feed back → repeat
|
# Agent loop: LLM → tool calls → execute → feed back → repeat
|
||||||
max_iterations = 5
|
max_iterations = 5
|
||||||
for iteration in range(max_iterations):
|
for iteration in range(max_iterations):
|
||||||
# Add tools to params if available
|
# Add tools to params if available, but NOT on the last iteration
|
||||||
if tool_schemas:
|
# to force the LLM to give a final answer instead of looping
|
||||||
|
if tool_schemas and iteration < max_iterations - 1:
|
||||||
params["tools"] = tool_schemas
|
params["tools"] = tool_schemas
|
||||||
|
elif "tools" in params:
|
||||||
|
del params["tools"]
|
||||||
|
|
||||||
# Stream LLM response
|
# Stream LLM response
|
||||||
collected_content = ""
|
collected_content = ""
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
fetchSessions, createSession, deleteSession, updateSession,
|
fetchSessions, createSession, deleteSession, updateSession,
|
||||||
fetchFolders, createFolder, deleteFolder, updateFolder,
|
fetchFolders, createFolder, deleteFolder, updateFolder,
|
||||||
@@ -261,6 +262,7 @@ export function SessionList({ activeSessionId, onSelectSession, isSidebar, class
|
|||||||
const [dragSessionId, setDragSessionId] = useState<string | null>(null);
|
const [dragSessionId, setDragSessionId] = useState<string | null>(null);
|
||||||
const [dragFolderId, setDragFolderId] = useState<string | null>(null);
|
const [dragFolderId, setDragFolderId] = useState<string | null>(null);
|
||||||
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -287,7 +289,10 @@ export function SessionList({ activeSessionId, onSelectSession, isSidebar, class
|
|||||||
if (!newName) return;
|
if (!newName) return;
|
||||||
try {
|
try {
|
||||||
if (type === 'session') await updateSession(id, { title: newName });
|
if (type === 'session') await updateSession(id, { title: newName });
|
||||||
else await updateFolder(id, { name: newName });
|
else {
|
||||||
|
await updateFolder(id, { name: newName });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['ai', 'folders'] });
|
||||||
|
}
|
||||||
loadData();
|
loadData();
|
||||||
} catch (e) { setError(e instanceof Error ? e.message : 'Rename failed'); }
|
} catch (e) { setError(e instanceof Error ? e.message : 'Rename failed'); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -327,6 +327,31 @@ export function CalendarPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const items = [
|
const items = [
|
||||||
|
// New appointment — ganz links
|
||||||
|
{
|
||||||
|
id: 'new-appointment',
|
||||||
|
plugin: 'calendar',
|
||||||
|
label: t('calendar.newAppointment'),
|
||||||
|
group: 'actions',
|
||||||
|
onClick: () => {
|
||||||
|
openWindow({
|
||||||
|
title: t('calendar.newAppointment'),
|
||||||
|
type: 'appointment-create',
|
||||||
|
component: AppointmentEditForm,
|
||||||
|
componentProps: {
|
||||||
|
entry: null,
|
||||||
|
prefillDate: null,
|
||||||
|
calendars,
|
||||||
|
defaultCalendarId: activeCalendarId,
|
||||||
|
onSaved: handleSaved,
|
||||||
|
onDeleted: handleDeleted,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: (
|
||||||
|
<Plus className="w-3.5 h-3.5" strokeWidth={2} />
|
||||||
|
),
|
||||||
|
},
|
||||||
// Navigation group
|
// Navigation group
|
||||||
{
|
{
|
||||||
id: 'nav-prev',
|
id: 'nav-prev',
|
||||||
@@ -384,31 +409,6 @@ export function CalendarPage() {
|
|||||||
onSelect: (value: string) => setViewMode(value as CalendarViewMode),
|
onSelect: (value: string) => setViewMode(value as CalendarViewMode),
|
||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
},
|
},
|
||||||
// Actions group
|
|
||||||
{
|
|
||||||
id: 'new-appointment',
|
|
||||||
plugin: 'calendar',
|
|
||||||
label: t('calendar.newAppointment'),
|
|
||||||
group: 'actions',
|
|
||||||
onClick: () => {
|
|
||||||
openWindow({
|
|
||||||
title: t('calendar.newAppointment'),
|
|
||||||
type: 'appointment-create',
|
|
||||||
component: AppointmentEditForm,
|
|
||||||
componentProps: {
|
|
||||||
entry: null,
|
|
||||||
prefillDate: null,
|
|
||||||
calendars,
|
|
||||||
defaultCalendarId: activeCalendarId,
|
|
||||||
onSaved: handleSaved,
|
|
||||||
onDeleted: handleDeleted,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: (
|
|
||||||
<Plus className="w-3.5 h-3.5" strokeWidth={2} />
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'ics-import',
|
id: 'ics-import',
|
||||||
plugin: 'calendar',
|
plugin: 'calendar',
|
||||||
|
|||||||
+37
-21
@@ -594,6 +594,43 @@ export function MailPage() {
|
|||||||
onSearch: handleSearch,
|
onSearch: handleSearch,
|
||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
},
|
},
|
||||||
|
// FilterPanel — SavedFilterBar + TagSelector
|
||||||
|
{
|
||||||
|
id: 'filter-panel',
|
||||||
|
plugin: 'mail',
|
||||||
|
label: 'Filter',
|
||||||
|
type: 'custom' as const,
|
||||||
|
group: 'filter',
|
||||||
|
customComponent: (
|
||||||
|
<SavedFilterBar
|
||||||
|
entityType="mail"
|
||||||
|
currentFilters={{ search: searchQuery, sortBy, sortOrder, folderId: selectedFolderId }}
|
||||||
|
onApplyFilter={(criteria) => {
|
||||||
|
if (criteria.search !== undefined) handleSearch(criteria.search);
|
||||||
|
if (criteria.sortBy) setSortBy(criteria.sortBy as 'date' | 'from' | 'subject');
|
||||||
|
if (criteria.sortOrder) setSortOrder(criteria.sortOrder as 'asc' | 'desc');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
onClick: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tag-filter',
|
||||||
|
plugin: 'mail',
|
||||||
|
label: 'Tags',
|
||||||
|
type: 'custom' as const,
|
||||||
|
group: 'filter',
|
||||||
|
customComponent: (
|
||||||
|
<TagSelector
|
||||||
|
entityType="file"
|
||||||
|
entityId={selectedMail?.id ?? ''}
|
||||||
|
selectedTags={selectedTags}
|
||||||
|
onChange={setSelectedTags}
|
||||||
|
placeholder={t('tags.selectPlaceholder', 'Tags filtern...')}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
onClick: () => {},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'compose',
|
id: 'compose',
|
||||||
plugin: 'mail',
|
plugin: 'mail',
|
||||||
@@ -814,27 +851,6 @@ export function MailPage() {
|
|||||||
className="border-r border-secondary-200 bg-white"
|
className="border-r border-secondary-200 bg-white"
|
||||||
data-testid="mail-list-pane"
|
data-testid="mail-list-pane"
|
||||||
>
|
>
|
||||||
{/* Saved filter bar + tag selector */}
|
|
||||||
<div className="flex items-center gap-2 px-3 py-2 border-b border-secondary-200 bg-secondary-50 flex-wrap" data-testid="mail-filter-bar">
|
|
||||||
<SavedFilterBar
|
|
||||||
entityType="mail"
|
|
||||||
currentFilters={{ search: searchQuery, sortBy, sortOrder, folderId: selectedFolderId }}
|
|
||||||
onApplyFilter={(criteria) => {
|
|
||||||
if (criteria.search !== undefined) handleSearch(criteria.search);
|
|
||||||
if (criteria.sortBy) setSortBy(criteria.sortBy as 'date' | 'from' | 'subject');
|
|
||||||
if (criteria.sortOrder) setSortOrder(criteria.sortOrder as 'asc' | 'desc');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex-1 min-w-[180px]" data-testid="mail-tag-selector">
|
|
||||||
<TagSelector
|
|
||||||
entityType="file"
|
|
||||||
entityId={selectedMail?.id ?? ''}
|
|
||||||
selectedTags={selectedTags}
|
|
||||||
onChange={setSelectedTags}
|
|
||||||
placeholder={t('tags.selectPlaceholder', 'Tags filtern...')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<MailList
|
<MailList
|
||||||
mails={mails}
|
mails={mails}
|
||||||
selectedMailId={selectedMail?.id || null}
|
selectedMailId={selectedMail?.id || null}
|
||||||
|
|||||||
Reference in New Issue
Block a user