Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,17 +15,17 @@ class AgentRunQueryPersistence(
workspaceId,
id,
).firstOrNull()
fun chatExists(workspaceId: UUID, chatId: UUID): Boolean = sqlExecutor.queryForObject(
fun sessionExists(workspaceId: UUID, sessionId: UUID): Boolean = sqlExecutor.queryForObject(
"select exists(select 1 from work_sessions where workspace_id = ? and id = ?)",
Boolean::class.java,
workspaceId,
chatId,
sessionId,
) ?: false
fun listChatAgentRuns(workspaceId: UUID, chatId: UUID): List<AgentRunRecord> = sqlExecutor.query(
selectAgentRunSql + " where a.workspace_id = ? and a.work_session_id = ? and a.origin = 'CHAT' order by a.created_at, a.id",
fun listSessionAgentRuns(workspaceId: UUID, sessionId: UUID): List<AgentRunRecord> = sqlExecutor.query(
selectAgentRunSql + " where a.workspace_id = ? and a.work_session_id = ? order by a.created_at, a.id",
agentRunMapper,
workspaceId,
chatId,
sessionId,
)
fun findChatAgentRunByIdempotencyKey(
workspaceId: UUID,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -185,11 +185,11 @@ class ChatAgentAdmissionService(
return run.toChatResponseFor(agentRunQueryPersistence)
}

fun listForChat(chatId: UUID): List<ChatAgentRunResponse> {
if (!agentRunQueryPersistence.chatExists(devContext.devWorkspaceId, chatId)) {
fun listForSession(sessionId: UUID): List<ChatAgentRunResponse> {
if (!agentRunQueryPersistence.sessionExists(devContext.devWorkspaceId, sessionId)) {
throw ApiException(HttpStatus.NOT_FOUND, "NOT_FOUND", "Chat not found")
}
return agentRunQueryPersistence.listChatAgentRuns(devContext.devWorkspaceId, chatId)
return agentRunQueryPersistence.listSessionAgentRuns(devContext.devWorkspaceId, sessionId)
.map { it.toChatResponseFor(agentRunQueryPersistence) }
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ class WorkSessionController(
}

@GetMapping("/{id}/agent-runs")
fun listAgentRuns(@PathVariable id: UUID): List<ChatAgentRunResponse> = chatAgentAdmissionService.listForChat(id)
fun listAgentRuns(@PathVariable id: UUID): List<ChatAgentRunResponse> = chatAgentAdmissionService.listForSession(id)

@PostMapping
fun create(@RequestBody request: CreateWorkSessionRequest): WorkSessionResponse {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -223,6 +223,14 @@ class AgentRunWorkerIntegrationTest {
routinePersistence.find(routine.workspaceId, routine.id)?.activityCursorSequence,
)
assertEquals(1, count("select count(*) from content_packs where generation_run_id = ?", artifactWorkflowRunId))
val routineChatId = jdbcTemplate.queryForObject(
"select work_session_id from agent_runs where id = ?",
UUID::class.java,
agentRunId,
)!!
val historyRuns = chatAdmission.listForSession(routineChatId)
assertEquals(listOf(agentRunId), historyRuns.map { it.id })
assertNotNull(historyRuns.single().artifactId)
assertEquals(3, agentModel.requests.size)
assertTrue(agentModel.requests.none { request ->
request.toString().contains("MUTATED SECRET") || request.toString().contains("Authorization")
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/app/(app)/layout.tsx
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
import "@astryxdesign/core/astryx.css";

import type { ReactNode } from "react";

import { ProductShell } from "@/components/layout/product-shell";
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/app/globals.css
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
@import 'tailwindcss';
@layer reset, theme, base, astryx-base, astryx-theme, components, utilities;

@import 'tailwindcss/theme.css' layer(theme);
@import 'tailwindcss/preflight.css' layer(base);
@import '@astryxdesign/core/reset.css';
@import '@astryxdesign/core/tailwind-theme.css';
@import 'tailwindcss/utilities.css' layer(utilities);

@custom-variant dark (&:is(.dark *));

Expand Down
1 change: 1 addition & 0 deletions apps/web/src/app/layout.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,7 @@ export default function RootLayout({
return (
<html
lang="en"
data-theme="light"
className={`${instrumentSans.variable} ${instrumentSerif.variable} ${jetbrainsMono.variable} h-full antialiased`}
>
<body className="min-h-full">{children}</body>
Expand Down
36 changes: 28 additions & 8 deletions apps/web/src/components/layout/product-shell.test.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
// @vitest-environment jsdom

import { render, screen } from "@testing-library/react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";

const navigation = vi.hoisted(() => ({ pathname: "/artifacts" }));
Expand All@@ -10,19 +10,20 @@ vi.mock("next/navigation", () => ({
}));

vi.mock("@/components/layout/product-sidebar", () => ({
ProductSidebar: () => null,
ProductSidebar: ({ onThemeChange }: { onThemeChange: (theme: "system" | "light" | "dark") => void }) => (
<div>
<button type="button" onClick={() => onThemeChange("light")}>Use light theme</button>
<button type="button" onClick={() => onThemeChange("dark")}>Use dark theme</button>
</div>
),
}));

import { ProductShell } from "./product-shell";

describe("ProductShell mobile navigation", () => {
describe("ProductShell", () => {
beforeEach(() => {
navigation.pathname = "/artifacts";
vi.stubGlobal("matchMedia", () => ({
matches: false,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}));
document.documentElement.dataset.theme = "light";
});

it("links directly to workspace Settings and marks Artifacts as current", () => {
Expand DownExpand Up@@ -53,4 +54,23 @@ describe("ProductShell mobile navigation", () => {
expect(screen.getByRole("link", { name: "Chat" })).not.toHaveAttribute("aria-current");
expect(screen.getByRole("link", { name: "Artifacts" })).not.toHaveAttribute("aria-current");
});

it("keeps the document theme in sync with the product theme", async () => {
const { unmount } = render(
<ProductShell>
<div>Content</div>
</ProductShell>,
);

fireEvent.click(screen.getByRole("button", { name: "Use dark theme" }));
await waitFor(() => expect(document.documentElement.dataset.theme).toBe("dark"));

fireEvent.click(screen.getByRole("button", { name: "Use light theme" }));
await waitFor(() => expect(document.documentElement.dataset.theme).toBe("light"));

fireEvent.click(screen.getByRole("button", { name: "Use dark theme" }));
await waitFor(() => expect(document.documentElement.dataset.theme).toBe("dark"));
unmount();
expect(document.documentElement.dataset.theme).toBe("light");
});
});
10 changes: 10 additions & 0 deletions apps/web/src/components/layout/product-shell.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,16 @@ export function ProductShell({ children }: { children: ReactNode }) {

const darkMode = theme === "dark" || (theme === "system" && systemDark);

useEffect(() => {
document.documentElement.dataset.theme = darkMode ? "dark" : "light";
}, [darkMode]);

useEffect(() => {
return () => {
document.documentElement.dataset.theme = "light";
};
}, []);

return (
<div className={darkMode ? "dark" : undefined}>
<div className="flex min-h-dvh bg-[#eef0f3] text-[#18181b] dark:bg-[#202126] dark:text-[#f4f4f5] lg:h-dvh lg:overflow-hidden">
Expand Down
16 changes: 12 additions & 4 deletions apps/web/src/features/artifacts/artifact-document-surface.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ type ArtifactDocumentSurfaceProps = {
onDraftChange?: (draft: Omit<SaveArtifactInput, "expectedRevisionNumber">) => void;
onSaveArtifact: (input: SaveArtifactInput) => Promise<Artifact>;
onPackChange: (pack: Artifact) => void;
presentation?: "panel" | "canvas";
presentation?: "panel" | "canvas" | "workspace";
saveRequestToken?: number;
};

Expand All@@ -35,10 +35,18 @@ export function ArtifactDocumentSurface({
const readOnly = Boolean(historical);
const shownPack = historical?.artifact ?? pack;

if (presentation === "canvas") {
if (presentation === "canvas" || presentation === "workspace") {
const workspacePresentation = presentation === "workspace";
return (
<article aria-label="Artifact document surface" className="min-h-[min(956px,calc(100dvh-128px))] w-full max-w-[980px] overflow-hidden rounded-[8px] border border-black/10 bg-white px-[clamp(28px,7.35vw,72px)] pb-[52px] pt-16 shadow-[0_8px_20px_rgba(0,0,0,0.08)] dark:border-white/10 dark:bg-[#202024]">
<h1 className="font-display text-[30px] leading-[38px] text-black/88 dark:text-white/90">{shownPack.title || "Generated artifact"}</h1>
<article
aria-label="Artifact document surface"
className={workspacePresentation
? "min-h-full w-full max-w-[980px] px-6 pb-16 text-black/88 dark:text-white/90"
: "min-h-[min(956px,calc(100dvh-128px))] w-full max-w-[980px] overflow-hidden rounded-[8px] border border-black/10 bg-white px-[clamp(28px,7.35vw,72px)] pb-[52px] pt-16 shadow-[0_8px_20px_rgba(0,0,0,0.08)] dark:border-white/10 dark:bg-[#202024]"}
>
{!workspacePresentation ? (
<h1 className="font-display text-[30px] leading-[38px] text-black/88 dark:text-white/90">{shownPack.title || "Generated artifact"}</h1>
) : null}
<CitedDraftEditor
pack={shownPack}
embedded
Expand Down
Loading
Loading