Skip to content
Documentation GitHub
Agent

Agent Proactive Suggestions

Agent Proactive Suggestions

Covers the proactive suggestion system: the SuggestionList and SuggestionCard UI components that surface agent-generated workspace insights, the backend queue commands (get_pending_suggestions, dismiss_suggestion, dismiss_all_suggestions), the three-condition gate that controls when suggestions are generated (proactive enabled, agent running, agent not streaming), the configurable scheduling interval (proactive_interval_minutes), and the disabling path when the agent feature is turned off. This spec is P2 because proactive suggestions are a forward-looking premium capability — they enhance the experience for configured users but the core app functions without them.

The suggestion queue is an in-memory RwLock-backed store in the Tauri process. Suggestions are generated by the ProactiveSchedulerTask on a configurable interval (5–120 minutes, default 30) and pushed to the queue. The frontend listens for agent:proactive-suggestion Tauri events and fetches the current queue via get_pending_suggestions. Dismissal marks a suggestion’s dismissed flag — it is not removed from the queue but is excluded from get_pending.

Preconditions

  • HTTP bridge running on port 9990
  • A workspace initialized via initialize_workspace before each scenario
  • Bridge shim injected via playwright.config.ts
  • The HTTP bridge exposes get_pending_suggestions, dismiss_suggestion, and dismiss_all_suggestions routes. The SuggestionList and SuggestionCard UI components are frontend-only and require the Playwright browser context to observe rendered output.

Scenarios

Seed: seed.spec.ts

1. Empty suggestion queue returns empty list

On a fresh workspace with no agent activity, the suggestion queue is empty.

Steps:

  1. Initialize a workspace.
  2. Call get_pending_suggestions via the bridge.

Expected: The response is an empty JSON array ([]). No suggestions are shown. The SuggestionList component renders nothing (it returns null when suggestions.length === 0).

2. Suggestion card appears when a suggestion is queued

When the backend pushes a suggestion to the queue and emits agent:proactive-suggestion, the SuggestionList re-fetches and renders the card.

Steps:

  1. Initialize a workspace.
  2. Inject a test suggestion directly into the queue via the bridge (or trigger conditions that cause the scheduler to fire — proactive enabled, agent running, agent not streaming).
  3. Observe the [data-testid="suggestion-list"] element in the rendered UI.
  4. Observe the first [data-testid="suggestion-card"] inside the list.

Expected: [data-testid="suggestion-list"] is present in the DOM. At least one [data-testid="suggestion-card"] is visible. The card shows the summary text and detail text from the suggestion. The skill label (e.g., “Organization”, “Consistency”, or “Relationships”) is displayed in the card header.

3. Dismissing a single suggestion removes it from the pending list

Clicking “Dismiss” on a suggestion card calls dismiss_suggestion and the card disappears.

Steps:

  1. Initialize a workspace.
  2. Ensure at least two suggestions are in the queue (inject two test suggestions).
  3. Observe the initial suggestion count in the SuggestionList header (e.g., “Suggestions (2)”).
  4. Click “Dismiss” on the first suggestion card.
  5. Observe the SuggestionList after dismissal.

Expected: The dismissed card disappears from the list. The SuggestionList header updates to “Suggestions (1)”. The second suggestion is still visible. Calling get_pending_suggestions via the bridge confirms only one suggestion is returned (the dismissed one is excluded from get_pending).

4. Dismiss all suggestions clears the list

The “Dismiss all” button calls dismiss_all_suggestions and the entire list disappears.

Steps:

  1. Initialize a workspace.
  2. Ensure at least two suggestions are in the queue.
  3. Observe the “Dismiss all” button in the SuggestionList header (it is only shown when suggestions.length > 1).
  4. Click “Dismiss all”.
  5. Observe the SuggestionList.

Expected: All suggestion cards disappear. [data-testid="suggestion-list"] is no longer in the DOM (the component returns null when suggestions.length === 0). Calling get_pending_suggestions returns an empty array. The underlying queue still contains the suggestions (total len() is unchanged) — they are marked dismissed, not deleted.

5. Dismissed suggestion is excluded from pending but not destroyed

Dismissal marks the dismissed flag rather than removing the record from the queue.

Steps:

  1. Initialize a workspace.
  2. Push one suggestion and record its id.
  3. Call dismiss_suggestion with that id via the bridge.
  4. Call get_pending_suggestions via the bridge.

Expected: get_pending_suggestions returns an empty array. dismiss_suggestion returned true (found and dismissed). The suggestion was NOT deleted from the in-memory store — it was only flagged. This is confirmed by the total queue len() still being 1 (the dismissed item is counted but excluded from pending).

6. Dismissing a non-existent suggestion ID returns false

dismiss_suggestion with an unknown UUID does not crash and returns false.

Steps:

  1. Initialize a workspace.
  2. Call dismiss_suggestion with a valid UUID format that does not correspond to any queued suggestion (e.g., a freshly generated UUID not previously assigned to a suggestion).
  3. Call get_pending_suggestions.

Expected: dismiss_suggestion returns false (not found). No error is thrown. The suggestion queue is unaffected — get_pending_suggestions returns the same suggestions as before.

7. Dismissing with invalid UUID format returns a validation error

dismiss_suggestion rejects non-UUID strings before touching the queue.

Steps:

  1. Initialize a workspace.
  2. Call dismiss_suggestion with suggestion_id: "not-a-uuid".

Expected: The command returns a validation error (not an internal error). The error message mentions “Invalid suggestion ID”. No suggestions in the queue are affected.

8. Suggestion only triggers when all three conditions are met

The proactive scheduler does not generate a suggestion unless: proactive_enabled is true, the agent is running, and the agent is not streaming.

Steps:

  1. Initialize a workspace.
  2. Configure agent settings with proactive_enabled: false (all other conditions met). Trigger a scheduler cycle. Observe the queue.
  3. Configure proactive_enabled: true but with the agent not running. Trigger a scheduler cycle. Observe the queue.
  4. Configure proactive_enabled: true, agent running, but agent streaming. Trigger a scheduler cycle. Observe the queue.
  5. Configure proactive_enabled: true, agent running, agent not streaming. Trigger a scheduler cycle. Observe the queue.

Expected: After steps 2, 3, and 4: get_pending_suggestions returns an empty array — no suggestion was generated. After step 5: get_pending_suggestions returns at least one suggestion. This validates the ProactiveConditions.should_trigger gate.

9. Proactive interval configuration is read from agent settings

The interval at which the scheduler fires is controlled by agent.proactive_interval_minutes in settings (5–120, default 30).

Steps:

  1. Initialize a workspace.
  2. Call get_settings and confirm agent.proactive_interval_minutes is 30 (default).
  3. Update agent settings to set proactive_interval_minutes: 60.
  4. Call get_settings and confirm the updated value.

Expected: get_settings returns agent.proactive_interval_minutes: 30 initially. After update, it returns 60. The task runner’s cached interval is set at construction from this value — the scheduler will fire every 60 minutes after the next app or task restart. Note: the interval is cached at task construction; runtime changes to settings take effect only after the scheduler is restarted.

10. Interval below minimum is clamped to 5 minutes

Setting proactive_interval_minutes below the 5-minute minimum clamps to MIN_PROACTIVE_INTERVAL.

Steps:

  1. Initialize a workspace.
  2. Update agent settings to set proactive_interval_minutes: 1 (below the 5-minute minimum).
  3. Call get_settings and read the stored value.

Expected: The stored proactive_interval_minutes is clamped to 5 (the MIN_PROACTIVE_INTERVAL constant). No error is thrown — the value is silently clamped. The interval cannot be set to a value that would cause the scheduler to run more than once every 5 minutes.

11. Interval above maximum is clamped to 120 minutes

Setting proactive_interval_minutes above the 120-minute maximum clamps to MAX_PROACTIVE_INTERVAL.

Steps:

  1. Initialize a workspace.
  2. Update agent settings to set proactive_interval_minutes: 999 (above the 120-minute maximum).
  3. Call get_settings and read the stored value.

Expected: The stored proactive_interval_minutes is clamped to 120 (the MAX_PROACTIVE_INTERVAL constant). The scheduler will fire at most every 120 minutes.

12. Suggestions survive page navigation

A queued suggestion is not cleared when the user navigates between pages.

Steps:

  1. Initialize a workspace.
  2. Create two pages: “Page Alpha” and “Page Beta”.
  3. Inject a suggestion into the queue.
  4. Navigate to “Page Alpha” in the editor.
  5. Call get_pending_suggestions.
  6. Navigate to “Page Beta” in the editor.
  7. Call get_pending_suggestions.

Expected: get_pending_suggestions returns the same suggestion after both navigations. The queue is in-memory in the Tauri process and is not reset by page navigation. The SuggestionList component re-fetches on mount and on agent:proactive-suggestion events — not on navigation — so previously fetched suggestions persist across navigations.

13. SuggestionList renders “Dismiss all” only when more than one suggestion is present

The “Dismiss all” button appears only when the list has two or more suggestions.

Steps:

  1. Initialize a workspace.
  2. Ensure exactly one suggestion is in the queue.
  3. Observe the SuggestionList header for the “Dismiss all” button.
  4. Add a second suggestion.
  5. Observe the SuggestionList header again.

Expected: With one suggestion, the “Dismiss all” button is NOT rendered in the header. With two or more suggestions, the “Dismiss all” button IS rendered. This matches the suggestions.length > 1 condition in SuggestionList.

Test Data

KeyValueNotes
default_interval_minutes30Default proactive interval from DEFAULT_PROACTIVE_INTERVAL
min_interval_minutes5MIN_PROACTIVE_INTERVAL — below this clamps up
max_interval_minutes120MAX_PROACTIVE_INTERVAL — above this clamps down
initial_delay_seconds60Scheduler waits 60s before first check after app start
jitter_percent15Applied to interval to prevent thundering herd
suggestion_list_testidsuggestion-listdata-testid on the SuggestionList wrapper
suggestion_card_testidsuggestion-carddata-testid on each SuggestionCard
skill_organizationproactive_organization_auditInternal name → displayed as “Organization”
skill_consistencyproactive_consistency_checkInternal name → displayed as “Consistency”
skill_relationshipsproactive_relationship_discoveryInternal name → displayed as “Relationships”
placeholder_skill_nameworkspace_analysisSlice 3 placeholder used until real skill evaluation ships

Notes

  • The SuggestionQueue is in-memory and not persisted to SQLite. Restarting the Tauri process clears all suggestions. Tests that push suggestions must do so within the same app session.
  • The scheduler has a 60-second initial delay before the first proactive check. Tests cannot rely on the scheduler firing automatically — they should inject suggestions directly via the bridge or by calling commands that simulate a scheduler cycle. The scheduler interval is also cached at task construction, not re-read on every tick (INK-509).
  • The ProactiveConditions.should_trigger gate checks three flags: proactive_enabled (from settings agent.proactive_enabled), agent_is_running (active session exists), and agent_is_streaming (currently generating tokens). All three must pass for a suggestion to be generated.
  • The SuggestionList component fetches suggestions on mount AND on every agent:proactive-suggestion Tauri event. It does not poll on a timer. Tests that inject suggestions via the bridge may need to emit a fake agent:proactive-suggestion event or trigger a re-render to see the updated list without waiting for the real Tauri event.
  • “Dismiss all” is only visible when suggestions.length > 1. With exactly one suggestion, only the per-card “Dismiss” button is shown.
  • The suggested_action field is optional. When present, the SuggestionCard renders a “View suggestion” button. When absent (as in the Slice 3 placeholder suggestion), no action button is shown.
  • Slice 3 placeholder suggestions use skill_name: "workspace_analysis" and do not have a suggested_action. Real skill-based suggestions (Slice 4) will use specific skill names (proactive_organization_audit, etc.) and may include action slugs.
  • Agent settings are nested under settings.agent as an optional struct. If settings.agent is null (unconfigured), proactive_enabled defaults to false and no suggestions will ever be generated, regardless of agent running state.

Was this page helpful?