diff --git a/README.md b/README.md index aa8a3048..f81b8f9a 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,7 @@ CopilotKit SDK telemetry collects usage metadata separately from conversation pe | Slack | Managed Channels SDK declaration with workspace and user allowlists | | Calls | WebRTC speech, delegated compute, bounded sessions, hangup, and timeline receipts | | Background work | Scheduled server-side turns in their original conversation, with pause and retry controls | +| Goals and reminders | SQLite-backed responsibilities, editable progress and status, and one dismissible one-time reminder per goal | | Browser | Separate read-only public-page service with page capture and navigation limits | | Dot computers | Per-Dot browser profiles, files, shell, takeover, permissions, and action records through OpenBot | | Connections | Per-Dot MCP servers, Composio connected apps, per-tool access, and owner approval for MCP write actions | @@ -221,13 +222,13 @@ Local checks cover setup, persistence, permissions, SDK failure handling, and br Automatic Learning routing and skill delivery are configured locally. Cloud schedules, eligible-thread counts, and published-skill delivery still need connected-service verification. Skills require review and publication in Intelligence; existing conversations without a container are not enrolled retroactively. -This is a single-owner starting point. Shared editing, invitations, file uploads, and interactive page embeds are not included. Schedules are recurring instructions, not a complete goal or event-trigger system. Specialist Dots have separate roles and conversations; multi-Dot group conversations and automatic delegation are further work. +This is a single-owner starting point. Shared editing, invitations, file uploads, and interactive page embeds are not included. Goals and their one-time reminders are managed in the workspace; due reminders appear there for the owner to dismiss, and do not send external notifications or trigger agent work. Recurring schedules remain a separate feature. Specialist Dots have separate roles and conversations; multi-Dot group conversations and automatic delegation are further work. ### Extending the template - Add identity, Space membership, and shared page editing for multi-user deployments. - Add file attachments and richer page content. -- Add event triggers and a persistent responsibility lifecycle. +- Add optional notification delivery for reminders with explicit provider setup and owner controls. - Extend tools and approval flows for your own workflows. - Add richer artifacts, connected-app context, and specialist coordination. diff --git a/docs/SETUP.md b/docs/SETUP.md index 80b0c40d..63342e2c 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -1,6 +1,6 @@ # Running the template -OpenDots runs a React app and a Node server. The server stores pages, Space and Dot configuration, and thread bindings in SQLite and connects to your configured conversation, model, and messaging services. +OpenDots runs a React app and a Node server. The server stores pages, Space and Dot configuration, thread bindings, goals, and reminder state in SQLite and connects to your configured conversation, model, and messaging services. ## Local development @@ -97,6 +97,12 @@ Use the conversation's save-to-page action to create a document from its saved t Back up both storage layers: SQLite contains page content and thread bindings; the Intelligence project contains conversation history. The template does not include multi-user page sharing, realtime collaboration, file uploads, or arbitrary interactive embeds. +## Goals and reminders + +Open **Goals & reminders** in the workspace to create responsibilities, add notes, track progress from 0–100%, and set a one-time reminder. Edit a goal to change its status (active, paused, completed, or cancelled), progress, or reminder time. When an active or paused goal's reminder becomes due, it is highlighted in the workspace; dismissing it is persisted. Changing or clearing the reminder resets its dismissed state. + +Goals and reminder timestamps are stored in the configured SQLite database and survive server restarts. They are separate from recurring scheduled turns. Due reminders are only shown in the app while it is open; OpenDots does not send email, push, Slack, or other external notifications for them and does not start agent work when they become due. + ## Browser tool Parallel is selected by default (`WEB_SEARCH_PROVIDER=parallel`). Live research needs the model configuration, but no browser worker. An optional server-side `PARALLEL_API_KEY` enables authenticated usage and higher limits. The anonymous MCP endpoint is free for light use. Queries, selected URLs, research objectives and a stable session identifier go to Parallel. See [public-web research](../README.md#public-web-research) for data sharing, permissions and limitations. diff --git a/src/client/App.tsx b/src/client/App.tsx index e5cc6466..3bed7ed3 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -20,6 +20,7 @@ import { Plus, Search, Settings2, + Target, Trash2, X, } from 'lucide-react'; @@ -46,6 +47,7 @@ import { ThreadList } from './ThreadList'; import { ResultPane } from './ResultPane'; import { TaskRow } from './TaskPresentation'; import { TaskActions } from './TaskActions'; +import { GoalsPanel } from './GoalsPanel'; import { WorkspaceDialog, type Dialog } from './WorkspaceDialog'; import { submitComposerOnEnter } from './chat-composer'; @@ -62,9 +64,9 @@ export function App() { const [workspace, setWorkspace] = useState(); const [selectedDot, setSelectedDot] = useState(''); const [selectedThread, setSelectedThread] = useState(); - const [view, rawSetView] = useState<'chat' | 'tasks' | 'memories' | 'space'>( - 'chat', - ); + const [view, rawSetView] = useState< + 'chat' | 'tasks' | 'goals' | 'memories' | 'space' + >('chat'); const dirtyPage = useRef(false); const [spaceId, setSpaceId] = useState(''); const [pageId, setPageId] = useState(); @@ -472,6 +474,17 @@ export function App() { )}
+
@@ -775,12 +790,16 @@ export function App() {

{view === 'memories' ? 'Memories' - : 'A little follow-through.'} + : view === 'goals' + ? 'Goals & reminders' + : 'A little follow-through.'}

{view === 'memories' ? 'Preferences you choose to share with your Dots.' - : 'Scheduled turns run on the server in their original conversation.'} + : view === 'goals' + ? 'Keep responsibilities, progress, and one-time reminders in your workspace.' + : 'Scheduled turns run on the server in their original conversation.'}

{view === 'memories' && ( @@ -793,7 +812,9 @@ export function App() { )} - {view === 'memories' ? ( + {view === 'goals' ? ( + + ) : view === 'memories' ? ( <>
{state.memories.map((memory) => ( diff --git a/src/client/GoalsPanel.tsx b/src/client/GoalsPanel.tsx new file mode 100644 index 00000000..46c5b7e4 --- /dev/null +++ b/src/client/GoalsPanel.tsx @@ -0,0 +1,358 @@ +import { useState, type FormEvent } from 'react'; +import { + BellOff, + CalendarClock, + Check, + Circle, + Pencil, + Plus, + Trash2, +} from 'lucide-react'; +import type { Goal, GoalStatus } from '../shared/types'; + +type Draft = Pick & { + reminder: string; +}; + +const toLocalInput = (timestamp: number | null) => { + if (timestamp === null) return ''; + const local = new Date( + timestamp - new Date(timestamp).getTimezoneOffset() * 60_000, + ); + return local.toISOString().slice(0, 16); +}; + +const fromLocalInput = (value: string) => (value ? Date.parse(value) : null); + +function reminderText(goal: Goal, now: number) { + if (goal.reminderAt === null) return 'No reminder'; + if (goal.reminderDismissedAt !== null) return 'Reminder dismissed'; + if (goal.status === 'completed' || goal.status === 'cancelled') + return `Reminder inactive (${goal.status})`; + if ( + goal.reminderAt <= now && + (goal.status === 'active' || goal.status === 'paused') + ) + return 'Reminder due'; + return `Reminder ${new Date(goal.reminderAt).toLocaleString()}`; +} + +export function GoalsPanel({ + goals, + mutate, +}: { + goals: Goal[]; + mutate: (path: string, method: string, body?: unknown) => Promise; +}) { + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [reminder, setReminder] = useState(''); + const [editing, setEditing] = useState(); + const [draft, setDraft] = useState(); + const [search, setSearch] = useState(''); + const [dateError, setDateError] = useState(''); + + const createGoal = async (event: FormEvent) => { + event.preventDefault(); + const reminderAt = fromLocalInput(reminder); + if (reminder && !Number.isFinite(reminderAt)) { + setDateError('Choose a valid reminder time.'); + return; + } + if ( + await mutate('/goals', 'POST', { + title, + description, + reminderAt, + }) + ) { + setTitle(''); + setDescription(''); + setReminder(''); + setDateError(''); + } + }; + + const saveGoal = async (event: FormEvent, id: string) => { + event.preventDefault(); + if (!draft) return; + const reminderAt = fromLocalInput(draft.reminder); + if (draft.reminder && !Number.isFinite(reminderAt)) { + setDateError('Choose a valid reminder time.'); + return; + } + if ( + await mutate(`/goals/${id}`, 'PUT', { + title: draft.title, + description: draft.description, + status: draft.status, + progress: draft.progress, + reminderAt, + }) + ) { + setEditing(undefined); + setDraft(undefined); + setDateError(''); + } + }; + + const beginEdit = (goal: Goal) => { + setEditing(goal.id); + setDraft({ + title: goal.title, + description: goal.description, + status: goal.status, + progress: goal.progress, + reminder: toLocalInput(goal.reminderAt), + }); + }; + const visibleGoals = goals.filter((goal) => + `${goal.title} ${goal.description}` + .toLowerCase() + .includes(search.toLowerCase()), + ); + + return ( +
+
void createGoal(event)} + > + +