Skip to content

feat: add follow actions for session clips and scenes#993

Open
ChuxiJ wants to merge 1 commit intomainfrom
feat/issue-920
Open

feat: add follow actions for session clips and scenes#993
ChuxiJ wants to merge 1 commit intomainfrom
feat/issue-920

Conversation

@ChuxiJ
Copy link
Copy Markdown

@ChuxiJ ChuxiJ commented Mar 27, 2026

Summary

  • Adds a follow actions system for session clips, allowing clips to automatically trigger other clips (stop, next, previous, first, last, any, other, again) after a configurable time in beats
  • Implements clip group detection (consecutive occupied slots), A/B probability rolling, and follow action scheduling via the pending launch queue
  • Adds UI: global Follow Actions toggle, per-slot follow action badge, and a right-click context menu with action selectors, chance slider, and time input

Test plan

  • 20 unit tests for utility functions (clip group detection, action resolution, probability rolling)
  • 9 unit tests for store actions (setSessionSlotFollowAction, setSessionFollowActionsEnabled, scheduleFollowAction, commitPendingSessionLaunches)
  • npx tsc --noEmit passes with 0 errors
  • npm test — all 2784 tests pass
  • npm run build — succeeds

Closes #920

🤖 Generated with Claude Code

Implement follow actions system that allows clips to automatically
trigger other clips when they finish playing, similar to Ableton Live.

- Add FollowActionType, FollowActionConfig types to project.ts
- Add followAction field to SessionClipSlot, followActionsEnabled to SessionState
- Add 'follow-action' to SessionPendingLaunch type union
- Create src/utils/followActions.ts with clip group detection, action resolution,
  and probability rolling utilities
- Add store actions: setSessionSlotFollowAction, setSessionFollowActionsEnabled,
  scheduleFollowAction
- Handle follow-action in commitPendingSessionLaunches
- Add UI: global toggle button, follow action badge on clips, context menu
  with action A/B selectors, chance slider, and time input
- 29 comprehensive unit tests covering utilities and store actions

Closes #920

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 27, 2026 06:22
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements a “follow actions” system for Session View clip slots, including core follow-action utilities, store actions for configuring/scheduling follow actions via the pending launch queue, and Session View UI controls (global toggle + per-slot configuration/badge).

Changes:

  • Added follow-action domain model (FollowActionType, FollowActionConfig) and expanded SessionPendingLaunch to include follow-action.
  • Implemented follow-action utilities (clip group detection, action resolution, probability roll) and store actions to set/toggle/schedule follow actions.
  • Added Session View UI to enable/disable follow actions globally and configure per-slot follow actions via context menu, plus a visual badge.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/unit/followActionsStore.test.ts Unit tests for new store actions (set/toggle/schedule/commit).
tests/unit/followActions.test.ts Unit tests for follow-action utilities (grouping, resolving, probability rolling).
src/utils/followActions.ts New utility functions for clip grouping and follow-action resolution/rolling.
src/types/project.ts Adds follow-action types/config, slot config field, pending launch type, and global toggle.
src/store/projectStore.ts Adds store APIs for follow actions and processes follow-action pending launches.
src/components/session/SessionView.tsx Adds UI controls for follow actions (global toggle, per-slot menu, badge).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +4489 to +4523
scheduleFollowAction: (trackId, currentSlotId, launchTime) => {
const state = get();
if (!state.project) return;
const session = ensureProjectSession(state.project).session!;

// Check global toggle (default true)
if (session.followActionsEnabled === false) return;

const currentSlot = session.slots.find((s) => s.id === currentSlotId);
if (!currentSlot?.followAction?.enabled) return;

const followAction = currentSlot.followAction;

// Detect the clip group containing the current slot
const groups = detectClipGroups(session.slots, session.scenes, trackId);
const group = groups.find((g) => g.some((s) => s.id === currentSlotId));
if (!group) return;

// Roll A/B and resolve target
const action = rollFollowAction(followAction);
const targetSlot = resolveFollowAction(action, currentSlot, group);

// Calculate fire time: launchTime + followAction.time beats
const beatDuration = 60 / Math.max(1, state.project.bpm);
const executeAt = launchTime + followAction.time * beatDuration;

// Queue a follow-action pending launch
const nextSession = queuePendingSessionLaunch(session, {
type: 'follow-action',
trackId,
sceneId: targetSlot?.sceneId,
clipId: targetSlot?.clipId ?? null,
executeAt,
});

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

scheduleFollowAction is never invoked from the actual session launch flow (immediate launch, pending launch commit, or follow-action commit). As a result, follow actions won’t fire automatically and follow-action launches won’t chain into subsequent follow actions. Wire scheduling into the place where a clip launch is actually applied (after applySessionTrackLaunch for clip/scene/follow-action executions), and ensure it runs after the launch is committed (calling queuePendingSessionLaunch too early would replace an existing pending launch for the same track).

Copilot uses AI. Check for mistakes.
Comment on lines +4489 to +4505
scheduleFollowAction: (trackId, currentSlotId, launchTime) => {
const state = get();
if (!state.project) return;
const session = ensureProjectSession(state.project).session!;

// Check global toggle (default true)
if (session.followActionsEnabled === false) return;

const currentSlot = session.slots.find((s) => s.id === currentSlotId);
if (!currentSlot?.followAction?.enabled) return;

const followAction = currentSlot.followAction;

// Detect the clip group containing the current slot
const groups = detectClipGroups(session.slots, session.scenes, trackId);
const group = groups.find((g) => g.some((s) => s.id === currentSlotId));
if (!group) return;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

scheduleFollowAction takes both trackId and currentSlotId, but the track can be derived from the slot. If these ever disagree, group detection will silently fail (detectClipGroups(..., trackId) won’t include the slot). Consider removing the redundant trackId parameter or adding a guard to validate currentSlot.trackId === trackId and early-return with a clear handling path.

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +115
const handleFollowActionChange = useCallback((field: string, value: string | number | boolean) => {
if (!colorMenu) return;
setSessionSlotFollowAction(colorMenu.slotId, { [field]: value });
// Update local state to reflect change immediately
setColorMenu((prev) => {
if (!prev) return null;
const defaultFA: FollowActionConfig = { actionA: 'next', actionB: 'stop', chanceA: 1, time: 4, enabled: true };
return {
...prev,
followAction: { ...(prev.followAction ?? defaultFA), [field]: value },
};
});
}, [colorMenu, setSessionSlotFollowAction]);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

handleFollowActionChange is typed as (field: string, value: string | number | boolean), and then uses a computed key ({ [field]: value }). This is easy to misuse (typos become silent no-ops / unexpected state shape) and loses type-safety for actionA/actionB (should be FollowActionType) vs chanceA/time/enabled. Prefer typing field as keyof FollowActionConfig and using per-field handlers (or a typed helper) so the value type matches the selected field.

Suggested change
const handleFollowActionChange = useCallback((field: string, value: string | number | boolean) => {
if (!colorMenu) return;
setSessionSlotFollowAction(colorMenu.slotId, { [field]: value });
// Update local state to reflect change immediately
setColorMenu((prev) => {
if (!prev) return null;
const defaultFA: FollowActionConfig = { actionA: 'next', actionB: 'stop', chanceA: 1, time: 4, enabled: true };
return {
...prev,
followAction: { ...(prev.followAction ?? defaultFA), [field]: value },
};
});
}, [colorMenu, setSessionSlotFollowAction]);
const handleFollowActionChange = useCallback(
<K extends keyof FollowActionConfig>(field: K, value: FollowActionConfig[K]) => {
if (!colorMenu) return;
setSessionSlotFollowAction(colorMenu.slotId, { [field]: value });
// Update local state to reflect change immediately
setColorMenu((prev) => {
if (!prev) return null;
const defaultFA: FollowActionConfig = { actionA: 'next', actionB: 'stop', chanceA: 1, time: 4, enabled: true };
return {
...prev,
followAction: { ...(prev.followAction ?? defaultFA), [field]: value },
};
});
},
[colorMenu, setSessionSlotFollowAction],
);

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +113
const defaultFA: FollowActionConfig = { actionA: 'next', actionB: 'stop', chanceA: 1, time: 4, enabled: true };
return {
...prev,
followAction: { ...(prev.followAction ?? defaultFA), [field]: value },
};
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The default follow-action config is duplicated here (defaultFA) and in setSessionSlotFollowAction in the store. Keeping two sources of truth risks them drifting (e.g., if defaults change later, the UI and store could disagree). Consider exporting a shared DEFAULT_FOLLOW_ACTION_CONFIG constant (or a small helper) and reusing it in both places.

Copilot uses AI. Check for mistakes.
Comment on lines 619 to +656
@@ -633,11 +652,13 @@ export interface SessionClipSlot {
color?: string | null;
/** When true (default), an empty slot acts as a stop button for the track. */
hasStopButton?: boolean;
/** Follow action configuration for this clip slot. */
followAction?: FollowActionConfig;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

PR title/description mention follow actions for “session clips and scenes”, but the types added here only support per-clip-slot follow actions (SessionClipSlot.followAction) and a global toggle—there’s no scene-level follow-action config on SessionScene or elsewhere. If scene follow actions are in scope for this PR (and to fully close #920), the project types/store will need a scene follow-action representation and execution path.

Copilot uses AI. Check for mistakes.
stopSessionTrack: (trackId: string) => void;
stopAllSessionClips: () => void;
commitPendingSessionLaunches: (currentTime: number) => void;
setSessionSlotFollowAction: (slotId: string, config: Partial<import('../types/project').FollowActionConfig>) => void;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

In ProjectState, setSessionSlotFollowAction uses Partial<import('../types/project').FollowActionConfig> even though FollowActionConfig is already imported in this file. Using the imported type directly keeps the signature consistent/readable and avoids an unnecessary inline import type.

Suggested change
setSessionSlotFollowAction: (slotId: string, config: Partial<import('../types/project').FollowActionConfig>) => void;
setSessionSlotFollowAction: (slotId: string, config: Partial<FollowActionConfig>) => void;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

ChuxiJ commented Mar 27, 2026

Triage: Hold for Phase 2 — Session view follow actions are Ableton-parity features. Not blocking for core DAW usability. Defer until session view foundation is complete.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Add Follow Actions for session clips and scenes

2 participants