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_workspacebefore each scenario - Bridge shim injected via
playwright.config.ts - The HTTP bridge exposes
get_pending_suggestions,dismiss_suggestion, anddismiss_all_suggestionsroutes. TheSuggestionListandSuggestionCardUI 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:
- Initialize a workspace.
- Call
get_pending_suggestionsvia 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:
- Initialize a workspace.
- 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).
- Observe the
[data-testid="suggestion-list"]element in the rendered UI. - 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:
- Initialize a workspace.
- Ensure at least two suggestions are in the queue (inject two test suggestions).
- Observe the initial suggestion count in the
SuggestionListheader (e.g., “Suggestions (2)”). - Click “Dismiss” on the first suggestion card.
- Observe the
SuggestionListafter 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:
- Initialize a workspace.
- Ensure at least two suggestions are in the queue.
- Observe the “Dismiss all” button in the
SuggestionListheader (it is only shown whensuggestions.length > 1). - Click “Dismiss all”.
- 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:
- Initialize a workspace.
- Push one suggestion and record its
id. - Call
dismiss_suggestionwith thatidvia the bridge. - Call
get_pending_suggestionsvia 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:
- Initialize a workspace.
- Call
dismiss_suggestionwith a valid UUID format that does not correspond to any queued suggestion (e.g., a freshly generated UUID not previously assigned to a suggestion). - 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:
- Initialize a workspace.
- Call
dismiss_suggestionwithsuggestion_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:
- Initialize a workspace.
- Configure agent settings with
proactive_enabled: false(all other conditions met). Trigger a scheduler cycle. Observe the queue. - Configure
proactive_enabled: truebut with the agent not running. Trigger a scheduler cycle. Observe the queue. - Configure
proactive_enabled: true, agent running, but agent streaming. Trigger a scheduler cycle. Observe the queue. - 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:
- Initialize a workspace.
- Call
get_settingsand confirmagent.proactive_interval_minutesis30(default). - Update agent settings to set
proactive_interval_minutes: 60. - Call
get_settingsand 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:
- Initialize a workspace.
- Update agent settings to set
proactive_interval_minutes: 1(below the 5-minute minimum). - Call
get_settingsand 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:
- Initialize a workspace.
- Update agent settings to set
proactive_interval_minutes: 999(above the 120-minute maximum). - Call
get_settingsand 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:
- Initialize a workspace.
- Create two pages: “Page Alpha” and “Page Beta”.
- Inject a suggestion into the queue.
- Navigate to “Page Alpha” in the editor.
- Call
get_pending_suggestions. - Navigate to “Page Beta” in the editor.
- 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:
- Initialize a workspace.
- Ensure exactly one suggestion is in the queue.
- Observe the
SuggestionListheader for the “Dismiss all” button. - Add a second suggestion.
- Observe the
SuggestionListheader 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
| Key | Value | Notes |
|---|---|---|
| default_interval_minutes | 30 | Default proactive interval from DEFAULT_PROACTIVE_INTERVAL |
| min_interval_minutes | 5 | MIN_PROACTIVE_INTERVAL — below this clamps up |
| max_interval_minutes | 120 | MAX_PROACTIVE_INTERVAL — above this clamps down |
| initial_delay_seconds | 60 | Scheduler waits 60s before first check after app start |
| jitter_percent | 15 | Applied to interval to prevent thundering herd |
| suggestion_list_testid | suggestion-list | data-testid on the SuggestionList wrapper |
| suggestion_card_testid | suggestion-card | data-testid on each SuggestionCard |
| skill_organization | proactive_organization_audit | Internal name → displayed as “Organization” |
| skill_consistency | proactive_consistency_check | Internal name → displayed as “Consistency” |
| skill_relationships | proactive_relationship_discovery | Internal name → displayed as “Relationships” |
| placeholder_skill_name | workspace_analysis | Slice 3 placeholder used until real skill evaluation ships |
Notes
- The
SuggestionQueueis 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_triggergate checks three flags:proactive_enabled(from settingsagent.proactive_enabled),agent_is_running(active session exists), andagent_is_streaming(currently generating tokens). All three must pass for a suggestion to be generated. - The
SuggestionListcomponent fetches suggestions on mount AND on everyagent:proactive-suggestionTauri event. It does not poll on a timer. Tests that inject suggestions via the bridge may need to emit a fakeagent:proactive-suggestionevent 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_actionfield is optional. When present, theSuggestionCardrenders 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 asuggested_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.agentas an optional struct. Ifsettings.agentisnull(unconfigured),proactive_enableddefaults tofalseand no suggestions will ever be generated, regardless of agent running state.
Was this page helpful?
Thanks for your feedback!