Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2494bc4
Add the V2 workflow run tracker core for chat result delivery (6b-2)
paullizer Oct 3, 2026
d726fbc
Add V2 workflow run card, delivery footer, running tag and recurring …
paullizer Oct 3, 2026
23e6b27
Test the V2 workflow run tracking helpers (6b-2)
paullizer Oct 3, 2026
6bd2545
Merge paullizer-react-v2-ui (#1630) into phase 6b-2
paullizer Oct 3, 2026
8bd8d26
Merge commit 'e1e6949f28ccabea7b512f6c2d4e6d26b17a9b87' into paullize…
paullizer Oct 3, 2026
d8170f6
Bump version to 0.261.233 for Phase 6b-2
paullizer Oct 5, 2026
ecdd1e6
Add V2 run card and tracker Playwright tests
paullizer Oct 5, 2026
97a0961
Test the workflow run notices and the recurring card's run lines
paullizer Oct 5, 2026
67a7785
Add the XSS guardrail for the V2 workflow run tracking
paullizer Oct 5, 2026
c8cbd9b
Fail tracker tests on a hang instead of waiting forever
paullizer Oct 5, 2026
5b42cd0
Document the V2 experience for workflow results in chat (6b-2)
paullizer Oct 5, 2026
9d7c8f9
Document V2 workflow run tracking and posted results (6b-2)
paullizer Oct 5, 2026
462f32c
Add 0.261.233 release notes for V2 workflow run tracking (6b-2)
paullizer Oct 5, 2026
042af8c
Merge origin/paullizer-react-v2-ui into 6b-2
paullizer Oct 5, 2026
c852d34
Renumber 6b-2 to 0.261.234
paullizer Oct 5, 2026
9786915
Hold Review and approve while a run action is pending (binding note 6)
paullizer Oct 5, 2026
26caaa3
Declare screenshot slots for the run card, posted results and created…
paullizer Oct 5, 2026
bd57828
Keep the XSS guardrail checks running under python -O
paullizer Oct 5, 2026
bf74a50
Check that posted messages offer no Edit
paullizer Oct 5, 2026
ee8383a
Pass harness chats and proposal to the base classes
paullizer Oct 5, 2026
483291d
Show tracked runs the plan's list doesn't name; keep a question sent …
paullizer Oct 5, 2026
1556049
Renumber 6b-2 to 0.261.237
paullizer Oct 5, 2026
3fd4e28
Merge origin/paullizer-react-v2-ui into 6b-2
paullizer Oct 5, 2026
d7ea650
Pin every PermissionError answer of the runtime response as a 403
paullizer Oct 5, 2026
d751471
Merge origin/paullizer-react-v2-ui into 6b-2
paullizer Oct 5, 2026
f21dcb7
Accept #1636's widened bootstrap features in the tracker gate
paullizer Oct 5, 2026
461cd63
Merge origin/paullizer-react-v2-ui (#1643) into 6b-2
paullizer Oct 5, 2026
a5c28d6
Merge origin/paullizer-react-v2-ui (#1644) into 6b-2
paullizer Oct 5, 2026
636d582
Renumber 6b-2 to 0.261.239
paullizer Oct 5, 2026
db2e6a4
Merge V2 #1641 into 6b-2
paullizer Oct 6, 2026
90f87fe
Renumber 6b-2 to 0.261.249
paullizer Oct 6, 2026
d5b8204
Merge V2 #1640 into 6b-2
paullizer Oct 6, 2026
eb09006
Renumber 6b-2 to 0.261.251
paullizer Oct 6, 2026
bd1a250
Set the run card harness's first stream number through the constructor
paullizer Oct 6, 2026
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
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.233"
VERSION = "0.261.234"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
5 changes: 5 additions & 0 deletions application/v2_ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { initializeTheme, hydrateUiPreferences } from './stores/uiStore';
import { startImageApprovalTracking } from './lib/imageProposalResume';
import { useNotificationRuntime } from './lib/useNotificationRuntime';
import { useWorkflowAlertRuntime } from './lib/useWorkflowAlertRuntime';
import { useWorkflowRunTracker } from './lib/useWorkflowRunTracker';
import { workflowRunTrackerShouldRun } from './lib/workflowRunTracker';
import { restorePersistedRuns } from './stores/orchestrationStore';
import { ChatPage } from './pages/ChatPage';
import { HomePage } from './pages/HomePage';
Expand Down Expand Up @@ -182,6 +184,9 @@ export function App() {
useNotificationRuntime(Boolean(data) && !error);
// Workflow alerts that ask to pop up. It listens to the bell's count rather than polling.
useWorkflowAlertRuntime(Boolean(data) && !error);
// The saved workflows chats started: one tracker for the tab, for the run cards, the chat
// list's running tag and the results each run posts back to its chat.
useWorkflowRunTracker(Boolean(data) && !error && workflowRunTrackerShouldRun(data?.features));

if (loading) {
return <BootScreen />;
Expand Down
2 changes: 2 additions & 0 deletions application/v2_ui/src/components/chat/ConversationRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import { ConfirmDialog } from '../ui/ConfirmDialog';
import { Skeleton } from '../ui/primitives';
import { ConversationExportDialog } from './ConversationExportDialog';
import { WorkflowRunningTag } from './WorkflowRunningTag';
import type { ReactNode } from 'react';
import type { Conversation } from '../../lib/types';

Expand Down Expand Up @@ -309,6 +310,7 @@
/>
)}
<GeneratingImagesTag conversation={conversation} />
<WorkflowRunningTag conversation={conversation} />

Check warning on line 313 in application/v2_ui/src/components/chat/ConversationRail.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
</button>
</div>

Expand Down
6 changes: 5 additions & 1 deletion application/v2_ui/src/components/chat/MessageActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { readSources } from '../../lib/messageDetails';
import { messageToPlainText } from '../../lib/messageText';
import { canMask, readMaskState } from '../../lib/masking';
import { buildReplyPreview, messageAuthorName } from '../../lib/sharedMessage';
import { isWorkflowDeliveryMessage } from '../../lib/workflowDelivery';
import type { InspectorSection } from './MessageInspector';
import type { ChatMessage, Json } from '../../lib/types';
import {
Expand Down Expand Up @@ -483,6 +484,9 @@ export function MessageActions({

const [copied, setCopied] = useState(false);
const isUser = message.role === 'user';
// A workflow run posted this message: the server refuses to retry it, so it isn't offered.
// The message's own footer offers Retry workflow run when the run can still be resumed.
const workflowDelivery = isWorkflowDeliveryMessage(message);
const attempts = attemptState(message, attemptsByThread);
const sources = readSources(message as unknown as Json);
const masks = readMaskState(message);
Expand Down Expand Up @@ -616,7 +620,7 @@ export function MessageActions({
<Reply size={15} />
</IconButton>
)
) : (
) : workflowDelivery ? null : (
<IconButton
label={message.metadata?.orchestration ? 'Review orchestration recovery' : 'Retry'}
onClick={() => void retryMessage(message.id)}
Expand Down
25 changes: 23 additions & 2 deletions application/v2_ui/src/components/chat/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
import { OrchestrationOutputs } from './OrchestrationOutputs';
import { WorkflowProposalCards } from './WorkflowProposalCard';
import { WorkflowRunLinks } from './WorkflowRunLinks';
import { WorkflowRunCard } from './WorkflowRunCard';
import { WorkflowDeliveryFooter } from './WorkflowDeliveryFooter';
import { MessageInspector, type InspectorSection } from './MessageInspector';
import { ThoughtsList, ThoughtsProgressCard } from './ThoughtsList';
import { OrchestrationPlanCard } from './OrchestrationPlanCard';
Expand Down Expand Up @@ -78,6 +80,8 @@
import { isOrchestrationOutputArtifact } from '../../lib/orchestrationOutputs';
import { orchestrationProposedWorkflow } from '../../lib/workflowProposals';
import { orchestrationStartedWorkflow } from '../../lib/orchestrationWorkflowRuns';
import { readWorkflowDelivery } from '../../lib/workflowDelivery';
import { workflowRunTrackerShouldRun } from '../../lib/workflowRunTracker';
import { analysisUnavailableMessage, readSavedAnalysis, sameAnalysis } from '../../lib/savedAnalysis';
import { readMessagePrompt } from '../../lib/messagePrompt';
import { PromptCard } from './PromptCard';
Expand Down Expand Up @@ -797,6 +801,10 @@
const messages = useChatStore((state) => state.messages);
const activeConversationId = useChatStore((state) => state.activeConversationId);
const personalConversation = useChatStore((state) => state.activeConversationKind === 'personal');
// Live run status comes from the tab's workflow run tracker, which runs only while both flags are on.
const liveRunStatus = useBootstrapStore((state) => workflowRunTrackerShouldRun(state.data?.features));
// A result or note a chat-started workflow run posted back to this conversation.

Check warning on line 806 in application/v2_ui/src/components/chat/MessageList.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
const workflowDelivery = useMemo(() => readWorkflowDelivery(message), [message]);
// the thread, which is often: the list re-renders on each streaming token.
const masks = useMemo(() => readMaskState(message), [message]);
// A stable list, so the image cards' shared scope is not rebuilt on every render.
Expand Down Expand Up @@ -1122,11 +1130,14 @@
&& orchestrationProposedWorkflow(message.metadata?.orchestration) ? (
<WorkflowProposalCards conversationId={message.conversation_id} runId={orchestration.run_id} />
) : null}
{/* A started workflow's run links only for the person who asked, in their own conversation. */}
{/* A started workflow's runs only for the person who asked, in their own conversation:

Check warning on line 1133 in application/v2_ui/src/components/chat/MessageList.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
live while the tab keeps a run tracker, else as they stood when the answer loaded. */}
{orchestration.run_id && masks.ranges.length === 0 && personalConversation
&& message.conversation_id === activeConversationId
&& orchestrationStartedWorkflow(message.metadata?.orchestration) ? (
<WorkflowRunLinks conversationId={message.conversation_id} runId={orchestration.run_id} />
liveRunStatus
? <WorkflowRunCard conversationId={message.conversation_id} runId={orchestration.run_id} />

Check warning on line 1139 in application/v2_ui/src/components/chat/MessageList.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
: <WorkflowRunLinks conversationId={message.conversation_id} runId={orchestration.run_id} />

Check warning on line 1140 in application/v2_ui/src/components/chat/MessageList.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
) : null}
{/* Inside the bubble, because a generated file belongs to the reply
that produced it rather than sitting loose in the thread. */}
Expand All @@ -1149,6 +1160,16 @@
</details>
) : artifactCards
)}
{/* What the requester can do next with a result a workflow run posted back here. */}
{workflowDelivery && message.role === 'assistant' && masks.ranges.length === 0

Check warning on line 1164 in application/v2_ui/src/components/chat/MessageList.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
&& personalConversation && message.conversation_id === activeConversationId ? (

Check warning on line 1165 in application/v2_ui/src/components/chat/MessageList.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
<WorkflowDeliveryFooter
messageId={message.id}
conversationId={message.conversation_id}

Check warning on line 1168 in application/v2_ui/src/components/chat/MessageList.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
delivery={workflowDelivery}
metadata={message.metadata}
/>
) : null}
</>
)}
</div>
Expand Down
130 changes: 130 additions & 0 deletions application/v2_ui/src/components/chat/WorkflowDeliveryFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// WorkflowDeliveryFooter.tsx
// The actions under a message a chat-started workflow run posted back to its chat (phase 6b).
//
// The message itself, a label line naming the workflow and when it was asked for and then the
// result or a note on how the run ended, is the server's text and renders like any other answer.
// This footer adds what the chat can do next with it:
//
// - Follow up, on a result, makes that result the composer's source in this chat.
// - Open run opens the run in Workflows.
// - Retry workflow run, on a failed run's note, resumes that same run. It shows only while the
// tab's run tracker has just read the run and the server says it would still resume it, for the
// generation this note reported. It never starts a new run.
//
// The chat's own Retry and Edit are hidden on these messages: the server refuses both.

Check warning on line 14 in application/v2_ui/src/components/chat/WorkflowDeliveryFooter.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.

import { useEffect, useMemo, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { Loader2 } from 'lucide-react';
import { GlassButton } from '../ui/primitives';
import { useFeature } from '../../stores/bootstrapStore';
import { useChatStore } from '../../stores/chatStore';
import { useWorkflowRunTrackerStore } from '../../stores/workflowRunTrackerStore';
import { useFocusFallback } from '../../lib/useFocusFallback';
import { useWorkflowRunAction } from '../../lib/useWorkflowRunAction';
import { requestWorkflowConversationRuns } from '../../lib/useWorkflowRunTracker';

Check warning on line 25 in application/v2_ui/src/components/chat/WorkflowDeliveryFooter.tsx

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
import {
workflowDeliveryCanRetry,
workflowDeliveryFollowUp,
workflowDeliveryFooterId,
workflowDeliveryRun,
WORKFLOW_DELIVERY_FOLLOW_UP_UNAVAILABLE_TEXT,
type WorkflowDeliveryMetadata,
} from '../../lib/workflowDelivery';
import { workflowRunHref } from '../../lib/workflowRunLink';

const LINK_CLASS = 'inline-flex h-8 items-center rounded-xl px-3 text-sm font-medium text-accent hover:bg-surface-3 focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent';

export function WorkflowDeliveryFooter({
messageId,
conversationId,
delivery,
metadata,
}: {
messageId: string;
conversationId: string;
delivery: WorkflowDeliveryMetadata;
/** The message's whole metadata, which carries the result's descriptor. */
metadata: unknown;
}) {
const resultsInChat = useFeature('enable_chat_workflow_results');
const workflowsOn = useFeature('allow_user_workflows');
const descriptor = useMemo(
() => (resultsInChat ? workflowDeliveryFollowUp(delivery, metadata) : null),
[resultsInChat, delivery, metadata],
);
const canRetry = useWorkflowRunTrackerStore((state) =>
workflowDeliveryCanRetry(state.snapshot, delivery, conversationId));
// The tracker's name for the run, when it has read it; the message's label names it either way.
const name = useWorkflowRunTrackerStore((state) =>
workflowDeliveryRun(state.snapshot, delivery, conversationId)?.row.workflow_name ?? '');
const { pending, outcome, run } = useWorkflowRunAction(conversationId, delivery.workflow_id, delivery.run_id);
const [followUpNote, setFollowUpNote] = useState('');
const section = useRef<HTMLElement>(null);
const armFocus = useFocusFallback(section, canRetry, pending !== null);
const failed = delivery.kind === 'failed';

// A failed run's note offers Retry only after a fresh read says the run can still be resumed.
useEffect(() => {
if (failed) {
void requestWorkflowConversationRuns(conversationId);
}
}, [failed, conversationId]);

if (!descriptor && !workflowsOn) {
return null;
}

const followUp = () => {
if (!descriptor) return;
if (useChatStore.getState().selectWorkflowResult(descriptor, conversationId)) {
setFollowUpNote('');
document.getElementById('composer-input')?.focus();
} else {
setFollowUpNote(WORKFLOW_DELIVERY_FOLLOW_UP_UNAVAILABLE_TEXT);
}
};
const note = outcome || followUpNote;

return (
<section
ref={section}
id={workflowDeliveryFooterId(messageId)}
tabIndex={-1}
aria-label={name ? `Workflow run: ${name}` : 'Workflow run'}
className="mt-3 min-w-0 rounded-xl border border-edge p-3 focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
>
<div className="flex flex-wrap items-center gap-2">
{descriptor ? (
<GlassButton type="button" size="sm" variant="subtle"
aria-label={name ? `Follow up on ${name}` : 'Follow up on this result'}
onClick={followUp}>
Follow up
</GlassButton>
) : null}
{canRetry ? (
<GlassButton type="button" size="sm" variant="ghost"
className="aria-disabled:cursor-not-allowed aria-disabled:opacity-50"
aria-label={name ? `Retry workflow run of ${name}` : 'Retry workflow run'}
aria-disabled={pending !== null || undefined}
onClick={() => {
if (pending !== null) return;
armFocus();
void run('retry');
}}>
{pending === 'retry' ? <Loader2 size={14} className="animate-spin" aria-hidden="true" /> : null}
Retry workflow run
</GlassButton>
) : null}
{workflowsOn ? (
<Link to={workflowRunHref(delivery.workflow_id, delivery.run_id)}
aria-label={name ? `Open run of ${name}` : 'Open run that posted this message'}
className={LINK_CLASS}>
Open run
</Link>
) : null}
</div>
<p role="status" className={note ? 'mt-2 break-words text-xs text-text-2' : 'sr-only'}>{note}</p>
</section>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { Link } from 'react-router-dom';
import { ConfirmDialog } from '../ui/ConfirmDialog';
import { GlassButton } from '../ui/primitives';
import { WorkflowEditorDialog } from '../workflows/WorkflowEditorDialog';
import { WorkflowProposalRunSummary } from './WorkflowProposalRunSummary';
import { ApiError } from '../../lib/apiClient';
import { M365_CONNECT_HREF } from '../../lib/m365Links';
import {
fetchWorkflowEditorOptions,
normalizeWorkflowDefinition,
Expand Down Expand Up @@ -41,7 +43,6 @@ const CREATING_POLL_MS = 3000;
const CREATING_POLL_LIMIT = 40;
const CREATING_WINDOW_MS = 120_000;
// Classic pages: V2 has no Microsoft 365 connection or approval page of its own.
const M365_CONNECT_HREF = '/profile?tab=settings#m365-connection-status';
const M365_APPROVALS_HREF = '/approvals';
const URL_ACCESS_REFUSED = 'URL Access is not available for workflows created from chat.';

Expand Down Expand Up @@ -425,6 +426,7 @@ function ProposalCard({
</div>
<ProposalDetails proposal={proposal} />
<p className="break-words text-text-2">{[stateNote(proposal), expiry].filter(Boolean).join(' ')}</p>
{isCreated && proposal.workflow ? <WorkflowProposalRunSummary workflowId={proposal.workflow.id} workflowName={name} /> : null}
{proposal.state === 'creating' && pollingStopped ? (
<p className="break-words text-text-2">
This is taking longer than expected.{' '}
Expand Down
Loading
Loading