Ironclaw rebrand: new identity, animated CLI banner, and iron palette
Rebrand the project from the OpenClaw/Lobster identity to Ironclaw with a new iron-metallic visual language across CLI and web UI. ## CLI identity - Rename default CLI name from `openclaw` to `ironclaw` (keep `openclaw` in KNOWN_CLI_NAMES and regex for backward compat) - Set process.title to `ironclaw`; update all `[openclaw]` log prefixes to `[ironclaw]` - Add `IRONCLAW_*` env var checks (IRONCLAW_HIDE_BANNER, IRONCLAW_NO_RESPAWN, IRONCLAW_NODE_OPTIONS_READY, IRONCLAW_TAGLINE_INDEX) with fallback to legacy `OPENCLAW_*` variants ## Animated ASCII banner - Replace the old lobster block-art with a figlet "ANSI Shadow" font IRONCLAW ASCII wordmark - Add `gradient-string` dependency for terminal gradient rendering - Implement iron shimmer animation: a bright highlight sweeps across the ASCII art (~2.5 s at 12 fps, 3 full gradient cycles) using a rotating iron-to-silver color array - Make `emitCliBanner` async to support the animation; update all call sites (preaction hook, route, run-main) to await it - Move banner emission earlier in `runCli()` so it appears for all invocations (bare command, subcommands, help) with the existing bannerEmitted guard preventing double-emission ## Iron palette and theme - Rename LOBSTER_PALETTE → IRON_PALETTE in `src/terminal/palette.ts` with new cool-steel color tokens (steel grey accent, bright silver highlight, dark iron dim, steel bl info) - Re-export LOBSTER_PALETTE as backward-compatible alias - Update `src/terminal/theme.ts` to import and use IRON_PALETTE ## Tagline cleanup - Remove lobster-themed, Apple-specific, and platform-joke taglines - Fix smart-quote and em-dash formatting across remaining taglines - Add "Holiday taglines" comment grouping for date-gated entries ## Web UI - Add `framer-motion`, `fuse.js`, and `next-themes` to web app deps - Add custom font files: Bookerly (regular/bold/italic), SpaceGrotesk (light/regular/medium/semibold/bold), FoundationTitlesHand - Update chat panel labels: "OpenClaw Chat" → "Ironclaw Chat", "Message OpenClaw..." → "Message Ironclaw..." - Update sidebar header: "OpenClaw Dench" → "Ironclaw" - CSS formatting cleanup: expand single-lins, add consistent blank lines between selector blocks, normalize child combinator spacing (li > ul → li>ul)
This commit is contained in:
parent
7c9f7aa2d2
commit
e8f5eddacb
324
.cursor/plans/full_web_ui_redesign_9ad2e285.plan.md
Normal file
324
.cursor/plans/full_web_ui_redesign_9ad2e285.plan.md
Normal file
@ -0,0 +1,324 @@
|
||||
---
|
||||
name: Full Web UI Redesign
|
||||
overview: "Complete redesign of the OpenClaw web app (apps/web/) to match the Dench design system: switch from dark theme to light, adopt Instrument Serif + Inter fonts, port the Dench color palette and layout patterns, and rewrite every component and page from the ground up."
|
||||
todos:
|
||||
- id: foundation
|
||||
content: "Phase 1: Rewrite globals.css (light theme, HSL tokens, font imports) and layout.tsx (next/font, remove dark mode)"
|
||||
status: pending
|
||||
- id: landing
|
||||
content: "Phase 2: Rewrite app/page.tsx as Dench-style landing page (navbar, hero, demo sections, footer)"
|
||||
status: pending
|
||||
- id: layout-shell
|
||||
content: "Phase 3: Create app-navbar.tsx, rewrite workspace/page.tsx layout with top navbar + sidebar grid"
|
||||
status: pending
|
||||
- id: sidebar
|
||||
content: "Phase 4: Redesign workspace-sidebar.tsx and file-manager-tree.tsx to match Dench sidebar"
|
||||
status: pending
|
||||
- id: data-table
|
||||
content: "Phase 5: Redesign object-table.tsx with Dench-style toolbar, sticky headers, pagination, enum badges"
|
||||
status: pending
|
||||
- id: kanban
|
||||
content: "Phase 6: Redesign object-kanban.tsx with light cards, columns, board header"
|
||||
status: pending
|
||||
- id: entry-detail
|
||||
content: "Phase 7: Redesign entry-detail-modal.tsx as right-panel slide-out with properties list"
|
||||
status: pending
|
||||
- id: dashboard-chat
|
||||
content: "Phase 8a: Build dashboard view with greeting, centered chat input, suggestion chips, and animate-down-to-bottom Framer Motion layoutId transition"
|
||||
status: pending
|
||||
- id: chat
|
||||
content: "Phase 8b: Restyle chat-panel.tsx, chat-message.tsx, chain-of-thought.tsx for light theme + bottom composer"
|
||||
status: pending
|
||||
- id: remaining
|
||||
content: "Phase 9: Restyle all remaining components (breadcrumbs, document-view, file-viewer, database-viewer, empty-state, markdown, context-menu, slash-command, charts, etc.)"
|
||||
status: pending
|
||||
- id: deps
|
||||
content: "Phase 10: Add framer-motion dependency, verify fonts work, test build"
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Full Web UI Redesign — Dench Design System
|
||||
|
||||
## Current State
|
||||
|
||||
The OpenClaw web app is a **dark-themed** Next.js 15 app with:
|
||||
|
||||
- Dark background (`#0a0a0a`), dark surfaces (`#141414`), orange accent (`#e85d3a`)
|
||||
- Inter font only, no serif headings
|
||||
- Minimal homepage (centered text + CTA)
|
||||
- Workspace layout: left sidebar (260px) + content + optional chat panel
|
||||
- Custom table/kanban/viewer components, all dark-styled
|
||||
- Tailwind v4 (CSS-based config), no shadcn/ui
|
||||
|
||||
## Target State (Dench Design)
|
||||
|
||||
Per the screenshots and Dench source:
|
||||
|
||||
- **Light theme** — `bg-neutral-50` layout, white cards, `bg-neutral-100` sidebar/navbar
|
||||
- **Instrument Serif** for headings/titles, **Inter** for body text, **Lora** for branding
|
||||
- **Top navbar** (grid 3-col, with Dashboard/Workflows/Integrations tabs, org logo, user menu)
|
||||
- **Left sidebar** (260px, `bg-neutral-100`, collapsible knowledge tree with item counts)
|
||||
- **Data tables** with: sticky header, column borders, search bar, filter/column controls, enum badges, relation chips, pagination
|
||||
- **Kanban board** with rounded cards, priority badges, assignee avatars
|
||||
- **Entry detail** right-panel slide-out with property list
|
||||
- **Landing page** with hero section, demo sections, clean navigation bar
|
||||
- **Dashboard chat UX** — centered greeting ("Good evening, Kumar?") in Instrument Serif + centered chat input with suggestion chips; on first message, the input animates down to a bottom-docked composer via Framer Motion shared `layoutId` spring transition
|
||||
- HSL-based CSS variables (shadcn pattern), `--radius: 0.5rem`, neutral base color
|
||||
|
||||
## Architecture Decision: Tailwind v4
|
||||
|
||||
The OpenClaw app uses **Tailwind v4** (CSS-based config via `@import "tailwindcss"`), while Dench uses Tailwind v3 (JS config). We will keep Tailwind v4 but port all design tokens into `globals.css` using `@theme` blocks and CSS custom properties. No downgrade needed.
|
||||
|
||||
## Architecture Decision: Light + Dark Theme
|
||||
|
||||
Dench is light-only. We will use Dench's light palette as the `:root` default AND create a custom dark palette under `.dark` (class-based toggle via `<html class="dark">`). All components will use CSS variable references (e.g. `bg-background`, `text-foreground`, `border-border`) so they automatically adapt. No hardcoded hex/rgb in components.
|
||||
|
||||
**Light palette** (from Dench):
|
||||
|
||||
- `--background: 0 0% 96%` (neutral-50 feel)
|
||||
- `--foreground: 0 0% 3.9%`
|
||||
- `--card: 0 0% 100%` / `--card-foreground: 0 0% 3.9%`
|
||||
- `--muted: 0 0% 96.1%` / `--muted-foreground: 0 0% 45.1%`
|
||||
- `--border: 0 0% 89.8%`
|
||||
- `--primary: 0 0% 9%` / `--primary-foreground: 0 0% 98%`
|
||||
- `--accent: 0 0% 96.1%` / `--accent-foreground: 0 0% 9%`
|
||||
- `--destructive: 0 84.2% 60.2%`
|
||||
|
||||
**Dark palette** (custom, designed to complement Dench's light theme):
|
||||
|
||||
- `--background: 0 0% 7%` (#121212 — rich near-black, not pure black)
|
||||
- `--foreground: 0 0% 93%` (#ededed)
|
||||
- `--card: 0 0% 10%` (#1a1a1a) / `--card-foreground: 0 0% 93%`
|
||||
- `--muted: 0 0% 14%` (#242424) / `--muted-foreground: 0 0% 55%` (#8c8c8c)
|
||||
- `--border: 0 0% 18%` (#2e2e2e)
|
||||
- `--primary: 0 0% 93%` / `--primary-foreground: 0 0% 9%`
|
||||
- `--accent: 0 0% 16%` (#292929) / `--accent-foreground: 0 0% 93%`
|
||||
- `--destructive: 0 62% 55%`
|
||||
- Sidebar: `--sidebar-bg: 0 0% 9%` (#171717)
|
||||
- Navbar: similar to sidebar, subtle `border-b` at `--border`
|
||||
|
||||
Sidebar/navbar in dark mode use a slightly elevated surface (`#171717`) rather than pure background, for depth.
|
||||
|
||||
**Theme toggle:** add a sun/moon toggle button in the navbar (right side, near user avatar). Use `next-themes` or a simple `useEffect` + `localStorage` approach to persist preference and apply `.dark` class on `<html>`.
|
||||
|
||||
---
|
||||
|
||||
## Files to Change
|
||||
|
||||
### Phase 1 — Foundation (Theme, Fonts, Layout Shell)
|
||||
|
||||
**[app/globals.css](apps/web/app/globals.css)** — Complete rewrite:
|
||||
|
||||
- `:root` block: Dench's light-theme HSL palette (background, foreground, card, primary, secondary, muted, accent, destructive, border, ring, sidebar, chart-1 through chart-5)
|
||||
- `.dark` block: custom dark palette (see "Architecture Decision: Light + Dark Theme" above) — all same variable names, dark values
|
||||
- Add `@theme` block for Tailwind v4 mapping CSS vars to utility classes (`bg-background`, `text-foreground`, `border-border`, `bg-card`, `text-muted-foreground`, etc.)
|
||||
- Import Instrument Serif from Google Fonts
|
||||
- Add `.font-instrument` utility class
|
||||
- Port scrollbar, prose, editor, and slash-command styles using CSS variables (theme-aware, not hardcoded)
|
||||
- Port workflow state colors (`--workflow-active`, `--workflow-processing`, `--workflow-idle`)
|
||||
|
||||
**[app/layout.tsx](apps/web/app/layout.tsx)** — Rewrite:
|
||||
|
||||
- Import Inter and Lora via `next/font/google`
|
||||
- Set CSS variables `--font-corporate` and `--font-lora`
|
||||
- Default to light: no `className="dark"` on `<html>` (let theme provider handle it)
|
||||
- Apply `font-corporate` to `<body>`
|
||||
- Add `suppressHydrationWarning` on `<html>` for theme flash prevention
|
||||
- Add inline script or `next-themes` `ThemeProvider` for class-based dark mode toggle with `localStorage` persistence
|
||||
- Update metadata title/description to "Dench" branding
|
||||
|
||||
**New: `app/hooks/use-theme.ts**` — Simple theme hook:
|
||||
|
||||
- Read/write `localStorage` key `"theme"` (`"light"` | `"dark"` | `"system"`)
|
||||
- Apply/remove `.dark` class on `document.documentElement`
|
||||
- Expose `theme`, `setTheme`, `resolvedTheme` for components
|
||||
|
||||
### Phase 2 — Landing Page
|
||||
|
||||
**[app/page.tsx](apps/web/app/page.tsx)** — Full rewrite to match Dench landing:
|
||||
|
||||
- Sticky navigation bar (logo "Dench" in `font-lora`, Login button in rounded-full blue pill)
|
||||
- Hero section: "AI CRM" headline in `font-instrument font-bold`, subtext, "Get Started Free" CTA
|
||||
- Full-width CRM demo area (window chrome with traffic-light dots, scaled mock table)
|
||||
- Additional demo sections (workflow, kanban) — simplified versions
|
||||
- Footer with copyright, links
|
||||
|
||||
### Phase 3 — Workspace Layout Shell
|
||||
|
||||
**[app/workspace/page.tsx](apps/web/app/workspace/page.tsx)** — Rewrite layout structure:
|
||||
|
||||
- Add top `AppNavbar` component: `bg-neutral-100 border-b border-border shadow-[0_0_40px_rgba(0,0,0,0.05)]`
|
||||
- Left: org logo + "Powered by Dench" + org name in `font-instrument`
|
||||
- Center: tab navigation (Dashboard, Workflows, Integrations) with active state
|
||||
- Right: credit display, notification bell, sun/moon theme toggle, user avatar dropdown
|
||||
- Main area: `grid lg:grid-cols-[260px_1fr]` under navbar
|
||||
- Full height: `h-[100dvh] flex flex-col bg-neutral-50`
|
||||
- Content area: `overflow-y-auto overflow-x-hidden`
|
||||
- Replace all inline `style={{}}` dark colors with Tailwind classes
|
||||
|
||||
**New component: `app/components/workspace/app-navbar.tsx**` — Top navbar (extracted for reuse)
|
||||
|
||||
### Phase 4 — Sidebar Redesign
|
||||
|
||||
**[app/components/workspace/workspace-sidebar.tsx](apps/web/app/components/workspace/workspace-sidebar.tsx)** — Full rewrite:
|
||||
|
||||
- Background: `bg-sidebar` with `border-r border-border` (light: neutral-100, dark: #171717 via CSS var)
|
||||
- Shadow: theme-aware subtle shadow
|
||||
- Header: "KNOWLEDGE" section label in uppercase `text-[11px] font-medium tracking-wider text-muted-foreground`
|
||||
- Knowledge items: `text-[13px]`, hover `bg-accent`, `rounded-xl`
|
||||
- Item badges showing entry counts in `bg-muted border border-border` pills
|
||||
- Icons per item type (objects get custom icons, documents get doc icon)
|
||||
- Collapsible sections: KNOWLEDGE, CHATS, TELEPHONY
|
||||
- Bottom: "API Keys" link
|
||||
- Remove all inline `style={{}}` dark colors
|
||||
|
||||
**[app/components/workspace/file-manager-tree.tsx](apps/web/app/components/workspace/file-manager-tree.tsx)** — Restyle tree items:
|
||||
|
||||
- Light-theme hover states, active states matching `bg-neutral-200`
|
||||
- `text-[13px]` sizing, proper icon colors
|
||||
- Drag-and-drop visual indicators in light theme
|
||||
|
||||
### Phase 5 — Data Table Redesign
|
||||
|
||||
**[app/components/workspace/object-table.tsx](apps/web/app/components/workspace/object-table.tsx)** — Complete rewrite to match Dench data-table:
|
||||
|
||||
- Toolbar: object name in `font-instrument`, search input (`rounded-full shadow-[0_0_21px_0_rgba(0,0,0,0.07)]`), "Ask AI" button, Table/Board view toggle, refresh/import/filter/columns/+ Add buttons
|
||||
- Table header: `sticky top-0 z-30 bg-card border-b-2 border-border/80`, sortable columns with sort arrows
|
||||
- Table cells: `px-4 border-r border-border/30`, proper text truncation
|
||||
- Enum badges: colored pill style matching Dench (translucent background + border)
|
||||
- Relation chips: link icon + blue text
|
||||
- Row hover: `hover:bg-muted/50`
|
||||
- Pagination bar: "Showing 1 to N of N results", rows-per-page selector, page navigation
|
||||
- "..." action menu per row (right column)
|
||||
|
||||
### Phase 6 — Kanban Board Redesign
|
||||
|
||||
**[app/components/workspace/object-kanban.tsx](apps/web/app/components/workspace/object-kanban.tsx)** — Rewrite:
|
||||
|
||||
- Board header: view toggle (Board/Table), "Ask AI" button, search, "Group by" selector
|
||||
- Columns: `bg-muted/50 rounded-2xl border border-border/60`, column title + count badge
|
||||
- Cards: `bg-card rounded-xl border border-border/80 shadow-sm`
|
||||
- Card content: title, field badges (objective, risk profile), date, assignee avatar
|
||||
- "+ Add Item" at column bottom
|
||||
- "Drop cards here" empty column placeholder
|
||||
|
||||
### Phase 7 — Entry Detail Panel
|
||||
|
||||
**[app/components/workspace/entry-detail-modal.tsx](apps/web/app/components/workspace/entry-detail-modal.tsx)** — Redesign as right-panel slide-out:
|
||||
|
||||
- Takes ~40% of content width, pushes table left
|
||||
- Header: icon + title in large font, "Created Jan 12, 2026 at 12:47 PM" subtitle
|
||||
- "PROPERTIES" section label
|
||||
- Property rows: label (uppercase text-xs text-muted-foreground) + value
|
||||
- Relation fields show colored link chips
|
||||
- Enum fields show colored badges (matching table)
|
||||
- "Add a property" at bottom
|
||||
- Close button (>> icon) top-right
|
||||
|
||||
### Phase 8a — Dashboard Chat UX (Greeting + Animate-to-Bottom Input)
|
||||
|
||||
This is the hero interaction on the workspace "Dashboard" tab — a centered greeting with a chat input that transitions into the bottom-docked composer after the first message.
|
||||
|
||||
**How Dench implements it:**
|
||||
|
||||
- `DashboardHeader`: time-based greeting ("Good morning/afternoon/evening, Name?") with staggered word-by-word Framer Motion entrance (`y:20 → 0`, `blur(8px) → blur(0)`)
|
||||
- `DashboardChatbox`: centered TipTap input with placeholder "Build a workflow to automate your tasks", attach/voice/submit buttons, suggestion chips below (shuffled from a pool of ~27 templates, showing 7 in two rows)
|
||||
- **Layout animation:** both the centered input and the bottom composer share a Framer Motion `layoutId="chat-thread-composer"`. When `showStartComposer` flips to false after the first message, Framer Motion automatically animates the input from center to bottom with `transition={{ type: "spring", stiffness: 260, damping: 30 }}`
|
||||
|
||||
**New components to create:**
|
||||
|
||||
`app/components/workspace/dashboard-view.tsx` — Dashboard home view:
|
||||
|
||||
- Greeting in `font-instrument text-4xl` with time-based message + user name
|
||||
- Word-by-word staggered Framer Motion entrance animation
|
||||
- Centered chat input area below greeting
|
||||
|
||||
`app/components/workspace/dashboard-chatbox.tsx` — Centered input + chips:
|
||||
|
||||
- Rounded white card with subtle shadow, textarea/input with placeholder
|
||||
- Attach (paperclip), voice (mic), submit (arrow) icon buttons
|
||||
- Suggestion chip rows: 3 on first row, 4 on second row, each with icon + label + `rounded-xl` border
|
||||
- Accepts `layoutId` prop for shared layout animation
|
||||
- `mode` prop: `"dashboard"` (centered, with greeting) vs `"thread"` (same input but used within chat thread)
|
||||
- Entry animation: `opacity: 0, y: 20` → `opacity: 1, y: 0`, duration 0.8s
|
||||
|
||||
**Modify [app/workspace/page.tsx](apps/web/app/workspace/page.tsx):**
|
||||
|
||||
- When no content selected (Dashboard tab active), render `DashboardView`
|
||||
- On chat submit: transition to chat thread view
|
||||
- Use `LayoutGroup` from Framer Motion to wrap the dashboard + chat area
|
||||
- Track `showStartComposer` state: when true, show centered `DashboardChatbox`; when false, show messages + bottom `ChatComposer` — both sharing the same `layoutId`
|
||||
|
||||
**Prompt templates** (simplified set for OpenClaw):
|
||||
|
||||
- Follow-up Emails, Calendly Prep, Zoom Recap, Facebook Leads, Calendar Sync, Salesforce Sync, Intercom Chat (matching the Dench screenshot chips)
|
||||
|
||||
### Phase 8b — Chat & Message Restyling
|
||||
|
||||
**[app/components/chat-panel.tsx](apps/web/app/components/chat-panel.tsx)** — Restyle:
|
||||
|
||||
- Theme-aware background (`bg-card`), card-colored input area
|
||||
- Input: rounded border, subtle shadow, consistent with dashboard chatbox style
|
||||
- Bottom-docked composer with `layoutId` for shared animation
|
||||
- Session tabs in light theme
|
||||
- Tool call indicators in light theme
|
||||
- Send button styling (rounded, neutral)
|
||||
|
||||
**[app/components/chat-message.tsx](apps/web/app/components/chat-message.tsx)** — Restyle:
|
||||
|
||||
- Theme-aware message bubbles (user: `bg-muted`, assistant: `bg-card`)
|
||||
- Code blocks with `bg-muted`
|
||||
- Markdown rendering in light theme
|
||||
- Chain-of-thought styling update
|
||||
|
||||
**[app/components/chain-of-thought.tsx](apps/web/app/components/chain-of-thought.tsx)** — Light theme
|
||||
|
||||
### Phase 9 — Remaining Components
|
||||
|
||||
All components below: replace every hardcoded color (`style={{}}`, hex, rgb) with semantic Tailwind utilities (`bg-background`, `text-foreground`, `border-border`, `bg-card`, `text-muted-foreground`, `bg-muted`, etc.) so they work in both light and dark:
|
||||
|
||||
- **[breadcrumbs.tsx](apps/web/app/components/workspace/breadcrumbs.tsx)** — `text-muted-foreground`, `hover:text-foreground`
|
||||
- **[document-view.tsx](apps/web/app/components/workspace/document-view.tsx)** — `bg-card` background, `border-border`
|
||||
- **[file-viewer.tsx](apps/web/app/components/workspace/file-viewer.tsx)** — `bg-muted` code blocks, `text-foreground`
|
||||
- **[database-viewer.tsx](apps/web/app/components/workspace/database-viewer.tsx)** — `bg-card` tables, `bg-muted` query editor
|
||||
- **[empty-state.tsx](apps/web/app/components/workspace/empty-state.tsx)** — `text-muted-foreground` illustration
|
||||
- **[markdown-content.tsx](apps/web/app/components/workspace/markdown-content.tsx)** — Prose styles via CSS vars
|
||||
- **[markdown-editor.tsx](apps/web/app/components/workspace/markdown-editor.tsx)** — `bg-card` editor chrome
|
||||
- **[context-menu.tsx](apps/web/app/components/workspace/context-menu.tsx)** — `bg-card` dropdown, `border-border`
|
||||
- **[slash-command.tsx](apps/web/app/components/workspace/slash-command.tsx)** — `bg-card` command palette
|
||||
- **[inline-rename.tsx](apps/web/app/components/workspace/inline-rename.tsx)** — `bg-card` input, `border-border`
|
||||
- **[knowledge-tree.tsx](apps/web/app/components/workspace/knowledge-tree.tsx)** — Theme-aware tree styles
|
||||
- **[charts/](apps/web/app/components/charts/)** — All chart components: CSS var chart colors, `bg-card` panels
|
||||
- **[sidebar.tsx](apps/web/app/components/sidebar.tsx)** — Theme-aware (if still used)
|
||||
|
||||
### Phase 10 — Package Dependencies
|
||||
|
||||
**[package.json](apps/web/package.json)** — Add if needed:
|
||||
|
||||
- `framer-motion` (for landing page + dashboard chat animations)
|
||||
- `next-themes` (for dark/light toggle with `localStorage` + class-based switching, SSR-safe)
|
||||
- Verify `next/font/google` is available (bundled with Next.js)
|
||||
|
||||
---
|
||||
|
||||
## Key Design Tokens
|
||||
|
||||
- **Radius:** `0.5rem` base
|
||||
- **Primary font:** Inter via `next/font/google`
|
||||
- **Heading font:** Instrument Serif via Google Fonts import
|
||||
- **Brand font:** Lora via `next/font/google`
|
||||
- **Sidebar width:** 260px
|
||||
- **Shadows (light):** `shadow-[0_0_40px_rgba(0,0,0,0.05)]` (sidebar/navbar), `shadow-[0_0_21px_0_rgba(0,0,0,0.07)]` (search)
|
||||
- **Shadows (dark):** `shadow-[0_0_40px_rgba(0,0,0,0.2)]` (sidebar/navbar), `shadow-[0_0_21px_0_rgba(0,0,0,0.15)]` (search)
|
||||
|
||||
## Component Styling Rules (Theme-Safe)
|
||||
|
||||
All components MUST use semantic CSS variable-backed utilities — never hardcoded colors:
|
||||
|
||||
- `bg-background` / `bg-card` / `bg-muted` / `bg-accent` — not `bg-white`, `bg-neutral-50`, `bg-[#1a1a1a]`
|
||||
- `text-foreground` / `text-muted-foreground` / `text-card-foreground` — not `text-black`, `text-gray-500`
|
||||
- `border-border` — not `border-neutral-200`, `border-[#2e2e2e]`
|
||||
- `bg-sidebar` for sidebar/navbar backgrounds
|
||||
- For shadows that differ between themes: use a CSS variable `--shadow-subtle` / `--shadow-elevated` or conditional `dark:shadow-*` utilities
|
||||
- Exceptions: Dench-specific decorative elements (landing page traffic-light dots, brand colors) can use fixed values
|
||||
@ -657,7 +657,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
🦞
|
||||
</p>
|
||||
<h3 className="text-lg font-semibold mb-1">
|
||||
OpenClaw Chat
|
||||
Ironclaw Chat
|
||||
</h3>
|
||||
<p
|
||||
className="text-sm"
|
||||
@ -740,7 +740,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
||||
placeholder={
|
||||
compact && fileContext
|
||||
? `Ask about ${fileContext.filename}...`
|
||||
: "Message OpenClaw..."
|
||||
: "Message Ironclaw..."
|
||||
}
|
||||
disabled={
|
||||
isStreaming ||
|
||||
|
||||
@ -402,7 +402,7 @@ export function Sidebar({
|
||||
{/* Header with New Chat button */}
|
||||
<div className="px-4 py-4 border-b border-[var(--color-border)] flex items-center justify-between">
|
||||
<h1 className="text-base font-bold flex items-center gap-2">
|
||||
<span>OpenClaw Dench</span>
|
||||
<span>Ironclaw</span>
|
||||
</h1>
|
||||
<button
|
||||
onClick={onNewSession}
|
||||
|
||||
@ -27,13 +27,16 @@ body {
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-muted);
|
||||
}
|
||||
@ -66,13 +69,20 @@ body {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.workspace-prose h2 {
|
||||
font-size: 1.375rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: 0.4rem;
|
||||
}
|
||||
.workspace-prose h3 { font-size: 1.125rem; }
|
||||
.workspace-prose h4 { font-size: 1rem; }
|
||||
|
||||
.workspace-prose h3 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.workspace-prose h4 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.workspace-prose p {
|
||||
margin-bottom: 1em;
|
||||
@ -83,6 +93,7 @@ body {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.workspace-prose a:hover {
|
||||
color: #93bbfd;
|
||||
}
|
||||
@ -114,8 +125,8 @@ body {
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.workspace-prose li > ul,
|
||||
.workspace-prose li > ol {
|
||||
.workspace-prose li>ul,
|
||||
.workspace-prose li>ol {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@ -305,7 +316,7 @@ body {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.editor-content-area .tiptap ul[data-type="taskList"] li > div {
|
||||
.editor-content-area .tiptap ul[data-type="taskList"] li>div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
64
apps/web/package-lock.json
generated
64
apps/web/package-lock.json
generated
@ -27,7 +27,10 @@
|
||||
"@tiptap/starter-kit": "^3.19.0",
|
||||
"@tiptap/suggestion": "^3.19.0",
|
||||
"ai": "^6.0.73",
|
||||
"framer-motion": "^12.34.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"next": "^15.3.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
@ -2755,6 +2758,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.34.0",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.0.tgz",
|
||||
"integrity": "sha512-+/H49owhzkzQyxtn7nZeF4kdH++I2FWrESQ184Zbcw5cEqNHYkE5yxWxcTLSj5lNx3NWdbIRy5FHqUvetD8FWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.34.0",
|
||||
"motion-utils": "^12.29.2",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@ -2770,6 +2800,15 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fuse.js": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
|
||||
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-jsx-runtime": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
||||
@ -3814,6 +3853,21 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.34.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.0.tgz",
|
||||
"integrity": "sha512-Lql3NuEcScRDxTAO6GgUsRHBZOWI/3fnMlkMcH5NftzcN37zJta+bpbMAV9px4Nj057TuvRooMK7QrzMCgtz6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.29.2"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.29.2",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz",
|
||||
"integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@ -3843,6 +3897,16 @@
|
||||
"resolved": "../../node_modules/.pnpm/next@15.3.3_react-dom@19.1.0_react@19.1.0/node_modules/next",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/next-themes": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
|
||||
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||
|
||||
@ -29,7 +29,10 @@
|
||||
"@tiptap/starter-kit": "^3.19.0",
|
||||
"@tiptap/suggestion": "^3.19.0",
|
||||
"ai": "^6.0.73",
|
||||
"framer-motion": "^12.34.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"next": "^15.3.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
|
||||
BIN
apps/web/public/fonts/Bookerly-Bold.ttf
Normal file
BIN
apps/web/public/fonts/Bookerly-Bold.ttf
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/Bookerly-BoldItalic.ttf
Normal file
BIN
apps/web/public/fonts/Bookerly-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/Bookerly-Regular.ttf
Normal file
BIN
apps/web/public/fonts/Bookerly-Regular.ttf
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/Bookerly-RegularItalic.ttf
Normal file
BIN
apps/web/public/fonts/Bookerly-RegularItalic.ttf
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/FoundationTitlesHand.ttf
Normal file
BIN
apps/web/public/fonts/FoundationTitlesHand.ttf
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/SpaceGrotesk-Bold.ttf
Normal file
BIN
apps/web/public/fonts/SpaceGrotesk-Bold.ttf
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/SpaceGrotesk-Light.ttf
Normal file
BIN
apps/web/public/fonts/SpaceGrotesk-Light.ttf
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/SpaceGrotesk-Medium.ttf
Normal file
BIN
apps/web/public/fonts/SpaceGrotesk-Medium.ttf
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/SpaceGrotesk-Regular.ttf
Normal file
BIN
apps/web/public/fonts/SpaceGrotesk-Regular.ttf
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/SpaceGrotesk-SemiBold.ttf
Normal file
BIN
apps/web/public/fonts/SpaceGrotesk-SemiBold.ttf
Normal file
Binary file not shown.
@ -152,6 +152,7 @@
|
||||
"dotenv": "^17.2.4",
|
||||
"express": "^5.2.1",
|
||||
"file-type": "^21.3.0",
|
||||
"gradient-string": "^3.0.0",
|
||||
"grammy": "^1.39.3",
|
||||
"hono": "4.11.9",
|
||||
"jiti": "^2.6.1",
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import gradient from "gradient-string";
|
||||
import { resolveCommitHash } from "../infra/git-commit.js";
|
||||
import { visibleWidth } from "../terminal/ansi.js";
|
||||
import { isRich, theme } from "../terminal/theme.js";
|
||||
@ -12,103 +13,130 @@ type BannerOptions = TaglineOptions & {
|
||||
|
||||
let bannerEmitted = false;
|
||||
|
||||
const graphemeSegmenter =
|
||||
typeof Intl !== "undefined" && "Segmenter" in Intl
|
||||
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
||||
: null;
|
||||
|
||||
function splitGraphemes(value: string): string[] {
|
||||
if (!graphemeSegmenter) {
|
||||
return Array.from(value);
|
||||
}
|
||||
try {
|
||||
return Array.from(graphemeSegmenter.segment(value), (seg) => seg.segment);
|
||||
} catch {
|
||||
return Array.from(value);
|
||||
}
|
||||
}
|
||||
|
||||
const hasJsonFlag = (argv: string[]) =>
|
||||
argv.some((arg) => arg === "--json" || arg.startsWith("--json="));
|
||||
|
||||
const hasVersionFlag = (argv: string[]) =>
|
||||
argv.some((arg) => arg === "--version" || arg === "-V" || arg === "-v");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IRONCLAW ASCII art (figlet "ANSI Shadow" font, baked at build time)
|
||||
// ---------------------------------------------------------------------------
|
||||
const IRONCLAW_ASCII = [
|
||||
" ██╗██████╗ ██████╗ ███╗ ██╗ ██████╗██╗ █████╗ ██╗ ██╗",
|
||||
" ██║██╔══██╗██╔═══██╗████╗ ██║██╔════╝██║ ██╔══██╗██║ ██║",
|
||||
" ██║██████╔╝██║ ██║██╔██╗ ██║██║ ██║ ███████║██║ █╗ ██║",
|
||||
" ██║██╔══██╗██║ ██║██║╚██╗██║██║ ██║ ██╔══██║██║███╗██║",
|
||||
" ██║██║ ██║╚██████╔╝██║ ╚████║╚██████╗███████╗██║ ██║╚███╔███╔╝",
|
||||
" ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝ ",
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Iron-metallic gradient colors (dark iron → bright silver → dark iron)
|
||||
// ---------------------------------------------------------------------------
|
||||
const IRON_GRADIENT_COLORS = [
|
||||
"#374151", // dark iron
|
||||
"#4B5563",
|
||||
"#6B7280", // medium iron
|
||||
"#9CA3AF", // steel
|
||||
"#D1D5DB", // bright silver
|
||||
"#F3F4F6", // near-white highlight
|
||||
"#D1D5DB",
|
||||
"#9CA3AF",
|
||||
"#6B7280",
|
||||
"#4B5563",
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Gradient animation helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function rotateArray<T>(arr: T[], offset: number): T[] {
|
||||
const n = arr.length;
|
||||
const o = ((offset % n) + n) % n;
|
||||
return [...arr.slice(o), ...arr.slice(0, o)];
|
||||
}
|
||||
|
||||
function renderGradientFrame(lines: string[], frame: number): string {
|
||||
const colors = rotateArray(IRON_GRADIENT_COLORS, frame);
|
||||
return gradient(colors).multiline(lines.join("\n"));
|
||||
}
|
||||
|
||||
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
/**
|
||||
* Play the iron shimmer animation: a bright highlight sweeps across the
|
||||
* ASCII art like light glinting off polished metal. Runs for ~2.5 seconds
|
||||
* at 12 fps, completing 3 full gradient cycles.
|
||||
*/
|
||||
async function animateIronBanner(): Promise<void> {
|
||||
const lineCount = IRONCLAW_ASCII.length;
|
||||
const fps = 12;
|
||||
const totalFrames = IRON_GRADIENT_COLORS.length * 3; // 3 full shimmer sweeps
|
||||
const frameMs = Math.round(1000 / fps);
|
||||
|
||||
// Print the first frame to claim vertical space
|
||||
process.stdout.write(renderGradientFrame(IRONCLAW_ASCII, 0) + "\n");
|
||||
|
||||
for (let frame = 1; frame < totalFrames; frame++) {
|
||||
await sleep(frameMs);
|
||||
// Move cursor up to overwrite the previous frame
|
||||
process.stdout.write(`\x1b[${lineCount}A\r`);
|
||||
process.stdout.write(renderGradientFrame(IRONCLAW_ASCII, frame) + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static (non-animated) banner rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function formatCliBannerArt(options: BannerOptions = {}): string {
|
||||
const rich = options.richTty ?? isRich();
|
||||
if (!rich) {
|
||||
return IRONCLAW_ASCII.join("\n");
|
||||
}
|
||||
return renderGradientFrame(IRONCLAW_ASCII, 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// One-line version + tagline (prints below the ASCII art)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function formatCliBannerLine(version: string, options: BannerOptions = {}): string {
|
||||
const commit = options.commit ?? resolveCommitHash({ env: options.env });
|
||||
const commitLabel = commit ?? "unknown";
|
||||
const tagline = pickTagline(options);
|
||||
const rich = options.richTty ?? isRich();
|
||||
const title = "🦞 OpenClaw";
|
||||
const prefix = "🦞 ";
|
||||
const title = "IRONCLAW";
|
||||
const prefix = " ";
|
||||
const columns = options.columns ?? process.stdout.columns ?? 120;
|
||||
const plainFullLine = `${title} ${version} (${commitLabel}) — ${tagline}`;
|
||||
const plainFullLine = `${prefix}${title} ${version} (${commitLabel}) — ${tagline}`;
|
||||
const fitsOnOneLine = visibleWidth(plainFullLine) <= columns;
|
||||
if (rich) {
|
||||
if (fitsOnOneLine) {
|
||||
return `${theme.heading(title)} ${theme.info(version)} ${theme.muted(
|
||||
return `${prefix}${theme.heading(title)} ${theme.info(version)} ${theme.muted(
|
||||
`(${commitLabel})`,
|
||||
)} ${theme.muted("—")} ${theme.accentDim(tagline)}`;
|
||||
}
|
||||
const line1 = `${theme.heading(title)} ${theme.info(version)} ${theme.muted(
|
||||
const line1 = `${prefix}${theme.heading(title)} ${theme.info(version)} ${theme.muted(
|
||||
`(${commitLabel})`,
|
||||
)}`;
|
||||
const line2 = `${" ".repeat(prefix.length)}${theme.accentDim(tagline)}`;
|
||||
const line2 = `${prefix}${theme.accentDim(tagline)}`;
|
||||
return `${line1}\n${line2}`;
|
||||
}
|
||||
if (fitsOnOneLine) {
|
||||
return plainFullLine;
|
||||
}
|
||||
const line1 = `${title} ${version} (${commitLabel})`;
|
||||
const line2 = `${" ".repeat(prefix.length)}${tagline}`;
|
||||
const line1 = `${prefix}${title} ${version} (${commitLabel})`;
|
||||
const line2 = `${prefix}${tagline}`;
|
||||
return `${line1}\n${line2}`;
|
||||
}
|
||||
|
||||
const LOBSTER_ASCII = [
|
||||
"▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄",
|
||||
"██░▄▄▄░██░▄▄░██░▄▄▄██░▀██░██░▄▄▀██░████░▄▄▀██░███░██",
|
||||
"██░███░██░▀▀░██░▄▄▄██░█░█░██░█████░████░▀▀░██░█░█░██",
|
||||
"██░▀▀▀░██░█████░▀▀▀██░██▄░██░▀▀▄██░▀▀░█░██░██▄▀▄▀▄██",
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
|
||||
" 🦞 OPENCLAW 🦞 ",
|
||||
" ",
|
||||
];
|
||||
// ---------------------------------------------------------------------------
|
||||
// Emit the full banner (animated ASCII art + version line)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function formatCliBannerArt(options: BannerOptions = {}): string {
|
||||
const rich = options.richTty ?? isRich();
|
||||
if (!rich) {
|
||||
return LOBSTER_ASCII.join("\n");
|
||||
}
|
||||
|
||||
const colorChar = (ch: string) => {
|
||||
if (ch === "█") {
|
||||
return theme.accentBright(ch);
|
||||
}
|
||||
if (ch === "░") {
|
||||
return theme.accentDim(ch);
|
||||
}
|
||||
if (ch === "▀") {
|
||||
return theme.accent(ch);
|
||||
}
|
||||
return theme.muted(ch);
|
||||
};
|
||||
|
||||
const colored = LOBSTER_ASCII.map((line) => {
|
||||
if (line.includes("OPENCLAW")) {
|
||||
return (
|
||||
theme.muted(" ") +
|
||||
theme.accent("🦞") +
|
||||
theme.info(" OPENCLAW ") +
|
||||
theme.accent("🦞")
|
||||
);
|
||||
}
|
||||
return splitGraphemes(line).map(colorChar).join("");
|
||||
});
|
||||
|
||||
return colored.join("\n");
|
||||
}
|
||||
|
||||
export function emitCliBanner(version: string, options: BannerOptions = {}) {
|
||||
export async function emitCliBanner(version: string, options: BannerOptions = {}) {
|
||||
if (bannerEmitted) {
|
||||
return;
|
||||
}
|
||||
@ -122,9 +150,22 @@ export function emitCliBanner(version: string, options: BannerOptions = {}) {
|
||||
if (hasVersionFlag(argv)) {
|
||||
return;
|
||||
}
|
||||
const line = formatCliBannerLine(version, options);
|
||||
process.stdout.write(`\n${line}\n\n`);
|
||||
|
||||
bannerEmitted = true;
|
||||
const rich = options.richTty ?? isRich();
|
||||
|
||||
process.stdout.write("\n");
|
||||
|
||||
if (rich) {
|
||||
// Animated iron shimmer
|
||||
await animateIronBanner();
|
||||
} else {
|
||||
// Plain ASCII fallback
|
||||
process.stdout.write(IRONCLAW_ASCII.join("\n") + "\n");
|
||||
}
|
||||
|
||||
const line = formatCliBannerLine(version, options);
|
||||
process.stdout.write(`${line}\n\n`);
|
||||
}
|
||||
|
||||
export function hasEmittedCliBanner(): boolean {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import path from "node:path";
|
||||
|
||||
export const DEFAULT_CLI_NAME = "openclaw";
|
||||
export const DEFAULT_CLI_NAME = "ironclaw";
|
||||
|
||||
const KNOWN_CLI_NAMES = new Set([DEFAULT_CLI_NAME]);
|
||||
const CLI_PREFIX_RE = /^(?:((?:pnpm|npm|bunx|npx)\s+))?(openclaw)\b/;
|
||||
const KNOWN_CLI_NAMES = new Set([DEFAULT_CLI_NAME, "openclaw"]);
|
||||
const CLI_PREFIX_RE = /^(?:((?:pnpm|npm|bunx|npx)\s+))?(ironclaw|openclaw)\b/;
|
||||
|
||||
export function resolveCliName(argv: string[] = process.argv): string {
|
||||
const argv1 = argv[1];
|
||||
|
||||
@ -33,12 +33,13 @@ export function registerPreActionHooks(program: Command, programVersion: string)
|
||||
}
|
||||
const commandPath = getCommandPath(argv, 2);
|
||||
const hideBanner =
|
||||
isTruthyEnvValue(process.env.IRONCLAW_HIDE_BANNER) ||
|
||||
isTruthyEnvValue(process.env.OPENCLAW_HIDE_BANNER) ||
|
||||
commandPath[0] === "update" ||
|
||||
commandPath[0] === "completion" ||
|
||||
(commandPath[0] === "plugins" && commandPath[1] === "update");
|
||||
if (!hideBanner) {
|
||||
emitCliBanner(programVersion);
|
||||
await emitCliBanner(programVersion);
|
||||
}
|
||||
const verbose = getVerboseFlag(argv, { includeDebug: true });
|
||||
setVerbose(verbose);
|
||||
|
||||
@ -12,7 +12,7 @@ async function prepareRoutedCommand(params: {
|
||||
commandPath: string[];
|
||||
loadPlugins?: boolean;
|
||||
}) {
|
||||
emitCliBanner(VERSION, { argv: params.argv });
|
||||
await emitCliBanner(VERSION, { argv: params.argv });
|
||||
await ensureConfigReady({ runtime: defaultRuntime, commandPath: params.commandPath });
|
||||
if (params.loadPlugins) {
|
||||
ensurePluginRegistryLoaded();
|
||||
|
||||
@ -3,14 +3,16 @@ import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { loadDotEnv } from "../infra/dotenv.js";
|
||||
import { normalizeEnv } from "../infra/env.js";
|
||||
import { isTruthyEnvValue, normalizeEnv } from "../infra/env.js";
|
||||
import { formatUncaughtError } from "../infra/errors.js";
|
||||
import { isMainModule } from "../infra/is-main.js";
|
||||
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
|
||||
import { assertSupportedRuntime } from "../infra/runtime-guard.js";
|
||||
import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
|
||||
import { enableConsoleCapture } from "../logging.js";
|
||||
import { getPrimaryCommand, hasHelpOrVersion } from "./argv.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import { getCommandPath, getPrimaryCommand, hasHelpOrVersion } from "./argv.js";
|
||||
import { emitCliBanner } from "./banner.js";
|
||||
import { tryRouteCli } from "./route.js";
|
||||
|
||||
export function rewriteUpdateFlagArgv(argv: string[]): string[] {
|
||||
@ -33,6 +35,20 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
// Enforce the minimum supported runtime before doing any work.
|
||||
assertSupportedRuntime();
|
||||
|
||||
// Show the animated Ironclaw banner early so it appears for ALL invocations
|
||||
// (bare `ironclaw`, subcommands, help, etc.). The bannerEmitted flag inside
|
||||
// emitCliBanner prevents double-emission from the route / preAction hooks.
|
||||
const commandPath = getCommandPath(normalizedArgv, 2);
|
||||
const hideBanner =
|
||||
isTruthyEnvValue(process.env.IRONCLAW_HIDE_BANNER) ||
|
||||
isTruthyEnvValue(process.env.OPENCLAW_HIDE_BANNER) ||
|
||||
commandPath[0] === "update" ||
|
||||
commandPath[0] === "completion" ||
|
||||
(commandPath[0] === "plugins" && commandPath[1] === "update");
|
||||
if (!hideBanner) {
|
||||
await emitCliBanner(VERSION, { argv: normalizedArgv });
|
||||
}
|
||||
|
||||
if (await tryRouteCli(normalizedArgv)) {
|
||||
return;
|
||||
}
|
||||
@ -48,7 +64,7 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
installUnhandledRejectionHandler();
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
console.error("[openclaw] Uncaught exception:", formatUncaughtError(error));
|
||||
console.error("[ironclaw] Uncaught exception:", formatUncaughtError(error));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
@ -1,96 +1,88 @@
|
||||
const DEFAULT_TAGLINE = "All your chats, one OpenClaw.";
|
||||
const DEFAULT_TAGLINE = "Forge your workflow. Command your data.";
|
||||
|
||||
const HOLIDAY_TAGLINES = {
|
||||
newYear:
|
||||
"New Year's Day: New year, new config—same old EADDRINUSE, but this time we resolve it like grown-ups.",
|
||||
"New Year's Day: New year, fresh schema — same old EADDRINUSE, but this time we temper it like grown-ups.",
|
||||
lunarNewYear:
|
||||
"Lunar New Year: May your builds be lucky, your branches prosperous, and your merge conflicts chased away with fireworks.",
|
||||
"Lunar New Year: May your builds be lucky, your pipelines prosperous, and your merge conflicts hammered flat on the anvil.",
|
||||
christmas:
|
||||
"Christmas: Ho ho ho—Santa's little claw-sistant is here to ship joy, roll back chaos, and stash the keys safely.",
|
||||
eid: "Eid al-Fitr: Celebration mode: queues cleared, tasks completed, and good vibes committed to main with clean history.",
|
||||
"Christmas: Ho ho ho — Santa's iron-clad assistant is here to ship joy, roll back chaos, and forge the keys safely.",
|
||||
eid: "Eid al-Fitr: Celebration mode: queues cleared, deals closed, and good vibes committed to main with clean history.",
|
||||
diwali:
|
||||
"Diwali: Let the logs sparkle and the bugs flee—today we light up the terminal and ship with pride.",
|
||||
"Diwali: Let the forge glow bright and the bugs flee — today we light up the terminal and ship with pride.",
|
||||
easter:
|
||||
"Easter: I found your missing environment variable—consider it a tiny CLI egg hunt with fewer jellybeans.",
|
||||
"Easter: I found your missing environment variable — consider it a tiny CLI egg hunt with fewer jellybeans.",
|
||||
hanukkah:
|
||||
"Hanukkah: Eight nights, eight retries, zero shame—may your gateway stay lit and your deployments stay peaceful.",
|
||||
"Hanukkah: Eight nights, eight retries, zero shame — may your gateway stay lit and your deployments stay ironclad.",
|
||||
halloween:
|
||||
"Halloween: Spooky season: beware haunted dependencies, cursed caches, and the ghost of node_modules past.",
|
||||
thanksgiving:
|
||||
"Thanksgiving: Grateful for stable ports, working DNS, and a bot that reads the logs so nobody has to.",
|
||||
"Thanksgiving: Grateful for stable ports, working DNS, and an agent that reads the logs so nobody has to.",
|
||||
valentines:
|
||||
"Valentine's Day: Roses are typed, violets are piped—I'll automate the chores so you can spend time with humans.",
|
||||
"Valentine's Day: Roses are typed, violets are piped — I'll automate the chores so you can spend time with humans.",
|
||||
} as const;
|
||||
|
||||
const TAGLINES: string[] = [
|
||||
"Your terminal just grew claws—type something and let the bot pinch the busywork.",
|
||||
// Iron / forge metaphors
|
||||
"Your terminal just grew iron claws — type something and watch it forge results.",
|
||||
"Hot metal, cold data, zero patience for manual entry.",
|
||||
"Tempered in TypeScript, quenched in production.",
|
||||
"Iron sharpens iron — and this CLI sharpens your workflow.",
|
||||
"Gateway online — please keep hands inside the forge at all times.",
|
||||
"I'll refactor your busywork like it owes me steel ingots.",
|
||||
"Forged in the fires of git rebase, cooled by the tears of resolved conflicts.",
|
||||
"The anvil is hot. Your pipeline is hotter.",
|
||||
"Strike while the deploy is hot.",
|
||||
"Built different. Literally — we use DuckDB.",
|
||||
// CRM + data humor
|
||||
"I speak fluent SQL, mild sarcasm, and aggressive pipeline-closing energy.",
|
||||
"One CLI to rule your contacts, your deals, and your sanity.",
|
||||
"If your CRM could bench press, this is what it would look like.",
|
||||
"Your CRM grew claws. Your leads never stood a chance.",
|
||||
"I don't just autocomplete — I auto-close deals (emotionally), then ask you to review (logically).",
|
||||
'Less clicking, more shipping, fewer "where did that contact go" moments.',
|
||||
"I can PIVOT your data, but I can't PIVOT your life choices.",
|
||||
"Your .env is showing; don't worry, the forge keeps secrets.",
|
||||
"If it's repetitive, I'll automate it; if it's hard, I'll bring SQL and a rollback plan.",
|
||||
"I don't judge, but your missing API keys are absolutely judging you.",
|
||||
// General CLI wit
|
||||
"Welcome to the command line: where dreams compile and confidence segfaults.",
|
||||
'I run on caffeine, JSON5, and the audacity of "it worked on my machine."',
|
||||
"Gateway online—please keep hands, feet, and appendages inside the shell at all times.",
|
||||
"I speak fluent bash, mild sarcasm, and aggressive tab-completion energy.",
|
||||
"One CLI to rule them all, and one more restart because you changed the port.",
|
||||
"If it works, it's automation; if it breaks, it's a \"learning opportunity.\"",
|
||||
"Pairing codes exist because even bots believe in consent—and good security hygiene.",
|
||||
"Your .env is showing; don't worry, I'll pretend I didn't see it.",
|
||||
"I'll do the boring stuff while you dramatically stare at the logs like it's cinema.",
|
||||
"I'm not saying your workflow is chaotic... I'm just bringing a linter and a helmet.",
|
||||
"Type the command with confidence—nature will provide the stack trace if needed.",
|
||||
"I don't judge, but your missing API keys are absolutely judging you.",
|
||||
"I can grep it, git blame it, and gently roast it—pick your coping mechanism.",
|
||||
"Type the command with confidence — nature will provide the stack trace if needed.",
|
||||
"I can grep it, git blame it, and gently roast it — pick your coping mechanism.",
|
||||
"Hot reload for config, cold sweat for deploys.",
|
||||
"I'm the assistant your terminal demanded, not the one your sleep schedule requested.",
|
||||
"I keep secrets like a vault... unless you print them in debug logs again.",
|
||||
"Automation with claws: minimal fuss, maximal pinch.",
|
||||
"I'm basically a Swiss Army knife, but with more opinions and fewer sharp edges.",
|
||||
"If you're lost, run doctor; if you're brave, run prod; if you're wise, run tests.",
|
||||
"Your task has been queued; your dignity has been deprecated.",
|
||||
"I can't fix your code taste, but I can fix your build and your backlog.",
|
||||
"I'm not magic—I'm just extremely persistent with retries and coping strategies.",
|
||||
"I'm not magic — I'm just extremely persistent with retries and coping strategies.",
|
||||
'It\'s not "failing," it\'s "discovering new ways to configure the same thing wrong."',
|
||||
"Give me a workspace and I'll give you fewer tabs, fewer toggles, and more oxygen.",
|
||||
"I read logs so you can keep pretending you don't have to.",
|
||||
"If something's on fire, I can't extinguish it—but I can write a beautiful postmortem.",
|
||||
"I'll refactor your busywork like it owes me money.",
|
||||
'Say "stop" and I\'ll stop—say "ship" and we\'ll both learn a lesson.',
|
||||
"If something's on fire, I can't extinguish it — but I can write a beautiful postmortem.",
|
||||
'Say "stop" and I\'ll stop — say "ship" and we\'ll both learn a lesson.',
|
||||
"I'm the reason your shell history looks like a hacker-movie montage.",
|
||||
"I'm like tmux: confusing at first, then suddenly you can't live without me.",
|
||||
"I can run local, remote, or purely on vibes—results may vary with DNS.",
|
||||
"If you can describe it, I can probably automate it—or at least make it funnier.",
|
||||
"I can run local, remote, or purely on vibes — results may vary with DNS.",
|
||||
"If you can describe it, I can probably automate it — or at least make it funnier.",
|
||||
"Your config is valid, your assumptions are not.",
|
||||
"I don't just autocomplete—I auto-commit (emotionally), then ask you to review (logically).",
|
||||
'Less clicking, more shipping, fewer "where did that file go" moments.',
|
||||
"Claws out, commit in—let's ship something mildly responsible.",
|
||||
"I'll butter your workflow like a lobster roll: messy, delicious, effective.",
|
||||
"Shell yeah—I'm here to pinch the toil and leave you the glory.",
|
||||
"If it's repetitive, I'll automate it; if it's hard, I'll bring jokes and a rollback plan.",
|
||||
"Because texting yourself reminders is so 2024.",
|
||||
// Multi-channel / product
|
||||
"Your inbox, your infra, your rules.",
|
||||
'Turning "I\'ll reply later" into "my bot replied instantly".',
|
||||
"The only crab in your contacts you actually want to hear from. 🦞",
|
||||
'Turning "I\'ll reply later" into "my agent replied instantly".',
|
||||
"Chat automation for people who peaked at IRC.",
|
||||
"Because Siri wasn't answering at 3AM.",
|
||||
"IPC, but it's your phone.",
|
||||
"The UNIX philosophy meets your DMs.",
|
||||
"curl for conversations.",
|
||||
"Less middlemen, more messages.",
|
||||
"Ship fast, log faster.",
|
||||
"End-to-end encrypted, drama-to-drama excluded.",
|
||||
"The only bot that stays out of your training set.",
|
||||
'WhatsApp automation without the "please accept our new privacy policy".',
|
||||
"Chat APIs that don't require a Senate hearing.",
|
||||
"Meta wishes they shipped this fast.",
|
||||
"Because the right answer is usually a script.",
|
||||
"Your messages, your servers, your control.",
|
||||
"OpenAI-compatible, not OpenAI-dependent.",
|
||||
"iMessage green bubble energy, but for everyone.",
|
||||
"Siri's competent cousin.",
|
||||
"Works on Android. Crazy concept, we know.",
|
||||
"No $999 stand required.",
|
||||
"We ship features faster than Apple ships calculator updates.",
|
||||
"Your AI assistant, now without the $3,499 headset.",
|
||||
"Think different. Actually think.",
|
||||
"Ah, the fruit tree company! 🍎",
|
||||
"Greetings, Professor Falken",
|
||||
"Because the right answer is usually a script.",
|
||||
// Holiday taglines (gated by date rules below)
|
||||
HOLIDAY_TAGLINES.newYear,
|
||||
HOLIDAY_TAGLINES.lunarNewYear,
|
||||
HOLIDAY_TAGLINES.christmas,
|
||||
@ -253,7 +245,8 @@ export function activeTaglines(options: TaglineOptions = {}): string[] {
|
||||
|
||||
export function pickTagline(options: TaglineOptions = {}): string {
|
||||
const env = options.env ?? process.env;
|
||||
const override = env?.OPENCLAW_TAGLINE_INDEX;
|
||||
// Check Ironclaw env first, fall back to legacy OpenClaw env
|
||||
const override = env?.IRONCLAW_TAGLINE_INDEX ?? env?.OPENCLAW_TAGLINE_INDEX;
|
||||
if (override !== undefined) {
|
||||
const parsed = Number.parseInt(override, 10);
|
||||
if (!Number.isNaN(parsed) && parsed >= 0) {
|
||||
|
||||
19
src/entry.ts
19
src/entry.ts
@ -7,7 +7,7 @@ import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js";
|
||||
import { installProcessWarningFilter } from "./infra/warning-filter.js";
|
||||
import { attachChildProcessBridge } from "./process/child-process-bridge.js";
|
||||
|
||||
process.title = "openclaw";
|
||||
process.title = "ironclaw";
|
||||
installProcessWarningFilter();
|
||||
normalizeEnv();
|
||||
|
||||
@ -32,10 +32,16 @@ function hasExperimentalWarningSuppressed(): boolean {
|
||||
}
|
||||
|
||||
function ensureExperimentalWarningSuppressed(): boolean {
|
||||
if (isTruthyEnvValue(process.env.OPENCLAW_NO_RESPAWN)) {
|
||||
if (
|
||||
isTruthyEnvValue(process.env.IRONCLAW_NO_RESPAWN) ||
|
||||
isTruthyEnvValue(process.env.OPENCLAW_NO_RESPAWN)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (isTruthyEnvValue(process.env.OPENCLAW_NODE_OPTIONS_READY)) {
|
||||
if (
|
||||
isTruthyEnvValue(process.env.IRONCLAW_NODE_OPTIONS_READY) ||
|
||||
isTruthyEnvValue(process.env.OPENCLAW_NODE_OPTIONS_READY)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (hasExperimentalWarningSuppressed()) {
|
||||
@ -43,6 +49,7 @@ function ensureExperimentalWarningSuppressed(): boolean {
|
||||
}
|
||||
|
||||
// Respawn guard (and keep recursion bounded if something goes wrong).
|
||||
process.env.IRONCLAW_NODE_OPTIONS_READY = "1";
|
||||
process.env.OPENCLAW_NODE_OPTIONS_READY = "1";
|
||||
// Pass flag as a Node CLI option, not via NODE_OPTIONS (--disable-warning is disallowed in NODE_OPTIONS).
|
||||
const child = spawn(
|
||||
@ -66,7 +73,7 @@ function ensureExperimentalWarningSuppressed(): boolean {
|
||||
|
||||
child.once("error", (error) => {
|
||||
console.error(
|
||||
"[openclaw] Failed to respawn CLI:",
|
||||
"[ironclaw] Failed to respawn CLI:",
|
||||
error instanceof Error ? (error.stack ?? error.message) : error,
|
||||
);
|
||||
process.exit(1);
|
||||
@ -149,7 +156,7 @@ if (!ensureExperimentalWarningSuppressed()) {
|
||||
const parsed = parseCliProfileArgs(process.argv);
|
||||
if (!parsed.ok) {
|
||||
// Keep it simple; Commander will handle rich help/errors after we strip flags.
|
||||
console.error(`[openclaw] ${parsed.error}`);
|
||||
console.error(`[ironclaw] ${parsed.error}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
@ -163,7 +170,7 @@ if (!ensureExperimentalWarningSuppressed()) {
|
||||
.then(({ runCli }) => runCli(process.argv))
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"[openclaw] Failed to start CLI:",
|
||||
"[ironclaw] Failed to start CLI:",
|
||||
error instanceof Error ? (error.stack ?? error.message) : error,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
// Lobster palette tokens for CLI/UI theming. "lobster seam" == use this palette.
|
||||
// Iron palette tokens for CLI/UI theming. "iron seam" == use this palette.
|
||||
// Keep in sync with docs/cli/index.md (CLI palette section).
|
||||
export const LOBSTER_PALETTE = {
|
||||
accent: "#FF5A2D",
|
||||
accentBright: "#FF7A3D",
|
||||
accentDim: "#D14A22",
|
||||
info: "#FF8A5B",
|
||||
success: "#2FBF71",
|
||||
warn: "#FFB020",
|
||||
error: "#E23D2D",
|
||||
muted: "#8B7F77",
|
||||
export const IRON_PALETTE = {
|
||||
accent: "#9CA3AF", // cool steel grey
|
||||
accentBright: "#D1D5DB", // bright silver highlight
|
||||
accentDim: "#6B7280", // dark iron
|
||||
info: "#93C5FD", // steel blue
|
||||
success: "#34D399", // emerald
|
||||
warn: "#FBBF24", // amber
|
||||
error: "#F87171", // red
|
||||
muted: "#6B7280", // iron grey
|
||||
} as const;
|
||||
|
||||
// Backward-compatible alias for any external importers.
|
||||
export { IRON_PALETTE as LOBSTER_PALETTE };
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import chalk, { Chalk } from "chalk";
|
||||
import { LOBSTER_PALETTE } from "./palette.js";
|
||||
import { IRON_PALETTE } from "./palette.js";
|
||||
|
||||
const hasForceColor =
|
||||
typeof process.env.FORCE_COLOR === "string" &&
|
||||
@ -11,17 +11,17 @@ const baseChalk = process.env.NO_COLOR && !hasForceColor ? new Chalk({ level: 0
|
||||
const hex = (value: string) => baseChalk.hex(value);
|
||||
|
||||
export const theme = {
|
||||
accent: hex(LOBSTER_PALETTE.accent),
|
||||
accentBright: hex(LOBSTER_PALETTE.accentBright),
|
||||
accentDim: hex(LOBSTER_PALETTE.accentDim),
|
||||
info: hex(LOBSTER_PALETTE.info),
|
||||
success: hex(LOBSTER_PALETTE.success),
|
||||
warn: hex(LOBSTER_PALETTE.warn),
|
||||
error: hex(LOBSTER_PALETTE.error),
|
||||
muted: hex(LOBSTER_PALETTE.muted),
|
||||
heading: baseChalk.bold.hex(LOBSTER_PALETTE.accent),
|
||||
command: hex(LOBSTER_PALETTE.accentBright),
|
||||
option: hex(LOBSTER_PALETTE.warn),
|
||||
accent: hex(IRON_PALETTE.accent),
|
||||
accentBright: hex(IRON_PALETTE.accentBright),
|
||||
accentDim: hex(IRON_PALETTE.accentDim),
|
||||
info: hex(IRON_PALETTE.info),
|
||||
success: hex(IRON_PALETTE.success),
|
||||
warn: hex(IRON_PALETTE.warn),
|
||||
error: hex(IRON_PALETTE.error),
|
||||
muted: hex(IRON_PALETTE.muted),
|
||||
heading: baseChalk.bold.hex(IRON_PALETTE.accent),
|
||||
command: hex(IRON_PALETTE.accentBright),
|
||||
option: hex(IRON_PALETTE.warn),
|
||||
} as const;
|
||||
|
||||
export const isRich = () => Boolean(baseChalk.level > 0);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user