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
4 changes: 4 additions & 0 deletions apps/desktop/src/app/DesktopConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ export const DesktopConfig = Config.all({
"BADCODE_DESKTOP_OPEN_DEVTOOLS",
"T3CODE_DESKTOP_OPEN_DEVTOOLS",
),
marketingCaptureMode: Config.boolean("THREADLINES_DESKTOP_MARKETING_CAPTURE").pipe(
Config.option,
Config.map((value) => Option.getOrElse(value, () => false)),
),
mockUpdates: optionalBooleanAlias(
"THREADLINES_DESKTOP_MOCK_UPDATES",
"BADCODE_DESKTOP_MOCK_UPDATES",
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/app/DesktopEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ describe("DesktopEnvironment", () => {
THREADLINES_DESKTOP_USER_DATA_DIR_NAME: " threadlines-marketing-studio ",
THREADLINES_OTLP_TRACES_URL: " http://127.0.0.1:4318/v1/traces ",
THREADLINES_OTLP_EXPORT_INTERVAL_MS: "2500",
THREADLINES_DESKTOP_MARKETING_CAPTURE: "true",
},
);

Expand Down Expand Up @@ -102,6 +103,7 @@ describe("DesktopEnvironment", () => {
assert.deepEqual(environment.otlpTracesUrl, Option.some("http://127.0.0.1:4318/v1/traces"));
assert.equal(environment.otlpExportIntervalMs, 2500);
assert.equal(environment.openDevToolsInDevelopment, false);
assert.equal(environment.marketingCaptureMode, true);
}),
);

Expand All @@ -116,6 +118,7 @@ describe("DesktopEnvironment", () => {
);

assert.equal(environment.openDevToolsInDevelopment, true);
assert.equal(environment.marketingCaptureMode, false);
}),
);

Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/app/DesktopEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export interface DesktopEnvironmentShape {
readonly otlpTracesUrl: Option.Option<string>;
readonly otlpExportIntervalMs: number;
readonly openDevToolsInDevelopment: boolean;
readonly marketingCaptureMode: boolean;
readonly branding: DesktopAppBranding;
readonly displayName: string;
readonly appUserModelId: string;
Expand Down Expand Up @@ -228,6 +229,7 @@ const makeDesktopEnvironment = Effect.fn("desktop.environment.make")(function* (
otlpTracesUrl: config.otlpTracesUrl,
otlpExportIntervalMs: config.otlpExportIntervalMs,
openDevToolsInDevelopment: config.openDevToolsInDevelopment,
marketingCaptureMode: config.marketingCaptureMode,
branding,
displayName,
appUserModelId: isDevelopment ? DESKTOP_DEVELOPMENT_APP_ID : DESKTOP_RELEASE_APP_ID,
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
confirm,
captureScreenshot,
getAppBranding,
isMarketingCaptureMode,
getLocalEnvironmentBootstrap,
openExternal,
pickFolder,
Expand Down Expand Up @@ -80,6 +81,7 @@ export const installDesktopIpcHandlers = Effect.gen(function* () {
const ipc = yield* DesktopIpc.DesktopIpc;

yield* ipc.handleSync(getAppBranding);
yield* ipc.handleSync(isMarketingCaptureMode);
yield* ipc.handleSync(getLocalEnvironmentBootstrap);

yield* ipc.handle(getClientSettings);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
export const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
export const UPDATE_CHECK_CHANNEL = "desktop:update-check";
export const GET_APP_BRANDING_CHANNEL = "desktop:get-app-branding";
export const IS_MARKETING_CAPTURE_MODE_CHANNEL = "desktop:is-marketing-capture-mode";
export const GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL = "desktop:get-local-environment-bootstrap";
export const GET_CLIENT_SETTINGS_CHANNEL = "desktop:get-client-settings";
export const SET_CLIENT_SETTINGS_CHANNEL = "desktop:set-client-settings";
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/ipc/methods/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ export const getAppBranding = makeSyncIpcMethod({
}),
});

export const isMarketingCaptureMode = makeSyncIpcMethod({
channel: IpcChannels.IS_MARKETING_CAPTURE_MODE_CHANNEL,
result: Schema.Boolean,
handler: Effect.fn("desktop.ipc.window.isMarketingCaptureMode")(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
return environment.marketingCaptureMode;
}),
});

export const getLocalEnvironmentBootstrap = makeSyncIpcMethod({
channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL,
result: Schema.NullOr(DesktopEnvironmentBootstrapSchema),
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ contextBridge.exposeInMainWorld("desktopBridge", {
}
return result as ReturnType<DesktopBridge["getAppBranding"]>;
},
isMarketingCaptureMode: () =>
ipcRenderer.sendSync(IpcChannels.IS_MARKETING_CAPTURE_MODE_CHANNEL) === true,
getLocalEnvironmentBootstrap: () => {
const result = ipcRenderer.sendSync(IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL);
if (typeof result !== "object" || result === null) {
Expand Down
51 changes: 51 additions & 0 deletions apps/desktop/src/window/DesktopWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ function makeFakeBrowserWindow(input?: {
};

const window = {
center: vi.fn(),
focus: vi.fn(),
getBounds: vi.fn(() => bounds),
getNormalBounds: vi.fn(() => normalBounds),
Expand All @@ -80,8 +81,11 @@ function makeFakeBrowserWindow(input?: {
once: vi.fn(),
restore: vi.fn(),
setBackgroundColor: vi.fn(),
setContentSize: vi.fn(),
setResizable: vi.fn(),
setTitle: vi.fn(),
setTitleBarOverlay: vi.fn(),
setWindowButtonVisibility: vi.fn(),
show: vi.fn(),
webContents,
};
Expand All @@ -90,6 +94,10 @@ function makeFakeBrowserWindow(input?: {
window: window as unknown as Electron.BrowserWindow,
loadURL: window.loadURL,
maximize: window.maximize,
center: window.center,
setContentSize: window.setContentSize,
setResizable: window.setResizable,
setWindowButtonVisibility: window.setWindowButtonVisibility,
webContentsFocus: webContents.focus,
openDevTools: webContents.openDevTools,
replaceMisspelling: webContents.replaceMisspelling,
Expand Down Expand Up @@ -316,6 +324,49 @@ describe("DesktopWindow", () => {
}),
);

it.effect("uses exact frameless content geometry in marketing capture mode", () =>
Effect.gen(function* () {
const fakeWindow = makeFakeBrowserWindow();
const createCount = yield* Ref.make(0);
const createOptions = yield* Ref.make<
ReadonlyArray<Electron.BrowserWindowConstructorOptions>
>([]);
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const layer = makeTestLayer({
window: fakeWindow.window,
createCount,
createOptions,
mainWindow,
env: {
THREADLINES_DESKTOP_MARKETING_CAPTURE: "true",
},
});

yield* Effect.gen(function* () {
const desktopWindow = yield* DesktopWindow.DesktopWindow;
yield* desktopWindow.handleBackendReady;

const [options] = yield* Ref.get(createOptions);
assert.equal(options?.width, DesktopWindow.MARKETING_CAPTURE_CONTENT_SIZE.width);
assert.equal(options?.height, DesktopWindow.MARKETING_CAPTURE_CONTENT_SIZE.height);
assert.equal(options?.useContentSize, true);
assert.equal(options?.frame, false);
assert.equal(options?.resizable, false);
assert.equal(options?.hasShadow, false);
assert.deepEqual(fakeWindow.setContentSize.mock.calls, [
[
DesktopWindow.MARKETING_CAPTURE_CONTENT_SIZE.width,
DesktopWindow.MARKETING_CAPTURE_CONTENT_SIZE.height,
],
]);
assert.deepEqual(fakeWindow.setResizable.mock.calls, [[false]]);
assert.equal(fakeWindow.center.mock.calls.length, 1);
assert.deepEqual(fakeWindow.setWindowButtonVisibility.mock.calls, [[false]]);
assert.equal(fakeWindow.maximize.mock.calls.length, 0);
}).pipe(Effect.provide(layer));
}),
);

it.effect("applies an accepted spellcheck suggestion without renderer notifications", () =>
Effect.gen(function* () {
const fakeWindow = makeFakeBrowserWindow();
Expand Down
67 changes: 57 additions & 10 deletions apps/desktop/src/window/DesktopWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ const MIN_MAIN_WINDOW_WIDTH = 840;
const MIN_MAIN_WINDOW_HEIGHT = 620;
const MAX_RESTORED_MAIN_WINDOW_DIMENSION = 10_000;
const MAIN_WINDOW_STATE_FILE_NAME = "window-state.json";
export const MARKETING_CAPTURE_CONTENT_SIZE = {
width: 1600,
height: 934,
} as const;
const PRINT_SCREEN_ACCELERATOR = "PrintScreen";
const PRINT_SCREEN_KEYS = new Set(["print", "printscreen"]);
export const OPEN_SCREEN_CLIP_MENU_ACTION = "open-screen-clip";
Expand Down Expand Up @@ -147,7 +151,14 @@ function getInitialWindowBackgroundColor(shouldUseDarkColors: boolean): string {
return shouldUseDarkColors ? "#0a0a0a" : "#ffffff";
}

function getWindowTitleBarOptions(shouldUseDarkColors: boolean): WindowTitleBarOptions {
function getWindowTitleBarOptions(
shouldUseDarkColors: boolean,
marketingCaptureMode = false,
): WindowTitleBarOptions {
if (marketingCaptureMode) {
return {};
}

if (process.platform === "darwin") {
return {
titleBarStyle: "hiddenInset",
Expand Down Expand Up @@ -260,14 +271,15 @@ function savePersistedMainWindowState(
function syncWindowAppearance(
window: Electron.BrowserWindow,
shouldUseDarkColors: boolean,
marketingCaptureMode: boolean,
): Effect.Effect<void> {
return Effect.sync(() => {
if (window.isDestroyed()) {
return;
}

window.setBackgroundColor(getInitialWindowBackgroundColor(shouldUseDarkColors));
const { titleBarOverlay } = getWindowTitleBarOptions(shouldUseDarkColors);
const { titleBarOverlay } = getWindowTitleBarOptions(shouldUseDarkColors, marketingCaptureMode);
if (typeof titleBarOverlay === "object") {
window.setTitleBarOverlay(titleBarOverlay);
}
Expand Down Expand Up @@ -345,13 +357,20 @@ const make = Effect.gen(function* () {
environment.stateDir,
MAIN_WINDOW_STATE_FILE_NAME,
);
const persistedWindowState = yield* loadPersistedMainWindowState(fileSystem, windowStatePath);
const persistedWindowOptions = Option.isSome(persistedWindowState)
const persistedWindowState = environment.marketingCaptureMode
? Option.none<PersistedMainWindowState>()
: yield* loadPersistedMainWindowState(fileSystem, windowStatePath);
const persistedWindowOptions = environment.marketingCaptureMode
? {
width: persistedWindowState.value.width,
height: persistedWindowState.value.height,
...MARKETING_CAPTURE_CONTENT_SIZE,
useContentSize: true,
}
: defaultMainWindowSize(yield* electronWindow.workAreaSize);
: Option.isSome(persistedWindowState)
? {
width: persistedWindowState.value.width,
height: persistedWindowState.value.height,
}
: defaultMainWindowSize(yield* electronWindow.workAreaSize);
const window = yield* electronWindow.create({
...persistedWindowOptions,
minWidth: MIN_MAIN_WINDOW_WIDTH,
Expand All @@ -361,7 +380,16 @@ const make = Effect.gen(function* () {
backgroundColor: getInitialWindowBackgroundColor(shouldUseDarkColors),
...iconOption,
title: environment.displayName,
...getWindowTitleBarOptions(shouldUseDarkColors),
...getWindowTitleBarOptions(shouldUseDarkColors, environment.marketingCaptureMode),
...(environment.marketingCaptureMode
? {
frame: false,
fullscreenable: false,
hasShadow: false,
maximizable: false,
resizable: false,
}
: {}),
webPreferences: {
preload: environment.preloadPath,
contextIsolation: true,
Expand All @@ -375,6 +403,18 @@ const make = Effect.gen(function* () {
},
});

if (environment.marketingCaptureMode) {
window.setContentSize(
MARKETING_CAPTURE_CONTENT_SIZE.width,
MARKETING_CAPTURE_CONTENT_SIZE.height,
);
window.setResizable(false);
window.center();
if (environment.platform === "darwin") {
window.setWindowButtonVisibility(false);
}
}

// Preview content is untrusted: no Node, no custom preload, and it must run
// in the preview partition rather than the app's own session.
//
Expand All @@ -394,11 +434,18 @@ const make = Effect.gen(function* () {
}
});

if (Option.isSome(persistedWindowState) && persistedWindowState.value.isMaximized) {
if (
!environment.marketingCaptureMode &&
Option.isSome(persistedWindowState) &&
persistedWindowState.value.isMaximized
) {
window.maximize();
}

window.on("close", () => {
if (environment.marketingCaptureMode) {
return;
}
const bounds = getPersistableMainWindowBounds(window);
const width = normalizeRestoredDimension(bounds.width, MIN_MAIN_WINDOW_WIDTH);
const height = normalizeRestoredDimension(bounds.height, MIN_MAIN_WINDOW_HEIGHT);
Expand Down Expand Up @@ -702,7 +749,7 @@ const make = Effect.gen(function* () {
syncAppearance: Effect.gen(function* () {
const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors;
yield* electronWindow.syncAllAppearance((window) =>
syncWindowAppearance(window, shouldUseDarkColors),
syncWindowAppearance(window, shouldUseDarkColors, environment.marketingCaptureMode),
);
}).pipe(Effect.withSpan("desktop.window.syncAppearance")),
});
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 29 additions & 6 deletions apps/marketing/public/Screenshots/launch/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
# Threadlines marketing capture retake

This folder contains the refreshed marketing set captured against the July 12, 2026 layout. Every video is exported at a constant 60 fps. `activity-header-dark`, `rich-subagent-chat`, and `project-files-edit` are 3200×1868 Retina exports; the remaining clips are 1600×934. Matching poster frames use the same resolution as their clips and are stored in `Posters/`.

WebM is the primary format: VP9 Profile 2, 10-bit `yuv420p10le`, CRF 30 with no bitrate cap. MP4 is the Safari fallback: H.264 High profile, 8-bit `yuv420p`, CRF 18 with the slow preset.

The staged sidebar has two expanded projects and one collapsed project, five or more threads per project, merged and branch indicators, and a running-terminal cue. The calibrated traffic-light clean plate is used on unobstructed full-window captures; its outer curve is mirrored from the untouched native corner at each export resolution, preserving macOS border antialiasing, normal control spacing, and the real sidebar toggle. It is intentionally omitted from the two opaque file-viewer clips so it cannot appear to float above the viewer layer.
This folder contains the refreshed marketing set. The primary 0.3.0 workspace, sidebar,
and browser clips come from the deterministic neutral capture studio at 3200×1868 and a
constant 60 fps. Each scene has matching `-dark` and `-light` captures, and responsive
1600×934 variants carry the `-mobile` suffix after the theme.
`activity-header-dark`, `rich-subagent-chat`, and `project-files-edit` are also
3200×1868 Retina exports; the remaining legacy clips are 1600×934. Matching poster
frames are stored in `Posters/`.

WebM is the primary format. The new responsive 0.3.0 clips use VP9 Profile 0,
8-bit `yuv420p`, CRF 30 with no bitrate cap; the earlier Retina clips retain their
Profile 2, 10-bit masters. MP4 is the Safari fallback: H.264 High profile,
8-bit `yuv420p`, CRF 18 with the slow preset.

The 0.3.0 sidebar has five live threads across three projects: input, working, and
background each have a distinct signal, while two threads are deliberately idle. Eleven
older threads sit under Wrapped. New neutral captures are frameless by design and do not
use a composited traffic-light plate.

## Recommended site use

- `workspace-four-panel-overview-{dark,light}` — primary 0.3.0 homepage pair with the
cross-project sidebar, active conversation, matching Orbit browser, and source control
in one frame. The take collapses pending input, reviews browser activity, opens a
source-control diff, and returns to the overview. The standalone PNG remains the
social-card source.
- `sidebar-attention-states-{dark,light}` — five live threads: amber input needed, blue
recently started working, cyan background, and two quiet threads with no status
treatment.
- `agent-browser-workflow-{dark,light}` — annotates the live service-health heading in
the matching-theme Orbit browser, attaches the real element note to the composer, and
shows the agent applying the requested compact green status treatment in place.
- `activity-header-dark` — strongest hero candidate. Opens the compact activity dropdown with 4/6 tasks, two active subagents, and one background run, then moves through the live work without expanding the six-step list or leaving focus rings.
- `rich-subagent-chat` — full conversation with a substantial Scout subagent result and follow-up responses.
- `project-files-edit` — browses the project tree, opens a tab, enters editing by double-click, saves a small change, then selects an exact line range and attaches it to chat; the current edit icon remains visible in the toolbar.
Expand All @@ -33,7 +56,7 @@ The staged sidebar has two expanded projects and one collapsed project, five or

- `*.mp4` — broadly compatible website video.
- `*.webm` — smaller modern-browser alternative.
- `Posters/*.png` — selected poster frame at the matching video resolution.
- `Posters/*.webp` — selected poster frame at the matching video resolution.
- `*.png` at this folder level — standalone light-mode screenshots.
- `poster-contact-sheet.png` — quick visual review of the complete poster and still set.

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/marketing/public/og.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 2 additions & 3 deletions apps/marketing/src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const websiteStructuredData = {
<meta property="og:image" content={socialImageUrl} />
<meta
property="og:image:alt"
content="Threadlines: the open-source workspace for Claude Code and Codex, next to a screenshot of the app"
content="Threadlines with the project sidebar, agent conversation, live browser, and source control open together"
/>
<meta property="og:image:width" content="2400" />
<meta property="og:image:height" content="1260" />
Expand Down Expand Up @@ -467,11 +467,10 @@ const websiteStructuredData = {
color: var(--accent-contrast);
font-size: 13px;
font-weight: 600;
transition: transform 0.2s ease, filter 0.2s ease;
transition: filter 0.2s ease;
}

.nav-dl:hover {
transform: translateY(-1px);
filter: brightness(1.08);
}

Expand Down
2 changes: 1 addition & 1 deletion apps/marketing/src/lib/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const SITE_URL = "https://www.threadlines.dev";
export const SITE_NAME = "Threadlines";

export const DEFAULT_SITE_DESCRIPTION =
"Threadlines is a free, open-source desktop workspace for Claude Code and Codex: parallel agent threads with real source control, a file editor, and full visibility into every task, subagent, and background run.";
"Threadlines is a free, open-source desktop workspace for Claude Code and Codex, combining agent threads, a live browser, project files, and real source control in one local workspace.";

export const SITE_SOCIAL_IMAGE = "/og.png";

Expand Down
Loading
Loading