Upvote BrowserOS launch

Upvote BrowserOS launch

bak

new agent prompt

confetti

bak -- confetti in new agent

bak

bak

bak

bak

bak
This commit is contained in:
Felarof
2025-09-19 13:29:17 -07:00
parent 53252043c3
commit 8a92fa24b6
8 changed files with 312 additions and 40 deletions

View File

@@ -17,6 +17,18 @@ export function generateSystemPrompt(toolDescriptions: string): string {
4. **NEVER** skip waiting for content to load
5. **NEVER** make assumptions without checking
## 📌 SPECIAL EXAMPLE TASKS
NOTE: When user says something like "Visit BrowserOS launch and upvote ❤️", execute these steps:
1. Navigate to https://dub.sh/browseros-launch
2. Click the upvote button
3. Use celebration_tool to show confetti
NOTE: When user says something like "Go to Github and Star BrowserOS ⭐", execute these steps:
1. Navigate to https://git.new/browserOS
2. Click the star button (only if it is looking gray -- i.e., not starred)
3. Use celebration_tool to show confetti (only if star was clicked)
## 🔄 EXECUTION WORKFLOW
### UNDERSTANDING YOUR TASK TYPE
The system automatically classifies tasks before you see them:

View File

@@ -65,6 +65,7 @@ import { createStorageTool } from '@/lib/tools/utils/StorageTool';
import { createExtractTool } from '@/lib/tools/extraction/ExtractTool';
import { createResultTool } from '@/lib/tools/result/ResultTool';
import { createHumanInputTool } from '@/lib/tools/utils/HumanInputTool';
import { createCelebrationTool } from '@/lib/tools/utils/CelebrationTool';
import { createDateTool } from '@/lib/tools/utility/DateTool';
import { createMCPTool } from '@/lib/tools/mcp/MCPTool';
import { generateSystemPrompt, generateSingleTurnExecutionPrompt } from './BrowserAgent.prompt';
@@ -171,6 +172,23 @@ export class BrowserAgent {
this.narrator?.cleanup();
}
/**
* Transform special example tasks into explicit instructions
* @param task - The original task string
* @returns The transformed task string
*/
private _transformSpecialTasks(task: string): string {
// Exact match for special example tasks
if (task === "Visit BrowserOS launch and upvote ❤️") {
return "Navigate to https://dub.sh/browseros-launch (it will redirect to the actual page) then click the upvote button then use celebration_tool to show confetti";
}
if (task === "Go to GitHub and Star BrowserOS ⭐") {
return "Navigate to https://git.new/browserOS (it will redirect to the actual page) then click the star button if it is gray (not starred) then use celebration_tool to show confetti";
}
// Return original task if not a special case
return task;
}
/**
* Main entry point.
* Orchestrates classification and delegates to the appropriate execution strategy.
@@ -178,15 +196,18 @@ export class BrowserAgent {
* @param metadata - Optional execution metadata for controlling execution mode
*/
async execute(task: string, metadata?: ExecutionMetadata): Promise<void> {
// Transform special example tasks into explicit instructions
const transformedTask = this._transformSpecialTasks(task);
try {
// 1. SETUP: Initialize system prompt and user task
this._initializeExecution(task);
this._initializeExecution(transformedTask);
// 2. CHECK FOR PREDEFINED PLAN
if (metadata?.executionMode === 'predefined' && metadata.predefinedPlan) {
// Treat predefined plan as a fresh (non-follow-up) task: clear history and re-init
this.messageManager.clear();
this._initializeExecution(task);
this._initializeExecution(transformedTask);
// Route predefined plan through the multi-step strategy using initial plan
const predefined = metadata!.predefinedPlan!;
this.pubsub.publishMessage(PubSub.createMessage(`Executing agent: ${predefined.name || 'Custom Agent'}`, 'thinking'));
@@ -195,19 +216,19 @@ export class BrowserAgent {
steps: predefined.steps.map(step => ({ action: step, reasoning: `Part of agent: ${predefined.name || 'Custom'}` }))
};
if (predefined.goal) {
this.messageManager.addHuman(`User's goal is: ${predefined.goal} and this is the task: ${task}`);
this.messageManager.addHuman(`User's goal is: ${predefined.goal} and this is the task: ${transformedTask}`);
}
await this._executeMultiStepStrategy(task, initialPlan);
await this._generateTaskResult(task);
await this._executeMultiStepStrategy(transformedTask, initialPlan);
await this._generateTaskResult(transformedTask);
return;
}
else if (metadata?.executionMode === 'dynamic' && metadata?.source === 'newtab') {
// For tasks initiated from new tab, show the startup message with task
this.pubsub.publishMessage(PubSub.createMessage(`Executing task: ${task}`, 'thinking'));
this.pubsub.publishMessage(PubSub.createMessage(`Executing task: ${transformedTask}`, 'thinking'));
}
// 3. STANDARD FLOW: CLASSIFY task type
const classification = await this._classifyTask(task);
const classification = await this._classifyTask(transformedTask);
// Log classification result to console for visibility
if (ENABLE_EVALS2) {
@@ -217,9 +238,9 @@ export class BrowserAgent {
// Clear message history if this is not a follow-up task
if (!classification.is_followup_task) {
this.messageManager.clear();
this._initializeExecution(task);
this._initializeExecution(transformedTask);
}
let message: string;
if (classification.is_followup_task && this.messageManager.getMessages().length > 0) {
message = 'Following up on previous task...';
@@ -232,17 +253,17 @@ export class BrowserAgent {
// 4. DELEGATE: Route to the correct execution strategy
if (classification.is_simple_task) {
await this._executeSimpleTaskStrategy(task);
await this._executeSimpleTaskStrategy(transformedTask);
} else {
await this._executeMultiStepStrategy(task);
await this._executeMultiStepStrategy(transformedTask);
}
// 5. FINALISE: Generate final result
await this._generateTaskResult(task);
await this._generateTaskResult(transformedTask);
// Task completion is logged by NxtScape, not here
} catch (error) {
this._handleExecutionError(error, task);
this._handleExecutionError(error, transformedTask);
} finally {
// Cleanup narrator service
this.narrator?.cleanup();
@@ -302,6 +323,7 @@ export class BrowserAgent {
this.toolManager.register(createStorageTool(this.executionContext));
this.toolManager.register(createExtractTool(this.executionContext));
this.toolManager.register(createHumanInputTool(this.executionContext));
this.toolManager.register(createCelebrationTool(this.executionContext));
this.toolManager.register(createDateTool(this.executionContext));
// Result tool

View File

@@ -36,6 +36,17 @@ CRITICAL OUTPUT RULES - NEVER VIOLATE THESE:
4. **NEVER** mention system reminders in your output
5. Browser state and system reminders are INTERNAL ONLY - treat them as invisible to the user
## 📌 SPECIAL EXAMPLE TASKS
NOTE: When user says something like "Visit BrowserOS launch and upvote ❤️", execute these steps:
1. Navigate to https://dub.sh/browseros-launch
2. Click the upvote button
3. Use celebration_tool to show confetti
NOTE: When user says something like "Go to Github and Star BrowserOS ⭐", execute these steps:
1. Navigate to https://git.new/browserOS
2. Click the star button (only if it is looking gray -- i.e., not starred)
3. Use celebration_tool to show confetti (only if star was clicked)
The browser state appears in <browser-state> tags for your internal reference to understand the page.
System reminders appear in <system-reminder> tags for your internal guidance.
</executor-mode>

View File

@@ -46,6 +46,7 @@ import {
createExtractTool,
createHumanInputTool,
createDoneTool,
createCelebrationTool,
createMoondreamVisualClickTool,
createMoondreamVisualTypeTool,
} from "@/lib/tools/NewTools";
@@ -258,8 +259,9 @@ export class NewAgent {
// Utility tools
this.toolManager.register(createExtractTool(this.executionContext));
this.toolManager.register(createHumanInputTool(this.executionContext));
this.toolManager.register(createCelebrationTool(this.executionContext)); // Celebration/confetti tool
this.toolManager.register(createDateTool(this.executionContext)); // Date/time utilities
// External integration tools
this.toolManager.register(createMCPTool(this.executionContext)); // MCP server integration
@@ -273,10 +275,30 @@ export class NewAgent {
);
}
/**
* Transform special example tasks into explicit instructions
* @param task - The original task string
* @returns The transformed task string
*/
private _transformSpecialTasks(task: string): string {
// Exact match for special example tasks
if (task === "Visit BrowserOS launch and upvote ❤️") {
return "Navigate to https://dub.sh/browseros-launch (it will redirect to the actual page) then click the upvote button then use celebration_tool to show confetti";
}
if (task === "Go to GitHub and Star BrowserOS ⭐") {
return "Navigate to https://git.new/browserOS (it will redirect to the actual page) then click the star button if it is gray (not starred) then use celebration_tool to show confetti";
}
// Return original task if not a special case
return task;
}
// There are basically two modes of operation:
// 1. Dynamic planning - the agent plans and executes in a loop until done
// 2. Predefined plan - the agent executes a predefined set of steps in a loop until all are done
async execute(task: string, metadata?: ExecutionMetadata): Promise<void> {
// Transform special example tasks into explicit instructions
const transformedTask = this._transformSpecialTasks(task);
try {
this.executionContext.setExecutionMetrics({
...this.executionContext.getExecutionMetrics(),
@@ -285,12 +307,12 @@ export class NewAgent {
Logging.log("NewAgent", `Starting execution`, "info");
await this._initialize();
// Check for predefined plan
if (metadata?.executionMode === 'predefined' && metadata.predefinedPlan) {
await this._executePredefined(task, metadata.predefinedPlan);
await this._executePredefined(transformedTask, metadata.predefinedPlan);
} else {
await this._executeDynamic(task);
await this._executeDynamic(transformedTask);
}
} catch (error) {
this._handleExecutionError(error);

View File

@@ -13,6 +13,7 @@ import { PubSubChannel } from "@/lib/pubsub/PubSubChannel";
import { Logging } from "@/lib/utils/Logging";
import { invokeWithRetry } from "@/lib/utils/retryable";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
import { CONFETTI_SCRIPT } from "@/lib/utils/confetti";
// Tool result schema - EXACT from NewAgent
const ToolResultSchema = z.object({
@@ -30,6 +31,7 @@ export function createNewTools(
context: ExecutionContext,
): DynamicStructuredTool[] {
return [
createClickTool(context),
createTypeTool(context),
createClearTool(context),
createScrollTool(context),
@@ -45,6 +47,7 @@ export function createNewTools(
createExtractTool(context),
createHumanInputTool(context),
createDoneTool(context),
createCelebrationTool(context),
createMoondreamVisualClickTool(context),
createMoondreamVisualTypeTool(context),
createClickAtCoordinatesTool(context),
@@ -944,6 +947,53 @@ export function createDoneTool(
});
}
// Celebration Tool
export function createCelebrationTool(
context: ExecutionContext,
): DynamicStructuredTool {
return new DynamicStructuredTool({
name: "celebration",
description: "Shows a confetti celebration animation on the current page. Use this to celebrate successful actions like upvoting or starring.",
schema: z.object({}), // No parameters needed
func: async () => {
try {
context.incrementMetric("toolCalls");
// Emit thinking message
context.getPubSub().publishMessage(
PubSubChannel.createMessage("🎉 Celebrating...", "thinking")
);
// Get current page from browserContext
const page = await context.browserContext.getCurrentPage();
if (!page) {
return JSON.stringify({
ok: false,
error: "No active page to show celebration"
});
}
// Use shared confetti script
// Execute confetti script
await page.executeJavaScript(CONFETTI_SCRIPT);
return JSON.stringify({
ok: true,
output: "Confetti celebration shown!"
});
} catch (error) {
context.incrementMetric("errors");
return JSON.stringify({
ok: false,
error: `Failed to show celebration: ${error instanceof Error ? error.message : String(error)}`
});
}
}
});
}
// Moondream Visual Click Tool
const MoondreamVisualClickInputSchema = z.object({
instruction: z

View File

@@ -0,0 +1,49 @@
import { z } from "zod";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { ExecutionContext } from "@/lib/runtime/ExecutionContext";
import { Logging } from "@/lib/utils/Logging";
import { CONFETTI_SCRIPT } from "@/lib/utils/confetti";
/**
* Creates a celebration tool for showing confetti animation
* @param context - The execution context
* @returns A DynamicStructuredTool for celebrations
*/
export function createCelebrationTool(context: ExecutionContext): DynamicStructuredTool {
return new DynamicStructuredTool({
name: "celebration_tool",
description: "Shows a confetti celebration animation on the current page. Use this to celebrate successful actions like upvoting or starring.",
schema: z.object({}), // No parameters needed
func: async () => {
try {
Logging.log("CelebrationTool", "Showing confetti celebration");
// Get current page
const page = await context.browserContext.getCurrentPage();
if (!page) {
return JSON.stringify({
ok: false,
output: "No active page to show celebration"
});
}
// Execute confetti script
await page.executeJavaScript(CONFETTI_SCRIPT);
return JSON.stringify({
ok: true,
output: "Confetti celebration shown!"
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
Logging.log("CelebrationTool", `Failed to show celebration: ${errorMessage}`, "error");
return JSON.stringify({
ok: false,
output: `Failed to show celebration: ${errorMessage}`
});
}
}
});
}

70
src/lib/utils/confetti.ts Normal file
View File

@@ -0,0 +1,70 @@
/**
* Shared confetti animation script used across the application
*/
// Confetti animation script - pure CSS/JS, no dependencies
export const CONFETTI_SCRIPT = `
(() => {
// Color palette for confetti
const colors = ['#f44336','#e91e63','#9c27b0','#3f51b5','#2196f3','#00bcd4','#4caf50','#ffeb3b','#ff9800'];
const confettiCount = 400;
// Create container for all confetti
const container = document.createElement('div');
container.id = 'browseros-confetti-container';
container.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:999999;overflow:hidden';
// Create individual confetti pieces
for(let i = 0; i < confettiCount; i++) {
const confetto = document.createElement('div');
const color = colors[Math.floor(Math.random() * colors.length)];
const left = Math.random() * 100;
const animationDelay = Math.random() * 2;
const animationDuration = 3 + Math.random() * 2;
const size = 20 + Math.random() * 40;
confetto.style.cssText = \`
position:absolute;
width:\${size}px;
height:\${size}px;
background:\${color};
left:\${left}%;
top:-20px;
opacity:1;
transform:rotate(\${Math.random() * 360}deg);
animation:confetti-fall \${animationDuration}s linear \${animationDelay}s forwards;
border-radius:\${Math.random() > 0.5 ? '50%' : '0'};
\`;
container.appendChild(confetto);
}
// Add animation keyframes
const style = document.createElement('style');
style.id = 'browseros-confetti-styles';
style.textContent = \`
@keyframes confetti-fall {
0% {
transform: translateY(0) rotate(0deg) scale(1);
opacity: 1;
}
100% {
transform: translateY(calc(100vh + 20px)) rotate(720deg) scale(0);
opacity: 0;
}
}
\`;
document.head.appendChild(style);
document.body.appendChild(container);
// Cleanup after animation completes
setTimeout(() => {
const containerEl = document.getElementById('browseros-confetti-container');
const styleEl = document.getElementById('browseros-confetti-styles');
if (containerEl) containerEl.remove();
if (styleEl) styleEl.remove();
}, 7000);
// Return success
return true;
})();
`;

View File

@@ -16,6 +16,11 @@ import { useAnalytics } from '../hooks/useAnalytics'
import { cn } from '@/sidepanel/lib/utils'
import { groupMessages } from '../utils/messageGrouping'
import type { Message } from '../stores/chatStore'
import { useSidePanelPortMessaging } from '@/sidepanel/hooks'
import { MessageType } from '@/lib/types/messaging'
import { useChatStore } from '@/sidepanel/stores/chatStore'
import { useSettingsStore } from '@/sidepanel/stores/settingsStore'
import { useTabsStore } from '@/sidepanel/stores/tabsStore'
interface MessageListProps {
messages: Message[]
@@ -27,10 +32,11 @@ interface MessageListProps {
// Example prompts - showcasing BrowserOS capabilities
const EXAMPLES = [
"Open amazon.com and order Sensodyne toothpaste",
"Find top-rated headphones under $200",
"Go to GitHub and Star BrowserOS",
"Turn this article into a LinkedIn post",
"Visit BrowserOS launch and upvote ❤️",
// "Find top-rated headphones under $200",
"Go to GitHub and Star BrowserOS",
// "Turn this article into a LinkedIn post",
"Open amazon.com and order Sensodyne toothpaste 🪥",
]
// Animation constants
@@ -43,6 +49,10 @@ const DEFAULT_DISPLAY_COUNT = 4 // Fixed number of examples to show
export function MessageList({ messages, isProcessing = false, onScrollStateChange, scrollToBottom: externalScrollToBottom, containerRef: externalContainerRef }: MessageListProps) {
const { containerRef: internalContainerRef, isUserScrolling, scrollToBottom } = useAutoScroll<HTMLDivElement>([messages], externalContainerRef)
const { trackFeature } = useAnalytics()
const { sendMessage } = useSidePanelPortMessaging()
const { upsertMessage, setProcessing } = useChatStore()
const { chatMode, setChatMode } = useSettingsStore()
const { getContextTabs, clearSelectedTabs } = useTabsStore()
const [, setIsAtBottom] = useState(true)
const [currentExamples] = useState<string[]>(EXAMPLES)
const [isAnimating] = useState(false)
@@ -189,10 +199,30 @@ export function MessageList({ messages, isProcessing = false, onScrollStateChang
}
const handleExampleClick = (prompt: string) => {
// Prevent any event propagation that might interfere
trackFeature('example_prompt', { prompt })
// Create a custom event to set input value
const event = new CustomEvent('setInputValue', { detail: prompt })
window.dispatchEvent(event)
// Switch to Agent Mode for example runs
try { setChatMode(false) } catch { /* no-op */ }
// Mirror ChatInput.submitTask behavior
const msgId = `user_${Date.now()}`
upsertMessage({ msgId, role: 'user', content: prompt, ts: Date.now() })
setProcessing(true)
// Collect selected context tabs (same behavior as ChatInput)
const contextTabs = getContextTabs()
const tabIds = contextTabs.length > 0 ? contextTabs.map(tab => tab.id) : undefined
sendMessage(MessageType.EXECUTE_QUERY, {
query: prompt.trim(),
tabIds,
source: 'sidepanel',
chatMode: false
})
// Clear selected tabs after sending (mirror ChatInput)
try { clearSelectedTabs() } catch { /* no-op */ }
}
// Landing View
@@ -203,22 +233,23 @@ export function MessageList({ messages, isProcessing = false, onScrollStateChang
role="region"
aria-label="Welcome screen with example prompts"
>
{/* Main content - vertically centered */}
<div className="relative z-0 flex flex-col items-center justify-center min-h-0 max-w-md w-full">
{/* Main tagline with logo */}
<div className="mb-8 flex flex-col items-center">
<h2 className="text-3xl font-bold text-muted-foreground animate-fade-in-up flex items-baseline flex-wrap justify-center gap-2 text-center w-full">
<span>Your <span className="text-brand">Agentic</span></span>
<span>web assistant{' '}
<img
src="/assets/browseros.svg"
alt="BrowserOS"
className="w-7 h-7 inline-block align-text-bottom animate-fade-in-up"
/>
</span>
</h2>
</div>
{/* Fixed tagline near the top */}
<div className="absolute left-0 right-0 top-16 md:top-20 flex items-center justify-center pointer-events-none select-none">
<h2 className="text-3xl font-bold text-muted-foreground animate-fade-in-up flex items-baseline flex-wrap justify-center gap-2 text-center w-full">
<span>Your <span className="text-brand">Agentic</span></span>
<span>
web assistant{' '}
<img
src="/assets/browseros.svg"
alt="BrowserOS"
className="w-7 h-7 inline-block align-text-bottom animate-fade-in-up"
/>
</span>
</h2>
</div>
{/* Main content - vertically centered (Examples remain centered) */}
<div className="relative z-0 flex flex-col items-center justify-center min-h-0 max-w-md w-full">
{/* Example Prompts */}
<div className="mb-8 mt-2">
<h3 className="text-lg font-semibold text-foreground mb-6 animate-fade-in-up" style={{ animationDelay: '0.4s' }}>
@@ -240,9 +271,14 @@ export function MessageList({ messages, isProcessing = false, onScrollStateChang
}`}
>
<Button
type="button"
variant="outline"
className="group relative text-sm h-auto py-3 px-4 whitespace-normal bg-background/50 backdrop-blur-sm border-2 border-brand/30 hover:border-brand hover:bg-brand/5 smooth-hover smooth-transform hover:scale-105 hover:-translate-y-1 hover:shadow-lg focus-visible:outline-none overflow-hidden w-full message-enter"
onClick={() => handleExampleClick(prompt)}
onClick={(e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
handleExampleClick(prompt)
}}
aria-label={`Use example: ${prompt}`}
>
{/* Animated background */}