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
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
/**
* ConfirmDialog styles
* Icon top-centered; content uses full width of a wide (landscape) modal.
*/
@use '../../styles/tokens' as *;

.confirm-dialog {
padding: $size-gap-4 $size-gap-5;
text-align: center;
min-width: 320px;
max-width: 480px;
display: flex;
flex-direction: column;
align-items: stretch;
width: 100%;
max-width: 560px;
box-sizing: border-box;
padding-top: $size-gap-16;

&__icon {
flex-shrink: 0;
width: 48px;
height: 48px;
margin: 0 auto $size-gap-3;
margin: 0 auto $size-gap-4;
display: flex;
align-items: center;
justify-content: center;
Expand Down Expand Up @@ -44,14 +49,20 @@
}

&__content {
margin-bottom: $size-gap-4;
flex: 1;
min-width: 0;
margin: 0;
padding: 0 $size-gap-6 $size-gap-4;
}

&__title {
font-size: $font-size-lg;
font-weight: $font-weight-semibold;
line-height: 1.35;
letter-spacing: -0.01em;
color: var(--color-text-primary);
margin: 0 0 $size-gap-2 0;
margin: 0 0 $size-gap-3;
text-align: center;
}

&__title--compact {
Expand All @@ -60,13 +71,65 @@

&__message {
font-size: $font-size-sm;
line-height: 1.55;
color: var(--color-text-secondary);
text-align: left;

> *:first-child {
margin-top: 0;
}

> *:last-child {
margin-bottom: 0;
}
}

/** Lead line before bullet list (e.g. “This will:”) */
&__message-intro {
margin: 0 0 $size-gap-3;
font-size: $font-size-sm;
font-weight: $font-weight-medium;
line-height: 1.5;
color: var(--color-text-primary);
}

/** Structured bullet list inside confirm body */
&__bullet-list {
list-style: none;
margin: 0;
padding: $size-gap-3 $size-gap-5;
border-radius: $size-radius-base;
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
}

&__bullet-list li {
position: relative;
margin: 0;
padding-left: $size-gap-5;
font-size: $font-size-sm;
line-height: 1.55;
color: var(--color-text-secondary);

&::before {
content: '';
position: absolute;
left: 2px;
top: 0.55em;
width: 6px;
height: 6px;
border-radius: 50%;
background: color-mix(in srgb, var(--color-text-secondary) 45%, transparent);
}
}

&__bullet-list li + li {
margin-top: $size-gap-2;
}

&__preview {
margin-top: $size-gap-3;
padding: $size-gap-2 $size-gap-3;
padding: $size-gap-3 $size-gap-4;
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
border-radius: $size-radius-base;
Expand All @@ -91,12 +154,11 @@
gap: $size-gap-2;
justify-content: flex-end;
align-items: center;
width: 100%;
box-sizing: border-box;
padding-inline: $size-gap-3;
padding: $size-gap-4 $size-gap-6 $size-gap-5;
border-top: 1px solid var(--color-border);

.btn {
min-width: 80px;
min-width: 88px;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Supports both controlled usage and imperative calls
*/

import React, { useEffect, useRef } from 'react';
import React, { useEffect, useId, useRef } from 'react';
import { useI18n } from '@/infrastructure/i18n';
import { Modal } from '../Modal/Modal';
import { Button } from '../Button/Button';
Expand Down Expand Up @@ -64,6 +64,7 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
previewMaxHeight = 200,
}) => {
const { t } = useI18n('components');
const titleId = useId();
const hasMessage = message !== null && message !== undefined && message !== '';

// Resolve i18n default values
Expand Down Expand Up @@ -94,22 +95,29 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
<Modal
isOpen={isOpen}
onClose={handleCancel}
size="small"
size="medium"
showCloseButton={false}
>
<div className={`confirm-dialog confirm-dialog--${type}`}>
<div className="confirm-dialog__icon">
<div className="confirm-dialog__icon" aria-hidden>
{iconMap[type]}
</div>

<div className="confirm-dialog__content">
<h3 className={`confirm-dialog__title${hasMessage ? '' : ' confirm-dialog__title--compact'}`}>{title}</h3>
<h3
className={`confirm-dialog__title${hasMessage ? '' : ' confirm-dialog__title--compact'}`}
id={titleId}
>
{title}
</h3>
{hasMessage ? (
<div className="confirm-dialog__message">{message}</div>
<div className="confirm-dialog__message" role="region" aria-labelledby={titleId}>
{message}
</div>
) : null}

{preview && (
<div
<div
className="confirm-dialog__preview"
style={{ maxHeight: previewMaxHeight }}
>
Expand Down
4 changes: 2 additions & 2 deletions src/web-ui/src/features/ssh-remote/ConfirmDialog.scss
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* Confirm Dialog Styles
* SSH remote confirmation modal (scoped block — avoid clashing with component-library .confirm-dialog)
*/

.confirm-dialog {
.ssh-remote-confirm-dialog {
padding: 8px 0;

&__warning {
Expand Down
8 changes: 4 additions & 4 deletions src/web-ui/src/features/ssh-remote/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
size="small"
showCloseButton
>
<div className="confirm-dialog">
<div className="ssh-remote-confirm-dialog">
{destructive && (
<div className="confirm-dialog__warning">
<div className="ssh-remote-confirm-dialog__warning">
<AlertTriangle size={20} />
<span>{title}</span>
</div>
)}
<p className="confirm-dialog__message">{message}</p>
<div className="confirm-dialog__actions">
<p className="ssh-remote-confirm-dialog__message">{message}</p>
<div className="ssh-remote-confirm-dialog__actions">
<Button variant="secondary" onClick={onCancel}>
{cancelText || t('actions.cancel')}
</Button>
Expand Down
31 changes: 21 additions & 10 deletions src/web-ui/src/flow_chat/components/TurnRollbackButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { snapshotAPI } from '@/infrastructure/api';
import { notificationService } from '@/shared/notification-system';
import { confirmDanger } from '@/component-library';
import { createLogger } from '@/shared/utils/logger';
import './TurnRollbackButton.scss';

Expand All @@ -19,20 +21,27 @@ export const TurnRollbackButton: React.FC<TurnRollbackButtonProps> = ({
isCurrent,
onRollbackComplete,
}) => {
const { t } = useTranslation('flow-chat');
const [loading, setLoading] = useState(false);

const handleRollback = async () => {
if (isCurrent || loading) return;

// In Tauri, window.confirm is async and must be awaited.
const confirmed = await window.confirm(
`Roll back to before turn ${turnIndex + 1}?\n\n` +
`This will:\n` +
`• Restore files to the state before turn ${turnIndex + 1}\n` +
`• Undo all file changes from turn ${turnIndex + 1} onward\n` +
`• Keep the session history (you can roll forward again anytime)`

const index = turnIndex + 1;
const confirmed = await confirmDanger(
t('message.rollbackPanelDialogTitle', { index }),
(
<>
<p className="confirm-dialog__message-intro">{t('message.rollbackPanelDialogIntro')}</p>
<ul className="confirm-dialog__bullet-list">
<li>{t('message.rollbackPanelBulletRestore', { index })}</li>
<li>{t('message.rollbackPanelBulletUndo', { index })}</li>
<li>{t('message.rollbackPanelBulletHistory')}</li>
</ul>
</>
)
);

if (!confirmed) return;

setLoading(true);
Expand Down Expand Up @@ -66,7 +75,9 @@ export const TurnRollbackButton: React.FC<TurnRollbackButtonProps> = ({

} catch (error) {
log.error('Rollback failed', { sessionId, turnIndex, error });
notificationService.error(`Rollback failed: ${error}`);
notificationService.error(
`${t('message.rollbackFailed')}: ${error instanceof Error ? error.message : String(error)}`
);
} finally {
setLoading(false);
}
Expand Down
18 changes: 10 additions & 8 deletions src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,10 @@

}

// Expanded state.
// Expanded state (outer scroll area).
.user-message-item--expanded {
max-height: 60vh;
overflow-y: auto;

.user-message-item__content {
white-space: pre-wrap;
overflow: visible;
text-overflow: unset;
max-height: none;
}
}

// Failed state.
Expand All @@ -101,6 +94,15 @@
}
}

// Expanded message body: after --failed so multiline / whitespace always apply when open.
.user-message-item--expanded .user-message-item__content {
white-space: pre-wrap;
overflow: visible;
text-overflow: unset;
max-height: none;
overflow-wrap: break-word;
}

.user-message-item__copy-btn {
// Align with the IconButton ghost variant.
display: inline-flex;
Expand Down
22 changes: 19 additions & 3 deletions src/web-ui/src/flow_chat/components/modern/UserMessageItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { flowChatStore } from '../../store/FlowChatStore';
import { snapshotAPI } from '@/infrastructure/api';
import { notificationService } from '@/shared/notification-system';
import { globalEventBus } from '@/infrastructure/event-bus';
import { ReproductionStepsBlock, Tooltip } from '@/component-library';
import { ReproductionStepsBlock, Tooltip, confirmDanger } from '@/component-library';
import { createLogger } from '@/shared/utils/logger';
import './UserMessageItem.scss';

Expand Down Expand Up @@ -101,7 +101,19 @@ export const UserMessageItem = React.memo<UserMessageItemProps>(
e.stopPropagation();
if (!canRollback || !sessionId) return;

const confirmed = await window.confirm(t('message.rollbackConfirm', { index: turnIndex + 1 }));
const index = turnIndex + 1;
const confirmed = await confirmDanger(
t('message.rollbackDialogTitle', { index }),
(
<>
<p className="confirm-dialog__message-intro">{t('message.rollbackDialogIntro')}</p>
<ul className="confirm-dialog__bullet-list">
<li>{t('message.rollbackDialogBulletFiles')}</li>
<li>{t('message.rollbackDialogBulletHistory')}</li>
</ul>
</>
)
);
if (!confirmed) return;

setIsRollingBack(true);
Expand Down Expand Up @@ -181,7 +193,11 @@ export const UserMessageItem = React.memo<UserMessageItemProps>(
className="user-message-item__content"
onClick={handleToggleExpand}
title={(hasOverflow || expanded) ? (expanded ? t('message.clickToCollapse') : t('message.clickToExpand')) : undefined}
style={{ cursor: (hasOverflow || expanded) ? 'pointer' : 'text' }}
style={{
cursor: (hasOverflow || expanded) ? 'pointer' : 'text',
// Inline so newline preservation wins over any global/cascade overrides.
...(expanded ? { whiteSpace: 'pre-wrap' as const } : {}),
}}
>
{displayText}
</div>
Expand Down
Loading
Loading