Skip to content
Merged
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
63 changes: 63 additions & 0 deletions docs/features/scheduled-tasks/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Implementation Plan

## Architecture

- **Shared types** (`src/shared/scheduledTasks.ts`) define the `Trigger`,
`Action`, `ScheduledTask`, and `ScheduledTasksSettings` shapes.
- **Route contracts** (`src/shared/contracts/routes/scheduledTasks.routes.ts`)
expose `scheduledTasks.{list,upsert,delete,toggle,fireNow}` via Zod
schemas, mirroring `onboarding.routes.ts`.
- **Persistence** is handled in `ConfigPresenter` (`get/setScheduledTasks`)
with a `normalizeScheduledTasksConfig` pass identical in pattern to
`normalizeHooksNotificationsConfig`.
- **Scheduling** lives in a new `ScheduledTasksService`
(`src/main/presenter/scheduledTasks/index.ts`). One `setTimeout` per
armed task, chained at most 12h at a time. Public surface:
- `start()` — read tasks, run startup pass (one-shot backfill, arm next
slot for recurring), called from the existing lifecycle init flow.
- `stop()` — clear all armed timers (called on app shutdown).
- `list()` / `upsert(task)` / `delete(id)` / `toggle(id, enabled)` /
`fireNow(id)` — back the IPC routes and rearm timers on mutation.
- `computeNextFireAt(task, after)` — pure function, exported for tests.
- **Action dispatch** is a small helper inside the service: switch on
`task.action.kind`, then call `notificationPresenter` and/or
`eventBus.sendToRenderer(DEEPLINK_EVENTS.START, ...)` and/or
`sessionService.createSession(...)`.

## Wiring

- `Presenter` constructor (`src/main/presenter/index.ts`) instantiates
`ScheduledTasksService` next to `hooksNotifications`, passing it
`configPresenter`, `notificationPresenter`, `windowPresenter`, and a
thunk that resolves `sessionService` lazily (the route runtime owns
sessionService, so the service exposes a setter the route runtime calls
during bootstrap).
- `src/main/routes/index.ts` wires the five new route cases against
`runtime.scheduledTasksService` and, in the same place that constructs
the runtime, sets the service's session-service reference so auto-send
has somewhere to call.
- Lifecycle `after-start` hook invokes `scheduledTasksService.start()`
after the other presenters have come up; the existing `beforeQuit` hook
calls `stop()`.

## UI

- Settings navigation adds `settings-scheduled-tasks` (group `tools`,
position 5.6, icon `lucide:clock-9`).
- `ScheduledTasksSettings.vue` mirrors `NotificationsHooksSettings.vue`:
ScrollArea + header + "新建任务" button + bordered cards per task.
- A renderer client `ScheduledTasksClient.ts` matches the
`OnboardingClient` shape.
- i18n keys go in every locale under `routes.settings-scheduled-tasks` and
`settings.scheduledTasks.*` so `pnpm run i18n` stays green.

## Validation

- `pnpm run format`
- `pnpm run i18n`
- `pnpm run lint`
- `pnpm run typecheck`
- Unit tests in `test/main/presenter/scheduledTasks.test.ts` cover
`computeNextFireAt` (daily wrap, weekly across the week, one-shot past
with/without `lastFiredAt`) and `normalizeScheduledTasksConfig`
(drops malformed entries, deduplicates ids, preserves valid ones).
59 changes: 59 additions & 0 deletions docs/features/scheduled-tasks/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Scheduled Tasks

## Problem

Closes [#1567](https://github.com/ThinkInAIXYZ/deepchat/issues/1567).

Users want to schedule "reminders" and "planned tasks" inside DeepChat —
either a plain notification ("drink water at 4pm every day") or a scheduled
chat prompt ("every morning at 9 ask the deepchat agent for today's plan").
No equivalent feature exists today; the only time-aware paths are
`hooksNotifications` (event-driven, not time-driven) and the deeplink
"start" flow (which has `autoSend` security-disabled, so it can only
prefill a chat draft).

## User Story

As a DeepChat user, I want to create a scheduled task with a trigger time
(once at a specific datetime, daily, or weekly on a chosen day) and an
action (raise a system notification, prefill a new chat thread with a
preset prompt, or auto-send a preset prompt to a chosen agent/model). I
want my tasks to persist across app restarts; one-shot tasks that I missed
because the app was closed should still fire on next launch.

## Acceptance Criteria

- A "定时任务" entry exists in Settings → Tools (between Notifications &
Hooks and Plugins). It lists, creates, edits, toggles, deletes, and
manually fires user-defined scheduled tasks.
- Each task has:
- A name and an enabled toggle.
- A trigger of one of three kinds: `once` (a specific datetime),
`daily` (hour + minute), or `weekly` (day-of-week + hour + minute).
- An action of one of two kinds: `notify` (title + body for the system
notification) or `prompt` (notification title + chat message + optional
agent / provider / model / system prompt + `autoSend` toggle).
- When a task fires:
- `notify`: a system notification appears via `notificationPresenter`,
subject to the existing `notificationsEnabled` config.
- `prompt` with `autoSend = false`: a system notification appears and the
main window's new-thread page receives the deeplink-start payload,
prefilling the chat input.
- `prompt` with `autoSend = true`: `sessionService.createSession` is
invoked directly using the configured agent/provider/model, so the LLM
actually responds without user interaction. A notification is raised
when the session is created.
- One-shot tasks whose `firesAt` was in the past at launch and that have no
`lastFiredAt` recorded are fired once on startup (backfill). Recurring
tasks are not backfilled — they simply jump to the next slot.
- Task records survive app restart (persisted through `ConfigPresenter`'s
ElectronStore as the `scheduledTasks` key).

## Non-goals

- Cron-expression input. Daily / weekly / once is sufficient for the
feature ask; a future iteration may add `cron` and `interval` trigger
kinds.
- Per-task timezone handling. Triggers use the OS's local time.
- Letting the LLM schedule tasks via an MCP tool. Possible follow-up.
- Calendar / iCal export.
16 changes: 16 additions & 0 deletions docs/features/scheduled-tasks/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Tasks

- [x] Add SDD artifacts.
- [x] Define shared types (`src/shared/scheduledTasks.ts`) and route contracts.
- [x] Implement `ScheduledTasksService` (presenter + `computeNextFireAt` + action dispatch).
- [x] Wire `ConfigPresenter` persistence (`scheduledTasks` key) with normalize-on-read.
- [x] Register routes in `src/main/routes/index.ts` and instantiate in `Presenter` constructor.
- [x] Hook lifecycle: start on `after-start`, stop on `beforeQuit`.
- [x] Add renderer client `src/renderer/api/ScheduledTasksClient.ts`.
- [x] Add settings navigation entry and dynamic route component.
- [x] Implement `ScheduledTasksSettings.vue` (CRUD, mirror NotificationsHooks layout).
- [x] Add i18n keys across all locales.
- [x] Unit tests for `computeNextFireAt` and `normalizeScheduledTasksConfig`.
- [x] Add service tests for notification firing, one-shot disable, and prompt auto-send dispatch.
- [x] Run `pnpm run format`, `pnpm run i18n`, `pnpm run lint`, `pnpm run typecheck`.
- [x] Address PR review comments without changing scheduled-task behavior.
25 changes: 25 additions & 0 deletions docs/issues/scheduled-task-prompt-picker-ux/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Plan

## Approach

- Keep the fix scoped to `src/renderer/settings/components/ScheduledTasksSettings.vue`.
- Make the settings page feel less cramped by separating the page header, empty state, task header, trigger panel, and action panel with clearer surfaces and spacing.
- Simplify visual weight by using softer panels, clearer section headers, and responsive control groups instead of dense nested card chrome.
- Keep each task card scannable with a compact status/summary row, a prominent editable name, and action buttons that wrap cleanly on smaller widths.
- Change the prompt action control grid so each column can shrink (`min-w-0`) and stacks earlier when space is tight.
- Force select triggers/buttons to use `w-full min-w-0` and truncate visible labels.
- Replace the model ID input with a `Popover` + existing `ModelSelect` picker.
- Resolve the displayed model name through `useModelStore.enabledModels`, with a fallback to the stored model ID.
- Close the model popover after selection and persist via the existing task upsert path.

## Data Flow

- Existing task action stores `providerId` and `modelId`.
- Agent selection may set both fields from `agent.config.defaultModelPreset`.
- Model picker selection updates `providerId` and `modelId` explicitly.

## Validation

- Run formatting and lint checks required by repository guidelines.
- Run i18n validation and ensure any new user-facing text is backed by locale keys.
- Review the changed template for responsive truncation and no new raw strings.
28 changes: 28 additions & 0 deletions docs/issues/scheduled-task-prompt-picker-ux/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Scheduled Task Prompt Picker UX

## User Story

As a user configuring scheduled tasks, I want the task list to feel clean, balanced, and easy to scan, and I want prompt agent/model controls to fit cleanly in the task card with selectable options, so I can configure automation without overlapping fields or manually typing model IDs.

## Acceptance Criteria

- Scheduled task settings use a cleaner, more balanced layout with readable hierarchy between page header, task header, trigger settings, and action settings.
- Task cards avoid overly heavy nested borders and keep controls aligned in a responsive two-panel composition.
- Agent and model controls in prompt actions do not overlap at common settings window widths.
- Long agent names or IDs are truncated inside their control instead of expanding the grid column.
- The model field is a selectable model picker populated from enabled models, not a free-form text input.
- Selecting an agent still applies its default model preset when available.
- Selecting a model persists both `providerId` and `modelId` for the scheduled task.
- Existing notify actions, trigger controls, and prompt text fields keep their current behavior.

## Non-Goals

- No changes to scheduled task execution semantics.
- No new model filtering rules beyond excluding ACP and using enabled models.
- No new IPC or persistence schema changes.

## Constraints

- Follow existing Vue 3 Composition API and shadcn/Tailwind patterns.
- Reuse existing model picker components where possible.
- Avoid new user-facing strings unless required.
9 changes: 9 additions & 0 deletions docs/issues/scheduled-task-prompt-picker-ux/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Tasks

- [x] Locate scheduled task prompt action UI.
- [x] Specify layout and model picker acceptance criteria.
- [x] Update prompt action agent/model controls.
- [x] Refine scheduled task card layout and i18n-backed UI copy.
- [x] Run required format/i18n/lint checks.
- [x] Rebalance scheduled task settings layout surfaces and spacing.
- [x] Re-run i18n validation after layout changes.
5 changes: 5 additions & 0 deletions docs/issues/scheduled-tasks-clone-error/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Implementation Plan

- Convert scheduled task trigger/action values to plain objects before invoking the scheduled task IPC route.
- Keep the change local to `ScheduledTasksSettings.vue` so route contracts and main-process logic remain unchanged.
- Validate with formatting, typecheck, lint, and the focused scheduled tasks test suite.
13 changes: 13 additions & 0 deletions docs/issues/scheduled-tasks-clone-error/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Scheduled Tasks Clone Error

## Problem

Editing a scheduled task in Settings can fail with `Error: An object could not be cloned.` from `ScheduledTasksSettings.vue` when persisting a task.

## Cause

The settings page stores tasks in Vue reactive state. `persistTask` forwards `task.trigger` and `task.action` directly to the IPC client. Those nested objects can be Vue proxies, which are not structured-cloneable by Electron IPC.

## Expected Behavior

Persisting an edited scheduled task sends a plain serializable payload to the route and succeeds without clone errors.
6 changes: 6 additions & 0 deletions docs/issues/scheduled-tasks-clone-error/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Tasks

- [x] Add SDD issue artifacts.
- [x] Serialize scheduled task edit payloads before IPC.
- [x] Run focused scheduled tasks test and typecheck.
- [x] Run i18n and lint.
6 changes: 6 additions & 0 deletions docs/issues/scheduled-tasks-real-dispatch/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Implementation Plan

- Reuse `ChatService` in the route runtime and call `sendMessage` after creating a scheduled task session.
- Load agents with `ConfigClient.listAgents()` in `ScheduledTasksSettings.vue`.
- Replace the raw agent ID input with an agent select and persist the selected agent plus its default provider/model preset.
- Keep manual model override available for users who need it.
11 changes: 11 additions & 0 deletions docs/issues/scheduled-tasks-real-dispatch/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Scheduled Tasks Real Dispatch

## Problem

Scheduled task prompt actions feel fake: `autoSend` creates a session but does not actually send the prompt to the agent, and the settings UI requires users to type raw agent/model IDs by hand.

## Expected Behavior

- `autoSend` creates a session and sends the configured message through the normal chat pipeline.
- The settings UI lists real enabled agents so users can select a target instead of guessing IDs.
- Selecting an agent carries over its default model preset when available.
6 changes: 6 additions & 0 deletions docs/issues/scheduled-tasks-real-dispatch/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Tasks

- [x] Add SDD issue artifacts.
- [x] Send prompt after scheduled auto-send session creation.
- [x] Use real enabled agents in scheduled task settings.
- [x] Run formatting, lint, typecheck, i18n, and focused tests.
24 changes: 23 additions & 1 deletion src/main/presenter/configPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ import {
createDefaultHooksNotificationsConfig,
normalizeHooksNotificationsConfig
} from '../hooksNotifications/config'
import { normalizeScheduledTasksConfig } from '../scheduledTasks/normalize'
import {
createDefaultScheduledTasksSettings,
type ScheduledTasksSettings
} from '@shared/scheduledTasks'
import {
AcpDbStore,
AppSettingsDbBackedStore,
Expand Down Expand Up @@ -131,6 +136,7 @@ interface IAppSettings {
enableSkills?: boolean // Skills system global toggle
skillDraftSuggestionsEnabled?: boolean // Whether agent may propose skill drafts after tasks
hooksNotifications?: HooksNotificationsSettings // Hooks & notifications settings
scheduledTasks?: ScheduledTasksSettings // User-defined scheduled tasks
defaultModel?: { providerId: string; modelId: string } // Default model for new conversations
defaultVisionModel?: { providerId: string; modelId: string } // Legacy vision model setting for migration only
defaultProjectPath?: string | null
Expand Down Expand Up @@ -434,7 +440,8 @@ export class ConfigPresenter implements IConfigPresenter {
skillDraftSuggestionsEnabled: false,
updateChannel: 'stable', // Default to stable version
appVersion: this.currentAppVersion,
hooksNotifications: createDefaultHooksNotificationsConfig()
hooksNotifications: createDefaultHooksNotificationsConfig(),
scheduledTasks: createDefaultScheduledTasksSettings()
}
})

Expand Down Expand Up @@ -3191,6 +3198,21 @@ export class ConfigPresenter implements IConfigPresenter {
return normalized
}

getScheduledTasksConfig(): ScheduledTasksSettings {
const raw = this.store.get('scheduledTasks')
const normalized = normalizeScheduledTasksConfig(raw)
if (!raw || JSON.stringify(raw) !== JSON.stringify(normalized)) {
this.store.set('scheduledTasks', normalized)
}
return normalized
}

setScheduledTasksConfig(config: ScheduledTasksSettings): ScheduledTasksSettings {
const normalized = normalizeScheduledTasksConfig(config)
this.store.set('scheduledTasks', normalized)
return normalized
Comment thread
zhangmo8 marked this conversation as resolved.
}

async testHookCommand(hookId: string): Promise<HookTestResult> {
return await presenter.hooksNotifications.testHookCommand(hookId)
}
Expand Down
Loading