Skip to content
Open
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
4 changes: 2 additions & 2 deletions apps/x/apps/main/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rowboat",
"productName": "Rowboat",
"name": "openclaw",
"productName": "OpenClaw",
"description": "AI coworker with memory",
"type": "module",
"version": "0.1.0",
Expand Down
4 changes: 2 additions & 2 deletions apps/x/apps/main/src/composio-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export async function initiateConnection(toolkitSlug: string): Promise<{
toolkit: { slug: toolkitSlug },
auth_config: {
type: 'use_composio_managed_auth',
name: `rowboat-${toolkitSlug}`,
name: `openclaw-${toolkitSlug}`,
},
});
authConfigId = created.auth_config.id;
Expand All @@ -102,7 +102,7 @@ export async function initiateConnection(toolkitSlug: string): Promise<{
const response = await composioClient.createConnectedAccount({
auth_config: { id: authConfigId },
connection: {
user_id: 'rowboat-user',
user_id: 'openclaw-user',
callback_url: callbackUrl,
},
});
Expand Down
33 changes: 21 additions & 12 deletions apps/x/apps/main/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,24 @@ const __dirname = dirname(__filename);
// run this as early in the main process as possible
if (started) app.quit();

// Gracefully ignore EPIPE errors on stdout/stderr (broken pipe from dev tooling)
process.stdout?.on?.("error", (err: NodeJS.ErrnoException) => { if (err.code !== "EPIPE") throw err; });
process.stderr?.on?.("error", (err: NodeJS.ErrnoException) => { if (err.code !== "EPIPE") throw err; });

// In dev mode, Electron sets process.defaultApp = true. This is more reliable
// than app.isPackaged because the esbuild-bundled .cjs can confuse isPackaged.
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Electron-specific property not in Node types
const isDev = !!(process as any).defaultApp;

// Path resolution differs between development and production:
const preloadPath = app.isPackaged
? path.join(__dirname, "../preload/dist/preload.js")
: path.join(__dirname, "../../../preload/dist/preload.js");
const preloadPath = isDev
? path.join(__dirname, "../../../preload/dist/preload.js")
: path.join(__dirname, "../preload/dist/preload.js");
console.log("preloadPath", preloadPath);

const rendererPath = app.isPackaged
? path.join(__dirname, "../renderer/dist") // Production
: path.join(__dirname, "../../../renderer/dist"); // Development
const rendererPath = isDev
? path.join(__dirname, "../../../renderer/dist") // Development
: path.join(__dirname, "../renderer/dist"); // Production
console.log("rendererPath", rendererPath);

// Register custom protocol for serving built renderer files in production.
Expand Down Expand Up @@ -77,7 +86,7 @@ function createWindow() {
const win = new BrowserWindow({
width: 1280,
height: 800,
show: false, // Don't show until ready
show: true, // Show immediately to prevent invisible window issues
backgroundColor: "#252525", // Prevent white flash (matches dark mode)
titleBarStyle: "hiddenInset",
trafficLightPosition: { x: 12, y: 12 },
Expand Down Expand Up @@ -112,21 +121,21 @@ function createWindow() {
}
});

if (app.isPackaged) {
win.loadURL("app://-/index.html");
} else {
if (isDev) {
win.loadURL("http://localhost:5173");
} else {
win.loadURL("app://-/index.html");
}
}

app.whenReady().then(async () => {
// Register custom protocol before creating window (for production builds)
if (app.isPackaged) {
if (!isDev) {
registerAppProtocol();
}

// Initialize auto-updater (only in production)
if (app.isPackaged) {
if (!isDev) {
updateElectronApp({
updateSource: {
type: UpdateSourceType.ElectronPublicUpdateService,
Expand Down
4 changes: 2 additions & 2 deletions apps/x/apps/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rowboat</title>
<title>OpenClaw</title>
<style>
/* Prevent flash of white background before CSS loads */
html, body { margin: 0; padding: 0; }
Expand All @@ -14,7 +14,7 @@
<script>
// Apply theme class immediately before render
(function() {
var stored = localStorage.getItem('rowboat-theme');
var stored = localStorage.getItem('openclaw-theme');
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
var theme = stored || 'system';
var resolved = theme === 'system' ? (prefersDark ? 'dark' : 'light') : theme;
Expand Down
24 changes: 24 additions & 0 deletions apps/x/apps/renderer/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,27 @@
pointer-events: none;
user-select: none;
}

/* ─── Enhanced Progress Bar: Striped & Animated ────────── */
@keyframes progress-stripes {
from { background-position: 1rem 0; }
to { background-position: 0 0; }
}

.progress-striped {
background-image: linear-gradient(
45deg,
rgba(255, 255, 255, 0.1) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.1) 50%,
rgba(255, 255, 255, 0.1) 75%,
transparent 75%,
transparent
);
background-size: 1rem 1rem;
}

.progress-striped-animated {
animation: progress-stripes 1s linear infinite;
}
26 changes: 16 additions & 10 deletions apps/x/apps/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ import {
type FileMention,
} from '@/components/ai-elements/prompt-input';
import { Reasoning, ReasoningContent, ReasoningTrigger } from '@/components/ai-elements/reasoning';
import { Shimmer } from '@/components/ai-elements/shimmer';
import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from '@/components/ai-elements/tool';
import { PermissionRequest } from '@/components/ai-elements/permission-request';
import { AskHumanRequest } from '@/components/ai-elements/ask-human-request';
import { Suggestions } from '@/components/ai-elements/suggestions';
import { TypingIndicator } from '@/components/ai-elements/typing-indicator';
import { ShortcutHint } from '@/components/ui/keyboard-shortcut';
import { ToolPermissionRequestEvent, AskHumanRequestEvent } from '@x/shared/src/runs.js';
import {
SidebarInset,
Expand Down Expand Up @@ -368,7 +369,7 @@ function ChatInputInner({
}, [controller])

return (
<div className="flex items-center gap-2 bg-background border border-border rounded-lg shadow-none px-4 py-4">
<div className="flex items-center gap-2 bg-card/60 backdrop-blur-sm border border-border/50 rounded-2xl shadow-sm px-4 py-4 transition-all duration-200 focus-within:ring-2 focus-within:ring-primary/15 focus-within:border-primary/30 focus-within:shadow-md">
<PromptInputTextarea
placeholder="Type your message..."
onKeyDown={handleKeyDown}
Expand Down Expand Up @@ -1235,6 +1236,11 @@ function App() {
setModelUsage(null)
break

case 'parallel-dispatch':
if (!isActiveRun) return
console.log(`[Parallel] Dispatching ${event.toolCallIds.length} tools in parallel:`, event.toolNames)
break

case 'llm-stream-event':
{
const llmEvent = event.event
Expand Down Expand Up @@ -2395,10 +2401,14 @@ function App() {
<ScrollPositionPreserver />
<ConversationContent className={conversationContentClassName}>
{!hasConversation ? (
<ConversationEmptyState className="h-auto">
<div className="text-2xl font-semibold tracking-tight text-foreground/80 sm:text-3xl md:text-4xl">
<ConversationEmptyState className="h-auto space-y-3">
<h2 className="text-2xl font-semibold tracking-tight text-foreground/80 sm:text-3xl md:text-4xl">
What are we working on?
</div>
</h2>
<p className="text-sm text-muted-foreground/60">
Ask anything, or pick a suggestion below
</p>
<ShortcutHint keys={["Enter"]} label="Press" size="sm" className="justify-center text-muted-foreground/40" />
</ConversationEmptyState>
) : (
<>
Expand Down Expand Up @@ -2452,11 +2462,7 @@ function App() {
)}

{isProcessing && !currentAssistantMessage && !currentReasoning && (
<Message from="assistant">
<MessageContent>
<Shimmer duration={1}>Thinking...</Shimmer>
</MessageContent>
</Message>
<TypingIndicator label="Thinking" />
)}
</>
)}
Expand Down
100 changes: 28 additions & 72 deletions apps/x/apps/renderer/src/components/ai-elements/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@/components/ui/hover-card";
import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
import { formatCompact, formatCurrency, formatPercent } from "@/lib/formatters";
import type { LanguageModelUsage } from "ai";
import { type ComponentProps, createContext, useContext } from "react";
import { getUsage } from "tokenlens";
Expand Down Expand Up @@ -39,6 +40,15 @@ const useContextValue = () => {
return context;
};

/** Compute USD cost for a given token usage via tokenlens */
const getTokenCost = (
modelId: string | undefined,
usage: Parameters<typeof getUsage>[0]["usage"]
) =>
modelId
? getUsage({ modelId, usage }).costUSD?.totalUSD
: undefined;

export type ContextProps = ComponentProps<typeof HoverCard> & ContextSchema;

export const Context = ({
Expand Down Expand Up @@ -106,10 +116,7 @@ export type ContextTriggerProps = ComponentProps<typeof Button>;
export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
const { usedTokens, maxTokens } = useContextValue();
const usedPercent = usedTokens / maxTokens;
const renderedPercent = new Intl.NumberFormat("en-US", {
style: "percent",
maximumFractionDigits: 1,
}).format(usedPercent);
const renderedPercent = formatPercent(usedPercent);

return (
<HoverCardTrigger asChild>
Expand Down Expand Up @@ -146,16 +153,9 @@ export const ContextContentHeader = ({
}: ContextContentHeaderProps) => {
const { usedTokens, maxTokens } = useContextValue();
const usedPercent = usedTokens / maxTokens;
const displayPct = new Intl.NumberFormat("en-US", {
style: "percent",
maximumFractionDigits: 1,
}).format(usedPercent);
const used = new Intl.NumberFormat("en-US", {
notation: "compact",
}).format(usedTokens);
const total = new Intl.NumberFormat("en-US", {
notation: "compact",
}).format(maxTokens);
const displayPct = formatPercent(usedPercent);
const used = formatCompact(usedTokens);
const total = formatCompact(maxTokens);

return (
<div className={cn("w-full space-y-2 p-3", className)} {...props}>
Expand Down Expand Up @@ -196,19 +196,11 @@ export const ContextContentFooter = ({
...props
}: ContextContentFooterProps) => {
const { modelId, usage } = useContextValue();
const costUSD = modelId
? getUsage({
modelId,
usage: {
input: usage?.inputTokens ?? 0,
output: usage?.outputTokens ?? 0,
},
}).costUSD?.totalUSD
: undefined;
const totalCost = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(costUSD ?? 0);
const costUSD = getTokenCost(modelId, {
input: usage?.inputTokens ?? 0,
output: usage?.outputTokens ?? 0,
});
const totalCost = formatCurrency(costUSD ?? 0);

return (
<div
Expand Down Expand Up @@ -246,16 +238,8 @@ export const ContextInputUsage = ({
return null;
}

const inputCost = modelId
? getUsage({
modelId,
usage: { input: inputTokens, output: 0 },
}).costUSD?.totalUSD
: undefined;
const inputCostText = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(inputCost ?? 0);
const inputCost = getTokenCost(modelId, { input: inputTokens, output: 0 });
const inputCostText = formatCurrency(inputCost ?? 0);

return (
<div
Expand Down Expand Up @@ -286,16 +270,8 @@ export const ContextOutputUsage = ({
return null;
}

const outputCost = modelId
? getUsage({
modelId,
usage: { input: 0, output: outputTokens },
}).costUSD?.totalUSD
: undefined;
const outputCostText = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(outputCost ?? 0);
const outputCost = getTokenCost(modelId, { input: 0, output: outputTokens });
const outputCostText = formatCurrency(outputCost ?? 0);

return (
<div
Expand Down Expand Up @@ -326,16 +302,8 @@ export const ContextReasoningUsage = ({
return null;
}

const reasoningCost = modelId
? getUsage({
modelId,
usage: { reasoningTokens },
}).costUSD?.totalUSD
: undefined;
const reasoningCostText = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(reasoningCost ?? 0);
const reasoningCost = getTokenCost(modelId, { reasoningTokens });
const reasoningCostText = formatCurrency(reasoningCost ?? 0);

return (
<div
Expand Down Expand Up @@ -366,16 +334,8 @@ export const ContextCacheUsage = ({
return null;
}

const cacheCost = modelId
? getUsage({
modelId,
usage: { cacheReads: cacheTokens, input: 0, output: 0 },
}).costUSD?.totalUSD
: undefined;
const cacheCostText = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(cacheCost ?? 0);
const cacheCost = getTokenCost(modelId, { cacheReads: cacheTokens, input: 0, output: 0 });
const cacheCostText = formatCurrency(cacheCost ?? 0);

return (
<div
Expand All @@ -396,11 +356,7 @@ const TokensWithCost = ({
costText?: string;
}) => (
<span>
{tokens === undefined
? "—"
: new Intl.NumberFormat("en-US", {
notation: "compact",
}).format(tokens)}
{tokens === undefined ? "—" : formatCompact(tokens)}
{costText ? (
<span className="ml-2 text-muted-foreground">• {costText}</span>
) : null}
Expand Down
Loading