Motion
ActionFocusSession
The focus timebox for one task in the actions cockpit: a live billable clock with approve, start, pause, run and done, and a burst of sparkles on completion.
Guidelines#
Use it for working one action at a time, such as an email, a LinkedIn message or a call log.
What you provide
task: aTaskDto. It readsstatus,estimatedMinutes(default 15),executionStartedAt,executionElapsedSecondsanddueAt.reasoning: aSuggestionReasoning; itsrationaleshows for suggestions.automatable,running,savingandrunLabel, plus optionalrunDisabledReasonandrunSummary(what Run sends, from which account).onApprove,onReject,onStart,onPause,onRun, andonComplete()returningPromise<boolean>.
Anatomy
- A
rounded-xl border px-4 py-3 shadow-sm backdrop-blur-xlbox, tintedprimarywhile working,amber-500in overtime andemerald-500when done. - A
text-[10px] uppercase tracking-[0.16em]eyebrow over atext-sm font-semiboldlabel, withtext-[11px]pills for the billable minutes and the due date. - The clock uses
AnimateDigits(text-lg,sm:text-xl). It counts down, then up as "+mm:ss" in overtime. - A
h-1bar up to 200px wide springs with stiffness 100 and damping 20. - Buttons change with status: Approve and Dismiss; Start or Resume; then amber Pause, Run and Done.
Behaviour
- The clock runs only while the task is
in_progress. Approval and deadlines never start it, and a pause keeps the seconds worked. - Done shows "Beautiful. That's done." as five sparkles rise 80px over 0.65s. It reverts if
onCompleteresolves false.
Don't drive the clock from dueAt, or show Run without saying what it sends.
Facts#
| Group | Motion |
|---|---|
| Product | Max |
| Global | window.DigitalCrew.ActionFocusSession |
| Source | max-agent: src/features/actions/ui/cockpit/action-focus-session.tsx |
| Import in the app | import { ActionFocusSession } from "@/features/actions/ui/cockpit/action-focus-session"; |
API#
TypeScript declarations emitted from src/features/actions/ui/cockpit/action-focus-session.tsx.
export interface ActionFocusSessionProps {
task: TaskDto;
reasoning: SuggestionReasoning;
automatable: boolean;
running: boolean;
saving: boolean;
runLabel: string;
runDisabledReason?: string | null;
/**
* What pressing Run will actually do, from the action catalogue — e.g.
* "Sends the email now from your connected mailbox." The catalogue has
* carried this copy for every automatable kind since it was written, and
* nothing rendered it: the least reversible button in the product said only
* "Run".
*/
runSummary?: string | null;
onApprove(): Promise<void>;
onReject(): Promise<void>;
onStart(): Promise<void>;
onPause(): Promise<void>;
onRun(): Promise<void>;
onComplete(): Promise<boolean>;
}
export declare function ActionFocusSession({ task, reasoning, automatable, running, saving, runLabel, runDisabledReason, runSummary, onApprove, onReject, onStart, onPause, onRun, onComplete, }: ActionFocusSessionProps): JSX.Element;Example#
The code of the preview above: plain React.createElement against window.DigitalCrew, with realistic data.
<div id="root" class="p-6"></div>
<script>
(function () {
var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
var MIN = 60 * 1000;
function task(over) {
var now = Date.now(), due = new Date(now + 24 * 60 * MIN);
due.setHours(17, 0, 0, 0);
return Object.assign({
id: "8b1d4c2a-0000-4000-8000-000000000001",
title: "Send Claire Dubois the Thursday call agenda",
description: "Why: she confirmed Thursday 10:00 CET and asked for the invite to go to her assistant.",
status: "in_progress", priority: "high", assigneeType: "user", assigneeUserId: "u-mathieu", assigneeAgent: null,
executionMode: "manual", dueAt: due.toISOString(), sourceType: "inbox",
sessionId: null, prospectId: "p-claire", sourceSummaryId: null, sourceTranscriptVersionId: null,
approvedAt: new Date(now - 40 * MIN).toISOString(), completedAt: null,
createdAt: new Date(now - 2 * 60 * MIN).toISOString(), updatedAt: new Date(now - 4 * MIN).toISOString(),
actionType: "send_email", channel: "email", automationStatus: "idle", estimatedMinutes: 15,
executionStartedAt: new Date(now - (4 * MIN + 12 * 1000)).toISOString(), executionElapsedSeconds: 0
}, over || {});
}
var REASONING = { notes: null, rationale: "She confirmed Thursday and asked for the invite to go to her assistant.", mentionedDate: null };
function wait(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
function App() {
var t = React.useState(function () { return task(); }), current = t[0], setTask = t[1];
var k = React.useState(0), key = k[0], setKey = k[1];
var r = React.useState(false), running = r[0], setRunning = r[1];
return h("div", { className: "mx-auto max-w-[560px]" },
h(DC.ActionFocusSession, {
key: key, task: current, reasoning: REASONING, automatable: true, running: running, saving: false,
runLabel: "Run task", runSummary: "Sends the email now from your connected mailbox. Sending as mathieu@digitalcrew.ai.",
onApprove: function () { setTask(task({ status: "approved", executionStartedAt: null })); return wait(0); },
onReject: function () { return wait(0); },
onStart: function () { setTask(Object.assign({}, current, { status: "in_progress", executionStartedAt: new Date().toISOString() })); return wait(0); },
onPause: function () {
var worked = (current.executionElapsedSeconds || 0) + Math.floor((Date.now() - Date.parse(current.executionStartedAt)) / 1000);
setTask(Object.assign({}, current, { status: "approved", executionStartedAt: null, executionElapsedSeconds: worked }));
return wait(0);
},
onRun: function () { setRunning(true); return wait(1400).then(function () { setRunning(false); }); },
onComplete: function () {
return wait(900).then(function () {
setTimeout(function () { setTask(task()); setKey(key + 1); }, 2600);
return true;
});
}
}));
}
ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>