From 102de338d7fe483d71e1334af241ddedc255a678 Mon Sep 17 00:00:00 2001 From: Gyanu Date: Sat, 22 Aug 2026 15:57:22 +0530 Subject: [PATCH 1/8] Generate system instructions from an existing chat history. Users can promote a working conversation into an editable draft instead of writing the agent prompt from scratch, and nothing is saved until they apply or copy it. Co-authored-by: Cursor --- .changeset/chat-instructions-from-history.md | 6 + .../src/atoms/GenerateInstructionsButton.tsx | 241 ++++++++++++++++++ packages/trueforge-ui/src/index.ts | 2 + .../trueforge-ui/src/layouts/DrawerLayout.tsx | 2 + .../src/layouts/SidebarLayout.tsx | 2 + .../src/layouts/StackChatPanel.tsx | 2 + .../chatServer.ts | 72 +++++- .../trueforge-agent-server-adapter/index.ts | 1 + .../server/generateInstructionsFromChat.ts | 25 ++ .../trueforge-ui/src/theme/defaultSlots.ts | 2 + .../atoms/GenerateInstructionsButton.test.tsx | 155 +++++++++++ .../harnessServer.test.ts | 22 ++ .../trueforge-ui/test/publicUiExports.test.ts | 1 + packages/trueforge/src/apis/sessions.ts | 73 +++++- .../trueforge/src/routes/sessionRoutes.ts | 38 +++ .../src/runtime/chatInstructionTranscript.ts | 193 ++++++++++++++ .../runtime/generateSessionInstructions.ts | 110 ++++++++ packages/trueforge/src/schemas/session.ts | 27 ++ .../tests/unit/apis/sessionHttp.test.ts | 15 ++ .../runtime/chatInstructionTranscript.test.ts | 137 ++++++++++ .../generateSessionInstructions.test.ts | 132 ++++++++++ 21 files changed, 1253 insertions(+), 5 deletions(-) create mode 100644 .changeset/chat-instructions-from-history.md create mode 100644 packages/trueforge-ui/src/atoms/GenerateInstructionsButton.tsx create mode 100644 packages/trueforge-ui/src/server/generateInstructionsFromChat.ts create mode 100644 packages/trueforge-ui/test/atoms/GenerateInstructionsButton.test.tsx create mode 100644 packages/trueforge/src/runtime/chatInstructionTranscript.ts create mode 100644 packages/trueforge/src/runtime/generateSessionInstructions.ts create mode 100644 packages/trueforge/tests/unit/runtime/chatInstructionTranscript.test.ts create mode 100644 packages/trueforge/tests/unit/runtime/generateSessionInstructions.test.ts diff --git a/.changeset/chat-instructions-from-history.md b/.changeset/chat-instructions-from-history.md new file mode 100644 index 000000000..fa1d2ef76 --- /dev/null +++ b/.changeset/chat-instructions-from-history.md @@ -0,0 +1,6 @@ +--- +"@truefoundry/trueforge": patch +"@truefoundry/trueforge-ui": patch +--- + +Draft system instructions from an existing chat, then let the user edit before applying diff --git a/packages/trueforge-ui/src/atoms/GenerateInstructionsButton.tsx b/packages/trueforge-ui/src/atoms/GenerateInstructionsButton.tsx new file mode 100644 index 000000000..93037fd77 --- /dev/null +++ b/packages/trueforge-ui/src/atoms/GenerateInstructionsButton.tsx @@ -0,0 +1,241 @@ +'use client'; + +import { + useTrueFoundryAdoptAgentSpec, + useTrueFoundryAgentSpec, + useTrueFoundryFlushAgentSpec, +} from '@truefoundry/assistant-ui-runtime'; +import { useEffect, useId, useRef, useState } from 'react'; +import { useAuiState } from '../assistant-ui.js'; +import { Icon } from '../icons/Icon.js'; +import { hasGenerateInstructionsFromChat } from '../server/generateInstructionsFromChat.js'; +import { useOptionalServer } from '../server/ServerContext.js'; +import { useOptionalShellMode } from '../server/ShellModeContext.js'; +import { getErrorMessage } from '../utils/getErrorMessage.js'; +import { auiButtonClass } from './lib/buttonClasses.js'; +import { auiInputClass } from './lib/inputClasses.js'; +import { CenteredModal } from './primitives/CenteredModal.js'; + +export type GenerateInstructionsButtonProps = { + disabled?: boolean; + className?: string; +}; + +export function useGenerateInstructionsVisible(): boolean { + const shell = useOptionalShellMode(); + const server = useOptionalServer(); + const remoteId = useAuiState(s => s.threadListItem.remoteId); + if (shell == null || shell.mode.status !== 'active') return false; + if (remoteId == null || remoteId.length === 0) return false; + return hasGenerateInstructionsFromChat(server); +} + +export function GenerateInstructionsButton({ disabled = false, className }: GenerateInstructionsButtonProps) { + const visible = useGenerateInstructionsVisible(); + if (!visible) return null; + return ; +} + +function GenerateInstructionsButtonContent({ disabled, className }: { disabled: boolean; className?: string }) { + const server = useOptionalServer(); + const shell = useOptionalShellMode(); + const remoteId = useAuiState(s => s.threadListItem.remoteId); + const { agentSpec } = useTrueFoundryAgentSpec(); + const agentSpecRef = useRef(agentSpec); + agentSpecRef.current = agentSpec; + const flushAgentSpec = useTrueFoundryFlushAgentSpec(); + const adoptAgentSpec = useTrueFoundryAdoptAgentSpec(); + const titleId = useId(); + const [open, setOpen] = useState(false); + const [loading, setLoading] = useState(false); + const [applying, setApplying] = useState(false); + const [draft, setDraft] = useState(''); + const [sources, setSources] = useState>([]); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(false); + const errorRef = useRef(null); + const canApply = shell?.mode.status === 'active' && shell.mode.isMutable && agentSpec != null; + + useEffect(() => { + if (error === null) return; + errorRef.current?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' }); + }, [error]); + + const close = () => { + if (loading || applying) return; + setOpen(false); + setDraft(''); + setSources([]); + setError(null); + setCopied(false); + }; + + const generate = async () => { + if (!hasGenerateInstructionsFromChat(server) || remoteId == null) return; + setLoading(true); + setError(null); + setCopied(false); + try { + const result = await server.generateInstructionsFromChat({ sessionId: remoteId }); + setDraft(result.instructions); + setSources(result.sources); + } catch (caught) { + setDraft(''); + setSources([]); + setError(getErrorMessage(caught, 'Could not generate instructions from this chat')); + } finally { + setLoading(false); + } + }; + + const show = () => { + setOpen(true); + void generate(); + }; + + const apply = async () => { + if (!canApply || agentSpecRef.current === null || remoteId == null || !hasGenerateInstructionsFromChat(server)) { + return; + } + const instructions = draft.trim(); + if (instructions.length === 0) return; + setApplying(true); + setError(null); + try { + await flushAgentSpec(); + const latest = agentSpecRef.current; + if (latest === null) return; + const next = { ...latest, instructions }; + const updated = await server.updateSession({ sessionId: remoteId, agentSpec: next }); + adoptAgentSpec({ agentSpec: next, updatedAt: updated.updatedAt }); + setOpen(false); + setDraft(''); + setSources([]); + } catch (caught) { + setError(getErrorMessage(caught, 'Could not apply instructions to this chat')); + } finally { + setApplying(false); + } + }; + + const copyDraft = async () => { + if (draft.trim().length === 0 || typeof navigator === 'undefined' || navigator.clipboard == null) return; + await navigator.clipboard.writeText(draft); + setCopied(true); + }; + + return ( + <> + + + !next && close()} + title="Instructions from this chat" + className="md:h-auto md:max-h-[85dvh] md:max-w-2xl" + aria-label="Instructions from this chat" + > +
+
+

+ This is a draft from the conversation. Edit it before it becomes this chat's system instructions. + Nothing is saved until you apply it. +

+ +