Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ This project uses selective package publishing. Each release entry lists the pub

### Added

- Desktop now finds T3 Code installed under any release channel — the launch-target lookup previously only checked for "T3 Code (Alpha)", so stable/Beta/Nightly installs got no app icon, no default "Open with" application, and no restart action. All channel variants are now probed (stable first), and the T3 Code rows fall back to the official t3.codes icon instead of the generic mark when the app isn't installed.
- Desktop's Home screen keeps the "Use AntSeed on your favorite app" pills visible after connecting a tool — previously connecting anything hid the whole list. The pitch now disappears only once the user has chats, and an already-connected app's pill shows as connected (green dot, green-tinted border) and opens the Apps page instead of reconnecting.
- Desktop's floating window now shows on traffic by default: when a connected app starts sending requests, the pill pops up on its own unless the "Show on traffic" preference is explicitly turned off (previously it defaulted to off).
- Desktop now nudges new users toward connecting their apps: a notification dot sits on the Apps nav item until the screen is first opened, and that first visit plays an animated coach-mark walkthrough — tooltips bobbing over the live view with a pulsing beacon, pointing at the app list and the "Add custom app" button. Shown once per install.
- Desktop's Home power button now asks for confirmation before disconnecting, so a mis-click no longer stops routing (and drops connected apps and in-progress chats). The prompt reuses the standard confirm modal and has a "Don't show this again" checkbox that is remembered per install.
- Desktop now includes privacy-conscious activation and reliability telemetry for first open, buyer-runtime/DHT/peer readiness, time to first visible model, fixed semantic user actions and each launch's first action, setup completion, coarse deposit milestones and failures, first chat, buyer-eligible model selections and free/paid classification, correlated per-request chat starts/outcomes, discovery failures, and clean/crash lifecycle signals. Events are main-process owned, consume structured runtime status rather than logs, use explicit allowlisted actions rather than frontend autocapture, are strictly allowlisted and bucketed, normalize failures to fixed local taxonomies without sending raw errors, exact prices, or peer IDs, emit model IDs only from a release-owned reviewed registry, briefly flush the final clean-shutdown event, disable PostHog GeoIP enrichment and person profiles, use the normalized public on-chain address as the PostHog identifier plus random session/request IDs, suppress successful periodic discovery captures, migrate existing identities without falsely emitting `app_first_opened`, collapse paired model persistence into one `model_selected` event, avoid overlapping model/peer `user_action` events, remain off in development/source builds without explicit configuration, and can be disabled from Preferences.
- Desktop Connected Apps now includes **Claude**: connecting it switches Anthropic's Claude desktop app into its native third-party inference mode, pointed at a new loopback Claude gateway inside AntSeed Desktop (default port 8380, `ANTSEED_CLAUDE_GATEWAY_PORT` to override). The gateway offers Claude's model picker **AntSeed Auto** (the route selected in the desktop, live — changing it applies to running Claude chats) plus the top of the desktop's curated model list under their real names, and forwards chats to the buyer proxy — no API key, no MITM proxy, and the user's normal Claude login, chats, and MCP config stay untouched in the separate 1p profile. Claude's per-chat title-generation requests are recognized as housekeeping, so they no longer appear as phantom conversations in Recent Chats. Because Claude's picker only accepts Anthropic model ids, network models are advertised behind those ids with their real names as display names, and a short note in each chat's system prompt tells the serving model the conversation is delivered over the AntSeed network. Connecting opens Claude (restarting it first when it was already running so it picks up the new profile). On Windows, every known Claude install layout is probed — the classic `%APPDATA%\Claude` installer, MSIX/Store installs under `%LOCALAPPDATA%\Packages\Claude_*`, and the `%LOCALAPPDATA%` variants. Disconnecting (or an enterprise-provisioned third-party profile being present) restores Claude to its usual profile.

Expand All @@ -16,6 +21,8 @@ This project uses selective package publishing. Each release entry lists the pub

### Fixed

- Desktop no longer shows routing as on before it actually is. The Home power button, hero status, footer status strip, and floating pill lit up as soon as the buyer process was spawned — on first launch and when turning routing back on — even though the local proxy was not yet accepting connections. They now stay in a "Starting..." state until the proxy port answers a reachability probe (re-checked every second during startup), and only then switch to on/Running.
- Desktop's footer status strip no longer reports the network as "Healthy" after routing is stopped — network stats kept their last DHT snapshot, so the strip read "Healthy | Stopped". It now shows "Offline" while the buyer runtime is stopped, and the "Stopped" state is shown in red like other error states.
- Desktop telemetry launches now appear as sessions in PostHog. Events carried the launch id only as a custom `session_id` property and as a v4 UUID, so PostHog's Sessions explorer — which keys on `$session_id` and derives the session start from a UUIDv7 timestamp — showed nothing. The launch id is now a UUIDv7 and is sent as both `session_id` and `$session_id`, enabling per-launch analysis (session duration, events per launch, crash rate) without changing anything built on `session_id`.
- Phones browsing the website in "Desktop site" mode no longer download desktop installers they can't run. Mobile detection for download CTAs previously relied on viewport width alone, so a phone requesting the desktop site (which widens the layout viewport and, in Samsung Internet, spoofs an `X11; Linux` user agent) was handed the Linux AppImage. The reroute to the `/get-started` flow now also checks touch-only hardware (`pointer: coarse` + `hover: none`) and the UA-CH mobile signal — neither of which desktop-site mode changes — and platform detection treats such devices as unknown, so an installer is never resolved for them. Analytics counts these taps as `get_started` funnel entries instead of download conversions, matching the behavior. Touchscreen laptops keep a fine, hover-capable primary pointer and still get the direct download.
- Seller and transport failures returned through the buyer protocol now clearly explain that the selected peer failed, suggest choosing another peer or Auto routing, preserve the seller's original response and status for diagnostics, and identify pinned-peer failures so clients can surface them immediately without retrying the same peer. Buyer-side failures, payment-required responses, and actionable request errors remain unchanged.
Expand All @@ -28,6 +35,11 @@ This project uses selective package publishing. Each release entry lists the pub

### Changed

- Desktop first-run setup screen now leads with what the user gets rather than raw network tallies: a drifting marquee of the model families on the network, and a miniature VPR window at the app's real proportions that plays as a walkthrough-loader — cycling Home (composer, app pills, usage tiles with the saving highlighted), Chat ("What is AntSeed?" asked and answered), Models (free and paid rows with price chips), and Connected apps (toggles flipping on for Claude, ChatGPT, Cursor, OpenCode, Telegram), with the left nav highlight following the scenes and a caption line narrating each one. The walkthrough content is fixed, so first-run users see it in full before discovery finds anything; the node/seller/model counts in the footer remain live. The headline text is gone; the bottom note now reads "Setting up — only on first run, takes 2 minutes max." The node/seller/model/free counts and the current-activity line remain, smaller, at the bottom of the screen.
- Desktop Home shows the Usage tiles (requests, tokens, saving) from the first launch — including for users without connected apps, at zero — instead of only once an app is connected. Saving reads "$0" rather than "-" until there is priced usage.
- Desktop Chat now opens wide by default — the window switches to the standard (wide) preset with the conversation list showing as soon as the Chat view is entered; the in-view toggle still collapses it to the thin panel.
- Desktop Home now leads with the chat. For users without connected apps, the pinned hero shows a single composer card under the power button — prompt on top, the model picker as a compact pill and the send button in a footer row, the same anatomy as the chat view's input — replacing the separate model card and pill-shaped ask box. The prompt reads "How can I help you today?", and until the first chat exists the send button pulses as soon as something is typed. The "Routing to your existing apps or start chatting here" heading is gone; the app list below keeps its own label. Users with connected apps keep the standalone model card. The first-run app list on Home is now a wrapping row of pills — Telegram, Claude (connect), Cursor (set up on the Apps page), then most of the catalog — instead of one line per app; the "More apps" / "Connect more apps" links are gone (the full catalog is on the Apps page).
- Desktop modals share one lighter header style: no divider under the title, a plain close glyph instead of a boxed button, tighter padding, and the app's card background — applied app-wide (chat confirmations, peer/reputation prompts, signer key dialogs, app settings) rather than only to the Connected Apps modals. The new disconnect confirmation adds an animated power-off badge.
- The `download.antseed.com` proxy now reports download telemetry per download instead of per HTTP request. Download managers that fetch an installer as several concurrent byte ranges, and browsers resuming a paused download, previously produced one `download_started`/`download_completed` per range (about 3× inflation for those clients); now only the request covering the first byte starts a download and only the one delivering the last byte finishes it, with other segments logged locally. Every proxy event also carries an `attributed` (1/0) GA4 param saying whether the visitor's browser passed its GA ids — `attributed=0` downloads come from browsers that blocked GA and so never fired the website's `download_vpr` click, which explains why server-side download counts can exceed click counts.
- Ox Alpha is no longer part of the desktop's curated free model lineup (first-run default and free-first model lists).

Expand Down
20 changes: 13 additions & 7 deletions apps/desktop/src/main/connected-apps/profile-targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { existsSync } from 'node:fs';
import { homedir } from 'node:os';
import path from 'node:path';
import { toolDesktopAppName } from '../../shared/tool-resume.js';
import { toolDesktopAppNames } from '../../shared/tool-resume.js';
import { resolveConnectDataDir } from '../runtime/process-manager.js';
import {
loadAppIdentitySettings,
Expand All @@ -28,19 +28,25 @@ import { fetchCustomAppSiteMetadata, loadCustomApps, saveCustomApps } from './cu
export const defaultLaunchTargetCache = new Map<string, AppLaunchTarget | null>();

export function defaultLaunchTargetForProfile(profileName: string): AppLaunchTarget | null {
const appName = toolDesktopAppName(profileName);
if (!appName) return null;
const appNames = toolDesktopAppNames(profileName);
if (appNames.length === 0) return null;
if (process.platform === 'darwin') {
for (const dir of ['/Applications', path.join(homedir(), 'Applications')]) {
const bundle = path.join(dir, `${appName}.app`);
if (existsSync(bundle)) return { name: appName, path: bundle };
for (const appName of appNames) {
for (const dir of ['/Applications', path.join(homedir(), 'Applications')]) {
const bundle = path.join(dir, `${appName}.app`);
if (existsSync(bundle)) return { name: appName, path: bundle };
}
}
return null;
}
if (process.platform === 'win32') {
const listed = listInstalledApplications();
if (!listed.ok) return null;
return listed.apps.find((entry) => entry.name.toLowerCase() === appName.toLowerCase()) ?? null;
for (const appName of appNames) {
const found = listed.apps.find((entry) => entry.name.toLowerCase() === appName.toLowerCase());
if (found) return found;
}
return null;
}
return null;
}
Expand Down
21 changes: 12 additions & 9 deletions apps/desktop/src/main/ipc/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
import { ipcMain } from 'electron';
import {
toolDesktopAppName,
toolDesktopAppNames,
toolResumeCommand,
} from '../../shared/tool-resume.js';
import {
Expand Down Expand Up @@ -140,19 +140,22 @@ export function registerDesktopIpc(): void {
ipcMain.handle('desktop:open-tool-session', (_event, tool: unknown, sessionKey: unknown, target: unknown) => {
const toolSlug = typeof tool === 'string' ? tool : '';
if (target === 'app') {
const appName = toolDesktopAppName(toolSlug);
if (!appName) {
const appNames = toolDesktopAppNames(toolSlug);
if (appNames.length === 0) {
return { ok: false, error: 'This tool has no known desktop app.' };
}
if (process.platform !== 'darwin') {
return { ok: false, error: `Open the ${appName} app manually on this platform.` };
return { ok: false, error: `Open the ${appNames[0]} app manually on this platform.` };
}
try {
execFileSync('open', ['-a', appName], { stdio: 'pipe' });
return { ok: true };
} catch {
return { ok: false, error: `The ${appName} app doesn't seem to be installed.` };
for (const appName of appNames) {
try {
execFileSync('open', ['-a', appName], { stdio: 'pipe' });
return { ok: true };
} catch {
// Try the next release-channel variant.
}
}
return { ok: false, error: `The ${appNames[0]} app doesn't seem to be installed.` };
}
const command = toolResumeCommand(
toolSlug,
Expand Down
11 changes: 8 additions & 3 deletions apps/desktop/src/renderer/core/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@ export type RendererUiState = {
chatConversations: unknown[];
chatConversationsLoaded: boolean;
chatProxyPort: number;
/** Buyer proxy port answered the last reachability probe. */
chatProxyOnline: boolean;
/** Chat opens thin (compact window, no conversation list). The header
toggle expands the window to the standard preset and shows the panel. */
chatPanelExpanded: boolean;
Expand Down Expand Up @@ -467,7 +469,7 @@ export function createInitialUiState(): RendererUiState {
daemonState: null,

// Runtime display
connectBadge: { tone: 'idle', label: 'Stopped' },
connectBadge: { tone: 'bad', label: 'Stopped' },
runtimeActivity: { tone: 'idle', message: 'Idle' },

// Logs
Expand Down Expand Up @@ -552,7 +554,10 @@ export function createInitialUiState(): RendererUiState {
chatConversations: [],
chatConversationsLoaded: false,
chatProxyPort: 0,
chatPanelExpanded: false,
chatProxyOnline: false,
// Chat opens wide (standard preset) with the conversation list showing;
// the in-view toggle collapses it to the thin panel.
chatPanelExpanded: true,
chatMessages: [],
chatStreamingMessage: null,
chatSending: false,
Expand Down Expand Up @@ -588,7 +593,7 @@ export function createInitialUiState(): RendererUiState {
blockedPeerIds: [],
},
vprFloatOpen: false,
vprFloatAutoOpen: false,
vprFloatAutoOpen: true,
vprFloatShowRoutedPeer: false,
chatDiscoverRowsLoaded: false,
chatSelectedServiceValue: '',
Expand Down
57 changes: 57 additions & 0 deletions apps/desktop/src/renderer/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1269,3 +1269,60 @@ select.form-input {
0%, 80%, 100% { opacity: 0.2; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1); }
}

/* ===================================================================
Shared @antseed/ui modal chrome (all desktop windows)
The Modal primitive ships the dashboard's `--as-*` look (bordered
header, boxed close button, generous padding). Restyle it once here
with the app's tokens so every desktop modal — confirms, settings,
pickers — matches the VPR views without per-dialog overrides.
Selectors are nested under `.as-modal` / `.as-modal-overlay` so they
outrank the primitive's own rules whatever the stylesheet order.
=================================================================== */

.as-modal-overlay .as-modal {
border: none;
border-radius: 16px;
background: var(--bg-card);
box-shadow: var(--shadow);
}

.as-modal .as-modal__header {
align-items: center;
gap: 12px;
padding: 16px 16px 6px;
border-bottom: none;
}

.as-modal .as-modal__title {
color: var(--text-primary);
font-size: 14px;
font-weight: 600;
letter-spacing: 0;
line-height: 20px;
}

.as-modal .as-modal__subtitle {
color: var(--text-muted);
font-size: 11px;
line-height: 15px;
}

.as-modal .as-modal__close {
width: 24px;
height: 24px;
border: none;
border-radius: 8px;
background: transparent;
color: var(--text-muted);
}

.as-modal .as-modal__close:hover:not(:disabled) {
border-color: transparent;
background: var(--bg-surface);
color: var(--text-primary);
}

.as-modal .as-modal__body {
padding: 10px 16px 16px;
}
42 changes: 42 additions & 0 deletions apps/desktop/src/renderer/modules/app/apps-onboarding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest';
import {
APPS_ONBOARDING_SEEN_KEY,
isAppsOnboardingSeen,
persistAppsOnboardingSeen,
shouldNotifyAppsOnboarding,
} from './apps-onboarding';

function memoryStorage(initial: Record<string, string> = {}) {
const map = new Map(Object.entries(initial));
return {
getItem: (key: string) => map.get(key) ?? null,
setItem: (key: string, value: string) => { map.set(key, value); },
map,
};
}

describe('apps-onboarding', () => {
it('is unseen by default', () => {
expect(isAppsOnboardingSeen(memoryStorage())).toBe(false);
expect(isAppsOnboardingSeen(null)).toBe(false);
});

it('notifies until the intro has been seen', () => {
const storage = memoryStorage();
expect(shouldNotifyAppsOnboarding(storage)).toBe(true);

persistAppsOnboardingSeen(storage);
expect(storage.getItem(APPS_ONBOARDING_SEEN_KEY)).toBe('true');
expect(shouldNotifyAppsOnboarding(storage)).toBe(false);
});

it('swallows storage errors', () => {
const broken = {
getItem: () => { throw new Error('denied'); },
setItem: () => { throw new Error('denied'); },
};
expect(isAppsOnboardingSeen(broken)).toBe(false);
expect(() => persistAppsOnboardingSeen(broken)).not.toThrow();
expect(shouldNotifyAppsOnboarding(broken)).toBe(true);
});
});
27 changes: 27 additions & 0 deletions apps/desktop/src/renderer/modules/app/apps-onboarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
defaultStorage,
readStorageFlag,
type StorageLike,
writeStorageFlag,
} from './local-storage-flag';

/**
* One-shot Apps onboarding: after the user's first chat, a dot appears on the
* Apps nav item; opening Apps then plays a "connect your tools" intro screen.
* Seen-state persists per install (localStorage) like the other one-off
* dialog dismissals.
*/
export const APPS_ONBOARDING_SEEN_KEY = 'antseed:appsOnboardingSeen';

export function isAppsOnboardingSeen(storage: StorageLike | null = defaultStorage()): boolean {
return readStorageFlag(APPS_ONBOARDING_SEEN_KEY, storage);
}

export function persistAppsOnboardingSeen(storage: StorageLike | null = defaultStorage()): void {
writeStorageFlag(APPS_ONBOARDING_SEEN_KEY, storage);
}

/** Nav-rail dot condition: the user hasn't opened the Apps screen yet. */
export function shouldNotifyAppsOnboarding(storage: StorageLike | null = defaultStorage()): boolean {
return !isAppsOnboardingSeen(storage);
}
Loading
Loading