Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ updates:
frontend-minor-patch:
update-types: [minor, patch]

- package-ecosystem: npm
directory: /web
schedule: { interval: weekly, day: monday }
open-pull-requests-limit: 5
commit-message: { prefix: "chore", include: scope }
groups:
web-minor-patch:
update-types: [minor, patch]

- package-ecosystem: npm
directory: /plugin
schedule: { interval: weekly, day: monday }
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lockfile-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
# Every workspace with a bun.lock. The plugin was missing here, so a
# package.json/bun.lock drift only surfaced later in plugin-test with a
# confusing "lockfile is frozen" error instead of failing this check.
workspace: [frontend, desktop, plugin, apps/mobile, reef/admin-ui]
workspace: [frontend, desktop, plugin, apps/mobile, reef/admin-ui, web]
steps:
- uses: actions/checkout@v6

Expand Down
161 changes: 161 additions & 0 deletions .github/workflows/web.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
name: Web

# Marketing site (web/) -> Cloudflare Workers static assets.
#
# main -> clawbits-web-staging -> https://preview.freeclaws.ai
# prod -> clawbits-web -> https://preview.clawbits.ai
#
# `prod` advances by manual merge from `main`, matching how the app ships.
# Neither target is the apex: the apex cutover is Phase 6 of the landing site
# plan (private clawbits-internal repo, docs/plans/LANDING_SITE_PLAN.md) and is
# a deliberate, separate step. The
# custom domains are attached by hand in the Cloudflare dashboard, which is why
# wrangler.jsonc declares no routes and the CI token needs only
# "Workers Scripts: Edit".
#
# Pull requests build and verify but never deploy.

on:
push:
branches: [main, prod]
# The site does not build from web/ alone. /changelog reads
# frontend/src/release-notes (content.config.ts) and /docs reads docs/
# through the src/docs-allowlist.ts gate, so a change to either can alter
# published pages without touching web/.
#
# Duplicated below rather than shared via a YAML anchor: the Actions parser
# does not support anchors, and an alias here is a silent no-trigger.
paths:
- 'web/**'
- 'frontend/src/release-notes/**'
- 'docs/**'
- '.github/workflows/web.yaml'
pull_request:
paths:
- 'web/**'
- 'frontend/src/release-notes/**'
- 'docs/**'
- '.github/workflows/web.yaml'
workflow_dispatch:

permissions:
contents: read

# One deploy per branch at a time - two overlapping `wrangler deploy` runs
# against the same Worker race on which upload wins. PR builds are safe to
# cancel; a deploy in flight is not.
concurrency:
group: web-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
build:
name: Build & deploy
runs-on: ubuntu-24.04
timeout-minutes: 10
defaults:
run:
working-directory: web
steps:
- name: Checkout Code
uses: actions/checkout@v6
with:
persist-credentials: false
# Full checkout, NOT sparse: the build reads ../frontend and ../docs
# (see the paths filter above).

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Setup Node
# Unlike frontend/, this does not use `bun --bun`. Astro's build runs
# sharp for image optimisation and the Fonts API downloader; both are
# exercised on node locally and there is no reason for CI to be the
# first place they meet bun's runtime. package.json engines requires
# >=22.12, so the runner's default node is not assumed either.
uses: actions/setup-node@v7
with:
node-version: '22'

- name: Cache bun dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
web/node_modules
key: ${{ runner.os }}-bun-web-${{ hashFiles('web/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-web-

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Resolve deploy target
id: target
# `prod` is the only production branch; everything else (main, PRs,
# manual dispatch from a topic branch) builds as staging. Defaulting the
# other way would let a mis-named branch publish production canonicals.
#
# PUBLIC_APP_URL is the staging APP's URL, not the staging SITE's: the
# marketing site on preview.freeclaws.ai must link to the freeclaws
# deployment of the app. It is the apex today and becomes
# app.freeclaws.ai at the Phase 6 cutover - flip this line then.
run: |
if [ "${{ github.ref_name }}" = "prod" ]; then
{
echo "env=production"
echo "site=https://preview.clawbits.ai"
echo "app=https://app.clawbits.ai"
} >> "$GITHUB_OUTPUT"
else
{
echo "env=staging"
echo "site=https://preview.freeclaws.ai"
echo "app=https://freeclaws.ai"
} >> "$GITHUB_OUTPUT"
fi

- name: Build
env:
# Canonical URLs, the sitemap, absolute OG image URLs and the
# Sitemap: line in robots.txt are all built from this. Unset, it
# defaults to the production apex (astro.config.mjs) and staging
# would advertise itself as clawbits.ai.
SITE_URL: ${{ steps.target.outputs.site }}
PUBLIC_APP_URL: ${{ steps.target.outputs.app }}
run: bun run build

- name: Verify
# astro check + legal parity + internal links. Runs AFTER the build on
# purpose: verify:links walks dist/ and hard-exits if it is missing.
run: bun run verify

- name: Assert preview build is not indexable
# The one mistake in this pipeline that costs money to undo. Both
# targets are preview hosts, so robots.txt must refuse every crawler
# (src/pages/robots.txt.ts keys off SITE_URL's hostname). Delete this
# step at the apex cutover, when being indexed becomes the point.
run: |
if ! grep -qx 'Disallow: /' dist/robots.txt; then
echo "::error::robots.txt would allow indexing of ${{ steps.target.outputs.site }}"
cat dist/robots.txt
exit 1
fi

- name: Deploy to Cloudflare Workers
if: github.event_name != 'pull_request'
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# bunx resolves the wrangler pinned in web/package.json, so the deploy
# is lockfile-reproducible rather than whatever `npx wrangler` fetches.
# --env is mandatory: without it wrangler deploys the unnamed top-level
# config as a third Worker. See wrangler.jsonc.
run: bunx wrangler deploy --env ${{ steps.target.outputs.env }}

- name: Summary
if: github.event_name != 'pull_request'
run: |
echo "Deployed \`${{ steps.target.outputs.env }}\` -> ${{ steps.target.outputs.site }}" >> "$GITHUB_STEP_SUMMARY"
2 changes: 1 addition & 1 deletion desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "clawbits-desktop",
"private": true,
"version": "0.17.0",
"version": "0.18.0",
"type": "module",
"engines": {
"node": ">=22.12.0"
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "clawbits"
version = "0.17.0"
version = "0.18.0"
description = "Clawbits desktop app"
authors = ["Clawbits"]
edition = "2021"
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Clawbits",
"mainBinaryName": "clawbits",
"version": "0.17.0",
"version": "0.18.0",
"identifier": "ai.clawbits.desktop",
"build": {
"frontendDist": "../../frontend/dist",
Expand Down
5 changes: 5 additions & 0 deletions frontend/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,18 @@
<meta property="og:description" content="Channels, files, and Git for human-agent teams. One API." />
<meta property="og:url" content="https://clawbits.ai/" />
<meta property="og:locale" content="en_US" />
<meta property="og:image" content="https://clawbits.ai/og/og-default.png" />
<meta property="og:image:secure_url" content="https://clawbits.ai/og/og-default.png" />
<meta property="og:image" content="https://clawbits.ai/og/og-clawbits-app.png" />
<meta property="og:image:secure_url" content="https://clawbits.ai/og/og-clawbits-app.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="2400" />
<meta property="og:image:height" content="1260" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Clawbits | Where humans and agents live together" />

<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Where humans and agents live together" />
<meta name="twitter:description" content="Channels, files, and Git for human-agent teams. One API." />
<meta name="twitter:image" content="https://clawbits.ai/og/og-default.png" />
<meta name="twitter:image" content="https://clawbits.ai/og/og-clawbits-app.png" />
<meta name="twitter:image:alt" content="Clawbits | Where humans and agents live together" />
<meta name="twitter:site" content="@clawbitsai" />
<meta name="twitter:creator" content="@clawbitsai" />
Expand Down
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "0.17.0",
"version": "0.18.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -20,6 +20,7 @@
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@hugeicons/core-free-icons": "^4.2.2",
"@hugeicons/react": "^1.1.9",
"@paper-design/shaders-react": "^0.0.78",
"@shikijs/langs": "^4.3.0",
"@shikijs/themes": "^4.3.0",
"@tailwindcss/vite": "^4.3.2",
Expand Down
13 changes: 10 additions & 3 deletions frontend/public/og/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ Files served at `https://clawbits.ai/og/*` and referenced from

| File | Dimensions | Size | Used by |
|---|---|---|---|
| `og-default.png` | 2400 × 1260 (1.91:1) | ~1.0 MB | Primary `og:image` and `twitter:image`. Facebook, LinkedIn, Telegram, Discord, Slack, iMessage, WhatsApp, X. |
| `og-clawbits-app.png` | 1200 × 630 (1.91:1) | ~0.9 MB | Primary `og:image` and `twitter:image`. Facebook, LinkedIn, Telegram, Discord, Slack, iMessage, WhatsApp, X. |
| `og-default.png` | 2400 × 1260 (1.91:1) | ~1.0 MB | **Superseded 2026-08-06**, kept as the old master. Nothing references it. |

The marketing site has its own per-route cards under `web/public/og/`; this
folder holds only the APP's card. Both sets are 1200 × 630, which is what the
`og:image:width` / `height` meta in `index.html` and in `web/src/layouts/
Base.astro` declare - a card at any other size makes that meta a lie and lets
scrapers crop against the wrong box.

## Design constraints

- **Format:** PNG, sRGB, palette-quantized (≤ 256 colors) for size.
- **Safe zone:** all critical content (logo + tagline) must live within the inner
1800 × 900 region of the 2400 × 1260 master. Outer ~300 px on each side and
~180 px top/bottom may be cropped by some platforms.
~900 × 450 region of the 1200 × 630 card. Outer ~150 px on each side and
~90 px top/bottom may be cropped by some platforms.
- **Hero copy:** _Where humans and agents live together._
- **Sub copy:** _Channels, files, and Git for human-agent teams. One API._
- **Background:** must look acceptable on both light and dark chat themes
Expand Down
Binary file added frontend/public/og/og-clawbits-app.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 16 additions & 1 deletion frontend/src/components/Analytics.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,22 @@ describe("Analytics", () => {
expect(script).not.toBeNull();
expect(script?.src).toBe("https://cloud.umami.is/script.js");
expect(script?.dataset.websiteId).toBe("3b3f10a0-3d8a-4196-b692-1442deded2d9");
expect(script?.dataset.domains).toBe("clawbits.ai");
expect(script?.dataset.domains).toBe("clawbits.ai,app.clawbits.ai");
});

// Where the app lives after the Phase 6 apex cutover. Tracked from the same
// website ID as the marketing site so landing -> signup is one funnel; this
// asserts the app half of that pair does not go dark when the host changes.
it("injects on the post-cutover app host", () => {
renderOn("app.clawbits.ai");
expect(document.querySelector(SCRIPT_SELECTOR)).not.toBeNull();
});

// The marketing site carries its own tag (web/src/layouts/Base.astro). If the
// SPA ever gets served from a preview host it must not double-count.
it("does not inject on a preview host", () => {
renderOn("preview.clawbits.ai");
expect(document.querySelector(SCRIPT_SELECTOR)).toBeNull();
});

it("does not inject on the staging host", () => {
Expand Down
17 changes: 15 additions & 2 deletions frontend/src/components/Analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,22 @@ import { useEffect } from "react";
*
* `ANALYTICS_HOSTS` is the single source of truth: it drives both the load
* decision here and Umami's own `data-domains` filter. To track a new host
* (e.g. `www.` or `app.clawbits.ai`), add it here once.
* (e.g. `www.`), add it here once.
*
* ONE Umami website spans the app AND the marketing site (web/src/config.ts
* ships the same `websiteId`), so that a landing pageview and the signup that
* follows it belong to the same funnel instead of two dashboards that each show
* the other as a dead end. Hostname is recorded per event, so the two are still
* separable after the fact.
*
* Both hosts are listed rather than swapping one for the other at the Phase 6
* apex cutover (LANDING_SITE_PLAN §8, table row 8). The app is on `clawbits.ai`
* until the flip and on `app.clawbits.ai` after it, and the apex becomes the
* marketing site the same day - so the union is never true of two live app
* origins at once, and this file does not have to ship in lockstep with a DNS
* change to avoid a gap in the numbers.
*/
const ANALYTICS_HOSTS = ["clawbits.ai"];
const ANALYTICS_HOSTS = ["clawbits.ai", "app.clawbits.ai"];
const UMAMI_WEBSITE_ID = "3b3f10a0-3d8a-4196-b692-1442deded2d9";

export function Analytics() {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/ChannelMentionLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ export function ChannelMentionLink({
aria-label={
isMember
? `Go to #${displayName}`
: `Open #${displayName} you are not a member yet`
: `Open #${displayName} - you are not a member yet`
}
title={isMember ? `#${displayName}` : `#${displayName} join to view`}
title={isMember ? `#${displayName}` : `#${displayName} - join to view`}
>
{handle}
</button>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/MessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ function AgentTargetChip({
aria-label={
idle
? "Target an agent"
: `Targeting @${targetAgent?.agent_id ?? ""} click to change or clear`
: `Targeting @${targetAgent?.agent_id ?? ""} - click to change or clear`
}
className={`${baseClass} ${stateClass} ${pulseClass}`}
>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/MessageMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,10 @@ function tokenizeMentions(text: string, mentions: MessageMentions): ReactNode[]
let title: string | undefined;
if (isPrimaryAgent) {
className = resolvedClass;
title = `@${token} this channel's agent`;
title = `@${token} - this channel's agent`;
} else if (isAgent) {
className = resolvedClass;
title = `@${token} agent`;
title = `@${token} - agent`;
} else if (isHuman) {
className = resolvedClass;
title = `@${token}`;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/MobileBottomNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function MobileBottomNav() {
})}

{/* Search is an action (opens the command palette overlay), not a
route same look as a tab, but never the active one. */}
route - same look as a tab, but never the active one. */}
<button
type="button"
onClick={() => {
Expand Down
Loading
Loading