Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d75b1f6
feat(web): first-run welcome wizard with agent setup and project import
t3dotgg Sep 5, 2026
7127be2
fix(web): resume imported custom-provider threads
juliusmarminge Sep 5, 2026
6e36e1f
fix(web): explain hosted connection prerequisites
saphid Sep 6, 2026
988b559
fix(web): onboarding installs agents without needing Node or npm (#10…
t3dotgg Sep 6, 2026
0cd3ab6
fix(web): onboarding wizard now supports light mode (#10432)
t3dotgg Sep 7, 2026
8538c5b
fix(server): import transcripts with oversized tool records
Yash-Singh1 Sep 7, 2026
d6add90
fix(web): make onboarding a shared multi-computer wizard
juliusmarminge Sep 7, 2026
0f7379a
feat(web): group onboarding project import by repository (#10493)
t3dotgg Sep 7, 2026
5058616
fix(web): tolerate servers that predate git identity in project impor…
juliusmarminge Sep 7, 2026
65b7d50
fix(server): skip Pylon worktrees in project import and document the …
rynfar Sep 11, 2026
ecfc5dc
docs: record the welcome wizard upstream batch
rynfar Sep 11, 2026
f70f083
docs: link the welcome wizard batch to its pull request
rynfar Sep 11, 2026
ff31083
fix(client-runtime): keep the welcome state stream private
rynfar Sep 11, 2026
48a097b
fix(clients): keep saved settings readable when another client wrote …
rynfar Sep 11, 2026
b7c574a
fix(web): sign in to Claude Code from setup without a terminal
rynfar Sep 11, 2026
13658ba
feat(web): reopen setup from the command palette
rynfar Sep 11, 2026
dad6995
fix(server): share one transcript scanner across connections
rynfar Sep 11, 2026
51002a5
test(server): cover the commit guard with insert-ignore bindings
rynfar Sep 11, 2026
8340ed7
docs: correct welcome wizard import limits, sign-in and reopening
rynfar Sep 11, 2026
7573162
docs: record the welcome wizard review fixes in the upstream ledger
rynfar Sep 11, 2026
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
1 change: 1 addition & 0 deletions .agents/upstream-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Historical groups are indexed in the linked archive. This file migration changes
| Live activity motion parity / `6c583620ff7ad3235b135af7107c0543467eecfa` | Corrects Pylon's rendering of already-classified `c7c1dfe4df` (#9709), `ce4712d5b0` (#9799) and the #444 port of `6cf0c6ea55` (#9093) / `c3b8825bf4` (#9606) | Pylon had diverged: `cdde7f3150` gave every active row the sweeping overlay and deleted `live-tool-shine`, while upstream reserves the overlay for label-only rows (`active && shimmer`, passed only by the Thinking row) and keeps the shine for tool labels. Restores upstream's `shimmer` prop, `animated = active && !failed` guard, row-level observer ref, overlay on worktree setup and compaction, and both CSS utilities byte-identical to upstream. That same commit also deleted `@utility visible-animate-spin` as dead while `spinner.tsx` and `refresh-icon.tsx` still applied it, so every spinner and refresh glyph was static; restored. Preserved: Pylon's `workingStepLabel`, and the `document.hasFocus()` gate AGENTS.md requires and upstream lacks. Cursor unchanged. | [Live activity parity #452](https://github.com/pylon-code/pylon/pull/452); 161 focused tests, web typecheck, scoped lint/format, region diffed against `t3code-upstream/main`. |
| Skeleton loading pulse / `6c583620ff7ad3235b135af7107c0543467eecfa` | `21b9dda5afb00a33e228a68d2ccc885bba7285dc` (#9448) | Adopted. The shared `Skeleton` drops its swept gradient band for upstream's single stepped opacity pulse, whose keyframes were already byte-identical to the `ghost-pulse` Pylon ran on the pull-request ghosts; `ghost-pulse` is retired and every loading state now uses `animate-skeleton`. Preserve Pylon's `!seed` gate on the detail ghost, so a panel already showing real content does not breathe, and Pylon's own usage fill-in timing. The maintainer chose upstream theming and styling with only the logo staying Pylon's, which settles this kind of visual divergence in upstream's favour by default. Cursor unchanged. | [Skeleton pulse #451](https://github.com/pylon-code/pylon/pull/451); 339 focused tests, web typecheck, scoped lint/format. |
| Panel links, markdown galleries, shared title model, usage columns, sidebar file drops, terminal link overrides, Codex limit naming and turn token telemetry / `6c583620ff7ad3235b135af7107c0543467eecfa` | Full ten-source list in PR #458, from #9132 through #10060 | Nine adopted; `b5d89038ae72142038dfa8cf69d49b7a607fe98e` is already covered as an empty duplicate of #7892. Turn telemetry stays inert without a Pylon PostHog key, is recorded only after runtime generation and session incarnation fences, and treats Prime, Cursor, Grok and Antigravity usage as unavailable; OpenCode uses the final #10116 step-retention design. Codex limit errors carry the session incarnation and Pylon keeps relaying every rate-limit notification. Preserve Pylon PR detection, environment-scoped PR caches, provider-instance settings writes, hub account keys, older-server window names, change-request row props and terminal external-browser fallback. New product analytics and usage data docs. Cursor unchanged. | [Web panels #458](https://github.com/pylon-code/pylon/pull/458); 1,687 focused tests, six package typechecks (web, server, shared, client-runtime, contracts, mobile), scoped lint/format; upstream UI evidence linked, no local client pass. |
| First-run welcome wizard, agent setup and transcript import / `6c583620ff7ad3235b135af7107c0543467eecfa` | Full nine-source list in [#459](https://github.com/pylon-code/pylon/pull/459), from #5362 through #10547 | Adopted all nine: the `/welcome` overlay with multi-computer selection, Claude Code and Codex readiness terminals with native installers, repository-grouped project import, server transcript scan/import with provider resume cursors, settings-hydration hardening, and the light-mode and theme follow-ups. Pylon adaptations: ProviderSessionDirectory keeps its commit guard and exact removal beside insert-ignore and imported transcript records; import invariants use Pylon's open-request scan and Pylon-only thread fields; tests use Pylon's rollback revision and provider-instance compare-and-sets; PylonMark and Pylon/Pylon Connect copy; scans skip Pylon runtime-home worktrees; imported projects keep resolving a default model until #9754 lands. Saved client settings decode per value and keep undecodable stored values (T3 Code's `confirmQuit` shape on a shared origin) instead of failing closed; desktop keeps the default quit hold when its settings file is unreadable. Duplicated sign-in: setup uses Pylon's terminal-free `ProviderSignInDialog` for Claude Code and keeps terminals for installs and `codex login`. Setup reopens from the command palette, and one transcript scanner is shared across connections. Prime and Antigravity stay in Settings → Providers with the other opt-in providers. `3faeee49ac67dfd9534369f1e1c627c0356b75ac` (#10832) is excluded and sequenced later. No migration. Cursor unchanged. | [Welcome wizard #459](https://github.com/pylon-code/pylon/pull/459); 1,703 focused tests, seven package typechecks, scoped lint/format and knip. |

## Deferred register

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const readWithLogs = (fileSystemLayer: Layer.Layer<FileSystem.FileSystem>) => {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const settings = yield* DesktopClientSettings.DesktopClientSettings;
return {
result: yield* settings.get,
result: yield* Effect.result(settings.get),
settingsPath: environment.clientSettingsPath,
records,
};
Expand All @@ -73,12 +73,13 @@ describe("DesktopClientSettings diagnostics", () => {
Effect.gen(function* () {
const result = yield* readWithLogs(FileSystem.layerNoop({}));

assert.isTrue(Option.isNone(result.result));
if (result.result._tag !== "Success") return assert.fail("expected a successful read");
assert.isTrue(Option.isNone(result.result.success));
assert.deepEqual(result.records, []);
}),
);

it.effect("logs non-missing filesystem failures with the settings path", () => {
it.effect("reports non-missing filesystem failures and logs the settings path", () => {
const permissionError = PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
Expand All @@ -93,7 +94,12 @@ describe("DesktopClientSettings diagnostics", () => {
}),
);

assert.isTrue(Option.isNone(result.result));
if (result.result._tag !== "Failure") return assert.fail("expected a read failure");
assert.instanceOf(
result.result.failure,
DesktopClientSettings.DesktopClientSettingsReadError,
);
assert.strictEqual(result.result.failure.cause, permissionError);
assert.equal(result.records.length, 1);
assert.deepEqual(result.records[0]?.message, [
"Could not read desktop client settings.",
Expand All @@ -103,15 +109,16 @@ describe("DesktopClientSettings diagnostics", () => {
});
});

it.effect("logs malformed settings documents with the settings path", () =>
it.effect("reads malformed settings documents as absent and logs the settings path", () =>
Effect.gen(function* () {
const result = yield* readWithLogs(
FileSystem.layerNoop({
readFileString: () => Effect.succeed("{not-json"),
}),
);

assert.isTrue(Option.isNone(result.result));
if (result.result._tag !== "Success") return assert.fail("expected a successful read");
assert.isTrue(Option.isNone(result.result.success));
assert.equal(result.records.length, 1);
const message = result.records[0]?.message;
if (!Array.isArray(message)) {
Expand Down
144 changes: 143 additions & 1 deletion apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const clientSettings: ClientSettings = {
fontSizeTerminal: 12,
fontSmoothing: true,
glassOpacity: 80,
onboardingCompletedAt: null,
panelAnimationDurationMs: 0,
proactivePanelsEnabled: true,
showSkillsInSlashMenu: false,
Expand Down Expand Up @@ -136,6 +137,59 @@ describe("DesktopClientSettings", () => {
),
);

for (const failure of [
{ label: "permission", reason: "PermissionDenied" },
{ label: "I/O", reason: "Unknown" },
] as const) {
it.effect(`preserves saved preferences across ${failure.label} read failures and retries`, () =>
withClientSettings(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const settings = yield* DesktopClientSettings.DesktopClientSettings;
const savedSettings = {
...clientSettings,
onboardingCompletedAt: "2026-09-05T12:00:00.000Z",
};
yield* settings.set(savedSettings);
const savedContents = yield* fileSystem.readFileString(environment.clientSettingsPath);
const cause = PlatformError.systemError({
_tag: failure.reason,
module: "FileSystem",
method: "readFileString",
pathOrDescriptor: environment.clientSettingsPath,
});
let failRead = true;
const retryableSettings = yield* DesktopClientSettings.make.pipe(
Effect.provideService(
FileSystem.FileSystem,
FileSystem.FileSystem.of({
...fileSystem,
readFileString: (path) =>
Effect.suspend(() =>
failRead ? Effect.fail(cause) : fileSystem.readFileString(path),
),
}),
),
);

const error = yield* retryableSettings.get.pipe(Effect.flip);
assert.instanceOf(error, DesktopClientSettings.DesktopClientSettingsReadError);
assert.equal(error.operation, "read-file");
assert.equal(error.path, environment.clientSettingsPath);
assert.strictEqual(error.cause, cause);
assert.equal(
yield* fileSystem.readFileString(environment.clientSettingsPath),
savedContents,
);

failRead = false;
assert.deepEqual(yield* retryableSettings.get, Option.some(savedSettings));
}),
),
);
}

it.effect("reports the failed client settings write operation and path", () =>
withClientSettings(
Effect.gen(function* () {
Expand Down Expand Up @@ -222,7 +276,7 @@ describe("DesktopClientSettings", () => {
),
);

it.effect("treats malformed client settings documents as absent", () =>
it.effect("reads a malformed settings file as no saved settings without rewriting it", () =>
withClientSettings(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
Expand All @@ -232,7 +286,95 @@ describe("DesktopClientSettings", () => {
yield* fileSystem.writeFileString(environment.clientSettingsPath, "{not-json");

assert.isTrue(Option.isNone(yield* settings.get));
assert.equal(yield* fileSystem.readFileString(environment.clientSettingsPath), "{not-json");
}),
),
);

for (const document of [
{ label: "direct", contents: '{"fontSizeCode":"large","timestampFormat":"12-hour"}' },
{
label: "legacy",
contents: '{"settings":{"fontSizeCode":"large","timestampFormat":"12-hour"}}',
},
]) {
it.effect(`keeps readable ${document.label} settings beside an undecodable value`, () =>
withClientSettings(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const settings = yield* DesktopClientSettings.DesktopClientSettings;
yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
yield* fileSystem.writeFileString(environment.clientSettingsPath, document.contents);

assert.deepEqual(
yield* settings.get,
Option.some({
...(yield* decodeClientSettingsJson("{}")),
timestampFormat: "12-hour" as const,
}),
);
assert.equal(
yield* fileSystem.readFileString(environment.clientSettingsPath),
document.contents,
);
}),
),
);
}

it.effect("leaves an undecodable value in the file until that setting changes", () =>
withClientSettings(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const settings = yield* DesktopClientSettings.DesktopClientSettings;
const readDocument = Effect.flatMap(
fileSystem.readFileString(environment.clientSettingsPath),
decodeRecordJson,
);
yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
yield* fileSystem.writeFileString(
environment.clientSettingsPath,
'{"confirmQuit":"hold","timestampFormat":"12-hour"}',
);

const saved = Option.getOrThrow(yield* settings.get);
assert.isTrue(saved.confirmQuit);
yield* settings.set({ ...saved, onboardingCompletedAt: "2026-09-10T12:00:00.000Z" });
assert.deepInclude(yield* readDocument, {
confirmQuit: "hold",
timestampFormat: "12-hour",
onboardingCompletedAt: "2026-09-10T12:00:00.000Z",
});

yield* settings.set({ ...saved, confirmQuit: false });
assert.deepInclude(yield* readDocument, { confirmQuit: false });
yield* settings.set({ ...saved, confirmQuit: false, wordWrap: false });
assert.deepInclude(yield* readDocument, { confirmQuit: false, wordWrap: false });
}),
),
);

it.effect("keeps the default quit hold when settings cannot be read", () =>
Effect.gen(function* () {
const failing = DesktopClientSettings.DesktopClientSettings.of({
get: Effect.fail(
new DesktopClientSettings.DesktopClientSettingsReadError({
operation: "read-file",
path: "/unreadable/client-settings.json",
cause: new Error("permission denied"),
}),
),
set: () => Effect.void,
});
const disabled = DesktopClientSettings.DesktopClientSettings.of({
get: Effect.succeed(Option.some({ ...clientSettings, confirmQuit: false })),
set: () => Effect.void,
});

assert.isTrue(yield* DesktopClientSettings.readConfirmQuit(failing));
assert.isFalse(yield* DesktopClientSettings.readConfirmQuit(disabled));
}),
);
});
Loading
Loading