Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/publish-ee-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ on:
- "packages/email/**"
- "packages/types/**"
- "packages/ui/**"
- "packages/ui-artifact-mcp/**"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "package.json"
Expand Down
27 changes: 27 additions & 0 deletions apps/app/src/app/lib/den.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import {
normalizeDesktopConfig,
type DesktopConfig as SharedDesktopConfig,
} from "@openwork/types/den/desktop-policies";
import {
uiArtifactPreferencesSchema,
type UiArtifactPreferences,
type UiArtifactPreferencesUpdate,
} from "@openwork/types/ui-artifact";

// Re-export the shared schema under the local alias so React consumers
// (e.g. the cloud domain's desktop-config provider) can import it alongside
Expand Down Expand Up @@ -2228,6 +2233,28 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
return normalizeDenDesktopConfig(payload);
},

async getUiArtifactPreferences(orgId: string): Promise<UiArtifactPreferences> {
const payload = await requestJson<unknown>(baseUrls, "/v1/me/ui-artifacts", {
method: "GET",
token,
organizationId: orgId,
});
return uiArtifactPreferencesSchema.parse(payload);
},

async updateUiArtifactPreferences(
orgId: string,
update: UiArtifactPreferencesUpdate,
): Promise<UiArtifactPreferences> {
const payload = await requestJson<unknown>(baseUrls, "/v1/me/ui-artifacts", {
method: "PUT",
token,
organizationId: orgId,
body: update,
});
return uiArtifactPreferencesSchema.parse(payload);
},

async getResourceSnapshot(orgId?: string | null): Promise<DenResourceSnapshot> {
const payload = await requestJson<unknown>(baseUrls, "/v1/resources", {
method: "GET",
Expand Down
47 changes: 39 additions & 8 deletions apps/app/src/components/chat/message-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { OpenWorkSessionCreateTool } from "@/components/tools/openwork-session-c
import { QuestionTool } from "@/components/tools/question"
import { SkillTool } from "@/components/tools/skill"
import { TodoWriteTool } from "@/components/tools/todowrite"
import { UiArtifactCard } from "@/components/tools/ui-artifact"
import { WebfetchTool } from "@/components/tools/webfetch"
import { WebsearchTool } from "@/components/tools/websearch"
import { useMessageList, useSessionErrorMessage } from "@/components/chat/message-list-provider"
Expand Down Expand Up @@ -107,6 +108,13 @@ import {
import { faviconUrlForHref } from "@/lib/favicon"
import { cn } from "@/lib/utils"
import { groupMessages, isMessageGroup, getLastTextPart, getAggregateOnlyParts, getAssistantRenderGroups, getFileTitle, getMediaBadge, getMessageCompleted, getMessageCreated, formatMessageTimestamp, splitTurnAtAnswer, type UIMessageWithIndex, getMessagesText, getSafeFileDownloadUrl, getSafeFileRevealPath } from "./utils"
import { useUiArtifactPreferencesSnapshot } from "@/react-app/domains/settings/state/feature-flags-preferences"
import {
buildUiArtifactDecisionPrompt,
isUiArtifactRenderInvocation,
parseUiArtifactRenderResult,
reconcileUiArtifactMessages,
} from "@/lib/ui-artifacts"
import type { AnyToolPart } from "@/lib/tool-aggregate"

const SEARCH_HIGHLIGHT_MARK_CLASS = "rounded px-0.5 bg-amber-4/70 text-current"
Expand Down Expand Up @@ -164,7 +172,25 @@ class ToolMessage extends React.Component<ToolMessageProps, { failed: boolean }>
}

const ToolMessageInner = ({ part }: ToolMessageProps) => {
const { onMcpReconnect, onMcpReopenAuthorization, onMcpRetry } = useMessageList()
const { onMcpReconnect, onMcpReopenAuthorization, onMcpRetry, setPrompt } = useMessageList()
const { uiArtifactsEnabled, enabledUiArtifactIds } = useUiArtifactPreferencesSnapshot()

if (
uiArtifactsEnabled &&
part.type === "dynamic-tool" &&
part.state === "output-available" &&
isUiArtifactRenderInvocation(part.toolName, part.input)
) {
const result = parseUiArtifactRenderResult(part.output)
if (result && enabledUiArtifactIds.includes(result.artifact.artifactId)) {
return (
<UiArtifactCard
result={result}
onRequestDecision={(action) => setPrompt(buildUiArtifactDecisionPrompt(action))}
/>
)
}
}

if (isBashToolPart(part)) {
return <BashTool part={part} />
Expand Down Expand Up @@ -1144,33 +1170,38 @@ export function shouldShowMessageListLoading(status: ThreadStatus, messageCount:

export function MessageList({ messages, status, retryStatus }: MessageListProps) {
const isStreaming = status === "streaming" || status === "retrying"
const { uiArtifactsEnabled } = useUiArtifactPreferencesSnapshot()
const visibleMessages = React.useMemo(
() => uiArtifactsEnabled ? reconcileUiArtifactMessages(messages) : messages,
[messages, uiArtifactsEnabled],
)
const showLoading = shouldShowMessageListLoading(status, messages.length)
const items = React.useMemo(() => groupMessages(messages, status), [messages, status]);
const items = React.useMemo(() => groupMessages(visibleMessages, status), [visibleMessages, status]);
const error = useSessionErrorMessage();
const hasSessionErrorMessage = React.useMemo(() => messages.some(isSessionErrorMessage), [messages])
const hasSessionErrorMessage = React.useMemo(() => visibleMessages.some(isSessionErrorMessage), [visibleMessages])
const liveActionLabel = isStreaming
? getActiveToolLabel(collectToolParts(messages))
? getActiveToolLabel(collectToolParts(visibleMessages))
: null

return (
<div className={cn("flex flex-col gap-2 @container/message-list")}>
{messages.length === 0 && <TaskSuggestions className="mx-auto w-full max-w-3xl shrink-0 px-3 pb-3 md:px-5 md:pb-5 grow" />}
{visibleMessages.length === 0 && <TaskSuggestions className="mx-auto w-full max-w-3xl shrink-0 px-3 pb-3 md:px-5 md:pb-5 grow" />}

{items.map((item) => {
if (isMessageGroup(item)) {
return (
<MessageGroup
key={item.messages[0]?.message.id ?? "empty-assistant-group"}
items={item.messages}
messages={messages}
messages={visibleMessages}
isStreaming={isStreaming}
/>
)
}

const isLastMessage = item.index === messages.length - 1
const isLastMessage = item.index === visibleMessages.length - 1
const isLastStep =
!messages[item.index + 1] || messages[item.index + 1].role !== item.message.role
!visibleMessages[item.index + 1] || visibleMessages[item.index + 1].role !== item.message.role

return (
<div key={item.message.id}>
Expand Down
Loading
Loading