Merge remote-tracking branch 'origin/main' into SaaS-update

# Conflicts:
#	frontend/editor/src/proprietary/components/chat/ChatContext.tsx
#	frontend/editor/src/saas/components/shared/TrialStatusBanner.tsx
This commit is contained in:
James Brunton
2026-06-12 09:58:40 +01:00
59 changed files with 10405 additions and 227 deletions
@@ -361,6 +361,7 @@ interface ChatContextValue {
/** Ordered log of every progress event for the current in-flight request. */
progressLog: AiWorkflowProgress[];
sendMessage: (content: string) => Promise<void>;
cancelMessage: () => void;
/** Abort any in-flight request and reset the chat to an empty conversation. */
clearChat: () => void;
}
@@ -422,38 +423,42 @@ export function ChatProvider({ children }: { children: ReactNode }) {
const files = await Promise.all(descriptors.map(downloadFile));
const operation: ToolOperation = {
toolId: "ai-workflow",
timestamp: Date.now(),
};
const isVersionMapping =
sourceStubs.length > 0 && files.length === sourceStubs.length;
const stubs = files.map((file, i) =>
isVersionMapping
? createChildStub(sourceStubs[i], operation, file)
: createNewStirlingFileStub(file),
);
const stirlingFiles = files.map((file, i) =>
createStirlingFile(file, stubs[i].id),
);
if (sourceStubs.length > 0) {
// Always consume the inputs so merge/split inputs are removed from the workbench.
// For 1:1 operations (rotate, compress) the outputs carry the version chain; for
// merge/split they're fresh roots.
const operation: ToolOperation = {
toolId: "ai-workflow",
timestamp: Date.now(),
};
const isVersionMapping = files.length === sourceStubs.length;
const stubs = files.map((file, i) =>
isVersionMapping
? createChildStub(sourceStubs[i], operation, file)
: createNewStirlingFileStub(file),
);
const stirlingFiles = files.map((file, i) =>
createStirlingFile(file, stubs[i].id),
);
await fileActions.consumeFiles(
sourceStubs.map((s) => s.id),
stirlingFiles,
stubs,
);
} else {
// No inputs were provided (unlikely for completed workflows, but handle it safely).
// No inputs: pass raw files so addFiles assigns consistent IDs. Pre-assigning stub IDs
// here would cause a fileId mismatch in filesRef, making getFiles() clone the file
// on every render and breaking useFileWithUrl's memoisation (continuous PDF reloads).
await fileActions.addFiles(files, { selectFiles: true });
}
},
[fileActions, downloadFile],
);
const cancelMessage = useCallback(() => {
abortRef.current?.abort();
}, []);
const clearChat = useCallback(() => {
abortRef.current?.abort();
abortRef.current = null;
@@ -494,7 +499,6 @@ export function ChatProvider({ children }: { children: ReactNode }) {
formData.append(`conversationHistory[${i}].role`, message.role);
formData.append(`conversationHistory[${i}].content`, message.content);
});
const response = await fetch(
`${getApiBaseUrl()}/api/v1/ai/orchestrate/stream`,
{
@@ -637,6 +641,7 @@ export function ChatProvider({ children }: { children: ReactNode }) {
progress: state.progress,
progressLog: state.progressLog,
sendMessage,
cancelMessage,
clearChat,
}}
>
@@ -625,7 +625,7 @@ export default function AdminGeneralSection() {
data={defaultLocaleOptions}
searchable
clearable
placeholder="en_GB"
placeholder="en_US"
comboboxProps={{ zIndex: Z_INDEX_CONFIG_MODAL }}
disabled={!loginEnabled}
/>
@@ -26,10 +26,10 @@ vi.mock("react-i18next", () => ({
// Mock i18n module to avoid initialization
vi.mock("@app/i18n", () => ({
updateSupportedLanguages: vi.fn(),
supportedLanguages: { "en-GB": "English" },
supportedLanguages: { "en-US": "English (US)" },
rtlLanguages: [],
default: {
language: "en-GB",
language: "en-US",
changeLanguage: vi.fn(),
options: {},
},