From 5227fd2ba51f8421ccd28595bdb789bbc61514d4 Mon Sep 17 00:00:00 2001 From: acaicai Date: Thu, 16 Jul 2026 08:28:17 -0700 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E4=BB=BB=E5=8A=A1=E9=A1=B5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20Open=20Design=20#od-task=3D=20=E9=94=9A?= =?UTF-8?q?=E7=82=B9=E9=A2=84=E5=A1=AB=E6=8F=90=E7=A4=BA=E8=AF=8D,sessionS?= =?UTF-8?q?torage=20=E4=B8=AD=E8=BD=AC=E8=B7=A8=20401=20=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E4=B8=8D=E4=B8=A2=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../components/console/task/task-input.tsx | 6 +- frontend/src/pages/console/user/tasks.tsx | 10 ++- frontend/src/utils/od-task-import.ts | 72 +++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 frontend/src/utils/od-task-import.ts diff --git a/frontend/src/components/console/task/task-input.tsx b/frontend/src/components/console/task/task-input.tsx index 0b728d88b..d019eabb2 100644 --- a/frontend/src/components/console/task/task-input.tsx +++ b/frontend/src/components/console/task/task-input.tsx @@ -51,13 +51,15 @@ function isIdentityWithRepos(identity: DomainGitIdentity): boolean { interface TaskInputProps { repos: string[]; + /** Initial task content (e.g. the Open Design #od-task= handoff prompt); applied on first mount only. */ + initialContent?: string; onTaskCreated: () => void; } -export function TaskInput({ repos, onTaskCreated }: TaskInputProps) { +export function TaskInput({ repos, initialContent, onTaskCreated }: TaskInputProps) { const navigate = useNavigate() const { t } = useTranslation() - const [taskContent, setTaskContent] = useState(""); + const [taskContent, setTaskContent] = useState(() => initialContent ?? ""); const taskType = ConstsTaskType.TaskTypeDevelop; const [skillPopoverOpen, setSkillPopoverOpen] = useState(false); const [searchInput, setSearchInput] = useState(""); diff --git a/frontend/src/pages/console/user/tasks.tsx b/frontend/src/pages/console/user/tasks.tsx index ad9657686..971b70ac3 100644 --- a/frontend/src/pages/console/user/tasks.tsx +++ b/frontend/src/pages/console/user/tasks.tsx @@ -25,6 +25,7 @@ import dayjs from "dayjs"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { useCommonData } from "@/components/console/data-provider"; +import { confirmOdTaskConsumed, takeOdTaskContent } from "@/utils/od-task-import"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; @@ -45,6 +46,12 @@ export default function TasksPage() { const [stopping, setStopping] = useState(false) const loadMoreRef = useRef(null) const loadingRef = useRef(false) + // Open Design handoff: od-web carries the prompt in the #od-task= fragment. + // takeOdTaskContent() decodes it (persisting to sessionStorage and stripping + // the URL on the spot, so the value survives a 401 → /login round trip and + // pre-fills again on the post-login mount); the persisted copy is dropped + // after the first successful list fetch proves the user is signed in. + const [importedTaskContent] = useState(() => takeOdTaskContent()) const handleConfirmDeleteTask = () => { if (!taskToDelete?.id) { @@ -135,6 +142,7 @@ export default function TasksPage() { } apiRequest('v1UsersTasksList', { page: pageNum, size: PAGE_SIZE }, [], (resp) => { if (resp.code === 0) { + confirmOdTaskConsumed() const newTasks = resp.data?.tasks || [] setTasks(prev => append ? [...prev, ...newTasks] : newTasks) setHasMore(newTasks.length >= PAGE_SIZE) @@ -191,7 +199,7 @@ export default function TasksPage() {

{t("consoleTasks.title")}

- { setPage(1); setHasMore(true); fetchTasks(1, false); reloadProjects(); reloadUnlinkedTasks(); }} /> + { setPage(1); setHasMore(true); fetchTasks(1, false); reloadProjects(); reloadUnlinkedTasks(); }} />
diff --git a/frontend/src/utils/od-task-import.ts b/frontend/src/utils/od-task-import.ts new file mode 100644 index 000000000..a0b18a220 --- /dev/null +++ b/frontend/src/utils/od-task-import.ts @@ -0,0 +1,72 @@ +import { MAX_TASK_CONTENT_LENGTH } from "@/components/console/task/task-content-limit" + +// Open Design "export to MonkeyCode" handoff: od-web puts the development +// prompt into the URL fragment as `#od-task=`. Fragments are +// never sent to the server, so the presigned OSS link inside the prompt stays +// out of access logs. The tasks page decodes it and pre-fills the task input. +// +// The decoded prompt is persisted to sessionStorage the moment it is seen, +// because the 401 login redirect (window.location.href = '/login') discards +// the current URL: sessionStorage survives the login round trip in the same +// tab, so the pre-fill works whether or not the user was signed in when the +// link opened. The fragment itself is stripped immediately — it has no +// further job, and leaving it invites refresh re-fills and link leaks. + +const OD_TASK_HASH_RE = /(?:^#|[#&])od-task=([A-Za-z0-9_-]+)/ +const PENDING_KEY = "mc-od-task-pending" + +function decodeOdTaskHash(): string | null { + const match = OD_TASK_HASH_RE.exec(window.location.hash) + if (!match) return null + try { + const b64 = match[1].replace(/-/g, "+").replace(/_/g, "/") + const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4) + const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0)) + let text = new TextDecoder().decode(bytes).slice(0, MAX_TASK_CONTENT_LENGTH) + // .slice counts UTF-16 units and can cut an astral pair in half; drop a + // trailing lone high surrogate rather than hand the textarea invalid text. + const last = text.charCodeAt(text.length - 1) + if (last >= 0xd800 && last <= 0xdbff) text = text.slice(0, -1) + return text + } catch { + return null + } +} + +/** + * Resolve the pending Open Design prompt for the task input, if any: a fresh + * `#od-task=` fragment wins (persisted to sessionStorage and stripped from the + * URL on the spot), otherwise a previously persisted value — e.g. from the + * mount that ran right before a 401 bounced this tab through /login. + */ +export function takeOdTaskContent(): string | null { + const fromHash = decodeOdTaskHash() + if (fromHash != null) { + try { + sessionStorage.setItem(PENDING_KEY, fromHash) + } catch { + // sessionStorage unavailable (private mode etc.): the prompt still + // pre-fills this mount; it just won't survive a login round trip. + } + history.replaceState(null, "", window.location.pathname + window.location.search) + return fromHash + } + try { + return sessionStorage.getItem(PENDING_KEY) + } catch { + return null + } +} + +/** + * Call once the user is proven signed in (first successful authenticated + * fetch on the tasks page): the displayed pre-fill is now stable, so drop the + * persisted copy to keep later visits from resurrecting a stale prompt. + */ +export function confirmOdTaskConsumed(): void { + try { + sessionStorage.removeItem(PENDING_KEY) + } catch { + // Best-effort cleanup; a leftover value is bounded by the tab's lifetime. + } +} From 32581e5d953e353f22cb60bdf1e6df64c2e7c36f Mon Sep 17 00:00:00 2001 From: acaicai Date: Fri, 17 Jul 2026 00:04:12 -0700 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E5=90=AF=E5=8A=A8=E6=97=B6=E5=89=A5?= =?UTF-8?q?=E7=A6=BB=20URL=20=E5=86=85=E5=B5=8C=20basic-auth=20=E5=87=AD?= =?UTF-8?q?=E8=AF=81=E2=80=94=E2=80=94=E5=B8=A6=E5=87=AD=E8=AF=81=E6=89=93?= =?UTF-8?q?=E5=BC=80=E6=97=B6=E6=89=80=E6=9C=89=20fetch=20=E8=A2=AB?= =?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=E6=8B=92=E7=BB=9D,=E8=A1=A8?= =?UTF-8?q?=E7=8E=B0=E4=B8=BA=E5=85=A8=E7=AB=99=E7=99=BB=E5=BD=95=E6=80=81?= =?UTF-8?q?=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- frontend/src/main.tsx | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index d8c9d19fa..d1c2579e9 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -16,6 +16,28 @@ import relativeTime from 'dayjs/plugin/relativeTime'; dayjs.extend(duration); dayjs.extend(relativeTime); +// Chrome refuses to construct fetch Requests on a document whose URL embeds +// basic-auth credentials (https://user:pass@host/...): every relative API call +// throws "Request cannot be constructed from a URL that includes credentials", +// which reads like a global sign-out. Gated test deployments are opened via +// such credentialed links (e.g. the Open Design export jump); that first +// navigation already primed the browser's HTTP auth cache, so replacing with +// the credential-free same-origin URL keeps access, keeps the fragment +// (#od-task= included), and unbreaks fetch. Must be absolute — a relative +// replace would resolve against the credentialed base and keep the userinfo. +function stripUrlCredentials(): boolean { + try { + const base = new URL(document.baseURI) + if (!base.username && !base.password) return false + window.location.replace( + window.location.origin + window.location.pathname + window.location.search + window.location.hash, + ) + return true + } catch { + return false + } +} + function renderApp() { createRoot(document.getElementById('root')!).render( @@ -26,4 +48,6 @@ function renderApp() { ) } -void initI18n().then(renderApp) +if (!stripUrlCredentials()) { + void initI18n().then(renderApp) +}