Skip to content
Open
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
4 changes: 4 additions & 0 deletions Cargo.lock

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

213 changes: 213 additions & 0 deletions apps/web/src/lib/core/component/AI/component/tool/Reminders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { formatDateAndTime } from '@entity';
import BellSimple from '@phosphor-icons/core/regular/bell-simple.svg';
import Check from '@phosphor-icons/core/regular/check.svg';
import Trash from '@phosphor-icons/core/regular/trash.svg';
import type { NamedTool } from '@service-cognition/generated/tools/tool';
import type {
ListReminders as ListRemindersTool,
ReminderEntityType,
UpdateReminder as UpdateReminderTool,
} from '@service-cognition/generated/tools/types';
import { createSignal, For, Show } from 'solid-js';
import { BaseTool } from './BaseTool';
import { Tool } from './Tool';
import { createToolRenderer } from './ToolRenderer';

type ToolReminder = NamedTool<
'ListReminders',
'response'
>['data']['reminders'][number];

const ENTITY_TYPE_LABELS: Record<ReminderEntityType, string> = {
document: 'a document',
ai_chat: 'a chat',
project: 'a project',
email: 'an email thread',
channel: 'a channel',
call: 'a call',
calendar_event: 'a calendar event',
};

/** What the list call asked for, in the same voice as the notification tools. */
const formatReminderFilters = (filters: ListRemindersTool) => {
if (filters.reminderIds?.length) {
const count = filters.reminderIds.length;
return `${count} reminder${count === 1 ? '' : 's'} by id`;
}

const parts = [filters.completed ? 'done' : 'not done'];
if (filters.overdue != null) {
parts.push(filters.overdue ? 'overdue' : 'upcoming');
}

let text = `filtered by ${parts.join(' and ')}`;
if (filters.entityType) {
text += ` for ${ENTITY_TYPE_LABELS[filters.entityType]}`;
}
return text;
};

/**
* What an update actually changed, so the row is readable without expanding
* the arguments. Reads off the request rather than the response because the
* response is the merged reminder and no longer says which fields moved.
*/
const formatReminderUpdate = (update: UpdateReminderTool) => {
const changes: string[] = [];
if (update.completed === true) changes.push('mark done');
if (update.completed === false) changes.push('reopen');
if (update.remindAt)
changes.push(`move to ${formatDateAndTime(update.remindAt)}`);
if (update.description != null) changes.push('reword');
return changes.length > 0 ? changes.join(', ') : 'update';
};

const ReminderList = (props: { reminders: ToolReminder[] }) => (
<Tool.List>
<div class="max-h-60 overflow-y-auto overscroll-contain">
<For each={props.reminders}>
{(reminder) => (
<Tool.ListItem icon={<BellSimple class="size-4" />}>
<div class="flex min-w-0 items-center justify-between gap-3">
<span class="min-w-0 truncate text-ink">
{reminder.description}
</span>
<span
class="shrink-0 whitespace-nowrap text-xs"
classList={{
'text-ink-extra-muted': !reminder.overdue,
'text-ink-muted': reminder.overdue,
}}
>
{reminder.overdue ? 'Overdue · ' : ''}
{formatDateAndTime(reminder.nextRunAt)}
</span>
</div>
</Tool.ListItem>
)}
</For>
</div>
</Tool.List>
);

const listRemindersHandler = createToolRenderer({
name: 'ListReminders',
render: (ctx) => {
const [isExpanded, setIsExpanded] = createSignal(false);
const reminders = () => ctx.response?.data.reminders ?? [];
const hasResults = () => reminders().length > 0;
const statusText = () => {
if (!ctx.response) return undefined;
const count = reminders().length;
if (count === 0) return 'No Results';
return `${count} reminder${count === 1 ? '' : 's'}`;
};
Comment on lines +99 to +104

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Show the backend response summary.

Lines 101-103 reconstruct the status from the current page length. The backend summary reports the total count, overdue count, and whether more results exist. This status hides those values when the response is paginated.

Proposed fix
 const statusText = () => {
   if (!ctx.response) return undefined;
-  const count = reminders().length;
-  if (count === 0) return 'No Results';
-  return `${count} reminder${count === 1 ? '' : 's'}`;
+  return ctx.response.data.summary;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const statusText = () => {
if (!ctx.response) return undefined;
const count = reminders().length;
if (count === 0) return 'No Results';
return `${count} reminder${count === 1 ? '' : 's'}`;
};
const statusText = () => {
if (!ctx.response) return undefined;
return ctx.response.data.summary;
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/lib/core/component/AI/component/tool/Reminders.tsx` around lines
99 - 104, Update statusText in Reminders to use the backend response summary
fields from ctx.response instead of deriving text from reminders().length.
Display the reported total count, overdue count, and whether more results exist,
preserving the existing no-response handling.


return (
<BaseTool
align="start"
icon={BellSimple}
renderContext={ctx.renderContext}
type="call"
response={
hasResults() && isExpanded() ? (
<ReminderList reminders={reminders()} />
) : undefined
}
>
<div class="flex min-w-0 flex-1 flex-col gap-1">
<div class="flex min-w-0 items-center justify-between gap-3 overflow-hidden">
<span class="min-w-0 truncate">Read reminders</span>
<Tool.ResultToggle
expanded={isExpanded()}
onToggle={() => setIsExpanded((expanded) => !expanded)}
showToggle={hasResults()}
status={statusText()}
/>
</div>
<div class="min-w-0 truncate text-xs text-ink-placeholder">
{formatReminderFilters(ctx.tool.data)}
</div>
</div>
</BaseTool>
);
},
});

const createReminderHandler = createToolRenderer({
name: 'CreateReminder',
render: (ctx) => (
<BaseTool
align="start"
icon={BellSimple}
renderContext={ctx.renderContext}
type="call"
>
<div class="flex min-w-0 flex-1 flex-col gap-1">
<div class="flex min-w-0 items-center gap-1.5 overflow-hidden">
<span class="shrink-0">
{ctx.response ? 'Created reminder' : 'Create reminder'}
</span>
<span class="min-w-0 truncate text-ink">
{ctx.response?.data.description ?? ctx.tool.data.description}
</span>
</div>
<div class="min-w-0 truncate text-xs text-ink-placeholder">
{formatDateAndTime(
ctx.response?.data.nextRunAt ?? ctx.tool.data.remindAt
)}
<Show when={ctx.tool.data.entityType}>
{(entityType) => <> · about {ENTITY_TYPE_LABELS[entityType()]}</>}
</Show>
</div>
</div>
</BaseTool>
),
});

const updateReminderHandler = createToolRenderer({
name: 'UpdateReminder',
render: (ctx) => (
<BaseTool
align="start"
icon={ctx.tool.data.completed === true ? Check : BellSimple}
renderContext={ctx.renderContext}
type="call"
>
<div class="flex min-w-0 flex-1 flex-col gap-1">
<div class="flex min-w-0 items-center gap-1.5 overflow-hidden">
<span class="shrink-0">
{ctx.response ? 'Updated reminder' : 'Update reminder'}
</span>
<Show when={ctx.response?.data.description}>
{(description) => (
<span class="min-w-0 truncate text-ink">{description()}</span>
)}
</Show>
</div>
<div class="min-w-0 truncate text-xs text-ink-placeholder">
{formatReminderUpdate(ctx.tool.data)}
<Show when={ctx.response?.data.nextRunAt}>
{(nextRunAt) => <> · fires {formatDateAndTime(nextRunAt())}</>}
</Show>
</div>
</div>
</BaseTool>
),
});

const deleteReminderHandler = createToolRenderer({
name: 'DeleteReminder',
render: (ctx) => (
<BaseTool icon={Trash} renderContext={ctx.renderContext} type="call">
{ctx.response ? 'Deleted reminder' : 'Delete reminder'}
</BaseTool>
),
});

export {
createReminderHandler,
deleteReminderHandler,
listRemindersHandler,
updateReminderHandler,
};
10 changes: 10 additions & 0 deletions apps/web/src/lib/core/component/AI/component/tool/handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ import { readContentHandler } from './ReadContent';
import { readMetadataHandler } from './ReadMetadata';
import { readProjectHandler } from './ReadProject';
import { readThreadHandler } from './ReadThread';
import {
createReminderHandler,
deleteReminderHandler,
listRemindersHandler,
updateReminderHandler,
} from './Reminders';
import { renameDocumentHandler } from './RenameDocument';
import { contentSearchHandler, nameSearchHandler } from './Search';
import { listSkillsHandler, searchSkillsHandler } from './SearchSkills';
Expand Down Expand Up @@ -81,6 +87,7 @@ const toolHandlers: ToolHandlerMap<RenderContext> = {
ListLabels: listLabelsHandler,
ListSkills: listSkillsHandler,
ListNotifications: listNotificationsHandler,
ListReminders: listRemindersHandler,
ListTags: listTagsHandler,
ListTeamMembers: listTeamMembersHandler,
LoadTools: loadToolsHandler,
Expand All @@ -92,7 +99,9 @@ const toolHandlers: ToolHandlerMap<RenderContext> = {
ContentSearch: contentSearchHandler,
CreateDocument: createDocumentHandler,
CreateProject: createProjectHandler,
CreateReminder: createReminderHandler,
CreateTag: createTagHandler,
DeleteReminder: deleteReminderHandler,
DeleteTag: deleteTagHandler,
EditDocument: editDocumentHandler,
EditTag: editTagHandler,
Expand All @@ -117,6 +126,7 @@ const toolHandlers: ToolHandlerMap<RenderContext> = {
BulkSetEntityPropertyOptions: bulkSetEntityPropertyOptionsHandler,
Subagent: subagentHandler,
TextEditorCodeExecution: textEditorCodeExecutionHandler,
UpdateReminder: updateReminderHandler,
UpdateThreadLabels: updateThreadLabelsHandler,
WebFetch: webFetchHandler,
WebSearch: webSearchHandler,
Expand Down
Loading
Loading