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 src/chrome/src/ui/sidepanel.html
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ <h2 id="new-conversation-confirm-title" data-i18n="sp.clear.title">Start a new c
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<span data-i18n="sp.mode.act.warning"></span>
<button id="act-warning-dismiss" type="button" data-i18n-aria-label="sp.review.close" aria-label="Dismiss">×</button>
</div>

<div id="selection-scope-banner" class="selection-scope-banner hidden" role="region" aria-labelledby="selection-scope-title">
Expand Down
24 changes: 22 additions & 2 deletions src/chrome/src/ui/sidepanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ const modeToggleHighlight = (() => {
return el;
})();
const actWarning = document.getElementById('act-warning');
const actWarningDismiss = document.getElementById('act-warning-dismiss');
const inputArea = document.getElementById('input-area');
const slashCommandMenuEl = document.getElementById('slash-command-menu');
const queuedMessagesEl = document.getElementById('queued-messages');
Expand Down Expand Up @@ -1253,9 +1254,12 @@ chrome.storage.onChanged.addListener((changes) => {
// prompted per consequential action, so the standing banner is redundant —
// only surface it in Act mode when the gate is disabled.
const PERMISSION_GATE_KEY = 'askBeforeConsequentialActions';
const ACT_WARNING_DISMISSED_KEY = 'actWarningDismissed';
const PERMISSION_EDUCATION_KEY = 'permissionPromptEducation';
const PERMISSION_EDUCATION_THRESHOLD = 2;
let askBeforeConsequential = true; // gate ON by default
let actWarningDismissed = false;
let actWarningPreferenceLoaded = false;
let permissionEducationState = { promptCount: 0, hintShown: false };

function normalizePermissionEducationState(value) {
Expand Down Expand Up @@ -1365,8 +1369,10 @@ async function maybeShowPermissionEducationHint(card) {
scrollToBottom();
}

chrome.storage.local.get(PERMISSION_GATE_KEY).then((stored) => {
chrome.storage.local.get([PERMISSION_GATE_KEY, ACT_WARNING_DISMISSED_KEY]).then((stored) => {
if (stored && stored[PERMISSION_GATE_KEY] === false) askBeforeConsequential = false;
actWarningDismissed = stored?.[ACT_WARNING_DISMISSED_KEY] === true;
actWarningPreferenceLoaded = true;
updateActWarning();
updateInputPlaceholder();
}).catch(() => {});
Expand All @@ -1382,14 +1388,28 @@ chrome.storage.onChanged.addListener((changes) => {
updateActWarning();
updateInputPlaceholder();
}
if (changes[ACT_WARNING_DISMISSED_KEY]) {
actWarningDismissed = changes[ACT_WARNING_DISMISSED_KEY].newValue === true;
actWarningPreferenceLoaded = true;
updateActWarning();
}
});

function updateActWarning() {
if (!actWarning) return;
const show = agentMode !== 'ask' && !askBeforeConsequential;
const show = actWarningPreferenceLoaded
&& !actWarningDismissed
&& agentMode !== 'ask'
&& !askBeforeConsequential;
actWarning.classList.toggle('hidden', !show);
}

actWarningDismiss?.addEventListener('click', () => {
actWarningDismissed = true;
updateActWarning();
void chrome.storage.local.set({ [ACT_WARNING_DISMISSED_KEY]: true }).catch(() => {});
});

/**
* Play a short chime when the agent finishes a task. Lazy-creates the Audio
* element the first time and reuses it after that — sidepanel.html is an
Expand Down
36 changes: 33 additions & 3 deletions src/chrome/styles/sidepanel.css
Original file line number Diff line number Diff line change
Expand Up @@ -1219,12 +1219,11 @@ body {

.chat-navigation {
position: absolute;
left: 50%;
inset-inline-end: 12px;
bottom: 12px;
z-index: 7;
display: inline-block;
max-width: calc(100% - 36px);
transform: translateX(-50%);
transition: transform 0.15s, opacity 0.4s ease;
}

Expand Down Expand Up @@ -1256,7 +1255,7 @@ body {

.chat-navigation:hover,
.chat-navigation:focus-within {
transform: translate(-50%, -1px);
transform: translateY(-1px);
opacity: 1;
transition: transform 0.15s, opacity 0.12s ease;
}
Expand Down Expand Up @@ -2081,6 +2080,37 @@ body {
flex-shrink: 0;
}

#act-warning-dismiss {
display: grid;
place-items: center;
width: 30px;
height: 30px;
margin-block: -4px;
margin-inline-start: auto;
padding: 0;
border: 0;
border-radius: 6px;
background: transparent;
color: currentColor;
cursor: pointer;
font: inherit;
font-size: 22px;
font-weight: 400;
line-height: 1;
opacity: 0.72;
}

#act-warning-dismiss:hover {
background: color-mix(in srgb, var(--warning) 12%, transparent);
opacity: 1;
}

#act-warning-dismiss:focus-visible {
outline: 3px solid color-mix(in srgb, var(--warning) 34%, transparent);
outline-offset: 1px;
opacity: 1;
}

.selection-scope-banner {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
Expand Down
1 change: 1 addition & 0 deletions src/firefox/src/ui/sidepanel.html
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ <h2 id="new-conversation-confirm-title" data-i18n="sp.clear.title">Start a new c
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<span data-i18n="sp.mode.act.warning"></span>
<button id="act-warning-dismiss" type="button" data-i18n-aria-label="sp.review.close" aria-label="Dismiss">×</button>
</div>

<div id="selection-scope-banner" class="selection-scope-banner hidden" role="region" aria-labelledby="selection-scope-title">
Expand Down
24 changes: 22 additions & 2 deletions src/firefox/src/ui/sidepanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ const modeToggleHighlight = (() => {
return el;
})();
const actWarning = document.getElementById('act-warning');
const actWarningDismiss = document.getElementById('act-warning-dismiss');
const inputArea = document.getElementById('input-area');
const slashCommandMenuEl = document.getElementById('slash-command-menu');
const queuedMessagesEl = document.getElementById('queued-messages');
Expand Down Expand Up @@ -1347,9 +1348,12 @@ function isSuccessfulAskCompletion(mode, response) {
// prompted per consequential action, so the standing banner is redundant —
// only surface it in Act mode when the gate is disabled.
const PERMISSION_GATE_KEY = 'askBeforeConsequentialActions';
const ACT_WARNING_DISMISSED_KEY = 'actWarningDismissed';
const PERMISSION_EDUCATION_KEY = 'permissionPromptEducation';
const PERMISSION_EDUCATION_THRESHOLD = 2;
let askBeforeConsequential = true; // gate ON by default
let actWarningDismissed = false;
let actWarningPreferenceLoaded = false;
let permissionEducationState = { promptCount: 0, hintShown: false };

function normalizePermissionEducationState(value) {
Expand Down Expand Up @@ -1459,8 +1463,10 @@ async function maybeShowPermissionEducationHint(card) {
scrollToBottom();
}

browser.storage.local.get(PERMISSION_GATE_KEY).then((stored) => {
browser.storage.local.get([PERMISSION_GATE_KEY, ACT_WARNING_DISMISSED_KEY]).then((stored) => {
if (stored && stored[PERMISSION_GATE_KEY] === false) askBeforeConsequential = false;
actWarningDismissed = stored?.[ACT_WARNING_DISMISSED_KEY] === true;
actWarningPreferenceLoaded = true;
updateActWarning();
updateInputPlaceholder();
}).catch(() => {});
Expand All @@ -1476,14 +1482,28 @@ browser.storage.onChanged.addListener((changes) => {
updateActWarning();
updateInputPlaceholder();
}
if (changes[ACT_WARNING_DISMISSED_KEY]) {
actWarningDismissed = changes[ACT_WARNING_DISMISSED_KEY].newValue === true;
actWarningPreferenceLoaded = true;
updateActWarning();
}
});

function updateActWarning() {
if (!actWarning) return;
const show = agentMode !== 'ask' && !askBeforeConsequential;
const show = actWarningPreferenceLoaded
&& !actWarningDismissed
&& agentMode !== 'ask'
&& !askBeforeConsequential;
actWarning.classList.toggle('hidden', !show);
}

actWarningDismiss?.addEventListener('click', () => {
actWarningDismissed = true;
updateActWarning();
void browser.storage.local.set({ [ACT_WARNING_DISMISSED_KEY]: true }).catch(() => {});
});

// Per-tab chat history (stores innerHTML of messages container).
// Also mirrored to browser.storage.session keyed `tabChat:<tabId>` so the
// conversation survives the sidebar being closed and reopened.
Expand Down
36 changes: 33 additions & 3 deletions src/firefox/styles/sidepanel.css
Original file line number Diff line number Diff line change
Expand Up @@ -1097,12 +1097,11 @@ body {

.chat-navigation {
position: absolute;
left: 50%;
inset-inline-end: 12px;
bottom: 12px;
z-index: 7;
display: inline-block;
max-width: calc(100% - 36px);
transform: translateX(-50%);
transition: transform 0.15s, opacity 0.4s ease;
}

Expand Down Expand Up @@ -1134,7 +1133,7 @@ body {

.chat-navigation:hover,
.chat-navigation:focus-within {
transform: translate(-50%, -1px);
transform: translateY(-1px);
opacity: 1;
transition: transform 0.15s, opacity 0.12s ease;
}
Expand Down Expand Up @@ -1959,6 +1958,37 @@ body {
flex-shrink: 0;
}

#act-warning-dismiss {
display: grid;
place-items: center;
width: 30px;
height: 30px;
margin-block: -4px;
margin-inline-start: auto;
padding: 0;
border: 0;
border-radius: 6px;
background: transparent;
color: currentColor;
cursor: pointer;
font: inherit;
font-size: 22px;
font-weight: 400;
line-height: 1;
opacity: 0.72;
}

#act-warning-dismiss:hover {
background: color-mix(in srgb, var(--warning) 12%, transparent);
opacity: 1;
}

#act-warning-dismiss:focus-visible {
outline: 3px solid color-mix(in srgb, var(--warning) 34%, transparent);
outline-offset: 1px;
opacity: 1;
}

.selection-scope-banner {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
Expand Down
44 changes: 42 additions & 2 deletions test/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -26466,9 +26466,10 @@ test('sidepanel long replies use reading-first turn navigation', () => {
);
assert.match(
css,
/#chat-shell \{[\s\S]*?position: relative;[\s\S]*?min-height: 0;[\s\S]*?\.chat-navigation \{[\s\S]*?position: absolute;[\s\S]*?left: 50%;[\s\S]*?transform: translateX\(-50%\);/,
`${label}: navigation pill should be shell-positioned and RTL-safe`,
/#chat-shell \{[\s\S]*?position: relative;[\s\S]*?min-height: 0;[\s\S]*?\.chat-navigation \{[\s\S]*?position: absolute;[\s\S]*?inset-inline-end: 12px;/,
`${label}: navigation pill should sit quietly at the shell's logical trailing edge`,
);
assert.doesNotMatch(css, /\.chat-navigation \{[^}]*left: 50%;/, `${label}: navigation pill should no longer be centered over the reply`);
assert.match(
css,
/\.message\.assistant\.chat-navigation-inset \.message-content \{[\s\S]*?padding-block-end: 60px;/,
Expand Down Expand Up @@ -26673,6 +26674,45 @@ test('sidepanel long replies use reading-first turn navigation', () => {
}
});

test('Act-mode risk warning can be dismissed permanently', () => {
for (const [label, prefix, storage] of [
['chrome', 'src/chrome', 'chrome'],
['firefox', 'src/firefox', 'browser'],
]) {
const panel = fs.readFileSync(path.join(ROOT, prefix, 'src/ui/sidepanel.js'), 'utf8');
const html = fs.readFileSync(path.join(ROOT, prefix, 'src/ui/sidepanel.html'), 'utf8');
const css = fs.readFileSync(path.join(ROOT, prefix, 'styles/sidepanel.css'), 'utf8');

assert.match(
html,
/id="act-warning"[\s\S]*?data-i18n="sp\.mode\.act\.warning"[\s\S]*?id="act-warning-dismiss"[\s\S]*?data-i18n-aria-label="sp\.review\.close"/,
`${label}: the warning should keep its copy on the left and expose an accessible close button`,
);
assert.match(
css,
/#act-warning-dismiss \{[\s\S]*?width: 30px;[\s\S]*?height: 30px;[\s\S]*?margin-inline-start: auto;[\s\S]*?font-size: 22px;/,
`${label}: the warning close button should be prominent and pinned to the logical right`,
);
assert.match(css, /#act-warning-dismiss:focus-visible \{[\s\S]*?outline:/, `${label}: the warning close button should have a keyboard focus treatment`);
assert.match(panel, /const ACT_WARNING_DISMISSED_KEY = 'actWarningDismissed';/, `${label}: permanent dismissal should have one storage key`);
assert.match(
panel,
new RegExp(`${storage}\\.storage\\.local\\.get\\(\\[PERMISSION_GATE_KEY, ACT_WARNING_DISMISSED_KEY\\]\\)[\\s\\S]*?actWarningDismissed = stored\\?\\.\\[ACT_WARNING_DISMISSED_KEY\\] === true;[\\s\\S]*?actWarningPreferenceLoaded = true;`),
`${label}: warning visibility should wait for and restore the saved dismissal`,
);
assert.match(
panel,
/function updateActWarning\(\) \{[\s\S]*?actWarningPreferenceLoaded[\s\S]*?!actWarningDismissed[\s\S]*?agentMode !== 'ask'[\s\S]*?!askBeforeConsequential/,
`${label}: a dismissed warning should stay hidden across mode changes`,
);
assert.match(
panel,
new RegExp(`actWarningDismiss\\?\\.addEventListener\\('click',[\\s\\S]*?actWarningDismissed = true;[\\s\\S]*?updateActWarning\\(\\);[\\s\\S]*?${storage}\\.storage\\.local\\.set\\(\\{ \\[ACT_WARNING_DISMISSED_KEY\\]: true \\}\\)`),
`${label}: closing the warning should hide it immediately and persist forever`,
);
}
});

test('compact clarification results settle the pending pre-card tool step', () => {
for (const [label, panelRel] of [
['chrome', 'src/chrome/src/ui/sidepanel.js'],
Expand Down
Loading