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
1 change: 1 addition & 0 deletions frontend/ui/src/components/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const icons = {
"window-cursor": `<path d="M17.9166 10.4167V3.75H2.08325V17.0833H10.4166M17.9166 13.5897L11.6666 11.6667L13.5897 17.9167L15.032 15.0321L17.9166 13.5897Z" stroke="currentColor" stroke-width="1.07143" stroke-linecap="square"/><path d="M5.00024 6.125C5.29925 6.12518 5.54126 6.36795 5.54126 6.66699C5.54108 6.96589 5.29914 7.20783 5.00024 7.20801C4.7012 7.20801 4.45843 6.966 4.45825 6.66699C4.45825 6.36784 4.70109 6.125 5.00024 6.125ZM7.91626 6.125C8.21541 6.125 8.45825 6.36784 8.45825 6.66699C8.45808 6.966 8.21531 7.20801 7.91626 7.20801C7.61736 7.20783 7.37542 6.96589 7.37524 6.66699C7.37524 6.36795 7.61726 6.12518 7.91626 6.125ZM10.8333 6.125C11.1324 6.125 11.3752 6.36784 11.3752 6.66699C11.3751 6.966 11.1323 7.20801 10.8333 7.20801C10.5342 7.20801 10.2914 6.966 10.2913 6.66699C10.2913 6.36784 10.5341 6.125 10.8333 6.125Z" fill="currentColor" stroke="currentColor" stroke-width="0.25" stroke-linecap="square"/>`,
task: `<path d="M9.99992 2.0835V17.9168M7.08325 3.75016H2.08325V16.2502H7.08325M12.9166 16.2502H17.9166V3.75016H12.9166" stroke="currentColor" stroke-linecap="square"/>`,
stop: `<rect x="5" y="5" width="10" height="10" fill="currentColor"/>`,
undo: `<path d="M7.08301 4.16699L3.33301 7.91699L7.08301 11.667M3.33301 7.91699H12.083C14.6143 7.91699 16.6663 9.96902 16.6663 12.5003C16.6663 15.0316 14.6143 17.0837 12.083 17.0837H7.49967" stroke="currentColor" stroke-linecap="square"/>`,
"layout-left": `<path d="M2.91675 2.91699L2.91675 2.41699L2.41675 2.41699L2.41675 2.91699L2.91675 2.91699ZM17.0834 2.91699L17.5834 2.91699L17.5834 2.41699L17.0834 2.41699L17.0834 2.91699ZM17.0834 17.0837L17.0834 17.5837L17.5834 17.5837L17.5834 17.0837L17.0834 17.0837ZM2.91675 17.0837L2.41675 17.0837L2.41675 17.5837L2.91675 17.5837L2.91675 17.0837ZM7.41674 17.0837L7.41674 17.5837L8.41674 17.5837L8.41674 17.0837L7.91674 17.0837L7.41674 17.0837ZM8.41674 2.91699L8.41674 2.41699L7.41674 2.41699L7.41674 2.91699L7.91674 2.91699L8.41674 2.91699ZM2.91675 2.91699L2.91675 3.41699L17.0834 3.41699L17.0834 2.91699L17.0834 2.41699L2.91675 2.41699L2.91675 2.91699ZM17.0834 2.91699L16.5834 2.91699L16.5834 17.0837L17.0834 17.0837L17.5834 17.0837L17.5834 2.91699L17.0834 2.91699ZM17.0834 17.0837L17.0834 16.5837L2.91675 16.5837L2.91675 17.0837L2.91675 17.5837L17.0834 17.5837L17.0834 17.0837ZM2.91675 17.0837L3.41675 17.0837L3.41675 2.91699L2.91675 2.91699L2.41675 2.91699L2.41675 17.0837L2.91675 17.0837ZM7.91674 17.0837L8.41674 17.0837L8.41674 2.91699L7.91674 2.91699L7.41674 2.91699L7.41674 17.0837L7.91674 17.0837Z" fill="currentColor"/>`,
"layout-left-partial": `<path d="M2.91732 2.91602L7.91732 2.91602L7.91732 17.0827H2.91732L2.91732 2.91602Z" fill="currentColor" fill-opacity="40%" /><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M2.91732 2.91602L7.91732 2.91602M17.084 2.91602L17.084 17.0827M17.084 2.91602L7.91732 2.91602M17.084 17.0827L2.91732 17.0827M17.084 17.0827L7.91732 17.0827M2.91732 17.0827H7.91732M7.91732 17.0827L7.91732 2.91602" stroke="currentColor" stroke-linecap="square"/>`,
"layout-left-full": `<path d="M2.91732 2.91602L7.91732 2.91602L7.91732 17.0827H2.91732L2.91732 2.91602Z" fill="currentColor"/><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M2.91732 2.91602L7.91732 2.91602M17.084 2.91602L17.084 17.0827M17.084 2.91602L7.91732 2.91602M17.084 17.0827L2.91732 17.0827M17.084 17.0827L7.91732 17.0827M2.91732 17.0827H7.91732M7.91732 17.0827L7.91732 2.91602" stroke="currentColor" stroke-linecap="square"/>`,
Expand Down
3 changes: 3 additions & 0 deletions frontend/ui/src/components/message-part.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@
position: absolute;
top: 7px;
right: 7px;
display: flex;
align-items: center;
gap: 4px;
opacity: 0;
transition: opacity 0.15s ease;
}
Expand Down
23 changes: 21 additions & 2 deletions frontend/ui/src/components/message-part.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ function DiagnosticsDisplay(props: { diagnostics: Diagnostic[] }): JSX.Element {
export interface MessageProps {
message: MessageType
parts: PartType[]
/** When set, user messages render an undo action that reverts the
* conversation (and file changes) back to this message. */
onRevert?: () => void
}

export interface MessagePartProps {
Expand Down Expand Up @@ -283,7 +286,9 @@ export function Message(props: MessageProps) {
return (
<Switch>
<Match when={props.message.role === "user" && props.message}>
{(userMessage) => <UserMessageDisplay message={userMessage() as UserMessage} parts={props.parts} />}
{(userMessage) => (
<UserMessageDisplay message={userMessage() as UserMessage} parts={props.parts} onRevert={props.onRevert} />
)}
</Match>
<Match when={props.message.role === "assistant" && props.message}>
{(assistantMessage) => (
Expand All @@ -307,7 +312,7 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part
return <For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For>
}

export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[]; onRevert?: () => void }) {
const dialog = useDialog()
const i18n = useI18n()
const [copied, setCopied] = createSignal(false)
Expand Down Expand Up @@ -424,6 +429,20 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
<Icon name="chevron-down" size="small" />
</button>
<div data-slot="user-message-copy-wrapper">
<Show when={props.onRevert}>
<Tooltip value={i18n.t("ui.message.revert")} placement="top" gutter={8}>
<IconButton
icon="undo"
variant="secondary"
onMouseDown={(e) => e.preventDefault()}
onClick={(event) => {
event.stopPropagation()
props.onRevert?.()
}}
aria-label={i18n.t("ui.message.revert")}
/>
</Tooltip>
</Show>
<Tooltip
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
placement="top"
Expand Down
9 changes: 8 additions & 1 deletion frontend/ui/src/components/session-turn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export function SessionTurn(
stepsExpanded?: boolean
onStepsExpandedToggle?: () => void
onUserInteracted?: () => void
onRevertMessage?: (messageID: string) => void
hideTools?: string[]
classes?: {
root?: string
Expand Down Expand Up @@ -572,7 +573,13 @@ export function SessionTurn(
<div data-slot="session-turn-sticky" ref={setStickyRef}>
{/* User Message */}
<div data-slot="session-turn-message-content" aria-live="off">
<Message message={msg()} parts={stickyParts()} />
<Message
message={msg()}
parts={stickyParts()}
onRevert={
props.onRevertMessage ? () => props.onRevertMessage?.(msg().id) : undefined
}
/>
</div>

{/* Trigger (sticky) */}
Expand Down
1 change: 1 addition & 0 deletions frontend/ui/src/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const dict = {
"ui.message.collapse": "طي الرسالة",
"ui.message.copy": "نسخ",
"ui.message.copied": "تم النسخ!",
"ui.message.revert": "التراجع من هنا",
"ui.message.attachment.alt": "مرفق",

"ui.patch.action.deleted": "محذوف",
Expand Down
1 change: 1 addition & 0 deletions frontend/ui/src/i18n/br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const dict = {
"ui.message.collapse": "Recolher mensagem",
"ui.message.copy": "Copiar",
"ui.message.copied": "Copiado!",
"ui.message.revert": "Desfazer a partir daqui",
"ui.message.attachment.alt": "anexo",

"ui.patch.action.deleted": "Excluído",
Expand Down
1 change: 1 addition & 0 deletions frontend/ui/src/i18n/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const dict = {
"ui.message.collapse": "Skjul besked",
"ui.message.copy": "Kopier",
"ui.message.copied": "Kopieret!",
"ui.message.revert": "Fortryd herfra",
"ui.message.attachment.alt": "vedhæftning",

"ui.patch.action.deleted": "Slettet",
Expand Down
1 change: 1 addition & 0 deletions frontend/ui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const dict = {
"ui.message.collapse": "Collapse message",
"ui.message.copy": "Copy",
"ui.message.copied": "Copied!",
"ui.message.revert": "Undo from here",
"ui.message.attachment.alt": "attachment",

"ui.patch.action.deleted": "Deleted",
Expand Down
1 change: 1 addition & 0 deletions frontend/ui/src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const dict = {
"ui.message.collapse": "Colapsar mensaje",
"ui.message.copy": "Copiar",
"ui.message.copied": "¡Copiado!",
"ui.message.revert": "Deshacer desde aquí",
"ui.message.attachment.alt": "adjunto",

"ui.patch.action.deleted": "Eliminado",
Expand Down
1 change: 1 addition & 0 deletions frontend/ui/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const dict = {
"ui.message.collapse": "Réduire le message",
"ui.message.copy": "Copier",
"ui.message.copied": "Copié !",
"ui.message.revert": "Annuler à partir d'ici",
"ui.message.attachment.alt": "pièce jointe",

"ui.patch.action.deleted": "Supprimé",
Expand Down
1 change: 1 addition & 0 deletions frontend/ui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const dict = {
"ui.message.collapse": "メッセージを折りたたむ",
"ui.message.copy": "コピー",
"ui.message.copied": "コピーしました!",
"ui.message.revert": "ここから元に戻す",
"ui.message.attachment.alt": "添付ファイル",

"ui.patch.action.deleted": "削除済み",
Expand Down
1 change: 1 addition & 0 deletions frontend/ui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const dict = {
"ui.message.collapse": "메시지 접기",
"ui.message.copy": "복사",
"ui.message.copied": "복사됨!",
"ui.message.revert": "여기서부터 실행 취소",
"ui.message.attachment.alt": "첨부 파일",

"ui.patch.action.deleted": "삭제됨",
Expand Down
1 change: 1 addition & 0 deletions frontend/ui/src/i18n/no.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const dict: Record<Keys, string> = {
"ui.message.collapse": "Skjul melding",
"ui.message.copy": "Kopier",
"ui.message.copied": "Kopiert!",
"ui.message.revert": "Angre herfra",
"ui.message.attachment.alt": "vedlegg",

"ui.patch.action.deleted": "Slettet",
Expand Down
1 change: 1 addition & 0 deletions frontend/ui/src/i18n/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const dict = {
"ui.message.collapse": "Zwiń wiadomość",
"ui.message.copy": "Kopiuj",
"ui.message.copied": "Skopiowano!",
"ui.message.revert": "Cofnij od tego miejsca",
"ui.message.attachment.alt": "załącznik",

"ui.patch.action.deleted": "Usunięto",
Expand Down
1 change: 1 addition & 0 deletions frontend/ui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const dict = {
"ui.message.collapse": "Свернуть сообщение",
"ui.message.copy": "Копировать",
"ui.message.copied": "Скопировано!",
"ui.message.revert": "Отменить с этого места",
"ui.message.attachment.alt": "вложение",

"ui.patch.action.deleted": "Удалено",
Expand Down
1 change: 1 addition & 0 deletions frontend/ui/src/i18n/th.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const dict = {
"ui.message.collapse": "ย่อข้อความ",
"ui.message.copy": "คัดลอก",
"ui.message.copied": "คัดลอกแล้ว!",
"ui.message.revert": "เลิกทำจากตรงนี้",
"ui.message.attachment.alt": "ไฟล์แนบ",

"ui.patch.action.deleted": "ลบ",
Expand Down
95 changes: 93 additions & 2 deletions frontend/workspace/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { FONT_MONO, FONT_SANS, FONT_SERIF, sectionTitle } from "@/styles/tokens"
import { uiStore } from "@/thesis/store/ui"
import { useGlobalKeys } from "@/thesis/useGlobalKeys"
import { useDialog } from "@synsci/ui/context/dialog"
import { confirmDialog } from "@/thesis/dialogs"
import { DialogSettings } from "@/components/dialog-settings"
import { DisconnectedPanel } from "@/thesis/DisconnectedPanel"
import { CommandPalette } from "@/thesis/CommandPalette"
Expand Down Expand Up @@ -182,7 +183,50 @@ export default function Page(): JSX.Element {
// each of the (often hundreds of) assistant messages paint an empty turn plus
// a divider, which stacked up as faint horizontal lines down the chat and
// bloated the DOM (slowing the reflow when the right pane opens).
const turnMessages = createMemo(() => messages().filter((m) => m.role === "user"))
// When the session is in a reverted state, turns at or past the revert point
// stay hidden until the user restores them or sends a new message (which
// makes the revert permanent server-side).
const activeSession = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const revertInfo = createMemo(() => activeSession()?.revert)
const turnMessages = createMemo(() => {
const revertID = revertInfo()?.messageID
return messages().filter((m) => m.role === "user" && (!revertID || m.id < revertID))
})
const revertedCount = createMemo(() => {
const revertID = revertInfo()?.messageID
if (!revertID) return 0
return messages().filter((m) => m.role === "user" && m.id >= revertID).length
})

const revertTo = async (messageID: string) => {
const id = params.id
if (!id) return
const ok = await confirmDialog(dialog, {
title: "Undo from here?",
message:
"Hides this message and everything after it, and rolls back the file changes they made. You can restore until you send the next message.",
confirmLabel: "undo",
danger: true,
})
if (!ok) return
try {
await sync.session.revert(id, messageID)
toast.success("reverted", "files rolled back. send a message to continue from here")
} catch (e: any) {
toast.error("undo failed", e?.message ?? String(e))
}
}

const restoreRevert = async () => {
const id = params.id
if (!id) return
try {
await sync.session.unrevert(id)
toast.success("messages restored")
} catch (e: any) {
toast.error("restore failed", e?.message ?? String(e))
}
}

const [stepsExpanded, setStepsExpanded] = createSignal<Record<string, boolean>>({})
const toggleSteps = (id: string) =>
Expand Down Expand Up @@ -380,6 +424,7 @@ export default function Page(): JSX.Element {
lastUserMessageID={lastUserMessage()?.id}
stepsExpanded={stepsExpanded()[message.id] ?? false}
onStepsExpandedToggle={() => toggleSteps(message.id)}
onRevertMessage={(id) => void revertTo(id)}
hideTools={["task"]}
classes={{
root: "min-w-0 w-full relative",
Expand Down Expand Up @@ -408,6 +453,46 @@ export default function Page(): JSX.Element {
</Match>
</Switch>

<Show when={revertInfo()}>
<div style={{ padding: "8px 16px 0" }}>
<div
style={{
display: "flex",
"align-items": "center",
gap: "12px",
padding: "8px 12px",
border: "1px solid var(--color-border)",
"border-radius": "6px",
"font-size": "12px",
"font-family": FONT_SANS,
color: "var(--color-text-secondary, var(--color-text))",
background: "var(--color-bg)",
}}
>
<span style={{ flex: 1, "min-width": 0 }}>
Conversation reverted. {revertedCount()} turn{revertedCount() === 1 ? "" : "s"} hidden and
file changes rolled back. Sending a new message makes this permanent.
</span>
<button
type="button"
onClick={() => void restoreRevert()}
style={{
border: "1px solid var(--color-border)",
background: "transparent",
color: "inherit",
padding: "4px 10px",
"border-radius": "4px",
"font-size": "12px",
cursor: "pointer",
"white-space": "nowrap",
}}
>
restore
</button>
</div>
</div>
</Show>

<Composer />
</div>

Expand Down Expand Up @@ -924,13 +1009,19 @@ function ChatWelcome(): JSX.Element {
cursor: "pointer",
padding: "5px 12px",
"border-radius": "4px",
border: uiStore.agent() === m.name ? "1px solid var(--color-text)" : "1px solid var(--color-border)",
border: uiStore.agent() === m.name ? "1px solid var(--color-border)" : "1px solid transparent",
background: uiStore.agent() === m.name ? "var(--color-accent-subtle)" : "transparent",
"font-family": FONT_MONO,
"font-size": "11px",
color: uiStore.agent() === m.name ? "var(--color-text)" : "var(--color-text-muted)",
transition: "border-color 120ms ease, background 120ms ease, color 120ms ease",
}}
onMouseEnter={(e) => {
if (uiStore.agent() !== m.name) e.currentTarget.style.background = "var(--color-bg-elevated)"
}}
onMouseLeave={(e) => {
if (uiStore.agent() !== m.name) e.currentTarget.style.background = "transparent"
}}
>
{m.name}
</button>
Expand Down
26 changes: 7 additions & 19 deletions frontend/workspace/src/styles/thesis.css
Original file line number Diff line number Diff line change
Expand Up @@ -164,16 +164,13 @@ html[data-color-scheme="dark"] {
}

/* ==========================================================
Base — softly textured ambient mesh.
Base — flat, quiet canvas. (The colored ambient mesh that used
to sit here read as a random gradient bleeding around the chat
and message boxes, so it's gone.)
========================================================== */
.thesis-root {
position: relative;
background: var(--color-bg);
background-image:
radial-gradient(at 8% -2%, rgba(99, 102, 241, 0.075), transparent 38%),
radial-gradient(at 98% 2%, rgba(249, 115, 22, 0.06), transparent 36%),
radial-gradient(at 50% 100%, rgba(34, 197, 94, 0.05), transparent 42%),
radial-gradient(at 0% 100%, rgba(59, 130, 246, 0.05), transparent 36%);
color: var(--color-text);
font-family: var(--font-sans);
letter-spacing: -0.011em;
Expand All @@ -182,19 +179,10 @@ html[data-color-scheme="dark"] {
font-feature-settings: "ss01", "ss02";
}

.dark .thesis-root,
html[data-color-scheme="dark"] .thesis-root {
background-image:
radial-gradient(at 8% -2%, rgba(99, 102, 241, 0.16), transparent 38%),
radial-gradient(at 98% 2%, rgba(249, 115, 22, 0.10), transparent 36%),
radial-gradient(at 50% 100%, rgba(34, 197, 94, 0.08), transparent 42%),
radial-gradient(at 0% 100%, rgba(59, 130, 246, 0.10), transparent 36%);
}

/* The ambient mesh above is kept; the 40px ruled grid overlay that used to
live in .thesis-root::before was removed — it read as distracting horizontal
lines in empty areas (e.g. the chat), and its full-viewport masked repaint
was costly during layout changes like opening the right pane. */
/* The 40px ruled grid overlay that used to live in .thesis-root::before was
removed — it read as distracting horizontal lines in empty areas (e.g. the
chat), and its full-viewport masked repaint was costly during layout changes
like opening the right pane. */

.thesis-root > * {
position: relative;
Expand Down
Loading
Loading