Skip to content
Merged
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
4 changes: 3 additions & 1 deletion src-tauri/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ pub async fn settings_set(
Ok(enabled) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review completed for this PR diff. No concrete inline issue was selected after aggregation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review completed for this PR diff. No concrete inline issue was selected after aggregation.

#[cfg(target_os = "macos")]
{
startup_manager::set_launch_at_login(enabled)?;
if let Err(error) = startup_manager::set_launch_at_login(enabled) {

This comment was marked as outdated.

tracing::warn!(error = %error, "failed to update launch at login via SMAppService (non-fatal)");
}
}

#[cfg(not(target_os = "macos"))]
Expand Down
5 changes: 5 additions & 0 deletions src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,11 @@ const en: Record<TranslationKey, string> = {
"worktree.error.selectBranch": "Please select a branch",
"contextCompressing": "Compressing context…",

// ── Artifact Card ─────────────────────────────────────────
"artifact.collapseCode": "Collapse code",
"artifact.expandCode": "Expand code",
"artifact.preview": "Preview",

// ── Long message preview ──────────────────────────────────
"longMessage.collapseAll": "Collapse",
"longMessage.expandAll": "Expand",
Expand Down
5 changes: 5 additions & 0 deletions src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,11 @@ const zhCN = {
"worktree.error.selectBranch": "请选择一个分支",
"contextCompressing": "正在压缩上下文…",

// ── Artifact Card ─────────────────────────────────────────
"artifact.collapseCode": "收起代码",
"artifact.expandCode": "展开代码",
"artifact.preview": "点击预览",

// ── Long message preview ──────────────────────────────────
"longMessage.collapseAll": "收起全文",
"longMessage.expandAll": "展开全文",
Expand Down
5 changes: 5 additions & 0 deletions src/modules/workbench-shell/ui/runtime-thread-surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,11 @@ export function RuntimeThreadSurface({
const message = error instanceof Error ? error.message : String(error);
setLoadError(message);
threadStore.setState({ runtimeContextUsage: null });
// Reset run machine so the optimistic "running" state doesn't permanently
// block the pending run effect when the snapshot IPC fails.
// Use "failed" rather than "idle" because Guard 2 in threadStore rejects
// idle/null writes when an optimistic running state with a real runId exists.
if (threadId) runMachine.reset("failed", { runId: null, errorMessage: message, retryCount: 0 });

This comment was marked as outdated.

setSnapshotReady(true);
setSnapshotThreadId(threadId);
} finally {
Expand Down
63 changes: 41 additions & 22 deletions src/modules/workbench-shell/ui/thread-chart-artifact-card.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Component, useEffect, useMemo, useRef, useState } from "react";
import type { ErrorInfo, ReactNode } from "react";
import { AlertCircleIcon, BarChart3Icon, CodeIcon, EyeIcon } from "lucide-react";
import { AlertCircleIcon, BarChart3Icon, ChevronsUpDownIcon, CodeIcon, EyeIcon } from "lucide-react";
import { useTheme } from "@/app/providers/theme-provider";
import { CodeBlock } from "@/components/ai-elements/code-block";
import { MessageResponse } from "@/components/ai-elements/message";
import type { SurfaceChartMessagePart } from "@/modules/workbench-shell/ui/runtime-thread-surface-state";
import { cn } from "@/shared/lib/utils";
import { validateSpec } from "@/modules/workbench-shell/ui/chart-spec-validation";
import { FilePreviewSurface } from "@/modules/workbench-shell/ui/file-preview-surface";
import { useT } from "@/i18n";

type ThreadChartArtifactCardProps = {
part: SurfaceChartMessagePart;
Expand Down Expand Up @@ -105,7 +106,8 @@ function VegaLiteRenderer({ spec }: { spec: unknown }) {
return <div ref={containerRef} className="w-full overflow-x-auto [&_.vega-embed]:!w-full" />;
}

function HtmlSvgRenderer({ source }: { source: string; library: string }) {
function HtmlSvgRenderer({ source, collapsed }: { source: string; collapsed: boolean }) {
if (collapsed) return null;
return (
<CodeBlock
code={source}
Expand All @@ -125,21 +127,28 @@ function ChartErrorFallback({ message }: { message: string }) {
}

export function ThreadChartArtifactCard({ part }: ThreadChartArtifactCardProps) {
const t = useT();
const isHtmlSvg = part.library === "html" || part.library === "svg";
const [showSpec, setShowSpec] = useState(false);
const [codeCollapsed, setCodeCollapsed] = useState(true);
const [previewOpen, setPreviewOpen] = useState(false);
const specText = useMemo(() => JSON.stringify(part.spec, null, 2), [part.spec]);
const validationError = !isHtmlSvg && part.status !== "loading" ? validateSpec(part.spec) : null;

return (
<>
<div className="overflow-hidden rounded-2xl border border-app-border/30 bg-app-surface/18 shadow-sm">
<div className="flex items-start justify-between gap-3 border-b border-app-border/20 px-4 py-3">
<div
className={cn(
"overflow-hidden rounded-2xl border shadow-sm",
"border-blue-400/25 bg-blue-50/40 dark:border-blue-500/20 dark:bg-blue-950/25",
)}
>
<div className="flex items-start justify-between gap-3 border-b border-blue-400/15 dark:border-blue-500/12 px-4 py-3">
<div className="min-w-0 space-y-1">
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.08em] text-app-subtle">
<BarChart3Icon className="size-3.5" />
<span>{getStatusLabel(part.status, part.library)}</span>
<span className="rounded-full border border-app-border/30 px-2 py-0.5 normal-case tracking-normal text-app-muted">
<span className="rounded-full border border-blue-400/25 dark:border-blue-500/20 px-2 py-0.5 normal-case tracking-normal text-app-muted">
{part.library}
</span>
</div>
Expand All @@ -150,18 +159,8 @@ export function ThreadChartArtifactCard({ part }: ThreadChartArtifactCardProps)
<p className="text-sm leading-6 text-app-muted">{part.caption}</p>
) : null}
</div>
<div className="flex shrink-0 items-center gap-1.5">
{isHtmlSvg && part.source ? (
<button
className="shrink-0 rounded-lg p-1.5 text-app-subtle transition-colors hover:bg-app-surface/50 hover:text-app-foreground"
onClick={() => setPreviewOpen(true)}
title="Preview"
type="button"
>
<EyeIcon className="size-4" />
</button>
) : null}
{!isHtmlSvg && (
{!isHtmlSvg && (
<div className="flex shrink-0 items-center gap-1.5">
<button
className="shrink-0 rounded-lg p-1.5 text-app-subtle transition-colors hover:bg-app-surface/50 hover:text-app-foreground"
onClick={() => setShowSpec((v) => !v)}
Expand All @@ -170,8 +169,8 @@ export function ThreadChartArtifactCard({ part }: ThreadChartArtifactCardProps)
>
{showSpec ? <EyeIcon className="size-4" /> : <CodeIcon className="size-4" />}
</button>
)}
</div>
</div>
)}
</div>

<div className="space-y-3 px-4 py-4">
Expand All @@ -184,12 +183,32 @@ export function ThreadChartArtifactCard({ part }: ThreadChartArtifactCardProps)
) : null}

{part.status === "loading" ? (
<div className="flex h-48 items-center justify-center rounded-xl border border-dashed border-app-border/30 bg-app-surface/35">
<div className="flex h-48 items-center justify-center rounded-xl border border-dashed border-blue-400/20 dark:border-blue-500/15 bg-blue-50/20 dark:bg-blue-950/15">
<span className="text-sm text-app-subtle animate-pulse">Generating…</span>
</div>
) : isHtmlSvg ? (
part.source ? (
<HtmlSvgRenderer source={part.source} library={part.library} />
<>
<HtmlSvgRenderer source={part.source} collapsed={codeCollapsed} />
<div className="flex items-center justify-between">
<button
className="flex items-center gap-1.5 rounded-lg px-2 py-1 text-xs text-app-subtle transition-colors hover:bg-app-surface/50 hover:text-app-foreground"
onClick={() => setCodeCollapsed((v) => !v)}
type="button"
>
<ChevronsUpDownIcon className="size-3.5" />
<span>{codeCollapsed ? t("artifact.expandCode") : t("artifact.collapseCode")}</span>
</button>
<button
className="flex items-center gap-1.5 rounded-lg px-2 py-1 text-xs text-app-subtle transition-colors hover:bg-app-surface/50 hover:text-app-foreground"
onClick={() => setPreviewOpen(true)}
type="button"
>
<EyeIcon className="size-3.5" />
<span>{t("artifact.preview")}</span>
</button>
</div>
</>
) : (
<ChartErrorFallback message="No source content available" />
)
Expand All @@ -211,7 +230,7 @@ export function ThreadChartArtifactCard({ part }: ThreadChartArtifactCardProps)
"rounded-xl border px-2 py-2",
part.status === "error"
? "border-app-danger/25 bg-app-danger/5"
: "border-app-border/20 bg-app-surface/35",
: "border-blue-400/15 dark:border-blue-500/12 bg-blue-50/15 dark:bg-blue-950/10",
)}
>
<VegaLiteRenderer spec={part.spec} />
Expand Down
Loading