From 58f0420c6b9c955dc57a2212ac24fec9728eaa24 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Wed, 14 Oct 2026 09:15:40 -0400 Subject: [PATCH 1/2] Preserve Ask AI conversations through agent action setup Add inline action setup cards, contextual return controls, and scoped in-memory conversation handoffs. Cover SQL and scoped editor round trips, update guides, and bump the application to 0.261.333. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- application/single_app/config.py | 2 +- application/v2_ui/src/App.tsx | 12 + .../src/components/chat/AssistThread.tsx | 8 +- .../editorAssist/EditorAskAiPanel.tsx | 68 +++++- .../editorAssist/useEditorAssist.ts | 46 +++- .../workspace/WorkspaceEditorFrame.tsx | 5 +- .../v2_ui/src/lib/editorAssistHandoff.ts | 93 ++++++++ .../v2_ui/src/lib/workspaceEditorDrafts.ts | 19 +- .../src/pages/workspace/ActionEditorPage.tsx | 6 +- .../src/pages/workspace/AgentEditorPage.tsx | 55 ++++- .../v2_ui/src/stores/assistThreadStore.ts | 10 + .../v2_ui/src/stores/editorAssistStore.ts | 9 + .../ASK_AI_ACTION_HANDOFF_CONTINUITY_FIX.md | 100 +++++++++ docs/guides/create-an-action.md | 23 +- docs/guides/create-an-agent.md | 26 ++- .../test_v2_editor_assist_handoff.mjs | 98 +++++++++ .../test_v2_admin_global_agents_actions.py | 25 ++- ui_tests/test_v2_agent_action_ask_ai.py | 208 +++++++++++++++++- ui_tests/test_v2_group_agents.py | 6 +- ui_tests/test_v2_workspace_authoring.py | 6 +- 20 files changed, 780 insertions(+), 45 deletions(-) create mode 100644 application/v2_ui/src/lib/editorAssistHandoff.ts create mode 100644 docs/explanation/fixes/ASK_AI_ACTION_HANDOFF_CONTINUITY_FIX.md create mode 100644 functional_tests/test_v2_editor_assist_handoff.mjs diff --git a/application/single_app/config.py b/application/single_app/config.py index 32497ce21..e4f365cf8 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -101,7 +101,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.261.332" +VERSION = "0.261.333" IS_DEVELOPMENT = is_development_env_enabled() # Opt-out for deployments where App Service Easy Auth is active but the platform diff --git a/application/v2_ui/src/App.tsx b/application/v2_ui/src/App.tsx index f6e4d3e53..4e65d9398 100644 --- a/application/v2_ui/src/App.tsx +++ b/application/v2_ui/src/App.tsx @@ -30,6 +30,9 @@ import { GroupDirectoryPage } from './pages/GroupDirectoryPage'; import { PublicWorkspacePage } from './pages/PublicWorkspacePage'; import { PublicDirectoryPage } from './pages/PublicDirectoryPage'; import { clearWorkspaceEditorDrafts } from './lib/workspaceEditorDrafts'; +import { clearAgentAssistHandoffs, pruneAgentAssistHandoffs } from './lib/editorAssistHandoff'; +import { useEditorAssistStore } from './stores/editorAssistStore'; +import { useAssistThreadStore } from './stores/assistThreadStore'; import { ContentReviewPage } from './pages/ContentReviewPage'; import { TermsOfUsePage } from './pages/TermsOfUsePage'; import { AccessRestrictedPage } from './pages/AccessRestrictedPage'; @@ -124,8 +127,17 @@ export function App() { useEffect(() => { clearWorkspaceEditorDrafts(); + clearAgentAssistHandoffs(); + for (const record of Object.values(useEditorAssistStore.getState().turns)) { + useAssistThreadStore.getState().forgetThread(record.threadKey); + } + useEditorAssistStore.getState().reset(); }, [data?.user?.id, authExpired]); + useEffect(() => { + pruneAgentAssistHandoffs(authExpired ? '' : data?.user?.id ?? '', location.pathname, location.search); + }, [data?.user?.id, authExpired, location.pathname, location.search]); + useEffect(() => { initializeTheme(); if (standalonePage) { diff --git a/application/v2_ui/src/components/chat/AssistThread.tsx b/application/v2_ui/src/components/chat/AssistThread.tsx index df7694d16..805e9552a 100644 --- a/application/v2_ui/src/components/chat/AssistThread.tsx +++ b/application/v2_ui/src/components/chat/AssistThread.tsx @@ -79,6 +79,8 @@ export interface AssistThreadProps { className?: string; logClassName?: string; composerClassName?: string; + /** Optional in-memory reading position for an editor returning from a setup detour. */ + logPosition?: { scrollTop: number }; } /** Seconds since a request started. Hidden from assistive technology so it is not read out every second. */ @@ -149,11 +151,13 @@ export function AssistThread({ className, logClassName, composerClassName, + logPosition, }: AssistThreadProps) { const id = useId(); const counterId = `${id}-counter`; const limitId = `${id}-limit`; const logRef = useRef(null); + const firstLogRender = useRef(true); const { exchanges, pending } = thread; const inputDisabled = disabled || !thread.key; const canSubmit = !inputDisabled && !busy && !pending && !thread.overLimit @@ -170,7 +174,8 @@ export function AssistThread({ useEffect(() => { const log = logRef.current; if (log) { - log.scrollTop = log.scrollHeight; + log.scrollTop = firstLogRender.current && logPosition ? logPosition.scrollTop : log.scrollHeight; + firstLogRender.current = false; } }, [turns.length, exchanges.length, pending?.id]); @@ -261,6 +266,7 @@ export function AssistThread({
{ logPosition.scrollTop = event.currentTarget.scrollTop; } : undefined} role="log" aria-live="polite" aria-label={logLabel} diff --git a/application/v2_ui/src/components/editorAssist/EditorAskAiPanel.tsx b/application/v2_ui/src/components/editorAssist/EditorAskAiPanel.tsx index a9609796e..439df7388 100644 --- a/application/v2_ui/src/components/editorAssist/EditorAskAiPanel.tsx +++ b/application/v2_ui/src/components/editorAssist/EditorAskAiPanel.tsx @@ -7,7 +7,7 @@ import { useEffect, useRef } from 'react'; import { clsx } from 'clsx'; -import { AlertTriangle, Loader2, Sparkles, X } from 'lucide-react'; +import { AlertTriangle, ArrowRight, CheckCircle2, Loader2, Sparkles, X } from 'lucide-react'; import { AssistThread } from '../chat/AssistThread'; import { GlassButton } from '../ui/primitives'; import { WorkflowAssistElapsed } from '../workflows/WorkflowAskAiTab'; @@ -15,6 +15,15 @@ import type { AssistExchange } from '../../lib/assistThread'; import type { EditorAssistKind } from '../../lib/editorAssist'; import type { EditorAssistTurnRecord } from '../../stores/editorAssistStore'; import { editorAssistTurnState, type EditorAssist } from './useEditorAssist'; +import type { AgentAssistSession } from '../../lib/editorAssistHandoff'; + +interface ActionSetup { + pending: readonly { reference: string; label: string; type: string; issues: readonly string[] }[]; + selected: readonly string[]; + onFinish: (reference: string) => void; + disabled: boolean; + restoring: boolean; +} interface QuickAction { readonly label: string; @@ -69,10 +78,11 @@ export function EditorAskAiToggle({ id, open, controls, onToggle }: { } /** One answer's card: the reply, then what it did to the draft. */ -function EditorAssistTurnCard({ turnId, record, assist }: { +function EditorAssistTurnCard({ turnId, record, assist, actionSetup }: { turnId: string; record: EditorAssistTurnRecord; assist: EditorAssist; + actionSetup?: ActionSetup; }) { const state = editorAssistTurnState(record); const undoRef = useRef(null); @@ -97,6 +107,36 @@ function EditorAssistTurnCard({ turnId, record, assist }: { return (
{record.reply ?

{record.reply}

: null} + {actionSetup && record.draftedActions?.map((item) => { + const pending = actionSetup.pending.find((action) => action.reference === item.reference); + const selected = Boolean(item.savedId && actionSetup.selected.includes(item.savedId)); + return ( +
+

{pending?.label ?? item.label}

+

{pending?.type ?? item.type}

+ {pending ? <> +

{pending.issues.length + ? `Needs attention before saving: ${pending.issues.join(' ')}` + : 'Drafted for this agent. Review its setup before saving.'}

+ actionSetup.onFinish(item.reference)}> + Finish setup in Actions +

Enter credentials in the action editor, not in Ask AI.

+ : item.savedId ? ( +

+

+ ) :

This action was removed from the draft.

} +
+ ); + })} {record.changes.length ? (

{stateText}

@@ -157,25 +197,42 @@ function EditorAssistTurnCard({ turnId, record, assist }: { } /** The Ask AI panel. Rendered beside the editor's form, never inside it: the thread has its own form. */ -export function EditorAskAiPanel({ assist, id, inputId, onClose }: { +export function EditorAskAiPanel({ assist, id, inputId, onClose, actionSetup, session }: { assist: EditorAssist; id: string; inputId: string; onClose: () => void; + actionSetup?: ActionSetup; + session?: AgentAssistSession; }) { const { thread, kind } = assist; const quickDisabled = assist.busy || Boolean(assist.pending); + const panelRef = useRef(null); + + useEffect(() => { + if (!session?.focusReference || actionSetup?.restoring) return undefined; + const reference = session.focusReference; + const frame = requestAnimationFrame(() => { + const card = Array.from(panelRef.current?.querySelectorAll('[data-editor-assist-action]') ?? []) + .find((element) => element.dataset.editorAssistAction === reference); + if (card) { + (card.querySelector('button') ?? card).focus({ preventScroll: true }); + session.focusReference = null; + } + }); + return () => cancelAnimationFrame(frame); + }, [session, actionSetup?.restoring]); const renderReply = (exchange: AssistExchange) => { const record = assist.turns[exchange.id]; if (!record || record.threadKey !== thread.key) { return

{exchange.reply}

; } - return ; + return ; }; return ( -