From a93b03c8a5a888cfa7fac1fd6cb7a814221adf72 Mon Sep 17 00:00:00 2001 From: maria Date: Wed, 21 Oct 2026 06:57:52 +0000 Subject: [PATCH 01/73] feat(web): composer stays usable during approvals, and sending declines and redirects (#21503) --- .../src/features/threads/ThreadComposer.tsx | 35 ++- .../features/threads/ThreadDetailScreen.tsx | 60 ++++- .../features/threads/ThreadRouteScreen.tsx | 1 + .../src/state/use-selected-thread-requests.ts | 1 + .../src/state/use-thread-composer-state.ts | 29 ++- apps/web/src/components/ChatView.tsx | 99 ++++++++ apps/web/src/components/chat/ChatComposer.tsx | 222 +++++++++--------- .../src/state/threadRequests.test.ts | 60 ++++- .../src/state/threadRequests.ts | 20 ++ 9 files changed, 391 insertions(+), 136 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index f0132833c4fa..1df4dcab7e49 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -178,7 +178,11 @@ export interface ThreadComposerProps { readonly onStopThread: () => void; /** Present while the thread's queue is held. */ readonly onResumeQueue: (() => Promise) | null; - readonly onSendMessage: (followUp?: ActiveTurnComposerAction) => Promise; + /** `stillSendable` reports whether the draft is unchanged after any wait before sending. */ + readonly onSendMessage: ( + followUp?: ActiveTurnComposerAction, + stillSendable?: () => boolean, + ) => Promise; readonly onShowUsageLimits: (report: UsageLimitsReport | null) => void; readonly canSwitchProvider: boolean; readonly onUpdateModelSelection: (modelSelection: ModelSelection) => void; @@ -356,6 +360,7 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps const inputRef = props.editorRef ?? fallbackInputRef; const [isFocused, setIsFocused] = useState(false); const pendingPastedTextAttachmentCountRef = useRef(0); + const pendingNativeImagePasteCountRef = useRef(0); const [pendingPastedTextAttachmentCount, setPendingPastedTextAttachmentCount] = useState(0); const settingsSheetPresentation = useThreadSettingsSheetPresentation({ editorRef: inputRef, @@ -599,7 +604,12 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps setComposerEditing(false); }; const handleSend = async (followUp?: ActiveTurnComposerAction) => { - if (isSubmissionLocked || pendingPastedTextAttachmentCountRef.current > 0) return; + if ( + isSubmissionLocked || + pendingPastedTextAttachmentCountRef.current > 0 || + pendingNativeImagePasteCountRef.current > 0 + ) + return; if (usageLimitsOffered && isUsageLimitsCommand(draftMessage) && draftAttachments.length === 0) { if (openUsageLimits()) onChangeDraftMessage(""); return; @@ -607,7 +617,20 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps const threadKey = scopedThreadKey(props.environmentId, props.selectedThread.id); if (inFlightThreadIdsRef.current.has(threadKey)) return; inFlightThreadIdsRef.current.add(threadKey); - const sent = await settlePromise(() => onSendMessage(followUp)); + const draftAtSend = getComposerDraftSnapshot(composerDraftKey); + const stillSendable = () => { + const draft = getComposerDraftSnapshot(composerDraftKey); + return ( + pendingPastedTextAttachmentCountRef.current === 0 && + pendingNativeImagePasteCountRef.current === 0 && + draft.text === draftAtSend.text && + draft.attachments.length === draftAtSend.attachments.length && + draft.attachments.every( + (attachment, index) => attachment.id === draftAtSend.attachments[index]?.id, + ) + ); + }; + const sent = await settlePromise(() => onSendMessage(followUp, stillSendable)); inFlightThreadIdsRef.current.delete(threadKey); if (AsyncResult.isFailure(sent)) { // The composer state already surfaces send errors; only the lock needed releasing. @@ -915,7 +938,11 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps }} onSelectionChange={composerMenu.onSelectionChange} onPasteImages={(uris) => { - if (!isSubmissionLocked) void props.onNativePasteImages(uris); + if (isSubmissionLocked) return; + pendingNativeImagePasteCountRef.current += 1; + void props.onNativePasteImages(uris).finally(() => { + pendingNativeImagePasteCountRef.current -= 1; + }); }} onPasteText={(paste) => { if (isSubmissionLocked) return; diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 85e8dd96183a..9741e4644441 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -39,7 +39,6 @@ import type { EnvironmentId, MessageId, ModelSelection, - ProviderApprovalDecision, ProviderInteractionMode, RuntimeMode, RuntimeRequestId, @@ -52,7 +51,11 @@ import { appendCodexArtifactTemplateUsePrompt, type CodexArtifactTemplate, } from "@t3tools/shared/codexArtifactTemplates"; -import type { ThreadUserInputQuestion } from "@t3tools/client-runtime/state/thread-requests"; +import { + declinePendingApprovals, + type ApprovalResponder, + type ThreadUserInputQuestion, +} from "@t3tools/client-runtime/state/thread-requests"; import { presentPendingBackgroundWork, presentProviderGoal, @@ -199,6 +202,7 @@ export interface ThreadDetailScreenProps { | { readonly kind: "failed"; readonly reason: string; readonly onEditTask: () => void } | null; readonly activePendingApproval: PendingApproval | null; + readonly activePendingApprovals: ReadonlyArray; readonly respondingApprovalId: RuntimeRequestId | null; readonly activePendingUserInput: PendingUserInput | null; readonly activePendingUserInputDrafts: Record; @@ -236,16 +240,16 @@ export interface ThreadDetailScreenProps { readonly onStopThread: () => void; /** Present while the thread's queue is held. */ readonly onResumeQueue: (() => Promise) | null; - readonly onSendMessage: (followUp?: ActiveTurnComposerAction) => Promise; + readonly onSendMessage: ( + followUp?: ActiveTurnComposerAction, + beforeDispatch?: () => Promise, + ) => Promise; readonly onReconnectEnvironment: () => void; readonly canSwitchThreadProvider: boolean; readonly onUpdateThreadModelSelection: (modelSelection: ModelSelection) => void; readonly onUpdateThreadRuntimeMode: (runtimeMode: RuntimeMode) => void; readonly onUpdateThreadInteractionMode: (interactionMode: ProviderInteractionMode) => void; - readonly onRespondToApproval: ( - requestId: RuntimeRequestId, - decision: ProviderApprovalDecision, - ) => Promise; + readonly onRespondToApproval: ApprovalResponder; readonly onSelectUserInputOption: ( requestId: RuntimeRequestId, question: ThreadUserInputQuestion, @@ -1152,12 +1156,37 @@ export const ThreadDetailScreen = function ThreadDetailScreen(props: ThreadDetai selectedThreadKey, ]); - const handleSendMessage = async (followUp?: ActiveTurnComposerAction) => { + const activePendingApprovalsRef = useRef(props.activePendingApprovals); + useLayoutEffect(() => { + activePendingApprovalsRef.current = props.activePendingApprovals; + }, [props.activePendingApprovals]); + const handleSendMessage = async ( + followUp?: ActiveTurnComposerAction, + stillSendable?: () => boolean, + ) => { const targetThreadKey = selectedThreadKey; const hasUserMessage = selectedThreadFeed.some( (entry) => entry.type === "message" && entry.message.role === "user", ); - const messageId = await props.onSendMessage(followUp); + // Runs only once the send path has validated the draft and is about to deliver it, so an + // empty submit, a queued-message edit, or `/feedback` leaves approvals alone. + const approvalsToDecline = props.activePendingApprovals; + const declineBeforeDispatch = + approvalsToDecline.length > 0 && props.activePendingUserInput === null + ? async () => + props.respondingApprovalId === null && + (await declinePendingApprovals(approvalsToDecline, props.onRespondToApproval)) && + selectedThreadKeyRef.current === targetThreadKey && + // A draft edited or pasted into during the decline isn't what the user sent. + stillSendable?.() !== false && + // An approval that arrived during the decline would block the redirect; the draft + // stays so the next send declines it too. + !activePendingApprovalsRef.current.some( + (approval) => + !approvalsToDecline.some((declined) => declined.requestId === approval.requestId), + ) + : undefined; + const messageId = await props.onSendMessage(followUp, declineBeforeDispatch); if (messageId === null || selectedThreadKeyRef.current !== targetThreadKey) { return messageId; } @@ -1568,7 +1597,11 @@ export const ThreadDetailScreen = function ThreadDetailScreen(props: ThreadDetai canOperateThread={props.canOperateThread} reportedModelSelection={reportedModelSelection} editorRef={composerEditorRef} - placeholder="Ask the repo agent, or run a command…" + placeholder={ + props.activePendingApproval + ? "Send a message to decline and redirect the agent" + : "Ask the repo agent, or run a command…" + } contentMaxWidth={contentMaxWidth} connectionState={props.connectionStateLabel} environmentLabel={props.environmentLabel} @@ -1592,7 +1625,12 @@ export const ThreadDetailScreen = function ThreadDetailScreen(props: ThreadDetai : null } draftKey={props.composerDraftKey ?? undefined} - followUpBehavior={props.followUpBehavior} + followUpBehavior={ + props.activePendingApprovals.length > 0 && + props.activePendingUserInput === null + ? "steer" + : props.followUpBehavior + } canSteerActiveTurn={props.canSteerActiveTurn} hidden={composerSlotHidden} queuedEdit={ diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 0abb16df6455..e134283ae541 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -932,6 +932,7 @@ function ThreadRouteContent( : null } activePendingApproval={requests.activePendingApproval} + activePendingApprovals={requests.activePendingApprovals} respondingApprovalId={requests.respondingApprovalId} activePendingUserInput={requests.activePendingUserInput} activePendingUserInputDrafts={requests.activePendingUserInputDrafts} diff --git a/apps/mobile/src/state/use-selected-thread-requests.ts b/apps/mobile/src/state/use-selected-thread-requests.ts index ac58170cd0d4..98bec9753325 100644 --- a/apps/mobile/src/state/use-selected-thread-requests.ts +++ b/apps/mobile/src/state/use-selected-thread-requests.ts @@ -371,6 +371,7 @@ export function useSelectedThreadRequests() { return { activePendingApproval, + activePendingApprovals, activePendingUserInput, activePendingUserInputDrafts, activePendingUserInputAnswers, diff --git a/apps/mobile/src/state/use-thread-composer-state.ts b/apps/mobile/src/state/use-thread-composer-state.ts index f40da862253f..ebdf9769d46f 100644 --- a/apps/mobile/src/state/use-thread-composer-state.ts +++ b/apps/mobile/src/state/use-thread-composer-state.ts @@ -528,7 +528,10 @@ export function useThreadComposerState() { scheduleUnusedComposerAttachmentCleanup(draft.attachments); }; - const onSendMessage = async (followUpOverride?: ActiveTurnComposerAction) => { + const onSendMessage = async ( + followUpOverride?: ActiveTurnComposerAction, + beforeDispatch?: () => Promise, + ) => { if ( selectedThreadShell && selectedEnvironmentRuntime?.connectionState === "connected" && @@ -645,15 +648,35 @@ export function useThreadComposerState() { return null; } + if (beforeDispatch !== undefined) { + if (!(await beforeDispatch())) return null; + // A context paste still landing, or any draft or settings change while approvals were + // declined, means this snapshot is no longer what the user sent. + if (appAtomRegistry.get(composerContextImportsAtom)[threadKey]) return null; + const current = getComposerDraftSnapshot(threadKey); + if ( + current.text !== draft.text || + JSON.stringify(current.context) !== JSON.stringify(draft.context) || + JSON.stringify(current.modelSelection) !== JSON.stringify(draft.modelSelection) || + current.runtimeMode !== draft.runtimeMode || + current.interactionMode !== draft.interactionMode || + current.attachments.length !== attachments.length || + current.attachments.some((attachment, index) => attachment.id !== attachments[index]?.id) + ) + return null; + } + // Resolved here rather than at drain time: the outbox can deliver minutes // later, and the choice belongs to the moment the user pressed send. // Steering travels as "auto" so a turn that ends in the meantime degrades // to a queued run on the server instead of failing the delivery and // bouncing the message back into the draft. + // A redirect declined approvals to steer the waiting turn, not to queue behind it. + const activeTurnDefault = beforeDispatch !== undefined ? "steer" : followUpBehavior; const followUpAction = resolveComposerDispatchMode({ running: activeThreadBusy && canSteerActiveTurn, - alternateModifier: followUpOverride !== undefined && followUpOverride !== followUpBehavior, - activeTurnDefault: followUpBehavior, + alternateModifier: followUpOverride !== undefined && followUpOverride !== activeTurnDefault, + activeTurnDefault, }); const setupRunning = [selectedThreadActivityRun?.status, selectedThreadRuntime?.status].some( (status) => status === "preparing" || status === "starting", diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 1f9eb813a455..f8b37133d460 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -91,6 +91,7 @@ import { import { readPastedComposerContext } from "./composerInlineTokenPaste"; import { isPasteAsTextShortcut } from "@t3tools/client-runtime/text-paste"; import { effectiveSnoozed, threadWokeAt } from "@t3tools/client-runtime/state/thread-settled"; +import { declinePendingApprovals } from "@t3tools/client-runtime/state/thread-requests"; import { presentThreadShell } from "@t3tools/client-runtime/state/shell"; import { childThreadInputsTitle } from "@t3tools/client-runtime/state/threads"; import { useAcknowledgeThreadWoke, useThreadActions } from "../hooks/useThreadActions"; @@ -2542,6 +2543,13 @@ export default function ChatView(props: ChatViewProps) { composerRef, setThreadError, }); + // A send awaits uploads before it declines, so it reads the approvals pending by then. + const pendingApprovalsRef = useRef(pendingApprovals); + const respondingRequestIdsRef = useRef(respondingRequestIds); + useLayoutEffect(() => { + pendingApprovalsRef.current = pendingApprovals; + respondingRequestIdsRef.current = respondingRequestIds; + }, [pendingApprovals, respondingRequestIds]); const activeProposedPlan = (() => { if (!latestRunSettled) { return null; @@ -6213,6 +6221,9 @@ export default function ChatView(props: ChatViewProps) { }, ) => { e?.preventDefault(); + // What the user pressed send on; uploads below can take a while, and a redirect must not + // decline approvals for a draft that changed meanwhile. + const promptAtSend = promptRef.current; if ( usageLimitsOffered && usageLimitsKey !== null && @@ -6787,6 +6798,94 @@ export default function ChatView(props: ChatViewProps) { ); return; } + // A message sent while approvals are pending redirects the agent. They are declined only + // here, after everything above has accepted the send, so local commands and rejected + // drafts leave them alone. After navigating away during an upload, the shown approvals + // are another thread's, so this send leaves them alone too. + const approvalsToDecline = + currentRouteThreadKeyRef.current === routeThreadKey + ? pendingApprovalsRef.current.filter((approval) => + sendCtx?.shownPendingApprovalIds.includes(approval.requestId), + ) + : []; + if (approvalsToDecline.length > 0) { + // A draft edited since send, or a paste still landing in it, is no longer the message + // the user sent. + const attachmentIdsAtSend = composerAttachmentsSnapshot.map((attachment) => attachment.id); + // A direct annotation send writes its image and reference into the draft as it sends, + // so the live draft may still lack them; anything else differing is a real edit. + const annotationImageId = directAnnotation?.image?.id; + const attachmentIdsBeforeAnnotation = attachmentIdsAtSend.filter( + (id) => id !== annotationImageId, + ); + const sameIds = (left: ReadonlyArray, right: ReadonlyArray) => + left.length === right.length && left.every((id, index) => id === right[index]); + const draftChangedSinceSend = () => { + if (composerRef.current?.hasPendingAttachments() === true) return true; + const sendContext = composerRef.current?.getSendContext(); + const attachmentIds = sendContext + ? [...sendContext.images, ...sendContext.files].map((attachment) => attachment.id) + : attachmentIdsAtSend; + const promptUnchanged = + promptRef.current === promptAtSend || promptRef.current === promptForSend; + const attachmentsUnchanged = + sameIds(attachmentIds, attachmentIdsAtSend) || + (annotationImageId !== undefined && + sameIds(attachmentIds, attachmentIdsBeforeAnnotation)); + return !promptUnchanged || !attachmentsUnchanged; + }; + const draftBeforeDecline = useComposerDraftStore + .getState() + .getComposerDraft(composerDraftTarget); + const changedBeforeDecline = draftChangedSinceSend(); + const pendingIdsBeforeDecline = pendingApprovalsRef.current.map( + (approval) => approval.requestId, + ); + const approvalResponding = approvalsToDecline.some((approval) => + respondingRequestIdsRef.current.includes(approval.requestId), + ); + const declined = + !changedBeforeDecline && + !approvalResponding && + (await declinePendingApprovals(approvalsToDecline, onRespondToApprovalStable).catch( + () => false, + )); + const draftChanged = + changedBeforeDecline || + draftChangedSinceSend() || + useComposerDraftStore.getState().getComposerDraft(composerDraftTarget) !== + draftBeforeDecline; + // An approval that arrived during the decline would block the redirect; keep the draft so + // the next send declines it too. + const approvalArrived = pendingApprovalsRef.current.some( + (approval) => !pendingIdsBeforeDecline.includes(approval.requestId), + ); + if ( + !declined || + draftChanged || + approvalArrived || + currentRouteThreadKeyRef.current !== routeThreadKey + ) { + sendInFlightRef.current = false; + setDockedDraftHeroThreadKey((currentThreadKey) => + currentThreadKey === activeThreadKey ? null : currentThreadKey, + ); + if (draftChanged || approvalResponding || (declined && approvalArrived)) { + toastManager.add({ + type: "info", + title: draftChanged + ? declined + ? "Approval declined. Your message changed, so it wasn't sent." + : "Your message changed, so it wasn't sent." + : approvalResponding + ? "An approval is already being answered, so your message wasn't sent." + : "Another approval arrived, so your message wasn't sent.", + description: "Send again when it's ready.", + }); + } + return; + } + } if (sendQueuesBehindSetup) { setSetupQueueSendThreadKey(activeThreadKey); } else { diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index f600b831c53e..ad8c6603ec1f 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -1489,6 +1489,7 @@ export interface ChatComposerHandle { selectedProviderModels: ReadonlyArray; interactionMode: ProviderInteractionMode; interactionModeEnabled: boolean; + shownPendingApprovalIds: ReadonlyArray; }; validateProviderInput: (providerInput: string) => boolean; setMultipleModelSelections: (selections: ReadonlyArray) => void; @@ -2828,7 +2829,14 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const nonPersistedComposerImageIdSet = new Set(nonPersistedComposerImageIds); const isComposerApprovalState = activePendingApproval !== null; - const composerSuggestionsVisible = composerMenuOpen && !isComposerApprovalState; + // A send declines the approvals shown here to redirect the agent, so it steers the waiting + // turn instead of queueing behind it. Approvals the typing guard holds stay unseen and pending. + const followUpBehavior = pendingApprovals.some( + (approval) => !heldRequestIds.has(approval.requestId), + ) + ? "steer" + : settings.followUpBehavior; + const composerSuggestionsVisible = composerMenuOpen; const composerSuggestionListVisible = composerSuggestionsVisible && composerMenuItems.length > 0; const activePendingUserInput = pendingUserInputs[0] ?? null; const isChoiceOnlyPendingQuestion = @@ -2837,7 +2845,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) isComposerApprovalState || pendingUserInputs.length > 0 || (!isComposerCollapsedMobile && showPlanFollowUpPrompt && activeProposedPlan !== null); - const showCollapsedMobilePromptRow = isComposerCollapsedMobile && !isComposerApprovalState; + const showCollapsedMobilePromptRow = isComposerCollapsedMobile; const showComposerAttachAction = fileStagingLimit !== null && (!activePendingProgress || @@ -4057,7 +4065,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) resolveComposerDispatchMode({ running: phase === "running", alternateModifier: false, - activeTurnDefault: settings.followUpBehavior, + activeTurnDefault: followUpBehavior, canSteer: !isCompacting, }), submissionIntent, @@ -4080,7 +4088,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) resolveComposerDispatchMode({ running: phase === "running", alternateModifier: event.metaKey || event.ctrlKey, - activeTurnDefault: settings.followUpBehavior, + activeTurnDefault: followUpBehavior, canSteer: !isCompacting, }), ); @@ -4092,13 +4100,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) resolveComposerDispatchMode({ running: phase === "running", alternateModifier: false, - activeTurnDefault: settings.followUpBehavior, + activeTurnDefault: followUpBehavior, canSteer: !isCompacting, }), ); }; const submitCitationAndSendStable = useStableCallback(submitCitationAndSend); - // A question or approval took the editor while a citation comment was + // A question took the editor while a citation comment was // unsaved. Keep the comment in the prompt that comes back afterward. const saveCitationDraftToPrompt = (replacedValue: string, savedValue: string) => { if (replacedValue !== prompt) return; @@ -4174,7 +4182,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) if (event.shiftKey || event.altKey || event.metaKey || event.ctrlKey || event.isComposing) { return false; } - if (isComposerApprovalState || pendingUserInputs.length > 0) return false; + if (pendingUserInputs.length > 0) return false; if ( composerImagesRef.current.length > 0 || composerFilesRef.current.length > 0 || @@ -4256,7 +4264,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) resolveComposerDispatchMode({ running: phase === "running", alternateModifier: submissionIntent === "alternate", - activeTurnDefault: settings.followUpBehavior, + activeTurnDefault: followUpBehavior, canSteer: !isCompacting, }), submissionIntent, @@ -5375,11 +5383,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) if (isCommandPaletteOpen() || isRevertingCheckpoint) { return; } - if (pendingUserInputs.length > 0 && !isComposerApprovalState) { + if (pendingUserInputs.length > 0) { setIsStashMenuOpen((open) => !open); return; } - if (isComposerApprovalState || projectSelectionRequired || activePendingProgress !== null) { + if (projectSelectionRequired || activePendingProgress !== null) { return; } void stashCurrentPromptStable(); @@ -5388,7 +5396,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) return () => window.removeEventListener("keydown", handler, true); }, [ activePendingProgress, - isComposerApprovalState, isComposerModelPickerOpen, keybindings, pendingUserInputs.length, @@ -5427,7 +5434,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) !options?.skipImageInlineChip && (options?.selection !== undefined || isConnecting || - isComposerApprovalState || pendingUserInputs.length > 0 || projectSelectionRequired || stripInlineContextReferences(promptRef.current).trim().length > 0); @@ -5750,7 +5756,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) if ( text.length === 0 || isConnecting || - isComposerApprovalState || activePendingIsResponding || (pendingUserInputs.length > 0 && !activePendingProgress?.activeQuestion) || projectSelectionRequired || @@ -5980,7 +5985,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const canAnswer = canOperateThread && !isConnecting && - !isComposerApprovalState && !activePendingIsResponding && activePendingProgress?.activeQuestion?.allowCustomAnswer !== false; const text = `${records @@ -6169,7 +6173,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) stepReasoningEffort: (direction) => { if ( noProviderAvailable || - isComposerApprovalState || pendingUserInputs.length > 0 || // A queued-message edit draft is discarded when the edit ends. isEditingQueuedMessage @@ -6298,6 +6301,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) }, getSendContext: () => ({ answeringPendingUserInput: activePendingProgress !== null, + shownPendingApprovalIds: pendingApprovals + .map((approval) => approval.requestId) + .filter((requestId) => !heldRequestIds.has(requestId)), prompt: promptRef.current, images: composerImagesRef.current, files: composerFilesRef.current, @@ -6348,6 +6354,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) isEditingQueuedMessage, composerCursor, activePendingProgress, + pendingApprovals, + heldRequestIds, composerTerminalContexts, insertComposerDraftTerminalContext, insertComposerTextStable, @@ -6362,7 +6370,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) environmentId, primaryEnvironmentId, isConnecting, - isComposerApprovalState, isChoiceOnlyPendingQuestion, isLiteralPendingAnswer, pendingUserInputs.length, @@ -6584,15 +6591,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ) : null} - {!isComposerApprovalState ? ( - - ) : null} +
- {isStashMenuOpen && !composerMenuOpen && !isComposerApprovalState && ( + {isStashMenuOpen && !composerMenuOpen && ( 0 && ( @@ -6756,7 +6759,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) )} {!isComposerCollapsedMobile && - !isComposerApprovalState && (uncommittedSnapShotIds.length > 0 || composerVideos.length > 0 || expandedComposerImages.length > 0) && ( @@ -7013,83 +7015,77 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
)} - {!isComposerCollapsedMobile && - !isComposerApprovalState && - composerOtherFiles.length > 0 && ( -
- {composerOtherFiles.map((file) => { - const fileCanUpload = - supportsAttachmentUploads && - maxFileAttachmentBytes !== null && - file.sizeBytes <= maxFileAttachmentBytes; - const upload = fileCanUpload ? uploadsByImageId[file.id] : undefined; - const needsReattach = composerFileNeedsReattach(file); - const canReattachFile = - fileStagingLimit !== null && file.sizeBytes <= fileStagingLimit; - return ( -
0 && ( +
+ {composerOtherFiles.map((file) => { + const fileCanUpload = + supportsAttachmentUploads && + maxFileAttachmentBytes !== null && + file.sizeBytes <= maxFileAttachmentBytes; + const upload = fileCanUpload ? uploadsByImageId[file.id] : undefined; + const needsReattach = composerFileNeedsReattach(file); + const canReattachFile = + fileStagingLimit !== null && file.sizeBytes <= fileStagingLimit; + return ( +
+ + - - {needsReattach - ? canReattachFile - ? "Attach again" - : "Remove to send" - : upload?.status === "uploading" - ? formatAttachmentUploadProgress(upload.progress) - : formatAttachmentSize(file.sizeBytes)} - - {!needsReattach && upload?.status === "failed" ? ( - - - retryAttachmentUpload({ - environmentId, - image: file, - draftTarget: attachmentDraftTarget, - }) - } - disabled={!canOperateThread} - aria-label={`Retry upload for ${file.name}`} - /> - } - > - - - {upload.reason} - - ) : null} - -
- ); - })} -
- )} + {file.name} + + + {needsReattach + ? canReattachFile + ? "Attach again" + : "Remove to send" + : upload?.status === "uploading" + ? formatAttachmentUploadProgress(upload.progress) + : formatAttachmentSize(file.sizeBytes)} + + {!needsReattach && upload?.status === "failed" ? ( + + + retryAttachmentUpload({ + environmentId, + image: file, + draftTarget: attachmentDraftTarget, + }) + } + disabled={!canOperateThread} + aria-label={`Retry upload for ${file.name}`} + /> + } + > + + + {upload.reason} + + ) : null} + +
+ ); + })} +
+ )}
- {isComposerCollapsedMobile || isComposerApprovalState ? null : ( + {isComposerCollapsedMobile ? null : (
{ + const approval: ThreadPendingApproval = { + requestId, + requestKind: "command", + createdAt: "2026-10-19T00:00:00Z", + responseCapability: "live", + }; + const second: ThreadPendingApproval = { + ...approval, + requestId: RuntimeRequestId.make("second-approval"), + }; + + it("declines every pending approval before the send continues", async () => { + let resolve!: (value: Awaited>) => void; + const firstResponse = new Promise>>((done) => { + resolve = done; + }); + const respond = vi.fn(async (id) => + id === requestId ? firstResponse : AsyncResult.success(undefined), + ); + const send = vi.fn(); + const submission = (async () => { + if (await declinePendingApprovals([approval, second], respond)) send(); + })(); + expect(respond).toHaveBeenCalledWith(requestId, "decline"); + expect(send).not.toHaveBeenCalled(); + resolve(AsyncResult.success(undefined)); + await submission; + expect(respond).toHaveBeenCalledWith(second.requestId, "decline"); + expect(send).toHaveBeenCalledOnce(); + }); + + it.each([AsyncResult.failure(Cause.fail("decline failed")), undefined])( + "does not send when a decline is unsuccessful: %s", + async (response) => { + const send = vi.fn(); + if (await declinePendingApprovals([approval], async () => response)) send(); + expect(send).not.toHaveBeenCalled(); + }, + ); + + it("skips approvals the provider can no longer answer", async () => { + const respond = vi.fn(async () => AsyncResult.success(undefined)); + expect( + await declinePendingApprovals( + [{ ...approval, responseCapability: "not_resumable" }], + respond, + ), + ).toBe(true); + expect(respond).not.toHaveBeenCalled(); + }); +}); + describe("initial user input drafts", () => { const questions = [ { id: "editor", initialAnswer: " Proposed commit\n\n" }, diff --git a/packages/client-runtime/src/state/threadRequests.ts b/packages/client-runtime/src/state/threadRequests.ts index 20dacefca5bc..3735869edb6b 100644 --- a/packages/client-runtime/src/state/threadRequests.ts +++ b/packages/client-runtime/src/state/threadRequests.ts @@ -3,10 +3,30 @@ import type { OrchestrationV2RuntimeRequest, OrchestrationV2UserInputQuestion, ProviderApprovalOption, + ProviderApprovalDecision, ProviderRequestKind, RuntimeRequestId, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; +import type { AtomCommandResult } from "./runtime.ts"; + +export type ApprovalResponder = ( + requestId: RuntimeRequestId, + decision: ProviderApprovalDecision, +) => Promise | undefined>; + +/** Declines every approval a redirect replaces; any one still pending would block the agent. */ +export async function declinePendingApprovals( + approvals: ReadonlyArray, + respond: ApprovalResponder, +): Promise { + for (const approval of approvals) { + // A request the provider can no longer answer has nothing to decline. + if (approval.responseCapability !== "live") continue; + if ((await respond(approval.requestId, "decline"))?._tag !== "Success") return false; + } + return true; +} export interface ThreadPendingApproval { readonly requestId: RuntimeRequestId; From 73daccf4ec123e8a98f84a09e8a5b6372852c9c6 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 21 Oct 2026 00:12:41 -0700 Subject: [PATCH 02/73] docs(agents): name the fast commands and stop polling (#22711) Co-authored-by: Claude Opus 5.5 --- AGENTS.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 95dd9e960654..e87e5bfb1d9f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,6 +83,7 @@ The most common defect in this repo is a change that works on the path you teste - Sharing over the tailnet is three steps: run `vp run dev --share` in the background, wait for the `pairingUrl:` line in its output, then give that full URL to an unpaired browser. Do not wire up `tailscale serve` by hand, open the URL yourself, or consume the user's pairing link. A browser with the reusable dev cookie can use the bare origin. If a normal one-time token was consumed, mint a fresh one with `node apps/server/src/bin.ts pair`. It carries standard scopes, while the startup URL carries admin scopes needed for Connections settings. - To reuse web dev auth across worktrees, configure one fixed `T3CODE_DEV_AUTH_TOKEN` in the main checkout's gitignored `.env`. The `t3.json` setup links that file into worktrees. Never commit or publish the token or a startup URL. See [Reusable dev credential](docs/operations/development.md#reusable-dev-credential). - Stop what you started, by the PID you tracked. See rule 1. +- Need a second checkout? `git worktree add` it next to this worktree, not in `/tmp`. Remove it when done. ## Test data @@ -95,7 +96,9 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real ## Verifying -- Smallest proof that the change works. `vp test run ` for the tests you touched, targeted lint and typecheck for the scope you changed. +- Smallest proof that the change works. `vp test run ` for the tests you touched (`-t ""` in big files), `vp lint `, and `vp run typecheck` in the package you changed. Not `npx tsc`; it is 3x slower than tsc-rs. +- Run `vp run knip:check` (about 35 s) when you add, remove, or rename exports, files, or dependencies. +- Run `node -e` snippets from `apps/server` or the owning package. `effect` does not resolve from the repo root. - Test meaningful logic or observable behavior. Do not render components to static markup to assert props or attributes, or add tests that merely assert callback wiring or mirror the implementation. - **Do not run repo-wide checks.** No `vp check`, no `vp run -r test`, no `vp run -r typecheck` unless I ask. CI owns the full suite. - Backend behavior changes ship with focused tests for that behavior. @@ -137,7 +140,7 @@ For authorized mobile verification, a missing or outdated native client is a bui - UI changes need before/after images. Motion or timing needs a short video. - Upload PR evidence to GitHub. Never commit PR-only screenshots or assets such as `.github/pr-assets/`. - One request is one PR. Split it only when the maintainer asks. Outside contributions follow the stricter [one problem per PR](CONTRIBUTING.md#one-problem) rule. -- When babysitting: poll checks and comments newer than the last push, verify each bot finding against the source, fix real ones, dismiss false positives with a written reason. Stay quiet when nothing is new. Stop when the bots are green on the latest commit. +- When babysitting: call `watch_pull_request` and end your turn. T3 Code wakes you on checks, comments, or conflicts. Do not poll or sleep. Verify each bot finding against the source, fix real ones, dismiss false positives with a written reason. Stay quiet when nothing is new. Stop when the bots are green on the latest commit. ## Documentation From a5d66e22396fcf3b4d3a59a2ae7ad1e158587ad4 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 21 Oct 2026 00:13:20 -0700 Subject: [PATCH 03/73] fix(desktop): new worktrees install Electron from the local download cache (#22712) Co-authored-by: Claude Opus 5.5 --- .github/workflows/ci.yml | 24 +++++++++++ .../scripts/ensure-electron-runtime.mjs | 43 +++++++++++++++---- pnpm-workspace.yaml | 4 +- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d29d970c75ff..d2c569b82175 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,14 @@ jobs: - name: Setup Vite+ uses: ./.github/actions/setup-ci-deps + # electron's install.js (run by ensure:electron) reads this cache before it + # downloads from GitHub. The key changes only with the Electron version. + - name: Cache Electron download + uses: actions/cache@v6 + with: + path: ~/.cache/electron + key: electron-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('node_modules/.pnpm/electron@*/node_modules/electron/package.json') }} + - name: Ensure Electron runtime is installed run: vp run --filter @t3tools/desktop ensure:electron @@ -132,6 +140,14 @@ jobs: - name: Setup Vite+ uses: ./.github/actions/setup-ci-deps + # electron's install.js (run by ensure:electron) reads this cache before it + # downloads from GitHub. The key changes only with the Electron version. + - name: Cache Electron download + uses: actions/cache@v6 + with: + path: ~/.cache/electron + key: electron-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('node_modules/.pnpm/electron@*/node_modules/electron/package.json') }} + - name: Ensure Electron runtime is installed id: electron run: vp run --filter @t3tools/desktop ensure:electron @@ -222,6 +238,14 @@ jobs: - name: Setup Vite+ uses: ./.github/actions/setup-ci-deps + # electron's install.js (run by ensure:electron) reads this cache before it + # downloads from GitHub. The key changes only with the Electron version. + - name: Cache Electron download + uses: actions/cache@v6 + with: + path: ~/.cache/electron + key: electron-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('node_modules/.pnpm/electron@*/node_modules/electron/package.json') }} + - name: Ensure Electron runtime is installed run: vp run --filter @t3tools/desktop ensure:electron diff --git a/apps/desktop/scripts/ensure-electron-runtime.mjs b/apps/desktop/scripts/ensure-electron-runtime.mjs index ae6cd05961b6..933a763dfa35 100644 --- a/apps/desktop/scripts/ensure-electron-runtime.mjs +++ b/apps/desktop/scripts/ensure-electron-runtime.mjs @@ -116,7 +116,26 @@ function runChecked(command, args) { ); } -function installElectronRuntime(electronDir, version) { +// electron's own installer goes through @electron/get, which reuses its +// download cache (~/.cache/electron, ~/Library/Caches/electron) and verifies +// the bundled checksums. pnpm 11 never runs it because electron has no +// install script, so a new worktree would otherwise download 130 MB again. +function installFromElectronCache(electronDir) { + const result = NodeChildProcess.spawnSync( + process.execPath, + [NodePath.join(electronDir, "install.js")], + { stdio: ["ignore", "inherit", "inherit"] }, + ); + if (result.status === 0) { + return true; + } + process.stderr.write( + `electron install.js failed with exit code ${result.status ?? "unknown"}, downloading with curl instead\n`, + ); + return false; +} + +function downloadElectronRuntime(electronDir, version) { const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-electron-")); const zipPath = NodePath.join(tempDir, `electron-v${version}-${hostPlatform}-${hostArch}.zip`); @@ -149,21 +168,27 @@ function installElectronRuntime(electronDir, version) { } } +function resetRuntime(electronDir) { + NodeFS.rmSync(NodePath.join(electronDir, "dist"), { recursive: true, force: true }); + NodeFS.rmSync(NodePath.join(electronDir, "path.txt"), { force: true }); +} + export function ensureElectronRuntime() { const electronPackageJsonPath = require.resolve("electron/package.json"); const electronPackageJson = JSON.parse(NodeFS.readFileSync(electronPackageJsonPath, "utf8")); const electronDir = NodePath.dirname(electronPackageJsonPath); const platformPath = getPlatformPath(); const electronPath = NodePath.join(electronDir, "dist", platformPath); - const missingBeforeInstall = missingRuntimePaths(electronDir, platformPath); - const invalidBeforeInstall = invalidRuntimePaths(electronDir, platformPath); - - if (missingBeforeInstall.length > 0 || invalidBeforeInstall.length > 0) { - if (NodeFS.existsSync(NodePath.join(electronDir, "dist"))) { - NodeFS.rmSync(NodePath.join(electronDir, "dist"), { recursive: true, force: true }); + const needsInstall = () => + missingRuntimePaths(electronDir, platformPath).length > 0 || + invalidRuntimePaths(electronDir, platformPath).length > 0; + + if (needsInstall()) { + resetRuntime(electronDir); + if (!installFromElectronCache(electronDir) || needsInstall()) { + resetRuntime(electronDir); + downloadElectronRuntime(electronDir, electronPackageJson.version); } - NodeFS.rmSync(NodePath.join(electronDir, "path.txt"), { force: true }); - installElectronRuntime(electronDir, electronPackageJson.version); } const missingAfterInstall = missingRuntimePaths(electronDir, platformPath); diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2e6aef538fde..e1d927e6f21e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,7 +12,9 @@ allowBuilds: browser-tabs-lock: false bufferutil: false core-js: false - electron: true + # No install script since Electron 44; ensure:electron runs install.js. + # Keep the key: pnpm fails installs that still record a removed `true`. + electron: false electron-winstaller: false esbuild: true msgpackr-extract: true From dd8452a06599732784c8f1725fde5e8695d652f2 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 21 Oct 2026 00:18:41 -0700 Subject: [PATCH 04/73] test(server): split replay fixtures into per-provider files (#22717) Co-authored-by: Claude Opus 5.5 --- ...RegistryReplayFixtures.integration.test.ts | 213 ++++++ .../ClaudeReplayFixtures.integration.test.ts | 227 +++++- .../CodexReplayFixtures.integration.test.ts | 17 + .../CursorReplayFixtures.integration.test.ts | 29 + .../GrokReplayFixtures.integration.test.ts | 10 + .../MuseReplayFixtures.integration.test.ts | 17 + ...OpenCodeReplayFixtures.integration.test.ts | 68 ++ ...estratorReplayFixtures.integration.test.ts | 704 ------------------ .../PiReplayFixtures.integration.test.ts | 10 + .../testkit/ProviderReplayFixtures.ts | 219 ++++++ apps/server/src/testUtils/shardWeights.json | 10 +- 11 files changed, 817 insertions(+), 707 deletions(-) create mode 100644 apps/server/src/orchestration-v2/testkit/AcpRegistryReplayFixtures.integration.test.ts create mode 100644 apps/server/src/orchestration-v2/testkit/CodexReplayFixtures.integration.test.ts create mode 100644 apps/server/src/orchestration-v2/testkit/CursorReplayFixtures.integration.test.ts create mode 100644 apps/server/src/orchestration-v2/testkit/GrokReplayFixtures.integration.test.ts create mode 100644 apps/server/src/orchestration-v2/testkit/MuseReplayFixtures.integration.test.ts create mode 100644 apps/server/src/orchestration-v2/testkit/OpenCodeReplayFixtures.integration.test.ts delete mode 100644 apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts create mode 100644 apps/server/src/orchestration-v2/testkit/PiReplayFixtures.integration.test.ts create mode 100644 apps/server/src/orchestration-v2/testkit/ProviderReplayFixtures.ts diff --git a/apps/server/src/orchestration-v2/testkit/AcpRegistryReplayFixtures.integration.test.ts b/apps/server/src/orchestration-v2/testkit/AcpRegistryReplayFixtures.integration.test.ts new file mode 100644 index 000000000000..e0a9a233bfd9 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/AcpRegistryReplayFixtures.integration.test.ts @@ -0,0 +1,213 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, describe, it } from "@effect/vitest"; +import { + ProviderDriverKind, + ProviderInstanceId, + type ProviderReplayTranscript, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Stream from "effect/Stream"; +import { TestClock } from "effect/testing"; + +import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; +import { checkpointWorkspace } from "@t3tools/provider-testing/replayWorkspace"; +import { AcpRegistryOrchestratorReplayHarness } from "../Adapters/AcpRegistryAdapterV2.testkit.ts"; +import * as EffectWorker from "../EffectWorker.ts"; +import * as Orchestrator from "../Orchestrator.ts"; +import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts"; +import { materializeFixtureInput, SIMPLE_PROMPT } from "./fixtures/shared.ts"; +import { + itRunsReplayFixtures, + readReplayFixtureTranscript, + runReplayFixture, +} from "./ProviderReplayFixtures.ts"; +import { layerProviderReplay } from "./ProviderReplayHarness.ts"; +import { materializeReplayTranscriptRuntimeInstructions } from "./ReplayRuntimeInstructions.ts"; + +describe("ACP registry replay fixtures", () => { + it.effect.each([ + { name: "open", firstTurn: false, code: -32602 }, + { name: "switch", firstTurn: true, code: -32602 }, + { name: "authentication", firstTurn: false, code: -32000 }, + ])("preserves the ACP model-write $name failure through orchestration", (testCase) => + Effect.gen(function* () { + const name = `acp_model_write_${testCase.name}`; + const sessionId = "grok-replay-session-1"; + const rejectedModel = "replay-model-b"; + const modelSelection = { + instanceId: ProviderInstanceId.make("acpRegistry"), + model: rejectedModel, + }; + const recorded = yield* readReplayFixtureTranscript( + new URL("./fixtures/simple/registry_transcript.ndjson", import.meta.url), + ); + const sessionIndex = recorded.entries.findIndex( + (entry) => "label" in entry && entry.label === "session.created", + ); + assert.isAtLeast(sessionIndex, 0); + const transcript = { + ...recorded, + scenario: name, + entries: [ + ...recorded.entries.slice(0, sessionIndex), + { + type: "emit_inbound", + label: "session.created", + frame: { + kind: "response", + method: "session/new", + result: { + sessionId, + configOptions: [ + { + id: "model", + name: "Model", + category: "model", + type: "select", + currentValue: "replay-model-a", + options: [ + { value: "replay-model-a", name: "Model A" }, + { value: rejectedModel, name: "Model B" }, + ], + }, + ], + }, + }, + }, + ...(testCase.firstTurn + ? recorded.entries + .slice(sessionIndex + 1) + .filter((entry) => entry.type !== "runtime_exit") + : []), + { + type: "expect_outbound", + label: "session.set_model", + frame: { + kind: "request", + method: "session/set_config_option", + params: { sessionId, configId: "model", value: rejectedModel }, + }, + }, + { + type: "emit_inbound", + label: "session.set_model.rejected", + frame: { + kind: "response", + method: "session/set_config_option", + error: { + code: testCase.code, + message: testCase.code === -32602 ? "Invalid provider" : "Authentication required", + }, + }, + }, + ], + } satisfies ProviderReplayTranscript; + const materialized = yield* materializeFixtureInput({ + scenario: name, + fixtureInput: { + steps: Array.from({ length: testCase.firstTurn ? 2 : 1 }, () => ({ + type: "message" as const, + text: SIMPLE_PROMPT, + })), + }, + driver: ProviderDriverKind.make("acpRegistry"), + modelSelection: { + ...modelSelection, + model: testCase.firstTurn ? "replay-model-a" : rejectedModel, + }, + }); + const scenario = { + ...materialized, + name, + transcript: yield* AcpRegistryOrchestratorReplayHarness.decodeTranscript( + materializeReplayTranscriptRuntimeInstructions(transcript, { + driver: ProviderDriverKind.make("acpRegistry"), + model: "replay-model-a", + }), + ), + runtimePolicyOverride: { cwd: yield* checkpointWorkspace(name) }, + }; + yield* Effect.gen(function* () { + const orchestrator = yield* Orchestrator.OrchestratorV2; + const worker = yield* EffectWorker.OrchestrationEffectWorkerV2; + if (testCase.firstTurn) yield* EffectWorker.runDaemon.pipe(Effect.forkScoped); + for (const [index, command] of materialized.commands.entries()) { + const isFirstTurn = + testCase.firstTurn && command.type === "message.dispatch" && index === 1; + yield* orchestrator.dispatch( + command.type === "message.dispatch" && !isFirstTurn + ? { ...command, modelSelection } + : command, + ); + if (isFirstTurn) { + // Wait for the committed terminal event before switching models. + yield* orchestrator.streamStoredEvents.pipe( + Stream.filter( + ({ event }) => event.type === "run.updated" && event.payload.status === "completed", + ), + Stream.runHead, + ); + } + } + if (testCase.firstTurn) { + yield* orchestrator.streamStoredEvents.pipe( + Stream.filter( + ({ event }) => event.type === "run.updated" && event.payload.status === "failed", + ), + Stream.runHead, + ); + } else { + // Failed opens retry on the outbox schedule. Advance to each recorded + // deadline, draining effects instead of polling the projection. + for (let attempt = 0; attempt < 10; attempt++) { + yield* worker.drain(); + const next = yield* worker.nextClaimableAt; + if (Option.isNone(next)) break; + const now = yield* DateTime.now; + yield* TestClock.adjust( + Math.max(0, DateTime.toEpochMillis(next.value) - DateTime.toEpochMillis(now)), + ); + } + } + const projection = yield* orchestrator.getThreadProjection( + materialized.projectionThreadIds[0]!, + ); + assert.deepEqual( + projection.runs.map((run) => run.status), + testCase.firstTurn ? ["completed", "failed"] : ["failed"], + ); + const errors = projection.turnItems.filter((item) => item.type === "error"); + assert.lengthOf(errors, 1); + assert.equal(errors[0]?.failure.class, "provider_error"); + if (testCase.code === -32602) { + assert.equal( + errors[0]?.failure.message, + 'The agent did not accept the model "replay-model-b". Choose another model for this provider, then retry.', + ); + } else { + assert.match( + errors[0]?.failure.message ?? "", + /^Failed to open acpRegistry provider session \S+\.$/u, + ); + } + }).pipe( + Effect.provide( + layerProviderReplay(scenario, AcpRegistryOrchestratorReplayHarness, { + runEffectWorker: false, + }), + ), + ); + }).pipe( + Effect.scoped, + provideDeterministicTestRuntime, + Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer)), + ), + ); + + itRunsReplayFixtures("acpRegistry", (input) => + runReplayFixture({ ...input, harness: AcpRegistryOrchestratorReplayHarness }), + ); +}); diff --git a/apps/server/src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts b/apps/server/src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts index 94a33411b8b9..8886daad3e01 100644 --- a/apps/server/src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts +++ b/apps/server/src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts @@ -5,6 +5,7 @@ import { isProviderNativeSubagentThread, MessageId, ProviderDriverKind, + type ProviderReplayEntry, } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; @@ -17,7 +18,17 @@ import { subagentInput } from "./fixtures/subagent/input.ts"; import { runOrchestratorV2Scenario } from "./OrchestratorScenario.ts"; import * as ProviderReplayHarness from "./ProviderReplayHarness.ts"; import { materializeReplayTranscriptRuntimeInstructions } from "./ReplayRuntimeInstructions.ts"; -import { CLAUDE_MODEL_SELECTION, materializeFixtureInput } from "./fixtures/shared.ts"; +import { + CLAUDE_MODEL_SELECTION, + materializeFixtureInput, + projectionFor, +} from "./fixtures/shared.ts"; +import { + findReplayFixture, + itRunsReplayFixtures, + runReplayFixture, + type ReplayFixtureCase, +} from "./ProviderReplayFixtures.ts"; import { readProviderReplayTranscript } from "@t3tools/provider-testing/replayTranscript"; const readTranscript = Effect.fn("readClaudeReplayFixture")(function* (file: URL) { @@ -28,7 +39,13 @@ function readClaudeTranscriptFixture(path: string) { return readTranscript(new URL(`./fixtures/${path}/claude_transcript.ndjson`, import.meta.url)); } +function runClaudeReplayFixture(input: ReplayFixtureCase) { + return runReplayFixture({ ...input, harness: ClaudeOrchestratorReplayHarness }); +} + describe("Claude Agent SDK replay fixtures", () => { + itRunsReplayFixtures("claudeAgent", runClaudeReplayFixture); + it.effect("refuses messages to a native subagent thread without touching it", () => Effect.gen(function* () { const raw = yield* readClaudeTranscriptFixture("subagent"); @@ -91,4 +108,212 @@ describe("Claude Agent SDK replay fixtures", () => { ); }), ); + + const afterRootFixture = findReplayFixture("claude_background_subagent_after_root"); + const afterRootProvider = afterRootFixture?.providers[0]; + if (afterRootFixture !== undefined && afterRootProvider !== undefined) { + const OPEN_TOOL_ID = "toolu_01QDa5jV5g1H9h6QyDfeJohD"; + const SUBAGENT_TASK_ID = "a2995bfced8019363"; + + const moveOpenToolBeforeRootResult = ( + move: boolean, + entries: ReadonlyArray, + ): ReadonlyArray => { + if (!move) return entries; + const toolIndex = entries.findIndex( + (entry) => + entry.type === "emit_inbound" && + (entry.frame as Record).message?.content?.[0]?.id === OPEN_TOOL_ID, + ); + const resultIndex = entries.findIndex( + (entry) => entry.type === "emit_inbound" && entry.label === "result", + ); + if (toolIndex < 0 || resultIndex < 0) throw new Error("transcript shape changed"); + const toolEntry = entries[toolIndex]!; + const rest = entries.filter((_, index) => index !== toolIndex); + return [...rest.slice(0, resultIndex), toolEntry, ...rest.slice(resultIndex)]; + }; + // A subagent's foreground Bash run during the root's turn is tailed like a + // root call's, so the child's command row streams live. + const SUBAGENT_BASH_ID = "toolu_01JMN3SSQCHzUTNua9AWdkiH"; + it.effect("streams a subagent's foreground Bash output live during the root turn", () => + runClaudeReplayFixture({ + fixtureName: afterRootFixture.name, + buildInput: afterRootFixture.buildInput, + driver: { + ...afterRootProvider, + assertOutput: (result) => { + const childThreadId = projectionFor(result, afterRootFixture.name).subagents[0] + ?.childThreadId; + assert.exists(childThreadId); + const command = result.projections + .get(childThreadId) + ?.turnItems.find( + (item) => + item.type === "command_execution" && + JSON.stringify(item.input).includes("SUB_DONE_1"), + ); + assert.exists(command); + const streamed = result.commandOutput?.get(command.id); + assert.exists(streamed, "the subagent's command must stream its output"); + assert.include(streamed.output.text, "SUB_DONE_1"); + }, + }, + transformTranscript: (transcript) => { + const isBashFrame = (entry: ProviderReplayEntry) => { + if (entry.type !== "emit_inbound") return false; + const frame = entry.frame as Record; + return ( + frame.message?.content?.[0]?.id === SUBAGENT_BASH_ID || + frame.message?.content?.[0]?.tool_use_id === SUBAGENT_BASH_ID || + (frame.type === "system" && frame.tool_use_id === SUBAGENT_BASH_ID) + ); + }; + // Recorded without the structured result the replay writes output files from. + const bashFrames = transcript.entries.filter(isBashFrame).map((entry) => { + const frame = (entry as { readonly frame: Record }).frame; + return frame.type === "user" + ? { + ...entry, + frame: { ...frame, tool_use_result: { stdout: "SUB_DONE_1\n", stderr: "" } }, + } + : entry; + }); + const rest = transcript.entries.filter((entry) => !isBashFrame(entry)); + const resultIndex = rest.findIndex( + (entry) => entry.type === "emit_inbound" && entry.label === "result", + ); + assert.lengthOf(bashFrames, 4, "transcript shape changed"); + assert.isAtLeast(resultIndex, 0, "transcript shape changed"); + return { + ...transcript, + entries: [...rest.slice(0, resultIndex), ...bashFrames, ...rest.slice(resultIndex)], + }; + }, + }), + ); + + // regression: #16486 a background subagent's tool left open while its parent is idle never settled + it.effect.each([ + ["WebFetch", "while root is idle", "stopped"], + ["Write", "before root settles", "failed"], + ] as const)( + "ends a background subagent's open %s (opened %s) when its notification is %s", + ([tool, opens, notificationStatus]) => + runClaudeReplayFixture({ + fixtureName: afterRootFixture.name, + buildInput: afterRootFixture.buildInput, + driver: { + ...afterRootProvider, + assertOutput: (result) => { + const parent = projectionFor(result, afterRootFixture.name); + assert.deepEqual( + parent.runs.map((run) => run.status), + ["completed", "completed"], + ); + const subagent = parent.subagents[0]; + assert.equal( + subagent?.status, + notificationStatus === "stopped" ? "cancelled" : "failed", + ); + const childThreadId = subagent?.childThreadId; + assert.exists(childThreadId); + const child = result.projections.get(childThreadId); + assert.exists(child); + const fetches = child.turnItems.filter( + (item) => item.type === (tool === "WebFetch" ? "web_search" : "file_change"), + ); + assert.lengthOf(fetches, 1, `the ${tool} reached the child thread`); + assert.deepEqual( + { + openTool: fetches.map((item) => item.status), + openChildNodes: child.nodes + .filter( + (node) => + node.status === "running" || + node.status === "pending" || + node.status === "waiting", + ) + .map((node) => `${node.kind}:${node.status}`), + }, + { + openTool: [notificationStatus === "stopped" ? "interrupted" : "failed"], + openChildNodes: [], + }, + ); + }, + }, + transformTranscript: (transcript) => ({ + ...transcript, + entries: moveOpenToolBeforeRootResult( + opens === "before root settles", + transcript.entries.flatMap((entry): ReadonlyArray => { + if (entry.type !== "emit_inbound") return [entry]; + const frame = entry.frame as Record; + if ( + frame.type === "assistant" && + frame.message?.content?.[0]?.id === OPEN_TOOL_ID + ) { + return [ + { + ...entry, + frame: { + ...frame, + message: { + ...frame.message, + content: [ + { + ...frame.message.content[0], + name: tool, + input: + tool === "WebFetch" + ? { url: "https://example.com", prompt: "Summarize" } + : { file_path: "notes.txt", content: "SUB_DONE_2" }, + }, + ], + }, + }, + }, + ]; + } + if ( + (frame.type === "system" && + (frame.subtype === "task_started" || frame.subtype === "task_notification") && + frame.tool_use_id === OPEN_TOOL_ID) || + (frame.type === "user" && + frame.message?.content?.[0]?.tool_use_id === OPEN_TOOL_ID) || + (frame.type === "assistant" && + frame.parent_tool_use_id !== null && + frame.message?.content?.[0]?.text === "SUB_FINAL_REPORT") + ) { + return []; + } + if (frame.type === "system" && frame.subtype === "task_updated") { + return [ + { + ...entry, + frame: { + ...frame, + patch: { + ...frame.patch, + status: notificationStatus === "stopped" ? "killed" : "failed", + }, + }, + }, + ]; + } + if ( + frame.type === "system" && + frame.subtype === "task_notification" && + frame.task_id === SUBAGENT_TASK_ID + ) { + return [{ ...entry, frame: { ...frame, status: notificationStatus } }]; + } + return [entry]; + }), + ), + }), + }), + ); + } }); diff --git a/apps/server/src/orchestration-v2/testkit/CodexReplayFixtures.integration.test.ts b/apps/server/src/orchestration-v2/testkit/CodexReplayFixtures.integration.test.ts new file mode 100644 index 000000000000..c0593351f20d --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/CodexReplayFixtures.integration.test.ts @@ -0,0 +1,17 @@ +import { describe } from "@effect/vitest"; +import * as Effect from "effect/Effect"; + +import { materializeReplayTranscriptWorkspace } from "@t3tools/provider-testing/replayTranscript"; +import { CodexOrchestratorReplayHarness } from "../Adapters/CodexAdapterV2.testkit.ts"; +import { itRunsReplayFixtures, runReplayFixture } from "./ProviderReplayFixtures.ts"; + +describe("Codex replay fixtures", () => { + itRunsReplayFixtures("codex", (input) => + runReplayFixture({ + ...input, + harness: CodexOrchestratorReplayHarness, + materializeWorkspace: (transcript, workspace) => + Effect.succeed(materializeReplayTranscriptWorkspace(transcript, workspace)), + }), + ); +}); diff --git a/apps/server/src/orchestration-v2/testkit/CursorReplayFixtures.integration.test.ts b/apps/server/src/orchestration-v2/testkit/CursorReplayFixtures.integration.test.ts new file mode 100644 index 000000000000..7557e8b3929f --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/CursorReplayFixtures.integration.test.ts @@ -0,0 +1,29 @@ +import { describe, it } from "@effect/vitest"; + +import { CursorOrchestratorReplayHarness } from "../Adapters/CursorAdapterV2.testkit.ts"; +import { messageRestartInput } from "./fixtures/message_steering/input.ts"; +import { + findReplayFixture, + itRunsReplayFixtures, + runReplayFixture, +} from "./ProviderReplayFixtures.ts"; + +describe("Cursor replay fixtures", () => { + itRunsReplayFixtures("cursor", (input) => + runReplayFixture({ ...input, harness: CursorOrchestratorReplayHarness }), + ); + + const cursorSteeringProvider = findReplayFixture("message_steering")?.providers.find( + (provider) => provider.driver === "cursor", + ); + if (cursorSteeringProvider !== undefined) { + it.effect("executes explicit Cursor restart_active through the recorded SDK boundary", () => + runReplayFixture({ + fixtureName: "message_steering", + buildInput: messageRestartInput, + driver: cursorSteeringProvider, + harness: CursorOrchestratorReplayHarness, + }), + ); + } +}); diff --git a/apps/server/src/orchestration-v2/testkit/GrokReplayFixtures.integration.test.ts b/apps/server/src/orchestration-v2/testkit/GrokReplayFixtures.integration.test.ts new file mode 100644 index 000000000000..606539ea7f46 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/GrokReplayFixtures.integration.test.ts @@ -0,0 +1,10 @@ +import { describe } from "@effect/vitest"; + +import { GrokOrchestratorReplayHarness } from "../Adapters/GrokAdapterV2.testkit.ts"; +import { itRunsReplayFixtures, runReplayFixture } from "./ProviderReplayFixtures.ts"; + +describe("Grok replay fixtures", () => { + itRunsReplayFixtures("grok", (input) => + runReplayFixture({ ...input, harness: GrokOrchestratorReplayHarness }), + ); +}); diff --git a/apps/server/src/orchestration-v2/testkit/MuseReplayFixtures.integration.test.ts b/apps/server/src/orchestration-v2/testkit/MuseReplayFixtures.integration.test.ts new file mode 100644 index 000000000000..fee15966df4e --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/MuseReplayFixtures.integration.test.ts @@ -0,0 +1,17 @@ +import { describe } from "@effect/vitest"; + +import { + materializeMuseReplayWorkspace, + MuseOrchestratorReplayHarness, +} from "../Adapters/MuseAdapterV2.testkit.ts"; +import { itRunsReplayFixtures, runReplayFixture } from "./ProviderReplayFixtures.ts"; + +describe("Muse replay fixtures", () => { + itRunsReplayFixtures("muse", (input) => + runReplayFixture({ + ...input, + harness: MuseOrchestratorReplayHarness, + materializeWorkspace: materializeMuseReplayWorkspace, + }), + ); +}); diff --git a/apps/server/src/orchestration-v2/testkit/OpenCodeReplayFixtures.integration.test.ts b/apps/server/src/orchestration-v2/testkit/OpenCodeReplayFixtures.integration.test.ts new file mode 100644 index 000000000000..f9a594042b71 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/OpenCodeReplayFixtures.integration.test.ts @@ -0,0 +1,68 @@ +import { describe, it } from "@effect/vitest"; +import type { ProviderReplayEntry } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; + +import { OpenCodeOrchestratorReplayHarness } from "../Adapters/OpenCodeAdapterV2.testkit.ts"; +import { + OPENCODE2_HTTP_PROTOCOL, + OpenCode2OrchestratorReplayHarness, +} from "../Adapters/OpenCode2AdapterV2.testkit.ts"; +import { + findReplayFixture, + itRunsReplayFixtures, + readReplayFixtureTranscript, + runReplayFixture, + type ReplayFixtureCase, +} from "./ProviderReplayFixtures.ts"; + +// OpenCode fixtures are recorded against either the v1 or the v2 HTTP protocol. +function runOpenCodeReplayFixture(input: ReplayFixtureCase) { + return readReplayFixtureTranscript(input.driver.transcriptFile).pipe( + Effect.flatMap((transcript) => + transcript.protocol === OPENCODE2_HTTP_PROTOCOL + ? runReplayFixture({ ...input, harness: OpenCode2OrchestratorReplayHarness }) + : runReplayFixture({ ...input, harness: OpenCodeOrchestratorReplayHarness }), + ), + ); +} + +describe("OpenCode replay fixtures", () => { + itRunsReplayFixtures("opencode", runOpenCodeReplayFixture); + + it.effect.each( + ( + [ + ["opencode2_subagent", "session.execution.started.2"], + ["opencode2_background", "session.execution.started.3"], + ] as const + ).flatMap(([fixtureName, label]) => { + const fixture = findReplayFixture(fixtureName); + const provider = fixture?.providers[0]; + return fixture === undefined || provider === undefined + ? [] + : [[fixtureName, label, fixture, provider] as const]; + }), + )( + "runs %s when %s is an execution start this build cannot decode", + ([fixtureName, label, fixture, provider]) => + runOpenCodeReplayFixture({ + fixtureName, + buildInput: fixture.buildInput, + driver: provider, + transformTranscript: (transcript) => ({ + ...transcript, + entries: transcript.entries.map((entry) => + entry.type === "emit_inbound" && entry.label === label + ? undecodableEvent(entry) + : entry, + ), + }), + }), + ); +}); + +function undecodableEvent(entry: ProviderReplayEntry): ProviderReplayEntry { + if (entry.type !== "emit_inbound") return entry; + const frame = entry.frame as { readonly event: Record }; + return { ...entry, frame: { ...frame, event: { ...frame.event, durable: "not-an-envelope" } } }; +} diff --git a/apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts b/apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts deleted file mode 100644 index 3a3fe85ffc4e..000000000000 --- a/apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts +++ /dev/null @@ -1,704 +0,0 @@ -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { assert, describe, it } from "@effect/vitest"; -import { - ProviderDriverKind, - ProviderInstanceId, - type OrchestrationV2DomainEvent, - type ProviderReplayEntry, - type ProviderReplayTranscript, -} from "@t3tools/contracts"; -import * as DateTime from "effect/DateTime"; -import * as Effect from "effect/Effect"; -import * as FileSystem from "effect/FileSystem"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import * as Path from "effect/Path"; -import * as Stream from "effect/Stream"; -import { TestClock } from "effect/testing"; - -import * as EffectWorker from "../EffectWorker.ts"; -import * as Orchestrator from "../Orchestrator.ts"; - -import { ClaudeOrchestratorReplayHarness } from "../Adapters/ClaudeAdapterV2.testkit.ts"; -import { CodexOrchestratorReplayHarness } from "../Adapters/CodexAdapterV2.testkit.ts"; -import { CursorOrchestratorReplayHarness } from "../Adapters/CursorAdapterV2.testkit.ts"; -import { AcpRegistryOrchestratorReplayHarness } from "../Adapters/AcpRegistryAdapterV2.testkit.ts"; -import { GrokOrchestratorReplayHarness } from "../Adapters/GrokAdapterV2.testkit.ts"; -import { OpenCodeOrchestratorReplayHarness } from "../Adapters/OpenCodeAdapterV2.testkit.ts"; -import { - OPENCODE2_HTTP_PROTOCOL, - OpenCode2OrchestratorReplayHarness, -} from "../Adapters/OpenCode2AdapterV2.testkit.ts"; -import { - materializeMuseReplayWorkspace, - MuseOrchestratorReplayHarness, -} from "../Adapters/MuseAdapterV2.testkit.ts"; -import { PiOrchestratorReplayHarness } from "../Adapters/PiAdapterV2.testkit.ts"; -import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; -import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts"; -import { ORCHESTRATOR_REPLAY_FIXTURES } from "./fixtures/index.ts"; -import { messageRestartInput } from "./fixtures/message_steering/input.ts"; -import { - assertProviderNativeSubagentRootTurns, - materializeFixtureInput, - SIMPLE_PROMPT, - projectionFor, - type OrchestratorFixtureInput, - type ProviderOrchestratorReplayVariant, -} from "./fixtures/shared.ts"; -import { - runOrchestratorV2ProviderReplayScenario, - layerProviderReplay, - type OrchestratorV2ProviderReplayHarness, -} from "./ProviderReplayHarness.ts"; -import { checkpointWorkspace } from "@t3tools/provider-testing/replayWorkspace"; -import { materializeReplayTranscriptRuntimeInstructions } from "./ReplayRuntimeInstructions.ts"; -import { - materializeReplayTranscriptWorkspace, - readProviderReplayTranscript, -} from "@t3tools/provider-testing/replayTranscript"; - -const readTranscript = Effect.fn("readOrchestratorReplayTranscript")(function* (file: URL) { - return yield* readProviderReplayTranscript(file); -}, Effect.provide(NodeServices.layer)); - -function normalizeTestError(cause: unknown): Error { - return cause instanceof Error ? cause : new Error(String(cause)); -} - -function transcriptEntriesThroughLabel( - transcript: ProviderReplayTranscript, - label: string | undefined, -): ProviderReplayTranscript { - if (label === undefined) { - return transcript; - } - const entryIndex = transcript.entries.findIndex( - (entry) => entry.type === "emit_inbound" && entry.label === label, - ); - if (entryIndex === -1) { - throw new Error(`${transcript.scenario} is missing inbound transcript label ${label}.`); - } - return { - ...transcript, - entries: transcript.entries.slice(0, entryIndex + 1), - }; -} - -function isStreamingAssistantEvent(event: OrchestrationV2DomainEvent): boolean { - switch (event.type) { - case "node.updated": - return event.payload.kind === "assistant_message" && event.payload.status === "running"; - case "message.updated": - return event.payload.role === "assistant" && event.payload.streaming; - case "turn-item.updated": - return event.payload.type === "assistant_message" && event.payload.streaming; - default: - return false; - } -} - -const runFixtureProvider = Effect.fn("runOrchestratorReplayFixture")(function* < - Transcript extends ProviderReplayTranscript, - Error, ->(input: { - readonly fixtureName: string; - readonly buildInput: () => OrchestratorFixtureInput; - readonly driver: ProviderOrchestratorReplayVariant; - readonly harness: OrchestratorV2ProviderReplayHarness; - readonly transformTranscript?: (transcript: ProviderReplayTranscript) => ProviderReplayTranscript; -}) { - const recordedTranscript = yield* readTranscript(input.driver.transcriptFile); - const rawTranscript = input.transformTranscript?.(recordedTranscript) ?? recordedTranscript; - const replayTranscript = materializeReplayTranscriptRuntimeInstructions( - transcriptEntriesThroughLabel(rawTranscript, input.driver.transcriptEntriesThroughLabel), - { driver: input.driver.driver, model: input.driver.modelSelection.model }, - ); - const fixtureInput = input.buildInput(); - const workspace = yield* checkpointWorkspace(input.fixtureName, fixtureInput.workspaceFiles); - const transcript = yield* input.harness.decodeTranscript( - input.driver.driver === "codex" - ? materializeReplayTranscriptWorkspace(replayTranscript, workspace) - : input.driver.driver === "muse" - ? yield* materializeMuseReplayWorkspace(replayTranscript, workspace) - : replayTranscript, - ); - const materialized = yield* materializeFixtureInput({ - scenario: input.fixtureName, - fixtureInput, - driver: input.driver.driver, - modelSelection: input.driver.modelSelection, - }).pipe(Effect.provide(IdAllocator.layer), provideDeterministicTestRuntime); - const scenario = { - name: `${input.fixtureName}/${input.driver.driver}`, - transcript, - commands: materialized.commands, - steps: materialized.steps, - projectionThreadIds: materialized.projectionThreadIds, - runtimePolicyOverride: { - ...input.driver.runtimePolicyOverride, - cwd: workspace, - }, - }; - - const result = yield* runOrchestratorV2ProviderReplayScenario( - scenario, - input.harness, - input.driver.runContinuationWorker === true ? { runContinuationWorker: true } : {}, - ).pipe(provideDeterministicTestRuntime); - input.driver.assertOutput(result, transcript); - assertProviderNativeSubagentRootTurns(result); - const expectedAbsentWorkspacePaths = input.driver.expectedAbsentWorkspacePaths; - if (expectedAbsentWorkspacePaths !== undefined) { - yield* Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - for (const relativePath of expectedAbsentWorkspacePaths) { - assert.isFalse( - yield* fs.exists(path.join(workspace, relativePath)), - `${input.fixtureName}/${input.driver.driver} must not create ${relativePath} in the replay workspace`, - ); - } - }).pipe(Effect.provide(NodeServices.layer)); - } - assert.isFalse( - result.domainEvents.some(isStreamingAssistantEvent), - "buffered delivery must not persist streaming assistant artifacts", - ); - const projectionThreadId = materialized.projectionThreadIds[0]; - assert.isDefined(projectionThreadId); - const projection = result.projections.get(projectionThreadId); - assert.isDefined(projection); - const latestRun = projection.runs.at(-1); - assert.deepEqual(latestRun?.modelSelection, input.driver.modelSelection); - if (projection.runs.some((run) => run.status === "completed")) { - const threadStartCheckpoint = projection.checkpoints.find( - (checkpoint) => checkpoint.ordinalWithinScope === 0 && checkpoint.appRunOrdinal === null, - ); - assert.isDefined( - threadStartCheckpoint, - "completed threads must retain an addressable thread-start checkpoint", - ); - assert.equal(threadStartCheckpoint.status, "ready"); - } - return result; -}); - -function runFixtureProviderWithRegisteredHarness(input: { - readonly fixtureName: string; - readonly buildInput: () => OrchestratorFixtureInput; - readonly driver: ProviderOrchestratorReplayVariant; - readonly transformTranscript?: (transcript: ProviderReplayTranscript) => ProviderReplayTranscript; -}) { - switch (input.driver.driver) { - case "codex": - return runFixtureProvider({ - ...input, - harness: CodexOrchestratorReplayHarness, - }).pipe(Effect.mapError(normalizeTestError), Effect.scoped); - case "claudeAgent": - return runFixtureProvider({ - ...input, - harness: ClaudeOrchestratorReplayHarness, - }).pipe(Effect.mapError(normalizeTestError), Effect.scoped); - case "cursor": - return runFixtureProvider({ - ...input, - harness: CursorOrchestratorReplayHarness, - }).pipe(Effect.mapError(normalizeTestError), Effect.scoped); - case "grok": - return runFixtureProvider({ - ...input, - harness: GrokOrchestratorReplayHarness, - }).pipe(Effect.mapError(normalizeTestError), Effect.scoped); - case "acpRegistry": - return runFixtureProvider({ - ...input, - harness: AcpRegistryOrchestratorReplayHarness, - }).pipe(Effect.mapError(normalizeTestError), Effect.scoped); - case "opencode": - return readTranscript(input.driver.transcriptFile).pipe( - Effect.flatMap((transcript) => - transcript.protocol === OPENCODE2_HTTP_PROTOCOL - ? runFixtureProvider({ ...input, harness: OpenCode2OrchestratorReplayHarness }) - : runFixtureProvider({ ...input, harness: OpenCodeOrchestratorReplayHarness }), - ), - Effect.mapError(normalizeTestError), - Effect.scoped, - ); - case "pi": - return runFixtureProvider({ - ...input, - harness: PiOrchestratorReplayHarness, - }).pipe(Effect.mapError(normalizeTestError), Effect.scoped); - case "muse": - return runFixtureProvider({ - ...input, - harness: MuseOrchestratorReplayHarness, - }).pipe(Effect.mapError(normalizeTestError), Effect.scoped); - default: - return Effect.die( - new Error(`No replay harness registered for provider ${input.driver.driver}.`), - ); - } -} - -describe("orchestrator replay fixtures", () => { - it.effect.each([ - { name: "open", firstTurn: false, code: -32602 }, - { name: "switch", firstTurn: true, code: -32602 }, - { name: "authentication", firstTurn: false, code: -32000 }, - ])("preserves the ACP model-write $name failure through orchestration", (testCase) => - Effect.gen(function* () { - const name = `acp_model_write_${testCase.name}`; - const sessionId = "grok-replay-session-1"; - const rejectedModel = "replay-model-b"; - const modelSelection = { - instanceId: ProviderInstanceId.make("acpRegistry"), - model: rejectedModel, - }; - const recorded = yield* readTranscript( - new URL("./fixtures/simple/registry_transcript.ndjson", import.meta.url), - ); - const sessionIndex = recorded.entries.findIndex( - (entry) => "label" in entry && entry.label === "session.created", - ); - assert.isAtLeast(sessionIndex, 0); - const transcript = { - ...recorded, - scenario: name, - entries: [ - ...recorded.entries.slice(0, sessionIndex), - { - type: "emit_inbound", - label: "session.created", - frame: { - kind: "response", - method: "session/new", - result: { - sessionId, - configOptions: [ - { - id: "model", - name: "Model", - category: "model", - type: "select", - currentValue: "replay-model-a", - options: [ - { value: "replay-model-a", name: "Model A" }, - { value: rejectedModel, name: "Model B" }, - ], - }, - ], - }, - }, - }, - ...(testCase.firstTurn - ? recorded.entries - .slice(sessionIndex + 1) - .filter((entry) => entry.type !== "runtime_exit") - : []), - { - type: "expect_outbound", - label: "session.set_model", - frame: { - kind: "request", - method: "session/set_config_option", - params: { sessionId, configId: "model", value: rejectedModel }, - }, - }, - { - type: "emit_inbound", - label: "session.set_model.rejected", - frame: { - kind: "response", - method: "session/set_config_option", - error: { - code: testCase.code, - message: testCase.code === -32602 ? "Invalid provider" : "Authentication required", - }, - }, - }, - ], - } satisfies ProviderReplayTranscript; - const materialized = yield* materializeFixtureInput({ - scenario: name, - fixtureInput: { - steps: Array.from({ length: testCase.firstTurn ? 2 : 1 }, () => ({ - type: "message" as const, - text: SIMPLE_PROMPT, - })), - }, - driver: ProviderDriverKind.make("acpRegistry"), - modelSelection: { - ...modelSelection, - model: testCase.firstTurn ? "replay-model-a" : rejectedModel, - }, - }); - const scenario = { - ...materialized, - name, - transcript: yield* AcpRegistryOrchestratorReplayHarness.decodeTranscript( - materializeReplayTranscriptRuntimeInstructions(transcript, { - driver: ProviderDriverKind.make("acpRegistry"), - model: "replay-model-a", - }), - ), - runtimePolicyOverride: { cwd: yield* checkpointWorkspace(name) }, - }; - yield* Effect.gen(function* () { - const orchestrator = yield* Orchestrator.OrchestratorV2; - const worker = yield* EffectWorker.OrchestrationEffectWorkerV2; - if (testCase.firstTurn) yield* EffectWorker.runDaemon.pipe(Effect.forkScoped); - for (const [index, command] of materialized.commands.entries()) { - const isFirstTurn = - testCase.firstTurn && command.type === "message.dispatch" && index === 1; - yield* orchestrator.dispatch( - command.type === "message.dispatch" && !isFirstTurn - ? { ...command, modelSelection } - : command, - ); - if (isFirstTurn) { - // Wait for the committed terminal event before switching models. - yield* orchestrator.streamStoredEvents.pipe( - Stream.filter( - ({ event }) => event.type === "run.updated" && event.payload.status === "completed", - ), - Stream.runHead, - ); - } - } - if (testCase.firstTurn) { - yield* orchestrator.streamStoredEvents.pipe( - Stream.filter( - ({ event }) => event.type === "run.updated" && event.payload.status === "failed", - ), - Stream.runHead, - ); - } else { - // Failed opens retry on the outbox schedule. Advance to each recorded - // deadline, draining effects instead of polling the projection. - for (let attempt = 0; attempt < 10; attempt++) { - yield* worker.drain(); - const next = yield* worker.nextClaimableAt; - if (Option.isNone(next)) break; - const now = yield* DateTime.now; - yield* TestClock.adjust( - Math.max(0, DateTime.toEpochMillis(next.value) - DateTime.toEpochMillis(now)), - ); - } - } - const projection = yield* orchestrator.getThreadProjection( - materialized.projectionThreadIds[0]!, - ); - assert.deepEqual( - projection.runs.map((run) => run.status), - testCase.firstTurn ? ["completed", "failed"] : ["failed"], - ); - const errors = projection.turnItems.filter((item) => item.type === "error"); - assert.lengthOf(errors, 1); - assert.equal(errors[0]?.failure.class, "provider_error"); - if (testCase.code === -32602) { - assert.equal( - errors[0]?.failure.message, - 'The agent did not accept the model "replay-model-b". Choose another model for this provider, then retry.', - ); - } else { - assert.match( - errors[0]?.failure.message ?? "", - /^Failed to open acpRegistry provider session \S+\.$/u, - ); - } - }).pipe( - Effect.provide( - layerProviderReplay(scenario, AcpRegistryOrchestratorReplayHarness, { - runEffectWorker: false, - }), - ), - ); - }).pipe( - Effect.scoped, - provideDeterministicTestRuntime, - Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer)), - ), - ); - - it.effect.each( - ORCHESTRATOR_REPLAY_FIXTURES.flatMap((fixture) => - fixture.providers.map( - (provider) => [fixture.name, provider.driver, fixture, provider] as const, - ), - ), - )("runs %s/%s through OrchestratorV2 using deterministic replay", ([, , fixture, provider]) => - runFixtureProviderWithRegisteredHarness({ - fixtureName: fixture.name, - buildInput: fixture.buildInput, - driver: provider, - }), - ); - - const steeringFixture = ORCHESTRATOR_REPLAY_FIXTURES.find( - (fixture) => fixture.name === "message_steering", - ); - const cursorSteeringProvider = steeringFixture?.providers.find( - (provider) => provider.driver === "cursor", - ); - if (cursorSteeringProvider !== undefined) { - it.effect("executes explicit Cursor restart_active through the recorded SDK boundary", () => - runFixtureProviderWithRegisteredHarness({ - fixtureName: "message_steering", - buildInput: messageRestartInput, - driver: cursorSteeringProvider, - }), - ); - } - - it.effect.each( - ( - [ - ["opencode2_subagent", "session.execution.started.2"], - ["opencode2_background", "session.execution.started.3"], - ] as const - ).flatMap(([fixtureName, label]) => { - const fixture = ORCHESTRATOR_REPLAY_FIXTURES.find( - (candidate) => candidate.name === fixtureName, - ); - const provider = fixture?.providers[0]; - return fixture === undefined || provider === undefined - ? [] - : [[fixtureName, label, fixture, provider] as const]; - }), - )( - "runs %s when %s is an execution start this build cannot decode", - ([fixtureName, label, fixture, provider]) => - runFixtureProviderWithRegisteredHarness({ - fixtureName, - buildInput: fixture.buildInput, - driver: provider, - transformTranscript: (transcript) => ({ - ...transcript, - entries: transcript.entries.map((entry) => - entry.type === "emit_inbound" && entry.label === label - ? undecodableEvent(entry) - : entry, - ), - }), - }), - ); - - const afterRootFixture = ORCHESTRATOR_REPLAY_FIXTURES.find( - (candidate) => candidate.name === "claude_background_subagent_after_root", - ); - const afterRootProvider = afterRootFixture?.providers[0]; - if (afterRootFixture !== undefined && afterRootProvider !== undefined) { - const OPEN_TOOL_ID = "toolu_01QDa5jV5g1H9h6QyDfeJohD"; - const SUBAGENT_TASK_ID = "a2995bfced8019363"; - - const moveOpenToolBeforeRootResult = ( - move: boolean, - entries: ReadonlyArray, - ): ReadonlyArray => { - if (!move) return entries; - const toolIndex = entries.findIndex( - (entry) => - entry.type === "emit_inbound" && - (entry.frame as Record).message?.content?.[0]?.id === OPEN_TOOL_ID, - ); - const resultIndex = entries.findIndex( - (entry) => entry.type === "emit_inbound" && entry.label === "result", - ); - if (toolIndex < 0 || resultIndex < 0) throw new Error("transcript shape changed"); - const toolEntry = entries[toolIndex]!; - const rest = entries.filter((_, index) => index !== toolIndex); - return [...rest.slice(0, resultIndex), toolEntry, ...rest.slice(resultIndex)]; - }; - // A subagent's foreground Bash run during the root's turn is tailed like a - // root call's, so the child's command row streams live. - const SUBAGENT_BASH_ID = "toolu_01JMN3SSQCHzUTNua9AWdkiH"; - it.effect("streams a subagent's foreground Bash output live during the root turn", () => - runFixtureProviderWithRegisteredHarness({ - fixtureName: afterRootFixture.name, - buildInput: afterRootFixture.buildInput, - driver: { - ...afterRootProvider, - assertOutput: (result) => { - const childThreadId = projectionFor(result, afterRootFixture.name).subagents[0] - ?.childThreadId; - assert.exists(childThreadId); - const command = result.projections - .get(childThreadId) - ?.turnItems.find( - (item) => - item.type === "command_execution" && - JSON.stringify(item.input).includes("SUB_DONE_1"), - ); - assert.exists(command); - const streamed = result.commandOutput?.get(command.id); - assert.exists(streamed, "the subagent's command must stream its output"); - assert.include(streamed.output.text, "SUB_DONE_1"); - }, - }, - transformTranscript: (transcript) => { - const isBashFrame = (entry: ProviderReplayEntry) => { - if (entry.type !== "emit_inbound") return false; - const frame = entry.frame as Record; - return ( - frame.message?.content?.[0]?.id === SUBAGENT_BASH_ID || - frame.message?.content?.[0]?.tool_use_id === SUBAGENT_BASH_ID || - (frame.type === "system" && frame.tool_use_id === SUBAGENT_BASH_ID) - ); - }; - // Recorded without the structured result the replay writes output files from. - const bashFrames = transcript.entries.filter(isBashFrame).map((entry) => { - const frame = (entry as { readonly frame: Record }).frame; - return frame.type === "user" - ? { - ...entry, - frame: { ...frame, tool_use_result: { stdout: "SUB_DONE_1\n", stderr: "" } }, - } - : entry; - }); - const rest = transcript.entries.filter((entry) => !isBashFrame(entry)); - const resultIndex = rest.findIndex( - (entry) => entry.type === "emit_inbound" && entry.label === "result", - ); - assert.lengthOf(bashFrames, 4, "transcript shape changed"); - assert.isAtLeast(resultIndex, 0, "transcript shape changed"); - return { - ...transcript, - entries: [...rest.slice(0, resultIndex), ...bashFrames, ...rest.slice(resultIndex)], - }; - }, - }), - ); - - // regression: #16486 a background subagent's tool left open while its parent is idle never settled - it.effect.each([ - ["WebFetch", "while root is idle", "stopped"], - ["Write", "before root settles", "failed"], - ] as const)( - "ends a background subagent's open %s (opened %s) when its notification is %s", - ([tool, opens, notificationStatus]) => - runFixtureProviderWithRegisteredHarness({ - fixtureName: afterRootFixture.name, - buildInput: afterRootFixture.buildInput, - driver: { - ...afterRootProvider, - assertOutput: (result) => { - const parent = projectionFor(result, afterRootFixture.name); - assert.deepEqual( - parent.runs.map((run) => run.status), - ["completed", "completed"], - ); - const subagent = parent.subagents[0]; - assert.equal( - subagent?.status, - notificationStatus === "stopped" ? "cancelled" : "failed", - ); - const childThreadId = subagent?.childThreadId; - assert.exists(childThreadId); - const child = result.projections.get(childThreadId); - assert.exists(child); - const fetches = child.turnItems.filter( - (item) => item.type === (tool === "WebFetch" ? "web_search" : "file_change"), - ); - assert.lengthOf(fetches, 1, `the ${tool} reached the child thread`); - assert.deepEqual( - { - openTool: fetches.map((item) => item.status), - openChildNodes: child.nodes - .filter( - (node) => - node.status === "running" || - node.status === "pending" || - node.status === "waiting", - ) - .map((node) => `${node.kind}:${node.status}`), - }, - { - openTool: [notificationStatus === "stopped" ? "interrupted" : "failed"], - openChildNodes: [], - }, - ); - }, - }, - transformTranscript: (transcript) => ({ - ...transcript, - entries: moveOpenToolBeforeRootResult( - opens === "before root settles", - transcript.entries.flatMap((entry): ReadonlyArray => { - if (entry.type !== "emit_inbound") return [entry]; - const frame = entry.frame as Record; - if ( - frame.type === "assistant" && - frame.message?.content?.[0]?.id === OPEN_TOOL_ID - ) { - return [ - { - ...entry, - frame: { - ...frame, - message: { - ...frame.message, - content: [ - { - ...frame.message.content[0], - name: tool, - input: - tool === "WebFetch" - ? { url: "https://example.com", prompt: "Summarize" } - : { file_path: "notes.txt", content: "SUB_DONE_2" }, - }, - ], - }, - }, - }, - ]; - } - if ( - (frame.type === "system" && - (frame.subtype === "task_started" || frame.subtype === "task_notification") && - frame.tool_use_id === OPEN_TOOL_ID) || - (frame.type === "user" && - frame.message?.content?.[0]?.tool_use_id === OPEN_TOOL_ID) || - (frame.type === "assistant" && - frame.parent_tool_use_id !== null && - frame.message?.content?.[0]?.text === "SUB_FINAL_REPORT") - ) { - return []; - } - if (frame.type === "system" && frame.subtype === "task_updated") { - return [ - { - ...entry, - frame: { - ...frame, - patch: { - ...frame.patch, - status: notificationStatus === "stopped" ? "killed" : "failed", - }, - }, - }, - ]; - } - if ( - frame.type === "system" && - frame.subtype === "task_notification" && - frame.task_id === SUBAGENT_TASK_ID - ) { - return [{ ...entry, frame: { ...frame, status: notificationStatus } }]; - } - return [entry]; - }), - ), - }), - }), - ); - } -}); - -function undecodableEvent(entry: ProviderReplayEntry): ProviderReplayEntry { - if (entry.type !== "emit_inbound") return entry; - const frame = entry.frame as { readonly event: Record }; - return { ...entry, frame: { ...frame, event: { ...frame.event, durable: "not-an-envelope" } } }; -} diff --git a/apps/server/src/orchestration-v2/testkit/PiReplayFixtures.integration.test.ts b/apps/server/src/orchestration-v2/testkit/PiReplayFixtures.integration.test.ts new file mode 100644 index 000000000000..b9db455d1cff --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/PiReplayFixtures.integration.test.ts @@ -0,0 +1,10 @@ +import { describe } from "@effect/vitest"; + +import { PiOrchestratorReplayHarness } from "../Adapters/PiAdapterV2.testkit.ts"; +import { itRunsReplayFixtures, runReplayFixture } from "./ProviderReplayFixtures.ts"; + +describe("Pi replay fixtures", () => { + itRunsReplayFixtures("pi", (input) => + runReplayFixture({ ...input, harness: PiOrchestratorReplayHarness }), + ); +}); diff --git a/apps/server/src/orchestration-v2/testkit/ProviderReplayFixtures.ts b/apps/server/src/orchestration-v2/testkit/ProviderReplayFixtures.ts new file mode 100644 index 000000000000..8c23e9c90181 --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/ProviderReplayFixtures.ts @@ -0,0 +1,219 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, it } from "@effect/vitest"; +import type { OrchestrationV2DomainEvent, ProviderReplayTranscript } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; + +import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; +import { checkpointWorkspace } from "@t3tools/provider-testing/replayWorkspace"; +import { readProviderReplayTranscript } from "@t3tools/provider-testing/replayTranscript"; +import { provideDeterministicTestRuntime } from "./DeterministicRuntime.ts"; +import { ORCHESTRATOR_REPLAY_FIXTURES } from "./fixtures/index.ts"; +import { + assertProviderNativeSubagentRootTurns, + materializeFixtureInput, + type OrchestratorFixtureInput, + type ProviderOrchestratorReplayVariant, +} from "./fixtures/shared.ts"; +import { + runOrchestratorV2ProviderReplayScenario, + type OrchestratorV2ProviderReplayHarness, +} from "./ProviderReplayHarness.ts"; +import { materializeReplayTranscriptRuntimeInstructions } from "./ReplayRuntimeInstructions.ts"; + +// Each driver's fixtures run from its own ReplayFixtures.integration.test.ts, +// so a focused run loads one adapter. A fixture for a driver missing here would +// silently never run, so loading this module fails instead. +const DRIVERS_WITH_REPLAY_FIXTURE_FILES = new Set([ + "acpRegistry", + "claudeAgent", + "codex", + "cursor", + "grok", + "muse", + "opencode", + "pi", +]); +for (const fixture of ORCHESTRATOR_REPLAY_FIXTURES) { + for (const provider of fixture.providers) { + if (!DRIVERS_WITH_REPLAY_FIXTURE_FILES.has(provider.driver)) { + throw new Error( + `${fixture.name}/${provider.driver} has no ReplayFixtures test file to run it.`, + ); + } + } +} + +export const readReplayFixtureTranscript = Effect.fn("readOrchestratorReplayTranscript")(function* ( + file: URL, +) { + return yield* readProviderReplayTranscript(file); +}, Effect.provide(NodeServices.layer)); + +function normalizeTestError(cause: unknown): Error { + return cause instanceof Error ? cause : new Error(String(cause)); +} + +function transcriptEntriesThroughLabel( + transcript: ProviderReplayTranscript, + label: string | undefined, +): ProviderReplayTranscript { + if (label === undefined) { + return transcript; + } + const entryIndex = transcript.entries.findIndex( + (entry) => entry.type === "emit_inbound" && entry.label === label, + ); + if (entryIndex === -1) { + throw new Error(`${transcript.scenario} is missing inbound transcript label ${label}.`); + } + return { + ...transcript, + entries: transcript.entries.slice(0, entryIndex + 1), + }; +} + +function isStreamingAssistantEvent(event: OrchestrationV2DomainEvent): boolean { + switch (event.type) { + case "node.updated": + return event.payload.kind === "assistant_message" && event.payload.status === "running"; + case "message.updated": + return event.payload.role === "assistant" && event.payload.streaming; + case "turn-item.updated": + return event.payload.type === "assistant_message" && event.payload.streaming; + default: + return false; + } +} + +export interface ReplayFixtureCase { + readonly fixtureName: string; + readonly buildInput: () => OrchestratorFixtureInput; + readonly driver: ProviderOrchestratorReplayVariant; + readonly transformTranscript?: (transcript: ProviderReplayTranscript) => ProviderReplayTranscript; +} + +export interface ReplayFixtureRun< + Transcript extends ProviderReplayTranscript, + Error, + WorkspaceError = never, +> extends ReplayFixtureCase { + readonly harness: OrchestratorV2ProviderReplayHarness; + /** Rewrites recorded workspace paths to the replay workspace, for drivers that record them. */ + readonly materializeWorkspace?: ( + transcript: ProviderReplayTranscript, + workspace: string, + ) => Effect.Effect; +} + +const runFixtureProvider = Effect.fn("runOrchestratorReplayFixture")(function* < + Transcript extends ProviderReplayTranscript, + Error, + WorkspaceError, +>(input: ReplayFixtureRun) { + const recordedTranscript = yield* readReplayFixtureTranscript(input.driver.transcriptFile); + const rawTranscript = input.transformTranscript?.(recordedTranscript) ?? recordedTranscript; + const replayTranscript = materializeReplayTranscriptRuntimeInstructions( + transcriptEntriesThroughLabel(rawTranscript, input.driver.transcriptEntriesThroughLabel), + { driver: input.driver.driver, model: input.driver.modelSelection.model }, + ); + const fixtureInput = input.buildInput(); + const workspace = yield* checkpointWorkspace(input.fixtureName, fixtureInput.workspaceFiles); + const transcript = yield* input.harness.decodeTranscript( + input.materializeWorkspace === undefined + ? replayTranscript + : yield* input.materializeWorkspace(replayTranscript, workspace), + ); + const materialized = yield* materializeFixtureInput({ + scenario: input.fixtureName, + fixtureInput, + driver: input.driver.driver, + modelSelection: input.driver.modelSelection, + }).pipe(Effect.provide(IdAllocator.layer), provideDeterministicTestRuntime); + const scenario = { + name: `${input.fixtureName}/${input.driver.driver}`, + transcript, + commands: materialized.commands, + steps: materialized.steps, + projectionThreadIds: materialized.projectionThreadIds, + runtimePolicyOverride: { + ...input.driver.runtimePolicyOverride, + cwd: workspace, + }, + }; + + const result = yield* runOrchestratorV2ProviderReplayScenario( + scenario, + input.harness, + input.driver.runContinuationWorker === true ? { runContinuationWorker: true } : {}, + ).pipe(provideDeterministicTestRuntime); + input.driver.assertOutput(result, transcript); + assertProviderNativeSubagentRootTurns(result); + const expectedAbsentWorkspacePaths = input.driver.expectedAbsentWorkspacePaths; + if (expectedAbsentWorkspacePaths !== undefined) { + yield* Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + for (const relativePath of expectedAbsentWorkspacePaths) { + assert.isFalse( + yield* fs.exists(path.join(workspace, relativePath)), + `${input.fixtureName}/${input.driver.driver} must not create ${relativePath} in the replay workspace`, + ); + } + }).pipe(Effect.provide(NodeServices.layer)); + } + assert.isFalse( + result.domainEvents.some(isStreamingAssistantEvent), + "buffered delivery must not persist streaming assistant artifacts", + ); + const projectionThreadId = materialized.projectionThreadIds[0]; + assert.isDefined(projectionThreadId); + const projection = result.projections.get(projectionThreadId); + assert.isDefined(projection); + const latestRun = projection.runs.at(-1); + assert.deepEqual(latestRun?.modelSelection, input.driver.modelSelection); + if (projection.runs.some((run) => run.status === "completed")) { + const threadStartCheckpoint = projection.checkpoints.find( + (checkpoint) => checkpoint.ordinalWithinScope === 0 && checkpoint.appRunOrdinal === null, + ); + assert.isDefined( + threadStartCheckpoint, + "completed threads must retain an addressable thread-start checkpoint", + ); + assert.equal(threadStartCheckpoint.status, "ready"); + } + return result; +}); + +/** Replays one fixture variant through OrchestratorV2 and runs the shared output checks. */ +export function runReplayFixture< + Transcript extends ProviderReplayTranscript, + Error, + WorkspaceError = never, +>(input: ReplayFixtureRun) { + return runFixtureProvider(input).pipe(Effect.mapError(normalizeTestError), Effect.scoped); +} + +export function findReplayFixture(name: string) { + return ORCHESTRATOR_REPLAY_FIXTURES.find((fixture) => fixture.name === name); +} + +/** + * Registers one test per recorded fixture variant of `driver`. Call it inside the + * provider file's `describe`. `run` picks the harness for each variant. + */ +export function itRunsReplayFixtures( + driver: string, + run: (input: ReplayFixtureCase) => Effect.Effect, +) { + it.effect.each( + ORCHESTRATOR_REPLAY_FIXTURES.flatMap((fixture) => + fixture.providers + .filter((provider) => provider.driver === driver) + .map((provider) => [fixture.name, provider.driver, fixture, provider] as const), + ), + )("runs %s/%s through OrchestratorV2 using deterministic replay", ([, , fixture, provider]) => + run({ fixtureName: fixture.name, buildInput: fixture.buildInput, driver: provider }), + ); +} diff --git a/apps/server/src/testUtils/shardWeights.json b/apps/server/src/testUtils/shardWeights.json index 185ee3e0d003..d0f1fafc6968 100644 --- a/apps/server/src/testUtils/shardWeights.json +++ b/apps/server/src/testUtils/shardWeights.json @@ -123,10 +123,16 @@ "src/orchestration-v2/legacy/LegacyV1Cutover.integration.test.ts": 1.4, "src/orchestration-v2/legacy/LegacyV1ThreadImporter.test.ts": 0.5, "src/orchestration-v2/runtimeLayer.test.ts": 2.8, - "src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts": 2.7, - "src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts": 44.6, + "src/orchestration-v2/testkit/AcpRegistryReplayFixtures.integration.test.ts": 4.8, + "src/orchestration-v2/testkit/ClaudeReplayFixtures.integration.test.ts": 13.7, + "src/orchestration-v2/testkit/CodexReplayFixtures.integration.test.ts": 6.4, + "src/orchestration-v2/testkit/CursorReplayFixtures.integration.test.ts": 4.8, + "src/orchestration-v2/testkit/GrokReplayFixtures.integration.test.ts": 5.1, + "src/orchestration-v2/testkit/MuseReplayFixtures.integration.test.ts": 2.4, + "src/orchestration-v2/testkit/OpenCodeReplayFixtures.integration.test.ts": 5.8, "src/orchestration-v2/testkit/OrchestratorReplayRecovery.integration.test.ts": 2.2, "src/orchestration-v2/testkit/OrchestratorReplayRestartBackgroundNote.integration.test.ts": 2.1, + "src/orchestration-v2/testkit/PiReplayFixtures.integration.test.ts": 4.3, "src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts": 16.1, "src/orchestration-v2/testkit/ThreadFork.integration.test.ts": 2.9, "src/orchestration-v2/testkit/ThreadMergeBack.integration.test.ts": 5.1, From 163bdeaa119f32bf92f25d2841048b808df542a5 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 21 Oct 2026 00:20:53 -0700 Subject: [PATCH 05/73] fix(server): delegates finish when their leftover work settles, and Grok commands stop holding runs open (#22709) Co-authored-by: Claude Opus 5.5 Co-authored-by: Fuksss --- apps/server/scripts/acp-mock-agent.ts | 125 +++++++++- .../Adapters/AcpAdapterV2.test.ts | 189 +++++++++++++++ .../DelegatedCompletionDelivery.test.ts | 220 ++++++++++++++++++ .../src/orchestration-v2/Orchestrator.ts | 42 ++++ packages/provider-acp/src/server/adapter.ts | 11 + .../src/server/xaiAcpExtension.test.ts | 148 ++++++++++++ .../src/server/xaiAcpExtension.ts | 44 +++- 7 files changed, 764 insertions(+), 15 deletions(-) diff --git a/apps/server/scripts/acp-mock-agent.ts b/apps/server/scripts/acp-mock-agent.ts index 6dcfde2ee332..7a749b501bd3 100644 --- a/apps/server/scripts/acp-mock-agent.ts +++ b/apps/server/scripts/acp-mock-agent.ts @@ -46,9 +46,11 @@ const emitXAiAskUserQuestionThenHang = process.env.T3_ACP_EMIT_XAI_ASK_USER_QUESTION_THEN_HANG === "1"; const emitContentThenHang = process.env.T3_ACP_EMIT_CONTENT_THEN_HANG === "1"; const emitPlanThenHang = process.env.T3_ACP_EMIT_PLAN_THEN_HANG === "1"; +const grokKillTaskShape = process.env.T3_ACP_GROK_KILL_TASK_SHAPE; const emitActiveToolThenHang = process.env.T3_ACP_EMIT_ACTIVE_TOOL_THEN_HANG === "1"; const emitGrokMonitorPostTurnPoll = process.env.T3_ACP_EMIT_GROK_MONITOR_POST_TURN_POLL === "1"; const emitGrokBackgroundTaskStarted = process.env.T3_ACP_EMIT_GROK_BACKGROUND_TASK_STARTED === "1"; +const emitGrokBackgroundTaskPoll = process.env.T3_ACP_GROK_BACKGROUND_TASK_COMPLETION === "poll"; const emitForeignSessionUpdates = process.env.T3_ACP_EMIT_FOREIGN_SESSION_UPDATES === "1"; const waitForResumeRelease = process.env.T3_ACP_WAIT_FOR_RESUME_RELEASE === "1"; const completeFirstPromptOnCancel = process.env.T3_ACP_COMPLETE_FIRST_PROMPT_ON_CANCEL === "1"; @@ -461,6 +463,71 @@ function modelState(): AcpCompat.SessionModelState { }; } +function emitGrokKillTaskScenario( + sessionId: string, + promptId: string | undefined, + shape: string, +): void { + const taskId = "call-fb9d0000-0000-0000-0000-000000000026-29"; + const outcome = + shape === "already_exited" || shape === "known_failure" ? "already_exited" : "killed"; + writeJsonRpcNotification("session/update", { + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: "kill-target-command", + title: "run_terminal_command", + kind: "execute", + status: "in_progress", + rawInput: { command: "sleep 40" }, + }, + }); + writeJsonRpcNotification("session/update", { + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: "kill-target-command", + status: "completed", + rawOutput: { + type: "BackgroundTaskStarted", + task_id: taskId, + task_type: "bash", + command: "sleep 40", + }, + }, + }); + if (shape === "known_failure") { + writeJsonRpcNotification("_x.ai/task_completed", { + sessionId, + update: { + sessionUpdate: "task_completed", + task_snapshot: { task_id: taskId, kind: "bash", exit_code: 7, output: "known failure\n" }, + }, + }); + } + writeJsonRpcNotification("session/update", { + sessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: "kill-command", + title: `kill ${taskId} (${outcome})`, + kind: "other", + status: "completed", + rawInput: { + task_id: taskId, + ...(shape === "output_only" ? {} : { variant: "KillTask" }), + }, + ...(shape === "variant_only" ? {} : { rawOutput: { type: "KillTask", outcome } }), + }, + }); + writeJsonRpcNotification("_x.ai/session/prompt_complete", { + sessionId, + promptId, + stopReason: "end_turn", + agentResult: null, + }); +} + const program = Effect.gen(function* () { const agent = yield* EffectAcpAgent.AcpAgent; const resumeRelease = yield* Deferred.make(); @@ -1498,6 +1565,16 @@ const program = Effect.gen(function* () { return yield* Effect.never; } + if (grokKillTaskShape !== undefined) { + emitGrokKillTaskScenario( + requestedSessionId, + promptIdFromRequestMeta(request), + grokKillTaskShape, + ); + // Keep the prompt RPC and peer alive after the kill result. + return yield* Effect.never; + } + if (emitPlanThenHang) { yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, @@ -1685,13 +1762,15 @@ const program = Effect.gen(function* () { return yield* Effect.never; } - if (emitGrokBackgroundTaskStarted) { - const toolCallId = "call-fb9d0000-0000-0000-0000-000000000026"; + if (emitGrokBackgroundTaskStarted || emitGrokBackgroundTaskPoll) { + const toolCallId = !emitGrokBackgroundTaskPoll + ? "call-fb9d0000-0000-0000-0000-000000000026" + : "call-fb9d0000-0000-0000-0000-000000000026-29"; const command = "sleep 40; echo done-a"; writeJsonRpcNotification("session/update", { sessionId: requestedSessionId, update: { - sessionUpdate: "tool_call", + sessionUpdate: !emitGrokBackgroundTaskPoll ? "tool_call" : "tool_call_update", toolCallId, title: "run_terminal_command", kind: "execute", @@ -1714,6 +1793,46 @@ const program = Effect.gen(function* () { }, }, }); + if (emitGrokBackgroundTaskPoll) { + writeJsonRpcNotification("session/update", { + sessionId: requestedSessionId, + update: { + sessionUpdate: "tool_call_update", + toolCallId: "call-background-poll", + title: "get_command_or_subagent_output", + kind: "other", + status: "completed", + rawOutput: { + type: "TaskOutput", + Result: { + task_id: toolCallId, + command, + status: "completed", + exit_code: 0, + output: "done-a", + }, + }, + }, + }); + + yield* agent.client.sessionUpdate({ + sessionId: requestedSessionId, + update: { + sessionUpdate: "agent_message_chunk", + content: { type: "text", text: "The background command finished." }, + }, + }); + writeJsonRpcNotification("_x.ai/session/update", { + sessionId: requestedSessionId, + update: { + sessionUpdate: "turn_completed", + prompt_id: promptIdFromRequestMeta(request), + stop_reason: "end_turn", + }, + }); + // Neither the prompt RPC nor the process ends, matching the Grok incident. + return yield* Effect.never; + } writeJsonRpcNotification("_x.ai/session/prompt_complete", { sessionId: requestedSessionId, promptId: promptIdFromRequestMeta(request) ?? "mock-xai-prompt-1", diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index fa4b0883c078..1a0c6caf1f92 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -63,6 +63,8 @@ import { } from "@t3tools/provider-acp/server/sessionConfig"; import { extractXAiAcpSubagentUpdate, + makeGrokAdapterV2, + makeXAiPromptCompletionRuntime, normalizeXAiAcpToolCallState, registerXAiBackgroundTaskTracking, } from "@t3tools/provider-grok/testing"; @@ -3079,6 +3081,193 @@ describe("AcpAdapterV2", () => { }).pipe(Effect.provide(layerTest), Effect.scoped), ); + // Oracle: Grok incident frames. Command completion must release the turn even + // when the CLI keeps the prompt RPC and process alive. Command polls stay visible. + it.live( + "completes a structured Grok background command from poll with an unanswered prompt RPC", + () => + Effect.gen(function* () { + const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const path = yield* Path.Path; + const mockAgentPath = yield* path.fromFileUrl( + new URL("../../../scripts/acp-mock-agent.ts", import.meta.url), + ); + const instanceId = ProviderInstanceId.make("grok-background-completion"); + const adapter = yield* makeGrokAdapterV2({ + instanceId, + settings: DEFAULT_GROK_SETTINGS, + environment: {}, + hostPlatform: yield* HostProcess.Platform, + selfInvocation: yield* resolveSelfInvocation(), + makeRuntime: (input) => + makeMockRuntime({ + childProcessSpawner, + mockAgentPath, + environment: { T3_ACP_GROK_BACKGROUND_TASK_COMPLETION: "poll" }, + })(input).pipe(Effect.flatMap(makeXAiPromptCompletionRuntime)), + }); + const threadId = ThreadId.make("grok-background-completion"); + const modelSelection = { instanceId, model: "default" } as const; + const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: process.cwd(), + }); + const runtime = yield* adapter.openSession({ + threadId, + providerSessionId: ProviderSessionId.make("grok-background-completion"), + modelSelection, + runtimePolicy, + }); + const providerThread = yield* runtime.ensureThread({ + threadId, + modelSelection, + runtimePolicy, + }); + const events = yield* Queue.unbounded(); + yield* runtime.events.pipe( + Stream.runForEach((event) => Queue.offer(events, event)), + Effect.forkScoped, + ); + yield* runtime.startTurn( + makeTurnInput({ + threadId, + providerThread, + instanceId, + runtimePolicy, + now: yield* DateTime.now, + }), + ); + const observed = yield* Stream.fromQueue(events).pipe( + Stream.takeUntil((event) => event.type === "turn.terminal"), + Stream.runCollect, + ); + const allEvents = [...observed]; + let next = yield* Queue.poll(events); + while (Option.isSome(next)) { + allEvents.push(next.value); + next = yield* Queue.poll(events); + } + const terminals = allEvents.filter((event) => event.type === "turn.terminal"); + assert.deepEqual( + terminals.map((event) => event.status), + ["completed"], + ); + const items = allEvents.flatMap((event) => + event.type === "turn_item.updated" ? [event.turnItem] : [], + ); + assert.isFalse(items.some((item) => item.type === "subagent")); + const command = items.findLast( + (item) => item.nativeItemRef?.nativeId === "call-fb9d0000-0000-0000-0000-000000000026-29", + ); + assert.equal(command?.type, "command_execution"); + assert.equal(command?.status, "completed"); + assert.isTrue( + items.some((item) => item.nativeItemRef?.nativeId === "call-background-poll"), + "command TaskOutput must retain its normal tool frame", + ); + }).pipe(Effect.provide(layerTest), Effect.scoped), + ); + + // Oracle: #15888 KillTask reports. Command completion must release the turn even + // when the CLI keeps the prompt RPC and process alive. Kill calls stay visible. + it.live.each([ + { shape: "output_only", expectedCommand: { type: "command_execution", status: "completed" } }, + { shape: "variant_only", expectedCommand: { type: "command_execution", status: "completed" } }, + { shape: "killed", expectedCommand: { type: "command_execution", status: "completed" } }, + { + shape: "already_exited", + expectedCommand: { type: "command_execution", status: "completed" }, + }, + { + shape: "known_failure", + expectedCommand: { type: "command_execution", status: "failed", output: "known failure\n" }, + }, + ] as const)( + "completes a Grok KillTask command ($shape) and preserves any known result", + ({ shape, expectedCommand }) => + Effect.gen(function* () { + const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const path = yield* Path.Path; + const mockAgentPath = yield* path.fromFileUrl( + new URL("../../../scripts/acp-mock-agent.ts", import.meta.url), + ); + const instanceId = ProviderInstanceId.make("grok-kill-completion"); + const adapter = yield* makeGrokAdapterV2({ + instanceId, + settings: DEFAULT_GROK_SETTINGS, + environment: {}, + hostPlatform: yield* HostProcess.Platform, + selfInvocation: yield* resolveSelfInvocation(), + makeRuntime: (input) => + makeMockRuntime({ + childProcessSpawner, + mockAgentPath, + environment: { T3_ACP_GROK_KILL_TASK_SHAPE: shape }, + })(input).pipe(Effect.flatMap(makeXAiPromptCompletionRuntime)), + }); + const threadId = ThreadId.make("grok-kill-completion"); + const modelSelection = { instanceId, model: "default" } as const; + const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: process.cwd(), + }); + const runtime = yield* adapter.openSession({ + threadId, + providerSessionId: ProviderSessionId.make("grok-kill-completion"), + modelSelection, + runtimePolicy, + }); + const providerThread = yield* runtime.ensureThread({ + threadId, + modelSelection, + runtimePolicy, + }); + const events = yield* Queue.unbounded(); + yield* runtime.events.pipe( + Stream.runForEach((event) => Queue.offer(events, event)), + Effect.forkScoped, + ); + yield* runtime.startTurn( + makeTurnInput({ + threadId, + providerThread, + instanceId, + runtimePolicy, + now: yield* DateTime.now, + }), + ); + const observed = yield* Stream.fromQueue(events).pipe( + Stream.takeUntil((event) => event.type === "turn.terminal"), + Stream.runCollect, + ); + const allEvents = [...observed]; + let next = yield* Queue.poll(events); + while (Option.isSome(next)) { + allEvents.push(next.value); + next = yield* Queue.poll(events); + } + const terminals = allEvents.filter((event) => event.type === "turn.terminal"); + assert.deepEqual( + terminals.map((event) => event.status), + ["completed"], + ); + const items = allEvents.flatMap((event) => + event.type === "turn_item.updated" ? [event.turnItem] : [], + ); + assert.isFalse(items.some((item) => item.type === "subagent")); + const command = items.findLast( + (item) => item.nativeItemRef?.nativeId === "kill-target-command", + ); + assert.deepInclude(command, expectedCommand); + assert.isTrue( + items.some((item) => item.nativeItemRef?.nativeId === "kill-command"), + "KillTask must retain its normal tool frame", + ); + }).pipe(Effect.provide(layerTest), Effect.scoped), + ); + it.effect("keeps an agent's empty-valued default choice selectable", () => Effect.gen(function* () { const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; diff --git a/apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts b/apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts index 0bdca007458a..e58c8a1e528f 100644 --- a/apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts +++ b/apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts @@ -2402,4 +2402,224 @@ it.layer(layerTest)("delegated tasks across a server restart", (it) => { ); }), ); + + it.effect.each(["a background command", "a nested subagent"] as const)( + "finalizes a child whose run ended before %s settled", + (blocker) => + Effect.gen(function* () { + const orchestrator = yield* Orchestrator.OrchestratorV2; + const sink = yield* EventSink.EventSinkV2; + const now = yield* DateTime.now; + const key = blocker === "a background command" ? "command" : "nested"; + const threadId = ThreadId.make(`late-settle-parent-${key}`); + const runId = RunId.make(`late-settle-parent-run-${key}`); + const rootNodeId = NodeId.make(`late-settle-parent-root-${key}`); + yield* seedParentWithTerminalTask({ + threadId, + projectId: ProjectId.make(`late-settle-project-${key}`), + runId, + rootNodeId, + taskId: NodeId.make(`late-settle-seed-task-${key}`), + deliveryState: "acknowledged", + now, + }); + // A delegated child with a running task on the parent and a running run of its own. + const delegate = (name: string) => + Effect.gen(function* () { + const taskId = NodeId.make(`node:late-settle-${key}-${name}`); + const childThreadId = ThreadId.make(`thread:late-settle-${key}-${name}`); + const run = runEvent({ + threadId: childThreadId, + runId: RunId.make(`run:late-settle-${key}-${name}`), + ordinal: 1, + status: "running", + now, + }); + yield* sink.write({ + events: [ + { + id: EventId.make(`event:late-settle-${key}-${name}:thread`), + type: "thread.created", + threadId: childThreadId, + occurredAt: now, + payload: { + createdBy: "agent", + creationSource: "server", + id: childThreadId, + projectId: ProjectId.make(`late-settle-project-${key}`), + title: name, + providerInstanceId: modelSelection.instanceId, + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: null, + lineage: { + parentThreadId: threadId, + relationshipToParent: "subagent", + rootThreadId: threadId, + }, + forkedFrom: { type: "node", nodeId: taskId }, + createdAt: now, + updatedAt: now, + archivedAt: null, + settledOverride: null, + settledAt: null, + lastVisitedAt: null, + deletedAt: null, + }, + }, + { + id: EventId.make(`event:late-settle-${key}-${name}:task`), + type: "subagent.updated", + threadId, + runId, + nodeId: taskId, + driver, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: { + id: taskId, + threadId, + runId, + parentNodeId: rootNodeId, + origin: "app_owned", + createdBy: "agent", + driver, + providerInstanceId: modelSelection.instanceId, + providerThreadId: null, + childThreadId, + nativeTaskRef: null, + prompt: `Run ${name}.`, + title: null, + model: null, + completionWake: "always", + status: "running", + result: null, + startedAt: now, + completedAt: null, + updatedAt: now, + }, + }, + run, + ], + }); + return { task: { id: taskId }, childThreadId, run: run.payload }; + }); + const settled = (taskId: NodeId, afterSequence: number) => + sink.stream({ afterSequence, eventType: "subagent.updated" }).pipe( + Stream.filter( + (stored) => + stored.event.type === "subagent.updated" && + stored.event.payload.id === taskId && + stored.event.payload.status === "completed", + ), + Stream.take(1), + Stream.runDrain, + ); + const blocked = yield* delegate("blocked"); + const barrier = yield* delegate("barrier"); + const providerThread = { + id: ProviderThreadId.make(`provider-thread:late-settle-child-${key}`), + driver, + providerInstanceId: modelSelection.instanceId, + providerSessionId: null, + appThreadId: blocked.childThreadId, + ownerNodeId: null, + nativeThreadRef: null, + nativeConversationHeadRef: null, + status: "idle" as const, + firstRunOrdinal: 1, + lastRunOrdinal: 1, + handoffIds: [], + forkedFrom: null, + createdAt: now, + updatedAt: now, + }; + const nested = { + id: NodeId.make(`late-settle-nested-${key}`), + threadId: blocked.childThreadId, + runId: blocked.run.id, + parentNodeId: NodeId.make(`late-settle-child-root-${key}`), + origin: "provider_native" as const, + createdBy: "agent" as const, + driver, + providerInstanceId: modelSelection.instanceId, + providerThreadId: null, + childThreadId: null, + nativeTaskRef: null, + prompt: "Nested work", + title: null, + model: null, + status: "running" as const, + result: null, + startedAt: now, + completedAt: null, + updatedAt: now, + }; + const blockerEvent = (open: boolean) => + blocker === "a background command" + ? { + id: EventId.make(`event:late-settle-roster-${key}-${open}`), + type: "provider-thread.updated" as const, + threadId: blocked.childThreadId, + driver, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: { + ...providerThread, + pendingBackgroundTasks: open + ? [{ taskId: "sleep-900", kind: "command" as const }] + : [], + }, + } + : { + id: EventId.make(`event:late-settle-nested-${key}-${open}`), + type: "subagent.updated" as const, + threadId: blocked.childThreadId, + runId: blocked.run.id, + nodeId: nested.id, + driver, + providerInstanceId: modelSelection.instanceId, + occurredAt: now, + payload: open + ? nested + : { ...nested, status: "completed" as const, result: "done", completedAt: now }, + }; + const ended = (child: typeof blocked) => ({ + ...runEvent({ + threadId: child.childThreadId, + runId: child.run.id, + ordinal: child.run.ordinal, + status: "completed", + now, + }), + payload: { ...child.run, status: "completed" as const, startedAt: now, completedAt: now }, + }); + + const beforeEnd = yield* sink.latestSequence(); + yield* sink.write({ events: [blockerEvent(true)] }); + yield* sink.write({ events: [ended(blocked)] }); + yield* sink.write({ events: [ended(barrier)] }); + // Run updates are handled in order, so the barrier settling means the blocked child's + // run end was handled too, while its work still ran. + yield* settled(barrier.task.id, beforeEnd); + assert.equal( + (yield* orchestrator.getThreadProjection(threadId)).subagents.find( + (task) => task.id === blocked.task.id, + )?.status, + "running", + ); + + const beforeSettle = yield* sink.latestSequence(); + yield* sink.write({ events: [blockerEvent(false)] }); + yield* settled(blocked.task.id, beforeSettle); + const finished = (yield* orchestrator.getThreadProjection(threadId)).subagents.find( + (task) => task.id === blocked.task.id, + ); + assert.equal(finished?.status, "completed"); + assert.notEqual(finished?.completionDelivery?.state, "disposed"); + }), + ); }); diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 22d3de23a696..213a1a31ab74 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -11826,6 +11826,48 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio Effect.forkDetach, ); + // A child whose run ends while its background tasks or its own subagents still run has no + // result yet. When that work settles there is no run update to finalize it, so these events + // do it (#17512). A background roster counts once it empties after it was seen non-empty. + const providerThreadsWithBackgroundWork = new Set(); + const settlesChildWork = (stored: OrchestrationV2StoredEvent) => { + const { event } = stored; + if (event.type === "subagent.updated") + return !isOrchestrationV2WorkActive(event.payload.status); + if (event.type !== "provider-thread.updated") return false; + if ((event.payload.pendingBackgroundTasks?.length ?? 0) > 0) { + providerThreadsWithBackgroundWork.add(event.payload.id); + return false; + } + return providerThreadsWithBackgroundWork.delete(event.payload.id); + }; + yield* Stream.merge( + eventSink.stream({ + afterSequence: terminalEventsAfterSequence, + eventType: "provider-thread.updated", + }), + eventSink.stream({ afterSequence: terminalEventsAfterSequence, eventType: "subagent.updated" }), + ).pipe( + Stream.filter(settlesChildWork), + Stream.runForEach((stored) => + Effect.gen(function* () { + const childThreadId = stored.event.threadId; + const parentThreadId = yield* appOwnedSubagentParentThreadId(childThreadId); + if (parentThreadId === undefined) return; + yield* threadDispatch.withLock(parentThreadId, finalizeAppOwnedSubagent(childThreadId)); + }).pipe( + Effect.catchCause((cause) => + Effect.logWarning("Failed to finalize a delegated child after its work settled", { + threadId: stored.event.threadId, + sequence: stored.sequence, + cause, + }), + ), + ), + ), + Effect.forkDetach, + ); + const recoverDelegatedTasks = Effect.gen(function* () { yield* projectionStore.getRecoveryThreadIds("subagent-results").pipe( Effect.tap((threadIds) => diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index 202f26c32e9f..01a729cd7c9e 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -3555,6 +3555,17 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( backgroundCompletion.appendOutput, ) : { ...target, status: nextStatus }; + // BackgroundTaskStarted normalization can reopen this row, so keep the + // completion for settlement when no task_completed recorded one. + if ( + backgroundCompletion.status !== "running" && + context.backgroundCommands.get(backgroundCompletion.taskId) === null + ) { + context.backgroundCommands.set(backgroundCompletion.taskId, { + status: backgroundCompletion.status, + output: toolOutputText(hydrated), + }); + } yield* emitTool(context, hydrated); } } diff --git a/packages/provider-grok/src/server/xaiAcpExtension.test.ts b/packages/provider-grok/src/server/xaiAcpExtension.test.ts index 5de0a88c1986..8245d5f16b87 100644 --- a/packages/provider-grok/src/server/xaiAcpExtension.test.ts +++ b/packages/provider-grok/src/server/xaiAcpExtension.test.ts @@ -484,6 +484,73 @@ describe("XAiAcpExtension", () => { expect(normalized.status).toBe("inProgress"); }); + it("completes structured command TaskOutput ids without treating them as subagents", () => { + // Oracle: BackgroundTaskStarted and TaskOutput from the Grok hang incident. + const toolCall: AcpToolCallState = { + toolCallId: "call-poll", + title: "get_command_or_subagent_output", + status: "completed", + data: { + rawInput: { + variant: "TaskOutput", + task_ids: ["call-7c835a60-7bf5-4284-a5c7-cf42a4212ece-29"], + }, + rawOutput: { + type: "TaskOutput", + Result: { + task_id: "call-7c835a60-7bf5-4284-a5c7-cf42a4212ece-29", + status: "completed", + exit_code: 0, + output: "done", + }, + }, + }, + }; + expect(extractXAiBackgroundTaskCompletion(toolCall)).toEqual([ + { + taskId: "call-7c835a60-7bf5-4284-a5c7-cf42a4212ece-29", + status: "completed", + appendOutput: "done", + }, + ]); + expect(extractXAiAcpSubagentUpdate(toolCall)).toBeUndefined(); + }); + + it("keeps the normal tool frame for a command result from a mixed-ID poll", () => { + // Oracle: review 4325739061 requires the result ID to override requested subagent IDs. + const toolCall: AcpToolCallState = { + toolCallId: "call-mixed-poll", + title: "get_command_or_subagent_output", + status: "completed", + data: { + rawInput: { + variant: "TaskOutput", + task_ids: [ + "019f44a6-4820-7402-925d-bc862ee711dd", + "call-7c835a60-7bf5-4284-a5c7-cf42a4212ece-29", + ], + }, + rawOutput: { + type: "TaskOutput", + Result: { + task_id: "call-7c835a60-7bf5-4284-a5c7-cf42a4212ece-29", + status: "completed", + exit_code: 0, + output: "done", + }, + }, + }, + }; + expect(extractXAiAcpSubagentUpdate(toolCall)).toBeUndefined(); + expect(extractXAiBackgroundTaskCompletion(toolCall)).toEqual([ + { + taskId: "call-7c835a60-7bf5-4284-a5c7-cf42a4212ece-29", + status: "completed", + appendOutput: "done", + }, + ]); + }); + it("hydrates monitor completion from a standalone TaskOutput frame without rawInput", () => { // Post-settle shape observed live (2026-07-12): the final // get_command_or_subagent_output update carries only rawOutput; the frame @@ -615,6 +682,87 @@ describe("XAiAcpExtension", () => { ]); }); + // Oracle: #15888 reports KillTask replies with killed and already_exited outcomes. + it.each([ + { rawInput: { task_id: "call-bg-1" }, rawOutput: { type: "KillTask" } }, + { rawInput: { task_id: "call-bg-1" }, rawOutput: { type: "KillTask", outcome: "killed" } }, + { rawInput: { variant: "KillTask", task_id: "call-bg-1" } }, + { + rawInput: { variant: "KillTask", task_id: "call-bg-1" }, + rawOutput: { type: "KillTask", outcome: "already_exited" }, + }, + ])("closes a background command from structured KillTask fields: %j", (data) => { + expect( + extractXAiKilledBackgroundTasks({ + toolCallId: "call-kill", + title: "kill call-bg-1 (killed)", + status: "completed", + data, + }), + ).toEqual([{ taskId: "call-bg-1", status: "completed", appendOutput: "" }]); + expect( + extractXAiAcpSubagentUpdate({ + toolCallId: "call-kill", + title: "kill call-bg-1 (killed)", + status: "completed", + data, + }), + ).toBeUndefined(); + }); + + // Native child status still comes from its lifecycle/result path, not the kill RPC. + it.each(["Kill", "KillTask"])( + "keeps %s aimed at a native child out of child result parsing", + (variant) => { + expect( + extractXAiAcpSubagentUpdate({ + toolCallId: "call-kill-child", + title: "kill 019f44b8-8e98-7c80-a40e-df1e26a5f9e3 (killed)", + status: "completed", + data: { + rawInput: { variant, task_id: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3" }, + rawOutput: { type: "KillTask", outcome: "killed" }, + }, + }), + ).toBeUndefined(); + }, + ); + + // A successful RPC is not proof of a successful kill when its outcome says otherwise. + it.each([ + { type: "KillTask", outcome: "failed" }, + { type: "KillTask", Result: { task_id: "call-bg-1", outcome: "error" } }, + ])("ignores a KillTask whose outcome is not a kill: %j", (rawOutput) => { + expect( + extractXAiKilledBackgroundTasks({ + toolCallId: "call-kill", + title: "kill call-bg-1 (failed)", + status: "completed", + data: { + rawInput: { variant: "KillTask", task_id: "call-bg-1" }, + rawOutput, + }, + }), + ).toEqual([]); + }); + + it.each(["failed", "inProgress"] as const)( + "does not close the target of a %s KillTask call", + (status) => { + expect( + extractXAiKilledBackgroundTasks({ + toolCallId: "call-kill", + title: "kill call-bg-1 (killed)", + status, + data: { + rawInput: { variant: "KillTask", task_id: "call-bg-1" }, + rawOutput: { type: "KillTask", outcome: "killed" }, + }, + }), + ).toEqual([]); + }, + ); + it("tombstones confirmed kills and ignores failed kill calls", () => { expect( extractXAiKilledBackgroundTasks({ diff --git a/packages/provider-grok/src/server/xaiAcpExtension.ts b/packages/provider-grok/src/server/xaiAcpExtension.ts index 92c0c14535de..4f04cf6b41f1 100644 --- a/packages/provider-grok/src/server/xaiAcpExtension.ts +++ b/packages/provider-grok/src/server/xaiAcpExtension.ts @@ -272,8 +272,8 @@ export function isXAiMonitorTool(toolCall: AcpToolCallState): boolean { * A shell command Grok runs in the background (asked for, or moved there after * its auto-background timeout) acknowledges with * `{ type: "BackgroundTaskStarted", task_id }` and completes its tool call - * while the process still runs. Like a monitor, it ends only with - * `x.ai/task_completed` for that task id. + * while the process still runs. It ends with a task completion notification + * or completed TaskOutput for that task id. */ export function xAiBackgroundShellTaskId(toolCall: AcpToolCallState): string | undefined { const rawOutput = unknownRecord(toolCall.data.rawOutput); @@ -333,7 +333,9 @@ export function extractXAiBackgroundTaskCompletion(toolCall: AcpToolCallState): const appendOutput = resultFromGetOutputTool(toolCall, text) ?? ""; // Prefer the completed identity from the result/header over the request list // (one call can poll multiple task_ids). - const resultTaskId = resultTaskIdFromGetOutputTool(toolCall, text); + // Command task ids can be `call--`; subagent ids stay UUID-only. + const resultTaskId = + structuredTaskIdFromGetOutputTool(toolCall) ?? resultTaskIdFromGetOutputTool(toolCall, text); if (resultTaskId !== undefined) { return [{ taskId: resultTaskId, status, appendOutput }]; } @@ -355,8 +357,8 @@ export function extractXAiBackgroundTaskCompletion(toolCall: AcpToolCallState): /** * A finished kill_command_or_subagent call is the genuine end signal for every - * task id it names: the Grok CLI emits no `x.ai/task_completed` for a killed - * task. A failed kill is not proof the target stopped, so leave the existing + * task id it names, even without a separate `x.ai/task_completed` notification. + * A failed kill is not proof the target stopped, so leave the existing * task state unchanged until a genuine terminal signal arrives. */ export function extractXAiKilledBackgroundTasks(toolCall: AcpToolCallState): ReadonlyArray<{ @@ -366,9 +368,15 @@ export function extractXAiKilledBackgroundTasks(toolCall: AcpToolCallState): Rea }> { const title = titleKey(toolCall); const rawInput = unknownRecord(toolCall.data.rawInput); + const rawOutput = unknownRecord(toolCall.data.rawOutput); const variant = nonEmptyString(rawInput?.variant)?.toLowerCase(); - if (!title.includes("kill_command_or_subagent") && variant !== "kill") return []; + const legacy = title.includes("kill_command_or_subagent") || variant === "kill"; + if (!legacy && variant !== "killtask" && rawOutput?.type !== "KillTask") return []; if (toolCall.status !== "completed") return []; + // Grok nests the outcome under Result; only a missing, killed or already_exited one ends the task. + const outcome = rawOutput?.outcome ?? unknownRecord(rawOutput?.Result)?.outcome; + if (!legacy && outcome !== undefined && outcome !== "killed" && outcome !== "already_exited") + return []; const taskIds: string[] = []; const push = (value: unknown) => { const id = nonEmptyString(value); @@ -742,17 +750,22 @@ function taskIdsFromGetOutputTool( return [...new Set(ids)]; } -function resultTaskIdFromGetOutputTool( - toolCall: AcpToolCallState, - output: string | undefined, -): string | undefined { +function structuredTaskIdFromGetOutputTool(toolCall: AcpToolCallState): string | undefined { const rawOutput = unknownRecord(toolCall.data.rawOutput); const result = unknownRecord(rawOutput?.Result) ?? unknownRecord(rawOutput?.result); - const structured = + return ( nonEmptyString(result?.task_id) ?? nonEmptyString(result?.taskId) ?? nonEmptyString(rawOutput?.task_id) ?? - nonEmptyString(rawOutput?.taskId); + nonEmptyString(rawOutput?.taskId) + ); +} + +function resultTaskIdFromGetOutputTool( + toolCall: AcpToolCallState, + output: string | undefined, +): string | undefined { + const structured = structuredTaskIdFromGetOutputTool(toolCall); if (structured !== undefined && new RegExp(`^${XAI_UUID_RE}$`, "i").test(structured)) { return structured; } @@ -831,6 +844,13 @@ export function extractXAiAcpSubagentUpdate( const output = xAiToolOutputText(toolCall); if (isXAiGetSubagentOutputTool(toolCall, rawInput)) { + const structuredTaskId = structuredTaskIdFromGetOutputTool(toolCall); + if ( + structuredTaskId !== undefined && + !new RegExp(`^${XAI_UUID_RE}$`, "i").test(structuredTaskId) + ) { + return undefined; + } const taskIds = taskIdsFromGetOutputTool(toolCall, rawInput); const childSessionId = resultTaskIdFromGetOutputTool(toolCall, output) ?? From d0e427d4c3266395b81a1f37a70236aa4968c99d Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 21 Oct 2026 00:33:47 -0700 Subject: [PATCH 06/73] fix(server): storage cleanup no longer counts pnpm clones as freed space (#22713) Co-authored-by: Claude Opus 5.5 --- apps/server/src/storageCleanup.test.ts | 53 ++++++++++++++++++++++++++ apps/server/src/storageCleanup.ts | 29 ++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/apps/server/src/storageCleanup.test.ts b/apps/server/src/storageCleanup.test.ts index 00fabc7004fb..48529f28a9d5 100644 --- a/apps/server/src/storageCleanup.test.ts +++ b/apps/server/src/storageCleanup.test.ts @@ -1241,6 +1241,59 @@ describe("storage cleanup reports and local file policies", () => { }), ), ); + it.live.each(["same-device", "other-device", "no-manifest"] as const)( + "counts the pnpm virtual store only when its store forces copies (%s)", + (store) => + runCleanupTest( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const storeDir = yield* fs.makeTempDirectoryScoped({ prefix: "pnpm-store-" }); + const { service, worktree, setPolicy } = yield* cleanupFixture().pipe( + Effect.provideService(FileSystem.FileSystem, { + ...fs, + stat: (target) => + fs + .stat(target) + .pipe( + Effect.map((info) => + store === "other-device" && target === storeDir + ? { ...info, dev: info.dev + 1n } + : info, + ), + ), + }), + ); + setPolicy("tracked-changes"); + const nodeModules = `${worktree}/node_modules`; + yield* fs.makeDirectory(`${nodeModules}/.pnpm/pkg@1.0.0`, { recursive: true }); + yield* fs.makeDirectory(`${nodeModules}/.vite`); + yield* fs.writeFileString( + `${nodeModules}/.pnpm/pkg@1.0.0/index.js`, + "clone ".repeat(10_000), + ); + yield* fs.writeFileString(`${nodeModules}/.vite/deps.js`, "cache"); + if (store !== "no-manifest") + yield* fs.writeFileString( + `${nodeModules}/.modules.yaml`, + JSON.stringify({ storeDir, virtualStoreDir: ".pnpm" }), + ); + const counted = [ + ...(yield* fs.readDirectory(worktree)).flatMap((name) => + name === "node_modules" ? [] : [`${worktree}/${name}`], + ), + `${nodeModules}/.vite/deps.js`, + ...(store === "no-manifest" ? [] : [`${nodeModules}/.modules.yaml`]), + ...(store === "same-device" ? [] : [`${nodeModules}/.pnpm/pkg@1.0.0/index.js`]), + ]; + const bytes = (yield* Effect.forEach(counted, (file) => allocatedBytes(fs, file))).reduce( + (sum, size) => sum + size, + 0, + ); + const report = yield* service.runNow; + expect(report.entries[0]).toMatchObject({ outcome: "removed", bytes }); + }), + ), + ); it.live("still removes a worktree when measuring its size fails", () => runCleanupTest( Effect.gen(function* () { diff --git a/apps/server/src/storageCleanup.ts b/apps/server/src/storageCleanup.ts index a4d1e831d900..4b82338b1f57 100644 --- a/apps/server/src/storageCleanup.ts +++ b/apps/server/src/storageCleanup.ts @@ -32,6 +32,7 @@ import type { PlatformError } from "effect/PlatformError"; import * as Schedule from "effect/Schedule"; import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; +import { parse as parseYaml } from "yaml"; import * as ServerConfig from "./config.ts"; import * as GitManager from "./git/GitManager.ts"; @@ -55,6 +56,13 @@ const decodeCleanupThread = Schema.decodeUnknownEffect( const decodeCleanupSession = Schema.decodeUnknownEffect( Schema.fromJsonString(OrchestrationV2ProviderSessionJson), ); +// pnpm 11 writes this file as JSON, older versions as YAML. +const decodePnpmModulesManifest = Schema.decodeUnknownEffect( + Schema.Struct({ + storeDir: Schema.String, + virtualStoreDir: Schema.optional(Schema.String), + }), +); const DAY_MS = 86_400_000; // After a settle, the project's settle script needs time to open its @@ -247,14 +255,35 @@ const make = Effect.gen(function* () { ); }); + // pnpm imports packages by clone or reflink when its store shares a device + // with the worktree. Those files have one link but share blocks with the + // store, so removing them frees almost nothing. A store on another device + // forces real copies, which still count. + const sharedPnpmVirtualStore = Effect.fnUntraced( + function* (root: string) { + const nodeModules = path.join(root, "node_modules"); + const text = yield* fs.readFileString(path.join(nodeModules, ".modules.yaml")); + const manifest = yield* decodePnpmModulesManifest(yield* Effect.try(() => parseYaml(text))); + const virtualStore = path.resolve(nodeModules, manifest.virtualStoreDir ?? ".pnpm"); + const [store, virtual] = yield* Effect.all([ + fs.stat(manifest.storeDir), + fs.stat(virtualStore), + ]); + return store.dev === virtual.dev ? virtualStore : null; + }, + Effect.orElseSucceed(() => null), + ); + const measureWorktree = Effect.fn("storageCleanup.measureWorktree")( function* (worktreePath: string) { const root = yield* fs.realPath(worktreePath); + const skipped = yield* sharedPnpmVirtualStore(root); const pending = [root]; let entries = 1; let bytes = 0; while (pending.length > 0) { const target = pending.pop()!; + if (target === skipped) continue; // Effect's stat follows links. Probe with readLink first to get lstat // semantics, including skipping dangling links and directory links. const isLink = yield* fs.readLink(target).pipe( From a1dc856ee61278d0199c36d4bcdef5fdea4d21fe Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 21 Oct 2026 00:34:16 -0700 Subject: [PATCH 07/73] build: upgrade Vite+ to 1.3 and cache typecheck (#22716) Co-authored-by: Claude Opus 5.5 --- apps/desktop/package.json | 1 - apps/desktop/vite.config.ts | 5 + apps/marketing/package.json | 3 +- apps/marketing/vite.config.ts | 15 + apps/web/package.json | 3 +- apps/web/vite.config.ts | 9 + .../core-graph-and-data-model.md | 22 +- .../thread-lineage-and-context-transfer.md | 13 +- knip.jsonc | 4 + package.json | 4 +- pnpm-lock.yaml | 1578 ++++++++--------- pnpm-workspace.yaml | 4 +- vite.config.ts | 5 +- 13 files changed, 797 insertions(+), 869 deletions(-) create mode 100644 apps/marketing/vite.config.ts diff --git a/apps/desktop/package.json b/apps/desktop/package.json index fded4eec9e41..c652ccabc768 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -7,7 +7,6 @@ "scripts": { "ensure:electron": "node scripts/ensure-electron-runtime.mjs", "start": "node scripts/start-electron.mjs", - "typecheck": "tsc-rs --noEmit", "test": "vp test run --passWithNoTests", "smoke-test": "node scripts/smoke-test.mjs" }, diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index 893d6f8e076f..65a6e7a2892f 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -43,6 +43,11 @@ export default defineConfig({ dependsOn: ["t3#build"], cache: false, }, + // tsc-rs rewrites the tsbuildinfo it reads, which keeps a task out of the cache. + typecheck: { + command: "tsc-rs --noEmit", + cache: { input: [{ auto: true }, "!*.tsbuildinfo"] }, + }, }, }, pack: [ diff --git a/apps/marketing/package.json b/apps/marketing/package.json index 6eb4a6e41f69..36dc282e89b4 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -7,8 +7,7 @@ "stage:install-scripts": "node scripts/stage-install-scripts.mjs", "dev": "node scripts/stage-install-scripts.mjs && astro dev", "build": "node scripts/stage-install-scripts.mjs && astro build", - "preview": "astro preview", - "typecheck": "astro check" + "preview": "astro preview" }, "dependencies": { "@t3tools/shared": "workspace:*", diff --git a/apps/marketing/vite.config.ts b/apps/marketing/vite.config.ts new file mode 100644 index 000000000000..f927357c438d --- /dev/null +++ b/apps/marketing/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite-plus"; + +// Astro reads astro.config.mjs. This file only configures `vp run` tasks. +export default defineConfig({ + run: { + tasks: { + // `astro check` regenerates .astro/*.d.ts and then reads them, which + // keeps a task out of the cache. Its real inputs are tracked as usual. + typecheck: { + command: "astro check", + cache: { input: [{ auto: true }, "!.astro/**"] }, + }, + }, + }, +}); diff --git a/apps/web/package.json b/apps/web/package.json index a7a7d1c2354f..8e2171192774 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -8,7 +8,6 @@ "dev": "vp dev", "build": "vp build", "preview": "vp preview", - "typecheck": "tsc-rs --noEmit", "test": "vp test run --passWithNoTests --project unit --project browser" }, "dependencies": { @@ -92,7 +91,7 @@ "@types/use-sync-external-store": "^0.0.6", "@vercel/config": "^0.3.0", "@vitejs/plugin-react": "^6.1.2", - "@vitest/browser-playwright": "5.0.1", + "@vitest/browser-playwright": "5.0.5", "compression": "^1.8.2", "oxc-transform-react": "~0.152.0", "playwright": "1.60.0", diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index f6fecbfceef3..8158f66367df 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -181,6 +181,15 @@ const allowedHosts = [".ts.net", ...configuredAllowedHosts]; export default defineConfig(() => { return { + run: { + tasks: { + // tsc-rs rewrites the tsbuildinfo it reads, which keeps a task out of the cache. + typecheck: { + command: "tsc-rs --noEmit", + cache: { input: [{ auto: true }, "!*.tsbuildinfo"] }, + }, + }, + }, assetsInclude: ["**/*.wasm"], plugins: [ devCompressionPlugin(), diff --git a/docs/orchestration-v2/core-graph-and-data-model.md b/docs/orchestration-v2/core-graph-and-data-model.md index ef7dda9c118f..056f22cfa248 100644 --- a/docs/orchestration-v2/core-graph-and-data-model.md +++ b/docs/orchestration-v2/core-graph-and-data-model.md @@ -231,7 +231,13 @@ type RunAttempt = { providerTurnId: ProviderTurnId | null; reason: "initial" | "steering_restart" | "retry" | "provider_recovery"; status: - "pending" | "running" | "completed" | "interrupted" | "failed" | "cancelled" | "superseded"; + | "pending" + | "running" + | "completed" + | "interrupted" + | "failed" + | "cancelled" + | "superseded"; startedAt: string | null; completedAt: string | null; }; @@ -374,7 +380,12 @@ type ContextTransfer = { sourceProvider: ProviderKind | null; targetProvider: ProviderKind | null; status: - "pending" | "resolved_native" | "resolved_portable" | "failed" | "consumed" | "superseded"; + | "pending" + | "resolved_native" + | "resolved_portable" + | "failed" + | "consumed" + | "superseded"; resolution: ContextTransferResolution | null; createdBy: "user" | "agent" | "system"; createdAt: string; @@ -468,7 +479,12 @@ type RuntimeRequest = { providerTurnId: ProviderTurnId | null; nativeRequestRef: string | null; kind: - "command" | "file-read" | "file-change" | "dynamic_tool_call" | "user_input" | "auth_refresh"; + | "command" + | "file-read" + | "file-change" + | "dynamic_tool_call" + | "user_input" + | "auth_refresh"; status: "pending" | "resolved" | "expired" | "cancelled"; responseCapability: | { type: "live"; providerSessionId: ProviderSessionId } diff --git a/docs/orchestration-v2/thread-lineage-and-context-transfer.md b/docs/orchestration-v2/thread-lineage-and-context-transfer.md index bf2a117fec20..aafaad5176ea 100644 --- a/docs/orchestration-v2/thread-lineage-and-context-transfer.md +++ b/docs/orchestration-v2/thread-lineage-and-context-transfer.md @@ -79,7 +79,11 @@ Thread relationship describes why a target thread or run exists relative to anot ```ts type ThreadRelationshipKind = - "fork" | "provider_handoff" | "merge_back" | "subagent_spawn" | "subagent_result"; + | "fork" + | "provider_handoff" + | "merge_back" + | "subagent_spawn" + | "subagent_result"; ``` Relationships are product-level and provider-neutral. A fork is not the same thing as a provider-native fork. A provider-native fork is one possible resolution strategy for a fork relationship. @@ -116,7 +120,12 @@ type ContextTransfer = { sourceProvider?: ProviderKind; targetProvider?: ProviderKind; status: - "pending" | "resolved_native" | "resolved_portable" | "failed" | "consumed" | "superseded"; + | "pending" + | "resolved_native" + | "resolved_portable" + | "failed" + | "consumed" + | "superseded"; resolution?: ContextTransferResolution; createdBy: "user" | "agent" | "system"; createdAt: string; diff --git a/knip.jsonc b/knip.jsonc index 31213c07e8d5..7fbfe3825995 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -86,6 +86,10 @@ // reports an entire component module when nothing imports it. "ignoreIssues": { "src/components/ui/*.tsx": ["exports", "nsExports", "duplicates"] }, }, + "apps/marketing": { + // `astro check` runs from the typecheck task in vite.config.ts, which knip does not read. + "ignoreDependencies": ["@astrojs/check"], + }, "apps/mobile": { // Expo loads plugins and the fingerprint config by string; Metro embeds the browser entry in the native WebView. "entry": [ diff --git a/package.json b/package.json index 3e90ec7948a8..53580ef428a2 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "build:marketing": "vp run --filter @t3tools/marketing build", "build:desktop": "vp run --filter @t3tools/desktop --filter t3 build", "build:resource-monitor": "cargo build --locked --release --manifest-path native/resource-monitor/Cargo.toml", - "typecheck": "vp run -r --concurrency-limit 2 typecheck", - "tc": "vp run -r --concurrency-limit 2 typecheck", + "typecheck": "vp run -r --cache --concurrency-limit 4 typecheck", + "tc": "vp run -r --cache --concurrency-limit 4 typecheck", "lint": "vp lint --report-unused-disable-directives", "knip": "knip --preprocessor ./scripts/knip-schemas.ts", "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-grok --workspace packages/source-control-core --workspace packages/source-control-azure-devops --workspace packages/source-control-bitbucket --workspace packages/source-control-forgejo --workspace packages/source-control-gitcafe --workspace packages/source-control-github --workspace packages/source-control-gitlab --workspace packages/source-control-testing --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53b971302acc..8c03fc7d0e3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -160,8 +160,8 @@ overrides: '@types/ws@^8': 8.18.2 undici@^8: 8.11.2 ws@^8: 8.22.0 - vite: npm:@voidzero-dev/vite-plus-core@1.0.0 - vite-plus: 1.0.0 + vite: npm:@voidzero-dev/vite-plus-core@1.3.0 + vite-plus: 1.3.0 yaml: ^2.9.0 packageExtensionsChecksum: sha256-EowbsmlBFaoO0Ypufw901RnvdyjKG5dcdk2FRbc7Dr0= @@ -222,8 +222,8 @@ importers: specifier: 'catalog:' version: 7.0.2 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) apps/desktop: dependencies: @@ -290,7 +290,7 @@ importers: devDependencies: '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 @@ -307,8 +307,8 @@ importers: specifier: 4.3.3 version: 4.3.3 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) apps/marketing: dependencies: @@ -317,7 +317,7 @@ importers: version: link:../../packages/shared astro: specifier: ^7.3.5 - version: 7.3.5(@types/node@24.12.4)(aws4fetch@1.0.20)(idb-keyval@6.2.1)(ioredis@5.11.0)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0) + version: 7.3.5(@types/node@24.12.4)(@volar/typescript@2.4.28)(aws4fetch@1.0.20)(idb-keyval@6.2.1)(ioredis@5.11.0)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0) sharp: specifier: 0.35.4 version: 0.35.4(@types/node@24.12.4) @@ -634,7 +634,7 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@pierre/trees': specifier: 1.0.0-beta.6 version: 1.0.0-beta.6(@pierre/theme@2.0.0)(@shikijs/themes@4.2.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(shiki@4.2.0) @@ -767,7 +767,7 @@ importers: version: 4.0.5(@effect/platform-node@4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6))(effect@4.0.5) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@modelcontextprotocol/sdk': specifier: 1.29.0 version: 1.29.0(zod@4.6.5) @@ -811,8 +811,8 @@ importers: specifier: 16.14.1 version: 16.14.1 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) apps/web: dependencies: @@ -1020,13 +1020,13 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@tailwindcss/vite': specifier: 4.3.3 - version: 4.3.3(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)) + version: 4.3.3(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)) '@tanstack/router-plugin': specifier: ^1.161.0 - version: 1.168.13(@tanstack/react-router@1.170.10(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)) + version: 1.168.13(@tanstack/react-router@1.170.10(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)) '@types/compression': specifier: ^1.8.1 version: 1.8.1 @@ -1050,10 +1050,10 @@ importers: version: 0.3.0 '@vitejs/plugin-react': specifier: ^6.1.2 - version: 6.1.2(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7))(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(rolldown@1.2.13))(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(babel-plugin-react-compiler@1.0.0)(oxc-transform-react@0.152.0) + version: 6.1.2(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7))(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(rolldown@1.2.13))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(babel-plugin-react-compiler@1.0.0)(oxc-transform-react@0.152.0) '@vitest/browser-playwright': - specifier: 5.0.1 - version: 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(utf-8-validate@6.0.6)(vitest@5.0.1) + specifier: 5.0.5 + version: 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(utf-8-validate@6.0.6)(vitest@5.0.5) compression: specifier: ^1.8.2 version: 1.8.2 @@ -1067,11 +1067,11 @@ importers: specifier: 4.3.3 version: 4.3.3 vite: - specifier: npm:@voidzero-dev/vite-plus-core@1.0.0 - version: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' + specifier: npm:@voidzero-dev/vite-plus-core@1.3.0 + version: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) infra/relay: dependencies: @@ -1104,7 +1104,7 @@ importers: version: link:../../packages/shared alchemy: specifier: 2.0.0-beta.80 - version: 2.0.0-beta.80(patch_hash=bea1b6c0c0b23157fd5f439a1d7feff430f28ac1621ba135d8a13eeec60ccacd)(c218479f13e455b978e1beb907da1b95) + version: 2.0.0-beta.80(patch_hash=bea1b6c0c0b23157fd5f439a1d7feff430f28ac1621ba135d8a13eeec60ccacd)(d214a55921b9b1c12454f5ddcf93cf1a) drizzle-orm: specifier: 1.0.0-rc.5-ab785fc version: 1.0.0-rc.5-ab785fc(@cloudflare/workers-types@4.20260604.1)(@effect/sql-d1@4.0.0(effect@4.0.5))(@effect/sql-pg@4.0.5(effect@4.0.5))(@effect/sql-sqlite-bun@4.0.0-rc.112(effect@4.0.5))(@effect/sql-sqlite-do@4.0.5(effect@4.0.5))(@electric-sql/pglite@0.3.15)(@libsql/client@0.17.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(bun-types@1.3.14)(effect@4.0.5)(expo-sqlite@58.0.13(expo@58.0.14)(react-native@0.88.0(@babel/core@7.29.7)(@react-native/metro-config@0.88.0(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0))(pg@8.23.0)(valibot@1.2.0(typescript@7.0.2))(zod@4.4.3) @@ -1120,7 +1120,7 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 @@ -1131,11 +1131,11 @@ importers: specifier: 'catalog:' version: 7.0.2 vite: - specifier: npm:@voidzero-dev/vite-plus-core@1.0.0 - version: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' + specifier: npm:@voidzero-dev/vite-plus-core@1.3.0 + version: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) oxlint-plugin-t3code: dependencies: @@ -1151,10 +1151,10 @@ importers: devDependencies: '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/client-runtime: dependencies: @@ -1197,13 +1197,13 @@ importers: devDependencies: '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/three': specifier: 0.180.0 version: 0.180.0 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/contracts: dependencies: @@ -1213,10 +1213,10 @@ importers: devDependencies: '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/effect-acp: dependencies: @@ -1232,13 +1232,13 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/effect-codex-app-server: dependencies: @@ -1254,13 +1254,13 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/provider-acp: dependencies: @@ -1285,7 +1285,7 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 @@ -1293,8 +1293,8 @@ importers: specifier: workspace:* version: link:../effect-acp vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/provider-acp-registry: dependencies: @@ -1322,7 +1322,7 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/provider-testing': specifier: workspace:* version: link:../provider-testing @@ -1333,8 +1333,8 @@ importers: specifier: workspace:* version: link:../effect-acp vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/provider-core: dependencies: @@ -1353,13 +1353,13 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/provider-cursor: dependencies: @@ -1390,7 +1390,7 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/provider-testing': specifier: workspace:* version: link:../provider-testing @@ -1398,8 +1398,8 @@ importers: specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/provider-grok: dependencies: @@ -1424,7 +1424,7 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/provider-testing': specifier: workspace:* version: link:../provider-testing @@ -1435,8 +1435,8 @@ importers: specifier: workspace:* version: link:../effect-acp vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/provider-muse: dependencies: @@ -1461,7 +1461,7 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/provider-testing': specifier: workspace:* version: link:../provider-testing @@ -1469,8 +1469,8 @@ importers: specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/provider-opencode: dependencies: @@ -1504,7 +1504,7 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/provider-testing': specifier: workspace:* version: link:../provider-testing @@ -1512,8 +1512,8 @@ importers: specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/provider-pi: dependencies: @@ -1535,7 +1535,7 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/provider-testing': specifier: workspace:* version: link:../provider-testing @@ -1543,8 +1543,8 @@ importers: specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/provider-testing: dependencies: @@ -1630,7 +1630,7 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/mdast': specifier: ^4.0.4 version: 4.0.4 @@ -1641,8 +1641,8 @@ importers: specifier: 'catalog:' version: 2.0.2 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/source-control-azure-devops: dependencies: @@ -1667,7 +1667,7 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/source-control-testing': specifier: workspace:* version: link:../source-control-testing @@ -1675,8 +1675,8 @@ importers: specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/source-control-bitbucket: dependencies: @@ -1698,13 +1698,13 @@ importers: devDependencies: '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/source-control-core: dependencies: @@ -1720,13 +1720,13 @@ importers: devDependencies: '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/source-control-forgejo: dependencies: @@ -1748,7 +1748,7 @@ importers: devDependencies: '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/source-control-testing': specifier: workspace:* version: link:../source-control-testing @@ -1756,8 +1756,8 @@ importers: specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/source-control-gitcafe: dependencies: @@ -1779,7 +1779,7 @@ importers: devDependencies: '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/source-control-testing': specifier: workspace:* version: link:../source-control-testing @@ -1787,8 +1787,8 @@ importers: specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/source-control-github: dependencies: @@ -1816,7 +1816,7 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/source-control-testing': specifier: workspace:* version: link:../source-control-testing @@ -1824,8 +1824,8 @@ importers: specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/source-control-gitlab: dependencies: @@ -1844,7 +1844,7 @@ importers: devDependencies: '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@t3tools/source-control-testing': specifier: workspace:* version: link:../source-control-testing @@ -1852,8 +1852,8 @@ importers: specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/source-control-testing: dependencies: @@ -1891,13 +1891,13 @@ importers: version: 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/tailscale: dependencies: @@ -1910,13 +1910,13 @@ importers: devDependencies: '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) scripts: dependencies: @@ -1950,7 +1950,7 @@ importers: devDependencies: '@effect/vitest': specifier: 4.0.5 - version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/plist': specifier: 'catalog:' version: 3.0.5 @@ -1967,8 +1967,8 @@ importers: specifier: npm:typescript@~6.0.3 version: typescript@6.0.3 vite-plus: - specifier: 1.0.0 - version: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + specifier: 1.3.0 + version: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages: @@ -4900,8 +4900,8 @@ packages: cpu: [x64] os: [win32] - '@oxc-project/runtime@0.151.0': - resolution: {integrity: sha512-fNuliiqGTseerW2FTbxq6UOQavKL/NvNh9K3peJZZVNi1NbM4Ehscwp3MX9nu+9bfbSfmPUEivF779XQU5exiw==} + '@oxc-project/runtime@0.154.0': + resolution: {integrity: sha512-cBQNGoh659BNgvA8OC/e4BNDtFkv03dJaYdkp2adgOQNtISNAPQ+nB4S8XvWtNk7Xl+0g+7aa+iRGR25Xycd1Q==} engines: {node: ^20.19.0 || >=22.12.0} '@oxc-project/types@0.127.0': @@ -4919,12 +4919,12 @@ packages: '@oxc-project/types@0.148.0': resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} - '@oxc-project/types@0.151.0': - resolution: {integrity: sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==} - '@oxc-project/types@0.153.0': resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==} + '@oxc-project/types@0.154.0': + resolution: {integrity: sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==} + '@oxc-resolver/binding-android-arm-eabi@11.24.2': resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==} cpu: [arm] @@ -5150,276 +5150,276 @@ packages: cpu: [x64] os: [win32] - '@oxfmt/binding-android-arm-eabi@0.70.0': - resolution: {integrity: sha512-Xd7YO4/T2axEj6FTLcj4Why3mTBqFMg+x24xtorT4Lb2+1g82090GH0a/4U1m0pABGYiix2bq1pqkYrmV3f0Sw==} + '@oxfmt/binding-android-arm-eabi@0.73.0': + resolution: {integrity: sha512-dhunAdMbi/4o0Y66/+Wl6rpsNqpvE4i26Qw6bB77A97L2lJoktWg6m+X9QOon8iOEEqzD9rEZqFi5uZ4oIzhCA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.70.0': - resolution: {integrity: sha512-x9rlMYyKXdgKdYyUJzGsK1ZV8P4di/J32ipzcS6Jet6p9r9UAh28neXIMtdlSaJJycdi61Z4YkcLKLpk8ueFjg==} + '@oxfmt/binding-android-arm64@0.73.0': + resolution: {integrity: sha512-dBoypjC2/xeyKyIhN2waX0/XeoccH9k0lVVe+sGRc4X8HSJVRtqyZDoXdr7bGY9A75c51dX0z0dLDpGQKwCdCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.70.0': - resolution: {integrity: sha512-IUTUPvrBVYy7POh4stXzRdz4IVC/1QSaviCWoyenSlOhGu0X9j5K07vCTM9biLjAA2Zs31l0Rj5vvRpj9n95wA==} + '@oxfmt/binding-darwin-arm64@0.73.0': + resolution: {integrity: sha512-Tn7Y+qWIpCQK+qdYwBcn6YvqeBuPiuG3tcNlYg16a5QL2o3rSmrgBB1nCyYqo0uvc1LNrfQW9/CK2tzFuID/Fg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.70.0': - resolution: {integrity: sha512-vw745q870oTd6J517O24asoX4/E+eK0nxYIFoedSLqgJ+nI5En7+ZS82iZSHZ69zevQrnOXiyHP01dA+t8xD8w==} + '@oxfmt/binding-darwin-x64@0.73.0': + resolution: {integrity: sha512-yVb0E0tg/+9Xc9YJCBq66qF1DC/aN5mhb5nnHz0k4ikwkMANtSul2VPDSwjYdXR6/DFimc+pPUgLBqkIa5Ctuw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.70.0': - resolution: {integrity: sha512-NO14EgSM9dFkcg+MfGPxvsKqXYs9LKaxPrOKXpv1R0rLokGGFDcCq6dBMq18dE4wlpFOovX0UZY2uh1P30O7QA==} + '@oxfmt/binding-freebsd-x64@0.73.0': + resolution: {integrity: sha512-IKR4lFnBkYNL9Uqdxa5GMTQLvnLj+3sDwTvh2atHCzah0xeo+1xR5bigRU8uyXlOsA4VpXUn0UAimq4E4RBGmQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.70.0': - resolution: {integrity: sha512-139OEhHarj9CYoJ/i9gXlPv4KLBGtLj2toseOWYFf09QwlhklaZk+wW3aOvlqoeZtuayvkoSNVWra7WJ21s3VQ==} + '@oxfmt/binding-linux-arm-gnueabihf@0.73.0': + resolution: {integrity: sha512-7qnZ4g0ZCO3aZevOOQpZIABYwwXJW1lLDo2elviGdPQ0i11255xKEMFsi97fC1/nauQFseCJfELUy/k2JoOP+g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.70.0': - resolution: {integrity: sha512-GEh2PY3IWTE0M24eNhTduountANSbWyDmMnzFSQE/nGg/bjPugbUgiGuFu+xdqcQSd/HKSwH80/F2yVVD48yhA==} + '@oxfmt/binding-linux-arm-musleabihf@0.73.0': + resolution: {integrity: sha512-36eBico2j/WPB84gb4FYd/jjqGCKOwU4M+TWLBUIqlnzTozQueLy0joA5BwsSp2OZFVkOlpCwvA3ndPamdiyLw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.70.0': - resolution: {integrity: sha512-En5i+UJmZSPxuSf47F2Hl5YOzKB0bicQLnGQkeTCMQ35cWLtbrSwACJKfiLqRZrk05DwSnsJkhBRaM3OURtIaA==} + '@oxfmt/binding-linux-arm64-gnu@0.73.0': + resolution: {integrity: sha512-VZ3cSgCAT18knKo52DcACmAuIoqG6qVEyeZRFbJd0xmYQ8GyYjuBc6IAh6MZHijT6q7p4/HPI+6ZWsCuhdV6iw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.70.0': - resolution: {integrity: sha512-WWOoV5W9Im3flVwOVrWn/2DUlOF8v5vcCip+kcNuaMpulRCh6nzzt1Su2vcL2F908YJIXNV3HvegbBHuyLwKHg==} + '@oxfmt/binding-linux-arm64-musl@0.73.0': + resolution: {integrity: sha512-8YVzH59XSHcqwXNgc5qK851zCqwpmBgOifARXEPPe5/xTsIbHceYoPZAyqfpYV0CfKmzSaEDBgYHFza4rhx+Zw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.70.0': - resolution: {integrity: sha512-YUouneIqW+5n7aE8xx/zeZ6/utr/KH7oykcGoFyd8Uz8uh591T1oKlnoWA3BsRq/ZR42oY1w4MUYvS/0e/MQOA==} + '@oxfmt/binding-linux-ppc64-gnu@0.73.0': + resolution: {integrity: sha512-MqRJnmCys0rwOgpWyEidSA7v/bvOqfNDJhPkQoR9mPVYrvSmMaW2Xhdpl28cbTA+brOM1is62GjIvJplVlUVpw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.70.0': - resolution: {integrity: sha512-iEnMf21S5aGVa4hViDGY8sAQ/AHyCu2JPyrQF8P06wtHhSkD1YJBeT4m/KiGewgf7+a5XCYSCRIPcRQa1xwEoQ==} + '@oxfmt/binding-linux-riscv64-gnu@0.73.0': + resolution: {integrity: sha512-4t2F3SWJSK/O/kRRFnL0elJzn75fJGGu71gs1HtiWcS80SmJOpkHiwwLnTp9suGdns8VHEPxYS0GTcyVwHKWBA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.70.0': - resolution: {integrity: sha512-91Sdniaj20fQzyMeCxMDzTP4c9s4RB8dGQ308xHhDR0n6U7+1Xq7N9klE7mfXq8iV3lRmIGSXi5X23Hn/0XX/g==} + '@oxfmt/binding-linux-riscv64-musl@0.73.0': + resolution: {integrity: sha512-D8b4I8I1kLdJQFwdFXNV+cwh3xWSiNKofjdGUk0nDpc0AHOds8T+poGbP9RWQMvgsvMb9OMgp/aV7ahBR37TbQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.70.0': - resolution: {integrity: sha512-uUV30M6E+2TKKGMaKiwfeL4RZrviHXlUxsrYJ/jFBb+1EZy+pnFT+hF73eeWdzh5NqPOAnw0iiMAIqjqiLZPFg==} + '@oxfmt/binding-linux-s390x-gnu@0.73.0': + resolution: {integrity: sha512-QrC1S5VL1+N5rvorB15jH7CPvjVHA7GyUI5oVEd8Bd9m5605RDBQYTxDQ920DuylRoZOh2V7/xXpfCtFUrojUQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.70.0': - resolution: {integrity: sha512-ivMcX6kNDPhqtbOaBt/ItFlLlTlXNHLgRuNmxP6Na6UuYXRT10llpJcAPbGeRgjjb3Qzv4jwPp3fB0hui50WNQ==} + '@oxfmt/binding-linux-x64-gnu@0.73.0': + resolution: {integrity: sha512-dFcUKQPltCEPa+YlkF3zqufn6nJPZdG73LpnjBvZFFsU3laCMssDXSYB99mVaj43FqdEgjpz5TO6gbYXcczd9g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.70.0': - resolution: {integrity: sha512-w+S+fERxYmlZSyZlJK/U292FjyBoH8cCEj21/tYJX6atX5kNSn+HDkhlFQKT2zcMwUW0uAtUL/bOrlwJZwqRdA==} + '@oxfmt/binding-linux-x64-musl@0.73.0': + resolution: {integrity: sha512-LmQe+nV8e+eUwfPNCxDF+owbsg88toBiR/8rZRgVOeKQP3MaGefXZRvBoc1UG37iWEQ/9JlCXG9Mc4nf4N3agw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.70.0': - resolution: {integrity: sha512-Zlom1Xkx257R8bk4ZI4zJsrGno2opknz1+5v5baka3nn4FPyvNSdh8JUL4CdN1S1OWRMvJ9UJQ+RfIqGGCUEfA==} + '@oxfmt/binding-openharmony-arm64@0.73.0': + resolution: {integrity: sha512-8weOr1OKknE0G2DbR6ejJUT9AOsoBU6cXlsetmbihHv1K+bK7qP61T6lJxY0k/aAO7GyOJiRGNlL+04r4eH8Xg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.70.0': - resolution: {integrity: sha512-FQgPW5R17vzt7cgrJ8eG/dqX00o2xHsqFeLfw4xzA9FRHpN/DjFo9YDonvIIXGxiEuS9F/jZPnGO+KHNKuCo4Q==} + '@oxfmt/binding-win32-arm64-msvc@0.73.0': + resolution: {integrity: sha512-mZV+QxXYgb8E18gzAFItb937VIsE4dqWdStrD3tRRC52UubOYEuU+e7NaIqUsGdB5YsJqNeuck3Ue/af2r9wyA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.70.0': - resolution: {integrity: sha512-ZfZublNhZ+XBndMiXhkiLlPE+XyGRDa0CweeTL6t1fZypfCh1LTg7e5CvnOeTBunq15MskOcRempumSPGAaCQA==} + '@oxfmt/binding-win32-ia32-msvc@0.73.0': + resolution: {integrity: sha512-QJPUsbgUb97ZPPMkFUq6VQq0fLhC4Np19KL9aB/qKVwXnpKE6MIiILXqzhKnQ/UbnEDjKJ7Xv1d6zH4bYPCUdQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.70.0': - resolution: {integrity: sha512-HlIZEn+WzLQL0DszNzldiRl/DPRCX5R0Vkt6qeUPR1YHwy52hZZo4x6HoTOVmKRP2wUiwPGtKsihNY/f8KRaBg==} + '@oxfmt/binding-win32-x64-msvc@0.73.0': + resolution: {integrity: sha512-cGcze2fu+ykIaFtkEvZlkZ3HvnCLk+WISthtq99zWTWn0x13t5L9TxvGdLZMUMvtfYR+ICkiCwKnguVubdcw0w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint-tsgolint/darwin-arm64@7.0.2003': - resolution: {integrity: sha512-TgV33rXr6ueXBwvc+0nssUkTBSXHJxv77I8p4RCjDjCnvexHtmoPiudVRDfj3S3puMDdeQdHW6jdUgUPy3nr/w==} + '@oxlint-tsgolint/darwin-arm64@7.0.2004': + resolution: {integrity: sha512-uJk3ttju7Lz+Nv8WG4CsLyewoL1qf/Ny8S8OksJ7Bm/F14Tc9gMfW6arM1PCc57IzH/X/I79YsCWTgxX1FKibQ==} cpu: [arm64] os: [darwin] - '@oxlint-tsgolint/darwin-x64@7.0.2003': - resolution: {integrity: sha512-hY3FMAjIaPDdK3FNxyRXRfHPOFeoBn6dmnLyKZgQ2IbTTD1adXhl6PfCIqHhCPvurigv7nf7mYuWZZ19MmJzmg==} + '@oxlint-tsgolint/darwin-x64@7.0.2004': + resolution: {integrity: sha512-0BFfHTzDxNDFPcu0UZAXT6CyCJHAXM0dZveyDb4KEHa2qF/DSnOKOrmvK/wFuFol9kvijbGM9JfuOsor+zWXRw==} cpu: [x64] os: [darwin] - '@oxlint-tsgolint/linux-arm64@7.0.2003': - resolution: {integrity: sha512-eAET4JpyfBbg8SO0K74o4R55tEjVC292pIyxGOw2XGf8x/HrPNyxwQ478jMYOM54FIVOHc8sJ6VRHxluy6lucw==} + '@oxlint-tsgolint/linux-arm64@7.0.2004': + resolution: {integrity: sha512-TAehQc+qdRXyDN3nl/r/3/oPrBVrbtrDeGMkFyG6mf+XphpoJ2lAMVFLclyvrVZCw02RbKROjyw8h72XwQQHcw==} cpu: [arm64] os: [linux] - '@oxlint-tsgolint/linux-x64@7.0.2003': - resolution: {integrity: sha512-GXdyO/XyqDJ3s/llR/oOktLsYNjZtWSQBy0JMc+/0gsNPvTcseKPhn9c6KcFyWmnr6H4vljYALbujonqSzzEGw==} + '@oxlint-tsgolint/linux-x64@7.0.2004': + resolution: {integrity: sha512-LPunZvm6ZbWPfHMAyfZf9ci3HHvNEzLejO8zdqO3GoVGoSEHqqufHUD7+OgeatVSaJP7uXDJ2HaNOHIoH7go4Q==} cpu: [x64] os: [linux] - '@oxlint-tsgolint/win32-arm64@7.0.2003': - resolution: {integrity: sha512-TWauXnPfet0VgpmrstoAK58eJ4gbuwPUuUTQmYQAzE/RG1CpnxXtrKUJULf6lyerPE4KN3gM+DflIA1hOH+38Q==} + '@oxlint-tsgolint/win32-arm64@7.0.2004': + resolution: {integrity: sha512-FVWplzDImWhiuawnIZVETaQecv4GWom6Jv4Fx2xj7hhXXDZ5okEgiw1gTCImHTIQsi2ycC5oSxVwrn69bMh7iw==} cpu: [arm64] os: [win32] - '@oxlint-tsgolint/win32-x64@7.0.2003': - resolution: {integrity: sha512-DoRmfe7j8VqNlukp8liRVW45GQDhzRccNenjD/pdzelgtffW47pCMd1xbJLkPaPbKnTwID3onn3VZlL7JbebEA==} + '@oxlint-tsgolint/win32-x64@7.0.2004': + resolution: {integrity: sha512-7PsxuUx11Ry9DYTWcljTzRbmZY9B6O9fFFfZV+aodQEMy6pfRT3fhoQRv9PtGN/89z1pPz3nFKrsFm6WqNtX7w==} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.85.0': - resolution: {integrity: sha512-q2KO/Zso9UT+OMn0NF9ywn4E4t0MI3yxiDhNyhsQ7DyQJrC4FhFE4TXOi4bktFnOWXTMds8qZSbpv2XwRaNOBg==} + '@oxlint/binding-android-arm-eabi@1.88.0': + resolution: {integrity: sha512-k6Fgkf86ubfeH4GvqYsRolfuVAp0pfYVZvifSlXNHPdMaWUOfxUrmLEeDD7hF6U2oJuhsVxhSWq86/xCJ+9MmA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.85.0': - resolution: {integrity: sha512-SxLN3ALjoT9NNdvpjEevGeHvfzTAFrF0NBYB5tzK7/GtCKMze3j1e/m/X2ozqGj2U9hfGG/dg/OG8vpVK4PiDA==} + '@oxlint/binding-android-arm64@1.88.0': + resolution: {integrity: sha512-AsIyhl9o1ejwtac7NMbhKpMdSml6NWR0ljKjZERxC8OHYDAW8o/Om9n1QHVLi4VCEUo42Zte2/Op9kyoGpJZMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.85.0': - resolution: {integrity: sha512-Y/Sup/J4f0f9UGsSd/xyCNTeWL+gepO63GBdEDAfue9nBsnk9zMmnIXx1O6b1V8C90vB5nucYNZ0pbMXAp8zJA==} + '@oxlint/binding-darwin-arm64@1.88.0': + resolution: {integrity: sha512-qgBjB7eEKcCB4RqjXGuoSZq74g0wD09fWFJ+HWcCJcKaga8TeUHtb+BY0SsxdleSLBgx4RZ7qmgo0EzqEq7JoQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.85.0': - resolution: {integrity: sha512-ApOSNC04ynpDTwvBD+//0wyfODRSbEzvRoKpX8teffmc27z8AockwSNeMXGJXn5KP85eahDgR/2llICWLkzcnw==} + '@oxlint/binding-darwin-x64@1.88.0': + resolution: {integrity: sha512-sCz3X5Mw/xdzEzn4fsCt1Iq8NKJnGNfhR+b9B5k+4DysYAHsmIhYoocnBPiQ9kvY3+pUun2A0Z8Vtun/Nv6YoQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.85.0': - resolution: {integrity: sha512-bNrVrCOA/kHky3Tu79IXWXe5bhIgLXfUuUEDHlAGOHUk96MkvDZ1ecaQF19rwstrnaqfP1o9nBTqzIr9+ZHkUg==} + '@oxlint/binding-freebsd-x64@1.88.0': + resolution: {integrity: sha512-xTOdYOIqCufgIj0knvxu86FBfovP6QBiD7ngNva01gb8xPQCD5gXkE5APGczH35zStim1O3zqvFbLQmY85xXRg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.85.0': - resolution: {integrity: sha512-NUrzOJ1s/EqsVvfn2L/1D8Wro2LPIZUbihL8kOJLh5fEdGEN3rdOGUYq3HwnUIL8sjpoP+4N6RaGrgmMJnaMPw==} + '@oxlint/binding-linux-arm-gnueabihf@1.88.0': + resolution: {integrity: sha512-BLadtw88NdYJBrAtC1dYzrPfSP2d5zgx6iuZycdD0YmZBJ2Nqbit9Z4PSaIb6gxK25dweArGMPbOmq/0ugwUPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.85.0': - resolution: {integrity: sha512-UJXrAT3E/RWkEqXLIs2ehETja1qfgkPb+5gwLIIS+o/6cf+grHvoOXTa5997a/YNQfcJS0DRBTOfZt95cvOI1g==} + '@oxlint/binding-linux-arm-musleabihf@1.88.0': + resolution: {integrity: sha512-wILSczl/DaZsCcf7j7tvNbssp8hxEHIJxrv4j7+JjI3TAL1U/JTWbeUtoJJC/vo8+2hkTmHLYoKJjmiNbijdXw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.85.0': - resolution: {integrity: sha512-lK40QLjI0HxigO7CjDDshEtfYIeiYS0020v5BHFPqN4uuQBQxd2K9LNom2dW15o9F1937quSCRVp4ZsVhdbYdg==} + '@oxlint/binding-linux-arm64-gnu@1.88.0': + resolution: {integrity: sha512-jutT2Hy8JCuZuMwIFHo2HtyxfOfBJHyQxlBHBK1SL9W2f8n/WhjAP0FnDMz9d/mjhv9+QA0mx5AdQ9fDjsrFFg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.85.0': - resolution: {integrity: sha512-c2zbdBwGKreHXwRx3gWBuFGJxLhxgsg6YlZ+3H+RgRusU/UEV9jNwJ3HGYK+nRo0LvBa7mt6Kj86xoVotUo8cw==} + '@oxlint/binding-linux-arm64-musl@1.88.0': + resolution: {integrity: sha512-8N8iGV9hh4Lkeuq7QsEkjoXNRRflqR1G2qyoGYXcA0k5qllrdqmfylKEXyHIVERxqNDyMO4StqDQ0uLxXcBQAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.85.0': - resolution: {integrity: sha512-tlt/Hy8lZ97/lCPmCgw/B3k/mwh+BzaIPbPkldZEly7TwLmx0xe2CQcaW2g/rR0dOgS9JNGCZsMEqLhUNMGvaw==} + '@oxlint/binding-linux-ppc64-gnu@1.88.0': + resolution: {integrity: sha512-5ydcQt3znNQ/rGEWYRvpS2cK9+T18P2VYgnyJ+eloEAwi9YUmsXy5VGrRqNWIK6qzWwHGnEE3ojVqqQNo+f0hA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.85.0': - resolution: {integrity: sha512-3tNR9Xey82X0zKuY1d8hJ6Rc9gwRDurmqGLnQZa5xqOXy8/YyiqFXjAtugkKLY82obOlpK1eSiDRlgcNPuxtIg==} + '@oxlint/binding-linux-riscv64-gnu@1.88.0': + resolution: {integrity: sha512-iGx0uvFT54b/5DPJdEBCxYIrVB3v0uTy+JhRh3CyEp8/22BXDkG8rffkFwREdLsdj9mOrPbJYVOZw8ipcBuzjw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.85.0': - resolution: {integrity: sha512-wbGRd5PqCcjkJFHhZuZ2OBSUQY9czlQsoA/cQQB9JK/L9mC5MQgGoKAh+xd8QjA5V+0D3j+Qd1lAWn1I8zlelA==} + '@oxlint/binding-linux-riscv64-musl@1.88.0': + resolution: {integrity: sha512-Wt47mKMWelVjRjuR7BCFxXkB0m79K1sTEbqMHRWciKUgo5iv7sWsF4RXSOyFYHD9zerNrkRZblnN273rhwOA1Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.85.0': - resolution: {integrity: sha512-3Sn0kSrE4DPZCWV/8o+n4x3aFZxI9ulMnkYlwCbJ8eUVkwRK2IerohE/A/z3SNbCwoPFOCJmGE5Avrq0rrvdvQ==} + '@oxlint/binding-linux-s390x-gnu@1.88.0': + resolution: {integrity: sha512-g3L12KCS4+1gtUt3RbJ4F+Yxx4LNmmZLEPfhd8rAHEJjJk2lAgNfnjGoP/phJyhG5WS6VbNYbJtadJelNfvTMA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.85.0': - resolution: {integrity: sha512-JY2pxxYfB62bAGfejljVCqc44etItehPuAyaeSAdMuEMtwNA00ggMnS66lC1oIhos6oOXUkuU6mZ9bpFh3BqWg==} + '@oxlint/binding-linux-x64-gnu@1.88.0': + resolution: {integrity: sha512-og/XHVDOD9ZJgRiZkT9DgzQd4sr+JZW7e4l17Q/jVMZiIbeEd4uIu9iAoUbTF1gIvVZoRc4QDOxyVUvQGp4WHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.85.0': - resolution: {integrity: sha512-5k74vZ6qJBjBHEOlBk9B/iv68Yu0F1Afw/vvT2ar6OGCqEeXLaSjXz2n/IPCbhLG22UoKoYEJTzpYraRdcp6PA==} + '@oxlint/binding-linux-x64-musl@1.88.0': + resolution: {integrity: sha512-EmhBsp+xuKb6YbZNEMLBo80Xw71AZOIm18do8ni/GLX/Lw1XzNhVYpbxO1KasYtoAvrGtCOdCPULRlIE0bPbTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.85.0': - resolution: {integrity: sha512-GbAl5qt5TCkPLXTaIISZJnugrcBhra6rodcXc9jYt620UtdsTt71NlNmJmm0frxzFpd54x/G+MkitEJA8I/BoA==} + '@oxlint/binding-openharmony-arm64@1.88.0': + resolution: {integrity: sha512-DBUcx/KwYdg1DAz+G01P/GFv512J04EsZadbHptx2FTpWT/MWRg21ZToNHt9pnsfQRBQ3et/g+vqzbvrdAVbuA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.85.0': - resolution: {integrity: sha512-kjmws5MK0et2swk4ND85D7NVQyDHw162i6whtZDLUA/lo6FQyBZDcmMRCMcVZcNrAhIaftVb00x9ChGDOjjNJA==} + '@oxlint/binding-win32-arm64-msvc@1.88.0': + resolution: {integrity: sha512-xeVd0n4XG5HvynxfYvXi6uB+JvZbVZataowPS8cYpMbYg9mwE4sSyElFQd4LVJFifq8bRLDSR18XgEildmcXAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.85.0': - resolution: {integrity: sha512-eSsIJx9n4yxvOqYTZyPEMyEXRmE60XH7xGAU7i0Qbsn1lf6Za3CWJ9aRd82oSFKXaxhp+sA6/yMJVRIpLpna6A==} + '@oxlint/binding-win32-ia32-msvc@1.88.0': + resolution: {integrity: sha512-MA6iOw//amleaz2mVMsZrA7NHlEexjTrhIxSMFCXQi6IUuQO+3Od00GZhr2iCYw6RFzMW36jkKK/WHBXMsoQAw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.85.0': - resolution: {integrity: sha512-pBebIPUpKKhWrhSMWhy8TdAZBewiXnfxmaAGxhzxM1068GagqFaTwgKlU6e+UyJ2sPR+VoHouhXuGJkQjsrDvA==} + '@oxlint/binding-win32-x64-msvc@1.88.0': + resolution: {integrity: sha512-3Sz6rVWe8Dy+ufsXurFN1GX2dX43F/lQMSLelK9gFjiba7nHvLlqlhR5cHlWVnCpSqUY1hN9pyCX9XToe6ve6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -5428,8 +5428,8 @@ packages: resolution: {integrity: sha512-titLmukUt/h8ho7Svlf0xSBjoy2ccZKrXjpXpZCj+v6V4CJccC2KyP45BLSCMx8YIpifMyiDyUptM4+5sruKbQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@oxlint/plugins@1.79.0': - resolution: {integrity: sha512-S0uyoxakDINJ4DPgqxGlEEvrdSMeQb7Z2lKVjxoY2gwsbZbfg2Xr8Klfeo5ZeraHmmdBCELFUHkSe6KEmBpMvg==} + '@oxlint/plugins@1.88.0': + resolution: {integrity: sha512-G2xbfWTrbqYRhR/vfeKNMQf/sS3/ti0iKJAAkRxHuXWZH/hne/dyIb9QKk5LuR6cJLARxsL6FMoAYmfspd74MQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} '@peculiar/asn1-schema@2.8.0': @@ -6335,8 +6335,8 @@ packages: resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==} engines: {node: '>=20.19'} - '@testing-library/dom@10.4.1': - resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + '@testing-library/dom@10.4.2': + resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==} engines: {node: '>=18'} '@testing-library/user-event@14.6.7': @@ -6863,26 +6863,26 @@ packages: oxc-transform-react: optional: true - '@vitest/browser-playwright@5.0.1': - resolution: {integrity: sha512-BlknN81ZeoCyP5CSm5u7+1BTlS7d0cDU5DfndNSjEC3SmbzOESGrp+DQpNhQmTlggawWc+yKVEM03mM/aBynqg==} + '@vitest/browser-playwright@5.0.5': + resolution: {integrity: sha512-lcdThFUUcMV9mGGV1SUiZXPxfjgW4CssA7o3STxzh8WK0tT2r526kc12tZIBRZfoLMwkhxjLmMXkWXONzdsDQw==} peerDependencies: playwright: '*' - vitest: 5.0.1 + vitest: 5.0.5 - '@vitest/browser-preview@5.0.1': - resolution: {integrity: sha512-NagNnZD6A8ccl+pycLWX2b/e9YJtH5NuDUyiiadQjptMRuZVBAB9r9I3itdKm6exxtcl+rOTv2rWt6vu5uOm1A==} + '@vitest/browser-preview@5.0.5': + resolution: {integrity: sha512-ViNg4UlomGSJHtnnr7Txm+55P7/aAMMoS+1WYnWjp5w5tzf7YEUAICcs2sb1uMovEnC8el/8zUfkWZ8+7fPCKA==} peerDependencies: - vitest: 5.0.1 + vitest: 5.0.5 - '@vitest/browser@5.0.1': - resolution: {integrity: sha512-s2UroEhP2BZPoen1qrXXTtVqnUcs6y2sxMLGCZ8tZDkH2iM/6tTC2GVTPCgF+oVF3X5M6wC/X2O0MvvoHNmH0Q==} + '@vitest/browser@5.0.5': + resolution: {integrity: sha512-IDMoZX/iZ8rkilw7weaSgN8tJ7iZ9hUv0wYoKXyiyjWjWFwLSWAY707DcVTgxTYOudWC1E3HA9LYn7kVa9a7uQ==} peerDependencies: - vitest: 5.0.1 + vitest: 5.0.5 - '@vitest/mocker@5.0.1': - resolution: {integrity: sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==} + '@vitest/mocker@5.0.5': + resolution: {integrity: sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==} peerDependencies: - msw: ^2.4.9 + msw: ^2.4.9 || ^3.0.0 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: @@ -6890,30 +6890,33 @@ packages: vite: optional: true - '@vitest/pretty-format@5.0.1': - resolution: {integrity: sha512-6guWwj5d9bguuefTOvJoq387tfpkzSv554YdUEGzjJH2PnnmvzTLQ1UQSuAk5wBFVhf2CUmy/S/palOcb6dmpA==} + '@vitest/pretty-format@5.0.5': + resolution: {integrity: sha512-24sSdIlMA9DVPXnYiB9siwU0bTd887Y9jX3+vabztCSsvHwo7ipvJ/a2L3Rljw8+2rK1f5fCj3h5U9I/Jsqgbw==} - '@vitest/snapshot@5.0.1': - resolution: {integrity: sha512-aEW4fuQVg1i3fnC781qvwxC5QNptUvb+JjIUfX1WuWJqkjCgpiU8+I89mMcJmxOPLdaGAwEPFFVk02rGKYexvw==} + '@vitest/snapshot@5.0.5': + resolution: {integrity: sha512-dHyFElWMQy0wFBGrIaPPeUWfJFh+jAUrv46ImIJGd8CADdKV3mlrAu3FyD0wesdxELFCs9M807e040Kba/XKtA==} - '@vitest/spy@5.0.1': - resolution: {integrity: sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==} + '@vitest/spy@5.0.5': + resolution: {integrity: sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==} - '@vitest/ui@5.0.1': - resolution: {integrity: sha512-7PvQu/X9/pQoHYfNLAYL22qsD4/+sx2k7zpUA7XvjW0sc40r3/r0lGZ2fsEyOHMuO8Q1KjiO1QQVjJdnWUy/WQ==} + '@vitest/ui@5.0.5': + resolution: {integrity: sha512-dCoGUOdgmCaSB2g+WQ/spPCBkytAWdNUkFSXY4z+eYp2d8JEOY8v0uDRaJTG9qf/kTlTVZ4CuYMYMz/f1HS/qQ==} peerDependencies: - vitest: 5.0.1 + vitest: 5.0.5 - '@vitest/utils@5.0.1': - resolution: {integrity: sha512-E9+yEA+jsfaoxZcUHFzEqUrQcoNh2EwrPT5efIqkUPUwD5Ua2Li9BRWaYeRwvzvdLgTSVrre8oKNeyrfg7KkdQ==} + '@vitest/utils@5.0.5': + resolution: {integrity: sha512-/rBu7gzLDQI/SC79Tn98b5DwOgaHU/Gt4I9OVeP3YofzNaYtbImRCOJQmT1S6vOJg9gEIlWC+r3QxKkjuSa1nQ==} - '@voidzero-dev/vite-plus-core@1.0.0': - resolution: {integrity: sha512-GHCd6d7h3JVojI/yxe/EAHwX5gn+OI7G932VBnYoTiwATLJrvHOs+okx7Ek2LGEJ57NJRENwCFgVYefjwPLfFA==} + '@voidzero-dev/vite-plus-core@1.3.0': + resolution: {integrity: sha512-Bk/LBwNrxoB8XbGUtGJE3r+U9P4puvdZ6UH36ekygE1J+on/ytwM/yBgQwBnvmX6/MVax+2xxj8wk0N/KkyNeA==} engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} peerDependencies: '@arethetypeswrong/core': ^0.18.1 '@types/node': 24.12.4 + '@typescript/native-preview': '*' '@vitejs/devtools': ^0.7.1 + '@volar/typescript': ~2.4.0 + '@vue/language-core': ~3.2.0 || ~3.3.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -6927,14 +6930,21 @@ packages: typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 unplugin-unused: '>=0.5.0' unrun: '*' + vue-tsc: ~3.2.0 || ~3.3.0 yaml: ^2.9.0 peerDependenciesMeta: '@arethetypeswrong/core': optional: true '@types/node': optional: true + '@typescript/native-preview': + optional: true '@vitejs/devtools': optional: true + '@volar/typescript': + optional: true + '@vue/language-core': + optional: true esbuild: optional: true jiti: @@ -6961,57 +6971,59 @@ packages: optional: true unrun: optional: true + vue-tsc: + optional: true yaml: optional: true - '@voidzero-dev/vite-plus-darwin-arm64@1.0.0': - resolution: {integrity: sha512-lR+0rHOCYQTUo4Un0Sy14Xi1NBqoPXRnhsRyc0Joe53fomZ+wPtgErk15tkvWQdSLGNOr8hDDWJKo/kzI0Y37w==} + '@voidzero-dev/vite-plus-darwin-arm64@1.3.0': + resolution: {integrity: sha512-BF4W+UwGxUkHPrH2pZLhafJfkJT20WZIbMzZgZLGQ44LPpZPX8Sb+qtblUnXUkkCzTqad4XtSPgiLKglvZkNQQ==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [darwin] - '@voidzero-dev/vite-plus-darwin-x64@1.0.0': - resolution: {integrity: sha512-MW3YnWpYP6wxvpd5KLFYTdtHTcAVnN0mVoLrmA9KCKWfOzO8nanr8hpuyEIdLMQT3r/7VNKRvgROI0CZZDP9/A==} + '@voidzero-dev/vite-plus-darwin-x64@1.3.0': + resolution: {integrity: sha512-vLZTk0r+s3e7mycj4SGBbcpK+xyM8a86hXQZtQbJyW7xrGJzsavISy7/m4gGZpWN32Hl4Tmx9V/ffeaoXLkRdg==} engines: {node: '>=20.0.0'} cpu: [x64] os: [darwin] - '@voidzero-dev/vite-plus-linux-arm64-gnu@1.0.0': - resolution: {integrity: sha512-n4onkD4oT+g4jLj9rR37TM4i2IbaYvIiU68p1A/5KrwnIHzbv2maO0BqBYdIRnBy1Mil8pIx9HsT8riWCjZeTw==} + '@voidzero-dev/vite-plus-linux-arm64-gnu@1.3.0': + resolution: {integrity: sha512-H43Dj9RkEyjOFXxTk3DLTa6hqmHhxUN9vbbqNdwQoc3/TPNhC1zsLZy3qgUFbYGLdS7M9jICCqFzj5GDSYrhIw==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-arm64-musl@1.0.0': - resolution: {integrity: sha512-EZfgINa0FL/eIsLkL3MfQz7+Dc4VDAeIrf1UZDlitt//8NIOqI6nVMPnIFzMZThaIncIose+cVWsEkKLAKMM/A==} + '@voidzero-dev/vite-plus-linux-arm64-musl@1.3.0': + resolution: {integrity: sha512-Se2GlvzUZOvFy+5hoQHoG7LFeiplKs+EJwaskayCSLwZAP+e6/LTK7EK+8OcdMpmdsLPtZoTJHkIjSzrQowJwQ==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-linux-x64-gnu@1.0.0': - resolution: {integrity: sha512-R6+/oVLCSEvatOG1utYo0d0I++QPjWuRt2moD21+UUu2mQ8VyZHaaQLgoxrQG9shA9o0RBDmwxhGUXL20Rk1FA==} + '@voidzero-dev/vite-plus-linux-x64-gnu@1.3.0': + resolution: {integrity: sha512-moxojk6zmog2SaLE4/cG75acE6X3cjR0/52Ba8OG2ct0r+dv55VSE0kellAuxS6o9VHy7TBhkIpwpFpQ34rnAA==} engines: {node: '>=20.0.0'} cpu: [x64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-x64-musl@1.0.0': - resolution: {integrity: sha512-B1Ottn22hn9RZ6XIVrWCXAAMfCOQRJ4KWoKbPL4JxC+UgvEiY9kC1ZFPqEAI89zIbW8eLdP3BeT14NQ0h1rtxw==} + '@voidzero-dev/vite-plus-linux-x64-musl@1.3.0': + resolution: {integrity: sha512-CykxH008Dy9u20Myki9/UIqq3Dbo1ZMqa098aS6AtWp3QKyEKzSLPB3jKDUklgmu83xiFe5BpPA+33oF0yNY7A==} engines: {node: '>=20.0.0'} cpu: [x64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-win32-arm64-msvc@1.0.0': - resolution: {integrity: sha512-XXK6N5xMkGVIcqMaVDjqDh44z0Z00DA8dYn2BWUkFOQflUU1Z3IWt+bPm/1Y7LjkwBWj5qD2zsQY1ANDCeX9QA==} + '@voidzero-dev/vite-plus-win32-arm64-msvc@1.3.0': + resolution: {integrity: sha512-l61S9b8aeiDpi8wPRfz4HonqRkLpt/OVYs0sCskvlXYSW/L8qfLOtuWH/xpF8xDjMIfBefKejBVfU1UlRax5Tw==} engines: {node: '>=20.0.0'} cpu: [arm64] os: [win32] - '@voidzero-dev/vite-plus-win32-x64-msvc@1.0.0': - resolution: {integrity: sha512-XabdwdAsJ9QqEVSpSOuiiyJjiEwbwOspGahei9svQUGjs7e8MPuj4y/ltBHJZpyCBdZI6fhlc7Xaps9bUtl7Og==} + '@voidzero-dev/vite-plus-win32-x64-msvc@1.3.0': + resolution: {integrity: sha512-b6Ng8YezA6JA3adQBNA+8Mn26+OX4fw/tsPfnk//5Cx+1XFDZoAxWYXtGzc0yUQPEKnoGtfw/0OUvcibg61yYg==} engines: {node: '>=20.0.0'} cpu: [x64] os: [win32] @@ -7053,164 +7065,74 @@ packages: resolution: {integrity: sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==} engines: {node: '>=14.6'} - '@yuku-codegen/binding-android-arm64@0.9.5': - resolution: {integrity: sha512-jrOY5WM+AaAqkv51fHP1x28ifto4WgcZVzodLUyMU1jMWn5Sq+VciRdk/n8E0Ey5w4p4cWWE+m5GfXWOYh7Kzw==} - cpu: [arm64] - os: [android] - deprecated: yuku-codegen is pure JavaScript since 0.14 - - '@yuku-codegen/binding-darwin-arm64@0.9.5': - resolution: {integrity: sha512-4O4lkCQIzPZGjiNB1GORSI6hBECbiiSBS/APZXPvNKYH+nhY4uuqv03LNXA+SET3hoBjvr95P5rIhY8KQaQUBA==} - cpu: [arm64] - os: [darwin] - deprecated: yuku-codegen is pure JavaScript since 0.14 - - '@yuku-codegen/binding-darwin-x64@0.9.5': - resolution: {integrity: sha512-9EvoUO0SEhD6/d8VHdWuPerepPMSR1y84+UEgz8Un1Ope14Oe7xMvePpEQLTLovoIFZ8Zg3iZ8z8E11pZMqC3g==} - cpu: [x64] - os: [darwin] - deprecated: yuku-codegen is pure JavaScript since 0.14 - - '@yuku-codegen/binding-freebsd-x64@0.9.5': - resolution: {integrity: sha512-HqT78WwgHTmp8lwujoUa9CrIortX4DdpuiVC18ZPSGvuuJf4ylpIEI6QrQEM78Zwz3muhAWAOXZ5irdiYY+AyA==} - cpu: [x64] - os: [freebsd] - deprecated: yuku-codegen is pure JavaScript since 0.14 - - '@yuku-codegen/binding-linux-arm-gnu@0.9.5': - resolution: {integrity: sha512-QJXwIW6Ms3QIawbcBIedmrmXnfdpuAOjZJ/eAABq5XTzWvSxZ7lutu9W5yIHahlaTaFnBo7ikrVMD1szLTpPUw==} - cpu: [arm] - os: [linux] - libc: [glibc] - deprecated: yuku-codegen is pure JavaScript since 0.14 - - '@yuku-codegen/binding-linux-arm-musl@0.9.5': - resolution: {integrity: sha512-eHbFy3IHGb+IYarrYwAo0yWSEQV3eAmEn6VrsKA6I1Wy1YPJxWqSJlV69JhqsHtJuJlljUIF3ex0y46yaKIu9w==} - cpu: [arm] - os: [linux] - libc: [musl] - deprecated: yuku-codegen is pure JavaScript since 0.14 - - '@yuku-codegen/binding-linux-arm64-gnu@0.9.5': - resolution: {integrity: sha512-ByoJMbySTaDhjAXSu8q6Lh7HKg3YoesXpcT72aYk0Aiw4PCznmY4ybpLTq0RCvp0RIPhFm/6yECFiGBwyCC1nw==} - cpu: [arm64] - os: [linux] - libc: [glibc] - deprecated: yuku-codegen is pure JavaScript since 0.14 - - '@yuku-codegen/binding-linux-arm64-musl@0.9.5': - resolution: {integrity: sha512-gD9vfXIoBw1toSxhO9rgmSu/FEfy3PMznJAxVjIuH8DrWEiDKXmJO0pJKfj1Ltbe/TmtWVZR+TjISqeSIGQzMg==} - cpu: [arm64] - os: [linux] - libc: [musl] - deprecated: yuku-codegen is pure JavaScript since 0.14 - - '@yuku-codegen/binding-linux-x64-gnu@0.9.5': - resolution: {integrity: sha512-BARvdnvqMGjOr5Iel2JH+9H5vAIE0R6H0Z2fsT02xrvmI/1ZXNB8lkuX+ZGevPhZb3zJ9WeC/R8JZstrsUOK5g==} - cpu: [x64] - os: [linux] - libc: [glibc] - deprecated: yuku-codegen is pure JavaScript since 0.14 - - '@yuku-codegen/binding-linux-x64-musl@0.9.5': - resolution: {integrity: sha512-x8flcevS1fbb7ESrmpOY/pON4eInSaE/7Ktjqx2udOE2W33BNSZmJuwpyUgo54AoHNqrcaM7szqydyJn5fNvew==} - cpu: [x64] - os: [linux] - libc: [musl] - deprecated: yuku-codegen is pure JavaScript since 0.14 - - '@yuku-codegen/binding-win32-arm64@0.9.5': - resolution: {integrity: sha512-KOL/rBatWqH4ZpCNoF8ZtSNdOJbAxBJLmk/VeRciepGROPTbPum1A9t67GpFgOU7qrkUWlPJdJ+KHKbvbmOt+w==} - cpu: [arm64] - os: [win32] - deprecated: yuku-codegen is pure JavaScript since 0.14 - - '@yuku-codegen/binding-win32-x64@0.9.5': - resolution: {integrity: sha512-FxENahEjWSan59Syh/us/Kf4wNq8qrJLFJ3R2N4Oiwtb6yNdz/rWLNTIoNSssnsp7IoWJdUP6o/Z8ppg7lXMcg==} - cpu: [x64] - os: [win32] - deprecated: yuku-codegen is pure JavaScript since 0.14 - - '@yuku-parser/binding-android-arm64@0.9.5': - resolution: {integrity: sha512-A2JCFCSHfnficqYEw4Iujpx7XrkMM3UfFcgJFmYpZSo9zM4nyhmdIIE0FogSduuU60lhM0/UcfuUBXIVNBMlGQ==} + '@yuku-core/binding-android-arm64@0.19.0': + resolution: {integrity: sha512-wdL2XmTiZixAOaHk9Z0JJZ8u9Qm8d333Zjpc6gaxXFX+UR5BlsfxZ7a2+2Rm71pamppsPYGUGFq2oVLbh6DISw==} cpu: [arm64] os: [android] - deprecated: yuku-parser runs on yuku-core since 0.14 - '@yuku-parser/binding-darwin-arm64@0.9.5': - resolution: {integrity: sha512-3PiyU+Eare4YuKaQ22N98/yAiROPY5o/NQJHraICzDvk4pgS+m+bgLKOvkGBRn33OnV95Vdv1Mn38b+MQHpULQ==} + '@yuku-core/binding-darwin-arm64@0.19.0': + resolution: {integrity: sha512-7sNJa6XJhyetnTy64jw3NEi4YgUcezHk1G5GH9FSvXAuC8wIlbaOwT9lgq5qo5sspjrZJQlBR1TWflP49uRpMw==} cpu: [arm64] os: [darwin] - deprecated: yuku-parser runs on yuku-core since 0.14 - '@yuku-parser/binding-darwin-x64@0.9.5': - resolution: {integrity: sha512-blFMAFI7AInI83XaiOF8cIeiRM46Nz9EfpZtZPRLbKxSA9aAr5v8aHYpmfN6NoyXxAv/10pwS4gsAuc1W6fy5Q==} + '@yuku-core/binding-darwin-x64@0.19.0': + resolution: {integrity: sha512-pTC7dYEc59isdyizc3nsuyFNBu2LU5wMYZ1be6UMr59PY/AJNtO1aBLWV0MgL/z7C1nv/2sX4TNjGgKLoeevJw==} cpu: [x64] os: [darwin] - deprecated: yuku-parser runs on yuku-core since 0.14 - '@yuku-parser/binding-freebsd-x64@0.9.5': - resolution: {integrity: sha512-TsNuL4qsZdO0tHp5GW43Y5fHeLPpPHA675wdcPNTcdq2ZO5AvsQWFFoiDg8CH4oBktfsQ9tdvKhjLnLYwKvp4g==} + '@yuku-core/binding-freebsd-x64@0.19.0': + resolution: {integrity: sha512-xrZjF6XOEwyLOiDIfrwDdRCS6ghxcJeirNC1KivQhVcUwlRonaU79S3/Uiyx3lBylPnT8qKJ3/aMT+H52nGTJQ==} cpu: [x64] os: [freebsd] - deprecated: yuku-parser runs on yuku-core since 0.14 - '@yuku-parser/binding-linux-arm-gnu@0.9.5': - resolution: {integrity: sha512-b0afYK5gHeV8RdmOcqAlgM8ONsye4cax4DMnIBaoJyPMd1UTGvTbpkqQcPVdhmHlcfcGVGMV1laeVovlr/dscw==} + '@yuku-core/binding-linux-arm-gnu@0.19.0': + resolution: {integrity: sha512-0OyIH53C0I1CbADHn9FSdCfX5qbpeLwJhmOVhonFJNYheybOnkSJ50qGMfTJIb8y5zrwqvLs7FVBvz6Fk8maZg==} cpu: [arm] os: [linux] libc: [glibc] - deprecated: yuku-parser runs on yuku-core since 0.14 - '@yuku-parser/binding-linux-arm-musl@0.9.5': - resolution: {integrity: sha512-fD3lKzl+r6j6n8DwiMY53qnh5DLqD8KJjCp+NbVud54Nnh3Q9Wprqss3YzyMHP3NSJk663Wlg1E1X5qOuFVFig==} + '@yuku-core/binding-linux-arm-musl@0.19.0': + resolution: {integrity: sha512-z/iPfkCOCSXE34VXNt0uP1ORSW+4mEa9b5CiJcWZeqZH08g49bFU/jRM2T04f+Wpw5mJ6HIzNBiojxeAbrHjeg==} cpu: [arm] os: [linux] libc: [musl] - deprecated: yuku-parser runs on yuku-core since 0.14 - '@yuku-parser/binding-linux-arm64-gnu@0.9.5': - resolution: {integrity: sha512-aRU/aCphV1MWCl/lvI6NX8LgucHQ68Fx+vz7NBb/5MEr2EuzCxiPOQqeFcMdWh+2AED/p0AvAREch0c+cSnlhA==} + '@yuku-core/binding-linux-arm64-gnu@0.19.0': + resolution: {integrity: sha512-jLNTi+gzaRwBJ+gwhCJ+rDb5dmx/8hEBx93UWd7hjMSqPTL1D1wvVSvXVAldV5kALvju7b0/m51EHEhs4+I1fA==} cpu: [arm64] os: [linux] libc: [glibc] - deprecated: yuku-parser runs on yuku-core since 0.14 - '@yuku-parser/binding-linux-arm64-musl@0.9.5': - resolution: {integrity: sha512-5+Guro0l8H473YXlEjVNBRLN/IbPbJdnQh1zo0OLt49xxnON+XMJRRaEyTOTfkn9QSRg0+BhEKGR4W9Gu2uZJQ==} + '@yuku-core/binding-linux-arm64-musl@0.19.0': + resolution: {integrity: sha512-YwyPIdW/rN2Fmv0MTFWi47FaB3F9g+2b+sJISyAw83Yd60RWH4T3Nx6XkAHHVN+uKJNBAd6So7/BVihSiFYvuQ==} cpu: [arm64] os: [linux] libc: [musl] - deprecated: yuku-parser runs on yuku-core since 0.14 - '@yuku-parser/binding-linux-x64-gnu@0.9.5': - resolution: {integrity: sha512-pwwSyV9q+GlvzSXlsMZBMlgk22L5bud714/vqZCdeUTivzeUVltQzUaf3IQXOGXdpc59JYTeuMCtbGquaAUoCA==} + '@yuku-core/binding-linux-x64-gnu@0.19.0': + resolution: {integrity: sha512-/vskaAXVX3ESR43FiTdfiYS7z1b9uoQJwVQ/0RgUHgN2sWoAyCZs+aX4CgU7eKGIHTVfp9VFwv+c+2XchYiEtQ==} cpu: [x64] os: [linux] libc: [glibc] - deprecated: yuku-parser runs on yuku-core since 0.14 - '@yuku-parser/binding-linux-x64-musl@0.9.5': - resolution: {integrity: sha512-z18j6JN3lBHH8vzN7gG1M8fI0nlgItSfnC9PfbmUbt97iHViKfw2iA2G9fGwRMe5LyPRZBrejIlapbygPbpdaw==} + '@yuku-core/binding-linux-x64-musl@0.19.0': + resolution: {integrity: sha512-FuXL5igeSKib9Rye02TdH+D6SQezTyGxIATMu1Qsn/QywrzwpGl35OLpS2C1x5vpGLeVE2XgiLd5mVPFHmIglQ==} cpu: [x64] os: [linux] libc: [musl] - deprecated: yuku-parser runs on yuku-core since 0.14 - '@yuku-parser/binding-win32-arm64@0.9.5': - resolution: {integrity: sha512-s6Gwttb1dQvtPX6Bgkw+UPC9IO3UBDXc6Zezog8MMgvu3UfJBBB9TQqKBX/2quu0Eyh+lLSAyNlIyyecaagUPg==} + '@yuku-core/binding-win32-arm64@0.19.0': + resolution: {integrity: sha512-YuJAe7zEKT1Xa7LuHlhGJpPnYQeVtvo0rZvNjJRqkH0hQGOw4C8QeP30Tg5VKGzZfo2x8CjhjEhKt4TmLwfmdg==} cpu: [arm64] os: [win32] - deprecated: yuku-parser runs on yuku-core since 0.14 - '@yuku-parser/binding-win32-x64@0.9.5': - resolution: {integrity: sha512-FrERt9YWatY3bJfSUdi4YWY+6iQcyo3MzCC821BxlWM5TFZEHijnpz/bknR79VHlXY/9CvXz8ZlaAGPsTtN3nw==} + '@yuku-core/binding-win32-x64@0.19.0': + resolution: {integrity: sha512-o8iY/mXwlRkedp0LNTr4+TEfXTPQwoNZmv6k+1WIAd1ZXEn8xJJTBCtB0osaVvdZo/x/cNmV16UrWrrGpM5Mvw==} cpu: [x64] os: [win32] - deprecated: yuku-parser runs on yuku-core since 0.14 - '@yuku-toolchain/types@0.9.5': - resolution: {integrity: sha512-KiuLNNgX9uNealaWAR+G3/cMXnRk9x4TY2EkYe/KIag+UPdwiA0RRf1hr1WAxzTP8KGzCTkqUdLPvqh32sEO3w==} + '@yuku-toolchain/types@0.19.0': + resolution: {integrity: sha512-tFkNbsP4WXX9UMUC5syJGdK9dPMLK0PPaIuXCcCYH75sZ8MrebfBZ2XvCILRTw8AgLivSSCfDa4R67iYBDa4Kg==} '@yuuang/ffi-rs-android-arm64@1.3.2': resolution: {integrity: sha512-eDYLT0kVBkp7e2BwdRDmt6N1rkeDPUHDefk3ZX0/nok+GLsqfy1WBoSL3Yg7HVXN1EyW8OBVc2uK8Zq8HbmaSA==} @@ -7754,8 +7676,8 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} engines: {node: '>=18'} chalk@2.4.2: @@ -9762,6 +9684,9 @@ packages: magic-string@1.4.1: resolution: {integrity: sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==} + magic-string@1.4.3: + resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} + magicast@0.5.5: resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} @@ -10337,10 +10262,6 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} - obug@2.2.1: - resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} - engines: {node: '>=12.20.0'} - obug@3.0.0: resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} engines: {node: '>=12.20.0'} @@ -10417,30 +10338,33 @@ packages: resolution: {integrity: sha512-CAx+SKDU4uSOmQnyn7daMj/v/h8tF3fnaUMi4yImKAARPnXV0BieHe/qw1LTcK4KlnIuAhpfUJfSfNKIBh8HqQ==} engines: {node: ^20.19.0 || >=22.12.0} - oxfmt@0.70.0: - resolution: {integrity: sha512-IsHxZ4y0wQLLMhnrJblBJgZsLDzfULrJnAw5j/QqsTlMa/m3AqsbToi+W71uhBGaqlqq/PbbjvHc09TJwdv3Tw==} + oxfmt@0.73.0: + resolution: {integrity: sha512-Ke4xbpuzqIBP4bkd48q815RhqWNJqOBzxVA9199M4qcF0y0LtpUv7UTg1dJ5ea6jw0RBUDZvX3qOUMNmmxfxbw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: + '@astrojs/compiler-rs': ^0.4.0 || ^0.5.0 svelte: ^5.0.0 - vite-plus: 1.0.0 + vite-plus: 1.3.0 peerDependenciesMeta: + '@astrojs/compiler-rs': + optional: true svelte: optional: true vite-plus: optional: true - oxlint-tsgolint@7.0.2003: - resolution: {integrity: sha512-VnK4zlqgmgq/7ZcjzCk/WpN8kKFsYGcB85Io9qT3wL4K8Un3RKmJkp793crNETieMusPMKOA4a+Mw2wbteI6TQ==} + oxlint-tsgolint@7.0.2004: + resolution: {integrity: sha512-vQ9WDF3fiZJvRdSDosNOflerOCOpwol6vWzVDxW76RcU19cjTh7engvkHLvmv+tzedR8fAiJUVUGr6IVZdkzXQ==} hasBin: true - oxlint@1.85.0: - resolution: {integrity: sha512-bc26s97nuvPj1ViyPsqmKecVkUWFMEdtayO8MaQ6oiLfs1pj94cQlZZhrh4BPNlr9HQosjhIlwgZKsfcwmcNgg==} + oxlint@1.88.0: + resolution: {integrity: sha512-uIcFgw8BmqvFRGzH5+r7d2zJMEexwNSrEmM6WQNnGh3VkO0XYTEFoyxyNE6tjKMUib9Cue4XJA2DoWruO5D6aw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - oxlint-tsgolint: '>=7.0.2001' - vite-plus: 1.0.0 + oxlint-tsgolint: '>=7.0.2003' + vite-plus: 1.3.0 peerDependenciesMeta: oxlint-tsgolint: optional: true @@ -10643,10 +10567,6 @@ packages: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} - postcss@8.5.28: - resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.30: resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==} engines: {node: ^10 || ^12 || >=14} @@ -11331,9 +11251,6 @@ packages: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -11409,9 +11326,6 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} @@ -11601,8 +11515,8 @@ packages: tiny-typed-emitter@2.1.0: resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} - tinybench@6.1.4: - resolution: {integrity: sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==} + tinybench@6.2.2: + resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==} engines: {node: '>=20.0.0'} tinyclip@1.0.3: @@ -11613,10 +11527,6 @@ packages: resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} - tinyexec@1.3.0: - resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} - engines: {node: '>=18'} - tinyexec@1.3.1: resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} engines: {node: '>=18'} @@ -11625,12 +11535,12 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - tinypool@2.1.2: - resolution: {integrity: sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww==} + tinypool@2.2.0: + resolution: {integrity: sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==} engines: {node: ^20.0.0 || >=22.0.0} - tinyrainbow@3.1.1: - resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + tinyrainbow@3.2.0: + resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==} engines: {node: '>=14.0.0'} tldts-core@7.4.2: @@ -12000,18 +11910,21 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-plus@1.0.0: - resolution: {integrity: sha512-2ezzBWt+AVO+I2F9Cpb2Of0J12Lat35kWZ/FRL2monMnKZuACKfkYAA/nMqdDo1KwB8sWjTKzmD9mRCh1Q1n+g==} + vite-plus@1.3.0: + resolution: {integrity: sha512-wZGsDeYfPcrCegqgY68pJJHGTpEZdsZvIMH2/mWXkGkBD72UY3M7z24Sv3ZeOL/FrqbbtyqBOfkKO6BvoQ8wMw==} engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} hasBin: true peerDependencies: - '@vitest/browser-playwright': 5.0.1 + '@vitest/browser-playwright': 5.0.5 '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + playwright: '*' peerDependenciesMeta: '@vitest/browser-playwright': optional: true '@vitest/browser-webdriverio': optional: true + playwright: + optional: true vite@8.3.4: resolution: {integrity: sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==} @@ -12064,20 +11977,20 @@ packages: vite: optional: true - vitest@5.0.1: - resolution: {integrity: sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==} + vitest@5.0.5: + resolution: {integrity: sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==} engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': 24.12.4 - '@vitest/browser-playwright': 5.0.1 - '@vitest/browser-preview': 5.0.1 + '@vitest/browser-playwright': 5.0.5 + '@vitest/browser-preview': 5.0.5 '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 - '@vitest/coverage-istanbul': 5.0.1 - '@vitest/coverage-v8': 5.0.1 - '@vitest/ui': 5.0.1 + '@vitest/coverage-istanbul': 5.0.5 + '@vitest/coverage-v8': 5.0.5 + '@vitest/ui': 5.0.5 happy-dom: '*' jsdom: '*' vite: ^6.4.0 || ^7.0.0 || ^8.0.0 @@ -12280,9 +12193,9 @@ packages: engines: {node: ^20.17.0 || >=22.9.0} hasBin: true - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} hasBin: true workerd@1.20260918.1: @@ -12426,14 +12339,13 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} - yuku-ast@0.9.5: - resolution: {integrity: sha512-Q8qW8WwQnN5Cm0ZZivdRIfv0sRLTjUq0YumXJkw8CYN1aCdICH9rk4C47/4n6kA5EqDtlj+F608twoTSV2MwdQ==} - - yuku-codegen@0.9.5: - resolution: {integrity: sha512-zGUVyDpSK4b6c9B0yyxi2P0wujn1XZTbG9dCb7d6gyAoP63EMgyLzUwPUvwxPG5jgYqhlI7w+S3oCyapqGr4PA==} + yuku-core@0.19.0: + resolution: {integrity: sha512-kYK9e1XdfirNhkipzW0wmMcSWcVF/CWnuugodfVYRCUwvoBefweL9HzB96OiKEhsx78V5WbUxbJqlizMX869Qg==} + engines: {node: '>=14.18.0'} - yuku-parser@0.9.5: - resolution: {integrity: sha512-IBnAdNVMswWbJcM7woPluOudectJzFjJ1N8jVRxP9Uq4CIv5hZdBIcdmtRbFDYF/Ph2j2gVup4YJ4N9mh0jYFA==} + yuku-parser@0.19.0: + resolution: {integrity: sha512-XrBh9ZiQK5mJVRxZBAvD6LwRyAYrK3MFtH3+onyuxFL5hFD44EC3QOmIC1FjTj6H2MMwq46/NAI+NZxLZPZgxg==} + engines: {node: '>=14.18.0'} zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} @@ -12500,7 +12412,7 @@ snapshots: dependencies: json-schema: 0.4.0 - '@alchemy.run/cloudflare-runtime@2.0.0-beta.80(@distilled.cloud/cloudflare@1.0.0-rc.13(effect@4.0.5))(@effect/platform-node@4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6))(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(effect@4.0.5)(rolldown@1.2.5)(typescript@7.0.2)': + '@alchemy.run/cloudflare-runtime@2.0.0-beta.80(@distilled.cloud/cloudflare@1.0.0-rc.13(effect@4.0.5))(@effect/platform-node@4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6))(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(effect@4.0.5)(rolldown@1.2.5)(typescript@7.0.2)': dependencies: '@alchemy.run/node-utils': 2.0.0-beta.80 '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260918.1) @@ -12517,7 +12429,7 @@ snapshots: optionalDependencies: '@effect/platform-node': 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) rolldown: 1.2.5 - vite: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' transitivePeerDependencies: - '@types/node' - proxy-agent @@ -13927,21 +13839,25 @@ snapshots: '@effect/tsgo-win32-arm64': 0.51.1 '@effect/tsgo-win32-x64': 0.51.1 - '@effect/vitest@4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)': + '@effect/vitest@4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)': dependencies: effect: 4.0.5 - vite-plus: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + vite-plus: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) transitivePeerDependencies: - '@arethetypeswrong/core' + - '@astrojs/compiler-rs' - '@edge-runtime/vm' - '@opentelemetry/api' - '@types/node' + - '@typescript/native-preview' - '@vitejs/devtools' - '@vitest/browser-playwright' - '@vitest/browser-webdriverio' - '@vitest/coverage-istanbul' - '@vitest/coverage-v8' - '@vitest/ui' + - '@volar/typescript' + - '@vue/language-core' - bufferutil - esbuild - happy-dom @@ -13949,6 +13865,7 @@ snapshots: - jsdom - less - msw + - playwright - publint - sass - sass-embedded @@ -13961,6 +13878,7 @@ snapshots: - unplugin-unused - unrun - utf-8-validate + - vue-tsc - yaml '@electric-sql/pglite@0.3.15': @@ -15509,7 +15427,7 @@ snapshots: '@oxc-parser/binding-win32-x64-msvc@0.148.0': optional: true - '@oxc-project/runtime@0.151.0': {} + '@oxc-project/runtime@0.154.0': {} '@oxc-project/types@0.127.0': optional: true @@ -15523,11 +15441,11 @@ snapshots: '@oxc-project/types@0.148.0': optional: true - '@oxc-project/types@0.151.0': {} - '@oxc-project/types@0.153.0': optional: true + '@oxc-project/types@0.154.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.24.2': optional: true @@ -15646,141 +15564,141 @@ snapshots: '@oxc-transform-react/binding-win32-x64-msvc@0.152.0': optional: true - '@oxfmt/binding-android-arm-eabi@0.70.0': + '@oxfmt/binding-android-arm-eabi@0.73.0': optional: true - '@oxfmt/binding-android-arm64@0.70.0': + '@oxfmt/binding-android-arm64@0.73.0': optional: true - '@oxfmt/binding-darwin-arm64@0.70.0': + '@oxfmt/binding-darwin-arm64@0.73.0': optional: true - '@oxfmt/binding-darwin-x64@0.70.0': + '@oxfmt/binding-darwin-x64@0.73.0': optional: true - '@oxfmt/binding-freebsd-x64@0.70.0': + '@oxfmt/binding-freebsd-x64@0.73.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.70.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.73.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.70.0': + '@oxfmt/binding-linux-arm-musleabihf@0.73.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.70.0': + '@oxfmt/binding-linux-arm64-gnu@0.73.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.70.0': + '@oxfmt/binding-linux-arm64-musl@0.73.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.70.0': + '@oxfmt/binding-linux-ppc64-gnu@0.73.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.70.0': + '@oxfmt/binding-linux-riscv64-gnu@0.73.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.70.0': + '@oxfmt/binding-linux-riscv64-musl@0.73.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.70.0': + '@oxfmt/binding-linux-s390x-gnu@0.73.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.70.0': + '@oxfmt/binding-linux-x64-gnu@0.73.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.70.0': + '@oxfmt/binding-linux-x64-musl@0.73.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.70.0': + '@oxfmt/binding-openharmony-arm64@0.73.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.70.0': + '@oxfmt/binding-win32-arm64-msvc@0.73.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.70.0': + '@oxfmt/binding-win32-ia32-msvc@0.73.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.70.0': + '@oxfmt/binding-win32-x64-msvc@0.73.0': optional: true - '@oxlint-tsgolint/darwin-arm64@7.0.2003': + '@oxlint-tsgolint/darwin-arm64@7.0.2004': optional: true - '@oxlint-tsgolint/darwin-x64@7.0.2003': + '@oxlint-tsgolint/darwin-x64@7.0.2004': optional: true - '@oxlint-tsgolint/linux-arm64@7.0.2003': + '@oxlint-tsgolint/linux-arm64@7.0.2004': optional: true - '@oxlint-tsgolint/linux-x64@7.0.2003': + '@oxlint-tsgolint/linux-x64@7.0.2004': optional: true - '@oxlint-tsgolint/win32-arm64@7.0.2003': + '@oxlint-tsgolint/win32-arm64@7.0.2004': optional: true - '@oxlint-tsgolint/win32-x64@7.0.2003': + '@oxlint-tsgolint/win32-x64@7.0.2004': optional: true - '@oxlint/binding-android-arm-eabi@1.85.0': + '@oxlint/binding-android-arm-eabi@1.88.0': optional: true - '@oxlint/binding-android-arm64@1.85.0': + '@oxlint/binding-android-arm64@1.88.0': optional: true - '@oxlint/binding-darwin-arm64@1.85.0': + '@oxlint/binding-darwin-arm64@1.88.0': optional: true - '@oxlint/binding-darwin-x64@1.85.0': + '@oxlint/binding-darwin-x64@1.88.0': optional: true - '@oxlint/binding-freebsd-x64@1.85.0': + '@oxlint/binding-freebsd-x64@1.88.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.85.0': + '@oxlint/binding-linux-arm-gnueabihf@1.88.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.85.0': + '@oxlint/binding-linux-arm-musleabihf@1.88.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.85.0': + '@oxlint/binding-linux-arm64-gnu@1.88.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.85.0': + '@oxlint/binding-linux-arm64-musl@1.88.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.85.0': + '@oxlint/binding-linux-ppc64-gnu@1.88.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.85.0': + '@oxlint/binding-linux-riscv64-gnu@1.88.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.85.0': + '@oxlint/binding-linux-riscv64-musl@1.88.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.85.0': + '@oxlint/binding-linux-s390x-gnu@1.88.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.85.0': + '@oxlint/binding-linux-x64-gnu@1.88.0': optional: true - '@oxlint/binding-linux-x64-musl@1.85.0': + '@oxlint/binding-linux-x64-musl@1.88.0': optional: true - '@oxlint/binding-openharmony-arm64@1.85.0': + '@oxlint/binding-openharmony-arm64@1.88.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.85.0': + '@oxlint/binding-win32-arm64-msvc@1.88.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.85.0': + '@oxlint/binding-win32-ia32-msvc@1.88.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.85.0': + '@oxlint/binding-win32-x64-msvc@1.88.0': optional: true '@oxlint/plugins@1.68.0': {} - '@oxlint/plugins@1.79.0': {} + '@oxlint/plugins@1.88.0': {} '@peculiar/asn1-schema@2.8.0': dependencies: @@ -16127,7 +16045,7 @@ snapshots: '@react-navigation/routers@7.6.5': dependencies: - nanoid: 3.3.19 + nanoid: 3.3.20 '@redis/bloom@6.2.1(@redis/client@6.2.1)': dependencies: @@ -16288,7 +16206,7 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.2.5': optional: true - '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7))(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(rolldown@1.2.13)': + '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7))(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(rolldown@1.2.13)': dependencies: '@babel/core': 7.29.7 picomatch: 4.0.7 @@ -16296,7 +16214,7 @@ snapshots: optionalDependencies: '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7) '@babel/runtime': 7.29.7 - vite: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' optional: true '@rolldown/pluginutils@1.0.0-rc.17': @@ -16523,12 +16441,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.3(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' '@tanstack/devtools-event-client@0.4.3': {} @@ -16591,7 +16509,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.168.13(@tanstack/react-router@1.170.10(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))': + '@tanstack/router-plugin@1.168.13(@tanstack/react-router@1.170.10(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) @@ -16608,7 +16526,7 @@ snapshots: zod: 4.4.3 optionalDependencies: '@tanstack/react-router': 1.170.10(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - vite: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' transitivePeerDependencies: - supports-color @@ -16632,7 +16550,7 @@ snapshots: '@tanstack/virtual-file-routes@1.162.0': {} - '@testing-library/dom@10.4.1': + '@testing-library/dom@10.4.2': dependencies: '@babel/code-frame': 7.29.7 '@babel/runtime': 7.29.7 @@ -16643,9 +16561,9 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/user-event@14.6.7(@testing-library/dom@10.4.1)': + '@testing-library/user-event@14.6.7(@testing-library/dom@10.4.2)': dependencies: - '@testing-library/dom': 10.4.1 + '@testing-library/dom': 10.4.2 '@tiptap/core@3.31.3(@tiptap/pm@3.31.3)': dependencies: @@ -17128,35 +17046,35 @@ snapshots: optionalDependencies: ajv: 6.15.0 - '@vitejs/plugin-react@6.1.2(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7))(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(rolldown@1.2.13))(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(babel-plugin-react-compiler@1.0.0)(oxc-transform-react@0.152.0)': + '@vitejs/plugin-react@6.1.2(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7))(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(rolldown@1.2.13))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(babel-plugin-react-compiler@1.0.0)(oxc-transform-react@0.152.0)': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' optionalDependencies: - '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.7)(@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7))(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(rolldown@1.2.13) + '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.7)(@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7))(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(rolldown@1.2.13) babel-plugin-react-compiler: 1.0.0 oxc-transform-react: 0.152.0 - '@vitest/browser-playwright@5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(utf-8-validate@6.0.6)(vitest@5.0.1)': + '@vitest/browser-playwright@5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(utf-8-validate@6.0.6)(vitest@5.0.5)': dependencies: - '@vitest/browser': 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.1) - '@vitest/mocker': 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + '@vitest/browser': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.5) + '@vitest/mocker': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) playwright: 1.60.0 - tinyrainbow: 3.1.1 - vitest: 5.0.1(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/browser-preview@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + tinyrainbow: 3.2.0 + vitest: 5.0.5(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser-playwright@5.0.1(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.1)': + '@vitest/browser-playwright@5.0.5(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.5)': dependencies: - '@vitest/browser': 5.0.1(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.1) - '@vitest/mocker': 5.0.1(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitest/browser': 5.0.5(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.5) + '@vitest/mocker': 5.0.5(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) playwright: 1.60.0 - tinyrainbow: 3.1.1 - vitest: 5.0.1(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/browser-preview@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) + tinyrainbow: 3.2.0 + vitest: 5.0.5(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) transitivePeerDependencies: - bufferutil - msw @@ -17164,24 +17082,24 @@ snapshots: - vite optional: true - '@vitest/browser-preview@5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.1)': + '@vitest/browser-preview@5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.5)': dependencies: - '@testing-library/dom': 10.4.1 - '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.1) - '@vitest/browser': 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.1) - vitest: 5.0.1(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/browser-preview@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.5) + vitest: 5.0.5(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser-preview@5.0.1(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.1)': + '@vitest/browser-preview@5.0.5(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.5)': dependencies: - '@testing-library/dom': 10.4.1 - '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.1) - '@vitest/browser': 5.0.1(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.1) - vitest: 5.0.1(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/browser-preview@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) + '@testing-library/dom': 10.4.2 + '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.2) + '@vitest/browser': 5.0.5(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.5) + vitest: 5.0.5(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) transitivePeerDependencies: - bufferutil - msw @@ -17189,17 +17107,17 @@ snapshots: - vite optional: true - '@vitest/browser@5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.1)': + '@vitest/browser@5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.5)': dependencies: '@blazediff/core': 1.10.0 - '@vitest/mocker': 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) - '@vitest/ui': 5.0.1(vitest@5.0.1) - '@vitest/utils': 5.0.1 - magic-string: 1.4.1 + '@vitest/mocker': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + '@vitest/ui': 5.0.5(vitest@5.0.5) + '@vitest/utils': 5.0.5 + magic-string: 1.4.3 pngjs: 7.0.0 sirv: 3.0.2 - tinyrainbow: 3.1.1 - vitest: 5.0.1(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/browser-preview@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + tinyrainbow: 3.2.0 + vitest: 5.0.5(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) ws: 8.22.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil @@ -17207,17 +17125,17 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@5.0.1(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.1)': + '@vitest/browser@5.0.5(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.5)': dependencies: '@blazediff/core': 1.10.0 - '@vitest/mocker': 5.0.1(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) - '@vitest/ui': 5.0.1(vitest@5.0.1) - '@vitest/utils': 5.0.1 - magic-string: 1.4.1 + '@vitest/mocker': 5.0.5(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitest/ui': 5.0.5(vitest@5.0.5) + '@vitest/utils': 5.0.5 + magic-string: 1.4.3 pngjs: 7.0.0 sirv: 3.0.2 - tinyrainbow: 3.1.1 - vitest: 5.0.1(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/browser-preview@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) + tinyrainbow: 3.2.0 + vitest: 5.0.5(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) ws: 8.22.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil @@ -17226,74 +17144,74 @@ snapshots: - vite optional: true - '@vitest/mocker@5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))': + '@vitest/mocker@5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))': dependencies: '@jridgewell/trace-mapping': 0.3.31 - '@vitest/spy': 5.0.1 + '@vitest/spy': 5.0.5 estree-walker: 3.0.3 - magic-string: 1.4.1 + magic-string: 1.4.3 optionalDependencies: msw: 2.12.11(@types/node@24.12.4)(typescript@7.0.2) - vite: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' - '@vitest/mocker@5.0.1(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))': + '@vitest/mocker@5.0.5(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@jridgewell/trace-mapping': 0.3.31 - '@vitest/spy': 5.0.1 + '@vitest/spy': 5.0.5 estree-walker: 3.0.3 - magic-string: 1.4.1 + magic-string: 1.4.3 optionalDependencies: msw: 2.12.11(@types/node@24.12.4)(typescript@7.0.2) vite: 8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0) optional: true - '@vitest/pretty-format@5.0.1': + '@vitest/pretty-format@5.0.5': dependencies: - tinyrainbow: 3.1.1 + tinyrainbow: 3.2.0 - '@vitest/snapshot@5.0.1': + '@vitest/snapshot@5.0.5': dependencies: - '@vitest/pretty-format': 5.0.1 - '@vitest/utils': 5.0.1 - magic-string: 1.4.1 + '@vitest/pretty-format': 5.0.5 + '@vitest/utils': 5.0.5 + magic-string: 1.4.3 pathe: 2.0.3 - '@vitest/spy@5.0.1': {} + '@vitest/spy@5.0.5': {} - '@vitest/ui@5.0.1(vitest@5.0.1)': + '@vitest/ui@5.0.5(vitest@5.0.5)': dependencies: - '@vitest/utils': 5.0.1 + '@vitest/utils': 5.0.5 fflate: 0.8.3 flatted: 3.4.4 pathe: 2.0.3 sirv: 3.0.2 - tinyrainbow: 3.1.1 - vitest: 5.0.1(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/browser-preview@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + tinyrainbow: 3.2.0 + vitest: 5.0.5(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) - '@vitest/utils@5.0.1': + '@vitest/utils@5.0.5': dependencies: - '@vitest/pretty-format': 5.0.1 + '@vitest/pretty-format': 5.0.5 convert-source-map: 2.0.0 - tinyrainbow: 3.1.1 + tinyrainbow: 3.2.0 - '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)': + '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)': dependencies: - '@oxc-project/runtime': 0.151.0 - '@oxc-project/types': 0.151.0 + '@oxc-project/runtime': 0.154.0 + '@oxc-project/types': 0.154.0 lightningcss: 1.33.0 - postcss: 8.5.28 - yuku-codegen: 0.9.5 - yuku-parser: 0.9.5 + postcss: 8.5.30 + yuku-parser: 0.19.0 optionalDependencies: '@types/node': 24.12.4 - '@voidzero-dev/vite-plus-darwin-arm64': 1.0.0 - '@voidzero-dev/vite-plus-darwin-x64': 1.0.0 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.0.0 - '@voidzero-dev/vite-plus-linux-arm64-musl': 1.0.0 - '@voidzero-dev/vite-plus-linux-x64-gnu': 1.0.0 - '@voidzero-dev/vite-plus-linux-x64-musl': 1.0.0 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.0.0 - '@voidzero-dev/vite-plus-win32-x64-msvc': 1.0.0 + '@voidzero-dev/vite-plus-darwin-arm64': 1.3.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.3.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.3.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.3.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.3.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.3.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.3.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.3.0 + '@volar/typescript': 2.4.28 esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 @@ -17302,24 +17220,24 @@ snapshots: unrun: 0.2.39 yaml: 2.9.0 - '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)': + '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)': dependencies: - '@oxc-project/runtime': 0.151.0 - '@oxc-project/types': 0.151.0 + '@oxc-project/runtime': 0.154.0 + '@oxc-project/types': 0.154.0 lightningcss: 1.33.0 - postcss: 8.5.28 - yuku-codegen: 0.9.5 - yuku-parser: 0.9.5 + postcss: 8.5.30 + yuku-parser: 0.19.0 optionalDependencies: '@types/node': 24.12.4 - '@voidzero-dev/vite-plus-darwin-arm64': 1.0.0 - '@voidzero-dev/vite-plus-darwin-x64': 1.0.0 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.0.0 - '@voidzero-dev/vite-plus-linux-arm64-musl': 1.0.0 - '@voidzero-dev/vite-plus-linux-x64-gnu': 1.0.0 - '@voidzero-dev/vite-plus-linux-x64-musl': 1.0.0 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.0.0 - '@voidzero-dev/vite-plus-win32-x64-msvc': 1.0.0 + '@voidzero-dev/vite-plus-darwin-arm64': 1.3.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.3.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.3.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.3.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.3.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.3.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.3.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.3.0 + '@volar/typescript': 2.4.28 esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 @@ -17328,28 +17246,28 @@ snapshots: unrun: 0.2.39 yaml: 2.9.0 - '@voidzero-dev/vite-plus-darwin-arm64@1.0.0': + '@voidzero-dev/vite-plus-darwin-arm64@1.3.0': optional: true - '@voidzero-dev/vite-plus-darwin-x64@1.0.0': + '@voidzero-dev/vite-plus-darwin-x64@1.3.0': optional: true - '@voidzero-dev/vite-plus-linux-arm64-gnu@1.0.0': + '@voidzero-dev/vite-plus-linux-arm64-gnu@1.3.0': optional: true - '@voidzero-dev/vite-plus-linux-arm64-musl@1.0.0': + '@voidzero-dev/vite-plus-linux-arm64-musl@1.3.0': optional: true - '@voidzero-dev/vite-plus-linux-x64-gnu@1.0.0': + '@voidzero-dev/vite-plus-linux-x64-gnu@1.3.0': optional: true - '@voidzero-dev/vite-plus-linux-x64-musl@1.0.0': + '@voidzero-dev/vite-plus-linux-x64-musl@1.3.0': optional: true - '@voidzero-dev/vite-plus-win32-arm64-msvc@1.0.0': + '@voidzero-dev/vite-plus-win32-arm64-msvc@1.3.0': optional: true - '@voidzero-dev/vite-plus-win32-x64-msvc@1.0.0': + '@voidzero-dev/vite-plus-win32-x64-msvc@1.3.0': optional: true '@volar/kit@2.4.28(typescript@6.0.3)': @@ -17408,79 +17326,43 @@ snapshots: '@xmldom/xmldom@0.9.12': {} - '@yuku-codegen/binding-android-arm64@0.9.5': - optional: true - - '@yuku-codegen/binding-darwin-arm64@0.9.5': - optional: true - - '@yuku-codegen/binding-darwin-x64@0.9.5': - optional: true - - '@yuku-codegen/binding-freebsd-x64@0.9.5': - optional: true - - '@yuku-codegen/binding-linux-arm-gnu@0.9.5': - optional: true - - '@yuku-codegen/binding-linux-arm-musl@0.9.5': - optional: true - - '@yuku-codegen/binding-linux-arm64-gnu@0.9.5': - optional: true - - '@yuku-codegen/binding-linux-arm64-musl@0.9.5': - optional: true - - '@yuku-codegen/binding-linux-x64-gnu@0.9.5': - optional: true - - '@yuku-codegen/binding-linux-x64-musl@0.9.5': + '@yuku-core/binding-android-arm64@0.19.0': optional: true - '@yuku-codegen/binding-win32-arm64@0.9.5': + '@yuku-core/binding-darwin-arm64@0.19.0': optional: true - '@yuku-codegen/binding-win32-x64@0.9.5': + '@yuku-core/binding-darwin-x64@0.19.0': optional: true - '@yuku-parser/binding-android-arm64@0.9.5': + '@yuku-core/binding-freebsd-x64@0.19.0': optional: true - '@yuku-parser/binding-darwin-arm64@0.9.5': + '@yuku-core/binding-linux-arm-gnu@0.19.0': optional: true - '@yuku-parser/binding-darwin-x64@0.9.5': + '@yuku-core/binding-linux-arm-musl@0.19.0': optional: true - '@yuku-parser/binding-freebsd-x64@0.9.5': + '@yuku-core/binding-linux-arm64-gnu@0.19.0': optional: true - '@yuku-parser/binding-linux-arm-gnu@0.9.5': + '@yuku-core/binding-linux-arm64-musl@0.19.0': optional: true - '@yuku-parser/binding-linux-arm-musl@0.9.5': + '@yuku-core/binding-linux-x64-gnu@0.19.0': optional: true - '@yuku-parser/binding-linux-arm64-gnu@0.9.5': + '@yuku-core/binding-linux-x64-musl@0.19.0': optional: true - '@yuku-parser/binding-linux-arm64-musl@0.9.5': + '@yuku-core/binding-win32-arm64@0.19.0': optional: true - '@yuku-parser/binding-linux-x64-gnu@0.9.5': + '@yuku-core/binding-win32-x64@0.19.0': optional: true - '@yuku-parser/binding-linux-x64-musl@0.9.5': - optional: true - - '@yuku-parser/binding-win32-arm64@0.9.5': - optional: true - - '@yuku-parser/binding-win32-x64@0.9.5': - optional: true - - '@yuku-toolchain/types@0.9.5': {} + '@yuku-toolchain/types@0.19.0': {} '@yuuang/ffi-rs-android-arm64@1.3.2': optional: true @@ -17584,9 +17466,9 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - alchemy@2.0.0-beta.80(patch_hash=bea1b6c0c0b23157fd5f439a1d7feff430f28ac1621ba135d8a13eeec60ccacd)(c218479f13e455b978e1beb907da1b95): + alchemy@2.0.0-beta.80(patch_hash=bea1b6c0c0b23157fd5f439a1d7feff430f28ac1621ba135d8a13eeec60ccacd)(d214a55921b9b1c12454f5ddcf93cf1a): dependencies: - '@alchemy.run/cloudflare-runtime': 2.0.0-beta.80(@distilled.cloud/cloudflare@1.0.0-rc.13(effect@4.0.5))(@effect/platform-node@4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6))(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(effect@4.0.5)(rolldown@1.2.5)(typescript@7.0.2) + '@alchemy.run/cloudflare-runtime': 2.0.0-beta.80(@distilled.cloud/cloudflare@1.0.0-rc.13(effect@4.0.5))(@effect/platform-node@4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6))(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(effect@4.0.5)(rolldown@1.2.5)(typescript@7.0.2) '@alchemy.run/floci': 2.0.0-beta.80(effect@4.0.5) '@alchemy.run/node-utils': 2.0.0-beta.80 '@alchemy.run/sigil': 0.1.0-alpha.1(@types/react@19.3.0)(react-devtools-core@6.1.5(bufferutil@4.1.0)(utf-8-validate@6.0.6)) @@ -17625,12 +17507,12 @@ snapshots: optionalDependencies: '@effect/platform-node': 4.0.5(bufferutil@4.1.0)(effect@4.0.5)(redis@6.2.1)(utf-8-validate@6.0.6) '@effect/sql-pg': 4.0.5(effect@4.0.5) - '@effect/vitest': 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + '@effect/vitest': 4.0.5(patch_hash=2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(effect@4.0.5)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) drizzle-kit: 1.0.0-rc.5-ab785fc drizzle-orm: 1.0.0-rc.5-ab785fc(@cloudflare/workers-types@4.20260604.1)(@effect/sql-d1@4.0.0(effect@4.0.5))(@effect/sql-pg@4.0.5(effect@4.0.5))(@effect/sql-sqlite-bun@4.0.0-rc.112(effect@4.0.5))(@effect/sql-sqlite-do@4.0.5(effect@4.0.5))(@electric-sql/pglite@0.3.15)(@libsql/client@0.17.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(bun-types@1.3.14)(effect@4.0.5)(expo-sqlite@58.0.13(expo@58.0.14)(react-native@0.88.0(@babel/core@7.29.7)(@react-native/metro-config@0.88.0(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0))(pg@8.23.0)(valibot@1.2.0(typescript@7.0.2))(zod@4.4.3) mongodb: 6.21.0(@aws-sdk/credential-providers@3.1062.0)(socks@2.8.9) pg: 8.23.0 - vite: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' ws: 8.22.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@types/node' @@ -17752,7 +17634,7 @@ snapshots: assertion-error@2.0.1: {} - astro@7.3.5(@types/node@24.12.4)(aws4fetch@1.0.20)(idb-keyval@6.2.1)(ioredis@5.11.0)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0): + astro@7.3.5(@types/node@24.12.4)(@volar/typescript@2.4.28)(aws4fetch@1.0.20)(idb-keyval@6.2.1)(ioredis@5.11.0)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0): dependencies: '@astrojs/compiler-rs': 0.5.1 '@astrojs/internal-helpers': 0.11.0 @@ -17802,8 +17684,8 @@ snapshots: unifont: 0.7.5 unstorage: 1.17.5(aws4fetch@1.0.20)(idb-keyval@6.2.1)(ioredis@5.11.0) verkit: 0.4.1 - vite: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)' - vitefu: 1.1.3(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)) + vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)' + vitefu: 1.1.3(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 zod: 4.6.5 @@ -17822,11 +17704,14 @@ snapshots: - '@netlify/blobs' - '@planetscale/database' - '@types/node' + - '@typescript/native-preview' - '@upstash/redis' - '@vercel/blob' - '@vercel/functions' - '@vercel/kv' - '@vitejs/devtools' + - '@volar/typescript' + - '@vue/language-core' - aws4fetch - db0 - idb-keyval @@ -17844,6 +17729,7 @@ snapshots: - unplugin-unused - unrun - uploadthing + - vue-tsc - yaml async-exit-hook@2.0.1: {} @@ -18149,7 +18035,7 @@ snapshots: ccount@2.0.1: {} - chai@6.2.2: {} + chai@6.3.0: {} chalk@2.4.2: dependencies: @@ -20221,6 +20107,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + magic-string@1.4.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + magicast@0.5.5: dependencies: '@babel/parser': 7.29.8 @@ -21108,8 +20998,6 @@ snapshots: object-keys@1.1.1: optional: true - obug@2.2.1: {} - obug@3.0.0: {} ofetch@1.5.1: @@ -21278,112 +21166,114 @@ snapshots: '@oxc-transform-react/binding-win32-ia32-msvc': 0.152.0 '@oxc-transform-react/binding-win32-x64-msvc': 0.152.0 - oxfmt@0.70.0(vite-plus@1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): + oxfmt@0.73.0(@astrojs/compiler-rs@0.5.1)(vite-plus@1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): dependencies: - tinypool: 2.1.2 + tinypool: 2.2.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.70.0 - '@oxfmt/binding-android-arm64': 0.70.0 - '@oxfmt/binding-darwin-arm64': 0.70.0 - '@oxfmt/binding-darwin-x64': 0.70.0 - '@oxfmt/binding-freebsd-x64': 0.70.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.70.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.70.0 - '@oxfmt/binding-linux-arm64-gnu': 0.70.0 - '@oxfmt/binding-linux-arm64-musl': 0.70.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.70.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.70.0 - '@oxfmt/binding-linux-riscv64-musl': 0.70.0 - '@oxfmt/binding-linux-s390x-gnu': 0.70.0 - '@oxfmt/binding-linux-x64-gnu': 0.70.0 - '@oxfmt/binding-linux-x64-musl': 0.70.0 - '@oxfmt/binding-openharmony-arm64': 0.70.0 - '@oxfmt/binding-win32-arm64-msvc': 0.70.0 - '@oxfmt/binding-win32-ia32-msvc': 0.70.0 - '@oxfmt/binding-win32-x64-msvc': 0.70.0 - vite-plus: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) - - oxfmt@0.70.0(vite-plus@1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): - dependencies: - tinypool: 2.1.2 + '@astrojs/compiler-rs': 0.5.1 + '@oxfmt/binding-android-arm-eabi': 0.73.0 + '@oxfmt/binding-android-arm64': 0.73.0 + '@oxfmt/binding-darwin-arm64': 0.73.0 + '@oxfmt/binding-darwin-x64': 0.73.0 + '@oxfmt/binding-freebsd-x64': 0.73.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.73.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.73.0 + '@oxfmt/binding-linux-arm64-gnu': 0.73.0 + '@oxfmt/binding-linux-arm64-musl': 0.73.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.73.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.73.0 + '@oxfmt/binding-linux-riscv64-musl': 0.73.0 + '@oxfmt/binding-linux-s390x-gnu': 0.73.0 + '@oxfmt/binding-linux-x64-gnu': 0.73.0 + '@oxfmt/binding-linux-x64-musl': 0.73.0 + '@oxfmt/binding-openharmony-arm64': 0.73.0 + '@oxfmt/binding-win32-arm64-msvc': 0.73.0 + '@oxfmt/binding-win32-ia32-msvc': 0.73.0 + '@oxfmt/binding-win32-x64-msvc': 0.73.0 + vite-plus: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + + oxfmt@0.73.0(@astrojs/compiler-rs@0.5.1)(vite-plus@1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): + dependencies: + tinypool: 2.2.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.70.0 - '@oxfmt/binding-android-arm64': 0.70.0 - '@oxfmt/binding-darwin-arm64': 0.70.0 - '@oxfmt/binding-darwin-x64': 0.70.0 - '@oxfmt/binding-freebsd-x64': 0.70.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.70.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.70.0 - '@oxfmt/binding-linux-arm64-gnu': 0.70.0 - '@oxfmt/binding-linux-arm64-musl': 0.70.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.70.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.70.0 - '@oxfmt/binding-linux-riscv64-musl': 0.70.0 - '@oxfmt/binding-linux-s390x-gnu': 0.70.0 - '@oxfmt/binding-linux-x64-gnu': 0.70.0 - '@oxfmt/binding-linux-x64-musl': 0.70.0 - '@oxfmt/binding-openharmony-arm64': 0.70.0 - '@oxfmt/binding-win32-arm64-msvc': 0.70.0 - '@oxfmt/binding-win32-ia32-msvc': 0.70.0 - '@oxfmt/binding-win32-x64-msvc': 0.70.0 - vite-plus: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) - - oxlint-tsgolint@7.0.2003: + '@astrojs/compiler-rs': 0.5.1 + '@oxfmt/binding-android-arm-eabi': 0.73.0 + '@oxfmt/binding-android-arm64': 0.73.0 + '@oxfmt/binding-darwin-arm64': 0.73.0 + '@oxfmt/binding-darwin-x64': 0.73.0 + '@oxfmt/binding-freebsd-x64': 0.73.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.73.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.73.0 + '@oxfmt/binding-linux-arm64-gnu': 0.73.0 + '@oxfmt/binding-linux-arm64-musl': 0.73.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.73.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.73.0 + '@oxfmt/binding-linux-riscv64-musl': 0.73.0 + '@oxfmt/binding-linux-s390x-gnu': 0.73.0 + '@oxfmt/binding-linux-x64-gnu': 0.73.0 + '@oxfmt/binding-linux-x64-musl': 0.73.0 + '@oxfmt/binding-openharmony-arm64': 0.73.0 + '@oxfmt/binding-win32-arm64-msvc': 0.73.0 + '@oxfmt/binding-win32-ia32-msvc': 0.73.0 + '@oxfmt/binding-win32-x64-msvc': 0.73.0 + vite-plus: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + + oxlint-tsgolint@7.0.2004: optionalDependencies: - '@oxlint-tsgolint/darwin-arm64': 7.0.2003 - '@oxlint-tsgolint/darwin-x64': 7.0.2003 - '@oxlint-tsgolint/linux-arm64': 7.0.2003 - '@oxlint-tsgolint/linux-x64': 7.0.2003 - '@oxlint-tsgolint/win32-arm64': 7.0.2003 - '@oxlint-tsgolint/win32-x64': 7.0.2003 - - oxlint@1.85.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): + '@oxlint-tsgolint/darwin-arm64': 7.0.2004 + '@oxlint-tsgolint/darwin-x64': 7.0.2004 + '@oxlint-tsgolint/linux-arm64': 7.0.2004 + '@oxlint-tsgolint/linux-x64': 7.0.2004 + '@oxlint-tsgolint/win32-arm64': 7.0.2004 + '@oxlint-tsgolint/win32-x64': 7.0.2004 + + oxlint@1.88.0(oxlint-tsgolint@7.0.2004)(vite-plus@1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.85.0 - '@oxlint/binding-android-arm64': 1.85.0 - '@oxlint/binding-darwin-arm64': 1.85.0 - '@oxlint/binding-darwin-x64': 1.85.0 - '@oxlint/binding-freebsd-x64': 1.85.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.85.0 - '@oxlint/binding-linux-arm-musleabihf': 1.85.0 - '@oxlint/binding-linux-arm64-gnu': 1.85.0 - '@oxlint/binding-linux-arm64-musl': 1.85.0 - '@oxlint/binding-linux-ppc64-gnu': 1.85.0 - '@oxlint/binding-linux-riscv64-gnu': 1.85.0 - '@oxlint/binding-linux-riscv64-musl': 1.85.0 - '@oxlint/binding-linux-s390x-gnu': 1.85.0 - '@oxlint/binding-linux-x64-gnu': 1.85.0 - '@oxlint/binding-linux-x64-musl': 1.85.0 - '@oxlint/binding-openharmony-arm64': 1.85.0 - '@oxlint/binding-win32-arm64-msvc': 1.85.0 - '@oxlint/binding-win32-ia32-msvc': 1.85.0 - '@oxlint/binding-win32-x64-msvc': 1.85.0 - oxlint-tsgolint: 7.0.2003 - vite-plus: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) - - oxlint@1.85.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): + '@oxlint/binding-android-arm-eabi': 1.88.0 + '@oxlint/binding-android-arm64': 1.88.0 + '@oxlint/binding-darwin-arm64': 1.88.0 + '@oxlint/binding-darwin-x64': 1.88.0 + '@oxlint/binding-freebsd-x64': 1.88.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.88.0 + '@oxlint/binding-linux-arm-musleabihf': 1.88.0 + '@oxlint/binding-linux-arm64-gnu': 1.88.0 + '@oxlint/binding-linux-arm64-musl': 1.88.0 + '@oxlint/binding-linux-ppc64-gnu': 1.88.0 + '@oxlint/binding-linux-riscv64-gnu': 1.88.0 + '@oxlint/binding-linux-riscv64-musl': 1.88.0 + '@oxlint/binding-linux-s390x-gnu': 1.88.0 + '@oxlint/binding-linux-x64-gnu': 1.88.0 + '@oxlint/binding-linux-x64-musl': 1.88.0 + '@oxlint/binding-openharmony-arm64': 1.88.0 + '@oxlint/binding-win32-arm64-msvc': 1.88.0 + '@oxlint/binding-win32-ia32-msvc': 1.88.0 + '@oxlint/binding-win32-x64-msvc': 1.88.0 + oxlint-tsgolint: 7.0.2004 + vite-plus: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + + oxlint@1.88.0(oxlint-tsgolint@7.0.2004)(vite-plus@1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.85.0 - '@oxlint/binding-android-arm64': 1.85.0 - '@oxlint/binding-darwin-arm64': 1.85.0 - '@oxlint/binding-darwin-x64': 1.85.0 - '@oxlint/binding-freebsd-x64': 1.85.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.85.0 - '@oxlint/binding-linux-arm-musleabihf': 1.85.0 - '@oxlint/binding-linux-arm64-gnu': 1.85.0 - '@oxlint/binding-linux-arm64-musl': 1.85.0 - '@oxlint/binding-linux-ppc64-gnu': 1.85.0 - '@oxlint/binding-linux-riscv64-gnu': 1.85.0 - '@oxlint/binding-linux-riscv64-musl': 1.85.0 - '@oxlint/binding-linux-s390x-gnu': 1.85.0 - '@oxlint/binding-linux-x64-gnu': 1.85.0 - '@oxlint/binding-linux-x64-musl': 1.85.0 - '@oxlint/binding-openharmony-arm64': 1.85.0 - '@oxlint/binding-win32-arm64-msvc': 1.85.0 - '@oxlint/binding-win32-ia32-msvc': 1.85.0 - '@oxlint/binding-win32-x64-msvc': 1.85.0 - oxlint-tsgolint: 7.0.2003 - vite-plus: 1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + '@oxlint/binding-android-arm-eabi': 1.88.0 + '@oxlint/binding-android-arm64': 1.88.0 + '@oxlint/binding-darwin-arm64': 1.88.0 + '@oxlint/binding-darwin-x64': 1.88.0 + '@oxlint/binding-freebsd-x64': 1.88.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.88.0 + '@oxlint/binding-linux-arm-musleabihf': 1.88.0 + '@oxlint/binding-linux-arm64-gnu': 1.88.0 + '@oxlint/binding-linux-arm64-musl': 1.88.0 + '@oxlint/binding-linux-ppc64-gnu': 1.88.0 + '@oxlint/binding-linux-riscv64-gnu': 1.88.0 + '@oxlint/binding-linux-riscv64-musl': 1.88.0 + '@oxlint/binding-linux-s390x-gnu': 1.88.0 + '@oxlint/binding-linux-x64-gnu': 1.88.0 + '@oxlint/binding-linux-x64-musl': 1.88.0 + '@oxlint/binding-openharmony-arm64': 1.88.0 + '@oxlint/binding-win32-arm64-msvc': 1.88.0 + '@oxlint/binding-win32-ia32-msvc': 1.88.0 + '@oxlint/binding-win32-x64-msvc': 1.88.0 + oxlint-tsgolint: 7.0.2004 + vite-plus: 1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) p-cancelable@2.1.1: {} @@ -21567,12 +21457,6 @@ snapshots: pngjs@7.0.0: {} - postcss@8.5.28: - dependencies: - nanoid: 3.3.19 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.30: dependencies: nanoid: 3.3.20 @@ -22511,8 +22395,6 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - siginfo@2.0.0: {} - signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -22582,8 +22464,6 @@ snapshots: sprintf-js@1.1.3: optional: true - stackback@0.0.2: {} - stackframe@1.3.4: {} stacktrace-parser@0.1.11: @@ -22787,14 +22667,12 @@ snapshots: tiny-typed-emitter@2.1.0: {} - tinybench@6.1.4: {} + tinybench@6.2.2: {} tinyclip@1.0.3: {} tinyexec@1.2.4: {} - tinyexec@1.3.0: {} - tinyexec@1.3.1: {} tinyglobby@0.2.17: @@ -22802,9 +22680,9 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 - tinypool@2.1.2: {} + tinypool@2.2.0: {} - tinyrainbow@3.1.1: {} + tinyrainbow@3.2.0: {} tldts-core@7.4.2: {} @@ -23114,41 +22992,46 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plus@1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0): - dependencies: - '@oxc-project/types': 0.151.0 - '@oxlint/plugins': 1.79.0 - '@vitest/browser': 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.1) - '@vitest/browser-preview': 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.1) - '@vitest/mocker': 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) - '@vitest/pretty-format': 5.0.1 - '@vitest/snapshot': 5.0.1 - '@vitest/spy': 5.0.1 - '@vitest/utils': 5.0.1 - oxfmt: 0.70.0(vite-plus@1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) - oxlint: 1.85.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) - oxlint-tsgolint: 7.0.2003 - vite: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' - vitest: 5.0.1(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/browser-preview@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + vite-plus@1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0): + dependencies: + '@oxc-project/types': 0.154.0 + '@oxlint/plugins': 1.88.0 + '@vitest/browser': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.5) + '@vitest/browser-preview': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.5) + '@vitest/mocker': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + '@vitest/pretty-format': 5.0.5 + '@vitest/snapshot': 5.0.5 + '@vitest/spy': 5.0.5 + '@vitest/utils': 5.0.5 + oxfmt: 0.73.0(@astrojs/compiler-rs@0.5.1)(vite-plus@1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) + oxlint: 1.88.0(oxlint-tsgolint@7.0.2004)(vite-plus@1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) + oxlint-tsgolint: 7.0.2004 + vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' + vitest: 5.0.5(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) optionalDependencies: - '@vitest/browser-playwright': 5.0.1(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.1) - '@voidzero-dev/vite-plus-darwin-arm64': 1.0.0 - '@voidzero-dev/vite-plus-darwin-x64': 1.0.0 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.0.0 - '@voidzero-dev/vite-plus-linux-arm64-musl': 1.0.0 - '@voidzero-dev/vite-plus-linux-x64-gnu': 1.0.0 - '@voidzero-dev/vite-plus-linux-x64-musl': 1.0.0 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.0.0 - '@voidzero-dev/vite-plus-win32-x64-msvc': 1.0.0 + '@vitest/browser-playwright': 5.0.5(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.5) + '@voidzero-dev/vite-plus-darwin-arm64': 1.3.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.3.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.3.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.3.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.3.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.3.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.3.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.3.0 + playwright: 1.60.0 transitivePeerDependencies: - '@arethetypeswrong/core' + - '@astrojs/compiler-rs' - '@edge-runtime/vm' - '@opentelemetry/api' - '@types/node' + - '@typescript/native-preview' - '@vitejs/devtools' - '@vitest/coverage-istanbul' - '@vitest/coverage-v8' - '@vitest/ui' + - '@volar/typescript' + - '@vue/language-core' - bufferutil - esbuild - happy-dom @@ -23168,43 +23051,49 @@ snapshots: - unplugin-unused - unrun - utf-8-validate + - vue-tsc - yaml - vite-plus@1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0): - dependencies: - '@oxc-project/types': 0.151.0 - '@oxlint/plugins': 1.79.0 - '@vitest/browser': 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.1) - '@vitest/browser-preview': 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.1) - '@vitest/mocker': 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) - '@vitest/pretty-format': 5.0.1 - '@vitest/snapshot': 5.0.1 - '@vitest/spy': 5.0.1 - '@vitest/utils': 5.0.1 - oxfmt: 0.70.0(vite-plus@1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) - oxlint: 1.85.0(oxlint-tsgolint@7.0.2003)(vite-plus@1.0.0(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) - oxlint-tsgolint: 7.0.2003 - vite: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' - vitest: 5.0.1(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/browser-preview@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + vite-plus@1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0): + dependencies: + '@oxc-project/types': 0.154.0 + '@oxlint/plugins': 1.88.0 + '@vitest/browser': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.5) + '@vitest/browser-preview': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.5) + '@vitest/mocker': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + '@vitest/pretty-format': 5.0.5 + '@vitest/snapshot': 5.0.5 + '@vitest/spy': 5.0.5 + '@vitest/utils': 5.0.5 + oxfmt: 0.73.0(@astrojs/compiler-rs@0.5.1)(vite-plus@1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) + oxlint: 1.88.0(oxlint-tsgolint@7.0.2004)(vite-plus@1.3.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28)(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) + oxlint-tsgolint: 7.0.2004 + vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' + vitest: 5.0.5(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) optionalDependencies: - '@vitest/browser-playwright': 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(utf-8-validate@6.0.6)(vitest@5.0.1) - '@voidzero-dev/vite-plus-darwin-arm64': 1.0.0 - '@voidzero-dev/vite-plus-darwin-x64': 1.0.0 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.0.0 - '@voidzero-dev/vite-plus-linux-arm64-musl': 1.0.0 - '@voidzero-dev/vite-plus-linux-x64-gnu': 1.0.0 - '@voidzero-dev/vite-plus-linux-x64-musl': 1.0.0 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.0.0 - '@voidzero-dev/vite-plus-win32-x64-msvc': 1.0.0 + '@vitest/browser-playwright': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(utf-8-validate@6.0.6)(vitest@5.0.5) + '@voidzero-dev/vite-plus-darwin-arm64': 1.3.0 + '@voidzero-dev/vite-plus-darwin-x64': 1.3.0 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.3.0 + '@voidzero-dev/vite-plus-linux-arm64-musl': 1.3.0 + '@voidzero-dev/vite-plus-linux-x64-gnu': 1.3.0 + '@voidzero-dev/vite-plus-linux-x64-musl': 1.3.0 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.3.0 + '@voidzero-dev/vite-plus-win32-x64-msvc': 1.3.0 + playwright: 1.60.0 transitivePeerDependencies: - '@arethetypeswrong/core' + - '@astrojs/compiler-rs' - '@edge-runtime/vm' - '@opentelemetry/api' - '@types/node' + - '@typescript/native-preview' - '@vitejs/devtools' - '@vitest/coverage-istanbul' - '@vitest/coverage-v8' - '@vitest/ui' + - '@volar/typescript' + - '@vue/language-core' - bufferutil - esbuild - happy-dom @@ -23224,6 +23113,7 @@ snapshots: - unplugin-unused - unrun - utf-8-validate + - vue-tsc - yaml vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0): @@ -23242,56 +23132,56 @@ snapshots: yaml: 2.9.0 optional: true - vitefu@1.1.3(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)): + vitefu@1.1.3(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)): optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)' + vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)' - vitest@5.0.1(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/browser-preview@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)): + vitest@5.0.5(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)): dependencies: '@types/chai': 5.2.3 - '@vitest/mocker': 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) - chai: 6.2.2 + '@vitest/mocker': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) + chai: 6.3.0 es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 1.4.1 - obug: 2.2.1 + magic-string: 1.4.3 + obug: 3.0.0 picomatch: 4.0.7 std-env: 4.3.0 - tinybench: 6.1.4 - tinyexec: 1.3.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 tinyglobby: 0.2.17 - vite: '@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' - why-is-node-running: 2.3.0 + vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0)' + why-is-node-running: 3.2.1 optionalDependencies: '@types/node': 24.12.4 - '@vitest/browser-playwright': 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(utf-8-validate@6.0.6)(vitest@5.0.1) - '@vitest/browser-preview': 5.0.1(@voidzero-dev/vite-plus-core@1.0.0(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.1) - '@vitest/ui': 5.0.1(vitest@5.0.1) + '@vitest/browser-playwright': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(utf-8-validate@6.0.6)(vitest@5.0.5) + '@vitest/browser-preview': 5.0.5(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.12.4)(@volar/typescript@2.4.28)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vitest@5.0.5) + '@vitest/ui': 5.0.5(vitest@5.0.5) jsdom: 30.1.0(@noble/hashes@1.8.0) transitivePeerDependencies: - msw - vitest@5.0.1(@types/node@24.12.4)(@vitest/browser-playwright@5.0.1)(@vitest/browser-preview@5.0.1)(@vitest/ui@5.0.1(vitest@5.0.1))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)): + vitest@5.0.5(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5(vitest@5.0.5))(jsdom@30.1.0(@noble/hashes@1.8.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: '@types/chai': 5.2.3 - '@vitest/mocker': 5.0.1(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) - chai: 6.2.2 + '@vitest/mocker': 5.0.5(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) + chai: 6.3.0 es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 1.4.1 - obug: 2.2.1 + magic-string: 1.4.3 + obug: 3.0.0 picomatch: 4.0.7 std-env: 4.3.0 - tinybench: 6.1.4 - tinyexec: 1.3.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 tinyglobby: 0.2.17 vite: 8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0) - why-is-node-running: 2.3.0 + why-is-node-running: 3.2.1 optionalDependencies: '@types/node': 24.12.4 - '@vitest/browser-playwright': 5.0.1(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.1) - '@vitest/browser-preview': 5.0.1(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.1) - '@vitest/ui': 5.0.1(vitest@5.0.1) + '@vitest/browser-playwright': 5.0.5(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(playwright@1.60.0)(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.5) + '@vitest/browser-preview': 5.0.5(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2))(utf-8-validate@6.0.6)(vite@8.3.4(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.5) + '@vitest/ui': 5.0.5(vitest@5.0.5) jsdom: 30.1.0(@noble/hashes@1.8.0) transitivePeerDependencies: - msw @@ -23478,10 +23368,7 @@ snapshots: dependencies: isexe: 4.0.0 - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 + why-is-node-running@3.2.1: {} workerd@1.20260918.1: optionalDependencies: @@ -23613,44 +23500,27 @@ snapshots: yoctocolors@2.1.2: {} - yuku-ast@0.9.5: - dependencies: - '@yuku-toolchain/types': 0.9.5 - - yuku-codegen@0.9.5: + yuku-core@0.19.0: dependencies: - '@yuku-toolchain/types': 0.9.5 - optionalDependencies: - '@yuku-codegen/binding-android-arm64': 0.9.5 - '@yuku-codegen/binding-darwin-arm64': 0.9.5 - '@yuku-codegen/binding-darwin-x64': 0.9.5 - '@yuku-codegen/binding-freebsd-x64': 0.9.5 - '@yuku-codegen/binding-linux-arm-gnu': 0.9.5 - '@yuku-codegen/binding-linux-arm-musl': 0.9.5 - '@yuku-codegen/binding-linux-arm64-gnu': 0.9.5 - '@yuku-codegen/binding-linux-arm64-musl': 0.9.5 - '@yuku-codegen/binding-linux-x64-gnu': 0.9.5 - '@yuku-codegen/binding-linux-x64-musl': 0.9.5 - '@yuku-codegen/binding-win32-arm64': 0.9.5 - '@yuku-codegen/binding-win32-x64': 0.9.5 - - yuku-parser@0.9.5: - dependencies: - '@yuku-toolchain/types': 0.9.5 - yuku-ast: 0.9.5 + '@yuku-toolchain/types': 0.19.0 optionalDependencies: - '@yuku-parser/binding-android-arm64': 0.9.5 - '@yuku-parser/binding-darwin-arm64': 0.9.5 - '@yuku-parser/binding-darwin-x64': 0.9.5 - '@yuku-parser/binding-freebsd-x64': 0.9.5 - '@yuku-parser/binding-linux-arm-gnu': 0.9.5 - '@yuku-parser/binding-linux-arm-musl': 0.9.5 - '@yuku-parser/binding-linux-arm64-gnu': 0.9.5 - '@yuku-parser/binding-linux-arm64-musl': 0.9.5 - '@yuku-parser/binding-linux-x64-gnu': 0.9.5 - '@yuku-parser/binding-linux-x64-musl': 0.9.5 - '@yuku-parser/binding-win32-arm64': 0.9.5 - '@yuku-parser/binding-win32-x64': 0.9.5 + '@yuku-core/binding-android-arm64': 0.19.0 + '@yuku-core/binding-darwin-arm64': 0.19.0 + '@yuku-core/binding-darwin-x64': 0.19.0 + '@yuku-core/binding-freebsd-x64': 0.19.0 + '@yuku-core/binding-linux-arm-gnu': 0.19.0 + '@yuku-core/binding-linux-arm-musl': 0.19.0 + '@yuku-core/binding-linux-arm64-gnu': 0.19.0 + '@yuku-core/binding-linux-arm64-musl': 0.19.0 + '@yuku-core/binding-linux-x64-gnu': 0.19.0 + '@yuku-core/binding-linux-x64-musl': 0.19.0 + '@yuku-core/binding-win32-arm64': 0.19.0 + '@yuku-core/binding-win32-x64': 0.19.0 + + yuku-parser@0.19.0: + dependencies: + '@yuku-toolchain/types': 0.19.0 + yuku-core: 0.19.0 zod-to-json-schema@3.25.2(zod@4.6.5): dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e1d927e6f21e..2fc5cd50e405 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -73,8 +73,8 @@ catalog: tldts: 7.4.2 typescript: 7.0.2 undici: 8.11.2 - vite: npm:@voidzero-dev/vite-plus-core@1.0.0 - vite-plus: 1.0.0 + vite: npm:@voidzero-dev/vite-plus-core@1.3.0 + vite-plus: 1.3.0 ws: 8.22.0 yaml: ^2.9.0 # Markdown pipeline: the server counts find matches with the same parser the diff --git a/vite.config.ts b/vite.config.ts index 4b0af4d05298..3c66ae3a5d3c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -397,7 +397,10 @@ export default defineConfig({ "react/jsx-no-constructed-context-values": "off", "react/memo-dependencies": "error", "react/preserve-manual-memoization": "error", - "react/purity": "error", + // oxlint 1.88 treats `new Date()` as impure (oxc#26894) and also flags it inside event + // handlers (oxc#27309). The compiler we ship (oxc-transform-react) still compiles + // those files, and reactCompilerCoverage.test.ts fails on real bailouts. + "react/purity": "warn", "react/refs": "error", "react/set-state-in-effect": "error", "react/set-state-in-render": "error", From 8f66e67de35ee42bc26d081d894f8c5eee6edf17 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 21 Oct 2026 00:42:07 -0700 Subject: [PATCH 08/73] test: local test runs use package configs and a clean env (#22714) Co-authored-by: Claude Opus 5.5 --- apps/desktop/vite.config.ts | 5 +- apps/mobile/vite.config.ts | 6 ++- apps/server/src/testUtils/gitConfig.setup.ts | 27 ++++++++++ apps/server/vite.config.ts | 6 ++- apps/web/vite.config.ts | 5 +- packages/client-runtime/vite.config.ts | 2 +- packages/shared/src/testing/providerEnv.ts | 22 ++++++++ vite.config.ts | 54 +++++++++++++++++--- 8 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 packages/shared/src/testing/providerEnv.ts diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index 65a6e7a2892f..c0a77e9a48b7 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -150,6 +150,9 @@ export default defineConfig({ // The Windows lane runs workspace suites concurrently; filesystem-heavy // desktop integration tests can exceed Vitest's 5 second default there. testTimeout: 15_000, - setupFiles: ["../../packages/shared/src/testing/longTempDir.ts"], + setupFiles: [ + "../../packages/shared/src/testing/longTempDir.ts", + "../../packages/shared/src/testing/providerEnv.ts", + ], }, }); diff --git a/apps/mobile/vite.config.ts b/apps/mobile/vite.config.ts index 8fc51ff74abb..3f66714c6881 100644 --- a/apps/mobile/vite.config.ts +++ b/apps/mobile/vite.config.ts @@ -20,7 +20,10 @@ export default defineConfig({ name: "unit", include: ["**/*.test.{ts,tsx,mjs}"], exclude: ["**/node_modules/**", ...reactTests], - setupFiles: ["../../packages/shared/src/testing/longTempDir.ts"], + setupFiles: [ + "../../packages/shared/src/testing/longTempDir.ts", + "../../packages/shared/src/testing/providerEnv.ts", + ], }, }, { @@ -30,6 +33,7 @@ export default defineConfig({ include: reactTests, // Native components mock React Native and render through react-dom in these tests. environment: "jsdom", + setupFiles: ["../../packages/shared/src/testing/providerEnv.ts"], }, }, ], diff --git a/apps/server/src/testUtils/gitConfig.setup.ts b/apps/server/src/testUtils/gitConfig.setup.ts index a8139563ff3b..19065fae6a9e 100644 --- a/apps/server/src/testUtils/gitConfig.setup.ts +++ b/apps/server/src/testUtils/gitConfig.setup.ts @@ -1,3 +1,9 @@ +// @effect-diagnostics nodeBuiltinImport:off - runs once at test setup, outside any Effect runtime. +import * as NodeChildProcess from "node:child_process"; +import * as NodeFS from "node:fs"; +import * as NodePath from "node:path"; +import * as HostProcess from "@t3tools/shared/HostProcess"; + // Git for Windows installs with core.autocrlf=true, which checks committed LF // files out as CRLF and breaks every byte-exact content assertion; a signing // key or a non-default init branch on the developer's machine breaks fixtures @@ -18,3 +24,24 @@ entries.forEach(([key, value], index) => { process.env[`GIT_CONFIG_KEY_${existing + index}`] = key; process.env[`GIT_CONFIG_VALUE_${existing + index}`] = value; }); + +// Without Homebrew git, macOS resolves `git` to /usr/bin/git, an xcrun shim that costs +// about 8 ms per spawn against 3 ms for the real binary. Git-heavy files such as +// GitVcsDriverCore.test.ts and GitManager.test.ts run twice as fast with the real one. +if (HostProcess.Platform.defaultValue() === "darwin") { + const gitOnPath = (process.env.PATH ?? "") + .split(NodePath.delimiter) + .map((dir) => NodePath.join(dir, "git")) + .find((candidate) => NodeFS.existsSync(candidate)); + if (gitOnPath === "/usr/bin/git") { + try { + const realGit = NodeChildProcess.execFileSync("xcrun", ["-f", "git"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + if (NodePath.isAbsolute(realGit)) { + process.env.PATH = `${NodePath.dirname(realGit)}${NodePath.delimiter}${process.env.PATH}`; + } + } catch {} + } +} diff --git a/apps/server/vite.config.ts b/apps/server/vite.config.ts index 5c6176b28c06..57549bec067f 100644 --- a/apps/server/vite.config.ts +++ b/apps/server/vite.config.ts @@ -58,8 +58,12 @@ const packExecutableTargets = packExecutableTarget ] : undefined; +// The root config's `projects` route repo-root runs to package configs, +// including this one. Inheriting them would make this config a container. +const { projects: _rootTestProjects, ...baseTestConfig } = baseConfig.test ?? {}; + export default mergeConfig( - baseConfig, + { ...baseConfig, test: baseTestConfig }, defineConfig({ run: { tasks: { diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 8158f66367df..6b91bf21de62 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -80,7 +80,10 @@ const unitTestProject = { exclude: ["src/**/*.browser.test.{ts,tsx}"], hookTimeout: 15_000, testTimeout: 15_000, - setupFiles: ["../../packages/shared/src/testing/longTempDir.ts"], + setupFiles: [ + "../../packages/shared/src/testing/longTempDir.ts", + "../../packages/shared/src/testing/providerEnv.ts", + ], }, } satisfies TestProjectInlineConfiguration; diff --git a/packages/client-runtime/vite.config.ts b/packages/client-runtime/vite.config.ts index 7f4ff74eca29..dbb29b7a4ef6 100644 --- a/packages/client-runtime/vite.config.ts +++ b/packages/client-runtime/vite.config.ts @@ -5,6 +5,6 @@ export default defineConfig({ test: { environment: "node", include: ["src/**/*.test.ts"], - setupFiles: ["../shared/src/testing/longTempDir.ts"], + setupFiles: ["../shared/src/testing/longTempDir.ts", "../shared/src/testing/providerEnv.ts"], }, }); diff --git a/packages/shared/src/testing/providerEnv.ts b/packages/shared/src/testing/providerEnv.ts new file mode 100644 index 000000000000..21bf59e52b50 --- /dev/null +++ b/packages/shared/src/testing/providerEnv.ts @@ -0,0 +1,22 @@ +// Agent shells carry real provider credentials (CLIProxy sets ANTHROPIC_AUTH_TOKEN and +// ANTHROPIC_BASE_URL) and the agent's own T3 state. Tests that read the ambient environment +// then pass or fail by who runs them. Tests that need one of these variables set it +// themselves. Set T3CODE_TEST_KEEP_PROVIDER_ENV=1 to keep them, for example for a live run. +const PROVIDER_ENV_PREFIXES = ["ANTHROPIC_", "CLAUDE_", "CODEX_", "OPENAI_", "T3CODE_"]; + +const KEPT_ENV = new Set([ + // CI sets these so the transfer budget integration test writes its report for the job summary. + "T3CODE_TRANSFER_BUDGET_REPORT_PATH", + "T3CODE_TRANSFER_BUDGET_RESULT_PATH", + // Opt-in: points the HtmlRender tests at a headless browser, which they skip without. + "T3CODE_TEST_HEADLESS_SHELL", +]); + +if (process.env.T3CODE_TEST_KEEP_PROVIDER_ENV !== "1") { + for (const key of Object.keys(process.env)) { + if (KEPT_ENV.has(key)) continue; + if (PROVIDER_ENV_PREFIXES.some((prefix) => key.startsWith(prefix))) { + delete process.env[key]; + } + } +} diff --git a/vite.config.ts b/vite.config.ts index 3c66ae3a5d3c..61430801adc5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,30 @@ import * as NodeURL from "node:url"; const DOM_TEST_ENVIRONMENT_ALLOWLIST: Array = []; +const TEST_EXCLUDE = [ + "**/.repos/**", + "**/.t3/**", + "**/node_modules/**", + "**/dist/**", + "**/dist-electron/**", + "**/.{idea,git,cache,output,temp}/**", +]; + +// Directories whose tests belong to a package's own test config. desktop also +// owns the kde-snap-shot tests through its `include`. +const TEST_CONFIG_PACKAGES = [ + "apps/desktop", + "apps/mobile", + "apps/server", + "apps/web", + "packages/client-runtime", +]; + +// `vp test run ` from the repo root hands each file to its package's +// config. Packages that borrow this config (`--config ../../vite.config.ts`) run +// from their own directory, as CI does, so they keep one plain project. +const isRepoRootTestRun = process.cwd() === import.meta.dirname; + const RESTRICTED_IMPORT_PATHS = [ { name: "@t3tools/client-runtime", @@ -56,14 +80,7 @@ export default defineConfig({ }, test: { environment: "node", - exclude: [ - "**/.repos/**", - "**/.t3/**", - "**/node_modules/**", - "**/dist/**", - "**/dist-electron/**", - "**/.{idea,git,cache,output,temp}/**", - ], + exclude: TEST_EXCLUDE, hookTimeout: 60_000, testTimeout: 60_000, allowOnly: !process.env.CI, @@ -89,7 +106,28 @@ export default defineConfig({ NodeURL.fileURLToPath( new URL("./packages/shared/src/testing/longTempDir.ts", import.meta.url), ), + NodeURL.fileURLToPath( + new URL("./packages/shared/src/testing/providerEnv.ts", import.meta.url), + ), ], + ...(isRepoRootTestRun + ? { + projects: [ + ...TEST_CONFIG_PACKAGES, + { + extends: true, + test: { + name: "root", + exclude: [ + ...TEST_EXCLUDE, + ...TEST_CONFIG_PACKAGES.map((dir) => `${dir}/**`), + "native/kde-snap-shot/**", + ], + }, + }, + ], + } + : {}), }, staged: { "*": "vp fmt --no-error-on-unmatched-pattern", From ca049dc2053806a35d55c38d5c4e7f7e9cf13659 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Wed, 21 Oct 2026 02:45:02 -0500 Subject: [PATCH 09/73] fix(mobile): restore low-latency decoding for simulator streams (#22733) --- docs/internals/devices.md | 12 +- .../src/device/avcConfig.test.ts | 122 +++++++++++++++ .../client-runtime/src/device/avcConfig.ts | 141 ++++++++++++++++++ .../client-runtime/src/device/stream.test.ts | 30 ++++ packages/client-runtime/src/device/stream.ts | 6 +- 5 files changed, 306 insertions(+), 5 deletions(-) create mode 100644 packages/client-runtime/src/device/avcConfig.test.ts create mode 100644 packages/client-runtime/src/device/avcConfig.ts diff --git a/docs/internals/devices.md b/docs/internals/devices.md index b0d03ab76a64..6e91307f09d4 100644 --- a/docs/internals/devices.md +++ b/docs/internals/devices.md @@ -105,10 +105,14 @@ binary WebSocket; Android multiplexes SEMU-framed H.264 and JSON gestures over one WebSocket. [`deviceStream.ts`](../../apps/web/src/components/device/deviceStream.ts) speaks both so one panel covers both platforms. -Simulators encode H.264 High 5.1. Hardware decoders on some machines and all -headless browsers reject that profile, and WebCodecs is secure-context only, so -the viewer probes `isConfigSupported` and falls back to the MJPEG endpoint on -iOS. Android has no MJPEG; there the panel reports that it cannot decode. +Simulators encode H.264 High 5.1 without frame reordering, but the hub's fallback +encoder can omit the SPS VUI that declares its reorder limits. Chromium's hardware +decoder then retains frames for seconds despite `optimizeForLatency` and an empty +`decodeQueueSize`. The iOS AVCC viewer restores the missing metadata before +configuring the decoder so hardware decoding can remain enabled. WebCodecs is +secure-context only, so the viewer probes `isConfigSupported` and falls back to +MJPEG on iOS when decoding is unavailable. Android has no MJPEG; there the panel +reports that it cannot decode. serve-sim can also be locked to WebRTC, as on EAS Simulator, and then refuses AVCC with a 409 naming the transport. The viewer still asks for AVCC first, so diff --git a/packages/client-runtime/src/device/avcConfig.test.ts b/packages/client-runtime/src/device/avcConfig.test.ts new file mode 100644 index 000000000000..4b2ff9906f3f --- /dev/null +++ b/packages/client-runtime/src/device/avcConfig.test.ts @@ -0,0 +1,122 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { simulatorAvcConfig } from "./avcConfig.ts"; + +// Captured from the hub before and after its encoder falls back from low-latency rate control. +const lowLatency = Uint8Array.from([ + 1, 100, 0, 51, 255, 225, 0, 22, 39, 100, 0, 51, 172, 19, 20, 60, 4, 192, 20, 158, 106, 154, 129, + 1, 1, 3, 194, 1, 8, 248, 1, 0, 4, 40, 238, 60, 176, +]); +const fallback = Uint8Array.from([ + 1, 100, 0, 51, 255, 225, 0, 12, 39, 100, 0, 51, 172, 86, 128, 76, 1, 73, 230, 164, 1, 0, 4, 40, + 238, 60, 176, +]); +const repaired = Uint8Array.from([ + 1, 100, 0, 51, 255, 225, 0, 17, 39, 100, 0, 51, 172, 86, 128, 76, 1, 73, 230, 168, 6, 208, 68, 35, + 80, 1, 0, 4, 40, 238, 60, 176, +]); + +describe("simulator AVC reorder metadata", () => { + it("adds zero reordering and one reference frame to the captured fallback SPS", () => { + expect(simulatorAvcConfig(fallback)).toEqual(repaired); + expect(simulatorAvcConfig(repaired)).toBe(repaired); + }); + + it("preserves existing VUI and leaves the original codec description untouched", () => { + const original = fallback.slice(); + simulatorAvcConfig(fallback); + expect(fallback).toEqual(original); + expect(simulatorAvcConfig(lowLatency)).toBe(lowLatency); + }); + + it("repairs multiple SPS entries while preserving PPS and profile extensions", () => { + const extensions = [0xfd, 0xf8, 0xf8, 0]; + const description = Uint8Array.from([ + ...fallback.subarray(0, 5), + 0xe2, + ...fallback.subarray(6, 20), + ...lowLatency.subarray(6, 30), + ...fallback.subarray(20), + ...extensions, + ]); + expect(simulatorAvcConfig(description)).toEqual( + Uint8Array.from([ + ...description.subarray(0, 6), + ...repaired.subarray(6, 25), + ...lowLatency.subarray(6, 30), + ...fallback.subarray(20), + ...extensions, + ]), + ); + }); + + it("passes through every truncated prefix of the captured configurations", () => { + for (const config of [fallback, lowLatency]) { + for (let length = 0; length < config.length; length++) { + const truncated = config.subarray(0, length); + expect(simulatorAvcConfig(truncated)).toBe(truncated); + } + } + }); + + it("does not modify unsupported profiles, scaling lists, or invalid parameter-set lengths", () => { + const unsupportedProfile = fallback.slice(); + unsupportedProfile[9] = 110; + const scalingLists = fallback.slice(); + scalingLists[12] = scalingLists[12]! | 1; + const invalidSpsLength = fallback.slice(); + invalidSpsLength[6] = 0xff; + const invalidPpsLength = fallback.slice(); + invalidPpsLength[21] = 0xff; + for (const config of [unsupportedProfile, scalingLists, invalidSpsLength, invalidPpsLength]) { + expect(simulatorAvcConfig(config)).toBe(config); + } + }); + + it("requires valid SPS trailing bits before appending VUI metadata", () => { + const invalidPadding = fallback.slice(); + invalidPadding[19] = 0; + expect(simulatorAvcConfig(invalidPadding)).toBe(invalidPadding); + }); + + it("passes through invalid NAL headers and unterminated Exp-Golomb values", () => { + const forbiddenBit = fallback.slice(); + forbiddenBit[8] = forbiddenBit[8]! | 0x80; + expect(simulatorAvcConfig(forbiddenBit)).toBe(forbiddenBit); + const unterminated = fallback.slice(); + unterminated.fill(0, 12, 20); + expect(simulatorAvcConfig(unterminated)).toBe(unterminated); + const overlong = Uint8Array.from([ + ...fallback.subarray(0, 6), + 0, + 13, + 39, + 100, + 0, + 51, + 0, + 0, + 3, + 0, + 0, + 3, + 0, + 0, + 0, + ...fallback.subarray(20), + ]); + expect(simulatorAvcConfig(overlong)).toBe(overlong); + }); + + it("bounds parsing of oversized configurations and SPS data", () => { + const oversizedConfig = new Uint8Array(64 * 1024 + 1); + oversizedConfig.set(fallback); + expect(simulatorAvcConfig(oversizedConfig)).toBe(oversizedConfig); + const oversizedSps = new Uint8Array(6 + 2 + 4097 + 7); + oversizedSps.set(fallback.subarray(0, 6)); + oversizedSps.set([0x10, 1], 6); + oversizedSps.set(fallback.subarray(8, 20), 8); + oversizedSps.set(fallback.subarray(20), 8 + 4097); + expect(simulatorAvcConfig(oversizedSps)).toBe(oversizedSps); + }); +}); diff --git a/packages/client-runtime/src/device/avcConfig.ts b/packages/client-runtime/src/device/avcConfig.ts new file mode 100644 index 000000000000..1c600635799c --- /dev/null +++ b/packages/client-runtime/src/device/avcConfig.ts @@ -0,0 +1,141 @@ +const MAX_CONFIG_BYTES = 64 * 1024; +const MAX_SPS_BYTES = 4096; + +const unsignedGolomb = (value: number) => { + const bits = (value + 1).toString(2); + return "0".repeat(bits.length - 1) + bits; +}; + +function withReorderLimits(sps: Uint8Array): Uint8Array { + if (sps.length < 4 || sps.length > MAX_SPS_BYTES || (sps[0]! & 0x9f) !== 7) return sps; + const rbsp: number[] = []; + let zeros = 0; + for (let index = 1; index < sps.length; index++) { + const byte = sps[index]!; + if (zeros >= 2 && byte === 3) { + if (index + 1 >= sps.length || sps[index + 1]! > 3) return sps; + zeros = 0; + continue; + } + rbsp.push(byte); + zeros = byte === 0 ? zeros + 1 : 0; + } + const bits = rbsp.map((byte) => byte.toString(2).padStart(8, "0")).join(""); + let position = 0; + const read = (count: number) => { + if (position + count > bits.length) throw new Error("Truncated H.264 SPS"); + const value = Number.parseInt(bits.slice(position, position + count), 2); + position += count; + return value; + }; + const readUnsignedGolomb = () => { + let leadingZeros = 0; + while (read(1) === 0) { + if (++leadingZeros > 31) throw new Error("Invalid H.264 Exp-Golomb value"); + } + return 2 ** leadingZeros - 1 + (leadingZeros === 0 ? 0 : read(leadingZeros)); + }; + + try { + // Only handle the hub's 8-bit 4:2:0 High profile without scaling lists. + // Unknown encoder configurations pass through rather than guessing their layout. + if (read(8) !== 100) return sps; + read(16); + if (readUnsignedGolomb() > 31 || readUnsignedGolomb() !== 1) return sps; + if (readUnsignedGolomb() !== 0 || readUnsignedGolomb() !== 0) return sps; + read(1); + if (read(1) !== 0) return sps; + if (readUnsignedGolomb() > 12) return sps; + const pictureOrderCountType = readUnsignedGolomb(); + if (pictureOrderCountType === 0) { + if (readUnsignedGolomb() > 12) return sps; + } else if (pictureOrderCountType === 1) { + read(1); + readUnsignedGolomb(); + readUnsignedGolomb(); + const cycleLength = readUnsignedGolomb(); + if (cycleLength > 255) return sps; + for (let index = 0; index < cycleLength; index++) readUnsignedGolomb(); + } else if (pictureOrderCountType !== 2) return sps; + const maxReferenceFrames = readUnsignedGolomb(); + if (maxReferenceFrames > 16) return sps; + read(1); + if (readUnsignedGolomb() > 65535 || readUnsignedGolomb() > 65535) return sps; + if (read(1) === 0) read(1); + read(1); + if (read(1) !== 0) { + for (let index = 0; index < 4; index++) readUnsignedGolomb(); + } + const vuiPosition = position; + if (read(1) !== 0) return sps; + // With no VUI, only rbsp_stop_one_bit and byte padding may follow. + if (!/^10{0,7}$/.test(bits.slice(position))) return sps; + + // serve-sim disables frame reordering. Its fallback encoder omits the VUI, + // causing Chromium to infer a large reorder queue despite optimizeForLatency. + // Preserve the SPS and add VUI defaults with max_num_reorder_frames = 0. + // The flag bits enable VUI, omit its eight optional sections, and enable + // bitstream restrictions and motion_vectors_over_pic_boundaries_flag. + const restricted = + bits.slice(0, vuiPosition) + + "10000000011" + + [2, 1, 16, 16, 0, maxReferenceFrames].map(unsignedGolomb).join("") + + "1"; + const padded = restricted.padEnd(Math.ceil(restricted.length / 8) * 8, "0"); + const output = [sps[0]!]; + zeros = 0; + for (let index = 0; index < padded.length; index += 8) { + const byte = Number.parseInt(padded.slice(index, index + 8), 2); + if (zeros >= 2 && byte <= 3) { + output.push(3); + zeros = 0; + } + output.push(byte); + zeros = byte === 0 ? zeros + 1 : 0; + } + return Uint8Array.from(output); + } catch { + return sps; + } +} + +/** Repair missing reorder metadata only for serve-sim's non-reordering AVCC feed. */ +export function simulatorAvcConfig(description: Uint8Array): Uint8Array { + if (description.length < 7 || description.length > MAX_CONFIG_BYTES || description[0] !== 1) + return description; + const parameterSets = description[5]! & 0x1f; + if (parameterSets === 0) return description; + const parts: Uint8Array[] = [description.subarray(0, 6)]; + let position = 6; + let changed = false; + for (let index = 0; index < parameterSets; index++) { + if (position + 2 > description.length) return description; + const length = description[position]! * 256 + description[position + 1]!; + position += 2; + if (length === 0 || position + length > description.length) return description; + const sps = description.subarray(position, position + length); + const repaired = withReorderLimits(sps); + changed ||= repaired !== sps; + parts.push(Uint8Array.of(repaired.length >>> 8, repaired.length & 0xff), repaired); + position += length; + } + const suffix = position; + if (position >= description.length) return description; + const pictureParameterSets = description[position++]!; + for (let index = 0; index < pictureParameterSets; index++) { + if (position + 2 > description.length) return description; + const length = description[position]! * 256 + description[position + 1]!; + position += 2; + if (length === 0 || position + length > description.length) return description; + position += length; + } + if (!changed) return description; + parts.push(description.subarray(suffix)); + const output = new Uint8Array(parts.reduce((length, part) => length + part.length, 0)); + position = 0; + for (const part of parts) { + output.set(part, position); + position += part.length; + } + return output; +} diff --git a/packages/client-runtime/src/device/stream.test.ts b/packages/client-runtime/src/device/stream.test.ts index 5ffdddb26f2e..7da96cb3d75b 100644 --- a/packages/client-runtime/src/device/stream.test.ts +++ b/packages/client-runtime/src/device/stream.test.ts @@ -323,6 +323,36 @@ describe("shared device stream readiness and recovery", () => { vi.unstubAllGlobals(); }); + it("repairs the hub's fallback configuration without disabling hardware decoding", async () => { + const { client, videoBodies, Decoder } = recoveryFixture("ios", false); + try { + client.start(); + await vi.advanceTimersByTimeAsync(0); + videoBodies[0]!.enqueue( + Uint8Array.from( + envelope( + 1, + [ + 1, 100, 0, 51, 255, 225, 0, 12, 39, 100, 0, 51, 172, 86, 128, 76, 1, 73, 230, 164, 1, + 0, 4, 40, 238, 60, 176, + ], + ), + ), + ); + await vi.advanceTimersByTimeAsync(0); + expect(Decoder.isConfigSupported).toHaveBeenCalledExactlyOnceWith({ + codec: "avc1.640033", + description: Uint8Array.from([ + 1, 100, 0, 51, 255, 225, 0, 17, 39, 100, 0, 51, 172, 86, 128, 76, 1, 73, 230, 168, 6, 208, + 68, 35, 80, 1, 0, 4, 40, 238, 60, 176, + ]), + optimizeForLatency: true, + }); + } finally { + client.stop(); + } + }); + it("does not repaint a delayed JPEG seed over newer decoded video", async () => { const { client, videoBodies, drawImage, decodedFrame } = recoveryFixture("ios", false); let resolveBitmap!: (bitmap: ImageBitmap) => void; diff --git a/packages/client-runtime/src/device/stream.ts b/packages/client-runtime/src/device/stream.ts index 9318c50fe691..04388e3429de 100644 --- a/packages/client-runtime/src/device/stream.ts +++ b/packages/client-runtime/src/device/stream.ts @@ -10,6 +10,7 @@ import { import * as Schema from "effect/Schema"; import * as Option from "effect/Option"; import { createCanvasFrameSink, type DeviceFrameSink } from "./frame.ts"; +import { simulatorAvcConfig } from "./avcConfig.ts"; import { type DeviceHubAccess, withDeviceHubQuery } from "./hubAccess.ts"; import type { DevicePlatform } from "@t3tools/contracts"; @@ -759,7 +760,10 @@ export function createDeviceStreamClient( case "description": { awaitingKeyframe = true; const configured = await configureDecoder( - { codec: avcCodecString(chunk.payload), description: chunk.payload }, + { + codec: avcCodecString(chunk.payload), + description: simulatorAvcConfig(chunk.payload), + }, isCurrent, ); if (!isCurrent()) return; From ea1c49419e742c688cc4951d63545046a06ac8c0 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 21 Oct 2026 00:49:46 -0700 Subject: [PATCH 10/73] fix(marketing): typecheck cache no longer misses when the shell env changes (#22735) Co-authored-by: Claude Opus 5.5 --- apps/marketing/astro.config.mjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/marketing/astro.config.mjs b/apps/marketing/astro.config.mjs index 5ba3da4fba10..15a8a8f91633 100644 --- a/apps/marketing/astro.config.mjs +++ b/apps/marketing/astro.config.mjs @@ -1,5 +1,11 @@ import { defineConfig } from "astro/config"; +// Astro loads env with an empty prefix, and under `vp run` Vite+'s loadEnv then reports every +// variable to the task cache. Any env difference (TERM, VP_RAW_SUBCOMMAND, an agent's session +// vars) would rerun `astro check`. Without the runner client, loadEnv reads process.env directly; +// file tracking does not use this client. +delete process.env.VP_RUN_NODE_CLIENT_PATH; + export default defineConfig({ site: "https://t3.codes", server: { From 618761a2d6d615b2c0a07e5de4d82384a583f717 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 21 Oct 2026 00:50:22 -0700 Subject: [PATCH 11/73] build(setup): leaner installs on Mac and Linux, cross-device store warning, setup-done marker (#22718) Co-authored-by: Claude Opus 5.5 --- .pnpmfile.cjs | 21 ++++++++++ AGENTS.md | 1 + pnpm-lock.yaml | 2 + pnpm-workspace.yaml | 5 --- scripts/setup-worktree.ts | 87 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 .pnpmfile.cjs diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs new file mode 100644 index 000000000000..ff75832369d8 --- /dev/null +++ b/.pnpmfile.cjs @@ -0,0 +1,21 @@ +// Windows WSL dev mode runs the server under Linux from the same node_modules +// as the Windows client, so Windows installs also need Linux x64 glibc native +// modules. Other platforms install only their own binaries; setting this +// everywhere added ~2.2 GB of foreign binaries to every macOS install. +// Release scripts stage their own installs with explicit architectures. +module.exports = { + hooks: { + updateConfig(config) { + // oxlint-disable-next-line t3code/no-global-process-runtime -- pnpm loads this file before any Effect runtime exists. + if (process.platform !== "win32") return config; + return { + ...config, + supportedArchitectures: { + os: ["current", "linux"], + cpu: ["current", "x64"], + libc: ["current", "glibc"], + }, + }; + }, + }, +}; diff --git a/AGENTS.md b/AGENTS.md index e87e5bfb1d9f..034e505b06ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,6 +78,7 @@ The most common defect in this repo is a change that works on the path you teste ## Dev servers - `vp i` installs. Worktrees get this from the t3.json setup script; if module resolution looks broken, it probably did not run. +- Setup runs in the background. While `.t3/setup-running` names a live PID, wait for it and do not run `vp i`. It ends with `.t3/setup-done` or `.t3/setup-failed` (with the error tail). With no marker at all, setup never ran here, so run `vp i` yourself. - `vp run dev` starts server and web. In a worktree, state defaults to that worktree's gitignored `.t3`, which deliberately outranks an ambient `T3CODE_HOME` so you cannot land on shared state by accident. An explicit `--home-dir` still wins. - Ports derive from the worktree path and are stable across restarts, but read the real ones from the `[dev-runner]` line since occupied ports shift. - Sharing over the tailnet is three steps: run `vp run dev --share` in the background, wait for the `pairingUrl:` line in its output, then give that full URL to an unpaired browser. Do not wire up `tailscale serve` by hand, open the URL yourself, or consume the user's pairing link. A browser with the reusable dev cookie can use the bare origin. If a normal one-time token was consumed, mint a fresh one with `node apps/server/src/bin.ts pair`. It carries standard scopes, while the startup URL carries admin scopes needed for Connections settings. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c03fc7d0e3a..34458e07f726 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -166,6 +166,8 @@ overrides: packageExtensionsChecksum: sha256-EowbsmlBFaoO0Ypufw901RnvdyjKG5dcdk2FRbc7Dr0= +pnpmfileChecksum: sha256-lS/yS+IvX/sj4KLdjVeRsq/hRipJMILKWjIbzIC+czo= + patchedDependencies: '@clerk/expo-native-components@0.2.0': 7a808b92da8460750b85818552e13be3fb73d111691c7db76965970351e124e4 '@effect/vitest@4.0.5': 2731c9c3761c595f25ddc625a7d61ca376b389d5b3049ecb837c8940f21cfa02 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2fc5cd50e405..715492185e13 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -373,8 +373,3 @@ peerDependencyRules: # pnpm keeps optional deps inside the virtual store, so hoist it to the root. publicHoistPattern: - "@cursor/sdk-*" - -supportedArchitectures: - cpu: [current, x64] - libc: [current, glibc] - os: [current, linux] diff --git a/scripts/setup-worktree.ts b/scripts/setup-worktree.ts index cee8b567aa97..6fabf341682c 100644 --- a/scripts/setup-worktree.ts +++ b/scripts/setup-worktree.ts @@ -5,23 +5,102 @@ import * as NodePath from "node:path"; const ENV_FILES = [".env", NodePath.join("infra", "relay", ".env")]; +const worktree = NodePath.dirname(import.meta.dirname); + +// T3 runs setup in the background, so agents can start before it ends. +// `setup-running` holds this PID until setup ends with `setup-done` or +// `setup-failed`, so agents wait instead of racing it with their own `vp i`. +const markerDir = NodePath.join(worktree, ".t3"); +const runningMarker = NodePath.join(markerDir, "setup-running"); +const doneMarker = NodePath.join(markerDir, "setup-done"); +const failedMarker = NodePath.join(markerDir, "setup-failed"); +NodeFS.rmSync(doneMarker, { force: true }); +NodeFS.rmSync(failedMarker, { force: true }); +NodeFS.mkdirSync(markerDir, { recursive: true }); +NodeFS.writeFileSync(runningMarker, `${process.pid}\n`); +let failureDetail = ""; +process.on("uncaughtExceptionMonitor", (error) => { + failureDetail = error.stack ?? String(error); +}); +process.on("exit", (code) => { + if (code === 0) NodeFS.writeFileSync(doneMarker, ""); + else NodeFS.writeFileSync(failedMarker, `Setup exited with code ${code}.\n${failureDetail}\n`); + NodeFS.rmSync(runningMarker, { force: true }); +}); +// A cancelled setup must still leave a marker, or agents would wait forever. +// During install, the signal goes on to the installer and setup exits when the +// install output closes. Any child the shell leaves behind holds that output +// open, so no marker appears while `vp i` can still write node_modules. +let cancelSignal: NodeJS.Signals | undefined; +let activeInstall: NodeChildProcess.ChildProcess | undefined; +for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) { + process.on(signal, () => { + cancelSignal = signal; + if (activeInstall) activeInstall.kill(signal); + else process.exit(1); + }); +} + const projectRoot = process.env.T3CODE_PROJECT_ROOT; if (!projectRoot) { throw new Error("T3CODE_PROJECT_ROOT is not set. Run this through the t3.json setup action."); } -const worktree = NodePath.dirname(import.meta.dirname); // `shell` resolves `vp` through PATH, including Windows command shims. // Setup has no one answering install prompts. Pass pnpm's confirmation flag // through vp and leave stdin closed so other prompts fail instead of hanging. // Install exactly the locked dependencies so setup never rewrites the lockfile. // A stale lockfile must fail setup rather than silently resolving a new one. -const install = NodeChildProcess.spawnSync("vp i --frozen-lockfile -- --yes", { +// Output is piped, not inherited, so a failure can keep its tail in the marker. +const install = NodeChildProcess.spawn("vp i --frozen-lockfile -- --yes", { cwd: worktree, shell: true, - stdio: ["ignore", "inherit", "inherit"], + stdio: ["ignore", "pipe", "pipe"], }); -if (install.status !== 0) process.exit(install.status ?? 1); +activeInstall = install; +let installOutput = ""; +for (const [source, target] of [ + [install.stdout, process.stdout], + [install.stderr, process.stderr], +] as const) { + source.setEncoding("utf8"); + source.on("data", (chunk: string) => { + target.write(chunk); + installOutput = (installOutput + chunk).slice(-8_000); + }); +} +const installStatus = await new Promise((resolve) => install.on("close", resolve)); +activeInstall = undefined; +if (installStatus !== 0 || cancelSignal) { + failureDetail = cancelSignal + ? `Cancelled by ${cancelSignal}.` + : installOutput.trimEnd().split("\n").slice(-30).join("\n"); + process.exit(installStatus || 1); +} + +// pnpm cannot clone or hardlink across filesystems, so a store on another +// device silently turns every worktree install into a full copy. +const storeDir: unknown = JSON.parse( + NodeFS.readFileSync(NodePath.join(worktree, "node_modules", ".modules.yaml"), "utf8"), +).storeDir; +if ( + typeof storeDir === "string" && + NodeFS.statSync(storeDir).dev !== NodeFS.statSync(NodePath.join(worktree, "node_modules")).dev +) { + process.stderr.write( + [ + "", + "WARNING: the pnpm store is on a different filesystem than this worktree.", + ` store: ${storeDir}`, + ` worktree: ${worktree}`, + "pnpm cannot hardlink or clone across filesystems, so it copied every package into node_modules.", + "Fix: put the store on the worktree's filesystem, for example:", + ` pnpm config set store-dir ${JSON.stringify(NodePath.join(NodePath.dirname(worktree), ".pnpm-store"))} --global`, + "", + "", + ].join("\n"), + ); +} // Env files live as real files in the main checkout; worktrees only get // symlinks to them. Only a symlink is ever replaced, so a real env file is From f4a3843a1c8ce2b65b535c3bfc610ea408310d1e Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 21 Oct 2026 01:07:46 -0700 Subject: [PATCH 12/73] build: upgrade pnpm to 12 for 4x faster installs (#22738) Co-authored-by: Claude Opus 5.5 --- package.json | 2 +- pnpm-lock.yaml | 158 ++++++++++++++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 12 +++- 3 files changed, 170 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 53580ef428a2..363a9261163c 100644 --- a/package.json +++ b/package.json @@ -63,5 +63,5 @@ "engines": { "node": "^24.13.1" }, - "packageManager": "pnpm@11.10.0" + "packageManager": "pnpm@12.14.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34458e07f726..ad93d5d4a425 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,3 +1,161 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.14.0 + version: 12.14.0 + +packages: + + '@pnpm/exe.android-arm64@12.14.0': + resolution: {integrity: sha512-+iSh7x21C3A2hqc+Z0ZmbKTcn6YNlzrXM142PsnQH/dLj0DYxFzoBvc6XAnL/yfQ5nnH2ebBiRj/zVxr61fZbQ==} + cpu: [arm64] + os: [android] + + '@pnpm/exe.android-x64@12.14.0': + resolution: {integrity: sha512-qT+0LqEr2wBVaxcG2T8cX/R8SSRz2Rvpp8nRR4fh8tnGUZRch87gFmsHUAoSQh7w6j1h0qTjrA384j4xi8frvw==} + cpu: [x64] + os: [android] + + '@pnpm/exe.darwin-arm64@12.14.0': + resolution: {integrity: sha512-tnrdUB8ecJoehuKKqi1YHCBVl61xtJ9XVGcx5EviC6fWO4DeGI8GtXrjvJxDBM3nuVjJeWistgsUp/N142x4sw==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.14.0': + resolution: {integrity: sha512-vlBfGkruqQRsEHTW43cPwpYd7QlbIJ//cvpAdb3bVXkI8SgTPoXhCnc05m+ZNiPrrEkBsX/t7KHw9aXpkzPm3Q==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.freebsd-x64@12.14.0': + resolution: {integrity: sha512-Qg5rxBrGYl6Tfso+EyygPf84fY1Fzy55E/0or+iuYKtnf0vc0MlHJfVIlT7pBukMRntQHRik7hUi0Z4s7AlJsw==} + cpu: [x64] + os: [freebsd] + + '@pnpm/exe.linux-arm64-musl@12.14.0': + resolution: {integrity: sha512-DRIe4yQkXBGA/PhVJdzlbnllxBdAtnMMvTVLLcGpsK5FvJz9kxD2Hyxtn68ovmNkVX06f6p45cQwfm3wx2jn1A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.14.0': + resolution: {integrity: sha512-7joGShZ44Nk3WpNU+60JoOPjCXlgxhJAq11S2UiVtwCUDgTer1yTPPfxGW8pxaD6046actYLIYDbZNlubHWs0A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-ppc64@12.14.0': + resolution: {integrity: sha512-gSoryn7ySdXd//gjgf3MvvA8ZerJayaSbuMZMtAWq4vb2/wlVEgpl0Kvqt54y58TRj533Qjq2txKgnxd4JyoAQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-riscv64@12.14.0': + resolution: {integrity: sha512-EMjew1H9WqQ7pL+tYqCPwiuB+0PeCiJRV7fyWeuFw0wwYUajFBjcaPO3IGAVu9sdmJDU+e+HYv/Zey6aKae0Vw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-s390x@12.14.0': + resolution: {integrity: sha512-shYOj/SFoXD35fHe0pWr2JofwSXulC0BnEIXLj2QokDi0EF//AfKsui35yKIkTEl+s2VJybc3PO4iJNhKIj5Qw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.14.0': + resolution: {integrity: sha512-btZiZ2Ot0llKK1ndSfA5i3IRX7/S0VrmzseNH/BJ4c+Cady+S81H6vvU1zByggHMyqQhC8OgxO5PiIY7D9bH0Q==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.14.0': + resolution: {integrity: sha512-F6K8e/jWxiuEYJaQtRRUG9ynBSTEHa+t7Lf86MKuOO5UA7WVVMbi+oB8nY6iKW++ncWS2yG6T9faXZGxqNsfoQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.14.0': + resolution: {integrity: sha512-DOjlkVB6TtTKeAAe0hNZXn5S7YqESmo1Z6SGbR4BZNQ4isRg31XVHjX2RFTbY8gnZq68IwzyLmPqlEjMmYxofg==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.14.0': + resolution: {integrity: sha512-/LSX6mew/sLf08XqcjemuklNKf1K/Ftpgj4//hK/HKzI2dKbmxDB27a/2OaQ5a0O3/3OFzlssXDziZsi+yVBwQ==} + cpu: [x64] + os: [win32] + + pnpm@12.14.0: + resolution: {integrity: sha512-wYum2HmT09Fky16JSPMrLAbuhUSH4GzY2GeBNC+z5btGNDZwNHEehoArqpw3vqkGzhkprQjXCITJfYAd8TOJpg==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.android-arm64@12.14.0': + optional: true + + '@pnpm/exe.android-x64@12.14.0': + optional: true + + '@pnpm/exe.darwin-arm64@12.14.0': + optional: true + + '@pnpm/exe.darwin-x64@12.14.0': + optional: true + + '@pnpm/exe.freebsd-x64@12.14.0': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.14.0': + optional: true + + '@pnpm/exe.linux-arm64@12.14.0': + optional: true + + '@pnpm/exe.linux-ppc64@12.14.0': + optional: true + + '@pnpm/exe.linux-riscv64@12.14.0': + optional: true + + '@pnpm/exe.linux-s390x@12.14.0': + optional: true + + '@pnpm/exe.linux-x64-musl@12.14.0': + optional: true + + '@pnpm/exe.linux-x64@12.14.0': + optional: true + + '@pnpm/exe.win32-arm64@12.14.0': + optional: true + + '@pnpm/exe.win32-x64@12.14.0': + optional: true + + pnpm@12.14.0: + optionalDependencies: + '@pnpm/exe.android-arm64': 12.14.0 + '@pnpm/exe.android-x64': 12.14.0 + '@pnpm/exe.darwin-arm64': 12.14.0 + '@pnpm/exe.darwin-x64': 12.14.0 + '@pnpm/exe.freebsd-x64': 12.14.0 + '@pnpm/exe.linux-arm64': 12.14.0 + '@pnpm/exe.linux-arm64-musl': 12.14.0 + '@pnpm/exe.linux-ppc64': 12.14.0 + '@pnpm/exe.linux-riscv64': 12.14.0 + '@pnpm/exe.linux-s390x': 12.14.0 + '@pnpm/exe.linux-x64': 12.14.0 + '@pnpm/exe.linux-x64-musl': 12.14.0 + '@pnpm/exe.win32-arm64': 12.14.0 + '@pnpm/exe.win32-x64': 12.14.0 + +--- lockfileVersion: '9.0' settings: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 715492185e13..d87dd95cd060 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,7 +5,7 @@ packages: - packages/* - scripts -# Successor to onlyBuiltDependencies/ignoredBuiltDependencies (pnpm 11). +# Successor to onlyBuiltDependencies/ignoredBuiltDependencies (pnpm 11+). # true = allowed to run build scripts; false mirrors the pnpm 10 behavior # where anything outside onlyBuiltDependencies was silently not built. allowBuilds: @@ -368,6 +368,16 @@ peerDependencyRules: allowedVersions: vite: "*" +# pnpm 12 links agent skills that dependencies ship into .claude/skills once +# approved. We curate our own skills, so deny these to keep installs quiet. +permissions: + "@tanstack/router-plugin": + skills: false + drizzle-kit: + skills: false + expo-sqlite: + skills: false + # @cursor/sdk finds its platform package (sandbox helper, rg, tree-sitter) by # walking up from the entry script for node_modules/@cursor/sdk--. # pnpm keeps optional deps inside the virtual store, so hoist it to the root. From 76e4aac3e9b90ae2395bb62fa540e90b43bd70a6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 21 Oct 2026 01:12:48 -0700 Subject: [PATCH 13/73] refactor(web): the timeline minimap lives in its own file (#22662) Co-authored-by: Claude Opus 5.5 --- .../src/components/chat/MessagesTimeline.tsx | 335 +----------------- .../src/components/chat/TimelineMinimap.tsx | 335 ++++++++++++++++++ 2 files changed, 337 insertions(+), 333 deletions(-) create mode 100644 apps/web/src/components/chat/TimelineMinimap.tsx diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 1e591b568363..bb7f613602f5 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -16,11 +16,7 @@ import { getQuestionTextPreview, hasQuestionAnswer, } from "@t3tools/client-runtime/work-log/user-input"; -import { - deriveTimelineMinimapItems, - resolveTimelineMinimapPreview, - type TimelineMinimapItem, -} from "./timelineMinimapItems"; +import { deriveTimelineMinimapItems } from "./timelineMinimapItems"; import { COMPOSER_CONTEXT_KINDS, ORCHESTRATION_V2_WORKSPACE_PREPARATION_FAILURE_CODE, @@ -127,7 +123,6 @@ import { CheckIcon, ChevronDownIcon, ChevronRightIcon, - ChevronUpIcon, CircleAlertIcon, DownloadIcon, EyeIcon, @@ -210,12 +205,7 @@ import { resolveTimelineMinimapCurrentIndex, isTimelineMinimapRowVisible, resolveTimelineMinimapVisibleRange, - resolveTimelineMinimapHeightStyle, resolveTimelineMinimapHitStripWidth, - resolveTimelineMinimapIndexFromPointer, - resolveTimelineMinimapInteractiveWidth, - resolveTimelineMinimapNavigationInteractive, - resolveTimelineMinimapTopPercent, resolveWorkGroupScrollIndex, shouldCollapseUserMessage, shouldFollowWorkGroupAppend, @@ -230,7 +220,6 @@ import { worktreeSetupAgentStarted, type StableMessagesTimelineRowsState, type MessagesTimelineRow, - TIMELINE_MINIMAP_MIN_ITEMS, type TimelineLatestRun, type WorkGroupScrollAnchor, } from "./MessagesTimeline.logic"; @@ -309,6 +298,7 @@ import { formatReviewCommentFence, type ReviewCommentContext, } from "../../reviewCommentContext"; +import { TimelineMinimap } from "./TimelineMinimap"; interface TimelineRowSharedState { citationRequest: AssistantCitationTarget | null; @@ -1630,327 +1620,6 @@ function getItemType(item: MessagesTimelineRow) { return `message:${item.message.role}:${lengthBucket}`; } -function timelineMinimapEventTargetsPreview(target: EventTarget): boolean { - return target instanceof Element && target.closest("[data-minimap-preview]") !== null; -} - -// A release outside the window can skip `mouseup`, so the next button-free -// `mousemove` also counts. -function onMouseButtonsReleased(onRelease: () => void): () => void { - const check = (event: globalThis.MouseEvent) => { - if (event.buttons !== 0) return; - stop(); - onRelease(); - }; - const stop = () => { - window.removeEventListener("mouseup", check); - window.removeEventListener("mousemove", check); - }; - window.addEventListener("mouseup", check); - window.addEventListener("mousemove", check); - return stop; -} - -function TimelineMinimap({ - hasPersistentGutter, - hitStripWidth, - currentIndex, - items, - stripMap, - onWheel, - onSelect, -}: { - hasPersistentGutter: boolean; - hitStripWidth: number; - currentIndex: number | null; - items: ReadonlyArray; - stripMap: Map; - onWheel: (event: WheelEvent) => void; - onSelect: (item: TimelineMinimapItem) => void; -}) { - const [activeIndex, setActiveIndex] = useState(null); - const wheelTargetRef = (node: HTMLDivElement | null) => { - if (!node) return; - node.addEventListener("wheel", onWheel, { passive: false }); - return () => node.removeEventListener("wheel", onWheel); - }; - - const pressedOnStripRef = useRef(false); - const [passThrough, setPassThrough] = useState(false); - - useEffect(() => { - if (!passThrough) return; - return onMouseButtonsReleased(() => setPassThrough(false)); - }, [passThrough]); - - const resolvedActiveIndex = - activeIndex !== null && activeIndex < items.length ? activeIndex : null; - const activeItem = resolveTimelineMinimapPreview( - resolvedActiveIndex === null ? null : (items[resolvedActiveIndex] ?? null), - ); - const navigationInteractive = - resolveTimelineMinimapNavigationInteractive(hitStripWidth) && !passThrough; - const activeTopPercent = - resolvedActiveIndex === null - ? 0 - : resolveTimelineMinimapTopPercent(resolvedActiveIndex, items.length); - const activeTooltipTranslate = - resolvedActiveIndex === null - ? "-50%" - : resolvedActiveIndex === 0 - ? "0%" - : resolvedActiveIndex === items.length - 1 - ? "-100%" - : "-50%"; - const resolvedCurrentIndex = - currentIndex !== null && currentIndex >= 0 && currentIndex < items.length ? currentIndex : null; - const previousItem = - resolvedCurrentIndex === null ? null : (items[resolvedCurrentIndex - 1] ?? null); - const nextItem = resolvedCurrentIndex === null ? null : (items[resolvedCurrentIndex + 1] ?? null); - - const resolveActiveIndexFromPointer = (event: MouseEvent) => { - const rect = event.currentTarget.getBoundingClientRect(); - return resolveTimelineMinimapIndexFromPointer({ - itemCount: items.length, - railTop: rect.top, - railHeight: rect.height, - pointerY: event.clientY, - }); - }; - - const updateActiveIndexFromPointer = (event: MouseEvent) => { - const nextIndex = resolveActiveIndexFromPointer(event); - setActiveIndex(nextIndex); - }; - - const moveActiveIndex = (delta: number) => { - setActiveIndex((current) => { - const base = current ?? 0; - return Math.max(0, Math.min(items.length - 1, base + delta)); - }); - }; - - if (items.length < TIMELINE_MINIMAP_MIN_ITEMS) { - return null; - } - - return ( - - ); -} - -function TimelineMinimapNavigationButton({ - direction, - disabled, - interactive, - onClick, -}: { - direction: "previous" | "next"; - disabled: boolean; - interactive: boolean; - onClick: () => void; -}) { - const previous = direction === "previous"; - const label = previous ? "Previous turn" : "Next turn"; - const Icon = previous ? ChevronUpIcon : ChevronDownIcon; - - return ( - - - } - > - - - {label} - - ); -} - type TimelineWorkEntry = Extract["groupedEntries"][number]; type TimelineRow = MessagesTimelineRow; diff --git a/apps/web/src/components/chat/TimelineMinimap.tsx b/apps/web/src/components/chat/TimelineMinimap.tsx new file mode 100644 index 000000000000..c4cc203e8a75 --- /dev/null +++ b/apps/web/src/components/chat/TimelineMinimap.tsx @@ -0,0 +1,335 @@ +import { resolveTimelineMinimapPreview, type TimelineMinimapItem } from "./timelineMinimapItems"; +import { useEffect, useRef, useState, type MouseEvent } from "react"; +import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; +import { Button } from "../ui/button"; +import { + resolveTimelineMinimapHeightStyle, + resolveTimelineMinimapIndexFromPointer, + resolveTimelineMinimapInteractiveWidth, + resolveTimelineMinimapNavigationInteractive, + resolveTimelineMinimapTopPercent, + TIMELINE_MINIMAP_MIN_ITEMS, +} from "./MessagesTimeline.logic"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { cn } from "~/lib/utils"; + +function timelineMinimapEventTargetsPreview(target: EventTarget): boolean { + return target instanceof Element && target.closest("[data-minimap-preview]") !== null; +} + +// A release outside the window can skip `mouseup`, so the next button-free +// `mousemove` also counts. +function onMouseButtonsReleased(onRelease: () => void): () => void { + const check = (event: globalThis.MouseEvent) => { + if (event.buttons !== 0) return; + stop(); + onRelease(); + }; + const stop = () => { + window.removeEventListener("mouseup", check); + window.removeEventListener("mousemove", check); + }; + window.addEventListener("mouseup", check); + window.addEventListener("mousemove", check); + return stop; +} + +export function TimelineMinimap({ + hasPersistentGutter, + hitStripWidth, + currentIndex, + items, + stripMap, + onWheel, + onSelect, +}: { + hasPersistentGutter: boolean; + hitStripWidth: number; + currentIndex: number | null; + items: ReadonlyArray; + stripMap: Map; + onWheel: (event: WheelEvent) => void; + onSelect: (item: TimelineMinimapItem) => void; +}) { + const [activeIndex, setActiveIndex] = useState(null); + const wheelTargetRef = (node: HTMLDivElement | null) => { + if (!node) return; + node.addEventListener("wheel", onWheel, { passive: false }); + return () => node.removeEventListener("wheel", onWheel); + }; + + const pressedOnStripRef = useRef(false); + const [passThrough, setPassThrough] = useState(false); + + useEffect(() => { + if (!passThrough) return; + return onMouseButtonsReleased(() => setPassThrough(false)); + }, [passThrough]); + + const resolvedActiveIndex = + activeIndex !== null && activeIndex < items.length ? activeIndex : null; + const activeItem = resolveTimelineMinimapPreview( + resolvedActiveIndex === null ? null : (items[resolvedActiveIndex] ?? null), + ); + const navigationInteractive = + resolveTimelineMinimapNavigationInteractive(hitStripWidth) && !passThrough; + const activeTopPercent = + resolvedActiveIndex === null + ? 0 + : resolveTimelineMinimapTopPercent(resolvedActiveIndex, items.length); + const activeTooltipTranslate = + resolvedActiveIndex === null + ? "-50%" + : resolvedActiveIndex === 0 + ? "0%" + : resolvedActiveIndex === items.length - 1 + ? "-100%" + : "-50%"; + const resolvedCurrentIndex = + currentIndex !== null && currentIndex >= 0 && currentIndex < items.length ? currentIndex : null; + const previousItem = + resolvedCurrentIndex === null ? null : (items[resolvedCurrentIndex - 1] ?? null); + const nextItem = resolvedCurrentIndex === null ? null : (items[resolvedCurrentIndex + 1] ?? null); + + const resolveActiveIndexFromPointer = (event: MouseEvent) => { + const rect = event.currentTarget.getBoundingClientRect(); + return resolveTimelineMinimapIndexFromPointer({ + itemCount: items.length, + railTop: rect.top, + railHeight: rect.height, + pointerY: event.clientY, + }); + }; + + const updateActiveIndexFromPointer = (event: MouseEvent) => { + const nextIndex = resolveActiveIndexFromPointer(event); + setActiveIndex(nextIndex); + }; + + const moveActiveIndex = (delta: number) => { + setActiveIndex((current) => { + const base = current ?? 0; + return Math.max(0, Math.min(items.length - 1, base + delta)); + }); + }; + + if (items.length < TIMELINE_MINIMAP_MIN_ITEMS) { + return null; + } + + return ( + + ); +} + +function TimelineMinimapNavigationButton({ + direction, + disabled, + interactive, + onClick, +}: { + direction: "previous" | "next"; + disabled: boolean; + interactive: boolean; + onClick: () => void; +}) { + const previous = direction === "previous"; + const label = previous ? "Previous turn" : "Next turn"; + const Icon = previous ? ChevronUpIcon : ChevronDownIcon; + + return ( + + + } + > + + + {label} + + ); +} From bea879025debc5b3f0c6c62949e3d2cb02ffc6e5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 21 Oct 2026 01:12:49 -0700 Subject: [PATCH 14/73] refactor(web): timeline work entry rows and tool icons live in their own files (#22674) Co-authored-by: Claude Opus 5.5 --- .../chat/MessagesTimeline.browser.test.tsx | 3 +- .../src/components/chat/MessagesTimeline.tsx | 1242 +---------------- .../src/components/chat/TimelineRowParts.tsx | 55 + .../chat/TimelineToolActivityIcon.tsx | 272 ++++ .../chat/TimelineWorkEntryRow.logic.ts | 236 ++++ .../components/chat/TimelineWorkEntryRow.tsx | 617 ++++++++ .../src/components/chat/timelineRowContext.ts | 99 ++ 7 files changed, 1321 insertions(+), 1203 deletions(-) create mode 100644 apps/web/src/components/chat/TimelineRowParts.tsx create mode 100644 apps/web/src/components/chat/TimelineToolActivityIcon.tsx create mode 100644 apps/web/src/components/chat/TimelineWorkEntryRow.logic.ts create mode 100644 apps/web/src/components/chat/TimelineWorkEntryRow.tsx create mode 100644 apps/web/src/components/chat/timelineRowContext.ts diff --git a/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx b/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx index df846a5e7bf5..ddf8d87049fa 100644 --- a/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.browser.test.tsx @@ -6,7 +6,8 @@ import { describe, expect, it, vi } from "vite-plus/test"; import "~/index.css"; import { act, render } from "../../../test/render"; -import { MessagesTimeline, remarkThoughtPreview } from "./MessagesTimeline"; +import { MessagesTimeline } from "./MessagesTimeline"; +import { remarkThoughtPreview } from "./TimelineWorkEntryRow.logic"; import { parseMarkdownWithMath } from "@t3tools/shared/markdownMath"; const pullRequestQuery = vi.hoisted(() => ({ error: "Pull request not found" })); diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index bb7f613602f5..2bcd94805f26 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1,31 +1,27 @@ import { useRenderedAppearance } from "../../hooks/useTheme"; -import type { Root, RootContent } from "mdast"; import { ThreadFindTimelineContext } from "./ThreadFindProvider"; import { shallow } from "zustand/vanilla/shallow"; import { shouldPreserveAssistantLineBreaks } from "@t3tools/shared/markdownPipeline"; import { MarkdownFindContext, findRevealRef } from "./markdownFindContext"; import { useChatCanvas } from "./ChatCanvasContext"; -import { WorkLogBlock, WorkLogButton, WorkLogDetails, WorkLogList, WorkLogRow } from "./WorkLog"; +import { WorkLogBlock, WorkLogButton, WorkLogList, WorkLogRow } from "./WorkLog"; import { PullRequestGlyph } from "../pullRequest/pullRequestIcons"; import type { WorktreeSetupSnapshot } from "@t3tools/contracts"; import { ReadOnlySourcePreview } from "../files/AttachmentFilePreview"; import { useRightPanelStore } from "~/rightPanelStore"; import { getQuestionAnswerPreview, - getQuestionAnswerText, getQuestionTextPreview, hasQuestionAnswer, } from "@t3tools/client-runtime/work-log/user-input"; import { deriveTimelineMinimapItems } from "./timelineMinimapItems"; import { COMPOSER_CONTEXT_KINDS, - ORCHESTRATION_V2_WORKSPACE_PREPARATION_FAILURE_CODE, type AssistantCitation, type EnvironmentId, type MessageId, type OrchestrationV2TurnItem, type RunAttemptId, - type ScopedThreadRef, type ServerProviderSkill, type RunId, ThreadId, @@ -37,19 +33,9 @@ import { environmentThreadDetails } from "../../state/threads"; import { resolveUserMessagePresentation } from "@t3tools/client-runtime/user-message"; import { Link } from "@tanstack/react-router"; import { canForkProjectedAssistantItem } from "@t3tools/client-runtime/state/thread-workflows"; -import { notificationChildThreadId } from "@t3tools/client-runtime/state/thread-execution"; import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences"; -import { - liveThoughtLine, - resolveWorkEntryToolPresentation, - resolveViewedImageAsset, - workEntryViewedImagePath, -} from "@t3tools/client-runtime/work-log/presentation"; +import { liveThoughtLine } from "@t3tools/client-runtime/work-log/presentation"; import { resolveWorkGroupScrollAnchor } from "@t3tools/client-runtime/work-log/scroll-anchor"; -import { - turnItemHasDetail, - turnItemNeedsDetailFetch, -} from "@t3tools/client-runtime/work-log/item-detail"; import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments"; import { subagentGroupSummary, @@ -63,13 +49,9 @@ const NOOP_USE_ARTIFACT_TEMPLATE = () => {}; const NOOP_OPEN_ATTACHMENT = (_attachment: ChatFileAttachment) => {}; import { resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList"; -import { toolActivityFaviconUrl } from "@t3tools/shared/favicon"; import { formatDuration } from "@t3tools/shared/orchestrationTiming"; -import { getProjectFaviconCacheKey } from "@t3tools/shared/projectFavicon"; -import { claudeSkillInvocation, isJavaScriptReplTool } from "@t3tools/shared/toolActivity"; import { observeVisibleAnimation } from "../../lib/visibleAnimation"; import { - createContext, memo, use, useContext, @@ -77,8 +59,6 @@ import { useLayoutEffect, useRef, useState, - type KeyboardEvent, - type MouseEvent, type ReactNode, } from "react"; import { @@ -94,8 +74,6 @@ import { createMessageAttachmentPreviewProjector, selectMessageImageResources, workEntryDisplayIndicatesToolFailure, - workEntrySignalsSevereFailure, - workLogEntryIsToolLike, } from "../../session-logic"; import type { CodexArtifactTemplate } from "@t3tools/shared/codexArtifactTemplates"; import { @@ -113,21 +91,17 @@ import { resolveFileDiffPath, } from "../../lib/diffRendering"; import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting"; -import ChatMarkdown, { ChatMarkdownAssetImage } from "../ChatMarkdown"; -import { T3Wordmark } from "../T3Wordmark"; +import ChatMarkdown from "../ChatMarkdown"; import { ThreadContextChip } from "../ThreadContextChip"; import { ArrowRightIcon, BotIcon, - BrainIcon, CheckIcon, ChevronDownIcon, - ChevronRightIcon, CircleAlertIcon, DownloadIcon, EyeIcon, GitForkIcon, - GlobeIcon, type LucideIcon, MessageCircleIcon, MousePointerClickIcon, @@ -135,17 +109,10 @@ import { MinusIcon, Redo2Icon, Minimize2Icon, - SearchIcon, - SmartphoneIcon, - SquarePenIcon, - TerminalIcon, Undo2Icon, - HammerIcon, WrenchIcon, - Code2Icon, XIcon, ZapIcon, - RotateCcwIcon, } from "lucide-react"; import { ChevronDown, ChevronRight, Link as LinkIconNode } from "lucide"; import type { @@ -188,11 +155,7 @@ import { PierreEntryIcon } from "./PierreEntryIcon"; import { WorkspaceEntryIcon, WorkspaceEntryTooltip } from "./WorkspaceEntryIcon"; import { AssistantSelectionToolbar } from "./AssistantSelectionToolbar"; import type { AssistantCitationSourceAnchor } from "~/lib/assistantTextSelection"; -import { - AssistantCitationSource, - type AssistantCitationRequest, - type AssistantCitationTarget, -} from "./AssistantCitationSource"; +import { AssistantCitationSource, type AssistantCitationRequest } from "./AssistantCitationSource"; import { useAssistantCitationTarget, type CitationHistoryPage } from "./useAssistantCitationTarget"; import { computeStableMessagesTimelineRows, @@ -210,18 +173,11 @@ import { shouldCollapseUserMessage, shouldFollowWorkGroupAppend, timelineTurnFoldRunIdsByEntryId, - threadReadLabelPrefix, - threadReadTargetId, - threadReadTargetTitle, - toolGroupAction, - workEntryDisplayLabel, - workEntryReadOutput, workEntryIsVisibleInGroup, worktreeSetupAgentStarted, type StableMessagesTimelineRowsState, type MessagesTimelineRow, type TimelineLatestRun, - type WorkGroupScrollAnchor, } from "./MessagesTimeline.logic"; import { type ThreadFindMatch } from "./threadFind"; import { useThreadFindNavigation } from "./useThreadFindNavigation"; @@ -269,19 +225,14 @@ import { useMediaQuery } from "~/hooks/useMediaQuery"; import { cn } from "~/lib/utils"; import { useUiStateStore } from "~/uiStateStore"; import { type TimestampFormat } from "@t3tools/contracts/settings"; -import { - formatChatTimestampTooltip, - formatDayAwareTimestamp, - formatUpcomingTimestamp, -} from "../../timestampFormat"; -import { FetchedToolOutput, V2ItemInspector } from "./V2ItemInspector"; +import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat"; +import { V2ItemInspector } from "./V2ItemInspector"; import { useV2ItemSupport } from "../../state/v2ItemSupport"; import { Collapsible, CollapsibleTrigger, CollapsiblePanel } from "../ui/collapsible"; import { isV2LifecycleItem, EnvironmentSubagentAvatar, SubagentElapsed, - SubagentNotificationLink, V2LifecycleRow, type HandoffTimelineRun, } from "./V2LifecycleRow"; @@ -299,76 +250,41 @@ import { type ReviewCommentContext, } from "../../reviewCommentContext"; import { TimelineMinimap } from "./TimelineMinimap"; +import { + type TimelineRowSharedState, + type TimelineRowActivityState, + TimelineRowCtx, + TimelineRowActivityCtx, + type WorkGroupViewState, + WorkGroupViewCtx, + type TimelineWorkEntry, + type TimelineRow, +} from "./timelineRowContext"; +import { + MESSAGE_HEADING_LEVEL, + MessageAuthorHeading, + TimelineRowTimestamp, + ActivityShimmerOverlay, +} from "./TimelineRowParts"; +import { + failedToolIconClassName, + toolIconAcceptsTint, + type WorkEntryIconName, + ToolActivityIconView, +} from "./TimelineToolActivityIcon"; +import { + useThreadReadTarget, + threadReadLabel, + ThreadReadLabel, + ReasoningTraceContent, + SimpleWorkEntryRow, +} from "./TimelineWorkEntryRow"; +import { + remarkThoughtPreview, + toolGroupSummaryIconName, + workEntryIconName, +} from "./TimelineWorkEntryRow.logic"; -interface TimelineRowSharedState { - citationRequest: AssistantCitationTarget | null; - listRef: React.RefObject; - timestampFormat: TimestampFormat; - routeThreadKey: string; - threadRef: ScopedThreadRef | null; - markdownCwd: string | undefined; - resolvedTheme: "light" | "dark"; - workspaceRoot: string | undefined; - skills: ReadonlyArray>; - runs: ReadonlyArray; - activeThreadEnvironmentId: EnvironmentId; - onRevertToTurnCount: (targetTurnCount: number, messageId: MessageId) => void; - onUseArtifactTemplate: (template: CodexArtifactTemplate) => void; - onSendAppMessage: ((text: string) => Promise) | undefined; - onAppFullscreenChange: (rowId: string, fullscreen: boolean) => void; - onRunShellCommand: ((command: string) => void) | undefined; - findActive: boolean; - onImageExpand: (preview: ExpandedImagePreview) => void; - displayThreadKey?: string; - onOpenTurnDiff: (runId: RunId, filePath?: string) => void; - onOpenThread: (threadId: OrchestrationV2TurnItem["threadId"]) => void; - workflowOpening: WorkflowOpening | null; - onForkFromRun: (input: { - readonly sourceThreadId: ThreadId; - readonly runId: RunId; - }) => Promise; - onRollbackCheckpoint: (input: { - readonly checkpointId: string; - readonly scopeId: string; - }) => void; - onToggleTurnFold: (runId: RunId) => void; - onToggleAttemptFold: (attemptId: RunAttemptId) => void; - onFileOpen: (attachment: ChatFileAttachment) => void; - onFileDownload: (attachment: ChatFileAttachment) => void; - openPullRequest: (event: MouseEvent, url: string) => void; - onToggleWorkGroup: (groupId: string, anchorKey: string) => void; - onToggleWorkEntry: (anchorKey: string, collapsed: boolean) => void; - onCancelWorktreeSetup: (() => void) | null; - retryableWorkspacePreparationRunIds: ReadonlySet; - onRetryWorkspacePreparation: ((runId: RunId) => void) | null; - onWorktreeSetupWorkLocally: (() => void) | null; - onOpenWorktreeSetupTerminal: ((terminalId: string) => void) | null; - workGroupViewState: WorkGroupViewState; -} - -interface TimelineRowActivityState { - isWorking: boolean; - isCompacting: boolean; - isRevertingCheckpoint: boolean; - activeTurnInProgress: boolean; - awaitingUser: boolean; - isPreparingWorktree: boolean; - latestRunId: RunId | null; - backgroundWorktreeSetup: WorktreeSetupSnapshot | null; -} - -const TimelineRowCtx = createContext(null!); -const TimelineRowActivityCtx = createContext(null!); - -interface WorkGroupViewState { - scrollPositions: Map; - expandedEntries: Set; -} - -const WorkGroupViewCtx = createContext<{ - state: WorkGroupViewState; - onToggleEntry: (collapsed: boolean) => void; -} | null>(null); function timelineListStateFor(identity: string): { readonly identity: string; readonly rememberedPosition: RememberedTimelinePosition | undefined; @@ -1620,9 +1536,6 @@ function getItemType(item: MessagesTimelineRow) { return `message:${item.message.role}:${lengthBucket}`; } -type TimelineWorkEntry = Extract["groupedEntries"][number]; -type TimelineRow = MessagesTimelineRow; - const TimelineRowContent = memo(function TimelineRowContent({ row }: { row: TimelineRow }) { const isExpandedToolGroup = row.kind === "work" && row.isExpandedToolGroup; const isSubagentGroup = row.kind === "event" && row.projectedItem.item.type === "subagent"; @@ -1810,12 +1723,6 @@ function UserVideoAttachment({ file }: { readonly file: ChatFileAttachment }) { ); } -const MESSAGE_HEADING_LEVEL = 3; - -function MessageAuthorHeading({ children }: { children: string }) { - return

{children}

; -} - function UserMessageRow({ row }: { row: Extract }) { const ctx = use(TimelineRowCtx); return ctx.workflowOpening?.messageId === row.message.id ? ( @@ -2253,37 +2160,6 @@ function RevertUserMessageButton({ ); } -function TimelineRowTimestamp({ - createdAt, - timestampFormat, - className, - alwaysVisible = false, -}: { - createdAt: string; - timestampFormat: TimestampFormat; - className?: string; - alwaysVisible?: boolean; -}) { - return ( - - - } - > - {formatDayAwareTimestamp(createdAt, timestampFormat)} - - {formatChatTimestampTooltip(createdAt, timestampFormat)} - - ); -} - function TurnFoldTimelineRow({ row }: { row: Extract }) { const ctx = use(TimelineRowCtx); @@ -3240,25 +3116,6 @@ function ExpandedWorkGroupEntries({ const workEntryKey = (entry: TimelineWorkEntry) => entry.id; -function ActivityShimmerOverlay({ children }: { children: ReactNode }) { - return ( - - - {children} - - - ); -} - -const failedToolIconClassName = "text-tool-error-icon/40"; - -function toolIconAcceptsTint(toolIcon: ToolActivityIcon | undefined): boolean { - return toolIcon === undefined; -} - function WorkingTimelineRow({ row }: { row: Extract }) { const { isCompacting, isPreparingWorktree, backgroundWorktreeSetup } = use(TimelineRowActivityCtx); @@ -3458,49 +3315,6 @@ function LiveActivityContent({ ); } -function useThreadReadTarget(entry: TimelineWorkEntry, environmentId: EnvironmentId) { - const rawThreadId = threadReadTargetId(entry); - const threadId = rawThreadId === null ? null : ThreadId.make(rawThreadId); - const shell = useThreadShell(threadId ? scopeThreadRef(environmentId, threadId) : null); - const title = threadReadTargetTitle(shell); - return threadId && title ? { threadId, title } : null; -} - -function threadReadLabel(label: string, target: ReturnType) { - const prefix = target && threadReadLabelPrefix(label); - return prefix ? { ...target, prefix, text: `${prefix} “${target.title}”` } : null; -} - -function ThreadReadLabel({ - label, - environmentId, - linked, -}: { - label: NonNullable>; - environmentId: EnvironmentId; - linked: boolean; -}) { - return ( - - {label.prefix} “ - {linked ? ( - - {label.title} - - ) : ( - {label.title} - )} - ” - - ); -} - function LiveWorkEntryTimelineRow({ row }: { row: Extract }) { const ctx = use(TimelineRowCtx); const threadTarget = useThreadReadTarget(row.entry, ctx.activeThreadEnvironmentId); @@ -3597,49 +3411,6 @@ function LiveWorkEntryTimelineRow({ row }: { row: Extract["summaryKind"], -): WorkEntryIconName { - switch (kind) { - case "pull-request": - case "link-pr": - case "unlink-pr": - case "list-prs": - case "watch-pr": - case "unwatch-pr": - return "pull-request"; - case "read": - return "eye"; - case "edit": - return "square-pen"; - case "command": - return "terminal"; - case "thread-create": - return "t3-code"; - case "browser": - return "browser"; - case "device": - return "device"; - case "search": - return "globe"; - case "code-search": - return "search"; - case "other": - return "wrench"; - case "dynamic-tool": - return "hammer"; - case "reasoning": - return "brain"; - case "agent-tool": - return "bot"; - case "tone-tool": - return "zap"; - case "update": - case "mixed": - return "hammer"; - } -} - function WorkGroupToggleTimelineRow({ row, }: { @@ -4506,936 +4277,3 @@ function useStableRows(rows: MessagesTimelineRow[], identity: string): MessagesT setState({ identity, source: rows, stable }); return stable.result; } - -type WorkEntryIconName = - | "bot" - | "brain" - | "browser" - | "check" - | "circle-alert" - | "computer" - | "code" - | "device" - | "eye" - | "globe" - | "hammer" - | "message-circle" - | "search" - | "square-pen" - | "terminal" - | "pull-request" - | "t3-code" - | "wrench" - | "x" - | "zap"; - -function BrowserAppIcon({ className }: { className: string }) { - return ( - - - - - - - ); -} - -function ToolActivityIconView(props: { - icon: ToolActivityIcon | undefined; - fallbackName: WorkEntryIconName; - className: string; - muted: boolean; -}) { - const { resolvedTheme } = use(TimelineRowCtx); - const fallbackClassName = cn(props.className, props.muted && "opacity-70 light:brightness-60"); - if (!props.icon) { - return ; - } - if (props.icon._tag === "website") { - const src = toolActivityFaviconUrl(props.icon, resolvedTheme, 32); - return src ? ( - - ) : ( - - ); - } - if (props.icon._tag === "themed-logo") { - const src = - resolvedTheme === "dark" - ? (props.icon.logoUrlDark ?? props.icon.logoUrl) - : props.icon.logoUrl; - return ( - - ); - } - return ( - - ); -} - -function NativeAppToolActivityIcon(props: { - app: Extract["app"]; - fallbackName: WorkEntryIconName; - className: string; - muted: boolean; -}) { - const { activeThreadEnvironmentId } = use(TimelineRowCtx); - const asset = useAssetUrlState(activeThreadEnvironmentId, { - _tag: "native-app-icon", - app: props.app, - }); - if (asset._tag !== "Success") { - return ( - - ); - } - const cacheKey = getProjectFaviconCacheKey( - activeThreadEnvironmentId, - JSON.stringify(props.app), - asset.url, - ); - return ( - - ); -} - -const loadedToolActivityIconSrcs = new Map(); - -function ToolActivityImageIcon(props: { - cacheKey: string; - src: string; - fallbackName: WorkEntryIconName; - className: string; - muted: boolean; -}) { - const [displayedSrc, setDisplayedSrc] = useState( - () => loadedToolActivityIconSrcs.get(props.cacheKey) ?? null, - ); - const isLoading = displayedSrc !== props.src; - const handleLoadError = (failedSrc: string) => { - if (loadedToolActivityIconSrcs.get(props.cacheKey) === failedSrc) { - loadedToolActivityIconSrcs.delete(props.cacheKey); - } - setDisplayedSrc((currentSrc) => (currentSrc === failedSrc ? null : currentSrc)); - }; - return ( - <> - {displayedSrc === null ? ( - - ) : null} - {displayedSrc ? ( - - handleLoadError(displayedSrc)} - /> - - ) : null} - {isLoading ? ( - { - loadedToolActivityIconSrcs.set(props.cacheKey, props.src); - setDisplayedSrc(props.src); - }} - onError={() => handleLoadError(props.src)} - /> - ) : null} - - ); -} - -function WorkEntryIcon({ name, className }: { name: WorkEntryIconName; className: string }) { - switch (name) { - case "pull-request": - return ; - case "bot": - return ; - case "brain": - return ; - case "browser": - case "computer": - return ; - case "code": - return ; - case "device": - return ; - case "t3-code": - return ; - case "check": - return ; - case "circle-alert": - return ; - case "eye": - return ; - case "globe": - return ; - case "hammer": - return ; - case "search": - return ; - case "message-circle": - return ; - case "square-pen": - return ; - case "terminal": - return ; - case "wrench": - return ; - case "x": - return ; - case "zap": - return ; - } -} - -function workToneIcon(tone: TimelineWorkEntry["tone"]): { - iconName: WorkEntryIconName; - className: string; -} { - if (tone === "error") { - return { - iconName: "circle-alert", - className: "text-foreground/92", - }; - } - if (tone === "thinking") { - return { - iconName: "brain", - className: "text-icon-muted", - }; - } - if (tone === "info") { - return { - iconName: "check", - className: "text-muted-foreground", - }; - } - return { - iconName: "zap", - className: "text-foreground/92", - }; -} - -function workEntryRawCommand( - workEntry: Pick, -): string | null { - const rawCommand = workEntry.rawCommand?.trim(); - if (!rawCommand || !workEntry.command) { - return null; - } - return rawCommand === workEntry.command.trim() ? null : rawCommand; -} - -function buildToolCallExpandedBody( - workEntry: TimelineWorkEntry, - workspaceRoot: string | undefined, - visibleLabel: string, - viewedImagePath: string | null, -): string | null { - const blocks: string[] = []; - const seen = new Set([visibleLabel.trim()]); - const addBlock = (value: string | null | undefined) => { - const text = value?.trim(); - if (!text || seen.has(text)) return; - seen.add(text); - blocks.push(text); - }; - if (workEntry.itemType === "dynamic_tool" && workEntry.toolData !== undefined) { - const input = - workEntry.structuredPayload?.type === "dynamic_tool" - ? workEntry.structuredPayload.input - : workEntry.toolData !== null && - typeof workEntry.toolData === "object" && - "input" in workEntry.toolData - ? workEntry.toolData.input - : undefined; - if (input !== undefined) addBlock(`Tool input\n${JSON.stringify(input, null, 2)}`); - } - const command = workEntry.command?.trim(); - const raw = workEntryRawCommand(workEntry); - if (command === visibleLabel.trim()) { - seen.add(command); - } else { - addBlock(raw ?? command); - } - const detail = workEntry.detail?.trim(); - if (detail !== viewedImagePath?.trim()) { - addBlock(detail); - } - const viewedImagePaths = new Set( - viewedImagePath - ? [viewedImagePath.trim(), formatWorkspaceRelativePath(viewedImagePath, workspaceRoot)] - : [], - ); - const changedFiles = (workEntry.changedFiles ?? []).flatMap((filePath) => { - const formattedPath = formatWorkspaceRelativePath(filePath, workspaceRoot); - return viewedImagePaths.has(filePath) || - viewedImagePaths.has(formattedPath) || - filePath.trim() === detail || - formattedPath === detail - ? [] - : [formattedPath]; - }); - if (changedFiles.length > 0) { - addBlock([...new Set(changedFiles)].join("\n")); - } - return blocks.length > 0 ? blocks.join("\n\n") : null; -} - -const toolCallExpandedBodyClassName = - "max-h-64 cursor-text overflow-auto whitespace-pre-wrap break-words font-mono text-secondary-label text-(length:--font-size-code,var(--text-2xs)) leading-relaxed select-text"; - -function workEntryIconName(workEntry: TimelineWorkEntry): WorkEntryIconName { - if (workEntry.structuredPayload?.type === "notification") { - if (workEntry.structuredPayload.outcome === "failed") return "circle-alert"; - const source = workEntry.structuredPayload.source; - switch (source.kind) { - case "subagent": - case "delegated_task": - return "bot"; - case "command": - return "terminal"; - case "monitor": - return "eye"; - case "background_task": - return "zap"; - case "system": - return "t3-code"; - default: - source satisfies never; - return "zap"; - } - } - if (workEntry.itemType === "user_input_request" || workEntry.itemType === "approval_request") { - return "message-circle"; - } - if (workEntry.toolSurface) return workEntry.toolSurface; - if ( - workEntry.structuredPayload?.type === "dynamic_tool" && - isJavaScriptReplTool(workEntry.structuredPayload.toolName) - ) - return "code"; - const toolPresentation = resolveWorkEntryToolPresentation(workEntry); - if (toolPresentation) return toolPresentation.icon; - const action = toolGroupAction(workEntry); - if (action !== "other") return toolGroupSummaryIconName(action); - - switch (workEntry.itemType) { - case "dynamic_tool": - return "wrench"; - case "subagent": - return "bot"; - } - - return workToneIcon(workEntry.tone).iconName; -} - -const stopRowToggle = (e: { stopPropagation: () => void }) => e.stopPropagation(); - -/** Compact reasoning keeps formulas while flattening other Markdown into label text. */ -export function remarkThoughtPreview(fallback: string) { - return (tree: Root) => { - const children: RootContent[] = []; - const append = (value: string) => { - const last = children.at(-1); - const text = value.replace(/\s+/g, " "); - if (last?.type === "text") last.value = (last.value + text).replace(/\s+/g, " "); - else children.push({ type: "text", value: text }); - }; - const visit = (node: Root | RootContent) => { - if (node.type === "html" || node.type === "definition") return; - if (node.type === "math" || node.type === "inlineMath") { - children.push({ - type: "inlineMath", - value: node.value, - position: node.position, - data: { - hName: "code", - hProperties: { className: ["language-math", "math-inline"] }, - hChildren: [{ type: "text", value: node.value }], - }, - }); - } else if ("alt" in node) append(node.alt ?? ""); - else if ("value" in node) append(node.value); - else if ("children" in node) { - node.children.forEach((child, index) => { - if ( - index && - ["root", "blockquote", "list", "listItem", "table", "tableRow"].includes(node.type) - ) - append(" "); - visit(child); - }); - } else if (node.type === "break") append(" "); - }; - visit(tree); - const first = children[0]; - const last = children.at(-1); - if (first?.type === "text") first.value = first.value.trimStart(); - if (last?.type === "text") last.value = last.value.trimEnd(); - tree.children = children.some((child) => child.type !== "text" || child.value) - ? children - : [{ type: "text", value: fallback }]; - }; -} - -function ReasoningTraceContent({ entries }: { entries: ReadonlyArray }) { - const ctx = use(TimelineRowCtx); - const { isWorking, latestRunId } = use(TimelineRowActivityCtx); - const isStreaming = (entry: TimelineWorkEntry) => - isWorking && entry.runId === latestRunId && entry.toolLifecycleStatus === "inProgress"; - return ( - - {entries.map((entry) => ( - - ))} - - ); -} - -type WorkEntryRowProps = { - workEntry: TimelineWorkEntry; - workspaceRoot: string | undefined; - displayLabel?: string | undefined; - onToggleEntry?: ((collapsed: boolean) => void) | undefined; -}; - -const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: WorkEntryRowProps) { - const ctx = use(TimelineRowCtx); - const item = props.workEntry.projectedItem?.item; - const childThreadId = - item?.type === "notification" ? notificationChildThreadId(item.source) : undefined; - if (item?.type !== "notification" || childThreadId === undefined) { - return ; - } - return ( - } - /> - ); -}); - -function WorkEntryLogRow(props: WorkEntryRowProps) { - const { workEntry, workspaceRoot, displayLabel } = props; - const ctx = use(TimelineRowCtx); - const { threadRef, onImageExpand, timestampFormat } = ctx; - const { retryableWorkspacePreparationRunIds, onRetryWorkspacePreparation } = ctx; - const threadTarget = useThreadReadTarget(workEntry, ctx.activeThreadEnvironmentId); - const createdThread = - workEntry.projectedItem?.item.type === "thread_created" - ? workEntry.projectedItem.item - : undefined; - const notifiedSubagentThreadId = - workEntry.projectedItem?.item.type === "notification" - ? notificationChildThreadId(workEntry.projectedItem.item.source) - : undefined; - const groupView = use(WorkGroupViewCtx); - const [expanded, setExpanded] = useState( - () => groupView?.state.expandedEntries.has(workEntry.id) ?? false, - ); - const toggleExpanded = () => { - const next = !expanded; - if (groupView) { - groupView.onToggleEntry(!next); - if (next) groupView.state.expandedEntries.add(workEntry.id); - else groupView.state.expandedEntries.delete(workEntry.id); - } else { - props.onToggleEntry?.(!next); - } - setExpanded(next); - }; - const failureItem = workEntry.projectedItem?.item; - if (failureItem?.type === "error" && failureItem.status === "failed") { - const warning = failureItem.failure.class === "usage_limit"; - const resetAt = failureItem.failure.resetAt; - const resetTime = resetAt ? formatUpcomingTimestamp(resetAt, timestampFormat) : null; - const label = warning - ? `Usage limit reached.${resetTime ? ` Retry after ${resetTime}.` : ""}` - : workEntry.label; - const retryRunId = - failureItem.runId !== null && - retryableWorkspacePreparationRunIds.has(failureItem.runId) && - failureItem.failure.code === ORCHESTRATION_V2_WORKSPACE_PREPARATION_FAILURE_CODE - ? failureItem.runId - : null; - return ( - - } - label={ - - {label} - - } - trailing={ - - } - > - {!warning ? ( -

- {failureItem.failure.message} -

- ) : null} - {retryRunId !== null && onRetryWorkspacePreparation ? ( -
- -
- ) : null} -
- ); - } - const iconConfig = workToneIcon(workEntry.tone); - const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning"; - const showFailedIndicator = - !showWarningIndicator && workEntryDisplayIndicatesToolFailure(workEntry); - const showDestructiveRowStyle = - !showWarningIndicator && - showFailedIndicator && - (workEntrySignalsSevereFailure(workEntry) || !workLogEntryIsToolLike(workEntry)); - const entryIconName = - showWarningIndicator || showDestructiveRowStyle ? "circle-alert" : workEntryIconName(workEntry); - const entryToolIcon = - showWarningIndicator || showDestructiveRowStyle - ? undefined - : (workEntry.toolIcon ?? workEntry.toolSource?.icon); - const isReasoning = workEntry.itemType === "reasoning"; - const questionHeading = workEntry.questionAnswer - ? getQuestionTextPreview(workEntry.questionAnswer) - : ""; - const previewText = - isReasoning && expanded - ? workEntry.toolLifecycleStatus === "inProgress" - ? "Thinking" - : "Thought" - : questionHeading || (displayLabel ?? workEntryDisplayLabel(workEntry, workspaceRoot)); - const answerPreview = - workEntry.questionAnswer && hasQuestionAnswer(workEntry.questionAnswer) - ? getQuestionAnswerPreview(workEntry.questionAnswer) - : null; - const viewedImagePath = workEntryViewedImagePath(workEntry); - const payload = workEntry.structuredPayload; - const skill = - payload?.type === "dynamic_tool" - ? claudeSkillInvocation(payload.toolName, payload.input) - : undefined; - const plainOutput = - toolGroupAction(workEntry) === "read" - ? workEntryReadOutput(workEntry, workspaceRoot) - : skill - ? (skill.args ?? null) - : undefined; - const viewedImage = - viewedImagePath && threadRef - ? resolveViewedImageAsset(viewedImagePath, { - threadId: threadRef.threadId, - workspaceRoot, - }) - : null; - const canExpand = - previewText.trim().length > 0 || - (workEntry.itemType === "dynamic_tool" && workEntry.toolData !== undefined) || - Boolean( - workEntryRawCommand(workEntry) || - workEntry.command?.trim() || - workEntry.detail?.trim() || - workEntry.changedFiles?.length || - viewedImage, - ); - const expandedBody = - expanded && !isReasoning - ? plainOutput !== undefined - ? plainOutput - : buildToolCallExpandedBody( - workEntry, - workspaceRoot, - previewText, - viewedImage ? viewedImagePath : null, - ) - : null; - const plainOutputFetches = - plainOutput !== undefined && - workEntry.projectedItem !== undefined && - turnItemNeedsDetailFetch(workEntry.projectedItem.item); - const canExpandProjectedItem = - plainOutput !== undefined - ? Boolean(plainOutput || viewedImage || workEntry.questionAnswer || plainOutputFetches) - : workEntry.projectedItem === undefined - ? canExpand - : isReasoning - ? Boolean(workEntry.detail?.trim()) - : Boolean( - viewedImage || - workEntry.questionAnswer || - turnItemHasDetail(workEntry.projectedItem.item), - ); - const iconWrapperClass = cn( - "flex size-4 items-center justify-center", - showWarningIndicator - ? "text-warning" - : showDestructiveRowStyle - ? "text-destructive" - : showFailedIndicator - ? failedToolIconClassName - : workEntry.tone === "tool" - ? "text-icon-muted" - : iconConfig.className, - ); - const headingClass = showWarningIndicator - ? "font-medium text-warning" - : showDestructiveRowStyle - ? "font-medium text-destructive" - : workLogEntryIsToolLike(workEntry) - ? "text-secondary-label" - : "text-foreground/80"; - const threadLabel = isReasoning ? null : threadReadLabel(previewText, threadTarget); - const accessiblePreview = [threadLabel?.text ?? previewText, answerPreview] - .filter(Boolean) - .join(": "); - const accessibleDisplayText = showFailedIndicator - ? `${accessiblePreview}, tool call failed` - : accessiblePreview; - const rowToggleProps = canExpandProjectedItem - ? { - role: "button" as const, - tabIndex: 0 as const, - "aria-label": accessibleDisplayText, - "aria-expanded": expanded, - onClick: toggleExpanded, - onKeyDown: (e: KeyboardEvent) => { - if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) { - e.preventDefault(); - toggleExpanded(); - } - }, - } - : {}; - - return ( - - - - } - label={ -
-

- - {isReasoning && !expanded ? ( - - ) : threadLabel ? ( - - ) : ( - previewText - )} - - {answerPreview ? ( - - {answerPreview} - - ) : null} -

-
- } - trailing={ - <> - {createdThread ? ( - - ) : null} - {notifiedSubagentThreadId ? ( - { - event.stopPropagation(); - ctx.onOpenThread(notifiedSubagentThreadId); - }} - onKeyDown={stopRowToggle} - > - Open subagent - - ) : null} - {showFailedIndicator && - !showDestructiveRowStyle && - !toolIconAcceptsTint(entryToolIcon) ? ( - - ) : null} - - - - - - } - > - {expanded && viewedImage && threadRef ? ( - - - - ) : null} - {expanded && workEntry.questionAnswer ? ( - - ) : null} - {expanded && isReasoning ? : null} - {expanded && - !isReasoning && - !workEntry.questionAnswer && - canExpandProjectedItem && - (expandedBody || - plainOutputFetches || - (workEntry.projectedItem && plainOutput === undefined)) ? ( - - {workEntry.projectedItem && plainOutput === undefined ? ( - - ) : ( - <> - {expandedBody ? ( -
{expandedBody}
- ) : null} - {plainOutputFetches && workEntry.projectedItem ? ( - - ) : null} - - )} -
- ) : null} -
- ); -} - -function QuestionAnswerHistory({ - answer, -}: { - answer: import("@t3tools/contracts").UserInputAttachmentAnswerPayload; -}) { - const { activeThreadEnvironmentId } = use(TimelineRowCtx); - const attachments = Object.values(answer.attachmentsByQuestionId).flat(); - const resources = attachments.map((attachment) => ({ - _tag: "attachment" as const, - attachmentId: attachment.id, - })); - const urls = useAssetUrls(activeThreadEnvironmentId, resources); - return ( -
- {[ - ...new Set([ - ...Object.keys(answer.questionTextById ?? {}), - ...Object.keys(answer.answers), - ...Object.keys(answer.attachmentsByQuestionId), - ]), - ].map((questionId) => ( -
- {answer.questionTextById?.[questionId] ? ( -

- {answer.questionTextById[questionId]} -

- ) : null} - {getQuestionAnswerText(answer.answers[questionId]) ? ( -

- {getQuestionAnswerText(answer.answers[questionId])} -

- ) : null} -
- {(answer.attachmentsByQuestionId[questionId] ?? []).map((attachment) => { - const url = urls[attachments.indexOf(attachment)]; - return ( - - {attachment.type === "image" && url ? ( - {attachment.name} - ) : ( - attachment.name - )} - - ); - })} -
-
- ))} -
- ); -} diff --git a/apps/web/src/components/chat/TimelineRowParts.tsx b/apps/web/src/components/chat/TimelineRowParts.tsx new file mode 100644 index 000000000000..549b9c7a422f --- /dev/null +++ b/apps/web/src/components/chat/TimelineRowParts.tsx @@ -0,0 +1,55 @@ +import { type ReactNode } from "react"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { cn } from "~/lib/utils"; +import { type TimestampFormat } from "@t3tools/contracts/settings"; +import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat"; + +export const MESSAGE_HEADING_LEVEL = 3; + +export function MessageAuthorHeading({ children }: { children: string }) { + return

{children}

; +} + +export function TimelineRowTimestamp({ + createdAt, + timestampFormat, + className, + alwaysVisible = false, +}: { + createdAt: string; + timestampFormat: TimestampFormat; + className?: string; + alwaysVisible?: boolean; +}) { + return ( + + + } + > + {formatDayAwareTimestamp(createdAt, timestampFormat)} + + {formatChatTimestampTooltip(createdAt, timestampFormat)} + + ); +} + +export function ActivityShimmerOverlay({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/apps/web/src/components/chat/TimelineToolActivityIcon.tsx b/apps/web/src/components/chat/TimelineToolActivityIcon.tsx new file mode 100644 index 000000000000..8d85ed2eefd6 --- /dev/null +++ b/apps/web/src/components/chat/TimelineToolActivityIcon.tsx @@ -0,0 +1,272 @@ +import { PullRequestGlyph } from "../pullRequest/pullRequestIcons"; +import { type ToolActivityIcon } from "@t3tools/contracts"; +import { toolActivityFaviconUrl } from "@t3tools/shared/favicon"; +import { getProjectFaviconCacheKey } from "@t3tools/shared/projectFavicon"; +import { use, useState } from "react"; +import { T3Wordmark } from "../T3Wordmark"; +import { + BotIcon, + BrainIcon, + CheckIcon, + CircleAlertIcon, + EyeIcon, + GlobeIcon, + MessageCircleIcon, + SearchIcon, + SmartphoneIcon, + SquarePenIcon, + TerminalIcon, + HammerIcon, + WrenchIcon, + Code2Icon, + XIcon, + ZapIcon, +} from "lucide-react"; +import { useAssetUrlState } from "../../assets/assetUrls"; +import { cn } from "~/lib/utils"; +import { TimelineRowCtx } from "./timelineRowContext"; + +export const failedToolIconClassName = "text-tool-error-icon/40"; + +export function toolIconAcceptsTint(toolIcon: ToolActivityIcon | undefined): boolean { + return toolIcon === undefined; +} + +export type WorkEntryIconName = + | "bot" + | "brain" + | "browser" + | "check" + | "circle-alert" + | "computer" + | "code" + | "device" + | "eye" + | "globe" + | "hammer" + | "message-circle" + | "search" + | "square-pen" + | "terminal" + | "pull-request" + | "t3-code" + | "wrench" + | "x" + | "zap"; + +function BrowserAppIcon({ className }: { className: string }) { + return ( + + + + + + + ); +} + +export function ToolActivityIconView(props: { + icon: ToolActivityIcon | undefined; + fallbackName: WorkEntryIconName; + className: string; + muted: boolean; +}) { + const { resolvedTheme } = use(TimelineRowCtx); + const fallbackClassName = cn(props.className, props.muted && "opacity-70 light:brightness-60"); + if (!props.icon) { + return ; + } + if (props.icon._tag === "website") { + const src = toolActivityFaviconUrl(props.icon, resolvedTheme, 32); + return src ? ( + + ) : ( + + ); + } + if (props.icon._tag === "themed-logo") { + const src = + resolvedTheme === "dark" + ? (props.icon.logoUrlDark ?? props.icon.logoUrl) + : props.icon.logoUrl; + return ( + + ); + } + return ( + + ); +} + +function NativeAppToolActivityIcon(props: { + app: Extract["app"]; + fallbackName: WorkEntryIconName; + className: string; + muted: boolean; +}) { + const { activeThreadEnvironmentId } = use(TimelineRowCtx); + const asset = useAssetUrlState(activeThreadEnvironmentId, { + _tag: "native-app-icon", + app: props.app, + }); + if (asset._tag !== "Success") { + return ( + + ); + } + const cacheKey = getProjectFaviconCacheKey( + activeThreadEnvironmentId, + JSON.stringify(props.app), + asset.url, + ); + return ( + + ); +} + +const loadedToolActivityIconSrcs = new Map(); + +function ToolActivityImageIcon(props: { + cacheKey: string; + src: string; + fallbackName: WorkEntryIconName; + className: string; + muted: boolean; +}) { + const [displayedSrc, setDisplayedSrc] = useState( + () => loadedToolActivityIconSrcs.get(props.cacheKey) ?? null, + ); + const isLoading = displayedSrc !== props.src; + const handleLoadError = (failedSrc: string) => { + if (loadedToolActivityIconSrcs.get(props.cacheKey) === failedSrc) { + loadedToolActivityIconSrcs.delete(props.cacheKey); + } + setDisplayedSrc((currentSrc) => (currentSrc === failedSrc ? null : currentSrc)); + }; + return ( + <> + {displayedSrc === null ? ( + + ) : null} + {displayedSrc ? ( + + handleLoadError(displayedSrc)} + /> + + ) : null} + {isLoading ? ( + { + loadedToolActivityIconSrcs.set(props.cacheKey, props.src); + setDisplayedSrc(props.src); + }} + onError={() => handleLoadError(props.src)} + /> + ) : null} + + ); +} + +function WorkEntryIcon({ name, className }: { name: WorkEntryIconName; className: string }) { + switch (name) { + case "pull-request": + return ; + case "bot": + return ; + case "brain": + return ; + case "browser": + case "computer": + return ; + case "code": + return ; + case "device": + return ; + case "t3-code": + return ; + case "check": + return ; + case "circle-alert": + return ; + case "eye": + return ; + case "globe": + return ; + case "hammer": + return ; + case "search": + return ; + case "message-circle": + return ; + case "square-pen": + return ; + case "terminal": + return ; + case "wrench": + return ; + case "x": + return ; + case "zap": + return ; + } +} diff --git a/apps/web/src/components/chat/TimelineWorkEntryRow.logic.ts b/apps/web/src/components/chat/TimelineWorkEntryRow.logic.ts new file mode 100644 index 000000000000..dc077090e430 --- /dev/null +++ b/apps/web/src/components/chat/TimelineWorkEntryRow.logic.ts @@ -0,0 +1,236 @@ +import type { Root, RootContent } from "mdast"; +import { resolveWorkEntryToolPresentation } from "@t3tools/client-runtime/work-log/presentation"; +import { isJavaScriptReplTool } from "@t3tools/shared/toolActivity"; +import { toolGroupAction } from "./MessagesTimeline.logic"; +import { formatWorkspaceRelativePath } from "../../filePathDisplay"; +import { type TimelineWorkEntry, type TimelineRow } from "./timelineRowContext"; +import { type WorkEntryIconName } from "./TimelineToolActivityIcon"; + +export function toolGroupSummaryIconName( + kind: Extract["summaryKind"], +): WorkEntryIconName { + switch (kind) { + case "pull-request": + case "link-pr": + case "unlink-pr": + case "list-prs": + case "watch-pr": + case "unwatch-pr": + return "pull-request"; + case "read": + return "eye"; + case "edit": + return "square-pen"; + case "command": + return "terminal"; + case "thread-create": + return "t3-code"; + case "browser": + return "browser"; + case "device": + return "device"; + case "search": + return "globe"; + case "code-search": + return "search"; + case "other": + return "wrench"; + case "dynamic-tool": + return "hammer"; + case "reasoning": + return "brain"; + case "agent-tool": + return "bot"; + case "tone-tool": + return "zap"; + case "update": + case "mixed": + return "hammer"; + } +} + +export function workToneIcon(tone: TimelineWorkEntry["tone"]): { + iconName: WorkEntryIconName; + className: string; +} { + if (tone === "error") { + return { + iconName: "circle-alert", + className: "text-foreground/92", + }; + } + if (tone === "thinking") { + return { + iconName: "brain", + className: "text-icon-muted", + }; + } + if (tone === "info") { + return { + iconName: "check", + className: "text-muted-foreground", + }; + } + return { + iconName: "zap", + className: "text-foreground/92", + }; +} + +export function workEntryRawCommand( + workEntry: Pick, +): string | null { + const rawCommand = workEntry.rawCommand?.trim(); + if (!rawCommand || !workEntry.command) { + return null; + } + return rawCommand === workEntry.command.trim() ? null : rawCommand; +} + +export function buildToolCallExpandedBody( + workEntry: TimelineWorkEntry, + workspaceRoot: string | undefined, + visibleLabel: string, + viewedImagePath: string | null, +): string | null { + const blocks: string[] = []; + const seen = new Set([visibleLabel.trim()]); + const addBlock = (value: string | null | undefined) => { + const text = value?.trim(); + if (!text || seen.has(text)) return; + seen.add(text); + blocks.push(text); + }; + if (workEntry.itemType === "dynamic_tool" && workEntry.toolData !== undefined) { + const input = + workEntry.structuredPayload?.type === "dynamic_tool" + ? workEntry.structuredPayload.input + : workEntry.toolData !== null && + typeof workEntry.toolData === "object" && + "input" in workEntry.toolData + ? workEntry.toolData.input + : undefined; + if (input !== undefined) addBlock(`Tool input\n${JSON.stringify(input, null, 2)}`); + } + const command = workEntry.command?.trim(); + const raw = workEntryRawCommand(workEntry); + if (command === visibleLabel.trim()) { + seen.add(command); + } else { + addBlock(raw ?? command); + } + const detail = workEntry.detail?.trim(); + if (detail !== viewedImagePath?.trim()) { + addBlock(detail); + } + const viewedImagePaths = new Set( + viewedImagePath + ? [viewedImagePath.trim(), formatWorkspaceRelativePath(viewedImagePath, workspaceRoot)] + : [], + ); + const changedFiles = (workEntry.changedFiles ?? []).flatMap((filePath) => { + const formattedPath = formatWorkspaceRelativePath(filePath, workspaceRoot); + return viewedImagePaths.has(filePath) || + viewedImagePaths.has(formattedPath) || + filePath.trim() === detail || + formattedPath === detail + ? [] + : [formattedPath]; + }); + if (changedFiles.length > 0) { + addBlock([...new Set(changedFiles)].join("\n")); + } + return blocks.length > 0 ? blocks.join("\n\n") : null; +} + +export function workEntryIconName(workEntry: TimelineWorkEntry): WorkEntryIconName { + if (workEntry.structuredPayload?.type === "notification") { + if (workEntry.structuredPayload.outcome === "failed") return "circle-alert"; + const source = workEntry.structuredPayload.source; + switch (source.kind) { + case "subagent": + case "delegated_task": + return "bot"; + case "command": + return "terminal"; + case "monitor": + return "eye"; + case "background_task": + return "zap"; + case "system": + return "t3-code"; + default: + source satisfies never; + return "zap"; + } + } + if (workEntry.itemType === "user_input_request" || workEntry.itemType === "approval_request") { + return "message-circle"; + } + if (workEntry.toolSurface) return workEntry.toolSurface; + if ( + workEntry.structuredPayload?.type === "dynamic_tool" && + isJavaScriptReplTool(workEntry.structuredPayload.toolName) + ) + return "code"; + const toolPresentation = resolveWorkEntryToolPresentation(workEntry); + if (toolPresentation) return toolPresentation.icon; + const action = toolGroupAction(workEntry); + if (action !== "other") return toolGroupSummaryIconName(action); + + switch (workEntry.itemType) { + case "dynamic_tool": + return "wrench"; + case "subagent": + return "bot"; + } + + return workToneIcon(workEntry.tone).iconName; +} + +/** Compact reasoning keeps formulas while flattening other Markdown into label text. */ +export function remarkThoughtPreview(fallback: string) { + return (tree: Root) => { + const children: RootContent[] = []; + const append = (value: string) => { + const last = children.at(-1); + const text = value.replace(/\s+/g, " "); + if (last?.type === "text") last.value = (last.value + text).replace(/\s+/g, " "); + else children.push({ type: "text", value: text }); + }; + const visit = (node: Root | RootContent) => { + if (node.type === "html" || node.type === "definition") return; + if (node.type === "math" || node.type === "inlineMath") { + children.push({ + type: "inlineMath", + value: node.value, + position: node.position, + data: { + hName: "code", + hProperties: { className: ["language-math", "math-inline"] }, + hChildren: [{ type: "text", value: node.value }], + }, + }); + } else if ("alt" in node) append(node.alt ?? ""); + else if ("value" in node) append(node.value); + else if ("children" in node) { + node.children.forEach((child, index) => { + if ( + index && + ["root", "blockquote", "list", "listItem", "table", "tableRow"].includes(node.type) + ) + append(" "); + visit(child); + }); + } else if (node.type === "break") append(" "); + }; + visit(tree); + const first = children[0]; + const last = children.at(-1); + if (first?.type === "text") first.value = first.value.trimStart(); + if (last?.type === "text") last.value = last.value.trimEnd(); + tree.children = children.some((child) => child.type !== "text" || child.value) + ? children + : [{ type: "text", value: fallback }]; + }; +} diff --git a/apps/web/src/components/chat/TimelineWorkEntryRow.tsx b/apps/web/src/components/chat/TimelineWorkEntryRow.tsx new file mode 100644 index 000000000000..cf72597f43ec --- /dev/null +++ b/apps/web/src/components/chat/TimelineWorkEntryRow.tsx @@ -0,0 +1,617 @@ +import { WorkLogDetails, WorkLogRow } from "./WorkLog"; +import { + getQuestionAnswerPreview, + getQuestionAnswerText, + getQuestionTextPreview, + hasQuestionAnswer, +} from "@t3tools/client-runtime/work-log/user-input"; +import { + ORCHESTRATION_V2_WORKSPACE_PREPARATION_FAILURE_CODE, + type EnvironmentId, + ThreadId, +} from "@t3tools/contracts"; +import { scopeThreadRef } from "@t3tools/client-runtime/environment"; +import { Link } from "@tanstack/react-router"; +import { notificationChildThreadId } from "@t3tools/client-runtime/state/thread-execution"; +import { + resolveViewedImageAsset, + workEntryViewedImagePath, +} from "@t3tools/client-runtime/work-log/presentation"; +import { + turnItemHasDetail, + turnItemNeedsDetailFetch, +} from "@t3tools/client-runtime/work-log/item-detail"; +import { claudeSkillInvocation } from "@t3tools/shared/toolActivity"; +import { memo, use, useState, type KeyboardEvent } from "react"; +import { + workEntryDisplayIndicatesToolFailure, + workEntrySignalsSevereFailure, + workLogEntryIsToolLike, +} from "../../session-logic"; +import ChatMarkdown, { ChatMarkdownAssetImage } from "../ChatMarkdown"; +import { ChevronRightIcon, CircleAlertIcon, XIcon, RotateCcwIcon } from "lucide-react"; +import { Button, InlineButton } from "../ui/button"; +import { useAssetUrls } from "../../assets/assetUrls"; +import { useThreadShell } from "../../state/entities"; +import { + threadReadLabelPrefix, + threadReadTargetId, + threadReadTargetTitle, + toolGroupAction, + workEntryDisplayLabel, + workEntryReadOutput, +} from "./MessagesTimeline.logic"; +import { cn } from "~/lib/utils"; +import { formatUpcomingTimestamp } from "../../timestampFormat"; +import { FetchedToolOutput, V2ItemInspector } from "./V2ItemInspector"; +import { SubagentNotificationLink } from "./V2LifecycleRow"; +import { + TimelineRowCtx, + TimelineRowActivityCtx, + WorkGroupViewCtx, + type TimelineWorkEntry, +} from "./timelineRowContext"; +import { MESSAGE_HEADING_LEVEL, TimelineRowTimestamp } from "./TimelineRowParts"; +import { + failedToolIconClassName, + toolIconAcceptsTint, + ToolActivityIconView, +} from "./TimelineToolActivityIcon"; +import { + workToneIcon, + workEntryRawCommand, + buildToolCallExpandedBody, + workEntryIconName, + remarkThoughtPreview, +} from "./TimelineWorkEntryRow.logic"; + +export function useThreadReadTarget(entry: TimelineWorkEntry, environmentId: EnvironmentId) { + const rawThreadId = threadReadTargetId(entry); + const threadId = rawThreadId === null ? null : ThreadId.make(rawThreadId); + const shell = useThreadShell(threadId ? scopeThreadRef(environmentId, threadId) : null); + const title = threadReadTargetTitle(shell); + return threadId && title ? { threadId, title } : null; +} + +export function threadReadLabel(label: string, target: ReturnType) { + const prefix = target && threadReadLabelPrefix(label); + return prefix ? { ...target, prefix, text: `${prefix} “${target.title}”` } : null; +} + +export function ThreadReadLabel({ + label, + environmentId, + linked, +}: { + label: NonNullable>; + environmentId: EnvironmentId; + linked: boolean; +}) { + return ( + + {label.prefix} “ + {linked ? ( + + {label.title} + + ) : ( + {label.title} + )} + ” + + ); +} + +const toolCallExpandedBodyClassName = + "max-h-64 cursor-text overflow-auto whitespace-pre-wrap break-words font-mono text-secondary-label text-(length:--font-size-code,var(--text-2xs)) leading-relaxed select-text"; + +const stopRowToggle = (e: { stopPropagation: () => void }) => e.stopPropagation(); + +export function ReasoningTraceContent({ entries }: { entries: ReadonlyArray }) { + const ctx = use(TimelineRowCtx); + const { isWorking, latestRunId } = use(TimelineRowActivityCtx); + const isStreaming = (entry: TimelineWorkEntry) => + isWorking && entry.runId === latestRunId && entry.toolLifecycleStatus === "inProgress"; + return ( + + {entries.map((entry) => ( + + ))} + + ); +} + +type WorkEntryRowProps = { + workEntry: TimelineWorkEntry; + workspaceRoot: string | undefined; + displayLabel?: string | undefined; + onToggleEntry?: ((collapsed: boolean) => void) | undefined; +}; + +export const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: WorkEntryRowProps) { + const ctx = use(TimelineRowCtx); + const item = props.workEntry.projectedItem?.item; + const childThreadId = + item?.type === "notification" ? notificationChildThreadId(item.source) : undefined; + if (item?.type !== "notification" || childThreadId === undefined) { + return ; + } + return ( + } + /> + ); +}); + +function WorkEntryLogRow(props: WorkEntryRowProps) { + const { workEntry, workspaceRoot, displayLabel } = props; + const ctx = use(TimelineRowCtx); + const { threadRef, onImageExpand, timestampFormat } = ctx; + const { retryableWorkspacePreparationRunIds, onRetryWorkspacePreparation } = ctx; + const threadTarget = useThreadReadTarget(workEntry, ctx.activeThreadEnvironmentId); + const createdThread = + workEntry.projectedItem?.item.type === "thread_created" + ? workEntry.projectedItem.item + : undefined; + const notifiedSubagentThreadId = + workEntry.projectedItem?.item.type === "notification" + ? notificationChildThreadId(workEntry.projectedItem.item.source) + : undefined; + const groupView = use(WorkGroupViewCtx); + const [expanded, setExpanded] = useState( + () => groupView?.state.expandedEntries.has(workEntry.id) ?? false, + ); + const toggleExpanded = () => { + const next = !expanded; + if (groupView) { + groupView.onToggleEntry(!next); + if (next) groupView.state.expandedEntries.add(workEntry.id); + else groupView.state.expandedEntries.delete(workEntry.id); + } else { + props.onToggleEntry?.(!next); + } + setExpanded(next); + }; + const failureItem = workEntry.projectedItem?.item; + if (failureItem?.type === "error" && failureItem.status === "failed") { + const warning = failureItem.failure.class === "usage_limit"; + const resetAt = failureItem.failure.resetAt; + const resetTime = resetAt ? formatUpcomingTimestamp(resetAt, timestampFormat) : null; + const label = warning + ? `Usage limit reached.${resetTime ? ` Retry after ${resetTime}.` : ""}` + : workEntry.label; + const retryRunId = + failureItem.runId !== null && + retryableWorkspacePreparationRunIds.has(failureItem.runId) && + failureItem.failure.code === ORCHESTRATION_V2_WORKSPACE_PREPARATION_FAILURE_CODE + ? failureItem.runId + : null; + return ( + + } + label={ + + {label} + + } + trailing={ + + } + > + {!warning ? ( +

+ {failureItem.failure.message} +

+ ) : null} + {retryRunId !== null && onRetryWorkspacePreparation ? ( +
+ +
+ ) : null} +
+ ); + } + const iconConfig = workToneIcon(workEntry.tone); + const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning"; + const showFailedIndicator = + !showWarningIndicator && workEntryDisplayIndicatesToolFailure(workEntry); + const showDestructiveRowStyle = + !showWarningIndicator && + showFailedIndicator && + (workEntrySignalsSevereFailure(workEntry) || !workLogEntryIsToolLike(workEntry)); + const entryIconName = + showWarningIndicator || showDestructiveRowStyle ? "circle-alert" : workEntryIconName(workEntry); + const entryToolIcon = + showWarningIndicator || showDestructiveRowStyle + ? undefined + : (workEntry.toolIcon ?? workEntry.toolSource?.icon); + const isReasoning = workEntry.itemType === "reasoning"; + const questionHeading = workEntry.questionAnswer + ? getQuestionTextPreview(workEntry.questionAnswer) + : ""; + const previewText = + isReasoning && expanded + ? workEntry.toolLifecycleStatus === "inProgress" + ? "Thinking" + : "Thought" + : questionHeading || (displayLabel ?? workEntryDisplayLabel(workEntry, workspaceRoot)); + const answerPreview = + workEntry.questionAnswer && hasQuestionAnswer(workEntry.questionAnswer) + ? getQuestionAnswerPreview(workEntry.questionAnswer) + : null; + const viewedImagePath = workEntryViewedImagePath(workEntry); + const payload = workEntry.structuredPayload; + const skill = + payload?.type === "dynamic_tool" + ? claudeSkillInvocation(payload.toolName, payload.input) + : undefined; + const plainOutput = + toolGroupAction(workEntry) === "read" + ? workEntryReadOutput(workEntry, workspaceRoot) + : skill + ? (skill.args ?? null) + : undefined; + const viewedImage = + viewedImagePath && threadRef + ? resolveViewedImageAsset(viewedImagePath, { + threadId: threadRef.threadId, + workspaceRoot, + }) + : null; + const canExpand = + previewText.trim().length > 0 || + (workEntry.itemType === "dynamic_tool" && workEntry.toolData !== undefined) || + Boolean( + workEntryRawCommand(workEntry) || + workEntry.command?.trim() || + workEntry.detail?.trim() || + workEntry.changedFiles?.length || + viewedImage, + ); + const expandedBody = + expanded && !isReasoning + ? plainOutput !== undefined + ? plainOutput + : buildToolCallExpandedBody( + workEntry, + workspaceRoot, + previewText, + viewedImage ? viewedImagePath : null, + ) + : null; + const plainOutputFetches = + plainOutput !== undefined && + workEntry.projectedItem !== undefined && + turnItemNeedsDetailFetch(workEntry.projectedItem.item); + const canExpandProjectedItem = + plainOutput !== undefined + ? Boolean(plainOutput || viewedImage || workEntry.questionAnswer || plainOutputFetches) + : workEntry.projectedItem === undefined + ? canExpand + : isReasoning + ? Boolean(workEntry.detail?.trim()) + : Boolean( + viewedImage || + workEntry.questionAnswer || + turnItemHasDetail(workEntry.projectedItem.item), + ); + const iconWrapperClass = cn( + "flex size-4 items-center justify-center", + showWarningIndicator + ? "text-warning" + : showDestructiveRowStyle + ? "text-destructive" + : showFailedIndicator + ? failedToolIconClassName + : workEntry.tone === "tool" + ? "text-icon-muted" + : iconConfig.className, + ); + const headingClass = showWarningIndicator + ? "font-medium text-warning" + : showDestructiveRowStyle + ? "font-medium text-destructive" + : workLogEntryIsToolLike(workEntry) + ? "text-secondary-label" + : "text-foreground/80"; + const threadLabel = isReasoning ? null : threadReadLabel(previewText, threadTarget); + const accessiblePreview = [threadLabel?.text ?? previewText, answerPreview] + .filter(Boolean) + .join(": "); + const accessibleDisplayText = showFailedIndicator + ? `${accessiblePreview}, tool call failed` + : accessiblePreview; + const rowToggleProps = canExpandProjectedItem + ? { + role: "button" as const, + tabIndex: 0 as const, + "aria-label": accessibleDisplayText, + "aria-expanded": expanded, + onClick: toggleExpanded, + onKeyDown: (e: KeyboardEvent) => { + if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) { + e.preventDefault(); + toggleExpanded(); + } + }, + } + : {}; + + return ( + + + + } + label={ +
+

+ + {isReasoning && !expanded ? ( + + ) : threadLabel ? ( + + ) : ( + previewText + )} + + {answerPreview ? ( + + {answerPreview} + + ) : null} +

+
+ } + trailing={ + <> + {createdThread ? ( + + ) : null} + {notifiedSubagentThreadId ? ( + { + event.stopPropagation(); + ctx.onOpenThread(notifiedSubagentThreadId); + }} + onKeyDown={stopRowToggle} + > + Open subagent + + ) : null} + {showFailedIndicator && + !showDestructiveRowStyle && + !toolIconAcceptsTint(entryToolIcon) ? ( + + ) : null} + + + + + + } + > + {expanded && viewedImage && threadRef ? ( + + + + ) : null} + {expanded && workEntry.questionAnswer ? ( + + ) : null} + {expanded && isReasoning ? : null} + {expanded && + !isReasoning && + !workEntry.questionAnswer && + canExpandProjectedItem && + (expandedBody || + plainOutputFetches || + (workEntry.projectedItem && plainOutput === undefined)) ? ( + + {workEntry.projectedItem && plainOutput === undefined ? ( + + ) : ( + <> + {expandedBody ? ( +
{expandedBody}
+ ) : null} + {plainOutputFetches && workEntry.projectedItem ? ( + + ) : null} + + )} +
+ ) : null} +
+ ); +} + +function QuestionAnswerHistory({ + answer, +}: { + answer: import("@t3tools/contracts").UserInputAttachmentAnswerPayload; +}) { + const { activeThreadEnvironmentId } = use(TimelineRowCtx); + const attachments = Object.values(answer.attachmentsByQuestionId).flat(); + const resources = attachments.map((attachment) => ({ + _tag: "attachment" as const, + attachmentId: attachment.id, + })); + const urls = useAssetUrls(activeThreadEnvironmentId, resources); + return ( +
+ {[ + ...new Set([ + ...Object.keys(answer.questionTextById ?? {}), + ...Object.keys(answer.answers), + ...Object.keys(answer.attachmentsByQuestionId), + ]), + ].map((questionId) => ( +
+ {answer.questionTextById?.[questionId] ? ( +

+ {answer.questionTextById[questionId]} +

+ ) : null} + {getQuestionAnswerText(answer.answers[questionId]) ? ( +

+ {getQuestionAnswerText(answer.answers[questionId])} +

+ ) : null} +
+ {(answer.attachmentsByQuestionId[questionId] ?? []).map((attachment) => { + const url = urls[attachments.indexOf(attachment)]; + return ( + + {attachment.type === "image" && url ? ( + {attachment.name} + ) : ( + attachment.name + )} + + ); + })} +
+
+ ))} +
+ ); +} diff --git a/apps/web/src/components/chat/timelineRowContext.ts b/apps/web/src/components/chat/timelineRowContext.ts new file mode 100644 index 000000000000..fdfb833ecc6a --- /dev/null +++ b/apps/web/src/components/chat/timelineRowContext.ts @@ -0,0 +1,99 @@ +import type { WorktreeSetupSnapshot } from "@t3tools/contracts"; +import { + type EnvironmentId, + type MessageId, + type OrchestrationV2TurnItem, + type RunAttemptId, + type ScopedThreadRef, + type ServerProviderSkill, + type RunId, + ThreadId, +} from "@t3tools/contracts"; +import type { WorkflowOpening } from "@t3tools/client-runtime/state/subagentRuntime"; +import { createContext, type MouseEvent } from "react"; +import { type LegendListRef } from "@legendapp/list/react"; +import type { CodexArtifactTemplate } from "@t3tools/shared/codexArtifactTemplates"; +import { type ChatFileAttachment } from "../../types"; +import { ExpandedImagePreview } from "./ExpandedImagePreview"; +import { type AssistantCitationTarget } from "./AssistantCitationSource"; +import { type MessagesTimelineRow, type WorkGroupScrollAnchor } from "./MessagesTimeline.logic"; +import { type TimestampFormat } from "@t3tools/contracts/settings"; +import { type HandoffTimelineRun } from "./V2LifecycleRow"; + +export interface TimelineRowSharedState { + citationRequest: AssistantCitationTarget | null; + listRef: React.RefObject; + timestampFormat: TimestampFormat; + routeThreadKey: string; + threadRef: ScopedThreadRef | null; + markdownCwd: string | undefined; + resolvedTheme: "light" | "dark"; + workspaceRoot: string | undefined; + skills: ReadonlyArray>; + runs: ReadonlyArray; + activeThreadEnvironmentId: EnvironmentId; + onRevertToTurnCount: (targetTurnCount: number, messageId: MessageId) => void; + onUseArtifactTemplate: (template: CodexArtifactTemplate) => void; + onSendAppMessage: ((text: string) => Promise) | undefined; + onAppFullscreenChange: (rowId: string, fullscreen: boolean) => void; + onRunShellCommand: ((command: string) => void) | undefined; + findActive: boolean; + onImageExpand: (preview: ExpandedImagePreview) => void; + displayThreadKey?: string; + onOpenTurnDiff: (runId: RunId, filePath?: string) => void; + onOpenThread: (threadId: OrchestrationV2TurnItem["threadId"]) => void; + workflowOpening: WorkflowOpening | null; + onForkFromRun: (input: { + readonly sourceThreadId: ThreadId; + readonly runId: RunId; + }) => Promise; + onRollbackCheckpoint: (input: { + readonly checkpointId: string; + readonly scopeId: string; + }) => void; + onToggleTurnFold: (runId: RunId) => void; + onToggleAttemptFold: (attemptId: RunAttemptId) => void; + onFileOpen: (attachment: ChatFileAttachment) => void; + onFileDownload: (attachment: ChatFileAttachment) => void; + openPullRequest: (event: MouseEvent, url: string) => void; + onToggleWorkGroup: (groupId: string, anchorKey: string) => void; + onToggleWorkEntry: (anchorKey: string, collapsed: boolean) => void; + onCancelWorktreeSetup: (() => void) | null; + retryableWorkspacePreparationRunIds: ReadonlySet; + onRetryWorkspacePreparation: ((runId: RunId) => void) | null; + onWorktreeSetupWorkLocally: (() => void) | null; + onOpenWorktreeSetupTerminal: ((terminalId: string) => void) | null; + workGroupViewState: WorkGroupViewState; +} + +export interface TimelineRowActivityState { + isWorking: boolean; + isCompacting: boolean; + isRevertingCheckpoint: boolean; + activeTurnInProgress: boolean; + awaitingUser: boolean; + isPreparingWorktree: boolean; + latestRunId: RunId | null; + backgroundWorktreeSetup: WorktreeSetupSnapshot | null; +} + +export const TimelineRowCtx = createContext(null!); + +export const TimelineRowActivityCtx = createContext(null!); + +export interface WorkGroupViewState { + scrollPositions: Map; + expandedEntries: Set; +} + +export const WorkGroupViewCtx = createContext<{ + state: WorkGroupViewState; + onToggleEntry: (collapsed: boolean) => void; +} | null>(null); + +export type TimelineWorkEntry = Extract< + MessagesTimelineRow, + { kind: "work" } +>["groupedEntries"][number]; + +export type TimelineRow = MessagesTimelineRow; From 9e672d0974640171c8576468d2db93d2f2975a58 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 21 Oct 2026 01:12:49 -0700 Subject: [PATCH 15/73] refactor(web): timeline work group and live activity rows live in their own file (#22675) Co-authored-by: Claude Opus 5.5 --- .../src/components/chat/MessagesTimeline.tsx | 640 +----------------- .../chat/TimelineWorkGroupRows.logic.ts | 20 + .../components/chat/TimelineWorkGroupRows.tsx | 640 ++++++++++++++++++ 3 files changed, 667 insertions(+), 633 deletions(-) create mode 100644 apps/web/src/components/chat/TimelineWorkGroupRows.logic.ts create mode 100644 apps/web/src/components/chat/TimelineWorkGroupRows.tsx diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 2bcd94805f26..956b82e3f716 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -4,16 +4,11 @@ import { shallow } from "zustand/vanilla/shallow"; import { shouldPreserveAssistantLineBreaks } from "@t3tools/shared/markdownPipeline"; import { MarkdownFindContext, findRevealRef } from "./markdownFindContext"; import { useChatCanvas } from "./ChatCanvasContext"; -import { WorkLogBlock, WorkLogButton, WorkLogList, WorkLogRow } from "./WorkLog"; +import { WorkLogBlock } from "./WorkLog"; import { PullRequestGlyph } from "../pullRequest/pullRequestIcons"; import type { WorktreeSetupSnapshot } from "@t3tools/contracts"; import { ReadOnlySourcePreview } from "../files/AttachmentFilePreview"; import { useRightPanelStore } from "~/rightPanelStore"; -import { - getQuestionAnswerPreview, - getQuestionTextPreview, - hasQuestionAnswer, -} from "@t3tools/client-runtime/work-log/user-input"; import { deriveTimelineMinimapItems } from "./timelineMinimapItems"; import { COMPOSER_CONTEXT_KINDS, @@ -25,7 +20,6 @@ import { type ServerProviderSkill, type RunId, ThreadId, - type ToolActivityIcon, } from "@t3tools/contracts"; import { parseScopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime/environment"; import { useAtomValue } from "@effect/atom-react"; @@ -34,7 +28,6 @@ import { resolveUserMessagePresentation } from "@t3tools/client-runtime/user-mes import { Link } from "@tanstack/react-router"; import { canForkProjectedAssistantItem } from "@t3tools/client-runtime/state/thread-workflows"; import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences"; -import { liveThoughtLine } from "@t3tools/client-runtime/work-log/presentation"; import { resolveWorkGroupScrollAnchor } from "@t3tools/client-runtime/work-log/scroll-anchor"; import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments"; import { @@ -49,7 +42,6 @@ const NOOP_USE_ARTIFACT_TEMPLATE = () => {}; const NOOP_OPEN_ATTACHMENT = (_attachment: ChatFileAttachment) => {}; import { resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList"; -import { formatDuration } from "@t3tools/shared/orchestrationTiming"; import { observeVisibleAnimation } from "../../lib/visibleAnimation"; import { memo, @@ -73,7 +65,6 @@ import { providerErrorPresentation, createMessageAttachmentPreviewProjector, selectMessageImageResources, - workEntryDisplayIndicatesToolFailure, } from "../../session-logic"; import type { CodexArtifactTemplate } from "@t3tools/shared/codexArtifactTemplates"; import { @@ -125,7 +116,6 @@ import { MorphIcon } from "~/components/MorphIcon"; import { useAssetUrlRefresh, useAssetUrls, useAssetUrlState } from "../../assets/assetUrls"; import { AttachmentImage } from "../media/AttachmentImage"; import { MediaVideoPlayer } from "../media/MediaVideoPlayer"; -import { getVirtualizedScrollFadeClassName } from "../ui/scroll-area"; import { buildAttachmentVideoAsset, buildAttachmentVideoPreview, @@ -161,7 +151,6 @@ import { computeStableMessagesTimelineRows, deriveMessagesTimelineRowsWithState, type MessagesTimelineRowsProjection, - liveWorkEntryLabel, resolveAssistantMessageCopyState, resolveTimelineIsAtEnd, resolveTimelineMinimapHasPersistentGutter, @@ -169,11 +158,8 @@ import { isTimelineMinimapRowVisible, resolveTimelineMinimapVisibleRange, resolveTimelineMinimapHitStripWidth, - resolveWorkGroupScrollIndex, shouldCollapseUserMessage, - shouldFollowWorkGroupAppend, timelineTurnFoldRunIdsByEntryId, - workEntryIsVisibleInGroup, worktreeSetupAgentStarted, type StableMessagesTimelineRowsState, type MessagesTimelineRow, @@ -184,8 +170,6 @@ import { useThreadFindNavigation } from "./useThreadFindNavigation"; import { readThreadFindPosition } from "./threadFindPosition"; import type { ThreadFindPositionReader } from "./threadFind"; import { TerminalContextInlineChip } from "./TerminalContextInlineChip"; -import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; -import { Spinner } from "../ui/spinner"; import { Tooltip, TooltipPopup, TooltipTrigger, TooltipScrollDismissArea } from "../ui/tooltip"; import { WorktreeSetupCard } from "./WorktreeSetupCard"; import { @@ -256,8 +240,6 @@ import { TimelineRowCtx, TimelineRowActivityCtx, type WorkGroupViewState, - WorkGroupViewCtx, - type TimelineWorkEntry, type TimelineRow, } from "./timelineRowContext"; import { @@ -267,23 +249,12 @@ import { ActivityShimmerOverlay, } from "./TimelineRowParts"; import { - failedToolIconClassName, - toolIconAcceptsTint, - type WorkEntryIconName, - ToolActivityIconView, -} from "./TimelineToolActivityIcon"; -import { - useThreadReadTarget, - threadReadLabel, - ThreadReadLabel, - ReasoningTraceContent, - SimpleWorkEntryRow, -} from "./TimelineWorkEntryRow"; -import { - remarkThoughtPreview, - toolGroupSummaryIconName, - workEntryIconName, -} from "./TimelineWorkEntryRow.logic"; + WorkGroupSection, + WorkingTimelineRow, + ThinkingTimelineRow, + LiveWorkEntryTimelineRow, + WorkGroupToggleTimelineRow, +} from "./TimelineWorkGroupRows"; function timelineListStateFor(identity: string): { readonly identity: string; @@ -2871,603 +2842,6 @@ const V2SubagentGroup = memo(function V2SubagentGroup({ ); }); -const WorkGroupSection = memo(function WorkGroupSection({ - anchorKey, - disclosureAnchorKey = anchorKey, - groupedEntries, - isExpandedToolGroup, - displayLabel, -}: { - anchorKey: string; - disclosureAnchorKey?: string; - groupedEntries: Extract["groupedEntries"]; - isExpandedToolGroup: boolean; - displayLabel?: string | undefined; -}) { - const { workspaceRoot, routeThreadKey, onToggleWorkEntry } = use(TimelineRowCtx); - const onToggleStandaloneEntry = (collapsed: boolean) => - onToggleWorkEntry(disclosureAnchorKey, collapsed); - const nonEmptyEntries = groupedEntries.filter((entry) => - workEntryIsVisibleInGroup(entry, isExpandedToolGroup), - ); - - if (nonEmptyEntries.length === 0) return null; - if (isExpandedToolGroup && nonEmptyEntries.every((entry) => entry.itemType === "reasoning")) { - return ; - } - if (isExpandedToolGroup) { - return ( - - ); - } - - return ( -
- {nonEmptyEntries.map((workEntry) => ( - - ))} -
- ); -}); - -function formatWorkingTimer(startIso: string, endIso: string): string | null { - const startedAtMs = Date.parse(startIso); - const endedAtMs = Date.parse(endIso); - if (!Number.isFinite(startedAtMs) || !Number.isFinite(endedAtMs)) { - return null; - } - - const elapsedSeconds = Math.max(0, Math.floor((endedAtMs - startedAtMs) / 1000)); - if (elapsedSeconds < 60) { - return `${elapsedSeconds}s`; - } - - return formatDuration(elapsedSeconds * 1_000); -} - -function formatWorkingTimerNow(startIso: string): string { - return formatWorkingTimer(startIso, new Date().toISOString()) ?? "0s"; -} - -function WorkingTimer({ createdAt }: { createdAt: string }) { - const textRef = useRef(null); - const initialText = formatWorkingTimerNow(createdAt); - - useEffect(() => { - const updateText = () => { - if (textRef.current) { - textRef.current.textContent = formatWorkingTimerNow(createdAt); - } - }; - updateText(); - const id = setInterval(updateText, 1000); - return () => clearInterval(id); - }, [createdAt]); - - return ( - - {initialText} - - ); -} - -const compactWorkEntryHeight = 24; - -function ExpandedWorkGroupEntries({ - anchorKey, - disclosureAnchorKey, - entries, - workspaceRoot, -}: { - anchorKey: string; - disclosureAnchorKey: string; - entries: TimelineWorkEntry[]; - workspaceRoot: string | undefined; -}) { - const { workGroupViewState: viewState, onToggleWorkEntry } = use(TimelineRowCtx); - const [initialScrollIndex] = useState(() => - resolveWorkGroupScrollIndex(entries, viewState.scrollPositions.get(anchorKey)), - ); - const [restoringPosition, setRestoringPosition] = useState(initialScrollIndex !== undefined); - const listRef = useRef(null); - const [expandedContentHeight, setExpandedContentHeight] = useState(0); - const [fades, setFades] = useState({ top: false, bottom: false, viewportHeight: 0 }); - const [appendState, setAppendState] = useState({ entries, follow: false }); - // Capture the pre-change edge once per incoming array, before new layout - // metrics arrive. Edge/viewport changes never turn a status update into a follow. - if (appendState.entries !== entries) { - setAppendState({ - entries, - follow: - fades.viewportHeight > 0 && - shouldFollowWorkGroupAppend(appendState.entries, entries, fades.bottom ? Infinity : 0), - }); - } - - const groupView = { - state: viewState, - onToggleEntry: (collapsed: boolean) => onToggleWorkEntry(disclosureAnchorKey, collapsed), - }; - const updateScrollFades = () => { - const element = listRef.current?.getScrollableNode(); - if (!element) return; - const distanceFromEnd = element.scrollHeight - element.clientHeight - element.scrollTop; - const viewportHeight = element.clientHeight; - const top = element.scrollTop > 1; - const bottom = distanceFromEnd > 1; - setFades((previous) => - previous.top === top && - previous.bottom === bottom && - previous.viewportHeight === viewportHeight - ? previous - : { top, bottom, viewportHeight }, - ); - }; - - const handleScroll = () => { - const state = listRef.current?.getState(); - const position = state && resolveWorkGroupScrollAnchor(state); - if (position) { - viewState.scrollPositions.set(anchorKey, { - entryId: position.rowId, - offset: position.offsetWithinRow, - }); - } - updateScrollFades(); - }; - - const handleLoad = () => { - const list = listRef.current; - const element = list?.getScrollableNode(); - if (initialScrollIndex && list && element) { - // Bootstrap can report the restored target before the DOM has applied it. - // Reconcile once at load, before releasing the measured anchor row. - const offset = Math.max( - 0, - Math.min(list.getState().scroll, element.scrollHeight - element.clientHeight), - ); - if (Math.abs(element.scrollTop - offset) > 1) { - void list.scrollToOffset({ offset, animated: false }); - } - } - setRestoringPosition(false); - }; - - useLayoutEffect(() => { - const element = listRef.current?.getScrollableNode(); - if (!element) return; - updateScrollFades(); - const content = element.firstElementChild; - return observeResize(content ? [element, content] : element, updateScrollFades); - }, [updateScrollFades]); - - const renderEntry = ({ item }: { item: TimelineWorkEntry }) => ( - - ); - - const updateExpandedContentHeight = () => { - const state = listRef.current?.getState(); - let height = 0; - for (const entryId of viewState.expandedEntries) { - if (state?.indexByKey(entryId) === undefined) continue; - height += Math.max( - 0, - (state.sizes.get(entryId) ?? compactWorkEntryHeight) - compactWorkEntryHeight, - ); - } - setExpandedContentHeight(height); - }; - - useLayoutEffect(updateExpandedContentHeight, [entries, updateExpandedContentHeight]); - - return ( - - - - - - ); -} - -const workEntryKey = (entry: TimelineWorkEntry) => entry.id; - -function WorkingTimelineRow({ row }: { row: Extract }) { - const { isCompacting, isPreparingWorktree, backgroundWorktreeSetup } = - use(TimelineRowActivityCtx); - const shimmer = isPreparingWorktree || isCompacting; - const label = isPreparingWorktree ? ( - "Setting up worktree…" - ) : isCompacting ? ( - - ) : row.createdAt ? ( - <> - Working for - - ) : ( - "Working..." - ); - return ( -
-
- - {label} - {shimmer ? {label} : null} - - {backgroundWorktreeSetup ? ( - - ) : null} -
-
- ); -} - -function BackgroundWorktreeSetupChip({ snapshot }: { snapshot: WorktreeSetupSnapshot }) { - const ctx = use(TimelineRowCtx); - const terminalId = snapshot.setupScript?.terminalId ?? null; - const openTerminal = ctx.onOpenWorktreeSetupTerminal; - const onOpenTerminal = openTerminal && terminalId ? () => openTerminal(terminalId) : null; - const scriptName = snapshot.setupScript?.name ?? "Setup script"; - return ( - - - } - > - - {scriptName} - - - - - - ); -} - -function CompactingLabel() { - return ( - - - ); -} - -function ThinkingTimelineRow({ row }: { row: Extract }) { - const ctx = use(TimelineRowCtx); - const { isCompacting, isPreparingWorktree } = use(TimelineRowActivityCtx); - if (isPreparingWorktree || isCompacting) return ; - const activity = ; - const { groupId } = row; - if (groupId === undefined) return activity; - return ( - - ); -} - -function LiveActivityRow({ - label, - iconName, - toolIcon, - failed = false, - active = false, - shimmer = false, -}: { - label: ReactNode; - iconName?: WorkEntryIconName; - toolIcon?: ToolActivityIcon | undefined; - failed?: boolean; - active?: boolean; - shimmer?: boolean; -}) { - const animated = active && !failed; - const showShimmer = animated && shimmer; - return ( -
- - {showShimmer ? ( - - - - ) : null} -
- ); -} - -function LiveActivityContent({ - label, - iconName, - toolIcon, - failed = false, - announceFailure = false, - active = false, - highlighted = false, -}: { - label: ReactNode; - iconName: WorkEntryIconName | undefined; - toolIcon?: ToolActivityIcon | undefined; - failed?: boolean; - announceFailure?: boolean; - active?: boolean; - highlighted?: boolean; -}) { - const showTrailingFailureMark = - failed && iconName !== undefined && !toolIconAcceptsTint(toolIcon); - - return ( - - - - ) : null - } - label={ - - {label} - - } - trailing={ - showTrailingFailureMark ? ( - - ) : null - } - /> - ); -} - -function LiveWorkEntryTimelineRow({ row }: { row: Extract }) { - const ctx = use(TimelineRowCtx); - const threadTarget = useThreadReadTarget(row.entry, ctx.activeThreadEnvironmentId); - const questionHeading = row.entry.questionAnswer - ? getQuestionTextPreview(row.entry.questionAnswer) - : ""; - const label = questionHeading || liveWorkEntryLabel(row.entry, ctx.workspaceRoot, row.active); - const threadLabel = threadReadLabel(label, threadTarget); - - const failed = workEntryDisplayIndicatesToolFailure(row.entry); - const thoughtLine = row.thought && !row.expanded ? liveThoughtLine(row.thought.detail ?? "") : ""; - const thoughtIsStatus = thoughtLine !== "" && row.entry === row.thought; - - return ( - - ); -} - -function WorkGroupToggleTimelineRow({ - row, -}: { - row: Extract; -}) { - const ctx = use(TimelineRowCtx); - return ( - ctx.onToggleWorkGroup(row.groupId, row.id)} - /> - ); -} - -function WorkGroupHeader(props: { - label: string; - iconName: WorkEntryIconName; - toolIcon?: ToolActivityIcon | undefined; - failed?: boolean | undefined; - active?: boolean | undefined; - expanded: boolean; - createdAt: string; - timestampFormat: TimestampFormat; - onToggle: () => void; -}) { - return ( - - } - label={ - - {props.label} - - } - trailing={ - - } - /> - ); -} - /** Subscribes directly to the UI state store for expand/collapse state, * so toggling re-renders only this component — not the entire list. */ const AssistantChangedFilesSection = memo(function AssistantChangedFilesSection({ diff --git a/apps/web/src/components/chat/TimelineWorkGroupRows.logic.ts b/apps/web/src/components/chat/TimelineWorkGroupRows.logic.ts new file mode 100644 index 000000000000..e4a8fe4a583c --- /dev/null +++ b/apps/web/src/components/chat/TimelineWorkGroupRows.logic.ts @@ -0,0 +1,20 @@ +import { formatDuration } from "@t3tools/shared/orchestrationTiming"; + +function formatWorkingTimer(startIso: string, endIso: string): string | null { + const startedAtMs = Date.parse(startIso); + const endedAtMs = Date.parse(endIso); + if (!Number.isFinite(startedAtMs) || !Number.isFinite(endedAtMs)) { + return null; + } + + const elapsedSeconds = Math.max(0, Math.floor((endedAtMs - startedAtMs) / 1000)); + if (elapsedSeconds < 60) { + return `${elapsedSeconds}s`; + } + + return formatDuration(elapsedSeconds * 1_000); +} + +export function formatWorkingTimerNow(startIso: string): string { + return formatWorkingTimer(startIso, new Date().toISOString()) ?? "0s"; +} diff --git a/apps/web/src/components/chat/TimelineWorkGroupRows.tsx b/apps/web/src/components/chat/TimelineWorkGroupRows.tsx new file mode 100644 index 000000000000..11b56a19038c --- /dev/null +++ b/apps/web/src/components/chat/TimelineWorkGroupRows.tsx @@ -0,0 +1,640 @@ +import { WorkLogButton, WorkLogList, WorkLogRow } from "./WorkLog"; +import type { WorktreeSetupSnapshot } from "@t3tools/contracts"; +import { + getQuestionAnswerPreview, + getQuestionTextPreview, + hasQuestionAnswer, +} from "@t3tools/client-runtime/work-log/user-input"; +import { type ToolActivityIcon } from "@t3tools/contracts"; +import { liveThoughtLine } from "@t3tools/client-runtime/work-log/presentation"; +import { resolveWorkGroupScrollAnchor } from "@t3tools/client-runtime/work-log/scroll-anchor"; +import { observeResize } from "~/lib/observeResize"; +import { observeVisibleAnimation } from "../../lib/visibleAnimation"; +import { memo, use, useEffect, useLayoutEffect, useRef, useState, type ReactNode } from "react"; +import { LegendList, type LegendListRef } from "@legendapp/list/react"; +import { workEntryDisplayIndicatesToolFailure } from "../../session-logic"; +import ChatMarkdown from "../ChatMarkdown"; +import { Minimize2Icon, XIcon } from "lucide-react"; +import { Button } from "../ui/button"; +import { getVirtualizedScrollFadeClassName } from "../ui/scroll-area"; +import { + liveWorkEntryLabel, + resolveWorkGroupScrollIndex, + shouldFollowWorkGroupAppend, + workEntryIsVisibleInGroup, + type MessagesTimelineRow, +} from "./MessagesTimeline.logic"; +import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; +import { Spinner } from "../ui/spinner"; +import { WorktreeSetupCard } from "./WorktreeSetupCard"; +import { cn } from "~/lib/utils"; +import { type TimestampFormat } from "@t3tools/contracts/settings"; +import { + TimelineRowCtx, + TimelineRowActivityCtx, + WorkGroupViewCtx, + type TimelineWorkEntry, + type TimelineRow, +} from "./timelineRowContext"; +import { TimelineRowTimestamp, ActivityShimmerOverlay } from "./TimelineRowParts"; +import { + failedToolIconClassName, + toolIconAcceptsTint, + type WorkEntryIconName, + ToolActivityIconView, +} from "./TimelineToolActivityIcon"; +import { + useThreadReadTarget, + threadReadLabel, + ThreadReadLabel, + ReasoningTraceContent, + SimpleWorkEntryRow, +} from "./TimelineWorkEntryRow"; +import { + remarkThoughtPreview, + toolGroupSummaryIconName, + workEntryIconName, +} from "./TimelineWorkEntryRow.logic"; +import { formatWorkingTimerNow } from "./TimelineWorkGroupRows.logic"; + +export const WorkGroupSection = memo(function WorkGroupSection({ + anchorKey, + disclosureAnchorKey = anchorKey, + groupedEntries, + isExpandedToolGroup, + displayLabel, +}: { + anchorKey: string; + disclosureAnchorKey?: string; + groupedEntries: Extract["groupedEntries"]; + isExpandedToolGroup: boolean; + displayLabel?: string | undefined; +}) { + const { workspaceRoot, routeThreadKey, onToggleWorkEntry } = use(TimelineRowCtx); + const onToggleStandaloneEntry = (collapsed: boolean) => + onToggleWorkEntry(disclosureAnchorKey, collapsed); + const nonEmptyEntries = groupedEntries.filter((entry) => + workEntryIsVisibleInGroup(entry, isExpandedToolGroup), + ); + + if (nonEmptyEntries.length === 0) return null; + if (isExpandedToolGroup && nonEmptyEntries.every((entry) => entry.itemType === "reasoning")) { + return ; + } + if (isExpandedToolGroup) { + return ( + + ); + } + + return ( +
+ {nonEmptyEntries.map((workEntry) => ( + + ))} +
+ ); +}); + +function WorkingTimer({ createdAt }: { createdAt: string }) { + const textRef = useRef(null); + const initialText = formatWorkingTimerNow(createdAt); + + useEffect(() => { + const updateText = () => { + if (textRef.current) { + textRef.current.textContent = formatWorkingTimerNow(createdAt); + } + }; + updateText(); + const id = setInterval(updateText, 1000); + return () => clearInterval(id); + }, [createdAt]); + + return ( + + {initialText} + + ); +} + +const compactWorkEntryHeight = 24; + +function ExpandedWorkGroupEntries({ + anchorKey, + disclosureAnchorKey, + entries, + workspaceRoot, +}: { + anchorKey: string; + disclosureAnchorKey: string; + entries: TimelineWorkEntry[]; + workspaceRoot: string | undefined; +}) { + const { workGroupViewState: viewState, onToggleWorkEntry } = use(TimelineRowCtx); + const [initialScrollIndex] = useState(() => + resolveWorkGroupScrollIndex(entries, viewState.scrollPositions.get(anchorKey)), + ); + const [restoringPosition, setRestoringPosition] = useState(initialScrollIndex !== undefined); + const listRef = useRef(null); + const [expandedContentHeight, setExpandedContentHeight] = useState(0); + const [fades, setFades] = useState({ top: false, bottom: false, viewportHeight: 0 }); + const [appendState, setAppendState] = useState({ entries, follow: false }); + // Capture the pre-change edge once per incoming array, before new layout + // metrics arrive. Edge/viewport changes never turn a status update into a follow. + if (appendState.entries !== entries) { + setAppendState({ + entries, + follow: + fades.viewportHeight > 0 && + shouldFollowWorkGroupAppend(appendState.entries, entries, fades.bottom ? Infinity : 0), + }); + } + + const groupView = { + state: viewState, + onToggleEntry: (collapsed: boolean) => onToggleWorkEntry(disclosureAnchorKey, collapsed), + }; + const updateScrollFades = () => { + const element = listRef.current?.getScrollableNode(); + if (!element) return; + const distanceFromEnd = element.scrollHeight - element.clientHeight - element.scrollTop; + const viewportHeight = element.clientHeight; + const top = element.scrollTop > 1; + const bottom = distanceFromEnd > 1; + setFades((previous) => + previous.top === top && + previous.bottom === bottom && + previous.viewportHeight === viewportHeight + ? previous + : { top, bottom, viewportHeight }, + ); + }; + + const handleScroll = () => { + const state = listRef.current?.getState(); + const position = state && resolveWorkGroupScrollAnchor(state); + if (position) { + viewState.scrollPositions.set(anchorKey, { + entryId: position.rowId, + offset: position.offsetWithinRow, + }); + } + updateScrollFades(); + }; + + const handleLoad = () => { + const list = listRef.current; + const element = list?.getScrollableNode(); + if (initialScrollIndex && list && element) { + // Bootstrap can report the restored target before the DOM has applied it. + // Reconcile once at load, before releasing the measured anchor row. + const offset = Math.max( + 0, + Math.min(list.getState().scroll, element.scrollHeight - element.clientHeight), + ); + if (Math.abs(element.scrollTop - offset) > 1) { + void list.scrollToOffset({ offset, animated: false }); + } + } + setRestoringPosition(false); + }; + + useLayoutEffect(() => { + const element = listRef.current?.getScrollableNode(); + if (!element) return; + updateScrollFades(); + const content = element.firstElementChild; + return observeResize(content ? [element, content] : element, updateScrollFades); + }, [updateScrollFades]); + + const renderEntry = ({ item }: { item: TimelineWorkEntry }) => ( + + ); + + const updateExpandedContentHeight = () => { + const state = listRef.current?.getState(); + let height = 0; + for (const entryId of viewState.expandedEntries) { + if (state?.indexByKey(entryId) === undefined) continue; + height += Math.max( + 0, + (state.sizes.get(entryId) ?? compactWorkEntryHeight) - compactWorkEntryHeight, + ); + } + setExpandedContentHeight(height); + }; + + useLayoutEffect(updateExpandedContentHeight, [entries, updateExpandedContentHeight]); + + return ( + + + + + + ); +} + +const workEntryKey = (entry: TimelineWorkEntry) => entry.id; + +export function WorkingTimelineRow({ row }: { row: Extract }) { + const { isCompacting, isPreparingWorktree, backgroundWorktreeSetup } = + use(TimelineRowActivityCtx); + const shimmer = isPreparingWorktree || isCompacting; + const label = isPreparingWorktree ? ( + "Setting up worktree…" + ) : isCompacting ? ( + + ) : row.createdAt ? ( + <> + Working for + + ) : ( + "Working..." + ); + return ( +
+
+ + {label} + {shimmer ? {label} : null} + + {backgroundWorktreeSetup ? ( + + ) : null} +
+
+ ); +} + +function BackgroundWorktreeSetupChip({ snapshot }: { snapshot: WorktreeSetupSnapshot }) { + const ctx = use(TimelineRowCtx); + const terminalId = snapshot.setupScript?.terminalId ?? null; + const openTerminal = ctx.onOpenWorktreeSetupTerminal; + const onOpenTerminal = openTerminal && terminalId ? () => openTerminal(terminalId) : null; + const scriptName = snapshot.setupScript?.name ?? "Setup script"; + return ( + + + } + > + + {scriptName} + + + + + + ); +} + +function CompactingLabel() { + return ( + + + ); +} + +export function ThinkingTimelineRow({ row }: { row: Extract }) { + const ctx = use(TimelineRowCtx); + const { isCompacting, isPreparingWorktree } = use(TimelineRowActivityCtx); + if (isPreparingWorktree || isCompacting) return ; + const activity = ; + const { groupId } = row; + if (groupId === undefined) return activity; + return ( + + ); +} + +function LiveActivityRow({ + label, + iconName, + toolIcon, + failed = false, + active = false, + shimmer = false, +}: { + label: ReactNode; + iconName?: WorkEntryIconName; + toolIcon?: ToolActivityIcon | undefined; + failed?: boolean; + active?: boolean; + shimmer?: boolean; +}) { + const animated = active && !failed; + const showShimmer = animated && shimmer; + return ( +
+ + {showShimmer ? ( + + + + ) : null} +
+ ); +} + +function LiveActivityContent({ + label, + iconName, + toolIcon, + failed = false, + announceFailure = false, + active = false, + highlighted = false, +}: { + label: ReactNode; + iconName: WorkEntryIconName | undefined; + toolIcon?: ToolActivityIcon | undefined; + failed?: boolean; + announceFailure?: boolean; + active?: boolean; + highlighted?: boolean; +}) { + const showTrailingFailureMark = + failed && iconName !== undefined && !toolIconAcceptsTint(toolIcon); + + return ( + + + + ) : null + } + label={ + + {label} + + } + trailing={ + showTrailingFailureMark ? ( + + ) : null + } + /> + ); +} + +export function LiveWorkEntryTimelineRow({ + row, +}: { + row: Extract; +}) { + const ctx = use(TimelineRowCtx); + const threadTarget = useThreadReadTarget(row.entry, ctx.activeThreadEnvironmentId); + const questionHeading = row.entry.questionAnswer + ? getQuestionTextPreview(row.entry.questionAnswer) + : ""; + const label = questionHeading || liveWorkEntryLabel(row.entry, ctx.workspaceRoot, row.active); + const threadLabel = threadReadLabel(label, threadTarget); + + const failed = workEntryDisplayIndicatesToolFailure(row.entry); + const thoughtLine = row.thought && !row.expanded ? liveThoughtLine(row.thought.detail ?? "") : ""; + const thoughtIsStatus = thoughtLine !== "" && row.entry === row.thought; + + return ( + + ); +} + +export function WorkGroupToggleTimelineRow({ + row, +}: { + row: Extract; +}) { + const ctx = use(TimelineRowCtx); + return ( + ctx.onToggleWorkGroup(row.groupId, row.id)} + /> + ); +} + +function WorkGroupHeader(props: { + label: string; + iconName: WorkEntryIconName; + toolIcon?: ToolActivityIcon | undefined; + failed?: boolean | undefined; + active?: boolean | undefined; + expanded: boolean; + createdAt: string; + timestampFormat: TimestampFormat; + onToggle: () => void; +}) { + return ( + + } + label={ + + {props.label} + + } + trailing={ + + } + /> + ); +} From 3e36c5e1817734cec615e9ee88801a81208861d0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 21 Oct 2026 01:12:50 -0700 Subject: [PATCH 16/73] refactor(web): timeline user message rows and context chips live in their own files (#22676) Co-authored-by: Claude Opus 5.5 --- .../src/components/chat/MessagesTimeline.tsx | 1231 +---------------- .../chat/TimelineUserMessageRow.logic.ts | 25 + .../chat/TimelineUserMessageRow.tsx | 619 +++++++++ .../chat/UserMessageContextChips.tsx | 592 ++++++++ 4 files changed, 1243 insertions(+), 1224 deletions(-) create mode 100644 apps/web/src/components/chat/TimelineUserMessageRow.logic.ts create mode 100644 apps/web/src/components/chat/TimelineUserMessageRow.tsx create mode 100644 apps/web/src/components/chat/UserMessageContextChips.tsx diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 956b82e3f716..c707e369eb44 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1,17 +1,12 @@ -import { useRenderedAppearance } from "../../hooks/useTheme"; import { ThreadFindTimelineContext } from "./ThreadFindProvider"; import { shallow } from "zustand/vanilla/shallow"; import { shouldPreserveAssistantLineBreaks } from "@t3tools/shared/markdownPipeline"; -import { MarkdownFindContext, findRevealRef } from "./markdownFindContext"; +import { MarkdownFindContext } from "./markdownFindContext"; import { useChatCanvas } from "./ChatCanvasContext"; import { WorkLogBlock } from "./WorkLog"; -import { PullRequestGlyph } from "../pullRequest/pullRequestIcons"; import type { WorktreeSetupSnapshot } from "@t3tools/contracts"; -import { ReadOnlySourcePreview } from "../files/AttachmentFilePreview"; -import { useRightPanelStore } from "~/rightPanelStore"; import { deriveTimelineMinimapItems } from "./timelineMinimapItems"; import { - COMPOSER_CONTEXT_KINDS, type AssistantCitation, type EnvironmentId, type MessageId, @@ -24,12 +19,8 @@ import { import { parseScopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime/environment"; import { useAtomValue } from "@effect/atom-react"; import { environmentThreadDetails } from "../../state/threads"; -import { resolveUserMessagePresentation } from "@t3tools/client-runtime/user-message"; -import { Link } from "@tanstack/react-router"; import { canForkProjectedAssistantItem } from "@t3tools/client-runtime/state/thread-workflows"; -import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences"; import { resolveWorkGroupScrollAnchor } from "@t3tools/client-runtime/work-log/scroll-anchor"; -import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments"; import { subagentGroupSummary, summarizeSubagentStatuses, @@ -58,74 +49,28 @@ import { type LegendListRef, type MaintainScrollAtEndOptions, } from "@legendapp/list/react"; -import { FileDiff } from "@pierre/diffs/react"; -import { DiffWorkerPoolProvider } from "../DiffWorkerPoolProvider"; -import { - type TimelineEntry, - providerErrorPresentation, - createMessageAttachmentPreviewProjector, - selectMessageImageResources, -} from "../../session-logic"; +import { type TimelineEntry, providerErrorPresentation } from "../../session-logic"; import type { CodexArtifactTemplate } from "@t3tools/shared/codexArtifactTemplates"; -import { - type ChatMessage, - type ChatFileAttachment, - type ChatImageAttachment, - isFileAttachment, - isImageAttachment, - isVideoAttachment, - type TurnDiffSummary, -} from "../../types"; -import { - getRenderablePatch, - resolveDiffThemeName, - resolveFileDiffPath, -} from "../../lib/diffRendering"; -import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting"; +import { type ChatMessage, type ChatFileAttachment, type TurnDiffSummary } from "../../types"; import ChatMarkdown from "../ChatMarkdown"; -import { ThreadContextChip } from "../ThreadContextChip"; import { ArrowRightIcon, BotIcon, CheckIcon, ChevronDownIcon, CircleAlertIcon, - DownloadIcon, - EyeIcon, GitForkIcon, type LucideIcon, - MessageCircleIcon, - MousePointerClickIcon, - PaintbrushIcon, MinusIcon, - Redo2Icon, Minimize2Icon, - Undo2Icon, WrenchIcon, XIcon, ZapIcon, } from "lucide-react"; -import { ChevronDown, ChevronRight, Link as LinkIconNode } from "lucide"; -import type { - ComposerContextId, - ComposerContextRecord, - KnownComposerContextRecord, -} from "@t3tools/contracts"; -import { Button, InlineButton } from "../ui/button"; +import { ChevronDown, ChevronRight } from "lucide"; +import { Button } from "../ui/button"; import { MorphIcon } from "~/components/MorphIcon"; -import { useAssetUrlRefresh, useAssetUrls, useAssetUrlState } from "../../assets/assetUrls"; -import { AttachmentImage } from "../media/AttachmentImage"; -import { MediaVideoPlayer } from "../media/MediaVideoPlayer"; -import { - buildAttachmentVideoAsset, - buildAttachmentVideoPreview, - buildExpandedImagePreview, - ExpandedImagePreview, -} from "./ExpandedImagePreview"; -import { - SNAP_SHOT_ATTACHMENT_FRAME_CLASS, - SnapShotAttachmentDetails, -} from "./SnapShotAttachmentDetails"; +import { ExpandedImagePreview } from "./ExpandedImagePreview"; import { ProposedPlanCard } from "./ProposedPlanCard"; import { HtmlRenderFrame } from "./HtmlRenderFrame"; import { McpAppFrame } from "./McpAppFrame"; @@ -140,9 +85,6 @@ import { timelineContentOverflowsViewport, } from "./timelineScrollAnchoring"; import { MessageCopyButton } from "./MessageCopyButton"; -import { promptLinkFor } from "../promptLinks/copyPromptLink"; -import { PierreEntryIcon } from "./PierreEntryIcon"; -import { WorkspaceEntryIcon, WorkspaceEntryTooltip } from "./WorkspaceEntryIcon"; import { AssistantSelectionToolbar } from "./AssistantSelectionToolbar"; import type { AssistantCitationSourceAnchor } from "~/lib/assistantTextSelection"; import { AssistantCitationSource, type AssistantCitationRequest } from "./AssistantCitationSource"; @@ -158,7 +100,6 @@ import { isTimelineMinimapRowVisible, resolveTimelineMinimapVisibleRange, resolveTimelineMinimapHitStripWidth, - shouldCollapseUserMessage, timelineTurnFoldRunIdsByEntryId, worktreeSetupAgentStarted, type StableMessagesTimelineRowsState, @@ -169,42 +110,10 @@ import { type ThreadFindMatch } from "./threadFind"; import { useThreadFindNavigation } from "./useThreadFindNavigation"; import { readThreadFindPosition } from "./threadFindPosition"; import type { ThreadFindPositionReader } from "./threadFind"; -import { TerminalContextInlineChip } from "./TerminalContextInlineChip"; import { Tooltip, TooltipPopup, TooltipTrigger, TooltipScrollDismissArea } from "../ui/tooltip"; import { WorktreeSetupCard } from "./WorktreeSetupCard"; -import { - ContextChipPopover as UserMessageContextPopover, - ContextChipShell, - FileChip, - ImageChipButton, - PULL_REQUEST_CHIP_KINDS, - PullRequestChip, - UnresolvedChip, -} from "../contextChipParts"; -import { - asKnownContextRecord, - isPullRequestSummaryContext, - pullRequestContextDisplayState, - pullRequestContextKindLabel, - resolveUserMessageContext, - reviewCommentContextLabel, - selectedMessageContextFragment, -} from "~/lib/composerContextRecords"; -import { - collectComposerContextReferences, - formatComposerContextReference, -} from "@t3tools/shared/composerContextReferences"; -import { - COMPOSER_CONTEXT_CLIPBOARD_MIME, - encodeComposerContextClipboardHtml, - encodeComposerContextFragment, -} from "@t3tools/shared/composerContextClipboard"; -import { chatMarkdownClipboardPayload } from "../../markdown-clipboard"; -import { ContextChip, ContextChipLabel, type ContextChipKind } from "../ContextChip"; -import { createContextPresentationRegistry } from "../contextPresentationRegistry"; import { useOpenPrLink } from "~/lib/openPullRequestLink"; import { useClientSettings } from "~/hooks/useSettings"; -import type { ChatMarkdownContextReference } from "../ChatMarkdown"; import { useMediaQuery } from "~/hooks/useMediaQuery"; import { cn } from "~/lib/utils"; import { useUiStateStore } from "~/uiStateStore"; @@ -222,17 +131,9 @@ import { } from "./V2LifecycleRow"; import { SecretRequestCard } from "./SecretRequestCard"; import { TimelineSystemDivider } from "./TimelineSystemDivider"; -import { WorkflowOpeningBlock } from "./WorkflowOpeningBlock"; -import { SkillChipIcon } from "./SkillInlineText"; import * as DateTime from "effect/DateTime"; -import { formatWorkspaceRelativePath } from "../../filePathDisplay"; import { forwardWheelToTimeline } from "./timelineMinimapWheel"; -import { - buildReviewCommentRenderablePatch, - formatReviewCommentFence, - type ReviewCommentContext, -} from "../../reviewCommentContext"; import { TimelineMinimap } from "./TimelineMinimap"; import { type TimelineRowSharedState, @@ -255,6 +156,7 @@ import { LiveWorkEntryTimelineRow, WorkGroupToggleTimelineRow, } from "./TimelineWorkGroupRows"; +import { UserMessageRow } from "./TimelineUserMessageRow"; function timelineListStateFor(identity: string): { readonly identity: string; @@ -1650,487 +1552,6 @@ function ContextCompactionTimelineRow({ ); } -function UserVideoAttachment({ file }: { readonly file: ChatFileAttachment }) { - const ctx = use(TimelineRowCtx); - // Row updates rebuild attachment objects, so the asset follows only the - // fields it reads. - const { downloadable, id, mimeType, name } = file; - const environmentId = ctx.activeThreadEnvironmentId; - const asset = - downloadable === false - ? null - : buildAttachmentVideoAsset(environmentId, { id, mimeType, name }); - const resource = asset?.resource ?? null; - const assetUrl = useAssetUrlState(ctx.activeThreadEnvironmentId, resource); - const refreshAssetUrl = useAssetUrlRefresh(ctx.activeThreadEnvironmentId, resource); - const src = assetUrl._tag === "Success" ? assetUrl.url : (file.previewUrl ?? null); - - if (asset === null && src === null) { - return ( -
- {file.name} -
- ); - } - - return ( - { - const preview = buildAttachmentVideoPreview(ctx.activeThreadEnvironmentId, file); - if (preview) ctx.onImageExpand(preview); - }} - className="block aspect-[4/3] w-full" - videoClassName="aspect-auto size-full rounded-lg border border-border/80" - stateClassName="aspect-auto min-h-full rounded-lg border border-border/80 bg-black text-white" - onRetry={asset ? refreshAssetUrl : undefined} - actionsSource={asset ? { kind: "video", name: file.name, src, asset } : undefined} - /> - ); -} - -function UserMessageRow({ row }: { row: Extract }) { - const ctx = use(TimelineRowCtx); - return ctx.workflowOpening?.messageId === row.message.id ? ( - ctx.onOpenThread(ThreadId.make(threadId))} - /> - ) : ( - - ); -} - -function UserTimelineRow({ row }: { row: Extract }) { - const ctx = use(TimelineRowCtx); - const { onImageExpand, onFileOpen } = ctx; - const senderThreadId = row.message.senderThreadId; - const resources = selectMessageImageResources(row.message.attachments); - const previewUrls = useAssetUrls(ctx.activeThreadEnvironmentId, resources); - const [projectPreviews] = useState(createMessageAttachmentPreviewProjector); - const previewUrlsById = new Map( - resources.flatMap((resource, index) => { - const url = previewUrls[index]; - return url ? [[resource.attachmentId, url] as const] : []; - }), - ); - const messageWithPreviews = projectPreviews(row.message, (attachment) => - previewUrlsById.get(attachment.id), - ); - const userImages = (messageWithPreviews.attachments ?? []).filter(isImageAttachment); - const userFiles = (row.message.attachments ?? []).filter(isFileAttachment); - const userVideos = userFiles.filter(isVideoAttachment); - const otherUserFiles = userFiles.filter((file) => !isVideoAttachment(file)); - const unknownAttachments = (row.message.attachments ?? []).filter( - (attachment) => !isImageAttachment(attachment) && !isFileAttachment(attachment), - ); - const userMessage = resolveUserMessagePresentation(row.message); - const resolvedContext = resolveUserMessageContext(row.message); - const previewImages = userImages.filter((image) => image.name.startsWith("preview-annotation-")); - const revertTurnCount = row.revertTurnCount; - const chippedAttachmentIds = new Set( - collectComposerContextReferences(resolvedContext.text).flatMap((occurrence) => { - const record = asKnownContextRecord(resolvedContext.recordsById.get(occurrence.contextId)); - return record?.kind === "file" || record?.kind === "image" ? [record.attachmentId] : []; - }), - ); - const regularImages = userImages.filter((image) => !image.name.startsWith("preview-annotation-")); - const unchippedFiles = otherUserFiles.filter((file) => !chippedAttachmentIds.has(file.id)); - const annotationRecordIds = resolvedContext.records - .filter((record) => record.kind === "preview-annotation") - .map((record) => record.contextId); - const plainText = replaceComposerContextReferences( - resolvedContext.text, - (reference) => reference.label, - ); - const promptLink = promptLinkFor(plainText); - const contextClipboardFragment = - resolvedContext.records.length === 0 - ? null - : encodeComposerContextFragment({ - version: 1, - source: { - environmentId: ctx.activeThreadEnvironmentId, - ...(ctx.threadRef ? { threadId: ctx.threadRef.threadId } : {}), - messageId: row.message.id, - }, - records: resolvedContext.records, - }); - const onBodyCopyCapture = (event: React.ClipboardEvent) => { - if (resolvedContext.records.length === 0 || !event.clipboardData) return; - const selection = window.getSelection(); - if (!selection || selection.isCollapsed) return; - const copiedMarkdown: string[] = []; - for (let index = 0; index < selection.rangeCount; index += 1) { - const container = document.createElement("div"); - container.appendChild(selection.getRangeAt(index).cloneContents()); - // Diagram source is copied text, not authority to export context records. - for (const element of container.querySelectorAll( - "[data-markdown-copy]:not([data-markdown-mermaid])", - )) { - copiedMarkdown.push(element.getAttribute("data-markdown-copy") ?? ""); - } - } - const fragment = selectedMessageContextFragment({ - markdown: copiedMarkdown.join("\n"), - records: resolvedContext.records, - environmentId: ctx.activeThreadEnvironmentId, - ...(ctx.threadRef ? { threadId: ctx.threadRef.threadId } : {}), - messageId: row.message.id, - }); - if (!fragment) return; - // Claim the copy: without preventDefault the browser default overwrites the - // custom MIME type. The default content must then be written back explicitly. - const payload = chatMarkdownClipboardPayload(selection); - event.preventDefault(); - event.clipboardData.setData("text/plain", payload?.text ?? selection.toString()); - if (payload) { - event.clipboardData.setData( - "text/html", - encodeComposerContextClipboardHtml(payload.text, fragment, payload.html), - ); - } - event.clipboardData.setData(COMPOSER_CONTEXT_CLIPBOARD_MIME, fragment); - }; - const renderContextReference = (reference: ChatMarkdownContextReference) => { - const record = asKnownContextRecord(resolvedContext.recordsById.get(reference.contextId)); - const annotationImage = - record?.kind === "preview-annotation" - ? resolvePreviewAnnotationImage({ - record, - recordsById: resolvedContext.recordsById, - userImages, - previewImages, - annotationRecordIds, - }) - : null; - const attachment = - record?.kind === "image" - ? (userImages.find((image) => image.id === record.attachmentId) ?? null) - : record?.kind === "file" - ? (userFiles.find((file) => file.id === record.attachmentId) ?? null) - : null; - return ( - - { - const preview = buildExpandedImagePreview(userImages, image.id); - if (preview) onImageExpand(preview); - }} - onOpenFile={onFileOpen} - onExpandVideo={(file) => { - const preview = buildAttachmentVideoPreview(ctx.activeThreadEnvironmentId, file); - if (preview) onImageExpand(preview); - }} - /> - - ); - }; - - return ( -
- {userMessage.attribution === "automation" ? ( -

- {userMessage.scheduledTaskId ? ( - - Sent by automation - - ) : ( - "Sent by automation" - )} -

- ) : userMessage.attribution === "t3code" ? ( -

- Sent by T3 Code -

- ) : userMessage.attribution === "agent" ? ( -

- {senderThreadId ? ( - ctx.onOpenThread(senderThreadId)} - tone="muted" - aria-label="Open sending thread" - > - Sent by another agent - - ) : ( - "Sent by another agent" - )} -

- ) : null} - {row.message.inputIntent === "steer" || - row.message.inputIntent === "promoted_queued_to_steer" ? ( - - ) : null} -
- You - {(regularImages.length > 0 || userVideos.length > 0) && ( -
- {regularImages.map((image) => ( -
- {image.previewUrl ? ( - - ) : ( -
- {image.name} -
- )} - {image.previewUrl && image.source?.kind === "snap-shot" ? ( - - ) : null} -
- ))} - {userVideos.map((file) => ( - - ))} -
- )} - {unchippedFiles.length > 0 || unknownAttachments.length > 0 ? ( -
- {unchippedFiles.map((file) => { - const fileIdentity = ( - <> - - {file.name} - - ); - if (file.downloadable !== false) { - return ( -
- - - ctx.onFileDownload(file)} - /> - } - > - - - Download {file.name} - -
- ); - } - - return ( -
- {fileIdentity} -
- ); - })} - {unknownAttachments.map((attachment) => ( -
- - {attachment.name} -
- ))} -
- ) : null} -
- -
-
- {row.projectedItem && - row.projectedItem.item.status !== "completed" && - row.projectedItem.item.status !== "pending" && - row.projectedItem.item.status !== "waiting" ? ( -
- - {row.projectedItem.item.status} - -
- ) : null} -
-
- - }> - {formatDayAwareTimestamp(row.message.createdAt, ctx.timestampFormat)} - - - {formatChatTimestampTooltip(row.message.createdAt, ctx.timestampFormat)} - - -
- {typeof revertTurnCount === "number" && ( - - )} - {resolvedContext.text && ( - - )} - {promptLink !== null && ( - - )} -
-
-
-
- ); -} - -function UserMessageIntentMarker({ - intent, -}: { - readonly intent: "steer" | "promoted_queued_to_steer"; -}) { - return ( - - - } - > - - - {intent === "promoted_queued_to_steer" - ? "Originally queued, then promoted to steer the active turn" - : "Steered the active turn"} - - - ); -} - -function resolvePreviewAnnotationImage(input: { - record: Extract; - recordsById: ReadonlyMap; - userImages: ReadonlyArray; - previewImages: ReadonlyArray; - annotationRecordIds: ReadonlyArray; -}): ChatImageAttachment | null { - const screenshotRecord = input.record.screenshotContextId - ? asKnownContextRecord(input.recordsById.get(input.record.screenshotContextId)) - : undefined; - return ( - (screenshotRecord?.kind === "image" - ? input.userImages.find((image) => image.id === screenshotRecord.attachmentId) - : undefined) ?? - input.previewImages.find( - (image) => image.name === `preview-annotation-${input.record.annotationId}.png`, - ) ?? - input.previewImages[input.annotationRecordIds.indexOf(input.record.contextId)] ?? - null - ); -} - -function RevertUserMessageButton({ - turnCount, - messageId, -}: { - turnCount: number; - messageId: MessageId; -}) { - const ctx = use(TimelineRowCtx); - const activity = use(TimelineRowActivityCtx); - - return ( - - ctx.onRevertToTurnCount(turnCount, messageId)} - aria-label="Edit from here" - /> - } - > - - - Edit from here - - ); -} - function TurnFoldTimelineRow({ row }: { row: Extract }) { const ctx = use(TimelineRowCtx); @@ -2928,644 +2349,6 @@ function AssistantChangedFilesSectionInner({ ); } -function UserMessageMentionChip(props: { - record: Extract; - copyMarkdown: string; -}) { - const ctx = use(TimelineRowCtx); - return ( - - } - aria-label={`Preview ${props.record.path}`} - data-markdown-copy={props.copyMarkdown} - onClick={() => { - if (ctx.threadRef) - useRightPanelStore.getState().openFile(ctx.threadRef, props.record.path); - }} - > - - {props.record.label} - - } - /> - - - - - ); -} - -function UserMessageContextChip(props: { - icon: ReactNode; - label: string; - kindLabel?: string; - copyMarkdown: string; - tooltip?: string; - kind: ContextChipKind; -}) { - return ( - - ); -} - -function UserMessagePullRequestContextChip(props: { - record: Extract; - copyMarkdown: string; - kind: ContextChipKind; -}) { - const { activeThreadEnvironmentId, openPullRequest } = use(TimelineRowCtx); - const metadata = props.record.pullRequest; - if (metadata === undefined) return null; - return ( - - ); -} - -function UserMessagePreviewAnnotationDetails(props: { - record: Extract; - image: ChatImageAttachment | null; -}) { - const ctx = use(TimelineRowCtx); - const visibleElements = props.record.elements ?? []; - return ( -
- {props.image?.previewUrl ? ( - - ) : ( -
- Screenshot unavailable -
- )} -
-
- {props.record.pageTitle?.trim() || props.record.pageUrl || "Preview annotation"} -
- {props.record.comment ? ( -
- {props.record.comment} -
- ) : null} -
- {props.record.targetSummary ? ( - {props.record.targetSummary} - ) : null} - {(props.record.styleChanges?.length ?? 0) > 0 ? ( - - - {props.record.styleChanges?.length ?? 0} - - ) : null} -
- {visibleElements.length > 0 ? ( -
- {visibleElements.map((element) => { - const source = element.source; - const sourceLabel = source?.fileName - ? `${source.fileName}${source.lineNumber === null ? "" : `:${source.lineNumber}`}` - : null; - return ( -
-
- - {element.selector || `<${element.tagName}>`} - - {sourceLabel ? ( - {sourceLabel} - ) : null} -
- {element.htmlPreview?.trim() ? ( -
-                      {element.htmlPreview.trim()}
-                    
- ) : null} -
- ); - })} - {(props.record.elements?.length ?? 0) > visibleElements.length ? ( -
- {(props.record.elements?.length ?? 0) - visibleElements.length} more selected - elements -
- ) : null} -
- ) : null} -
-
- ); -} - -function UserMessageElementDetails({ - record, -}: { - record: Extract; -}) { - const sourceLabel = record.source?.fileName - ? `${record.source.fileName}${record.source.lineNumber === null ? "" : `:${record.source.lineNumber}`}` - : null; - return ( -
-
-
- {record.pageTitle?.trim() || record.pageUrl} -
-
{record.pageUrl}
-
-
-
- - {record.selector || `<${record.tagName}>`} - - {sourceLabel ? ( - {sourceLabel} - ) : null} -
- {record.htmlPreview?.trim() ? ( -
- -
- ) : null} - {record.styles?.trim() ? ( -
- -
- ) : null} -
-
- ); -} - -interface UserMessageContextRenderContext { - reference: ChatMarkdownContextReference; - annotationImage: ChatImageAttachment | null; - attachment: ChatImageAttachment | ChatFileAttachment | null; - resolvedTheme: "light" | "dark"; - copyMarkdown: string; - onExpandImage: (image: ChatImageAttachment) => void; - onExpandVideo: (file: ChatFileAttachment) => void; - onOpenFile: (file: ChatFileAttachment) => void; -} - -function UnavailableUserMessageContextChip(props: UserMessageContextRenderContext) { - return ( - - ); -} - -const userMessageContextPresentationRegistry = createContextPresentationRegistry< - KnownComposerContextRecord, - UserMessageContextRenderContext, - ReactNode ->({ - requiredKinds: COMPOSER_CONTEXT_KINDS, - handlers: [ - { - kind: "mention", - canRender: (record) => record.kind === "mention", - render: (record, context) => - record.kind === "mention" ? ( - - ) : ( - - ), - }, - { - kind: "skill", - canRender: (record) => record.kind === "skill", - render: (record, context) => - record.kind === "skill" ? ( - } - label={record.label || record.name} - kindLabel="Skill" - tooltip={`$${record.name}`} - copyMarkdown={context.copyMarkdown} - kind="skill" - /> - ) : ( - - ), - }, - { - kind: "thread", - canRender: (record) => record.kind === "thread", - render: (record, context) => - record.kind === "thread" ? ( - - ) : ( - - ), - }, - { - kind: "image", - canRender: (record, context) => - record.kind === "image" && - context.attachment !== null && - isImageAttachment(context.attachment), - render: (record, context) => { - if ( - record.kind !== "image" || - context.attachment === null || - !isImageAttachment(context.attachment) - ) { - return ; - } - const attachment = context.attachment; - return ( - context.onExpandImage(attachment)} - /> - ); - }, - }, - { - kind: "file", - canRender: (record, context) => - record.kind === "file" && context.attachment !== null && context.attachment.type === "file", - render: (record, context) => { - if ( - record.kind !== "file" || - context.attachment === null || - context.attachment.type !== "file" - ) { - return ; - } - const attachment = context.attachment; - const isVideo = isVideoAttachment(attachment); - const disabled = - attachment.downloadable === false && (!isVideo || attachment.previewUrl === undefined); - const size = formatAttachmentSize(record.sizeBytes); - return ( - - isVideo ? context.onExpandVideo(attachment) : context.onOpenFile(attachment) - } - tooltip={`${record.name}\n${size}`} - /> - ); - }, - }, - { - kind: "terminal", - canRender: (record) => record.kind === "terminal", - render: (record, context, definition) => - record.kind === "terminal" ? ( - - - - ) : ( - - ), - }, - { - kind: "element", - canRender: (record) => record.kind === "element", - render: (record, context) => - record.kind === "element" ? ( - } - label={record.label} - > - - - ) : ( - - ), - }, - { - kind: "review-comment", - canRender: (record) => record.kind === "review-comment", - render: (record, context) => { - if (record.kind !== "review-comment") { - return ; - } - const isPullRequest = isPullRequestSummaryContext(record); - const label = reviewCommentContextLabel(record); - const kindLabel = isPullRequest ? pullRequestContextKindLabel(record) : "Review comment"; - const pullRequestState = pullRequestContextDisplayState(record) ?? "unknown"; - if (isPullRequest && record.pullRequest !== undefined) { - return ( - - ); - } - return ( - : } - label={label} - > - - - ); - }, - }, - { - kind: "preview-annotation", - canRender: (record) => record.kind === "preview-annotation", - render: (record, context) => - record.kind === "preview-annotation" ? ( - } - label={record.label} - > - - - ) : ( - - ), - }, - ], - fallback: (_kind, _record, context) => , -}); - -function UserMessageContextReferenceChip(props: { - reference: ChatMarkdownContextReference; - record: KnownComposerContextRecord | undefined; - annotationImage: ChatImageAttachment | null; - attachment: ChatImageAttachment | ChatFileAttachment | null; - onExpandImage: (image: ChatImageAttachment) => void; - onExpandVideo: (file: ChatFileAttachment) => void; - onOpenFile: (file: ChatFileAttachment) => void; -}) { - const { resolvedTheme } = use(TimelineRowCtx); - const copyMarkdown = formatComposerContextReference({ - kind: props.reference.kind, - contextId: props.reference.contextId as ComposerContextId, - label: props.reference.label, - }); - return userMessageContextPresentationRegistry.render(props.reference.kind, props.record, { - reference: props.reference, - annotationImage: props.annotationImage, - attachment: props.attachment, - resolvedTheme, - copyMarkdown, - onExpandImage: props.onExpandImage, - onExpandVideo: props.onExpandVideo, - onOpenFile: props.onOpenFile, - }); -} - -const COLLAPSED_USER_MESSAGE_FADE_HEIGHT_REM = 1.75; -const COLLAPSED_USER_MESSAGE_FADE_MASK = `linear-gradient(to bottom, black calc(100% - ${COLLAPSED_USER_MESSAGE_FADE_HEIGHT_REM}rem), transparent)`; - -const CollapsibleUserMessageBody = memo(function CollapsibleUserMessageBody(props: { - text: string; - renderContextReference: (reference: ChatMarkdownContextReference) => ReactNode; - skills: ReadonlyArray>; - markdownCwd: string | undefined; - footer?: ReactNode; -}) { - const [expanded, setExpanded] = useState(false); - const revealForFind = () => setExpanded(true); - const revealRef = findRevealRef(revealForFind); - const hasVisibleBody = props.text.trim().length > 0; - const canCollapse = hasVisibleBody && shouldCollapseUserMessage(props.text); - const isCollapsed = canCollapse && !expanded; - const showCollapseControl = canCollapse; - - return ( -
- {hasVisibleBody ? ( -
- -
- ) : null} - {showCollapseControl || props.footer ? ( -
- {showCollapseControl ? ( - - ) : null} - {props.footer ? ( -
{props.footer}
- ) : null} -
- ) : null} -
- ); -}); - -const UserMessageBody = memo(function UserMessageBody(props: { - text: string; - renderContextReference?: (reference: ChatMarkdownContextReference) => ReactNode; - skills: ReadonlyArray>; - markdownCwd: string | undefined; -}) { - const ctx = use(TimelineRowCtx); - if (props.text.length === 0) { - return null; - } - return ( - - ); -}); - -function UserMessageReviewCommentCard({ comment }: { comment: ReviewCommentContext }) { - const renderedAppearance = useRenderedAppearance(); - const ctx = use(TimelineRowCtx); - const fenceLanguage = comment.fenceLanguage ?? "diff"; - const renderablePatch = getRenderablePatch( - buildReviewCommentRenderablePatch(comment), - `review-comment:${comment.id}`, - ); - - return ( -
-
-
- {formatWorkspaceRelativePath(comment.filePath, ctx.workspaceRoot)} -
-
- {comment.sectionTitle} · {comment.rangeLabel} -
-
- {comment.text.length > 0 && ( -
- -
- )} - {fenceLanguage !== "diff" && comment.diff.trim().length > 0 && ( - - )} - {renderablePatch?.kind === "files" && ( - - {renderablePatch.files.map((fileDiff) => ( - - ))} - - )} - {renderablePatch?.kind === "raw" && ( -
-          {renderablePatch.text}
-        
- )} -
- ); -} - function handoffRunsSignature(runs: ReadonlyArray): string { return runs .map( diff --git a/apps/web/src/components/chat/TimelineUserMessageRow.logic.ts b/apps/web/src/components/chat/TimelineUserMessageRow.logic.ts new file mode 100644 index 000000000000..9738e29d7cca --- /dev/null +++ b/apps/web/src/components/chat/TimelineUserMessageRow.logic.ts @@ -0,0 +1,25 @@ +import { type ChatImageAttachment } from "../../types"; +import type { ComposerContextRecord, KnownComposerContextRecord } from "@t3tools/contracts"; +import { asKnownContextRecord } from "~/lib/composerContextRecords"; + +export function resolvePreviewAnnotationImage(input: { + record: Extract; + recordsById: ReadonlyMap; + userImages: ReadonlyArray; + previewImages: ReadonlyArray; + annotationRecordIds: ReadonlyArray; +}): ChatImageAttachment | null { + const screenshotRecord = input.record.screenshotContextId + ? asKnownContextRecord(input.recordsById.get(input.record.screenshotContextId)) + : undefined; + return ( + (screenshotRecord?.kind === "image" + ? input.userImages.find((image) => image.id === screenshotRecord.attachmentId) + : undefined) ?? + input.previewImages.find( + (image) => image.name === `preview-annotation-${input.record.annotationId}.png`, + ) ?? + input.previewImages[input.annotationRecordIds.indexOf(input.record.contextId)] ?? + null + ); +} diff --git a/apps/web/src/components/chat/TimelineUserMessageRow.tsx b/apps/web/src/components/chat/TimelineUserMessageRow.tsx new file mode 100644 index 000000000000..388562ef25ea --- /dev/null +++ b/apps/web/src/components/chat/TimelineUserMessageRow.tsx @@ -0,0 +1,619 @@ +import { findRevealRef } from "./markdownFindContext"; +import { type MessageId, type ServerProviderSkill, ThreadId } from "@t3tools/contracts"; +import { resolveUserMessagePresentation } from "@t3tools/client-runtime/user-message"; +import { Link } from "@tanstack/react-router"; +import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences"; +import { memo, use, useState, type ReactNode } from "react"; +import { + createMessageAttachmentPreviewProjector, + selectMessageImageResources, +} from "../../session-logic"; +import { + type ChatFileAttachment, + isFileAttachment, + isImageAttachment, + isVideoAttachment, +} from "../../types"; +import ChatMarkdown from "../ChatMarkdown"; +import { DownloadIcon, EyeIcon, Redo2Icon, Undo2Icon } from "lucide-react"; +import { Link as LinkIconNode } from "lucide"; +import { Button, InlineButton } from "../ui/button"; +import { useAssetUrlRefresh, useAssetUrls, useAssetUrlState } from "../../assets/assetUrls"; +import { AttachmentImage } from "../media/AttachmentImage"; +import { MediaVideoPlayer } from "../media/MediaVideoPlayer"; +import { + buildAttachmentVideoAsset, + buildAttachmentVideoPreview, + buildExpandedImagePreview, +} from "./ExpandedImagePreview"; +import { + SNAP_SHOT_ATTACHMENT_FRAME_CLASS, + SnapShotAttachmentDetails, +} from "./SnapShotAttachmentDetails"; +import { MessageCopyButton } from "./MessageCopyButton"; +import { promptLinkFor } from "../promptLinks/copyPromptLink"; +import { PierreEntryIcon } from "./PierreEntryIcon"; +import { shouldCollapseUserMessage } from "./MessagesTimeline.logic"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { + asKnownContextRecord, + resolveUserMessageContext, + selectedMessageContextFragment, +} from "~/lib/composerContextRecords"; +import { collectComposerContextReferences } from "@t3tools/shared/composerContextReferences"; +import { + COMPOSER_CONTEXT_CLIPBOARD_MIME, + encodeComposerContextClipboardHtml, + encodeComposerContextFragment, +} from "@t3tools/shared/composerContextClipboard"; +import { chatMarkdownClipboardPayload } from "../../markdown-clipboard"; +import type { ChatMarkdownContextReference } from "../ChatMarkdown"; +import { cn } from "~/lib/utils"; +import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat"; +import { WorkflowOpeningBlock } from "./WorkflowOpeningBlock"; +import { TimelineRowCtx, TimelineRowActivityCtx, type TimelineRow } from "./timelineRowContext"; +import { MESSAGE_HEADING_LEVEL, MessageAuthorHeading } from "./TimelineRowParts"; +import { UserMessageContextReferenceChip } from "./UserMessageContextChips"; +import { resolvePreviewAnnotationImage } from "./TimelineUserMessageRow.logic"; + +function UserVideoAttachment({ file }: { readonly file: ChatFileAttachment }) { + const ctx = use(TimelineRowCtx); + // Row updates rebuild attachment objects, so the asset follows only the + // fields it reads. + const { downloadable, id, mimeType, name } = file; + const environmentId = ctx.activeThreadEnvironmentId; + const asset = + downloadable === false + ? null + : buildAttachmentVideoAsset(environmentId, { id, mimeType, name }); + const resource = asset?.resource ?? null; + const assetUrl = useAssetUrlState(ctx.activeThreadEnvironmentId, resource); + const refreshAssetUrl = useAssetUrlRefresh(ctx.activeThreadEnvironmentId, resource); + const src = assetUrl._tag === "Success" ? assetUrl.url : (file.previewUrl ?? null); + + if (asset === null && src === null) { + return ( +
+ {file.name} +
+ ); + } + + return ( + { + const preview = buildAttachmentVideoPreview(ctx.activeThreadEnvironmentId, file); + if (preview) ctx.onImageExpand(preview); + }} + className="block aspect-[4/3] w-full" + videoClassName="aspect-auto size-full rounded-lg border border-border/80" + stateClassName="aspect-auto min-h-full rounded-lg border border-border/80 bg-black text-white" + onRetry={asset ? refreshAssetUrl : undefined} + actionsSource={asset ? { kind: "video", name: file.name, src, asset } : undefined} + /> + ); +} + +export function UserMessageRow({ row }: { row: Extract }) { + const ctx = use(TimelineRowCtx); + return ctx.workflowOpening?.messageId === row.message.id ? ( + ctx.onOpenThread(ThreadId.make(threadId))} + /> + ) : ( + + ); +} + +function UserTimelineRow({ row }: { row: Extract }) { + const ctx = use(TimelineRowCtx); + const { onImageExpand, onFileOpen } = ctx; + const senderThreadId = row.message.senderThreadId; + const resources = selectMessageImageResources(row.message.attachments); + const previewUrls = useAssetUrls(ctx.activeThreadEnvironmentId, resources); + const [projectPreviews] = useState(createMessageAttachmentPreviewProjector); + const previewUrlsById = new Map( + resources.flatMap((resource, index) => { + const url = previewUrls[index]; + return url ? [[resource.attachmentId, url] as const] : []; + }), + ); + const messageWithPreviews = projectPreviews(row.message, (attachment) => + previewUrlsById.get(attachment.id), + ); + const userImages = (messageWithPreviews.attachments ?? []).filter(isImageAttachment); + const userFiles = (row.message.attachments ?? []).filter(isFileAttachment); + const userVideos = userFiles.filter(isVideoAttachment); + const otherUserFiles = userFiles.filter((file) => !isVideoAttachment(file)); + const unknownAttachments = (row.message.attachments ?? []).filter( + (attachment) => !isImageAttachment(attachment) && !isFileAttachment(attachment), + ); + const userMessage = resolveUserMessagePresentation(row.message); + const resolvedContext = resolveUserMessageContext(row.message); + const previewImages = userImages.filter((image) => image.name.startsWith("preview-annotation-")); + const revertTurnCount = row.revertTurnCount; + const chippedAttachmentIds = new Set( + collectComposerContextReferences(resolvedContext.text).flatMap((occurrence) => { + const record = asKnownContextRecord(resolvedContext.recordsById.get(occurrence.contextId)); + return record?.kind === "file" || record?.kind === "image" ? [record.attachmentId] : []; + }), + ); + const regularImages = userImages.filter((image) => !image.name.startsWith("preview-annotation-")); + const unchippedFiles = otherUserFiles.filter((file) => !chippedAttachmentIds.has(file.id)); + const annotationRecordIds = resolvedContext.records + .filter((record) => record.kind === "preview-annotation") + .map((record) => record.contextId); + const plainText = replaceComposerContextReferences( + resolvedContext.text, + (reference) => reference.label, + ); + const promptLink = promptLinkFor(plainText); + const contextClipboardFragment = + resolvedContext.records.length === 0 + ? null + : encodeComposerContextFragment({ + version: 1, + source: { + environmentId: ctx.activeThreadEnvironmentId, + ...(ctx.threadRef ? { threadId: ctx.threadRef.threadId } : {}), + messageId: row.message.id, + }, + records: resolvedContext.records, + }); + const onBodyCopyCapture = (event: React.ClipboardEvent) => { + if (resolvedContext.records.length === 0 || !event.clipboardData) return; + const selection = window.getSelection(); + if (!selection || selection.isCollapsed) return; + const copiedMarkdown: string[] = []; + for (let index = 0; index < selection.rangeCount; index += 1) { + const container = document.createElement("div"); + container.appendChild(selection.getRangeAt(index).cloneContents()); + // Diagram source is copied text, not authority to export context records. + for (const element of container.querySelectorAll( + "[data-markdown-copy]:not([data-markdown-mermaid])", + )) { + copiedMarkdown.push(element.getAttribute("data-markdown-copy") ?? ""); + } + } + const fragment = selectedMessageContextFragment({ + markdown: copiedMarkdown.join("\n"), + records: resolvedContext.records, + environmentId: ctx.activeThreadEnvironmentId, + ...(ctx.threadRef ? { threadId: ctx.threadRef.threadId } : {}), + messageId: row.message.id, + }); + if (!fragment) return; + // Claim the copy: without preventDefault the browser default overwrites the + // custom MIME type. The default content must then be written back explicitly. + const payload = chatMarkdownClipboardPayload(selection); + event.preventDefault(); + event.clipboardData.setData("text/plain", payload?.text ?? selection.toString()); + if (payload) { + event.clipboardData.setData( + "text/html", + encodeComposerContextClipboardHtml(payload.text, fragment, payload.html), + ); + } + event.clipboardData.setData(COMPOSER_CONTEXT_CLIPBOARD_MIME, fragment); + }; + const renderContextReference = (reference: ChatMarkdownContextReference) => { + const record = asKnownContextRecord(resolvedContext.recordsById.get(reference.contextId)); + const annotationImage = + record?.kind === "preview-annotation" + ? resolvePreviewAnnotationImage({ + record, + recordsById: resolvedContext.recordsById, + userImages, + previewImages, + annotationRecordIds, + }) + : null; + const attachment = + record?.kind === "image" + ? (userImages.find((image) => image.id === record.attachmentId) ?? null) + : record?.kind === "file" + ? (userFiles.find((file) => file.id === record.attachmentId) ?? null) + : null; + return ( + + { + const preview = buildExpandedImagePreview(userImages, image.id); + if (preview) onImageExpand(preview); + }} + onOpenFile={onFileOpen} + onExpandVideo={(file) => { + const preview = buildAttachmentVideoPreview(ctx.activeThreadEnvironmentId, file); + if (preview) onImageExpand(preview); + }} + /> + + ); + }; + + return ( +
+ {userMessage.attribution === "automation" ? ( +

+ {userMessage.scheduledTaskId ? ( + + Sent by automation + + ) : ( + "Sent by automation" + )} +

+ ) : userMessage.attribution === "t3code" ? ( +

+ Sent by T3 Code +

+ ) : userMessage.attribution === "agent" ? ( +

+ {senderThreadId ? ( + ctx.onOpenThread(senderThreadId)} + tone="muted" + aria-label="Open sending thread" + > + Sent by another agent + + ) : ( + "Sent by another agent" + )} +

+ ) : null} + {row.message.inputIntent === "steer" || + row.message.inputIntent === "promoted_queued_to_steer" ? ( + + ) : null} +
+ You + {(regularImages.length > 0 || userVideos.length > 0) && ( +
+ {regularImages.map((image) => ( +
+ {image.previewUrl ? ( + + ) : ( +
+ {image.name} +
+ )} + {image.previewUrl && image.source?.kind === "snap-shot" ? ( + + ) : null} +
+ ))} + {userVideos.map((file) => ( + + ))} +
+ )} + {unchippedFiles.length > 0 || unknownAttachments.length > 0 ? ( +
+ {unchippedFiles.map((file) => { + const fileIdentity = ( + <> + + {file.name} + + ); + if (file.downloadable !== false) { + return ( +
+ + + ctx.onFileDownload(file)} + /> + } + > + + + Download {file.name} + +
+ ); + } + + return ( +
+ {fileIdentity} +
+ ); + })} + {unknownAttachments.map((attachment) => ( +
+ + {attachment.name} +
+ ))} +
+ ) : null} +
+ +
+
+ {row.projectedItem && + row.projectedItem.item.status !== "completed" && + row.projectedItem.item.status !== "pending" && + row.projectedItem.item.status !== "waiting" ? ( +
+ + {row.projectedItem.item.status} + +
+ ) : null} +
+
+ + }> + {formatDayAwareTimestamp(row.message.createdAt, ctx.timestampFormat)} + + + {formatChatTimestampTooltip(row.message.createdAt, ctx.timestampFormat)} + + +
+ {typeof revertTurnCount === "number" && ( + + )} + {resolvedContext.text && ( + + )} + {promptLink !== null && ( + + )} +
+
+
+
+ ); +} + +function UserMessageIntentMarker({ + intent, +}: { + readonly intent: "steer" | "promoted_queued_to_steer"; +}) { + return ( + + + } + > + + + {intent === "promoted_queued_to_steer" + ? "Originally queued, then promoted to steer the active turn" + : "Steered the active turn"} + + + ); +} + +function RevertUserMessageButton({ + turnCount, + messageId, +}: { + turnCount: number; + messageId: MessageId; +}) { + const ctx = use(TimelineRowCtx); + const activity = use(TimelineRowActivityCtx); + + return ( + + ctx.onRevertToTurnCount(turnCount, messageId)} + aria-label="Edit from here" + /> + } + > + + + Edit from here + + ); +} + +const COLLAPSED_USER_MESSAGE_FADE_HEIGHT_REM = 1.75; + +const COLLAPSED_USER_MESSAGE_FADE_MASK = `linear-gradient(to bottom, black calc(100% - ${COLLAPSED_USER_MESSAGE_FADE_HEIGHT_REM}rem), transparent)`; + +const CollapsibleUserMessageBody = memo(function CollapsibleUserMessageBody(props: { + text: string; + renderContextReference: (reference: ChatMarkdownContextReference) => ReactNode; + skills: ReadonlyArray>; + markdownCwd: string | undefined; + footer?: ReactNode; +}) { + const [expanded, setExpanded] = useState(false); + const revealForFind = () => setExpanded(true); + const revealRef = findRevealRef(revealForFind); + const hasVisibleBody = props.text.trim().length > 0; + const canCollapse = hasVisibleBody && shouldCollapseUserMessage(props.text); + const isCollapsed = canCollapse && !expanded; + const showCollapseControl = canCollapse; + + return ( +
+ {hasVisibleBody ? ( +
+ +
+ ) : null} + {showCollapseControl || props.footer ? ( +
+ {showCollapseControl ? ( + + ) : null} + {props.footer ? ( +
{props.footer}
+ ) : null} +
+ ) : null} +
+ ); +}); + +const UserMessageBody = memo(function UserMessageBody(props: { + text: string; + renderContextReference?: (reference: ChatMarkdownContextReference) => ReactNode; + skills: ReadonlyArray>; + markdownCwd: string | undefined; +}) { + const ctx = use(TimelineRowCtx); + if (props.text.length === 0) { + return null; + } + return ( + + ); +}); diff --git a/apps/web/src/components/chat/UserMessageContextChips.tsx b/apps/web/src/components/chat/UserMessageContextChips.tsx new file mode 100644 index 000000000000..cc5474c37901 --- /dev/null +++ b/apps/web/src/components/chat/UserMessageContextChips.tsx @@ -0,0 +1,592 @@ +import { useRenderedAppearance } from "../../hooks/useTheme"; +import { PullRequestGlyph } from "../pullRequest/pullRequestIcons"; +import { ReadOnlySourcePreview } from "../files/AttachmentFilePreview"; +import { useRightPanelStore } from "~/rightPanelStore"; +import { COMPOSER_CONTEXT_KINDS } from "@t3tools/contracts"; +import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments"; +import { use, type ReactNode } from "react"; +import { FileDiff } from "@pierre/diffs/react"; +import { DiffWorkerPoolProvider } from "../DiffWorkerPoolProvider"; +import { + type ChatFileAttachment, + type ChatImageAttachment, + isImageAttachment, + isVideoAttachment, +} from "../../types"; +import { + getRenderablePatch, + resolveDiffThemeName, + resolveFileDiffPath, +} from "../../lib/diffRendering"; +import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting"; +import ChatMarkdown from "../ChatMarkdown"; +import { ThreadContextChip } from "../ThreadContextChip"; +import { MessageCircleIcon, MousePointerClickIcon, PaintbrushIcon } from "lucide-react"; +import type { ComposerContextId, KnownComposerContextRecord } from "@t3tools/contracts"; +import { buildExpandedImagePreview } from "./ExpandedImagePreview"; +import { WorkspaceEntryIcon, WorkspaceEntryTooltip } from "./WorkspaceEntryIcon"; +import { TerminalContextInlineChip } from "./TerminalContextInlineChip"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { + ContextChipPopover as UserMessageContextPopover, + ContextChipShell, + FileChip, + ImageChipButton, + PULL_REQUEST_CHIP_KINDS, + PullRequestChip, + UnresolvedChip, +} from "../contextChipParts"; +import { + isPullRequestSummaryContext, + pullRequestContextDisplayState, + pullRequestContextKindLabel, + reviewCommentContextLabel, +} from "~/lib/composerContextRecords"; +import { formatComposerContextReference } from "@t3tools/shared/composerContextReferences"; +import { ContextChip, ContextChipLabel, type ContextChipKind } from "../ContextChip"; +import { createContextPresentationRegistry } from "../contextPresentationRegistry"; +import type { ChatMarkdownContextReference } from "../ChatMarkdown"; +import { SkillChipIcon } from "./SkillInlineText"; +import { formatWorkspaceRelativePath } from "../../filePathDisplay"; +import { + buildReviewCommentRenderablePatch, + formatReviewCommentFence, + type ReviewCommentContext, +} from "../../reviewCommentContext"; +import { TimelineRowCtx } from "./timelineRowContext"; + +function UserMessageMentionChip(props: { + record: Extract; + copyMarkdown: string; +}) { + const ctx = use(TimelineRowCtx); + return ( + + } + aria-label={`Preview ${props.record.path}`} + data-markdown-copy={props.copyMarkdown} + onClick={() => { + if (ctx.threadRef) + useRightPanelStore.getState().openFile(ctx.threadRef, props.record.path); + }} + > + + {props.record.label} + + } + /> + + + + + ); +} + +function UserMessageContextChip(props: { + icon: ReactNode; + label: string; + kindLabel?: string; + copyMarkdown: string; + tooltip?: string; + kind: ContextChipKind; +}) { + return ( + + ); +} + +function UserMessagePullRequestContextChip(props: { + record: Extract; + copyMarkdown: string; + kind: ContextChipKind; +}) { + const { activeThreadEnvironmentId, openPullRequest } = use(TimelineRowCtx); + const metadata = props.record.pullRequest; + if (metadata === undefined) return null; + return ( + + ); +} + +function UserMessagePreviewAnnotationDetails(props: { + record: Extract; + image: ChatImageAttachment | null; +}) { + const ctx = use(TimelineRowCtx); + const visibleElements = props.record.elements ?? []; + return ( +
+ {props.image?.previewUrl ? ( + + ) : ( +
+ Screenshot unavailable +
+ )} +
+
+ {props.record.pageTitle?.trim() || props.record.pageUrl || "Preview annotation"} +
+ {props.record.comment ? ( +
+ {props.record.comment} +
+ ) : null} +
+ {props.record.targetSummary ? ( + {props.record.targetSummary} + ) : null} + {(props.record.styleChanges?.length ?? 0) > 0 ? ( + + + {props.record.styleChanges?.length ?? 0} + + ) : null} +
+ {visibleElements.length > 0 ? ( +
+ {visibleElements.map((element) => { + const source = element.source; + const sourceLabel = source?.fileName + ? `${source.fileName}${source.lineNumber === null ? "" : `:${source.lineNumber}`}` + : null; + return ( +
+
+ + {element.selector || `<${element.tagName}>`} + + {sourceLabel ? ( + {sourceLabel} + ) : null} +
+ {element.htmlPreview?.trim() ? ( +
+                      {element.htmlPreview.trim()}
+                    
+ ) : null} +
+ ); + })} + {(props.record.elements?.length ?? 0) > visibleElements.length ? ( +
+ {(props.record.elements?.length ?? 0) - visibleElements.length} more selected + elements +
+ ) : null} +
+ ) : null} +
+
+ ); +} + +function UserMessageElementDetails({ + record, +}: { + record: Extract; +}) { + const sourceLabel = record.source?.fileName + ? `${record.source.fileName}${record.source.lineNumber === null ? "" : `:${record.source.lineNumber}`}` + : null; + return ( +
+
+
+ {record.pageTitle?.trim() || record.pageUrl} +
+
{record.pageUrl}
+
+
+
+ + {record.selector || `<${record.tagName}>`} + + {sourceLabel ? ( + {sourceLabel} + ) : null} +
+ {record.htmlPreview?.trim() ? ( +
+ +
+ ) : null} + {record.styles?.trim() ? ( +
+ +
+ ) : null} +
+
+ ); +} + +interface UserMessageContextRenderContext { + reference: ChatMarkdownContextReference; + annotationImage: ChatImageAttachment | null; + attachment: ChatImageAttachment | ChatFileAttachment | null; + resolvedTheme: "light" | "dark"; + copyMarkdown: string; + onExpandImage: (image: ChatImageAttachment) => void; + onExpandVideo: (file: ChatFileAttachment) => void; + onOpenFile: (file: ChatFileAttachment) => void; +} + +function UnavailableUserMessageContextChip(props: UserMessageContextRenderContext) { + return ( + + ); +} + +const userMessageContextPresentationRegistry = createContextPresentationRegistry< + KnownComposerContextRecord, + UserMessageContextRenderContext, + ReactNode +>({ + requiredKinds: COMPOSER_CONTEXT_KINDS, + handlers: [ + { + kind: "mention", + canRender: (record) => record.kind === "mention", + render: (record, context) => + record.kind === "mention" ? ( + + ) : ( + + ), + }, + { + kind: "skill", + canRender: (record) => record.kind === "skill", + render: (record, context) => + record.kind === "skill" ? ( + } + label={record.label || record.name} + kindLabel="Skill" + tooltip={`$${record.name}`} + copyMarkdown={context.copyMarkdown} + kind="skill" + /> + ) : ( + + ), + }, + { + kind: "thread", + canRender: (record) => record.kind === "thread", + render: (record, context) => + record.kind === "thread" ? ( + + ) : ( + + ), + }, + { + kind: "image", + canRender: (record, context) => + record.kind === "image" && + context.attachment !== null && + isImageAttachment(context.attachment), + render: (record, context) => { + if ( + record.kind !== "image" || + context.attachment === null || + !isImageAttachment(context.attachment) + ) { + return ; + } + const attachment = context.attachment; + return ( + context.onExpandImage(attachment)} + /> + ); + }, + }, + { + kind: "file", + canRender: (record, context) => + record.kind === "file" && context.attachment !== null && context.attachment.type === "file", + render: (record, context) => { + if ( + record.kind !== "file" || + context.attachment === null || + context.attachment.type !== "file" + ) { + return ; + } + const attachment = context.attachment; + const isVideo = isVideoAttachment(attachment); + const disabled = + attachment.downloadable === false && (!isVideo || attachment.previewUrl === undefined); + const size = formatAttachmentSize(record.sizeBytes); + return ( + + isVideo ? context.onExpandVideo(attachment) : context.onOpenFile(attachment) + } + tooltip={`${record.name}\n${size}`} + /> + ); + }, + }, + { + kind: "terminal", + canRender: (record) => record.kind === "terminal", + render: (record, context, definition) => + record.kind === "terminal" ? ( + + + + ) : ( + + ), + }, + { + kind: "element", + canRender: (record) => record.kind === "element", + render: (record, context) => + record.kind === "element" ? ( + } + label={record.label} + > + + + ) : ( + + ), + }, + { + kind: "review-comment", + canRender: (record) => record.kind === "review-comment", + render: (record, context) => { + if (record.kind !== "review-comment") { + return ; + } + const isPullRequest = isPullRequestSummaryContext(record); + const label = reviewCommentContextLabel(record); + const kindLabel = isPullRequest ? pullRequestContextKindLabel(record) : "Review comment"; + const pullRequestState = pullRequestContextDisplayState(record) ?? "unknown"; + if (isPullRequest && record.pullRequest !== undefined) { + return ( + + ); + } + return ( + : } + label={label} + > + + + ); + }, + }, + { + kind: "preview-annotation", + canRender: (record) => record.kind === "preview-annotation", + render: (record, context) => + record.kind === "preview-annotation" ? ( + } + label={record.label} + > + + + ) : ( + + ), + }, + ], + fallback: (_kind, _record, context) => , +}); + +export function UserMessageContextReferenceChip(props: { + reference: ChatMarkdownContextReference; + record: KnownComposerContextRecord | undefined; + annotationImage: ChatImageAttachment | null; + attachment: ChatImageAttachment | ChatFileAttachment | null; + onExpandImage: (image: ChatImageAttachment) => void; + onExpandVideo: (file: ChatFileAttachment) => void; + onOpenFile: (file: ChatFileAttachment) => void; +}) { + const { resolvedTheme } = use(TimelineRowCtx); + const copyMarkdown = formatComposerContextReference({ + kind: props.reference.kind, + contextId: props.reference.contextId as ComposerContextId, + label: props.reference.label, + }); + return userMessageContextPresentationRegistry.render(props.reference.kind, props.record, { + reference: props.reference, + annotationImage: props.annotationImage, + attachment: props.attachment, + resolvedTheme, + copyMarkdown, + onExpandImage: props.onExpandImage, + onExpandVideo: props.onExpandVideo, + onOpenFile: props.onOpenFile, + }); +} + +function UserMessageReviewCommentCard({ comment }: { comment: ReviewCommentContext }) { + const renderedAppearance = useRenderedAppearance(); + const ctx = use(TimelineRowCtx); + const fenceLanguage = comment.fenceLanguage ?? "diff"; + const renderablePatch = getRenderablePatch( + buildReviewCommentRenderablePatch(comment), + `review-comment:${comment.id}`, + ); + + return ( +
+
+
+ {formatWorkspaceRelativePath(comment.filePath, ctx.workspaceRoot)} +
+
+ {comment.sectionTitle} · {comment.rangeLabel} +
+
+ {comment.text.length > 0 && ( +
+ +
+ )} + {fenceLanguage !== "diff" && comment.diff.trim().length > 0 && ( + + )} + {renderablePatch?.kind === "files" && ( + + {renderablePatch.files.map((fileDiff) => ( + + ))} + + )} + {renderablePatch?.kind === "raw" && ( +
+          {renderablePatch.text}
+        
+ )} +
+ ); +} From 476e2c3e3044ec431bf2d65337e0713b40fa8142 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 21 Oct 2026 01:12:50 -0700 Subject: [PATCH 17/73] refactor(web): timeline assistant and orchestration event rows live in their own files (#22677) Co-authored-by: Claude Opus 5.5 --- .../src/components/chat/MessagesTimeline.tsx | 824 +----------------- .../components/chat/TimelineAssistantRows.tsx | 366 ++++++++ .../chat/TimelineV2EventRow.logic.ts | 128 +++ .../components/chat/TimelineV2EventRow.tsx | 321 +++++++ 4 files changed, 831 insertions(+), 808 deletions(-) create mode 100644 apps/web/src/components/chat/TimelineAssistantRows.tsx create mode 100644 apps/web/src/components/chat/TimelineV2EventRow.logic.ts create mode 100644 apps/web/src/components/chat/TimelineV2EventRow.tsx diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index c707e369eb44..ae09911bd9ae 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1,6 +1,5 @@ import { ThreadFindTimelineContext } from "./ThreadFindProvider"; import { shallow } from "zustand/vanilla/shallow"; -import { shouldPreserveAssistantLineBreaks } from "@t3tools/shared/markdownPipeline"; import { MarkdownFindContext } from "./markdownFindContext"; import { useChatCanvas } from "./ChatCanvasContext"; import { WorkLogBlock } from "./WorkLog"; @@ -16,15 +15,8 @@ import { type RunId, ThreadId, } from "@t3tools/contracts"; -import { parseScopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime/environment"; -import { useAtomValue } from "@effect/atom-react"; -import { environmentThreadDetails } from "../../state/threads"; -import { canForkProjectedAssistantItem } from "@t3tools/client-runtime/state/thread-workflows"; +import { parseScopedThreadKey } from "@t3tools/client-runtime/environment"; import { resolveWorkGroupScrollAnchor } from "@t3tools/client-runtime/work-log/scroll-anchor"; -import { - subagentGroupSummary, - summarizeSubagentStatuses, -} from "@t3tools/client-runtime/state/subagent-display"; import type { WorkflowOpening } from "@t3tools/client-runtime/state/subagentRuntime"; import { observeResize } from "~/lib/observeResize"; import { useStableCallback } from "~/hooks/useStableCallback"; @@ -49,34 +41,13 @@ import { type LegendListRef, type MaintainScrollAtEndOptions, } from "@legendapp/list/react"; -import { type TimelineEntry, providerErrorPresentation } from "../../session-logic"; +import { type TimelineEntry } from "../../session-logic"; import type { CodexArtifactTemplate } from "@t3tools/shared/codexArtifactTemplates"; -import { type ChatMessage, type ChatFileAttachment, type TurnDiffSummary } from "../../types"; -import ChatMarkdown from "../ChatMarkdown"; -import { - ArrowRightIcon, - BotIcon, - CheckIcon, - ChevronDownIcon, - CircleAlertIcon, - GitForkIcon, - type LucideIcon, - MinusIcon, - Minimize2Icon, - WrenchIcon, - XIcon, - ZapIcon, -} from "lucide-react"; +import { type ChatFileAttachment, type TurnDiffSummary } from "../../types"; +import { ArrowRightIcon, BotIcon, Minimize2Icon } from "lucide-react"; import { ChevronDown, ChevronRight } from "lucide"; -import { Button } from "../ui/button"; import { MorphIcon } from "~/components/MorphIcon"; import { ExpandedImagePreview } from "./ExpandedImagePreview"; -import { ProposedPlanCard } from "./ProposedPlanCard"; -import { HtmlRenderFrame } from "./HtmlRenderFrame"; -import { McpAppFrame } from "./McpAppFrame"; -import { ChangedFilesCard } from "./ChangedFilesTree"; -import { useFileContextMenuHandler } from "../../fileContextMenu"; -import { useProject, useThreadShell } from "../../state/entities"; import { CHAT_TIMELINE_ANCHOR_OFFSET, readTimelinePosition, @@ -84,16 +55,14 @@ import { rememberTimelinePosition, timelineContentOverflowsViewport, } from "./timelineScrollAnchoring"; -import { MessageCopyButton } from "./MessageCopyButton"; import { AssistantSelectionToolbar } from "./AssistantSelectionToolbar"; import type { AssistantCitationSourceAnchor } from "~/lib/assistantTextSelection"; -import { AssistantCitationSource, type AssistantCitationRequest } from "./AssistantCitationSource"; +import { type AssistantCitationRequest } from "./AssistantCitationSource"; import { useAssistantCitationTarget, type CitationHistoryPage } from "./useAssistantCitationTarget"; import { computeStableMessagesTimelineRows, deriveMessagesTimelineRowsWithState, type MessagesTimelineRowsProjection, - resolveAssistantMessageCopyState, resolveTimelineIsAtEnd, resolveTimelineMinimapHasPersistentGutter, resolveTimelineMinimapCurrentIndex, @@ -110,29 +79,16 @@ import { type ThreadFindMatch } from "./threadFind"; import { useThreadFindNavigation } from "./useThreadFindNavigation"; import { readThreadFindPosition } from "./threadFindPosition"; import type { ThreadFindPositionReader } from "./threadFind"; -import { Tooltip, TooltipPopup, TooltipTrigger, TooltipScrollDismissArea } from "../ui/tooltip"; +import { TooltipScrollDismissArea } from "../ui/tooltip"; import { WorktreeSetupCard } from "./WorktreeSetupCard"; import { useOpenPrLink } from "~/lib/openPullRequestLink"; import { useClientSettings } from "~/hooks/useSettings"; import { useMediaQuery } from "~/hooks/useMediaQuery"; import { cn } from "~/lib/utils"; -import { useUiStateStore } from "~/uiStateStore"; import { type TimestampFormat } from "@t3tools/contracts/settings"; -import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat"; -import { V2ItemInspector } from "./V2ItemInspector"; -import { useV2ItemSupport } from "../../state/v2ItemSupport"; -import { Collapsible, CollapsibleTrigger, CollapsiblePanel } from "../ui/collapsible"; -import { - isV2LifecycleItem, - EnvironmentSubagentAvatar, - SubagentElapsed, - V2LifecycleRow, - type HandoffTimelineRun, -} from "./V2LifecycleRow"; -import { SecretRequestCard } from "./SecretRequestCard"; +import { type HandoffTimelineRun } from "./V2LifecycleRow"; import { TimelineSystemDivider } from "./TimelineSystemDivider"; -import * as DateTime from "effect/DateTime"; import { forwardWheelToTimeline } from "./timelineMinimapWheel"; import { TimelineMinimap } from "./TimelineMinimap"; import { @@ -143,12 +99,7 @@ import { type WorkGroupViewState, type TimelineRow, } from "./timelineRowContext"; -import { - MESSAGE_HEADING_LEVEL, - MessageAuthorHeading, - TimelineRowTimestamp, - ActivityShimmerOverlay, -} from "./TimelineRowParts"; +import { TimelineRowTimestamp, ActivityShimmerOverlay } from "./TimelineRowParts"; import { WorkGroupSection, WorkingTimelineRow, @@ -157,6 +108,14 @@ import { WorkGroupToggleTimelineRow, } from "./TimelineWorkGroupRows"; import { UserMessageRow } from "./TimelineUserMessageRow"; +import { + AssistantTimelineRow, + AssistantMetaTimelineRow, + ProposedPlanTimelineRow, + HtmlRenderTimelineRow, + McpAppTimelineRow, +} from "./TimelineAssistantRows"; +import { V2EventTimelineRow } from "./TimelineV2EventRow"; function timelineListStateFor(identity: string): { readonly identity: string; @@ -1598,757 +1557,6 @@ function AttemptFoldTimelineRow({ row }: { row: Extract }) { - const ctx = use(TimelineRowCtx); - const messageText = row.message.text || (row.message.streaming ? "" : "(empty response)"); - - return ( - <> -
- T3 Code -
- - - -
- - {row.showAssistantMeta ? ( - - ) : null} -
- - ); -} - -function AssistantForkButton({ - projectedItem, -}: { - readonly projectedItem: NonNullable["projectedItem"]>; -}) { - const ctx = use(TimelineRowCtx); - const [busy, setBusy] = useState(false); - const support = useV2ItemSupport({ - environmentId: ctx.activeThreadEnvironmentId, - sourceThreadId: projectedItem.sourceThreadId, - sourceItemId: projectedItem.sourceItemId, - }); - const canFork = canForkProjectedAssistantItem({ - projectedItem, - capabilities: support.providerSession?.capabilities, - }); - - if (!canFork || projectedItem.item.runId === null) return null; - const runId = projectedItem.item.runId; - - return ( - - { - setBusy(true); - void ctx - .onForkFromRun({ sourceThreadId: projectedItem.sourceThreadId, runId }) - .finally(() => setBusy(false)); - }} - aria-label="Fork from this response" - /> - } - > - - - Fork from this response - - ); -} - -function AssistantMetaTimelineRow({ - row, -}: { - row: Extract; -}) { - return ( -
- -
- ); -} - -function AssistantMessageMeta({ - className, - projectedItem, - message, - showCopyButton, - copyStreaming, - alwaysVisible = false, -}: { - className?: string; - projectedItem?: Extract["projectedItem"]; - message: ChatMessage; - showCopyButton: boolean; - copyStreaming: boolean; - alwaysVisible?: boolean; -}) { - const ctx = use(TimelineRowCtx); - - return ( -
- {projectedItem && projectedItem.item.status !== "completed" ? ( - - {projectedItem.item.status} - - ) : null} - - {projectedItem?.item.type === "assistant_message" ? ( - - ) : null} - {!message.streaming && ( - - }> - {formatDayAwareTimestamp(message.updatedAt, ctx.timestampFormat)} - - - {formatChatTimestampTooltip(message.updatedAt, ctx.timestampFormat)} - - - )} -
- ); -} - -function AssistantCopyButton({ - message, - showCopyButton, - streaming, -}: { - message: ChatMessage; - showCopyButton: boolean; - streaming: boolean; -}) { - const assistantCopyState = resolveAssistantMessageCopyState({ - text: message.text ?? null, - showCopyButton, - streaming, - }); - - if (!assistantCopyState.visible) { - return null; - } - - return ; -} - -function ProposedPlanTimelineRow({ - row, -}: { - row: Extract; -}) { - const ctx = use(TimelineRowCtx); - - return ( -
- -
- ); -} - -function HtmlRenderTimelineRow({ row }: { row: Extract }) { - const ctx = use(TimelineRowCtx); - const { onAppFullscreenChange } = ctx; - const onFullscreenChange = (fullscreen: boolean) => onAppFullscreenChange(row.id, fullscreen); - - return ( -
- -
- ); -} - -function McpAppTimelineRow({ row }: { row: Extract }) { - const ctx = use(TimelineRowCtx); - const { awaitingUser } = use(TimelineRowActivityCtx); - - return ( -
- ctx.onAppFullscreenChange(row.id, fullscreen)} - /> -
- ); -} - -type V2EventTone = "muted" | "warning" | "danger" | "success"; - -function v2EventPresentation(item: OrchestrationV2TurnItem): { - readonly label: string; - readonly detail: string | null; - readonly tone: V2EventTone; - readonly icon: LucideIcon; -} { - switch (item.type) { - case "error": { - const presentation = providerErrorPresentation(item); - return { - ...presentation, - tone: - item.status === "completed" - ? "success" - : item.status === "running" || item.failure.class === "usage_limit" - ? "warning" - : "danger", - icon: CircleAlertIcon, - }; - } - case "run_interrupt_request": - return { - label: "Interrupt requested", - detail: item.message, - tone: "warning", - icon: CircleAlertIcon, - }; - case "run_interrupt_result": - return { - label: "Run interrupted", - detail: item.message, - tone: "danger", - icon: XIcon, - }; - case "handoff": - return { - label: "Context handoff", - detail: - item.summary ?? - `${item.fromProviderInstanceIds.join(", ")} → ${item.toProviderInstanceId}`, - tone: item.status === "failed" ? "danger" : "muted", - icon: ZapIcon, - }; - case "fork": - return { - label: "Conversation fork", - detail: `Continues in ${item.targetThreadId}`, - tone: "muted", - icon: GitForkIcon, - }; - case "compaction": { - const tokenSummary = - item.beforeTokenCount === undefined && item.afterTokenCount === undefined - ? null - : `${item.beforeTokenCount ?? "?"} → ${item.afterTokenCount ?? "?"} tokens`; - return { - label: "Context compacted", - detail: item.summary ?? tokenSummary, - tone: item.status === "failed" ? "danger" : "muted", - icon: MinusIcon, - }; - } - case "todo_list": { - const steps = item.steps.map((step) => `${step.status}: ${step.text}`).join("\n"); - return { - label: "Plan updated", - detail: [item.explanation, steps].filter(Boolean).join("\n\n") || null, - tone: item.status === "failed" ? "danger" : "success", - icon: CheckIcon, - }; - } - default: - return { - label: item.title?.trim() || item.type.replaceAll("_", " "), - detail: null, - tone: item.status === "failed" ? "danger" : "muted", - icon: WrenchIcon, - }; - } -} - -function V2EventTimelineRow({ row }: { row: Extract }) { - const ctx = use(TimelineRowCtx); - const { item, visibility, sourceThreadId } = row.projectedItem; - if (item.type === "subagent" && (row.subagents?.length ?? 1) > 1) { - return ; - } - if (item.type === "secret_request") { - return ( - - ); - } - if (isV2LifecycleItem(item)) { - return ( - - ); - } - const presentation = v2EventPresentation(item); - const Icon = presentation.icon; - if (item.type === "error") { - return ( -
- - - {presentation.label} - {item.status !== "completed" ? ( - - {item.status} - - ) : null} - {presentation.detail ? ( - - {presentation.detail} - - ) : null} - {visibility !== "local" ? ( - - {visibility === "inherited" ? "Inherited" : "Synthetic"} - - ) : null} - - -
- {presentation.detail ? ( -
- -
- ) : null} - {visibility === "inherited" ? ( -

- From {sourceThreadId} -

- ) : null} -
- -
-
-
- ); - } - return ( -
-
- -
-
- {presentation.label} - {item.status !== "completed" ? ( - - {item.status} - - ) : null} - {visibility !== "local" ? ( - - {visibility === "inherited" ? "Inherited" : "Synthetic"} - - ) : null} -
- {presentation.detail ? ( -
- -
- ) : null} - {visibility === "inherited" ? ( -

- From {sourceThreadId} -

- ) : null} -
- -
-
-
-
- ); -} - -function subagentGroupTiming( - agents: ReadonlyArray<{ - status: OrchestrationV2TurnItem["status"]; - startedAt: DateTime.Utc | null; - completedAt: DateTime.Utc | null; - }>, -) { - let startMs: number | null = null; - let endMs: number | null = null; - let endUnknown = false; - for (const agent of agents) { - if (agent.startedAt) { - const ms = DateTime.toEpochMillis(agent.startedAt); - startMs = startMs === null ? ms : Math.min(startMs, ms); - } - if (agent.completedAt) { - const ms = DateTime.toEpochMillis(agent.completedAt); - endMs = endMs === null ? ms : Math.max(endMs, ms); - } else { - endUnknown = true; - } - } - const live = agents.some( - ({ status }) => status === "pending" || status === "running" || status === "waiting", - ); - return { - status: live ? ("running" as const) : ("completed" as const), - startedAt: startMs === null ? null : new Date(startMs).toISOString(), - completedAt: live || endUnknown || endMs === null ? null : new Date(endMs).toISOString(), - }; -} - -const V2SubagentGroup = memo(function V2SubagentGroup({ - row, -}: { - row: Extract; -}) { - const ctx = use(TimelineRowCtx); - const groupId = `subagent-group:${row.id}`; - const [expanded, setExpanded] = useState(() => - ctx.workGroupViewState.expandedEntries.has(groupId), - ); - const members = (row.subagents ?? [row.projectedItem]).flatMap(({ item }) => - item.type === "subagent" ? [item] : [], - ); - const liveAgents = useAtomValue( - environmentThreadDetails.threadAtom( - scopeThreadRef(ctx.activeThreadEnvironmentId, row.projectedItem.item.threadId), - ), - (thread) => thread?.projection.subagents, - ); - const agents = members.map((item) => { - const live = liveAgents?.find((agent) => agent.id === item.subagentId); - return { - item, - status: live?.status ?? item.status, - startedAt: live?.startedAt ?? item.startedAt, - completedAt: live?.completedAt ?? item.completedAt, - }; - }); - const summary = subagentGroupSummary(agents); - const label = `${members.length} ${members.length === 1 ? "subagent" : "subagents"}`; - const statusSummary = summarizeSubagentStatuses(agents.map(({ status }) => status)); - const toggleExpanded = (open: boolean) => { - ctx.onToggleWorkEntry(row.id, expanded); - if (open) ctx.workGroupViewState.expandedEntries.add(groupId); - else ctx.workGroupViewState.expandedEntries.delete(groupId); - setExpanded(open); - }; - return ( - - - - - {agents.slice(0, 3).map(({ item, status }) => ( - - ))} - {agents.length > 3 ? ( - - +{agents.length - 3} - - ) : null} - - - {label} - - {statusSummary} - - - - - - - - {/* Virtualized rows must settle before disclosure scroll anchoring resumes. */} - - {expanded ? ( -
- {members.map((item) => ( - - ))} -
- ) : null} -
-
-
- ); -}); - -/** Subscribes directly to the UI state store for expand/collapse state, - * so toggling re-renders only this component — not the entire list. */ -const AssistantChangedFilesSection = memo(function AssistantChangedFilesSection({ - turnSummary, - routeThreadKey, - resolvedTheme, - onOpenTurnDiff, -}: { - turnSummary: TurnDiffSummary | undefined; - routeThreadKey: string; - resolvedTheme: "light" | "dark"; - displayThreadKey?: string; - onOpenTurnDiff: (runId: RunId, filePath?: string) => void; -}) { - if (!turnSummary) return null; - const checkpointFiles = turnSummary.files; - if (checkpointFiles.length === 0) return null; - - return ( - - ); -}); - -function AssistantChangedFilesSectionInner({ - turnSummary, - checkpointFiles, - routeThreadKey, - resolvedTheme, - onOpenTurnDiff, -}: { - turnSummary: TurnDiffSummary; - checkpointFiles: TurnDiffSummary["files"]; - routeThreadKey: string; - resolvedTheme: "light" | "dark"; - displayThreadKey?: string; - onOpenTurnDiff: (runId: RunId, filePath?: string) => void; -}) { - const ctx = use(TimelineRowCtx); - const persistedExpanded = useUiStateStore( - (store) => store.threadChangedFilesExpandedById[routeThreadKey]?.[turnSummary.runId], - ); - const setExpanded = useUiStateStore((store) => store.setThreadChangedFilesExpanded); - const allDirectoriesExpanded = persistedExpanded ?? false; - - const thread = useThreadShell(ctx.threadRef); - const activeProject = useProject( - thread && thread.projectId - ? { environmentId: thread.environmentId, projectId: thread.projectId } - : null, - ); - const onFileContextMenu = useFileContextMenuHandler(ctx.activeThreadEnvironmentId); - - return ( - - setExpanded(routeThreadKey, turnSummary.runId, !allDirectoriesExpanded) - } - onOpenTurnDiff={onOpenTurnDiff} - onFileContextMenu={(filePath, event) => - onFileContextMenu( - { - environmentId: ctx.activeThreadEnvironmentId, - filePath, - workspaceRoot: ctx.workspaceRoot, - repositoryRoot: - thread?.worktreePath == null - ? activeProject?.repositoryIdentity?.rootPath - : undefined, - }, - event, - ) - } - /> - ); -} - function handoffRunsSignature(runs: ReadonlyArray): string { return runs .map( diff --git a/apps/web/src/components/chat/TimelineAssistantRows.tsx b/apps/web/src/components/chat/TimelineAssistantRows.tsx new file mode 100644 index 000000000000..6f99213057f9 --- /dev/null +++ b/apps/web/src/components/chat/TimelineAssistantRows.tsx @@ -0,0 +1,366 @@ +import { shouldPreserveAssistantLineBreaks } from "@t3tools/shared/markdownPipeline"; +import { type RunId } from "@t3tools/contracts"; +import { canForkProjectedAssistantItem } from "@t3tools/client-runtime/state/thread-workflows"; +import { memo, use, useState } from "react"; +import { type ChatMessage, type TurnDiffSummary } from "../../types"; +import ChatMarkdown from "../ChatMarkdown"; +import { GitForkIcon } from "lucide-react"; +import { Button } from "../ui/button"; +import { ProposedPlanCard } from "./ProposedPlanCard"; +import { HtmlRenderFrame } from "./HtmlRenderFrame"; +import { McpAppFrame } from "./McpAppFrame"; +import { ChangedFilesCard } from "./ChangedFilesTree"; +import { useFileContextMenuHandler } from "../../fileContextMenu"; +import { useProject, useThreadShell } from "../../state/entities"; +import { MessageCopyButton } from "./MessageCopyButton"; +import { AssistantCitationSource } from "./AssistantCitationSource"; +import { resolveAssistantMessageCopyState } from "./MessagesTimeline.logic"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { cn } from "~/lib/utils"; +import { useUiStateStore } from "~/uiStateStore"; +import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat"; +import { useV2ItemSupport } from "../../state/v2ItemSupport"; +import { TimelineRowCtx, TimelineRowActivityCtx, type TimelineRow } from "./timelineRowContext"; +import { MESSAGE_HEADING_LEVEL, MessageAuthorHeading } from "./TimelineRowParts"; + +export function AssistantTimelineRow({ row }: { row: Extract }) { + const ctx = use(TimelineRowCtx); + const messageText = row.message.text || (row.message.streaming ? "" : "(empty response)"); + + return ( + <> +
+ T3 Code +
+ + + +
+ + {row.showAssistantMeta ? ( + + ) : null} +
+ + ); +} + +function AssistantForkButton({ + projectedItem, +}: { + readonly projectedItem: NonNullable["projectedItem"]>; +}) { + const ctx = use(TimelineRowCtx); + const [busy, setBusy] = useState(false); + const support = useV2ItemSupport({ + environmentId: ctx.activeThreadEnvironmentId, + sourceThreadId: projectedItem.sourceThreadId, + sourceItemId: projectedItem.sourceItemId, + }); + const canFork = canForkProjectedAssistantItem({ + projectedItem, + capabilities: support.providerSession?.capabilities, + }); + + if (!canFork || projectedItem.item.runId === null) return null; + const runId = projectedItem.item.runId; + + return ( + + { + setBusy(true); + void ctx + .onForkFromRun({ sourceThreadId: projectedItem.sourceThreadId, runId }) + .finally(() => setBusy(false)); + }} + aria-label="Fork from this response" + /> + } + > + + + Fork from this response + + ); +} + +export function AssistantMetaTimelineRow({ + row, +}: { + row: Extract; +}) { + return ( +
+ +
+ ); +} + +function AssistantMessageMeta({ + className, + projectedItem, + message, + showCopyButton, + copyStreaming, + alwaysVisible = false, +}: { + className?: string; + projectedItem?: Extract["projectedItem"]; + message: ChatMessage; + showCopyButton: boolean; + copyStreaming: boolean; + alwaysVisible?: boolean; +}) { + const ctx = use(TimelineRowCtx); + + return ( +
+ {projectedItem && projectedItem.item.status !== "completed" ? ( + + {projectedItem.item.status} + + ) : null} + + {projectedItem?.item.type === "assistant_message" ? ( + + ) : null} + {!message.streaming && ( + + }> + {formatDayAwareTimestamp(message.updatedAt, ctx.timestampFormat)} + + + {formatChatTimestampTooltip(message.updatedAt, ctx.timestampFormat)} + + + )} +
+ ); +} + +function AssistantCopyButton({ + message, + showCopyButton, + streaming, +}: { + message: ChatMessage; + showCopyButton: boolean; + streaming: boolean; +}) { + const assistantCopyState = resolveAssistantMessageCopyState({ + text: message.text ?? null, + showCopyButton, + streaming, + }); + + if (!assistantCopyState.visible) { + return null; + } + + return ; +} + +export function ProposedPlanTimelineRow({ + row, +}: { + row: Extract; +}) { + const ctx = use(TimelineRowCtx); + + return ( +
+ +
+ ); +} + +export function HtmlRenderTimelineRow({ + row, +}: { + row: Extract; +}) { + const ctx = use(TimelineRowCtx); + const { onAppFullscreenChange } = ctx; + const onFullscreenChange = (fullscreen: boolean) => onAppFullscreenChange(row.id, fullscreen); + + return ( +
+ +
+ ); +} + +export function McpAppTimelineRow({ row }: { row: Extract }) { + const ctx = use(TimelineRowCtx); + const { awaitingUser } = use(TimelineRowActivityCtx); + + return ( +
+ ctx.onAppFullscreenChange(row.id, fullscreen)} + /> +
+ ); +} + +/** Subscribes directly to the UI state store for expand/collapse state, + * so toggling re-renders only this component — not the entire list. */ +const AssistantChangedFilesSection = memo(function AssistantChangedFilesSection({ + turnSummary, + routeThreadKey, + resolvedTheme, + onOpenTurnDiff, +}: { + turnSummary: TurnDiffSummary | undefined; + routeThreadKey: string; + resolvedTheme: "light" | "dark"; + displayThreadKey?: string; + onOpenTurnDiff: (runId: RunId, filePath?: string) => void; +}) { + if (!turnSummary) return null; + const checkpointFiles = turnSummary.files; + if (checkpointFiles.length === 0) return null; + + return ( + + ); +}); + +function AssistantChangedFilesSectionInner({ + turnSummary, + checkpointFiles, + routeThreadKey, + resolvedTheme, + onOpenTurnDiff, +}: { + turnSummary: TurnDiffSummary; + checkpointFiles: TurnDiffSummary["files"]; + routeThreadKey: string; + resolvedTheme: "light" | "dark"; + displayThreadKey?: string; + onOpenTurnDiff: (runId: RunId, filePath?: string) => void; +}) { + const ctx = use(TimelineRowCtx); + const persistedExpanded = useUiStateStore( + (store) => store.threadChangedFilesExpandedById[routeThreadKey]?.[turnSummary.runId], + ); + const setExpanded = useUiStateStore((store) => store.setThreadChangedFilesExpanded); + const allDirectoriesExpanded = persistedExpanded ?? false; + + const thread = useThreadShell(ctx.threadRef); + const activeProject = useProject( + thread && thread.projectId + ? { environmentId: thread.environmentId, projectId: thread.projectId } + : null, + ); + const onFileContextMenu = useFileContextMenuHandler(ctx.activeThreadEnvironmentId); + + return ( + + setExpanded(routeThreadKey, turnSummary.runId, !allDirectoriesExpanded) + } + onOpenTurnDiff={onOpenTurnDiff} + onFileContextMenu={(filePath, event) => + onFileContextMenu( + { + environmentId: ctx.activeThreadEnvironmentId, + filePath, + workspaceRoot: ctx.workspaceRoot, + repositoryRoot: + thread?.worktreePath == null + ? activeProject?.repositoryIdentity?.rootPath + : undefined, + }, + event, + ) + } + /> + ); +} diff --git a/apps/web/src/components/chat/TimelineV2EventRow.logic.ts b/apps/web/src/components/chat/TimelineV2EventRow.logic.ts new file mode 100644 index 000000000000..fd12143ef30d --- /dev/null +++ b/apps/web/src/components/chat/TimelineV2EventRow.logic.ts @@ -0,0 +1,128 @@ +import { type OrchestrationV2TurnItem } from "@t3tools/contracts"; +import { providerErrorPresentation } from "../../session-logic"; +import { + CheckIcon, + CircleAlertIcon, + GitForkIcon, + type LucideIcon, + MinusIcon, + WrenchIcon, + XIcon, + ZapIcon, +} from "lucide-react"; +import * as DateTime from "effect/DateTime"; + +type V2EventTone = "muted" | "warning" | "danger" | "success"; + +export function v2EventPresentation(item: OrchestrationV2TurnItem): { + readonly label: string; + readonly detail: string | null; + readonly tone: V2EventTone; + readonly icon: LucideIcon; +} { + switch (item.type) { + case "error": { + const presentation = providerErrorPresentation(item); + return { + ...presentation, + tone: + item.status === "completed" + ? "success" + : item.status === "running" || item.failure.class === "usage_limit" + ? "warning" + : "danger", + icon: CircleAlertIcon, + }; + } + case "run_interrupt_request": + return { + label: "Interrupt requested", + detail: item.message, + tone: "warning", + icon: CircleAlertIcon, + }; + case "run_interrupt_result": + return { + label: "Run interrupted", + detail: item.message, + tone: "danger", + icon: XIcon, + }; + case "handoff": + return { + label: "Context handoff", + detail: + item.summary ?? + `${item.fromProviderInstanceIds.join(", ")} → ${item.toProviderInstanceId}`, + tone: item.status === "failed" ? "danger" : "muted", + icon: ZapIcon, + }; + case "fork": + return { + label: "Conversation fork", + detail: `Continues in ${item.targetThreadId}`, + tone: "muted", + icon: GitForkIcon, + }; + case "compaction": { + const tokenSummary = + item.beforeTokenCount === undefined && item.afterTokenCount === undefined + ? null + : `${item.beforeTokenCount ?? "?"} → ${item.afterTokenCount ?? "?"} tokens`; + return { + label: "Context compacted", + detail: item.summary ?? tokenSummary, + tone: item.status === "failed" ? "danger" : "muted", + icon: MinusIcon, + }; + } + case "todo_list": { + const steps = item.steps.map((step) => `${step.status}: ${step.text}`).join("\n"); + return { + label: "Plan updated", + detail: [item.explanation, steps].filter(Boolean).join("\n\n") || null, + tone: item.status === "failed" ? "danger" : "success", + icon: CheckIcon, + }; + } + default: + return { + label: item.title?.trim() || item.type.replaceAll("_", " "), + detail: null, + tone: item.status === "failed" ? "danger" : "muted", + icon: WrenchIcon, + }; + } +} + +export function subagentGroupTiming( + agents: ReadonlyArray<{ + status: OrchestrationV2TurnItem["status"]; + startedAt: DateTime.Utc | null; + completedAt: DateTime.Utc | null; + }>, +) { + let startMs: number | null = null; + let endMs: number | null = null; + let endUnknown = false; + for (const agent of agents) { + if (agent.startedAt) { + const ms = DateTime.toEpochMillis(agent.startedAt); + startMs = startMs === null ? ms : Math.min(startMs, ms); + } + if (agent.completedAt) { + const ms = DateTime.toEpochMillis(agent.completedAt); + endMs = endMs === null ? ms : Math.max(endMs, ms); + } else { + endUnknown = true; + } + } + const live = agents.some( + ({ status }) => status === "pending" || status === "running" || status === "waiting", + ); + return { + status: live ? ("running" as const) : ("completed" as const), + startedAt: startMs === null ? null : new Date(startMs).toISOString(), + completedAt: live || endUnknown || endMs === null ? null : new Date(endMs).toISOString(), + }; +} diff --git a/apps/web/src/components/chat/TimelineV2EventRow.tsx b/apps/web/src/components/chat/TimelineV2EventRow.tsx new file mode 100644 index 000000000000..a4d2adf902f3 --- /dev/null +++ b/apps/web/src/components/chat/TimelineV2EventRow.tsx @@ -0,0 +1,321 @@ +import { WorkLogBlock } from "./WorkLog"; +import { scopeThreadRef } from "@t3tools/client-runtime/environment"; +import { useAtomValue } from "@effect/atom-react"; +import { environmentThreadDetails } from "../../state/threads"; +import { + subagentGroupSummary, + summarizeSubagentStatuses, +} from "@t3tools/client-runtime/state/subagent-display"; +import { memo, use, useState } from "react"; +import ChatMarkdown from "../ChatMarkdown"; +import { ChevronDownIcon } from "lucide-react"; +import { cn } from "~/lib/utils"; +import { V2ItemInspector } from "./V2ItemInspector"; +import { Collapsible, CollapsibleTrigger, CollapsiblePanel } from "../ui/collapsible"; +import { + isV2LifecycleItem, + EnvironmentSubagentAvatar, + SubagentElapsed, + V2LifecycleRow, +} from "./V2LifecycleRow"; +import { SecretRequestCard } from "./SecretRequestCard"; +import { TimelineRowCtx, type TimelineRow } from "./timelineRowContext"; +import { v2EventPresentation, subagentGroupTiming } from "./TimelineV2EventRow.logic"; + +export function V2EventTimelineRow({ row }: { row: Extract }) { + const ctx = use(TimelineRowCtx); + const { item, visibility, sourceThreadId } = row.projectedItem; + if (item.type === "subagent" && (row.subagents?.length ?? 1) > 1) { + return ; + } + if (item.type === "secret_request") { + return ( + + ); + } + if (isV2LifecycleItem(item)) { + return ( + + ); + } + const presentation = v2EventPresentation(item); + const Icon = presentation.icon; + if (item.type === "error") { + return ( +
+ + + {presentation.label} + {item.status !== "completed" ? ( + + {item.status} + + ) : null} + {presentation.detail ? ( + + {presentation.detail} + + ) : null} + {visibility !== "local" ? ( + + {visibility === "inherited" ? "Inherited" : "Synthetic"} + + ) : null} + + +
+ {presentation.detail ? ( +
+ +
+ ) : null} + {visibility === "inherited" ? ( +

+ From {sourceThreadId} +

+ ) : null} +
+ +
+
+
+ ); + } + return ( +
+
+ +
+
+ {presentation.label} + {item.status !== "completed" ? ( + + {item.status} + + ) : null} + {visibility !== "local" ? ( + + {visibility === "inherited" ? "Inherited" : "Synthetic"} + + ) : null} +
+ {presentation.detail ? ( +
+ +
+ ) : null} + {visibility === "inherited" ? ( +

+ From {sourceThreadId} +

+ ) : null} +
+ +
+
+
+
+ ); +} + +const V2SubagentGroup = memo(function V2SubagentGroup({ + row, +}: { + row: Extract; +}) { + const ctx = use(TimelineRowCtx); + const groupId = `subagent-group:${row.id}`; + const [expanded, setExpanded] = useState(() => + ctx.workGroupViewState.expandedEntries.has(groupId), + ); + const members = (row.subagents ?? [row.projectedItem]).flatMap(({ item }) => + item.type === "subagent" ? [item] : [], + ); + const liveAgents = useAtomValue( + environmentThreadDetails.threadAtom( + scopeThreadRef(ctx.activeThreadEnvironmentId, row.projectedItem.item.threadId), + ), + (thread) => thread?.projection.subagents, + ); + const agents = members.map((item) => { + const live = liveAgents?.find((agent) => agent.id === item.subagentId); + return { + item, + status: live?.status ?? item.status, + startedAt: live?.startedAt ?? item.startedAt, + completedAt: live?.completedAt ?? item.completedAt, + }; + }); + const summary = subagentGroupSummary(agents); + const label = `${members.length} ${members.length === 1 ? "subagent" : "subagents"}`; + const statusSummary = summarizeSubagentStatuses(agents.map(({ status }) => status)); + const toggleExpanded = (open: boolean) => { + ctx.onToggleWorkEntry(row.id, expanded); + if (open) ctx.workGroupViewState.expandedEntries.add(groupId); + else ctx.workGroupViewState.expandedEntries.delete(groupId); + setExpanded(open); + }; + return ( + + + + + {agents.slice(0, 3).map(({ item, status }) => ( + + ))} + {agents.length > 3 ? ( + + +{agents.length - 3} + + ) : null} + + + {label} + + {statusSummary} + + + + + + + + {/* Virtualized rows must settle before disclosure scroll anchoring resumes. */} + + {expanded ? ( +
+ {members.map((item) => ( + + ))} +
+ ) : null} +
+
+
+ ); +}); From bef9142f8b9685dab9e1d13cc6c6750a7c78b2d3 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 21 Oct 2026 01:12:51 -0700 Subject: [PATCH 18/73] refactor(web): timeline row derivation, position restore and find reader live in hooks (#22682) Co-authored-by: Claude Opus 5.5 --- .../components/chat/MessagesTimeline.logic.ts | 28 +- .../src/components/chat/MessagesTimeline.tsx | 304 ++---------------- .../chat/useThreadFindPositionReader.ts | 100 ++++++ .../chat/useTimelinePositionRestore.ts | 166 ++++++++++ .../src/components/chat/useTimelineRows.ts | 78 +++++ 5 files changed, 391 insertions(+), 285 deletions(-) create mode 100644 apps/web/src/components/chat/useThreadFindPositionReader.ts create mode 100644 apps/web/src/components/chat/useTimelinePositionRestore.ts create mode 100644 apps/web/src/components/chat/useTimelineRows.ts diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.ts b/apps/web/src/components/chat/MessagesTimeline.logic.ts index 96c3a36a6e40..361bcdb62dbf 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts @@ -8,6 +8,7 @@ import { collectAssistantCitations, } from "@t3tools/shared/assistantCitations"; import { shallow } from "zustand/vanilla/shallow"; +import type { HandoffTimelineRun } from "@t3tools/client-runtime/handoff"; import { renderCodexDirectivesForCopy } from "@t3tools/shared/codexMarkdownDirectives"; import { commandDisplayText, @@ -2011,7 +2012,7 @@ function attachCreatedThreadSummaries( const WORKTREE_SETUP_ROW_ID = "worktree-setup-row"; -type MessagesTimelineRowsInput = Parameters[0]; +export type MessagesTimelineRowsInput = Parameters[0]; export interface MessagesTimelineRowsProjection { readonly input: MessagesTimelineRowsInput; @@ -2314,3 +2315,28 @@ export function shouldCollapseUserMessage(text: string): boolean { visible.split("\n").length > MAX_COLLAPSED_USER_MESSAGE_LINES ); } + +export function handoffRunsSignature(runs: ReadonlyArray): string { + return runs + .map( + (run) => + `${run.id}\0${run.providerInstanceId}\0${run.ordinal}\0${run.modelSelection.instanceId}\0${run.modelSelection.model}`, + ) + .join("\n"); +} + +// Expanded-id sets can be rebuilt during render; comparing them by content keeps +// the render-time state update in useIncrementalTimelineRows from repeating forever. +export function sameRowsInput( + left: MessagesTimelineRowsInput, + right: MessagesTimelineRowsInput, +): boolean { + for (const key of Object.keys(right) as Array) { + const previous = left[key]; + const next = right[key]; + if (Object.is(previous, next)) continue; + if (previous instanceof Set && next instanceof Set && shallow(previous, next)) continue; + return false; + } + return Object.keys(left).length === Object.keys(right).length; +} diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index ae09911bd9ae..8382c3224844 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1,5 +1,4 @@ import { ThreadFindTimelineContext } from "./ThreadFindProvider"; -import { shallow } from "zustand/vanilla/shallow"; import { MarkdownFindContext } from "./markdownFindContext"; import { useChatCanvas } from "./ChatCanvasContext"; import { WorkLogBlock } from "./WorkLog"; @@ -60,9 +59,6 @@ import type { AssistantCitationSourceAnchor } from "~/lib/assistantTextSelection import { type AssistantCitationRequest } from "./AssistantCitationSource"; import { useAssistantCitationTarget, type CitationHistoryPage } from "./useAssistantCitationTarget"; import { - computeStableMessagesTimelineRows, - deriveMessagesTimelineRowsWithState, - type MessagesTimelineRowsProjection, resolveTimelineIsAtEnd, resolveTimelineMinimapHasPersistentGutter, resolveTimelineMinimapCurrentIndex, @@ -71,13 +67,11 @@ import { resolveTimelineMinimapHitStripWidth, timelineTurnFoldRunIdsByEntryId, worktreeSetupAgentStarted, - type StableMessagesTimelineRowsState, type MessagesTimelineRow, type TimelineLatestRun, } from "./MessagesTimeline.logic"; import { type ThreadFindMatch } from "./threadFind"; import { useThreadFindNavigation } from "./useThreadFindNavigation"; -import { readThreadFindPosition } from "./threadFindPosition"; import type { ThreadFindPositionReader } from "./threadFind"; import { TooltipScrollDismissArea } from "../ui/tooltip"; import { WorktreeSetupCard } from "./WorktreeSetupCard"; @@ -116,6 +110,9 @@ import { McpAppTimelineRow, } from "./TimelineAssistantRows"; import { V2EventTimelineRow } from "./TimelineV2EventRow"; +import { useTimelinePositionRestore } from "./useTimelinePositionRestore"; +import { useThreadFindPositionReader } from "./useThreadFindPositionReader"; +import { useStableHandoffRuns, useIncrementalTimelineRows, useStableRows } from "./useTimelineRows"; function timelineListStateFor(identity: string): { readonly identity: string; @@ -163,7 +160,6 @@ const TIMELINE_MAINTAIN_SCROLL_AT_END = { layout: true, }, } as const satisfies MaintainScrollAtEndOptions; -const EMPTY_TIMELINE_RUNS: ReadonlyArray = []; const EMPTY_RUN_IDS: ReadonlySet = new Set(); const TIMELINE_MAINTAIN_SCROLL_AT_END_SMOOTH = { ...TIMELINE_MAINTAIN_SCROLL_AT_END, @@ -609,145 +605,23 @@ const ConversationTimeline = memo(function ConversationTimeline({ const runs = useStableHandoffRuns(runsProp); const minimapItems = deriveTimelineMinimapItems(rows); const minimapRowIndices = minimapItems.map((item) => item.rowIndex); - // Preserve initialScrollAtEnd's measured target until the list is reused - // for another thread; scrollToEnd would replace it with an estimated offset. - const listOpenedOnRef = useRef<{ - readonly list: LegendListRef; - identity: string | null; - } | null>(null); - useLayoutEffect(() => { - const list = listRef.current; - const opened = listOpenedOnRef.current; - if (!list) return; - if (opened?.list !== list) { - listOpenedOnRef.current = { list, identity: listIdentityKey }; - } else if (opened.identity !== listIdentityKey) { - opened.identity = null; - } - }); const restoreRowIndex = restoringThreadPosition && rememberedPosition?.atEnd === false ? rows.findIndex((row) => row.id === rememberedPosition.rowId) : -1; const restoringAlwaysRender = restoringThreadPosition && restoreRowIndex >= 0 ? { indices: [restoreRowIndex] } : undefined; - useLayoutEffect(() => { - if (!restoringThreadPosition || rows.length === 0) return; - const list = listRef.current; - if (!list) return; - if (citationRequest !== null) { - // oxlint-disable-next-line react/set-state-in-effect -- The citation scroll owns positioning once the list has mounted. - setPositionedThreadKey(listIdentityKey); - return; - } - let cancelled = false; - let settleFrame: number | null = null; - const viewport: HTMLElement | null = list.getScrollableNode(); - const cancelRestoration = () => { - if (cancelled) return; - cancelled = true; - if (settleFrame !== null) cancelAnimationFrame(settleFrame); - if (viewport) void list.scrollToOffset({ offset: viewport.scrollTop, animated: false }); - setPositionedThreadKey(listIdentityKey); - }; - const cancelForNavigation = () => { - cancelRestoration(); - onManualNavigation(); - }; - const onScrollKey = (event: globalThis.KeyboardEvent) => { - if ( - ["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(event.key) && - !( - event.target instanceof Element && - event.target.closest("input, textarea, [contenteditable=true]") - ) - ) - cancelForNavigation(); - }; - viewport?.addEventListener("wheel", cancelForNavigation, { passive: true }); - viewport?.addEventListener("touchmove", cancelForNavigation, { passive: true }); - viewport?.addEventListener("pointerdown", cancelForNavigation, { passive: true }); - viewport?.ownerDocument.addEventListener("keydown", onScrollKey); - const position = rememberedPosition; - const index = position ? rows.findIndex((row) => row.id === position.rowId) : -1; - if (position?.atEnd === false) onManualNavigation(); - if (cancelPositionRestoreRef) cancelPositionRestoreRef.current = cancelRestoration; - const scrolling = - position?.atEnd === false - ? index >= 0 - ? list.scrollToIndex({ - index, - animated: false, - viewPosition: 0, - viewOffset: -position.offsetWithinRow, - }) - : list.scrollToOffset({ offset: position.scrollOffset, animated: false }) - : listOpenedOnRef.current?.identity === listIdentityKey - ? undefined - : list.scrollToEnd({ animated: false }); - void Promise.resolve(scrolling).then(() => { - if (cancelled) return; - if (position?.atEnd !== false || index < 0) { - setPositionedThreadKey(listIdentityKey); - return; - } - // Index scrolling starts from estimates. Keep the saved row mounted - // until its measured position and the DOM agree for two layout frames. - let stableFrames = 0; - const reconcile = () => { - if (cancelled) return; - const state = list.getState(); - const rowIndex = state.indexByKey(position.rowId); - const row = rowIndex === undefined ? undefined : state.elementAtIndex(rowIndex); - const element = list.getScrollableNode(); - if (!row || !element) return; - const offset = Math.max( - 0, - Math.min( - element.scrollTop + - row.getBoundingClientRect().top - - element.getBoundingClientRect().top + - position.offsetWithinRow, - element.scrollHeight - element.clientHeight, - ), - ); - if (Math.abs(element.scrollTop - offset) > 1) { - stableFrames = 0; - void list.scrollToOffset({ offset, animated: false }).then(() => { - if (!cancelled) settleFrame = requestAnimationFrame(reconcile); - }); - return; - } - stableFrames += 1; - if (stableFrames >= 2) { - setPositionedThreadKey(listIdentityKey); - } else { - settleFrame = requestAnimationFrame(reconcile); - } - }; - settleFrame = requestAnimationFrame(reconcile); - }); - return () => { - cancelled = true; - if (cancelPositionRestoreRef?.current === cancelRestoration) { - cancelPositionRestoreRef.current = null; - } - if (settleFrame !== null) cancelAnimationFrame(settleFrame); - viewport?.removeEventListener("wheel", cancelForNavigation); - viewport?.removeEventListener("touchmove", cancelForNavigation); - viewport?.removeEventListener("pointerdown", cancelForNavigation); - viewport?.ownerDocument.removeEventListener("keydown", onScrollKey); - }; - }, [ - citationRequest, - cancelPositionRestoreRef, - listIdentityKey, + useTimelinePositionRestore({ listRef, - onManualNavigation, - rememberedPosition, + listIdentityKey, restoringThreadPosition, + rememberedPosition, rows, - ]); + citationRequest, + cancelPositionRestoreRef, + onManualNavigation, + onPositioned: setPositionedThreadKey, + }); const [timelineViewportElement, setTimelineViewportElement] = useState( null, @@ -1112,70 +986,18 @@ const ConversationTimeline = memo(function ConversationTimeline({ registerTimeline?.(list?.getScrollableNode() ?? null); }; - useLayoutEffect(() => { - if (!findPositionReaderRef || !timelineViewportElement) return; - const read: ThreadFindPositionReader = (query) => { - const foldRunIds = - findFoldRunIds ?? - timelineTurnFoldRunIdsByEntryId({ - timelineEntries, - latestRun, - runningRunId, - isWorking, - runlessWorkActive, - }); - const entryById = new Map(timelineEntries.map((entry) => [entry.id, entry])); - return readThreadFindPosition( - timelineViewportElement, - query, - (rowId) => { - const rowIndex = rows.findIndex((row) => row.id === rowId); - for (const row of rows.slice(Math.max(0, rowIndex))) { - if (row.kind !== "turn-fold" && row.kind !== "attempt-fold") { - const entry = entryById.get(row.kind === "assistant-meta" ? row.message.id : row.id); - if (entry && (entry.kind === "message" || entry.kind === "proposed-plan")) - return entry.id; - continue; - } - const entry = timelineEntries.find((entry) => { - const runId = - row.kind === "turn-fold" - ? foldRunIds.get(entry.id) - : entry.kind === "message" - ? entry.message.runId - : entry.kind === "proposed-plan" - ? entry.proposedPlan.runId - : null; - return ( - runId === row.runId && - (entry.kind !== "message" || entry.message.role === "assistant") && - (row.kind !== "attempt-fold" || entry.attempt?.id === row.attemptId) - ); - }); - if (entry && (entry.kind === "message" || entry.kind === "proposed-plan")) - return entry.id; - } - return undefined; - }, - contentInsetEndAdjustment, - ); - }; - findPositionReaderRef.current = read; - return () => { - if (findPositionReaderRef.current === read) findPositionReaderRef.current = null; - }; - }, [ - contentInsetEndAdjustment, - findFoldRunIds, + useThreadFindPositionReader({ findPositionReaderRef, - isWorking, + timelineViewportElement, + findFoldRunIds, + timelineEntries, latestRun, - rows, - runlessWorkActive, runningRunId, - timelineEntries, - timelineViewportElement, - ]); + isWorking, + runlessWorkActive, + rows, + contentInsetEndAdjustment, + }); useThreadFindNavigation({ listReady: findListReady, @@ -1556,89 +1378,3 @@ function AttemptFoldTimelineRow({ row }: { row: Extract ); } - -function handoffRunsSignature(runs: ReadonlyArray): string { - return runs - .map( - (run) => - `${run.id}\0${run.providerInstanceId}\0${run.ordinal}\0${run.modelSelection.instanceId}\0${run.modelSelection.model}`, - ) - .join("\n"); -} - -function useStableHandoffRuns( - runs: ReadonlyArray, -): ReadonlyArray { - const [state, setState] = useState<{ - readonly source: ReadonlyArray | null; - readonly signature: string; - readonly value: ReadonlyArray; - }>({ source: null, signature: "", value: EMPTY_TIMELINE_RUNS }); - if (state.source === runs) return state.value; - const signature = handoffRunsSignature(runs); - const value = - signature === state.signature - ? state.value - : runs.map((run) => ({ - id: run.id, - ordinal: run.ordinal, - providerInstanceId: run.providerInstanceId, - modelSelection: run.modelSelection, - })); - setState({ source: runs, signature, value }); - return value; -} - -type MessagesTimelineRowsInput = MessagesTimelineRowsProjection["input"]; - -// Expanded-id sets can be rebuilt during render; comparing them by content keeps -// the render-time state update below from repeating forever. -function sameRowsInput(left: MessagesTimelineRowsInput, right: MessagesTimelineRowsInput): boolean { - for (const key of Object.keys(right) as Array) { - const previous = left[key]; - const next = right[key]; - if (Object.is(previous, next)) continue; - if (previous instanceof Set && next instanceof Set && shallow(previous, next)) continue; - return false; - } - return Object.keys(left).length === Object.keys(right).length; -} - -/** - * Rows derived from the previous projection of the same list, so a streamed - * text update patches its message rows instead of deriving every row again. - */ -function useIncrementalTimelineRows( - identity: string, - input: MessagesTimelineRowsInput, -): MessagesTimelineRow[] { - const [state, setState] = useState(() => ({ - identity, - input, - projection: deriveMessagesTimelineRowsWithState(input), - })); - if (state.identity === identity && sameRowsInput(state.input, input)) { - return state.projection.rows; - } - const projection = deriveMessagesTimelineRowsWithState( - input, - state.identity === identity ? state.projection : null, - ); - setState({ identity, input, projection }); - return projection.rows; -} - -function useStableRows(rows: MessagesTimelineRow[], identity: string): MessagesTimelineRow[] { - const [state, setState] = useState<{ - readonly identity: string; - readonly source: MessagesTimelineRow[] | null; - readonly stable: StableMessagesTimelineRowsState; - }>({ identity, source: null, stable: { byId: new Map(), result: [] } }); - if (state.identity === identity && state.source === rows) return state.stable.result; - const stable = computeStableMessagesTimelineRows( - rows, - state.identity === identity ? state.stable : { byId: new Map(), result: [] }, - ); - setState({ identity, source: rows, stable }); - return stable.result; -} diff --git a/apps/web/src/components/chat/useThreadFindPositionReader.ts b/apps/web/src/components/chat/useThreadFindPositionReader.ts new file mode 100644 index 000000000000..5afc9ca25488 --- /dev/null +++ b/apps/web/src/components/chat/useThreadFindPositionReader.ts @@ -0,0 +1,100 @@ +import { useLayoutEffect } from "react"; +import type { RunId } from "@t3tools/contracts"; +import type { TimelineEntry } from "../../session-logic"; +import { + timelineTurnFoldRunIdsByEntryId, + type MessagesTimelineRow, + type TimelineLatestRun, +} from "./MessagesTimeline.logic"; +import type { ThreadFindPositionReader } from "./threadFind"; +import { readThreadFindPosition } from "./threadFindPosition"; + +/** Publishes how to read the find position of the timeline's current viewport. */ +export function useThreadFindPositionReader({ + findPositionReaderRef, + timelineViewportElement, + findFoldRunIds, + timelineEntries, + latestRun, + runningRunId, + isWorking, + runlessWorkActive, + rows, + contentInsetEndAdjustment, +}: { + readonly findPositionReaderRef: React.RefObject | undefined; + readonly timelineViewportElement: HTMLDivElement | null; + readonly findFoldRunIds: ReadonlyMap | null; + readonly timelineEntries: ReadonlyArray; + readonly latestRun: TimelineLatestRun | null; + readonly runningRunId: RunId | null; + readonly isWorking: boolean; + readonly runlessWorkActive: boolean; + readonly rows: MessagesTimelineRow[]; + readonly contentInsetEndAdjustment: number; +}) { + useLayoutEffect(() => { + if (!findPositionReaderRef || !timelineViewportElement) return; + const read: ThreadFindPositionReader = (query) => { + const foldRunIds = + findFoldRunIds ?? + timelineTurnFoldRunIdsByEntryId({ + timelineEntries, + latestRun, + runningRunId, + isWorking, + runlessWorkActive, + }); + const entryById = new Map(timelineEntries.map((entry) => [entry.id, entry])); + return readThreadFindPosition( + timelineViewportElement, + query, + (rowId) => { + const rowIndex = rows.findIndex((row) => row.id === rowId); + for (const row of rows.slice(Math.max(0, rowIndex))) { + if (row.kind !== "turn-fold" && row.kind !== "attempt-fold") { + const entry = entryById.get(row.kind === "assistant-meta" ? row.message.id : row.id); + if (entry && (entry.kind === "message" || entry.kind === "proposed-plan")) + return entry.id; + continue; + } + const entry = timelineEntries.find((entry) => { + const runId = + row.kind === "turn-fold" + ? foldRunIds.get(entry.id) + : entry.kind === "message" + ? entry.message.runId + : entry.kind === "proposed-plan" + ? entry.proposedPlan.runId + : null; + return ( + runId === row.runId && + (entry.kind !== "message" || entry.message.role === "assistant") && + (row.kind !== "attempt-fold" || entry.attempt?.id === row.attemptId) + ); + }); + if (entry && (entry.kind === "message" || entry.kind === "proposed-plan")) + return entry.id; + } + return undefined; + }, + contentInsetEndAdjustment, + ); + }; + findPositionReaderRef.current = read; + return () => { + if (findPositionReaderRef.current === read) findPositionReaderRef.current = null; + }; + }, [ + contentInsetEndAdjustment, + findFoldRunIds, + findPositionReaderRef, + isWorking, + latestRun, + rows, + runlessWorkActive, + runningRunId, + timelineEntries, + timelineViewportElement, + ]); +} diff --git a/apps/web/src/components/chat/useTimelinePositionRestore.ts b/apps/web/src/components/chat/useTimelinePositionRestore.ts new file mode 100644 index 000000000000..700a2a41af06 --- /dev/null +++ b/apps/web/src/components/chat/useTimelinePositionRestore.ts @@ -0,0 +1,166 @@ +import { useLayoutEffect, useRef } from "react"; +import type { LegendListRef } from "@legendapp/list/react"; +import type { AssistantCitationRequest } from "./AssistantCitationSource"; +import type { MessagesTimelineRow } from "./MessagesTimeline.logic"; +import type { RememberedTimelinePosition } from "./timelineScrollAnchoring"; + +/** + * Scrolls a newly shown thread back to where the user left it, or to its end, + * and reports the thread positioned once the saved row settles. + */ +export function useTimelinePositionRestore({ + listRef, + listIdentityKey, + restoringThreadPosition, + rememberedPosition, + rows, + citationRequest, + cancelPositionRestoreRef, + onManualNavigation, + onPositioned, +}: { + readonly listRef: React.RefObject; + readonly listIdentityKey: string; + readonly restoringThreadPosition: boolean; + readonly rememberedPosition: RememberedTimelinePosition | undefined; + readonly rows: MessagesTimelineRow[]; + readonly citationRequest: AssistantCitationRequest | null; + readonly cancelPositionRestoreRef: React.RefObject<(() => void) | null> | undefined; + readonly onManualNavigation: () => void; + readonly onPositioned: (identity: string) => void; +}) { + // Preserve initialScrollAtEnd's measured target until the list is reused + // for another thread; scrollToEnd would replace it with an estimated offset. + const listOpenedOnRef = useRef<{ + readonly list: LegendListRef; + identity: string | null; + } | null>(null); + useLayoutEffect(() => { + const list = listRef.current; + const opened = listOpenedOnRef.current; + if (!list) return; + if (opened?.list !== list) { + listOpenedOnRef.current = { list, identity: listIdentityKey }; + } else if (opened.identity !== listIdentityKey) { + opened.identity = null; + } + }); + useLayoutEffect(() => { + if (!restoringThreadPosition || rows.length === 0) return; + const list = listRef.current; + if (!list) return; + if (citationRequest !== null) { + // The citation scroll owns positioning once the list has mounted. + onPositioned(listIdentityKey); + return; + } + let cancelled = false; + let settleFrame: number | null = null; + const viewport: HTMLElement | null = list.getScrollableNode(); + const cancelRestoration = () => { + if (cancelled) return; + cancelled = true; + if (settleFrame !== null) cancelAnimationFrame(settleFrame); + if (viewport) void list.scrollToOffset({ offset: viewport.scrollTop, animated: false }); + onPositioned(listIdentityKey); + }; + const cancelForNavigation = () => { + cancelRestoration(); + onManualNavigation(); + }; + const onScrollKey = (event: globalThis.KeyboardEvent) => { + if ( + ["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(event.key) && + !( + event.target instanceof Element && + event.target.closest("input, textarea, [contenteditable=true]") + ) + ) + cancelForNavigation(); + }; + viewport?.addEventListener("wheel", cancelForNavigation, { passive: true }); + viewport?.addEventListener("touchmove", cancelForNavigation, { passive: true }); + viewport?.addEventListener("pointerdown", cancelForNavigation, { passive: true }); + viewport?.ownerDocument.addEventListener("keydown", onScrollKey); + const position = rememberedPosition; + const index = position ? rows.findIndex((row) => row.id === position.rowId) : -1; + if (position?.atEnd === false) onManualNavigation(); + if (cancelPositionRestoreRef) cancelPositionRestoreRef.current = cancelRestoration; + const scrolling = + position?.atEnd === false + ? index >= 0 + ? list.scrollToIndex({ + index, + animated: false, + viewPosition: 0, + viewOffset: -position.offsetWithinRow, + }) + : list.scrollToOffset({ offset: position.scrollOffset, animated: false }) + : listOpenedOnRef.current?.identity === listIdentityKey + ? undefined + : list.scrollToEnd({ animated: false }); + void Promise.resolve(scrolling).then(() => { + if (cancelled) return; + if (position?.atEnd !== false || index < 0) { + onPositioned(listIdentityKey); + return; + } + // Index scrolling starts from estimates. Keep the saved row mounted + // until its measured position and the DOM agree for two layout frames. + let stableFrames = 0; + const reconcile = () => { + if (cancelled) return; + const state = list.getState(); + const rowIndex = state.indexByKey(position.rowId); + const row = rowIndex === undefined ? undefined : state.elementAtIndex(rowIndex); + const element = list.getScrollableNode(); + if (!row || !element) return; + const offset = Math.max( + 0, + Math.min( + element.scrollTop + + row.getBoundingClientRect().top - + element.getBoundingClientRect().top + + position.offsetWithinRow, + element.scrollHeight - element.clientHeight, + ), + ); + if (Math.abs(element.scrollTop - offset) > 1) { + stableFrames = 0; + void list.scrollToOffset({ offset, animated: false }).then(() => { + if (!cancelled) settleFrame = requestAnimationFrame(reconcile); + }); + return; + } + stableFrames += 1; + if (stableFrames >= 2) { + onPositioned(listIdentityKey); + } else { + settleFrame = requestAnimationFrame(reconcile); + } + }; + settleFrame = requestAnimationFrame(reconcile); + }); + return () => { + cancelled = true; + if (cancelPositionRestoreRef?.current === cancelRestoration) { + cancelPositionRestoreRef.current = null; + } + if (settleFrame !== null) cancelAnimationFrame(settleFrame); + viewport?.removeEventListener("wheel", cancelForNavigation); + viewport?.removeEventListener("touchmove", cancelForNavigation); + viewport?.removeEventListener("pointerdown", cancelForNavigation); + viewport?.ownerDocument.removeEventListener("keydown", onScrollKey); + }; + }, [ + citationRequest, + cancelPositionRestoreRef, + listIdentityKey, + listRef, + onManualNavigation, + onPositioned, + rememberedPosition, + restoringThreadPosition, + rows, + ]); +} diff --git a/apps/web/src/components/chat/useTimelineRows.ts b/apps/web/src/components/chat/useTimelineRows.ts new file mode 100644 index 000000000000..847dcc594af7 --- /dev/null +++ b/apps/web/src/components/chat/useTimelineRows.ts @@ -0,0 +1,78 @@ +import { useState } from "react"; +import { + computeStableMessagesTimelineRows, + handoffRunsSignature, + type MessagesTimelineRowsInput, + sameRowsInput, + deriveMessagesTimelineRowsWithState, + type StableMessagesTimelineRowsState, + type MessagesTimelineRow, +} from "./MessagesTimeline.logic"; +import { type HandoffTimelineRun } from "./V2LifecycleRow"; + +const EMPTY_TIMELINE_RUNS: ReadonlyArray = []; + +export function useStableHandoffRuns( + runs: ReadonlyArray, +): ReadonlyArray { + const [state, setState] = useState<{ + readonly source: ReadonlyArray | null; + readonly signature: string; + readonly value: ReadonlyArray; + }>({ source: null, signature: "", value: EMPTY_TIMELINE_RUNS }); + if (state.source === runs) return state.value; + const signature = handoffRunsSignature(runs); + const value = + signature === state.signature + ? state.value + : runs.map((run) => ({ + id: run.id, + ordinal: run.ordinal, + providerInstanceId: run.providerInstanceId, + modelSelection: run.modelSelection, + })); + setState({ source: runs, signature, value }); + return value; +} + +/** + * Rows derived from the previous projection of the same list, so a streamed + * text update patches its message rows instead of deriving every row again. + */ +export function useIncrementalTimelineRows( + identity: string, + input: MessagesTimelineRowsInput, +): MessagesTimelineRow[] { + const [state, setState] = useState(() => ({ + identity, + input, + projection: deriveMessagesTimelineRowsWithState(input), + })); + if (state.identity === identity && sameRowsInput(state.input, input)) { + return state.projection.rows; + } + const projection = deriveMessagesTimelineRowsWithState( + input, + state.identity === identity ? state.projection : null, + ); + setState({ identity, input, projection }); + return projection.rows; +} + +export function useStableRows( + rows: MessagesTimelineRow[], + identity: string, +): MessagesTimelineRow[] { + const [state, setState] = useState<{ + readonly identity: string; + readonly source: MessagesTimelineRow[] | null; + readonly stable: StableMessagesTimelineRowsState; + }>({ identity, source: null, stable: { byId: new Map(), result: [] } }); + if (state.identity === identity && state.source === rows) return state.stable.result; + const stable = computeStableMessagesTimelineRows( + rows, + state.identity === identity ? state.stable : { byId: new Map(), result: [] }, + ); + setState({ identity, source: rows, stable }); + return stable.result; +} From 9ff8cdca8565d8ae0cbfc22130ea65c040d97545 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 21 Oct 2026 01:13:18 -0700 Subject: [PATCH 19/73] refactor(web): the command palette's theme items live in their own file (#22679) Co-authored-by: Claude Opus 5.5 --- apps/web/src/components/CommandPalette.tsx | 153 +--------------- .../components/CommandPaletteThemeItems.tsx | 163 ++++++++++++++++++ 2 files changed, 170 insertions(+), 146 deletions(-) create mode 100644 apps/web/src/components/CommandPaletteThemeItems.tsx diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 055c2d76c4fd..c44bb2c77858 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -67,13 +67,10 @@ import { MessageSquareDashedIcon, LinkIcon, MessageSquareIcon, - MonitorIcon, - MoonIcon, PaletteIcon, RotateCcwIcon, SettingsIcon, SquarePenIcon, - SunIcon, TextSearchIcon, } from "lucide-react"; import { requestThreadFindOpen } from "./chat/threadFindActionBus"; @@ -104,15 +101,6 @@ import { import { useClientSettings } from "../hooks/useSettings"; import { useStableCallback } from "../hooks/useStableCallback"; import { useTheme } from "../hooks/useTheme"; -import { useCustomThemes } from "../hooks/useCustomThemes"; -import { useEnvironmentThemeDefinitions } from "../hooks/useEnvironmentTheme"; -import { BUILT_IN_THEMES } from "@t3tools/shared/themePalettes"; -import { getThemeDefinition } from "../themePalette"; -import { - STANDARD_THEME_CARDS, - getThemeCardDefinition, - ThemePreviewCircle, -} from "./settings/ThemePreviewCircles"; import { readLocalApi } from "../localApi"; import { desktopLocalBackendId } from "../connection/desktopLocal"; import { filesystemEnvironment, useFilesystemReadAccess } from "../state/filesystem"; @@ -203,6 +191,11 @@ import { import { orderItemsByPreferredIds, sortLogicalProjectsForSidebar } from "./Sidebar.logic"; import { resolveEnvironmentOptionLabel } from "./BranchToolbar.logic"; import { CommandPaletteContent } from "./CommandPaletteContent"; +import { + APPEARANCE_OPTIONS, + notifyThemeSaveFailure, + useCommandPaletteThemeItems, +} from "./CommandPaletteThemeItems"; import { CommandPaletteTitleIcon, CommandPaletteVirtualizedResults, @@ -357,41 +350,9 @@ function overlayModeForCommand(command: string | null): SearchOverlayMode | null : null; } -const APPEARANCE_OPTIONS = [ - { mode: "system", label: "System", icon: MonitorIcon }, - { mode: "light", label: "Light", icon: SunIcon }, - { mode: "dark", label: "Dark", icon: MoonIcon }, -] as const; - -function notifyThemeSaveFailure(): void { - toastManager.add( - stackedThreadToast({ - type: "error", - title: "Couldn't save theme selection", - description: "Try again.", - }), - ); -} - // Each helper reads its own source hook so the compiler caches the derived value on that // source. Taking the source as an argument instead leaves the helper hook-free, and the // compiler then treats it as a plain function and caches nothing. -function useThemeCards() { - const customThemes = useCustomThemes(); - const environmentThemes = useEnvironmentThemeDefinitions(); - const seen = new Set(); - return [ - ...STANDARD_THEME_CARDS.map((card) => ({ ...card, id: null })), - ...[...BUILT_IN_THEMES, ...customThemes, ...environmentThemes] - .filter((definition) => { - if (seen.has(definition.id)) return false; - seen.add(definition.id); - return true; - }) - .map(getThemeCardDefinition), - ]; -} - function useThreadSearchWithContentMatches( environmentIds: ReadonlyArray, query: string, @@ -817,16 +778,7 @@ function OpenCommandPaletteDialog(props: { }; const threads = useThreadShells(); const keybindings = useAtomValue(primaryServerKeybindingsAtom); - const { - theme, - themeHalves, - resolvedTheme, - appearanceMode, - setAppearanceMode, - setTheme, - setThemeHalf, - } = useTheme(); - const themeCards = useThemeCards(); + const { changeThemeItem, changeAppearanceItem, themeEditorItem } = useCommandPaletteThemeItems(); const providers = useAtomValue(primaryServerProvidersAtom); const providersByEnvironment = useEnvironmentProviders(); const providerEntryByEnvironmentAndInstanceId = new Map( @@ -1863,84 +1815,7 @@ function OpenCommandPaletteDialog(props: { }); } - const changeThemeItem: CommandPaletteSubmenuItem = { - kind: "submenu", - value: "action:change-theme", - searchTerms: ["change theme", "appearance", "colors", "palette"], - title: "Change theme", - icon: , - addonIcon: , - shortcutCommand: "theme.select", - groups: [ - { - value: "themes", - label: "Change theme", - items: themeCards.map(({ id, label, previews }) => ({ - kind: "action", - value: id === null ? "theme:standard" : `theme:palette:${id}`, - title: label, - description: previews.length === 1 ? `For ${previews[0]!.mode} mode` : undefined, - searchTerms: [label, "theme", "appearance"], - icon: , - titleTrailingContent: ( - - {(themeHalves?.[resolvedTheme] ?? getThemeDefinition(theme)?.id ?? null) === id ? ( - Current - ) : null} - - {previews.map((preview) => ( - - ))} - - - ), - run: async () => { - const saved = - previews.length === 1 && id !== null - ? setThemeHalf(previews[0]!.mode, id) - : setTheme(id ?? appearanceMode); - if (!saved) notifyThemeSaveFailure(); - }, - })), - }, - ], - }; actionItems.push(changeThemeItem); - - const changeAppearanceItem: CommandPaletteSubmenuItem = { - kind: "submenu", - value: "action:change-appearance", - searchTerms: ["change appearance", "light", "dark", "system", "mode", "toggle"], - title: "Change appearance", - icon: , - addonIcon: , - shortcutCommand: "appearance.cycle", - groups: [ - { - value: "appearance", - label: "Change appearance", - items: APPEARANCE_OPTIONS.map(({ mode, label, icon: Icon }) => ({ - kind: "action", - value: `appearance:${mode}`, - title: label, - searchTerms: [label, "appearance", "mode"], - icon: , - titleTrailingContent: - appearanceMode === mode ? ( - Current - ) : undefined, - run: async () => { - if (!setAppearanceMode(mode)) notifyThemeSaveFailure(); - }, - })), - }, - ], - }; actionItems.push(changeAppearanceItem); // The parent pushes an intent into this mounted dialog; applying it clears the @@ -1984,21 +1859,7 @@ function OpenCommandPaletteDialog(props: { applyOpenIntent(openIntent); }, [openIntent, projectThreadItemCount]); - actionItems.push({ - kind: "action", - value: "action:theme-editor", - searchTerms: ["theme", "appearance", "colors", "palette", "customize"], - title: "Toggle theme editor", - icon: , - shortcutCommand: "themeEditor.toggle", - run: async () => { - toggleThemeEditorForTheme({ - theme, - themeHalves, - initialAppearance: resolvedTheme, - }); - }, - }); + actionItems.push(themeEditorItem); if ( environments.some( diff --git a/apps/web/src/components/CommandPaletteThemeItems.tsx b/apps/web/src/components/CommandPaletteThemeItems.tsx new file mode 100644 index 000000000000..818229d95066 --- /dev/null +++ b/apps/web/src/components/CommandPaletteThemeItems.tsx @@ -0,0 +1,163 @@ +import { BUILT_IN_THEMES } from "@t3tools/shared/themePalettes"; +import { MonitorIcon, MoonIcon, PaletteIcon, SunIcon } from "lucide-react"; + +import { useCustomThemes } from "../hooks/useCustomThemes"; +import { useEnvironmentThemeDefinitions } from "../hooks/useEnvironmentTheme"; +import { useTheme } from "../hooks/useTheme"; +import { getThemeDefinition } from "../themePalette"; +import { + ADDON_ICON_CLASS, + ITEM_ICON_CLASS, + type CommandPaletteActionItem, + type CommandPaletteSubmenuItem, +} from "./CommandPalette.logic"; +import { toggleThemeEditorForTheme } from "./settings/themeEditorStore"; +import { + STANDARD_THEME_CARDS, + getThemeCardDefinition, + ThemePreviewCircle, +} from "./settings/ThemePreviewCircles"; +import { stackedThreadToast, toastManager } from "./ui/toast"; + +export const APPEARANCE_OPTIONS = [ + { mode: "system", label: "System", icon: MonitorIcon }, + { mode: "light", label: "Light", icon: SunIcon }, + { mode: "dark", label: "Dark", icon: MoonIcon }, +] as const; + +export function notifyThemeSaveFailure(): void { + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Couldn't save theme selection", + description: "Try again.", + }), + ); +} + +// Reads its own source hooks so the compiler caches the cards on them. Taking the +// sources as arguments would leave it hook-free, and the compiler would cache nothing. +function useThemeCards() { + const customThemes = useCustomThemes(); + const environmentThemes = useEnvironmentThemeDefinitions(); + const seen = new Set(); + return [ + ...STANDARD_THEME_CARDS.map((card) => ({ ...card, id: null })), + ...[...BUILT_IN_THEMES, ...customThemes, ...environmentThemes] + .filter((definition) => { + if (seen.has(definition.id)) return false; + seen.add(definition.id); + return true; + }) + .map(getThemeCardDefinition), + ]; +} + +export function useCommandPaletteThemeItems() { + const { + theme, + themeHalves, + resolvedTheme, + appearanceMode, + setAppearanceMode, + setTheme, + setThemeHalf, + } = useTheme(); + const themeCards = useThemeCards(); + + const changeThemeItem: CommandPaletteSubmenuItem = { + kind: "submenu", + value: "action:change-theme", + searchTerms: ["change theme", "appearance", "colors", "palette"], + title: "Change theme", + icon: , + addonIcon: , + shortcutCommand: "theme.select", + groups: [ + { + value: "themes", + label: "Change theme", + items: themeCards.map(({ id, label, previews }) => ({ + kind: "action", + value: id === null ? "theme:standard" : `theme:palette:${id}`, + title: label, + description: previews.length === 1 ? `For ${previews[0]!.mode} mode` : undefined, + searchTerms: [label, "theme", "appearance"], + icon: , + titleTrailingContent: ( + + {(themeHalves?.[resolvedTheme] ?? getThemeDefinition(theme)?.id ?? null) === id ? ( + Current + ) : null} + + {previews.map((preview) => ( + + ))} + + + ), + run: async () => { + const saved = + previews.length === 1 && id !== null + ? setThemeHalf(previews[0]!.mode, id) + : setTheme(id ?? appearanceMode); + if (!saved) notifyThemeSaveFailure(); + }, + })), + }, + ], + }; + + const changeAppearanceItem: CommandPaletteSubmenuItem = { + kind: "submenu", + value: "action:change-appearance", + searchTerms: ["change appearance", "light", "dark", "system", "mode", "toggle"], + title: "Change appearance", + icon: , + addonIcon: , + shortcutCommand: "appearance.cycle", + groups: [ + { + value: "appearance", + label: "Change appearance", + items: APPEARANCE_OPTIONS.map(({ mode, label, icon: Icon }) => ({ + kind: "action", + value: `appearance:${mode}`, + title: label, + searchTerms: [label, "appearance", "mode"], + icon: , + titleTrailingContent: + appearanceMode === mode ? ( + Current + ) : undefined, + run: async () => { + if (!setAppearanceMode(mode)) notifyThemeSaveFailure(); + }, + })), + }, + ], + }; + + const themeEditorItem: CommandPaletteActionItem = { + kind: "action", + value: "action:theme-editor", + searchTerms: ["theme", "appearance", "colors", "palette", "customize"], + title: "Toggle theme editor", + icon: , + shortcutCommand: "themeEditor.toggle", + run: async () => { + toggleThemeEditorForTheme({ + theme, + themeHalves, + initialAppearance: resolvedTheme, + }); + }, + }; + + return { changeThemeItem, changeAppearanceItem, themeEditorItem }; +} From 767134a4a7a64d8d5dd29e84423157f94e28064e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 21 Oct 2026 01:13:19 -0700 Subject: [PATCH 20/73] refactor(web): the command palette's thread items come from their own hook (#22687) Co-authored-by: Claude Opus 5.5 --- apps/web/src/components/CommandPalette.tsx | 222 ++---------------- .../src/components/CommandPaletteLookups.ts | 93 ++++++++ .../components/CommandPaletteThreadItems.tsx | 150 ++++++++++++ 3 files changed, 257 insertions(+), 208 deletions(-) create mode 100644 apps/web/src/components/CommandPaletteLookups.ts create mode 100644 apps/web/src/components/CommandPaletteThreadItems.tsx diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index c44bb2c77858..4f434fce7a08 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -24,7 +24,6 @@ import { } from "@t3tools/client-runtime/operations/projects"; import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; import { connectionStatusText } from "@t3tools/client-runtime/connection"; -import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search"; import { resolveThreadReferenceCopyTarget } from "@t3tools/shared/threadReference"; import { canPreloadBrowsePath, @@ -42,11 +41,9 @@ import { AuthSourceControlWriteScope, AuthFilesystemReadScope, type DesktopWslState, - type ClientSettings, type EnvironmentId, type EnvironmentMachineKind, type FilesystemBrowseResult, - type ProjectId, type SourceControlDiscoveryResult, SourceControlProviderKind, type SourceControlRepositoryInfo, @@ -121,7 +118,6 @@ import { usePrimaryEnvironmentId, } from "../state/environments"; import { useProjects, useServerConfigs, useThreadShells, waitForProject } from "../state/entities"; -import { useThreadSearch } from "../state/queries"; import { resolveThreadActionProjectRef, startNewThreadFromContext } from "../lib/chatThreadActions"; import { appendBrowsePathSegment, @@ -166,7 +162,6 @@ import { buildCommandPaletteProjectMetadata, buildProjectActionItems, buildRootGroups, - buildThreadActionItems, buildLinkedThreadActionItems, buildCommandPaletteRows, enumerateCommandPaletteItems, @@ -188,9 +183,15 @@ import { reduceCommandPaletteSearchState, type SearchOverlayMode, } from "./CommandPalette.logic"; -import { orderItemsByPreferredIds, sortLogicalProjectsForSidebar } from "./Sidebar.logic"; import { resolveEnvironmentOptionLabel } from "./BranchToolbar.logic"; import { CommandPaletteContent } from "./CommandPaletteContent"; +import { + selectCommandPaletteClientSettings, + useEnvironmentLookups, + useProjectLookups, + useSortedProjectGroups, +} from "./CommandPaletteLookups"; +import { useCommandPaletteThreadItems } from "./CommandPaletteThreadItems"; import { APPEARANCE_OPTIONS, notifyThemeSaveFailure, @@ -210,22 +211,8 @@ import { openLinkPullRequestDialog } from "./pullRequest/LinkPullRequestDialog"; import { ProjectContentSearchDialog } from "./search/ProjectContentSearchDialog"; import { toggleThemeEditorForTheme } from "./settings/themeEditorStore"; import { searchSettings, SETTINGS_SECTION_LABELS } from "./settings/settingsSearch"; -import { - COMMAND_PALETTE_META_ICON_CLASS, - CommandPaletteMetaDot, - ThreadCommandSubtitle, -} from "./ThreadCommandSubtitle"; -import { - ThreadRowLeadingStatus, - ThreadRowTrailingStatus, - ThreadSearchPullRequestNumber, -} from "./ThreadStatusIndicators"; -import { primaryServerKeybindingsAtom, primaryServerProvidersAtom } from "../state/server"; -import { - applyProviderInstanceSettings, - deriveProviderInstanceEntries, - type ProviderInstanceEntry, -} from "../providerInstances"; +import { COMMAND_PALETTE_META_ICON_CLASS, CommandPaletteMetaDot } from "./ThreadCommandSubtitle"; +import { primaryServerKeybindingsAtom } from "../state/server"; import { resolveShortcutCommand, threadJumpIndexFromCommand } from "../keybindings"; import { CommandDialog, CommandDialogPopup, CommandFooterAction } from "./ui/command"; import { Button } from "./ui/button"; @@ -235,12 +222,7 @@ import { stackedThreadToast, toastManager } from "./ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { ComposerHandleContext, useComposerHandleContext } from "../composerHandleContext"; import type { ChatComposerHandle } from "./chat/ChatComposer"; -import { getProjectOrderKey } from "../logicalProject"; -import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore"; -import { - buildSidebarProjectPickerEntries, - buildSidebarProjectSnapshots, -} from "../sidebarProjectGrouping"; +import { buildSidebarProjectPickerEntries } from "../sidebarProjectGrouping"; import type { Project } from "../types"; import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; import { readPullRequestListPreferences } from "~/components/pullRequest/pullRequestListPreferences"; @@ -350,99 +332,6 @@ function overlayModeForCommand(command: string | null): SearchOverlayMode | null : null; } -// Each helper reads its own source hook so the compiler caches the derived value on that -// source. Taking the source as an argument instead leaves the helper hook-free, and the -// compiler then treats it as a plain function and caches nothing. -function useThreadSearchWithContentMatches( - environmentIds: ReadonlyArray, - query: string, -) { - const threadSearch = useThreadSearch(environmentIds, query); - const contentMatchByKey = new Map( - threadSearch.matches.flatMap((match) => - match.source === "user" || match.source === "assistant" - ? [[threadSearchMatchKey(match), match] as const] - : [], - ), - ); - return { threadSearch, contentMatchByKey }; -} - -function useEnvironmentLookups() { - const { environments } = useEnvironments(); - const environmentLabelById = new Map( - environments.map((environment) => [environment.environmentId, environment.label] as const), - ); - const projectEnvironmentLocationById = new Map( - environments.map((environment) => { - const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget"; - const isLocal = isPrimary || isDesktopLocalConnectionTarget(environment.entry.target); - return [ - environment.environmentId, - { - kind: isLocal ? "local" : "remote", - label: isPrimary ? "Local" : isLocal ? `${environment.label} (Local)` : environment.label, - machine: resolveEnvironmentMachineKind(environment.serverConfig), - }, - ] as const; - }), - ); - return { environmentLabelById, projectEnvironmentLocationById }; -} - -function useProjectLookups() { - const projects = useProjects(); - const projectCwdById = new Map( - projects.map((project) => [project.id, project.workspaceRoot]), - ); - const projectByKey = new Map( - projects.map((project) => [`${project.environmentId}:${project.id}`, project]), - ); - const projectTitleById = new Map( - projects.map((project) => [project.id, project.title]), - ); - return { projectCwdById, projectByKey, projectTitleById }; -} - -const selectCommandPaletteClientSettings = (settings: ClientSettings) => ({ - sidebarProjectGroupingMode: settings.sidebarProjectGroupingMode, - sidebarProjectGroupingOverrides: settings.sidebarProjectGroupingOverrides, - sidebarProjectSortOrder: settings.sidebarProjectSortOrder, - sidebarThreadSortOrder: settings.sidebarThreadSortOrder, -}); - -function useSortedProjectGroups(environmentLabelById: ReadonlyMap) { - const projects = useProjects(); - const threads = useThreadShells(); - const projectOrder = useUiStateStore((store) => store.projectOrder); - const clientSettings = useClientSettings(selectCommandPaletteClientSettings); - const primaryEnvironmentId = usePrimaryEnvironmentId(); - const projectGroupingSettings = { - sidebarProjectGroupingMode: clientSettings.sidebarProjectGroupingMode, - sidebarProjectGroupingOverrides: clientSettings.sidebarProjectGroupingOverrides, - }; - const orderedProjects = orderItemsByPreferredIds({ - items: projects, - preferredIds: projectOrder, - getId: getProjectOrderKey, - getPreferenceIds: (project) => [ - getProjectOrderKey(project), - legacyProjectCwdPreferenceKey(project.workspaceRoot), - ], - }); - const unsortedProjectGroups = buildSidebarProjectSnapshots({ - projects: clientSettings.sidebarProjectSortOrder === "manual" ? orderedProjects : projects, - settings: projectGroupingSettings, - primaryEnvironmentId, - resolveEnvironmentLabel: (environmentId) => environmentLabelById.get(environmentId) ?? null, - }); - return sortLogicalProjectsForSidebar( - unsortedProjectGroups, - threads, - clientSettings.sidebarProjectSortOrder, - ); -} - export function CommandPalette({ children }: { children: ReactNode }) { const navigate = useNavigate(); const [state, dispatch] = useReducer(reduceCommandPaletteUiState, { @@ -778,31 +667,10 @@ function OpenCommandPaletteDialog(props: { }; const threads = useThreadShells(); const keybindings = useAtomValue(primaryServerKeybindingsAtom); - const { changeThemeItem, changeAppearanceItem, themeEditorItem } = useCommandPaletteThemeItems(); - const providers = useAtomValue(primaryServerProvidersAtom); const providersByEnvironment = useEnvironmentProviders(); - const providerEntryByEnvironmentAndInstanceId = new Map( - environments.flatMap((environment) => { - const serverConfig = environment.serverConfig; - const derived = deriveProviderInstanceEntries( - providersByEnvironment.get(environment.environmentId) ?? - (environment.environmentId === primaryEnvironmentId ? providers : []), - ); - const entries = serverConfig - ? applyProviderInstanceSettings(derived, serverConfig.settings, null) - : derived; - return entries.map( - (entry) => [`${environment.environmentId}:${entry.instanceId}`, entry] as const, - ); - }), - ); + const { changeThemeItem, changeAppearanceItem, themeEditorItem } = useCommandPaletteThemeItems(); const currentView = viewStack.at(-1)?.view ?? null; - const environmentIds = environments - .filter((environment) => environment.connection.phase === "connected") - .map((environment) => environment.environmentId); const threadSearchQuery = currentView === null && !isActionsOnly ? deferredQuery : ""; - const { threadSearch, contentMatchByKey: threadContentMatchByKey } = - useThreadSearchWithContentMatches(environmentIds, threadSearchQuery); const [browseGeneration, setBrowseGeneration] = useState(0); const [browseNavigation] = useState(createBrowseNavigationCoordinator); const [addProjectEnvironmentId, setAddProjectEnvironmentId] = useState( @@ -1183,70 +1051,8 @@ function OpenCommandPaletteDialog(props: { ]; })(); - const allThreadItems = buildThreadActionItems({ - threads, - ...(activeThreadId ? { activeThreadId } : {}), - projectTitleById, - sortOrder: clientSettings.sidebarThreadSortOrder, - icon: , - renderLeadingContent: (thread) => , - renderTrailingContent: (thread) => ( - <> - - - - ), - renderDescription: (thread, { projectTitle }) => { - const modelInstanceId = - thread.runtime?.providerInstanceId ?? thread.modelSelection.instanceId; - const providerEntry = - providerEntryByEnvironmentAndInstanceId.get(`${thread.environmentId}:${modelInstanceId}`) ?? - null; - return ( - - ); - }, - getContentMatch: (thread) => { - const match = threadContentMatchByKey.get( - threadSearchMatchKey({ - environmentId: thread.environmentId, - threadId: thread.id, - }), - ); - return match && (match.source === "user" || match.source === "assistant") - ? { - source: match.source, - snippet: match.snippet, - query: threadSearch.query, - } - : undefined; - }, - runThread: async (thread) => { - await navigate({ - to: "/$environmentId/$threadId", - params: buildThreadRouteParams(scopeThreadRef(thread.environmentId, thread.id)), - }); - }, - }); + const { threadItems: allThreadItems, isSearchPending: isThreadSearchPending } = + useCommandPaletteThreadItems({ activeThreadId, threadSearchQuery }); const recentThreadItems = allThreadItems.slice(0, RECENT_THREAD_LIMIT); // Stable so the item builders that capture it during render never see the @@ -3208,7 +3014,7 @@ function OpenCommandPaletteDialog(props: { ? { emptyStateMessage: "Press Enter to create this folder and add it as a project.", } - : threadSearch.isPending + : isThreadSearchPending ? { emptyStateMessage: "Searching thread messages…" } : {})} /> diff --git a/apps/web/src/components/CommandPaletteLookups.ts b/apps/web/src/components/CommandPaletteLookups.ts new file mode 100644 index 000000000000..2b6d6497c0cb --- /dev/null +++ b/apps/web/src/components/CommandPaletteLookups.ts @@ -0,0 +1,93 @@ +import { + type ClientSettings, + type EnvironmentId, + type ProjectId, + resolveEnvironmentMachineKind, +} from "@t3tools/contracts"; + +import { isDesktopLocalConnectionTarget } from "../connection/desktopLocal"; +import { useClientSettings } from "../hooks/useSettings"; +import { getProjectOrderKey } from "../logicalProject"; +import { useProjects, useThreadShells } from "../state/entities"; +import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments"; +import { buildSidebarProjectSnapshots } from "../sidebarProjectGrouping"; +import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore"; +import { orderItemsByPreferredIds, sortLogicalProjectsForSidebar } from "./Sidebar.logic"; + +// Each helper reads its own source hook so the compiler caches the derived value on that +// source. Taking the source as an argument instead leaves the helper hook-free, and the +// compiler then treats it as a plain function and caches nothing. +export function useEnvironmentLookups() { + const { environments } = useEnvironments(); + const environmentLabelById = new Map( + environments.map((environment) => [environment.environmentId, environment.label] as const), + ); + const projectEnvironmentLocationById = new Map( + environments.map((environment) => { + const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget"; + const isLocal = isPrimary || isDesktopLocalConnectionTarget(environment.entry.target); + return [ + environment.environmentId, + { + kind: isLocal ? "local" : "remote", + label: isPrimary ? "Local" : isLocal ? `${environment.label} (Local)` : environment.label, + machine: resolveEnvironmentMachineKind(environment.serverConfig), + }, + ] as const; + }), + ); + return { environmentLabelById, projectEnvironmentLocationById }; +} + +export function useProjectLookups() { + const projects = useProjects(); + const projectCwdById = new Map( + projects.map((project) => [project.id, project.workspaceRoot]), + ); + const projectByKey = new Map( + projects.map((project) => [`${project.environmentId}:${project.id}`, project]), + ); + const projectTitleById = new Map( + projects.map((project) => [project.id, project.title]), + ); + return { projectCwdById, projectByKey, projectTitleById }; +} + +export const selectCommandPaletteClientSettings = (settings: ClientSettings) => ({ + sidebarProjectGroupingMode: settings.sidebarProjectGroupingMode, + sidebarProjectGroupingOverrides: settings.sidebarProjectGroupingOverrides, + sidebarProjectSortOrder: settings.sidebarProjectSortOrder, + sidebarThreadSortOrder: settings.sidebarThreadSortOrder, +}); + +export function useSortedProjectGroups(environmentLabelById: ReadonlyMap) { + const projects = useProjects(); + const threads = useThreadShells(); + const projectOrder = useUiStateStore((store) => store.projectOrder); + const clientSettings = useClientSettings(selectCommandPaletteClientSettings); + const primaryEnvironmentId = usePrimaryEnvironmentId(); + const projectGroupingSettings = { + sidebarProjectGroupingMode: clientSettings.sidebarProjectGroupingMode, + sidebarProjectGroupingOverrides: clientSettings.sidebarProjectGroupingOverrides, + }; + const orderedProjects = orderItemsByPreferredIds({ + items: projects, + preferredIds: projectOrder, + getId: getProjectOrderKey, + getPreferenceIds: (project) => [ + getProjectOrderKey(project), + legacyProjectCwdPreferenceKey(project.workspaceRoot), + ], + }); + const unsortedProjectGroups = buildSidebarProjectSnapshots({ + projects: clientSettings.sidebarProjectSortOrder === "manual" ? orderedProjects : projects, + settings: projectGroupingSettings, + primaryEnvironmentId, + resolveEnvironmentLabel: (environmentId) => environmentLabelById.get(environmentId) ?? null, + }); + return sortLogicalProjectsForSidebar( + unsortedProjectGroups, + threads, + clientSettings.sidebarProjectSortOrder, + ); +} diff --git a/apps/web/src/components/CommandPaletteThreadItems.tsx b/apps/web/src/components/CommandPaletteThreadItems.tsx new file mode 100644 index 000000000000..e2732f541908 --- /dev/null +++ b/apps/web/src/components/CommandPaletteThreadItems.tsx @@ -0,0 +1,150 @@ +import { scopeThreadRef } from "@t3tools/client-runtime/environment"; +import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search"; +import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; +import { useAtomValue } from "@effect/atom-react"; +import { useNavigate } from "@tanstack/react-router"; +import { MessageSquareIcon } from "lucide-react"; + +import { useClientSettings } from "../hooks/useSettings"; +import { + applyProviderInstanceSettings, + deriveProviderInstanceEntries, + type ProviderInstanceEntry, +} from "../providerInstances"; +import { useThreadShells } from "../state/entities"; +import { + useEnvironmentProviders, + useEnvironments, + usePrimaryEnvironmentId, +} from "../state/environments"; +import { useThreadSearch } from "../state/queries"; +import { primaryServerProvidersAtom } from "../state/server"; +import { buildThreadRouteParams } from "../threadRoutes"; +import { buildThreadActionItems, ITEM_ICON_CLASS } from "./CommandPalette.logic"; +import { useEnvironmentLookups, useProjectLookups } from "./CommandPaletteLookups"; +import { ThreadCommandSubtitle } from "./ThreadCommandSubtitle"; +import { + ThreadRowLeadingStatus, + ThreadRowTrailingStatus, + ThreadSearchPullRequestNumber, +} from "./ThreadStatusIndicators"; + +function useProviderEntryByEnvironmentAndInstanceId() { + const { environments } = useEnvironments(); + const primaryEnvironmentId = usePrimaryEnvironmentId(); + const providers = useAtomValue(primaryServerProvidersAtom); + const providersByEnvironment = useEnvironmentProviders(); + return new Map( + environments.flatMap((environment) => { + const serverConfig = environment.serverConfig; + const derived = deriveProviderInstanceEntries( + providersByEnvironment.get(environment.environmentId) ?? + (environment.environmentId === primaryEnvironmentId ? providers : []), + ); + const entries = serverConfig + ? applyProviderInstanceSettings(derived, serverConfig.settings, null) + : derived; + return entries.map( + (entry) => [`${environment.environmentId}:${entry.instanceId}`, entry] as const, + ); + }), + ); +} + +function useThreadSearchWithContentMatches(query: string) { + const { environments } = useEnvironments(); + const environmentIds: ReadonlyArray = environments + .filter((environment) => environment.connection.phase === "connected") + .map((environment) => environment.environmentId); + const threadSearch = useThreadSearch(environmentIds, query); + const contentMatchByKey = new Map( + threadSearch.matches.flatMap((match) => + match.source === "user" || match.source === "assistant" + ? [[threadSearchMatchKey(match), match] as const] + : [], + ), + ); + return { threadSearch, contentMatchByKey }; +} + +/** Every thread as a palette item, with message matches for a non-empty `threadSearchQuery`. */ +export function useCommandPaletteThreadItems(input: { + readonly activeThreadId: ThreadId | undefined; + readonly threadSearchQuery: string; +}) { + const { activeThreadId, threadSearchQuery } = input; + const navigate = useNavigate(); + const threads = useThreadShells(); + const sidebarThreadSortOrder = useClientSettings((settings) => settings.sidebarThreadSortOrder); + const { projectEnvironmentLocationById } = useEnvironmentLookups(); + const providerEntryByEnvironmentAndInstanceId = useProviderEntryByEnvironmentAndInstanceId(); + const { threadSearch, contentMatchByKey } = useThreadSearchWithContentMatches(threadSearchQuery); + const { projectByKey, projectTitleById } = useProjectLookups(); + + const threadItems = buildThreadActionItems({ + threads, + ...(activeThreadId ? { activeThreadId } : {}), + projectTitleById, + sortOrder: sidebarThreadSortOrder, + icon: , + renderLeadingContent: (thread) => , + renderTrailingContent: (thread) => ( + <> + + + + ), + renderDescription: (thread, { projectTitle }) => { + const modelInstanceId = + thread.runtime?.providerInstanceId ?? thread.modelSelection.instanceId; + const providerEntry = + providerEntryByEnvironmentAndInstanceId.get(`${thread.environmentId}:${modelInstanceId}`) ?? + null; + return ( + + ); + }, + getContentMatch: (thread) => { + const match = contentMatchByKey.get( + threadSearchMatchKey({ + environmentId: thread.environmentId, + threadId: thread.id, + }), + ); + return match && (match.source === "user" || match.source === "assistant") + ? { + source: match.source, + snippet: match.snippet, + query: threadSearch.query, + } + : undefined; + }, + runThread: async (thread) => { + await navigate({ + to: "/$environmentId/$threadId", + params: buildThreadRouteParams(scopeThreadRef(thread.environmentId, thread.id)), + }); + }, + }); + + return { threadItems, isSearchPending: threadSearch.isPending }; +} From 0b7c902df77db32f575c12a9ffc1579b27513fee Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 21 Oct 2026 01:13:19 -0700 Subject: [PATCH 21/73] refactor(web): the command palette's project items and pure helpers live in their own files (#22699) Co-authored-by: Claude Opus 5.5 --- .../src/components/CommandPalette.logic.ts | 94 ++++++++ apps/web/src/components/CommandPalette.tsx | 213 ++---------------- .../components/CommandPaletteProjectItems.tsx | 117 ++++++++++ 3 files changed, 228 insertions(+), 196 deletions(-) create mode 100644 apps/web/src/components/CommandPaletteProjectItems.tsx diff --git a/apps/web/src/components/CommandPalette.logic.ts b/apps/web/src/components/CommandPalette.logic.ts index 3037ef72159d..6fe9b7cfb693 100644 --- a/apps/web/src/components/CommandPalette.logic.ts +++ b/apps/web/src/components/CommandPalette.logic.ts @@ -1,9 +1,19 @@ import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests"; import type { CommandPaletteLinkedThreads } from "../commandPaletteBus"; +import { + addProjectRemoteSourceLabel, + addProjectRemoteSourcePathHint, + buildAddProjectRemoteSourceReadiness, + type AddProjectRemoteSource, + type AddProjectRemoteSourceReadiness, +} from "@t3tools/client-runtime/operations/projects"; import { type EnvironmentId, + type EnvironmentMachineKind, type FilesystemBrowseEntry, type KeybindingCommand, + type SourceControlDiscoveryResult, + type SourceControlRepositoryInfo, THREAD_JUMP_KEYBINDING_COMMANDS, } from "@t3tools/contracts"; import { filterFilesystemBrowseEntries } from "@t3tools/client-runtime/state/filesystem"; @@ -657,3 +667,87 @@ export function getCommandPaletteInputPlaceholder(mode: CommandPaletteMode): str return "Enter path (e.g. ~/projects/my-app)"; } } + +export function getEnvironmentBrowsePlatform(os: string | null | undefined): string { + if (os === "windows") { + return "Win32"; + } + if (os === "darwin") { + return "MacIntel"; + } + if (os === "linux") { + return "Linux"; + } + return typeof navigator === "undefined" ? "" : navigator.platform; +} + +export interface AddProjectEnvironmentOption { + readonly environmentId: EnvironmentId; + readonly label: string; + readonly machine: EnvironmentMachineKind; + readonly isPrimary: boolean; + readonly isConnected: boolean; + readonly status: string; +} + +export type AddProjectCloneFlow = + | { + readonly step: "repository"; + readonly environmentId: EnvironmentId; + readonly source: AddProjectRemoteSource; + } + | { + readonly step: "confirm"; + readonly environmentId: EnvironmentId; + readonly source: AddProjectRemoteSource; + readonly repositoryInput: string; + readonly repository: SourceControlRepositoryInfo | null; + readonly remoteUrl: string; + }; + +export function remoteProjectInputPlaceholder(flow: AddProjectCloneFlow | null): string | null { + if (!flow) return null; + if (flow.step === "confirm") return null; + if (flow.source === "url") { + return "Enter Git clone URL"; + } + return `Enter ${addProjectRemoteSourceLabel(flow.source)} repository (${addProjectRemoteSourcePathHint(flow.source)})`; +} + +export function buildPaletteRemoteSourceReadiness( + discovery: SourceControlDiscoveryResult | null, +): AddProjectRemoteSourceReadiness { + const readiness = buildAddProjectRemoteSourceReadiness(discovery); + return (source) => { + const entry = readiness(source); + return entry.hint === null + ? entry + : { + ...entry, + hint: entry.hint.replace( + "Open Source Control settings", + "Open Settings -> Source Control", + ), + }; + }; +} + +export function errorMessage(error: unknown): string { + if (error instanceof Error && error.message.trim().length > 0) { + return error.message; + } + return "An error occurred."; +} + +const OVERLAY_MODE_BY_COMMAND = { + "commandPalette.toggle": "command", + "filePicker.toggle": "files", + "projectSearch.toggle": "content", +} as const satisfies Partial>; + +export function overlayModeForCommand(command: string | null): SearchOverlayMode | null { + if (command === null) return null; + return command in OVERLAY_MODE_BY_COMMAND + ? OVERLAY_MODE_BY_COMMAND[command as keyof typeof OVERLAY_MODE_BY_COMMAND] + : null; +} diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 4f434fce7a08..99037bffdcba 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -17,7 +17,6 @@ import { addProjectRemoteSourceLabel, addProjectRemoteSourcePathHint, addProjectRemoteSourceProvider, - buildAddProjectRemoteSourceReadiness, sortAddProjectProviderSources, type AddProjectRemoteSource, type AddProjectRemoteSourceReadiness, @@ -42,11 +41,8 @@ import { AuthFilesystemReadScope, type DesktopWslState, type EnvironmentId, - type EnvironmentMachineKind, type FilesystemBrowseResult, - type SourceControlDiscoveryResult, SourceControlProviderKind, - type SourceControlRepositoryInfo, PRIMARY_LOCAL_ENVIRONMENT_ID, resolveEnvironmentMachineKind, } from "@t3tools/contracts"; @@ -159,8 +155,6 @@ import { ADDON_ICON_CLASS, browseInputEndPaddingClass, buildBrowseGroups, - buildCommandPaletteProjectMetadata, - buildProjectActionItems, buildRootGroups, buildLinkedThreadActionItems, buildCommandPaletteRows, @@ -180,6 +174,13 @@ import { RECENT_THREAD_LIMIT, reduceCommandPaletteUiState, createCommandPaletteSearchState, + type AddProjectCloneFlow, + type AddProjectEnvironmentOption, + buildPaletteRemoteSourceReadiness, + errorMessage, + getEnvironmentBrowsePlatform, + overlayModeForCommand, + remoteProjectInputPlaceholder, reduceCommandPaletteSearchState, type SearchOverlayMode, } from "./CommandPalette.logic"; @@ -191,6 +192,10 @@ import { useProjectLookups, useSortedProjectGroups, } from "./CommandPaletteLookups"; +import { + buildNewThreadInProjectItems, + buildProjectSearchItems, +} from "./CommandPaletteProjectItems"; import { useCommandPaletteThreadItems } from "./CommandPaletteThreadItems"; import { APPEARANCE_OPTIONS, @@ -205,13 +210,11 @@ import { import { sourceControlIcon } from "~/sourceControlPresentation"; import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; import { Checkbox } from "./ui/checkbox"; -import { ProjectFavicon } from "./ProjectFavicon"; import { ProjectFilePicker } from "./files/ProjectFilePicker"; import { openLinkPullRequestDialog } from "./pullRequest/LinkPullRequestDialog"; import { ProjectContentSearchDialog } from "./search/ProjectContentSearchDialog"; import { toggleThemeEditorForTheme } from "./settings/themeEditorStore"; import { searchSettings, SETTINGS_SECTION_LABELS } from "./settings/settingsSearch"; -import { COMMAND_PALETTE_META_ICON_CLASS, CommandPaletteMetaDot } from "./ThreadCommandSubtitle"; import { primaryServerKeybindingsAtom } from "../state/server"; import { resolveShortcutCommand, threadJumpIndexFromCommand } from "../keybindings"; import { CommandDialog, CommandDialogPopup, CommandFooterAction } from "./ui/command"; @@ -223,50 +226,12 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { ComposerHandleContext, useComposerHandleContext } from "../composerHandleContext"; import type { ChatComposerHandle } from "./chat/ChatComposer"; import { buildSidebarProjectPickerEntries } from "../sidebarProjectGrouping"; -import type { Project } from "../types"; import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; import { readPullRequestListPreferences } from "~/components/pullRequest/pullRequestListPreferences"; import { useThreadRouteTarget } from "../hooks/useThreadRouteTarget"; const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = []; -function getEnvironmentBrowsePlatform(os: string | null | undefined): string { - if (os === "windows") { - return "Win32"; - } - if (os === "darwin") { - return "MacIntel"; - } - if (os === "linux") { - return "Linux"; - } - return typeof navigator === "undefined" ? "" : navigator.platform; -} - -interface AddProjectEnvironmentOption { - readonly environmentId: EnvironmentId; - readonly label: string; - readonly machine: EnvironmentMachineKind; - readonly isPrimary: boolean; - readonly isConnected: boolean; - readonly status: string; -} - -type AddProjectCloneFlow = - | { - readonly step: "repository"; - readonly environmentId: EnvironmentId; - readonly source: AddProjectRemoteSource; - } - | { - readonly step: "confirm"; - readonly environmentId: EnvironmentId; - readonly source: AddProjectRemoteSource; - readonly repositoryInput: string; - readonly repository: SourceControlRepositoryInfo | null; - readonly remoteUrl: string; - }; - function remoteProjectSourceIcon(source: AddProjectRemoteSource, className: string): ReactNode { if (source === "url") return ; const Icon = sourceControlIcon(sourceControlClients.get(source)); @@ -281,57 +246,6 @@ function sourceControlHostIcon( return ; } -function projectFaviconIcon(project: Project): ReactNode { - return ; -} - -function remoteProjectInputPlaceholder(flow: AddProjectCloneFlow | null): string | null { - if (!flow) return null; - if (flow.step === "confirm") return null; - if (flow.source === "url") { - return "Enter Git clone URL"; - } - return `Enter ${addProjectRemoteSourceLabel(flow.source)} repository (${addProjectRemoteSourcePathHint(flow.source)})`; -} - -function buildPaletteRemoteSourceReadiness( - discovery: SourceControlDiscoveryResult | null, -): AddProjectRemoteSourceReadiness { - const readiness = buildAddProjectRemoteSourceReadiness(discovery); - return (source) => { - const entry = readiness(source); - return entry.hint === null - ? entry - : { - ...entry, - hint: entry.hint.replace( - "Open Source Control settings", - "Open Settings -> Source Control", - ), - }; - }; -} - -function errorMessage(error: unknown): string { - if (error instanceof Error && error.message.trim().length > 0) { - return error.message; - } - return "An error occurred."; -} - -const OVERLAY_MODE_BY_COMMAND = { - "commandPalette.toggle": "command", - "filePicker.toggle": "files", - "projectSearch.toggle": "content", -} as const satisfies Partial>; - -function overlayModeForCommand(command: string | null): SearchOverlayMode | null { - if (command === null) return null; - return command in OVERLAY_MODE_BY_COMMAND - ? OVERLAY_MODE_BY_COMMAND[command as keyof typeof OVERLAY_MODE_BY_COMMAND] - : null; -} - export function CommandPalette({ children }: { children: ReactNode }) { const navigate = useNavigate(); const [state, dispatch] = useReducer(reduceCommandPaletteUiState, { @@ -940,39 +854,10 @@ function OpenCommandPaletteDialog(props: { await handleNewThread(scopeProjectRef(project.environmentId, project.id)); }; - const projectSearchItems = buildProjectActionItems({ + const projectSearchItems = buildProjectSearchItems({ projects: pickerProjects, - valuePrefix: "project", - searchTerms: (project) => { - const members = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`) - ?.memberProjects ?? [project]; - return buildCommandPaletteProjectMetadata({ - projects: members, - locationByEnvironmentId: projectEnvironmentLocationById, - }).searchTerms; - }, - renderDescription: (project) => { - const members = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`) - ?.memberProjects ?? [project]; - const metadata = buildCommandPaletteProjectMetadata({ - projects: members, - locationByEnvironmentId: projectEnvironmentLocationById, - }); - const location = projectEnvironmentLocationById.get(project.environmentId) ?? { - kind: "remote" as const, - label: "Remote", - machine: "server" as const, - }; - return ( - 1} - location={location} - workspaceRoot={project.workspaceRoot} - /> - ); - }, - icon: projectFaviconIcon, + projectGroupByTargetKey, + locationByEnvironmentId: projectEnvironmentLocationById, runProject: openProjectFromSearch, }); @@ -980,42 +865,10 @@ function OpenCommandPaletteDialog(props: { const isScratch = (project: CommandPaletteProject) => isScratchProject(project, scratchWorkspaceRootFor(project.environmentId)); const projectItems = enumerateCommandPaletteItems( - buildProjectActionItems({ + buildNewThreadInProjectItems({ projects: pickerProjects.filter((project) => !isScratch(project)), - valuePrefix: "new-thread-in", - searchTerms: (project) => { - const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`); - const location = projectEnvironmentLocationById.get(project.environmentId); - return [ - ...(group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ?? - []), - ...(location ? [location.label] : []), - ]; - }, - renderDescription: (project) => { - const location = projectEnvironmentLocationById.get(project.environmentId) ?? { - kind: "remote", - label: "Remote", - machine: "server" as const, - }; - return ( - - - {location.kind === "remote" ? ( - - ) : null} - {location.label} - - - {project.workspaceRoot} - - ); - }, - icon: projectFaviconIcon, + projectGroupByTargetKey, + locationByEnvironmentId: projectEnvironmentLocationById, runProject: async (project) => { const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`); const contextualRefBelongsToGroup = @@ -3021,35 +2874,3 @@ function OpenCommandPaletteDialog(props: { ); } - -function ProjectSearchDescription(props: { - readonly environmentLabels: ReadonlyArray; - readonly grouped: boolean; - readonly location: { - readonly kind: "local" | "remote"; - readonly label: string; - readonly machine: EnvironmentMachineKind; - }; - readonly workspaceRoot: string; -}) { - if (!props.grouped) { - return ( - - - {props.location.kind === "remote" ? ( - - ) : null} - {props.location.label} - - - {props.workspaceRoot} - - ); - } - - return {props.environmentLabels.join(" · ")}; -} diff --git a/apps/web/src/components/CommandPaletteProjectItems.tsx b/apps/web/src/components/CommandPaletteProjectItems.tsx new file mode 100644 index 000000000000..90373413be4f --- /dev/null +++ b/apps/web/src/components/CommandPaletteProjectItems.tsx @@ -0,0 +1,117 @@ +import type { EnvironmentId, EnvironmentMachineKind } from "@t3tools/contracts"; + +import type { SidebarProjectSnapshot } from "../sidebarProjectGrouping"; +import type { Project } from "../types"; +import { + buildCommandPaletteProjectMetadata, + buildProjectActionItems, + type CommandPaletteProject, + ITEM_ICON_CLASS, +} from "./CommandPalette.logic"; +import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; +import { ProjectFavicon } from "./ProjectFavicon"; +import { COMMAND_PALETTE_META_ICON_CLASS, CommandPaletteMetaDot } from "./ThreadCommandSubtitle"; + +export interface CommandPaletteProjectLocation { + readonly kind: "local" | "remote"; + readonly label: string; + readonly machine: EnvironmentMachineKind; +} + +const UNKNOWN_PROJECT_LOCATION: CommandPaletteProjectLocation = { + kind: "remote", + label: "Remote", + machine: "server", +}; + +interface ProjectItemsInput { + readonly projects: ReadonlyArray; + readonly projectGroupByTargetKey: ReadonlyMap; + readonly locationByEnvironmentId: ReadonlyMap; + readonly runProject: (project: CommandPaletteProject) => Promise; +} + +function projectFaviconIcon(project: Project) { + return ; +} + +/** Projects as root search results; a grouped project matches on every member. */ +export function buildProjectSearchItems(input: ProjectItemsInput) { + const membersOf = (project: CommandPaletteProject) => + input.projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`)?.memberProjects ?? [ + project, + ]; + return buildProjectActionItems({ + projects: input.projects, + valuePrefix: "project", + searchTerms: (project) => + buildCommandPaletteProjectMetadata({ + projects: membersOf(project), + locationByEnvironmentId: input.locationByEnvironmentId, + }).searchTerms, + renderDescription: (project) => { + const members = membersOf(project); + const metadata = buildCommandPaletteProjectMetadata({ + projects: members, + locationByEnvironmentId: input.locationByEnvironmentId, + }); + return members.length > 1 ? ( + {metadata.environmentLabels.join(" · ")} + ) : ( + + ); + }, + icon: projectFaviconIcon, + runProject: input.runProject, + }); +} + +/** Projects as targets of the "New thread in..." submenu. */ +export function buildNewThreadInProjectItems(input: ProjectItemsInput) { + return buildProjectActionItems({ + projects: input.projects, + valuePrefix: "new-thread-in", + searchTerms: (project) => { + const group = input.projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`); + const location = input.locationByEnvironmentId.get(project.environmentId); + return [ + ...(group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ?? []), + ...(location ? [location.label] : []), + ]; + }, + renderDescription: (project) => ( + + ), + icon: projectFaviconIcon, + runProject: input.runProject, + }); +} + +function ProjectLocationDescription(props: { + readonly location: CommandPaletteProjectLocation | undefined; + readonly workspaceRoot: string; +}) { + const location = props.location ?? UNKNOWN_PROJECT_LOCATION; + return ( + + + {location.kind === "remote" ? ( + + ) : null} + {location.label} + + + {props.workspaceRoot} + + ); +} From 28a8d2b3d81699d6203671d62eae76cc8859681d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 21 Oct 2026 01:13:20 -0700 Subject: [PATCH 22/73] refactor(web): the command palette's add-project views are components (#22707) Co-authored-by: Claude Opus 5.5 --- apps/web/src/components/CommandPalette.tsx | 280 +++++------------- .../components/CommandPaletteAddProject.tsx | 171 +++++++++++ 2 files changed, 244 insertions(+), 207 deletions(-) create mode 100644 apps/web/src/components/CommandPaletteAddProject.tsx diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 99037bffdcba..5ee7676de332 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -12,7 +12,6 @@ import { getNewProjectPublishTarget, getNewProjectPublishTargets, getNewProjectRepository, - type NewProjectPublishTarget, normalizePastedCloneUrl, addProjectRemoteSourceLabel, addProjectRemoteSourcePathHint, @@ -196,6 +195,14 @@ import { buildNewThreadInProjectItems, buildProjectSearchItems, } from "./CommandPaletteProjectItems"; +import { + CommandPaletteSubmitButton, + NewProjectPublishHostPicker, + NewProjectSummary, + RemoteRepositorySummary, + remoteProjectSourceIcon, + sourceControlHostIcon, +} from "./CommandPaletteAddProject"; import { useCommandPaletteThreadItems } from "./CommandPaletteThreadItems"; import { APPEARANCE_OPTIONS, @@ -203,11 +210,9 @@ import { useCommandPaletteThemeItems, } from "./CommandPaletteThemeItems"; import { - CommandPaletteTitleIcon, CommandPaletteVirtualizedResults, scrollCommandPaletteRowIntoView, } from "./CommandPaletteResults"; -import { sourceControlIcon } from "~/sourceControlPresentation"; import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; import { Checkbox } from "./ui/checkbox"; import { ProjectFilePicker } from "./files/ProjectFilePicker"; @@ -219,8 +224,6 @@ import { primaryServerKeybindingsAtom } from "../state/server"; import { resolveShortcutCommand, threadJumpIndexFromCommand } from "../keybindings"; import { CommandDialog, CommandDialogPopup, CommandFooterAction } from "./ui/command"; import { Button } from "./ui/button"; -import { Kbd, KbdGroup } from "./ui/kbd"; -import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; import { stackedThreadToast, toastManager } from "./ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { ComposerHandleContext, useComposerHandleContext } from "../composerHandleContext"; @@ -232,20 +235,6 @@ import { useThreadRouteTarget } from "../hooks/useThreadRouteTarget"; const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = []; -function remoteProjectSourceIcon(source: AddProjectRemoteSource, className: string): ReactNode { - if (source === "url") return ; - const Icon = sourceControlIcon(sourceControlClients.get(source)); - return ; -} - -function sourceControlHostIcon( - definition: NewProjectPublishTarget["definition"], - className: string, -): ReactNode { - const Icon = sourceControlIcon(definition); - return ; -} - export function CommandPalette({ children }: { children: ReactNode }) { const navigate = useNavigate(); const [state, dispatch] = useReducer(reduceCommandPaletteUiState, { @@ -599,7 +588,6 @@ function OpenCommandPaletteDialog(props: { const [newProjectPublishes, setNewProjectPublishes] = useState(false); const [newProjectPublishKind, setNewProjectPublishKind] = useState(null); - const [newProjectHostPickerOpen, setNewProjectHostPickerOpen] = useState(false); const [isCreatingNewProject, setIsCreatingNewProject] = useState(false); // State lags a render behind, so a repeated Enter could start a second create. const newProjectSubmittingRef = useRef(false); @@ -2091,18 +2079,6 @@ function OpenCommandPaletteDialog(props: { group.value === "directories" ? { ...group, label: "Select where to clone" } : group, ); - const remoteProjectContext = (() => { - if (addProjectCloneFlow?.step !== "confirm") { - return null; - } - - return { - title: addProjectCloneFlow.repository?.nameWithOwner ?? addProjectCloneFlow.repositoryInput, - description: addProjectCloneFlow.repository?.url ?? addProjectCloneFlow.remoteUrl, - icon: remoteProjectSourceIcon(addProjectCloneFlow.source, ITEM_ICON_CLASS), - }; - })(); - const newProjectsRoot = newProjectFlow ? newProjectsRootFor(newProjectFlow.environmentId) : null; const newProjectPathPreview = newProjectsRoot === null ? null : getNewProjectPathPreview(newProjectsRoot, newProjectName); @@ -2209,55 +2185,12 @@ function OpenCommandPaletteDialog(props: { titleTrailingContent: ( {newProjectPublishTargets.length > 1 ? ( - // The row toggles on click, so the picker keeps its clicks to itself. The - // row also cancels pointerdown to keep focus in the input, which drops the - // mousedown Select opens on, so the picker opens on click instead. - { - event.stopPropagation(); - // React bubbles the popup's clicks here through its portal; only the - // trigger opens the picker. - if (!event.currentTarget.contains(event.target as Node)) return; - if (!isCreatingNewProject) setNewProjectHostPickerOpen(true); - }} - > - - + ) : null} @@ -2622,106 +2555,56 @@ function OpenCommandPaletteDialog(props: { const inputAccessory = newProjectFlow !== null ? ( - - { - event.preventDefault(); - }} - onClick={() => { - void submitNewProject(); - }} - /> - } - > - {isCreatingNewProject ? "Creating" : "Create"} - - {createShortcutLabel} - - - {`Create (${createShortcutLabel})`} - + { + void submitNewProject(); + }} + /> ) : addProjectCloneFlow?.step === "repository" ? ( - - { - event.preventDefault(); - }} - onClick={() => { - void submitAddProjectCloneFlow(); - }} - /> - } - > - {isRemoteProjectPending ? "Working" : remoteProjectButtonLabel} - - Enter - - - {remoteProjectButtonLabel ?? "Continue"} (Enter) - + { + void submitAddProjectCloneFlow(); + }} + /> ) : isBrowsing ? ( - - { - event.preventDefault(); - }} - onClick={() => { - if (relativePathNeedsActiveProject) { - return; - } - if (isCloneDestinationStep) { - void submitAddProjectCloneFlow(resolvedAddProjectPath); - } else { - void handleAddProject(resolvedAddProjectPath); - } - }} - /> - } - > - - {isCloneDestinationStep && isRemoteProjectPending ? "Cloning" : submitActionLabel} - - - {hasHighlightedBrowseItem ? `${submitModifierLabel} Enter` : "Enter"} - - - - {isCloneDestinationStep && !canCloneProject + - + : "This connection cannot add projects." + } + disabled={ + !canCreateProject || + !canCreateProjectInEnvironment(browseEnvironment?.connection.phase) || + relativePathNeedsActiveProject || + (isCloneDestinationStep && (!canCloneProject || isRemoteProjectPending)) + } + onSubmit={() => { + if (relativePathNeedsActiveProject) { + return; + } + if (isCloneDestinationStep) { + void submitAddProjectCloneFlow(resolvedAddProjectPath); + } else { + void handleAddProject(resolvedAddProjectPath); + } + }} + /> ) : null; const footerActionLabel = @@ -2803,38 +2686,21 @@ function OpenCommandPaletteDialog(props: { value={query} > {newProjectPathPreview !== null ? ( -
-
- - - - - - {newProjectName.length > 0 ? newProjectName : "New project"} - - - {newProjectName.length > 0 - ? `Creates ${newProjectPathPreview}` - : `Goes in ${newProjectsRoot}`} - {newProjectEnvironmentLabel === null ? null : ` on ${newProjectEnvironmentLabel}`} - - -
-
+ ) : null} - {remoteProjectContext ? ( -
-
Repository
-
- {remoteProjectContext.icon} - - {remoteProjectContext.title} - - {remoteProjectContext.description} - - -
-
+ {addProjectCloneFlow?.step === "confirm" ? ( + ) : null} {browseAccessError ? (
diff --git a/apps/web/src/components/CommandPaletteAddProject.tsx b/apps/web/src/components/CommandPaletteAddProject.tsx new file mode 100644 index 000000000000..808536403a4b --- /dev/null +++ b/apps/web/src/components/CommandPaletteAddProject.tsx @@ -0,0 +1,171 @@ +import { + type AddProjectRemoteSource, + type NewProjectPublishTarget, +} from "@t3tools/client-runtime/operations/projects"; +import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import type { SourceControlProviderKind } from "@t3tools/contracts"; +import { FolderGit2Icon, LinkIcon } from "lucide-react"; +import { type ReactNode, useState } from "react"; + +import { sourceControlIcon } from "~/sourceControlPresentation"; +import { ITEM_ICON_CLASS } from "./CommandPalette.logic"; +import { CommandPaletteTitleIcon } from "./CommandPaletteResults"; +import { Button } from "./ui/button"; +import { Kbd, KbdGroup } from "./ui/kbd"; +import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; + +export function remoteProjectSourceIcon( + source: AddProjectRemoteSource, + className: string, +): ReactNode { + if (source === "url") return ; + const Icon = sourceControlIcon(sourceControlClients.get(source)); + return ; +} + +export function sourceControlHostIcon( + definition: NewProjectPublishTarget["definition"], + className: string, +): ReactNode { + const Icon = sourceControlIcon(definition); + return ; +} + +/** The input's trailing submit button for the add-project, clone, and new-project flows. */ +export function CommandPaletteSubmitButton(props: { + readonly label: ReactNode; + readonly shortcutLabel: string; + readonly ariaLabel: string; + readonly tooltip: ReactNode; + readonly disabled: boolean; + readonly onSubmit: () => void; +}) { + return ( + + { + event.preventDefault(); + }} + onClick={props.onSubmit} + /> + } + > + {props.label} + + {props.shortcutLabel} + + + {props.tooltip} + + ); +} + +/** + * Picks which host a new project publishes to. The row it sits in toggles on click, so the + * picker keeps its clicks to itself. The row also cancels pointerdown to keep focus in the + * input, which drops the mousedown Select opens on, so the picker opens on click instead. + */ +export function NewProjectPublishHostPicker(props: { + readonly disabled: boolean; + readonly targets: ReadonlyArray; + readonly value: SourceControlProviderKind; + readonly onValueChange: (kind: SourceControlProviderKind) => void; +}) { + const [open, setOpen] = useState(false); + return ( + { + event.stopPropagation(); + // React bubbles the popup's clicks here through its portal; only the + // trigger opens the picker. + if (!event.currentTarget.contains(event.target as Node)) return; + if (!props.disabled) setOpen(true); + }} + > + + + ); +} + +export function NewProjectSummary(props: { + readonly name: string; + readonly pathPreview: string; + readonly projectsRoot: string | null; + readonly environmentLabel: string | null; +}) { + return ( +
+
+ + + + + + {props.name.length > 0 ? props.name : "New project"} + + + {props.name.length > 0 + ? `Creates ${props.pathPreview}` + : `Goes in ${props.projectsRoot}`} + {props.environmentLabel === null ? null : ` on ${props.environmentLabel}`} + + +
+
+ ); +} + +export function RemoteRepositorySummary(props: { + readonly source: AddProjectRemoteSource; + readonly title: string; + readonly description: string; +}) { + return ( +
+
Repository
+
+ + {remoteProjectSourceIcon(props.source, ITEM_ICON_CLASS)} + + + {props.title} + {props.description} + +
+
+ ); +} From 955b4cc80166fd0bfd01fe2ec40285bd755f986f Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 21 Oct 2026 01:17:04 -0700 Subject: [PATCH 23/73] feat(cli): start, message, and read threads from the terminal with t3 thread (#22732) Co-authored-by: Claude Opus 5.5 --- apps/server/src/auth/EnvironmentAuth.ts | 12 +- apps/server/src/binCli.ts | 2 + apps/server/src/cli/thread.test.ts | 75 ++ apps/server/src/cli/thread.ts | 710 ++++++++++++++++++ apps/server/src/mcp/AcpMcpStdioBridge.ts | 166 ++-- .../src/mcp/toolkits/project/handlers.test.ts | 125 ++- .../src/mcp/toolkits/project/handlers.ts | 29 +- docs/user/outside-agents.md | 22 + 8 files changed, 1041 insertions(+), 100 deletions(-) create mode 100644 apps/server/src/cli/thread.test.ts create mode 100644 apps/server/src/cli/thread.ts diff --git a/apps/server/src/auth/EnvironmentAuth.ts b/apps/server/src/auth/EnvironmentAuth.ts index 569ae75907d1..1a6d3206833a 100644 --- a/apps/server/src/auth/EnvironmentAuth.ts +++ b/apps/server/src/auth/EnvironmentAuth.ts @@ -558,7 +558,11 @@ export class EnvironmentAuth extends Context.Service< readonly access: AuthMcpClientAccess; readonly client: AuthClientMetadata; }) => Effect.Effect< - { readonly token: string; readonly expiresAt: DateTime.DateTime }, + { + readonly sessionId: AuthSessionId; + readonly token: string; + readonly expiresAt: DateTime.DateTime; + }, ServerAuthInternalError >; /** @@ -1292,7 +1296,11 @@ export const make = Effect.gen(function* () { client: { ...input.client, label: input.label, deviceType: "bot" }, }) .pipe( - Effect.map((issued) => ({ token: issued.token, expiresAt: issued.expiresAt })), + Effect.map((issued) => ({ + sessionId: issued.sessionId, + token: issued.token, + expiresAt: issued.expiresAt, + })), Effect.mapError((cause) => new ServerAuthSessionTokenIssueError({ cause })), Effect.withSpan("EnvironmentAuth.issueMcpClientSession"), ); diff --git a/apps/server/src/binCli.ts b/apps/server/src/binCli.ts index b0a2ceffef8f..2f7c24536caa 100644 --- a/apps/server/src/binCli.ts +++ b/apps/server/src/binCli.ts @@ -18,6 +18,7 @@ import { hasCloudPublicConfig } from "./cloud/publicConfig.ts"; import { extendConnectionAttemptTimeout, keepOutboundFetchOnHttp1 } from "./outboundHttp.ts"; import { sharedServerCommandFlags } from "./cli/config.ts"; import { projectCommand } from "./cli/project.ts"; +import { threadCommand } from "./cli/thread.ts"; import { runDefaultServerCommand, serveCommand, startCommand } from "./cli/server.ts"; import { updateCommand } from "./cli/update.ts"; import { uninstallCommand } from "./cli/uninstall.ts"; @@ -76,6 +77,7 @@ export const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) => authCommand, browserCommand, projectCommand, + threadCommand, serviceCommand, updateCommand, uninstallCommand, diff --git a/apps/server/src/cli/thread.test.ts b/apps/server/src/cli/thread.test.ts new file mode 100644 index 000000000000..c2e4bc5c8168 --- /dev/null +++ b/apps/server/src/cli/thread.test.ts @@ -0,0 +1,75 @@ +import { assert, it } from "@effect/vitest"; +import { RunId, ThreadId } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; + +import { type ThreadTools, waitForRun } from "./thread.ts"; + +const threadId = ThreadId.make("thread-1"); +const runId = RunId.make("run-1"); + +/** Answers each tool call with the next scripted response for that tool. */ +const scriptedTools = (script: Record>) => { + const calls: Array<{ readonly tool: string; readonly args: unknown }> = []; + const call = ((tool: string, args: unknown) => { + calls.push({ tool, args }); + const next = script[tool]?.shift(); + return next === undefined ? Effect.die(`unscripted ${tool} call`) : Effect.succeed(next); + }) as ThreadTools["call"]; + return { tools: { call } satisfies ThreadTools, calls }; +}; + +it.effect("--wait follows the sent run to its continuation's terminal status", () => + Effect.gen(function* () { + const continuation = RunId.make("run-2"); + const { tools, calls } = scriptedTools({ + t3_thread_wait: [ + { threadId, runId, status: "running", timedOut: true }, + { threadId, runId: continuation, status: "failed", timedOut: false }, + ], + t3_thread_read: [{ thread: { pendingRequestCount: 0 } }], + }); + + const outcome = yield* waitForRun(tools, threadId, runId); + + assert.deepStrictEqual(outcome, { type: "ended", runId: continuation, status: "failed" }); + assert.deepStrictEqual( + calls.filter((entry) => entry.tool === "t3_thread_wait").map((entry) => entry.args), + [ + { threadId, runId, timeoutMs: 15_000 }, + { threadId, runId, timeoutMs: 15_000 }, + ], + ); + }), +); + +it.effect("--wait stops when the thread waits for an answer instead of polling forever", () => + Effect.gen(function* () { + const { tools } = scriptedTools({ + t3_thread_wait: [ + { threadId, runId, status: "queued", timedOut: true }, + { threadId, runId, status: "queued", timedOut: true }, + ], + t3_thread_read: [{ thread: { pendingRequestCount: 1 } }], + }); + + assert.deepStrictEqual(yield* waitForRun(tools, threadId, runId), { type: "needs_input" }); + }), +); + +it.effect("--wait reports its run's result when a later run is the one waiting", () => + Effect.gen(function* () { + const { tools } = scriptedTools({ + t3_thread_wait: [ + { threadId, runId, status: "running", timedOut: true }, + { threadId, runId, status: "completed", timedOut: false }, + ], + t3_thread_read: [{ thread: { pendingRequestCount: 1 } }], + }); + + assert.deepStrictEqual(yield* waitForRun(tools, threadId, runId), { + type: "ended", + runId, + status: "completed", + }); + }), +); diff --git a/apps/server/src/cli/thread.ts b/apps/server/src/cli/thread.ts new file mode 100644 index 000000000000..989028afccb9 --- /dev/null +++ b/apps/server/src/cli/thread.ts @@ -0,0 +1,710 @@ +import { + OrchestratorMcpFailure, + OrchestratorMcpThreadListResult, + OrchestratorMcpThreadReadResult, + OrchestratorMcpThreadSendResult, + OrchestratorMcpThreadWaitResult, + ProjectId, + RunId, + ThreadId, + TrimmedNonEmptyString, + type OrchestratorMcpThreadListItem, + type OrchestratorMcpThreadStatus, + type OrchestratorMcpThreadTimelineItem, +} from "@t3tools/contracts"; +import * as HostProcess from "@t3tools/shared/HostProcess"; +import * as Console from "effect/Console"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Path from "effect/Path"; +import * as References from "effect/References"; +import * as Runtime from "effect/Runtime"; +import * as Schema from "effect/Schema"; +import * as Stdio from "effect/Stdio"; +import * as Stream from "effect/Stream"; +import { Argument, Command, Flag, GlobalFlag } from "effect/cli"; + +import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts"; +import * as ServerConfig from "../config.ts"; +import { mcpToolSession, type McpToolSession } from "../mcp/AcpMcpStdioBridge.ts"; +import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts"; +import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts"; + +/** + * Short enough that a run which stops to ask the user something is noticed + * soon, since `t3_thread_wait` only returns on a terminal run. + */ +const WAIT_SLICE_MS = 15_000; +const READ_PAGE_LIMIT = 100; +const TITLE_MAX_CHARS = 80; + +export class ThreadServerNotRunningError extends Schema.TaggedError()( + "ThreadServerNotRunningError", + { baseDir: Schema.String }, +) { + override readonly [Runtime.errorReported] = false; + override get message(): string { + return `No running T3 Code server uses ${this.baseDir}. Start T3 Code, then retry.`; + } +} + +export class ThreadServerUnreachableError extends Schema.TaggedError()( + "ThreadServerUnreachableError", + { origin: Schema.String, cause: Schema.Defect() }, +) { + override readonly [Runtime.errorReported] = false; + override get message(): string { + const reason = this.cause instanceof Error ? ` ${this.cause.message}` : ""; + return `Could not reach the T3 Code server at ${this.origin}.${reason}`; + } +} + +export class ThreadToolFailedError extends Schema.TaggedError()( + "ThreadToolFailedError", + { tool: Schema.String, detail: Schema.String }, +) { + override readonly [Runtime.errorReported] = false; + override get message(): string { + return this.detail; + } +} + +export class ThreadUsageError extends Schema.TaggedError()("ThreadUsageError", { + detail: Schema.String, +}) { + override readonly [Runtime.errorReported] = false; + override get message(): string { + return this.detail; + } +} + +export class ThreadProjectNotFoundError extends Schema.TaggedError()( + "ThreadProjectNotFoundError", + { identifier: Schema.String }, +) { + override readonly [Runtime.errorReported] = false; + override get message(): string { + return `No T3 Code project matches '${this.identifier}'. Pass a project id or a path inside a project.`; + } +} + +/** Exit code 2 lets a script tell "needs a human" apart from a failed run. */ +export class ThreadNeedsInputError extends Schema.TaggedError()( + "ThreadNeedsInputError", + { threadId: ThreadId }, +) { + override readonly [Runtime.errorReported] = false; + override readonly [Runtime.errorExitCode] = 2; + + override get message(): string { + return `Thread ${this.threadId} is waiting for an answer. Open it in T3 Code to respond.`; + } +} + +export class ThreadRunEndedError extends Schema.TaggedError()( + "ThreadRunEndedError", + { threadId: ThreadId, status: Schema.String, error: Schema.optional(Schema.String) }, +) { + override readonly [Runtime.errorReported] = false; + override get message(): string { + const reason = this.error === undefined ? "." : `: ${this.error}`; + return `Thread ${this.threadId} stopped with status ${this.status}${reason}`; + } +} + +const ThreadCliError = Schema.Union([ + ThreadServerNotRunningError, + ThreadServerUnreachableError, + ThreadToolFailedError, + ThreadUsageError, + ThreadProjectNotFoundError, + ThreadNeedsInputError, + ThreadRunEndedError, +]); +const isThreadCliError = Schema.is(ThreadCliError); + +/** + * These errors are expected outcomes for a script, so they print as one line + * on stderr instead of the runtime's logged stack trace. + */ +const reportThreadErrors = (effect: Effect.Effect) => + effect.pipe( + Effect.tapError((error) => + isThreadCliError(error) ? Console.error(error.message) : Effect.void, + ), + ); + +const decodeFailureText = Schema.decodeUnknownOption(Schema.fromJsonString(OrchestratorMcpFailure)); +const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); +const printJson = (value: unknown) => Console.log(encodeJson(value)); + +const ToolResult = Schema.Struct({ + isError: Schema.optional(Schema.Boolean), + structuredContent: Schema.optional(Schema.Unknown), + content: Schema.optional( + Schema.Array(Schema.Struct({ type: Schema.String, text: Schema.optional(Schema.String) })), + ), +}); + +const decodeToolResult = Schema.decodeUnknownEffect(ToolResult); + +const ProjectListResult = Schema.Struct({ + projects: Schema.Array( + Schema.Struct({ id: ProjectId, title: Schema.String, workspaceRoot: Schema.String }), + ), + nextCursor: Schema.NullOr(Schema.Int), +}); + +const ThreadLaunchResult = Schema.Struct({ + threadId: ThreadId, + projectId: ProjectId, + runId: Schema.NullOr(RunId), +}); + +const toolFailureDetail = (result: typeof ToolResult.Type) => { + const text = result.content?.find((part) => part.text !== undefined)?.text ?? ""; + return Option.match(decodeFailureText(text), { + onNone: () => text || "The tool call failed.", + onSome: (failure) => failure.message, + }); +}; + +const makeThreadTools = (session: McpToolSession, origin: string) => { + const unexpected = (tool: string) => + new ThreadToolFailedError({ + tool, + detail: `The server sent an unexpected ${tool} result. Update the t3 CLI to match the server.`, + }); + return { + call: Effect.fn("cli.thread.callTool")(function* >( + tool: string, + args: Readonly>, + schema: S, + ): Effect.fn.Return { + const raw = yield* session + .call(tool, args) + .pipe(Effect.mapError((cause) => new ThreadServerUnreachableError({ origin, cause }))); + const result = yield* decodeToolResult(raw).pipe(Effect.mapError(() => unexpected(tool))); + if (result.isError === true) { + return yield* new ThreadToolFailedError({ tool, detail: toolFailureDetail(result) }); + } + return yield* Schema.decodeUnknownEffect(schema)(result.structuredContent).pipe( + Effect.mapError(() => unexpected(tool)), + ); + }), + }; +}; +export type ThreadTools = ReturnType; + +/** + * Signs in as a short-lived MCP client of the running server, the same way an + * outside agent does, and revokes that session when the command ends. + */ +const withThreadTools = Effect.fn("cli.thread.withThreadTools")(function* ( + flags: { readonly baseDir: Option.Option }, + run: (tools: ThreadTools) => Effect.Effect, +) { + const logLevel = yield* GlobalFlag.LogLevel; + const config = yield* resolveCliAuthConfig(flags, logLevel); + return yield* Effect.gen(function* () { + const runtimeState = yield* readPersistedServerRuntimeState(config.serverRuntimeStatePath); + if (Option.isNone(runtimeState)) { + return yield* new ThreadServerNotRunningError({ baseDir: config.baseDir }); + } + const origin = runtimeState.value.origin; + const environmentAuth = yield* EnvironmentAuth.EnvironmentAuth; + const issued = yield* Effect.acquireRelease( + environmentAuth.issueMcpClientSession({ + label: "t3 thread cli", + // The CLI runs as the machine's owner. New threads still start at + // the project's default mode, never at this ceiling by default. + access: "full-access", + client: { deviceType: "bot" }, + }), + (session) => + environmentAuth.revokeSession(session.sessionId).pipe(Effect.ignore({ log: true })), + ); + const session = yield* mcpToolSession({ + endpoint: new URL("/mcp", origin).href, + authorization: `Bearer ${issued.token}`, + clientName: "t3-thread-cli", + }).pipe(Effect.mapError((cause) => new ThreadServerUnreachableError({ origin, cause }))); + return yield* run(makeThreadTools(session, origin)); + }).pipe( + Effect.scoped, + Effect.provide( + EnvironmentAuth.layerRuntime.pipe( + Layer.provide(ServerConfig.layer(config)), + Layer.provide(Layer.succeed(References.MinimumLogLevel, config.logLevel)), + ), + ), + ); +}); + +const readMessageArgument = Effect.fn("cli.thread.readMessage")(function* (argument: string) { + const text = + argument === "-" + ? yield* Stdio.Stdio.pipe( + Effect.flatMap((stdio) => stdio.stdin.pipe(Stream.decodeText(), Stream.mkString)), + ) + : argument; + if (text.trim().length === 0) { + return yield* new ThreadUsageError({ detail: "The message is empty." }); + } + return text; +}); + +const listAllProjects = Effect.fn("cli.thread.listAllProjects")(function* (tools: ThreadTools) { + const projects: Array<(typeof ProjectListResult.Type)["projects"][number]> = []; + let cursor: number | null = 0; + while (cursor !== null) { + const page: typeof ProjectListResult.Type = yield* tools.call( + "t3_project_list", + { cursor, limit: 100 }, + ProjectListResult, + ); + projects.push(...page.projects); + cursor = page.nextCursor; + } + return projects; +}); + +/** A project id, or a path inside a project's folder (deepest project wins). */ +const resolveProject = Effect.fn("cli.thread.resolveProject")(function* ( + tools: ThreadTools, + identifier: string, +) { + const projects = yield* listAllProjects(tools); + const byId = projects.find((project) => project.id === identifier); + if (byId !== undefined) return byId; + + const path = yield* Path.Path; + const fileSystem = yield* FileSystem.FileSystem; + // Stored roots keep the path the project was added with, which may pass + // through a symlink (macOS /tmp), so both sides compare as real paths. + const realPath = (value: string) => + fileSystem.realPath(value).pipe(Effect.orElseSucceed(() => value)); + const target = yield* realPath(path.resolve(yield* HostProcess.WorkingDirectory, identifier)); + const roots = yield* Effect.forEach(projects, (project) => + Effect.map(realPath(project.workspaceRoot), (root) => ({ project, root })), + ); + const containing = roots + .filter(({ root }) => { + const relative = path.relative(root, target); + return ( + relative === "" || + (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) + ); + }) + .toSorted((left, right) => right.root.length - left.root.length)[0]; + if (containing === undefined) { + return yield* new ThreadProjectNotFoundError({ identifier }); + } + return containing.project; +}); + +/** Every item on a timeline view, oldest first. */ +const readTimeline = Effect.fn("cli.thread.readTimeline")(function* ( + tools: ThreadTools, + threadId: ThreadId, + view: "messages" | "activity", +) { + const items: Array = []; + let afterPosition: number | undefined; + let page: OrchestratorMcpThreadReadResult; + do { + page = yield* tools.call( + "t3_thread_read", + { + threadId, + view, + limit: READ_PAGE_LIMIT, + runLimit: 1, + ...(afterPosition === undefined ? {} : { afterPosition }), + }, + OrchestratorMcpThreadReadResult, + ); + items.push(...page.items); + afterPosition = page.nextPosition ?? undefined; + } while (page.hasMore && afterPosition !== undefined); + return { thread: page.thread, items }; +}); + +/** Item text past the read's per-item cap. */ +const readFullText = Effect.fn("cli.thread.readFullText")(function* ( + tools: ThreadTools, + threadId: ThreadId, + item: OrchestratorMcpThreadTimelineItem, +) { + let text = item.text ?? ""; + let nextTextOffset = item.textTruncated ? (item.nextTextOffset ?? null) : null; + while (nextTextOffset !== null) { + const page = yield* tools.call( + "t3_thread_read", + { threadId, itemId: item.itemId, textOffset: nextTextOffset, runLimit: 1 }, + OrchestratorMcpThreadReadResult, + ); + const chunk = page.items.find((candidate) => candidate.itemId === item.itemId); + if (chunk === undefined) break; + text += chunk.text ?? ""; + nextTextOffset = chunk.textTruncated ? (chunk.nextTextOffset ?? null) : null; + } + return text; +}); + +/** The run's last assistant message, or its last error when it did not complete. */ +const runResultText = Effect.fn("cli.thread.runResultText")(function* ( + tools: ThreadTools, + threadId: ThreadId, + runId: RunId, + type: "assistant_message" | "error", +) { + const { items } = yield* readTimeline( + tools, + threadId, + type === "assistant_message" ? "messages" : "activity", + ); + const item = items.findLast((candidate) => candidate.runId === runId && candidate.type === type); + return item === undefined ? null : yield* readFullText(tools, threadId, item); +}); + +type WaitOutcome = + | { + readonly type: "ended"; + readonly runId: RunId | null; + readonly status: OrchestratorMcpThreadStatus; + } + | { readonly type: "needs_input" }; + +/** + * Waits for this exact run, following a restart to its continuation, so a + * message another client sends meanwhile cannot stand in for its result. + */ +export const waitForRun = Effect.fn("cli.thread.waitForRun")(function* ( + tools: ThreadTools, + threadId: ThreadId, + runId: RunId, +) { + const waitSlice = (timeoutMs: number) => + tools.call("t3_thread_wait", { threadId, runId, timeoutMs }, OrchestratorMcpThreadWaitResult); + const ended = (waited: OrchestratorMcpThreadWaitResult) => + ({ type: "ended", runId: waited.runId, status: waited.status }) satisfies WaitOutcome; + while (true) { + const waited = yield* waitSlice(WAIT_SLICE_MS); + if (!waited.timedOut) return ended(waited); + // A pending request on this run, or on an earlier run this one is queued + // behind, blocks it until someone answers. + const { thread } = yield* tools.call( + "t3_thread_read", + { threadId, limit: 1, runLimit: 1 }, + OrchestratorMcpThreadReadResult, + ); + if (thread.pendingRequestCount > 0) { + // The count is thread-wide: if this run ended since the slice, the + // request belongs to a later run. + const recheck = yield* waitSlice(1); + return recheck.timedOut ? ({ type: "needs_input" } satisfies WaitOutcome) : ended(recheck); + } + } +}); + +/** + * With --json the result always reaches stdout, so a script keeps the thread + * id even when the run fails. + */ +const finishRun = Effect.fn("cli.thread.finishRun")(function* ( + tools: ThreadTools, + input: { + readonly threadId: ThreadId; + readonly runId: RunId; + readonly json: boolean; + }, +) { + const outcome = yield* waitForRun(tools, input.threadId, input.runId); + const runId = outcome.type === "ended" ? (outcome.runId ?? input.runId) : input.runId; + const status = outcome.type === "ended" ? outcome.status : "waiting"; + const reply = + status === "completed" + ? yield* runResultText(tools, input.threadId, runId, "assistant_message") + : null; + const error = + status === "completed" || status === "waiting" + ? null + : yield* runResultText(tools, input.threadId, runId, "error"); + if (input.json) { + yield* printJson({ threadId: input.threadId, runId, status, reply, error }); + } else if (reply !== null) { + yield* Console.log(reply); + } + if (outcome.type === "needs_input") { + return yield* new ThreadNeedsInputError({ threadId: input.threadId }); + } + if (status !== "completed") { + return yield* new ThreadRunEndedError({ + threadId: input.threadId, + status, + ...(error === null ? {} : { error }), + }); + } +}); + +const titleFromMessage = (message: string) => { + const firstLine = message.trim().split("\n")[0]?.trim() ?? ""; + return firstLine.length > TITLE_MAX_CHARS + ? `${firstLine.slice(0, TITLE_MAX_CHARS - 1).trimEnd()}…` + : firstLine; +}; + +const launchWorkspaceStrategy = (input: { + readonly worktree: Option.Option; + readonly branch: Option.Option; +}) => + Option.match(input.worktree, { + onNone: () => + Option.isSome(input.branch) + ? Effect.fail(new ThreadUsageError({ detail: "--branch needs --worktree ." })) + : Effect.succeed(undefined), + onSome: (baseRef) => + Effect.succeed({ + type: "worktree" as const, + baseRef, + ...Option.match(input.branch, { onNone: () => ({}), onSome: (branch) => ({ branch }) }), + }), + }); + +const jsonFlag = Flag.Boolean("json").pipe( + Flag.withDescription("Print JSON instead of text."), + Flag.withDefault(false), +); +const waitFlag = Flag.Boolean("wait").pipe( + Flag.withDescription( + "Wait for the agent to finish and print its reply. Exits 1 if the run fails and 2 if it waits for an answer.", + ), + Flag.withDefault(false), +); +const messageArgument = Argument.String("message").pipe( + Argument.withDescription("Message for the agent, or - to read it from stdin."), +); +const threadArgument = Argument.String("thread").pipe(Argument.withDescription("Thread id.")); + +const decodeThreadIdValue = Schema.decodeUnknownEffect(ThreadId); +const decodeTitle = Schema.decodeUnknownEffect(TrimmedNonEmptyString); +const decodeThreadId = (value: string) => + decodeThreadIdValue(value.trim()).pipe( + Effect.mapError(() => new ThreadUsageError({ detail: `'${value}' is not a thread id.` })), + ); + +const threadListCommand = Command.make("list", { + ...projectLocationFlags, + project: Flag.String("project").pipe( + Flag.withDescription("Project id or a path inside the project. Defaults to this directory."), + Flag.withDefault("."), + ), + json: jsonFlag, +}).pipe( + Command.withDescription("List a project's threads, newest first."), + Command.withHandler((flags) => + withThreadTools( + flags, + Effect.fn("cli.thread.list")(function* (tools) { + const project = yield* resolveProject(tools, flags.project); + const threads: Array = []; + let cursor: number | null = 0; + while (cursor !== null) { + const page: OrchestratorMcpThreadListResult = yield* tools.call( + "t3_thread_list", + { projectId: project.id, includeSubagents: false, limit: 100, cursor }, + OrchestratorMcpThreadListResult, + ); + threads.push(...page.threads); + cursor = page.nextCursor; + } + if (flags.json) { + return yield* printJson(threads); + } + for (const thread of threads) { + yield* Console.log(`${thread.threadId} ${thread.status.padEnd(11)} ${thread.title}`); + } + }), + ).pipe(reportThreadErrors), + ), +); + +const threadStartCommand = Command.make("start", { + ...projectLocationFlags, + project: Argument.String("project").pipe( + Argument.withDescription("Project id or a path inside the project, such as ."), + ), + message: messageArgument, + title: Flag.String("title").pipe( + Flag.withDescription("Thread title. Defaults to the message's first line."), + Flag.optional, + ), + provider: Flag.String("provider").pipe( + Flag.withDescription("Provider instance id. Pass with --model."), + Flag.optional, + ), + model: Flag.String("model").pipe( + Flag.withDescription("Model slug. Defaults to the project's default model."), + Flag.optional, + ), + worktree: Flag.String("worktree").pipe( + Flag.withDescription( + "Run in a new git worktree based on this branch, tag, or commit, instead of the project folder.", + ), + Flag.optional, + ), + branch: Flag.String("branch").pipe( + Flag.withDescription("Branch name for the new worktree."), + Flag.optional, + ), + wait: waitFlag, + json: jsonFlag, +}).pipe( + Command.withDescription("Start a thread in a project and print its id."), + Command.withHandler((flags) => + Effect.gen(function* () { + const message = yield* readMessageArgument(flags.message); + const workspaceStrategy = yield* launchWorkspaceStrategy(flags); + if (Option.isSome(flags.provider) !== Option.isSome(flags.model)) { + return yield* new ThreadUsageError({ detail: "--provider and --model go together." }); + } + const title = yield* decodeTitle( + Option.getOrElse(flags.title, () => titleFromMessage(message)), + ).pipe(Effect.mapError(() => new ThreadUsageError({ detail: "The title is empty." }))); + yield* withThreadTools( + flags, + Effect.fn("cli.thread.start")(function* (tools) { + const project = yield* resolveProject(tools, flags.project); + const launched = yield* tools.call( + "t3_thread_launch", + { + projectId: project.id, + title, + message, + ...(workspaceStrategy === undefined ? {} : { workspaceStrategy }), + ...(Option.isSome(flags.provider) && Option.isSome(flags.model) + ? { + modelSelection: { + instanceId: flags.provider.value, + model: flags.model.value, + }, + } + : {}), + }, + ThreadLaunchResult, + ); + if (flags.wait && launched.runId !== null) { + if (!flags.json) yield* Console.error(`Thread ${launched.threadId}`); + return yield* finishRun(tools, { + threadId: launched.threadId, + runId: launched.runId, + json: flags.json, + }); + } + yield* flags.json ? printJson(launched) : Console.log(launched.threadId); + }), + ); + }).pipe(reportThreadErrors), + ), +); + +const threadSendCommand = Command.make("send", { + ...projectLocationFlags, + thread: threadArgument, + message: messageArgument, + wait: waitFlag, + json: jsonFlag, +}).pipe( + Command.withDescription( + "Send a message to a thread. A busy thread gets it as its next turn; nothing is interrupted.", + ), + Command.withHandler((flags) => + Effect.gen(function* () { + const threadId = yield* decodeThreadId(flags.thread); + const message = yield* readMessageArgument(flags.message); + yield* withThreadTools( + flags, + Effect.fn("cli.thread.send")(function* (tools) { + const sent = yield* tools.call( + "t3_thread_send", + { threadId, message, mode: "queue" }, + OrchestratorMcpThreadSendResult, + ); + if (flags.wait) { + return yield* finishRun(tools, { threadId, runId: sent.runId, json: flags.json }); + } + yield* flags.json + ? printJson(sent) + : Console.log(sent.delivery === "queued" ? "Queued." : "Sent."); + }), + ); + }).pipe(reportThreadErrors), + ), +); + +const threadShowCommand = Command.make("show", { + ...projectLocationFlags, + thread: threadArgument, + messages: Flag.Int("messages").pipe( + Flag.withDescription("How many of the latest messages to print."), + Flag.withDefault(10), + ), + json: jsonFlag, +}).pipe( + Command.withDescription("Show a thread's state and its latest messages."), + Command.withHandler((flags) => + Effect.gen(function* () { + const threadId = yield* decodeThreadId(flags.thread); + yield* withThreadTools( + flags, + Effect.fn("cli.thread.show")(function* (tools) { + const { thread, items } = yield* readTimeline(tools, threadId, "messages"); + const conversation = items.filter( + (item) => item.type === "user_message" || item.type === "assistant_message", + ); + const latest = conversation.slice(Math.max(0, conversation.length - flags.messages)); + const messages = yield* Effect.forEach(latest, (item) => + readFullText(tools, threadId, item).pipe( + Effect.map((text) => ({ + role: item.type === "user_message" ? "user" : "assistant", + runId: item.runId, + text, + })), + ), + ); + if (flags.json) { + return yield* printJson({ thread, messages }); + } + yield* Console.log(thread.title); + yield* Console.log( + [thread.threadId, thread.status, `${thread.providerInstanceId}/${thread.model}`].join( + " ", + ), + ); + if (thread.worktreePath !== null) { + yield* Console.log(`${thread.branch ?? "(no branch)"} ${thread.worktreePath}`); + } + if (thread.pendingRequestCount > 0) { + yield* Console.log(`Waiting for ${thread.pendingRequestCount} answer(s).`); + } + for (const entry of messages) { + yield* Console.log(`\n[${entry.role}]\n${entry.text}`); + } + }), + ); + }).pipe(reportThreadErrors), + ), +); + +export const threadCommand = Command.make("thread").pipe( + Command.withDescription("Start, message, and read threads on the running server."), + Command.withSubcommands([ + threadListCommand, + threadStartCommand, + threadSendCommand, + threadShowCommand, + ]), +); diff --git a/apps/server/src/mcp/AcpMcpStdioBridge.ts b/apps/server/src/mcp/AcpMcpStdioBridge.ts index 7338190ca56a..2db36ce5abc2 100644 --- a/apps/server/src/mcp/AcpMcpStdioBridge.ts +++ b/apps/server/src/mcp/AcpMcpStdioBridge.ts @@ -6,6 +6,7 @@ import * as NodeReadline from "node:readline"; import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; +import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import { discardResponseBody, responsePayloads } from "@t3tools/shared/mcpResponsePayloads"; @@ -14,7 +15,7 @@ import { discardResponseBody, responsePayloads } from "@t3tools/shared/mcpRespon * unevenly implemented. */ -class AcpMcpBridgeError extends Error { +export class AcpMcpBridgeError extends Error { readonly _tag = "AcpMcpBridgeError"; } @@ -53,94 +54,125 @@ function protocolVersionOf(entry: unknown): string | null { return typeof version === "string" && version.length > 0 ? version : null; } -export interface AcpMcpToolCallOptions { +export interface McpToolSessionOptions { readonly endpoint: string; readonly authorization: string; - readonly tool: string; - readonly arguments: Readonly>; + readonly clientName: string; readonly fetchImplementation?: (url: string, init?: RequestInit) => Promise; } +export interface McpToolSession { + /** Resolves to the JSON-RPC `result` of `tools/call`, a `CallToolResult`. */ + readonly call: ( + tool: string, + args: Readonly>, + ) => Effect.Effect; +} + /** - * This is the terminal fallback for ACP agents that accept `mcpServers` in - * `session/new` but fail to expose those tools to their model. Compliant ACP - * agents continue to use the stdio bridge above. + * One initialized MCP session over T3 Code's HTTP endpoint, terminated when + * the scope closes. Used by the ACP terminal fallback and `t3 thread`. */ -function callAcpMcpTool(options: AcpMcpToolCallOptions): Effect.Effect { +export const mcpToolSession = ( + options: McpToolSessionOptions, +): Effect.Effect => { const fetchImplementation = options.fetchImplementation ?? fetch; - return Effect.gen(function* () { - let sessionId: string | null = null; - let protocolVersion: string | null = null; + let sessionId: string | null = null; + let protocolVersion: string | null = null; + let nextId = 0; - const send = Effect.fn("AcpMcpStdioBridge.send")(function* ( - message: unknown, - ): Effect.fn.Return, AcpMcpBridgeError> { - const response = yield* Effect.tryPromise({ - try: () => - fetchImplementation(options.endpoint, { - method: "POST", - headers: { - "content-type": "application/json", - accept: "application/json, text/event-stream", - authorization: options.authorization, - ...(sessionId === null ? {} : { "mcp-session-id": sessionId }), - ...(protocolVersion === null ? {} : { "mcp-protocol-version": protocolVersion }), - }, - body: JSON.stringify(message), - }), - catch: bridgeError, - }); - sessionId = response.headers.get("mcp-session-id") ?? sessionId; - if (!response.ok) { - yield* discardResponseBody(response); - return yield* Effect.fail( - new AcpMcpBridgeError(`T3 Code MCP endpoint responded with HTTP ${response.status}.`), - ); - } - const payloads = yield* Stream.runCollect(responsePayloads(response, bridgeError)); - for (const payload of payloads) { - protocolVersion = protocolVersionOf(payload) ?? protocolVersion; - } - return payloads; - }); + const headers = () => ({ + accept: "application/json, text/event-stream", + authorization: options.authorization, + ...(sessionId === null ? {} : { "mcp-session-id": sessionId }), + ...(protocolVersion === null ? {} : { "mcp-protocol-version": protocolVersion }), + }); - const initializeId = "t3-acp-cli-initialize"; - const initialized = yield* send({ - jsonrpc: "2.0", - id: initializeId, - method: "initialize", - params: { - protocolVersion: MCP_PROTOCOL_VERSION, - capabilities: {}, - clientInfo: { name: "t3-code-acp-cli", version: "0.0.0" }, - }, + const send = Effect.fn("AcpMcpStdioBridge.send")(function* ( + message: unknown, + ): Effect.fn.Return, AcpMcpBridgeError> { + const response = yield* Effect.tryPromise({ + try: () => + fetchImplementation(options.endpoint, { + method: "POST", + headers: { "content-type": "application/json", ...headers() }, + body: JSON.stringify(message), + }), + catch: bridgeError, }); - const initializeResponse = initialized.find((entry) => asEnvelope(entry)?.id === initializeId); - if (initializeResponse === undefined || asEnvelope(initializeResponse)?.error !== undefined) { + sessionId = response.headers.get("mcp-session-id") ?? sessionId; + if (!response.ok) { + yield* discardResponseBody(response); return yield* Effect.fail( - new AcpMcpBridgeError("T3 Code MCP endpoint rejected initialization."), + new AcpMcpBridgeError(`T3 Code MCP endpoint responded with HTTP ${response.status}.`), ); } - yield* send({ jsonrpc: "2.0", method: "notifications/initialized" }); + const payloads = yield* Stream.runCollect(responsePayloads(response, bridgeError)); + for (const payload of payloads) { + protocolVersion = protocolVersionOf(payload) ?? protocolVersion; + } + return payloads; + }); - const callId = "t3-acp-cli-tool-call"; - const responses = yield* send({ - jsonrpc: "2.0", - id: callId, - method: "tools/call", - params: { name: options.tool, arguments: options.arguments }, - }); - const response = responses.find((entry) => asEnvelope(entry)?.id === callId); - const envelope = asEnvelope(response); + const request = Effect.fn("AcpMcpStdioBridge.request")(function* ( + method: string, + params: unknown, + ): Effect.fn.Return { + const id = `t3-cli-${nextId++}`; + const responses = yield* send({ jsonrpc: "2.0", id, method, params }); + const envelope = asEnvelope(responses.find((entry) => asEnvelope(entry)?.id === id)); if (envelope === null || envelope.error !== undefined) { return yield* Effect.fail( new AcpMcpBridgeError( - `T3 Code MCP tool call failed${envelope?.error === undefined ? "." : `: ${JSON.stringify(envelope.error)}`}`, + `T3 Code MCP ${method} failed${envelope?.error === undefined ? "." : `: ${JSON.stringify(envelope.error)}`}`, ), ); } - return envelope.result; + return envelope; + }); + + const terminate = Effect.suspend(() => + sessionId === null + ? Effect.void + : Effect.tryPromise(() => + fetchImplementation(options.endpoint, { method: "DELETE", headers: headers() }), + ).pipe(Effect.flatMap(discardResponseBody), Effect.ignore), + ); + + // Registered before the handshake so a session the server opened is ended + // even when the handshake fails, and the handshake stays interruptible. + return Effect.gen(function* () { + yield* Effect.addFinalizer(() => terminate); + yield* request("initialize", { + protocolVersion: MCP_PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { name: options.clientName, version: "0.0.0" }, + }); + yield* send({ jsonrpc: "2.0", method: "notifications/initialized" }); + return { + call: (tool, args) => + request("tools/call", { name: tool, arguments: args }).pipe( + Effect.map((envelope) => envelope.result), + ), + } satisfies McpToolSession; }); +}; + +/** + * This is the terminal fallback for ACP agents that accept `mcpServers` in + * `session/new` but fail to expose those tools to their model. Compliant ACP + * agents continue to use the stdio bridge above. + */ +function callAcpMcpTool(options: { + readonly endpoint: string; + readonly authorization: string; + readonly tool: string; + readonly arguments: Readonly>; +}): Effect.Effect { + return mcpToolSession({ ...options, clientName: "t3-code-acp-cli" }).pipe( + Effect.flatMap((session) => session.call(options.tool, options.arguments)), + Effect.scoped, + ); } export function runAcpMcpStdioBridge(options: AcpMcpStdioBridgeOptions): Effect.Effect { diff --git a/apps/server/src/mcp/toolkits/project/handlers.test.ts b/apps/server/src/mcp/toolkits/project/handlers.test.ts index aecc99f7dda2..4c22393bf476 100644 --- a/apps/server/src/mcp/toolkits/project/handlers.test.ts +++ b/apps/server/src/mcp/toolkits/project/handlers.test.ts @@ -4,6 +4,8 @@ import { ProjectId, ProviderInstanceId, type Project as ProjectRecord, + type ModelSelection, + type RuntimeMode, } from "@t3tools/contracts"; import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; @@ -28,7 +30,10 @@ import * as ProjectHandlers from "./handlers.ts"; import { ProjectToolkit } from "./tools.ts"; const clientLaunchHarness = (input: { - readonly runtimeModeCeiling: "approval-required" | "auto-accept-edits" | "auto" | "full-access"; + readonly runtimeModeCeiling: RuntimeMode; + readonly defaultRuntimeMode?: RuntimeMode; + /** Where Settings saves a project's model; the legacy project field stays null. */ + readonly overrideModel?: ModelSelection; readonly launched: Array; readonly workspaceRoot?: string; readonly checkWorktreeBase?: ThreadLaunch.ThreadLaunchService["Service"]["checkWorktreeBase"]; @@ -39,7 +44,19 @@ const clientLaunchHarness = (input: { model: "claude-opus", }; const layerDependencies = Layer.mergeAll( - ServerSettings.layerTest(), + ServerSettings.layerTest({ + ...(input.defaultRuntimeMode === undefined + ? {} + : { defaultRuntimeMode: input.defaultRuntimeMode }), + ...(input.overrideModel === undefined + ? {} + : { + projectSettingsFolded: true, + projectSettingsOverrides: { + [projectId]: { defaultModelSelection: input.overrideModel }, + }, + }), + }), NodeCrypto.layer, Layer.succeed(McpInvocationContext.McpInvocationContext, { environmentId: EnvironmentId.make("environment"), @@ -79,7 +96,7 @@ const clientLaunchHarness = (input: { ? Option.some({ id, workspaceRoot: input.workspaceRoot ?? "/projects/client-target", - defaultModelSelection: modelSelection, + defaultModelSelection: input.overrideModel === undefined ? modelSelection : null, } as unknown as ProjectRecord) : Option.none(), ), @@ -99,35 +116,85 @@ const clientLaunchHarness = (input: { return { projectId, modelSelection, dependencies: layerDependencies }; }; -it.effect("a client launches at its ceiling with the project's default model", () => - Effect.gen(function* () { - const launched: Array = []; - const { projectId, modelSelection, dependencies } = clientLaunchHarness({ - runtimeModeCeiling: "auto-accept-edits", - launched, - }); - const toolkit = yield* ProjectToolkit.pipe( - Effect.provide( - McpToolAccess.HandlersLayer.layer(ProjectHandlers.layer).pipe(Layer.provide(dependencies)), - ), - ); - const handle = (params: Parameters>[1]) => - toolkit - .handle("t3_thread_launch", params) - .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(dependencies)); +it.effect( + "a client launches with the project's default model and mode, capped at its ceiling", + () => + Effect.gen(function* () { + const launchWith = Effect.fn("test.launchWith")(function* ( + runtimeModeCeiling: RuntimeMode, + defaultRuntimeMode: RuntimeMode, + params: { + readonly title: string; + readonly message?: string; + readonly runtimeMode?: RuntimeMode; + readonly targeted?: false; + readonly overrideModel?: ModelSelection; + }, + ) { + const { targeted, overrideModel, ...input } = params; + const launched: Array = []; + const { projectId, modelSelection, dependencies } = clientLaunchHarness({ + runtimeModeCeiling, + defaultRuntimeMode, + launched, + ...(overrideModel === undefined ? {} : { overrideModel }), + }); + const toolkit = yield* ProjectToolkit.pipe( + Effect.provide( + McpToolAccess.HandlersLayer.layer(ProjectHandlers.layer).pipe( + Layer.provide(dependencies), + ), + ), + ); + const result = yield* toolkit + .handle("t3_thread_launch", targeted === false ? input : { ...input, projectId }) + .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(dependencies)); + return { result: result.at(-1)?.result, launched, projectId, modelSelection }; + }); - const result = yield* handle({ title: "Fix", projectId, message: "Fix the bug" }); - expect(result.at(-1)?.result).toMatchObject({ projectId, modelSelection }); - expect(launched[0]?.runtimeMode).toBe("auto-accept-edits"); - expect(launched[0]?.initialMessage?.senderThreadId).toBeUndefined(); + const belowCeiling = yield* launchWith("full-access", "approval-required", { + title: "Fix", + message: "Fix the bug", + }); + expect(belowCeiling.result).toMatchObject({ + projectId: belowCeiling.projectId, + modelSelection: belowCeiling.modelSelection, + }); + expect(belowCeiling.launched[0]?.runtimeMode).toBe("approval-required"); + expect(belowCeiling.launched[0]?.initialMessage?.senderThreadId).toBeUndefined(); - const escalated = yield* handle({ title: "Fix", projectId, runtimeMode: "full-access" }); - expect(escalated.at(-1)?.result).toMatchObject({ code: "runtime_mode_escalation_denied" }); + const aboveCeiling = yield* launchWith("auto-accept-edits", "full-access", { title: "Fix" }); + expect(aboveCeiling.launched[0]?.runtimeMode).toBe("auto-accept-edits"); - const untargeted = yield* handle({ title: "Fix" }); - expect(untargeted.at(-1)?.result).toMatchObject({ code: "target_required" }); - expect(launched).toHaveLength(1); - }), + const requested = yield* launchWith("full-access", "approval-required", { + title: "Fix", + runtimeMode: "auto", + }); + expect(requested.launched[0]?.runtimeMode).toBe("auto"); + + const escalated = yield* launchWith("auto-accept-edits", "auto-accept-edits", { + title: "Fix", + runtimeMode: "full-access", + }); + expect(escalated.result).toMatchObject({ code: "runtime_mode_escalation_denied" }); + expect(escalated.launched).toHaveLength(0); + + const untargeted = yield* launchWith("full-access", "full-access", { + title: "Fix", + targeted: false, + }); + expect(untargeted.result).toMatchObject({ code: "target_required" }); + + const overrideModel = { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-settings", + }; + const fromSettings = yield* launchWith("full-access", "full-access", { + title: "Fix", + overrideModel, + }); + expect(fromSettings.launched[0]?.modelSelection).toEqual(overrideModel); + }), ); it.effect("a launch binds only an existing checkout that is one of the project's worktrees", () => diff --git a/apps/server/src/mcp/toolkits/project/handlers.ts b/apps/server/src/mcp/toolkits/project/handlers.ts index 912d6ad127fc..380a1b52ec96 100644 --- a/apps/server/src/mcp/toolkits/project/handlers.ts +++ b/apps/server/src/mcp/toolkits/project/handlers.ts @@ -9,6 +9,9 @@ import * as Project from "../../../project/ProjectService.ts"; import * as ManagedProjectFolders from "../../../project/ManagedProjectFolders.ts"; import * as Repositories from "../../../sourceControl/SourceControlRepositoryService.ts"; import * as GitVcsDriver from "../../../vcs/GitVcsDriver.ts"; +import * as ServerSettings from "../../../serverSettings.ts"; +import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; +import { resolveRuntimeMode } from "../../OrchestratorMcpService.ts"; import * as McpToolAccess from "../../McpToolAccess.ts"; import { newCommandId, readCaller, resolveProjectId, unavailable } from "../../threadAccess.ts"; import { ProjectToolkit } from "./tools.ts"; @@ -122,10 +125,26 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, { }), ), ); + // A caller outside a thread has nothing to inherit, so it starts with the + // project's effective defaults, as the composer does. Its mode stays + // within the ceiling it was approved with. + const projectDefaults = + caller === undefined + ? yield* ServerSettings.ServerSettingsService.pipe( + Effect.flatMap((settings) => settings.getSettings), + Effect.mapError(unavailable), + Effect.flatMap((settings) => + Effect.map( + readProject, + (project) => resolveProjectSettings(settings, projectId, project).settings, + ), + ), + ) + : undefined; const modelSelection = input.modelSelection ?? caller?.modelSelection ?? - (yield* readProject)?.defaultModelSelection ?? + projectDefaults?.defaultModelSelection ?? undefined; if (modelSelection === undefined) return yield* new OrchestratorMcpFailure({ @@ -133,13 +152,19 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, { message: "Pass modelSelection: the project has no default model. orchestrator_capabilities lists providers and models.", }); + const startedRuntimeMode = + projectDefaults !== undefined && input.runtimeMode === undefined + ? yield* resolveRuntimeMode(runtimeMode, projectDefaults.defaultRuntimeMode).pipe( + Effect.orElseSucceed(() => runtimeMode), + ) + : runtimeMode; const result = yield* ThreadMessageIntake.launchThread({ commandId, threadId, projectId, title: input.title, modelSelection, - runtimeMode, + runtimeMode: startedRuntimeMode, interactionMode, workspaceStrategy: input.workspaceStrategy ?? { type: "root" }, ...(input.message === undefined && attachments.length === 0 diff --git a/docs/user/outside-agents.md b/docs/user/outside-agents.md index 7cc6692e38f6..1e7b80cde94c 100644 --- a/docs/user/outside-agents.md +++ b/docs/user/outside-agents.md @@ -88,6 +88,28 @@ Any agent that supports remote MCP servers over HTTP with OAuth can connect. Giv it the MCP URL and choose OAuth, and the agent finds the sign-in settings itself. Agents without OAuth support cannot connect. +## Scripts on the host + +On the machine that runs the environment, `t3 thread` uses the same tools +without a sign-in: + +```sh +id=$(t3 thread start . "Find why the tests are slow") +t3 thread send "$id" "Now fix it" --wait +t3 thread show "$id" +t3 thread list +``` + +`.` means the project that contains the current folder; a project id also +works. A new thread uses the project's default model and permission mode, and +runs in the project folder unless you pass `--worktree `. `send` +never interrupts: a busy thread gets the message as its next turn. Pass `-` as +the message to read it from stdin. + +`--wait` prints the agent's reply. It exits 1 when the run fails or stops, and +2 when the agent waits for an answer you give in the app. Add `--json` for a +result that always includes the thread id. + ## Remove an agent Approved agents appear under **Settings → Connections** like other clients. From 7781e5209bde99e7dbaeb87b0209a99eef3c2ebf Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 21 Oct 2026 01:24:32 -0700 Subject: [PATCH 24/73] refactor(web): composer attachment tiles live in their own component (#22689) Co-authored-by: Claude Opus 5.5 --- apps/web/src/components/chat/ChatComposer.tsx | 495 ++-------------- .../chat/ComposerAttachmentStrip.tsx | 535 ++++++++++++++++++ 2 files changed, 589 insertions(+), 441 deletions(-) create mode 100644 apps/web/src/components/chat/ComposerAttachmentStrip.tsx diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index ad8c6603ec1f..0d50637dd8a5 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -6,15 +6,12 @@ import { runtimeModeConfig, runtimeModeOptions as runtimeModes } from "./runtime import { isLocalEnvironmentDisabled } from "../../localEnvironment"; import { usePrimaryEnvironmentId } from "../../state/environments"; import { useRightPanelStore } from "~/rightPanelStore"; -import { AttachmentFilePreview } from "../files/AttachmentFilePreview"; -import { Dialog, DialogPopup, DialogTitle } from "../ui/dialog"; import { composerPullRequestsFromLinks, filterComposerPullRequestMatches, } from "@t3tools/shared/composerPullRequestMatches"; import { importPastedComposerText, readPastedComposerContext } from "../composerInlineTokenPaste"; import { elementContextToPreviewAnnotation } from "../../lib/elementContext"; -import { RefreshIcon } from "~/components/ui/refresh-icon"; import { questionAttachmentDraftId, countQuestionAttachments, @@ -41,7 +38,6 @@ import type { ScopedThreadRef, ServerProvider, ThreadId, - SnapShotSource, } from "@t3tools/contracts"; import { EnvironmentRequestInvalidError, @@ -69,7 +65,6 @@ import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model" import { USAGE_LIMITS_COMMAND } from "@t3tools/shared/usageLimits"; import { memo, - type ComponentProps, type ReactNode, useEffect, useEffectEvent, @@ -110,7 +105,6 @@ import { makeComposerMentionDragHandlers, } from "./composerMentionDrag"; import { - composerFloatingLayerProps, useComposerMenuProps, isInsideCollapsedComposerControls, isInsideRestingComposerControlScope, @@ -189,15 +183,11 @@ import { releaseAttachmentUpload, releaseDraftAttachment, releasePersistedAttachmentUpload, - retryAttachmentUpload, startAttachmentUpload, useAttachmentUploadStore, verifyStashedAttachmentUpload, } from "../../lib/attachmentUploadQueue"; -import { - attachmentUploadBlockReason, - formatAttachmentUploadProgress, -} from "../../lib/attachmentUploadState"; +import { attachmentUploadBlockReason } from "../../lib/attachmentUploadState"; import { isCommandPaletteOpen } from "../../commandPaletteBus"; import { getTerminalFocusOwner } from "../../lib/terminalFocus"; import type { AssistantCitationSourceAnchor } from "~/lib/assistantTextSelection"; @@ -297,6 +287,11 @@ import { import { ComposerPendingApprovalActions } from "./ComposerPendingApprovalActions"; import { CompactComposerControlsMenu } from "./CompactComposerControlsMenu"; import { ComposerImageThumbnail } from "./ComposerImageThumbnail"; +import { + ComposerAttachmentFileList, + ComposerAttachmentMediaRow, + ComposerFilePreviewDialog, +} from "./ComposerAttachmentStrip"; import { ComposerPrimaryActions } from "./ComposerPrimaryActions"; import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel"; import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel"; @@ -331,21 +326,13 @@ import { shouldReserveContextWindowMeter, } from "./ContextWindowMeter.logic"; import { - attachVideoThumbnail, buildAttachmentVideoPreview, buildExpandedImagePreview, type ExpandedImagePreview, } from "./ExpandedImagePreview"; -import { - SNAP_SHOT_ATTACHMENT_FRAME_CLASS, - SnapShotAttachmentDetails, -} from "./SnapShotAttachmentDetails"; import { getPendingSnapShotAnimations, pendingSnapShotAnimationIdsForTarget, - scheduleSnapShotAnimationDestination, - setSnapShotAnimationDestination, - shouldAnimateSnapShotArrival, subscribeToPendingSnapShotAnimations, } from "../../lib/snapShotAnimation"; import { resizeSnapShotSource } from "../../lib/snapShotSource"; @@ -371,28 +358,8 @@ import { resetComposerScrollGesture, suppressActiveComposerScrollGesture, } from "./composerScrollGesture"; -import { prepareVideoFirstFrame } from "../../lib/videoFirstFrame"; import { observeResize } from "~/lib/observeResize"; -function ComposerVideoThumbnail({ file }: { file: File }) { - const setVideo = (video: HTMLVideoElement | null) => { - if (!video) return; - return attachVideoThumbnail(video, file); - }; - - return ( -