feat: inject stop button to pages controlled by agent (#334)

* chore: baseline setup

* feat(agent): When the agent is running, right now we inject an orange glow. See the `apps/age

Task ID: TOiaMuDz

* fix: clean up agent storage

* fix: improve the stop button style

* fix: type issues with stopAgentStorage

---------

Co-authored-by: BrowserOS Coding Agent <coding-agent@browseros.com>
Co-authored-by: Dani Akash <DaniAkash@users.noreply.github.com>
This commit is contained in:
shivammittal274
2026-02-16 17:42:32 +05:30
committed by GitHub
parent 509451ac2f
commit 2c8c6f6120
6 changed files with 98 additions and 2 deletions

3
.gitignore vendored
View File

@@ -181,3 +181,6 @@ log.txt
# Testing iteration temp files
tmp/
# Coding agent artifacts
.agent/

View File

@@ -16,6 +16,7 @@ import {
syncScheduledJobs,
} from '@/lib/schedules/scheduleStorage'
import { searchActionsStorage } from '@/lib/search-actions/searchActionsStorage'
import { stopAgentStorage } from '@/lib/stop-agent/stop-agent-storage'
import { scheduledJobRuns } from './scheduledJobRuns'
export default defineBackground(() => {
@@ -69,6 +70,13 @@ export default defineBackground(() => {
url: chrome.runtime.getURL('app.html#/home'),
})
}
if (message?.type === 'stop-agent' && message?.conversationId) {
stopAgentStorage.setValue({
conversationId: message.conversationId,
timestamp: Date.now(),
})
}
})
sessionStorage.watch(async (newSession) => {

View File

@@ -2,10 +2,13 @@ import type { GlowMessage } from './GlowMessage'
const GLOW_OVERLAY_ID = 'browseros-glow-overlay'
const GLOW_STYLES_ID = 'browseros-glow-styles'
const GLOW_STOP_BTN_ID = 'browseros-glow-stop-btn'
const GLOW_THICKNESS = 1.0
const GLOW_OPACITY = 0.6
let activeConversationId: string | null = null
function injectStyles(): void {
if (document.getElementById(GLOW_STYLES_ID)) {
return
@@ -45,6 +48,11 @@ function injectStyles(): void {
to { opacity: ${GLOW_OPACITY}; }
}
@keyframes browseros-glow-btn-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
#${GLOW_OVERLAY_ID} {
position: fixed !important;
top: 0 !important;
@@ -59,6 +67,34 @@ function injectStyles(): void {
browseros-glow-pulse 3s ease-in-out infinite,
browseros-glow-fade-in 420ms cubic-bezier(0.22, 1, 0.36, 1) forwards !important;
}
#${GLOW_STOP_BTN_ID} {
position: fixed !important;
bottom: 24px !important;
left: 50% !important;
transform: translateX(-50%) !important;
width: 48px !important;
height: 48px !important;
border-radius: 50% !important;
background: rgba(220, 38, 38, 0.95) !important;
color: white !important;
border: none !important;
pointer-events: auto !important;
cursor: pointer !important;
z-index: 2147483647 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
line-height: 1 !important;
padding: 0 !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
opacity: 0;
animation: browseros-glow-btn-fade-in 420ms cubic-bezier(0.22, 1, 0.36, 1) forwards !important;
}
#${GLOW_STOP_BTN_ID}:hover {
background: rgba(185, 28, 28, 1) !important;
}
`
const appendStyle = () => document.head.appendChild(style)
@@ -76,6 +112,21 @@ function startGlow(): void {
const overlay = document.createElement('div')
overlay.id = GLOW_OVERLAY_ID
const button = document.createElement('button')
button.id = GLOW_STOP_BTN_ID
button.innerHTML =
'<svg width="16" height="16" viewBox="0 0 16 16" fill="white" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="14" height="14" rx="2"/></svg>'
button.addEventListener('click', () => {
if (activeConversationId) {
browser.runtime.sendMessage({
type: 'stop-agent',
conversationId: activeConversationId,
})
}
})
overlay.appendChild(button)
const appendOverlay = () => document.body.appendChild(overlay)
if (document.body) {
@@ -96,8 +147,6 @@ export default defineContentScript({
matches: ['*://*/*'],
runAt: 'document_start',
main() {
let activeConversationId: string | null = null
browser.runtime.onMessage.addListener(
(message: GlowMessage, _sender, sendResponse) => {
if (

View File

@@ -9,6 +9,7 @@ import { useAgentServerUrl } from '@/lib/browseros/useBrowserOSProviders'
import type { ChatAction } from '@/lib/chat-actions/types'
import {
CONVERSATION_RESET_EVENT,
GLOW_STOP_CLICKED_EVENT,
MESSAGE_DISLIKE_EVENT,
MESSAGE_LIKE_EVENT,
MESSAGE_SENT_EVENT,
@@ -23,6 +24,7 @@ import { useGraphqlQuery } from '@/lib/graphql/useGraphqlQuery'
import { useLlmProviders } from '@/lib/llm-providers/useLlmProviders'
import { track } from '@/lib/metrics/track'
import { searchActionsStorage } from '@/lib/search-actions/searchActionsStorage'
import { stopAgentStorage } from '@/lib/stop-agent/stop-agent-storage'
import { selectedWorkspaceStorage } from '@/lib/workspace/workspace-storage'
import type { ChatMode } from './chatTypes'
import { GetConversationWithMessagesDocument } from './graphql/chatSessionDocument'
@@ -400,6 +402,18 @@ export const useChatSession = () => {
return () => unwatch()
}, [])
// biome-ignore lint/correctness/useExhaustiveDependencies: only need to run this once
useEffect(() => {
const unwatch = stopAgentStorage.watch((signal) => {
if (signal && signal.conversationId === conversationIdRef.current) {
stop()
track(GLOW_STOP_CLICKED_EVENT)
stopAgentStorage.setValue(null)
}
})
return () => unwatch()
}, [])
const handleSelectProvider = (provider: Provider) => {
track(PROVIDER_SELECTED_EVENT, {
provider_id: provider.id,

View File

@@ -122,6 +122,9 @@ export const SIDEPANEL_MODE_CHANGED_EVENT = 'sidepanel.mode.changed'
/** @public */
export const SIDEPANEL_STOP_CLICKED_EVENT = 'sidepanel.generation.stopped'
/** @public */
export const GLOW_STOP_CLICKED_EVENT = 'glow.generation.stopped'
/** @public */
export const SIDEPANEL_MESSAGE_COPIED_EVENT = 'sidepanel.message.copied'

View File

@@ -0,0 +1,19 @@
import { storage } from '@wxt-dev/storage'
/**
* @public
*/
export interface StopAgentStorage {
conversationId: string
timestamp: number
}
/**
* @public
*/
export const stopAgentStorage = storage.defineItem<StopAgentStorage | null>(
'local:stop-agent',
{
fallback: null,
},
)