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
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<h1 align="center">flue</h1>

<p align="center"><strong>The desk stops mattering.</strong></p>
<p align="center"><strong>Continue your Claude Code and Codex sessions on any screen.</strong></p>

<p align="center">
<a href="https://github.com/karnstack/flue/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/karnstack/flue/ci.yml?branch=main&label=ci" alt="CI status"></a>
Expand All @@ -24,11 +24,13 @@
</picture>
</p>

Builds, agents and SSH sessions keep running on the machine that owns them.
Every one of them is one tab away, on any screen you have. A small Go daemon
holds the shells and their scrollback, and a web app draws them. Closing the
tab does not kill a session. It only detaches it, and reattaching replays what
you missed.
Start on your laptop. Check in from your phone or iPad. Pick up again at your
desk. The shell stays on the machine; flue moves the view.

A small Go daemon holds the shells and their scrollback, and a web app draws
them. Closing the tab does not kill a session. It only detaches it: the agent
keeps working, the build keeps running, the SSH session stays up, and
reattaching replays what you missed.

- **Sessions outlive the tab.** Close it and the build keeps running.
- **One list, every machine.** Name, tag, pin, group and search the whole
Expand Down
15 changes: 6 additions & 9 deletions docs/FOLLOW-UPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,15 +479,12 @@ documented promise depends on.
Distinct from item 10, which is the daemon's own outbound queue rather than the
relay's.

**Still open, and it changes character on a hosted relay.** Self-hosted, the
person streaming `yes` and the person holding the Cloudflare account are the
same person, so the missing cap costs them their own free tier and nothing
else. On flue.sh one Worker's allowance is shared by everybody: one account's
runaway session is spent against every other account's availability as well
as against the operator's bill. Nothing about a multi-tenant relay makes the
cap harder to write — it makes it the first thing the scale pass has to do,
and the counters that would size it now have to be read off a deployment
rather than off a dogfooding month.
**Still open.** Every relay is one you deployed into your own Cloudflare
account, so the person streaming `yes` and the person holding the account are
the same person: the missing cap costs them their own free tier and nobody
else's. That is what keeps this a follow-up rather than a blocker. The
counters that would size the cap come off a dogfooding month, which is the
work this item is waiting on.
- **`POST /api/pair` is credential-less and internet-reachable, and nothing
rate limits it.** It has to be credential-less — the device presenting a
pairing token is by definition a device holding no credential — and the
Expand Down
10 changes: 5 additions & 5 deletions site/public/llms.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# flue

> flue keeps your terminal sessions alive on the machine that owns them and hands them back on any device you have. A small Go daemon owns the shells and their scrollback, and a web app draws them. Closing the tab does not kill a session. It only detaches it, so a build, an agent run or an SSH session keeps going, and reattaching replays what was missed. One keystroke opens every session on every machine at once.
> flue lets you continue your Claude Code and Codex sessions on any screen: start on your laptop, check in from your phone or iPad, pick up again at your desk. A small Go daemon owns the shells and their scrollback, and a web app draws them, so the shell stays on the machine and flue only moves the view. Closing the tab does not kill a session. It only detaches it, so a build, an agent run or an SSH session keeps going, and reattaching replays what was missed. One keystroke opens every session on every machine at once.

flue is open source (MIT), pre-1.0, and free. It installs as one static Go
binary on macOS, Linux and WSL, with no Node, Python or other toolchain.
Expand All @@ -13,10 +13,10 @@ the daemon's static key pinned when a browser pairs, so the Worker forwards
ciphertext it holds no key for. flue.sh serves docs and downloads and is never
part of the data path.

Status: the local terminal, the login service, the fleet-wide sessions list and
the end-to-end pairing all work. The Cloudflare relay is built and deployable
but has not been through its manual end-to-end gate against a real account.
It is ready to try rather than ready to rely on.
Status: released and in daily use. The local terminal, the login service, the
fleet-wide sessions list, pairing and the Cloudflare relay all work, and the
relay has been through its manual end-to-end gate against a real account. It is
0.x, so commands, flags and the config file can still change between releases.

## Install

Expand Down
5 changes: 4 additions & 1 deletion site/src/components/walkthrough.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@ import { cn } from '@/lib/utils'
export function Walkthrough({
align = 'left',
className,
id,
}: {
/** Follows the section it lands in, the same way InstallBlock does. */
align?: 'left' | 'center'
className?: string
/** Set when something on the page links to the player. */
id?: string
}) {
return (
<figure className={className}>
<figure id={id} className={className}>
<div className="relative aspect-video overflow-hidden rounded-xl bg-zinc-950 ring-1 ring-zinc-950/10 dark:ring-white/10">
<MuxPlayer
playbackId={WALKTHROUGH_PLAYBACK_ID}
Expand Down
6 changes: 3 additions & 3 deletions site/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import type { ReactNode } from 'react'
import { SITE_URL } from '@/lib/site'
import appCss from '@/styles.css?url'

const TITLE = 'flue: every terminal session you have running, one tab away'
const TITLE = 'flue: continue your Claude Code and Codex sessions on any screen'
const DESCRIPTION =
'Builds, agents and SSH sessions keep running on the machine that owns them. Every one of them is one tab away, on any screen you have. One static Go binary, no hosted service.'
'Start on your laptop, check in from your phone or iPad, pick up again at your desk. A small Go daemon holds your terminals, so closing the tab detaches a session instead of killing it. One static Go binary, no hosted service.'

export const Route = createRootRoute({
head: () => ({
Expand All @@ -25,7 +25,7 @@ export const Route = createRootRoute({
{ property: 'og:image:height', content: '630' },
{
property: 'og:image:alt',
content: 'flue: every session on every machine you own, one tab away',
content: 'flue: your agent, build and SSH sessions on every screen you own',
},
{ name: 'twitter:card', content: 'summary_large_image' },
{ name: 'theme-color', media: '(prefers-color-scheme: light)', content: '#ffffff' },
Expand Down
57 changes: 43 additions & 14 deletions site/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { Fragment } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { ArrowRight, Cloud, HardDrive, Lock, Plus, Smartphone } from 'lucide-react'
import { ArrowRight, Cloud, HardDrive, Lock, Play, Plus, Smartphone } from 'lucide-react'

import { CopyCommand } from '@/components/copy-command'
import { FleetWindow, PhoneFrame } from '@/components/mock/fleet'
import { SwitcherWindow } from '@/components/mock/switcher'
import { MockTerminal, ok, output, prompt } from '@/components/mock/terminal'
import { SiteFooter } from '@/components/site-footer'
import { SiteHeader } from '@/components/site-header'
import { Button } from '@/components/ui/button'
import { Walkthrough } from '@/components/walkthrough'
import { GithubMark } from '@/components/wordmark'
import { BREW_CMD, INSTALL_CMD, REPO_URL } from '@/lib/site'
import { BREW_CMD, INSTALL_CMD, REPO_URL, WALKTHROUGH_RUNTIME } from '@/lib/site'

export const Route = createFileRoute('/')({
component: Home,
Expand Down Expand Up @@ -105,17 +106,17 @@ function Eyebrow() {

function Headline() {
return (
<h1 className="max-w-[24ch] text-4xl font-semibold tracking-tight text-balance sm:text-5xl lg:text-6xl">
The desk stops mattering.
<h1 className="max-w-[22ch] text-4xl font-semibold tracking-tight text-balance sm:text-5xl lg:text-6xl">
Continue your Claude Code and Codex sessions on any screen.
</h1>
)
}

function Subline() {
return (
<p className="mt-5 max-w-[52ch] text-lg text-pretty text-muted-foreground">
Builds, agents and SSH sessions keep running on the machine that owns them. Every one of them
is one tab away, on any screen you have.
Start on your laptop. Check in from your phone or iPad. Pick up again at your desk. The shell
stays on the machine; flue moves the view.
</p>
)
}
Expand All @@ -138,6 +139,29 @@ function InstallBlock({ align = 'left' }: { align?: 'left' | 'center' }) {
)
}

/**
* The hero's second action, and deliberately the quieter one.
*
* Copying the install line is the thing this page is for, so the command keeps
* the loud treatment and this gets an outline. It points at the recording
* further down the page rather than at /docs/setup: the reader who wants to
* see it work before running a script into their shell is answered in place,
* without losing the page they were on.
*/
function WatchWalkthrough() {
return (
<Button asChild variant="outline" size="lg" className="mt-8">
<a href="#walkthrough">
<Play className="size-4" aria-hidden="true" />
Watch the setup walkthrough
<span className="font-mono text-sm tabular-nums text-muted-foreground">
{WALKTHROUGH_RUNTIME}
</span>
</a>
</Button>
)
}

/**
* The two halves of the claim in one picture: the fleet on a machine, and the
* ringed row of it open on a phone. They sit beside each other rather than
Expand Down Expand Up @@ -175,6 +199,7 @@ function Hero() {
<div className="w-full">
<InstallBlock align="center" />
</div>
<WatchWalkthrough />
</div>
<div className="mt-16 lg:mt-20">
<ProofMock />
Expand Down Expand Up @@ -205,8 +230,8 @@ function Sessions() {
<p className="font-mono text-sm tracking-wide text-primary uppercase">Why it exists</p>
<blockquote className="mt-6 border-l-2 border-primary/40 pl-5">
<p className="max-w-[48ch] text-lg text-pretty">
I wanted my 10,000 steps. Coding agents had other plans, and I am not buying a
walking pad. So now I start the run on my machine, go for the walk, and read the
I wanted my 10,000 steps. Claude Code and Codex had other plans, and I am not buying
a walking pad. So now I start the run on my machine, go for the walk, and read the
answer on my phone.
</p>
<footer className="mt-3 text-base text-muted-foreground sm:text-sm">
Expand All @@ -221,8 +246,8 @@ function Sessions() {
</h2>
<p className="mt-5 max-w-[48ch] text-lg text-pretty text-muted-foreground">
A small Go daemon holds the terminals and their scrollback. Closing the tab does not
kill the session. It only detaches it. Reattach and the daemon replays what you
missed.
kill the session. It only detaches it: the agent keeps working, the build keeps
running, the SSH session stays up. Reattach and the daemon replays what you missed.
</p>
<p className="mt-4 max-w-[48ch] text-lg text-pretty text-muted-foreground">
Two devices can attach to one session and mirror live. Size follows the view you used
Expand All @@ -236,7 +261,7 @@ function Sessions() {
drawing occupies the same measure the drawing does. */}
<div className="mt-20 grid grid-cols-[minmax(0,1fr)] gap-x-10 gap-y-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] lg:gap-10">
<h3 className="max-w-[40ch] text-2xl font-semibold tracking-tight text-balance">
Finding one of them is the other half.
One keystroke to any session on any machine.
</h3>
<div>
<p className="max-w-[48ch] text-lg text-pretty text-muted-foreground">
Expand Down Expand Up @@ -429,7 +454,7 @@ function Install() {
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
<div className="flex flex-col items-center text-center">
<h2 className="max-w-[30ch] text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
Install it, then close the tab on purpose.
Install flue on the machine that runs the work.
</h2>
<p className="mt-5 max-w-[48ch] text-lg text-pretty text-muted-foreground">
<code className="font-mono text-foreground">flue enable</code> installs a login service,
Expand All @@ -445,8 +470,12 @@ function Install() {
{/* Full width, the way the switcher above sits full width in a
section that is otherwise columns. The caption is centred with
the rest of this section rather than left aligned, since it is
the only line here that would have read off axis. */}
<Walkthrough align="center" className="mt-10 w-full" />
the only line here that would have read off axis.

The id is the hero button's target, and scroll-mt clears the
sticky header so the jump lands on the player rather than under
the bar. */}
<Walkthrough align="center" id="walkthrough" className="mt-10 w-full scroll-mt-20" />
<div className="w-full">
<InstallBlock align="center" />
</div>
Expand Down