Skip to content
Closed
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
16 changes: 15 additions & 1 deletion apps/mobile/src/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useColorScheme } from "react-native";
import { Path, Svg } from "react-native-svg";
import { Path, Rect, Svg } from "react-native-svg";

type ProviderIconProps = {
readonly provider: string | null | undefined;
Expand All @@ -22,6 +22,20 @@ export function ProviderIcon(props: ProviderIconProps) {
);
}

if (props.provider === "pi") {
return (
<Svg width={size} height={size} viewBox="0 0 800 800" fill="none">
<Rect width="800" height="800" rx="160" fill="#000" />
<Path
fill="#fff"
fillRule="evenodd"
d="M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z"
/>
<Path fill="#fff" d="M517.36 400H634.72V634.72H517.36Z" />
</Svg>
);
}

if (props.provider === "grok") {
const fill = isDarkMode ? "#F5F5F5" : "#0F0F0F";
return (
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/lib/modelOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function providerDisplayLabel(provider: {
if (provider.displayName) return provider.displayName;
if (provider.driver === "codex") return "Codex";
if (provider.driver === "claudeAgent") return "Claude";
if (provider.driver === "pi") return "Pi";
return provider.instanceId;
}

Expand Down
58 changes: 58 additions & 0 deletions apps/server/src/provider/Drivers/PiDriver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as NodeAssert from "node:assert/strict";

import * as NodeServices from "@effect/platform-node/NodeServices";
import { ProviderInstanceId } from "@t3tools/contracts";
import { it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";

import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { BUILT_IN_DRIVERS } from "../builtInDrivers.ts";
import { PiDriver } from "./PiDriver.ts";

const assert: typeof NodeAssert = NodeAssert;
const backgroundPolicyLayer = Layer.mock(BackgroundPolicy.BackgroundPolicy)({
shouldRunScopeWork: () => Effect.succeed(true),
});
const testLayer = ServerConfig.layerTest(process.cwd(), {
prefix: "pi-driver-test-",
}).pipe(
Layer.provideMerge(NodeServices.layer),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(backgroundPolicyLayer),
);

it("registers Pi as a built-in driver", () => {
assert.equal(
BUILT_IN_DRIVERS.find((driver) => driver.driverKind === "pi"),
PiDriver,
);
});

it.layer(testLayer)("PiDriver", (it) => {
it.effect("creates the disabled adapter, snapshot, and text-generation bundle", () =>
Effect.scoped(
Effect.gen(function* () {
const instanceId = ProviderInstanceId.make("pi-work");
const instance = yield* PiDriver.create({
instanceId,
displayName: "Pi Work",
enabled: false,
environment: [{ name: "PI_HOME", value: "/tmp/pi-home", sensitive: false }],
config: PiDriver.defaultConfig(),
});
const snapshot = yield* instance.snapshot.getSnapshot;

assert.equal(instance.instanceId, instanceId);
assert.equal(instance.driverKind, "pi");
assert.equal(typeof instance.adapter.startSession, "function");
assert.equal(typeof instance.textGeneration.generateCommitMessage, "function");
assert.equal(snapshot.enabled, false);
assert.equal(snapshot.instanceId, instanceId);
assert.equal(snapshot.driver, "pi");
}),
),
);
});
124 changes: 124 additions & 0 deletions apps/server/src/provider/Drivers/PiDriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { PiSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
import * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import { ChildProcessSpawner } from "effect/unstable/process";

import { BackgroundPolicy } from "../../background/BackgroundPolicy.ts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { makePiTextGeneration } from "../../textGeneration/PiTextGeneration.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makePiAdapter } from "../Layers/PiAdapter.ts";
import { checkPiProviderStatus, makePendingPiProvider } from "../Layers/PiProvider.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import {
defaultProviderContinuationIdentity,
type ProviderDriver,
type ProviderInstance,
} from "../ProviderDriver.ts";
import type { ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
import {
haveProviderSnapshotSettingsChanged,
makeProviderSnapshotSettingsSource,
type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";

const DRIVER_KIND = ProviderDriverKind.make("pi");
const decodeSettings = Schema.decodeSync(PiSettings);

export type PiDriverEnv =
| BackgroundPolicy
| ChildProcessSpawner.ChildProcessSpawner
| Crypto.Crypto
| FileSystem.FileSystem
| Path.Path
| ServerConfig
| ServerSettingsService;

export const PiDriver: ProviderDriver<PiSettings, PiDriverEnv> = {
driverKind: DRIVER_KIND,
metadata: { displayName: "Pi", supportsMultipleInstances: true },
configSchema: PiSettings,
defaultConfig: () => decodeSettings({}),
create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
Effect.gen(function* () {
const serverConfig = yield* ServerConfig;
const serverSettings = yield* ServerSettingsService;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const processEnv = mergeProviderInstanceEnvironment(environment);
const effectiveConfig = { ...config, enabled } satisfies PiSettings;
const continuationIdentity = defaultProviderContinuationIdentity({
driverKind: DRIVER_KIND,
instanceId,
});
const stamp = (snapshot: ServerProviderDraft): ServerProvider => ({
...snapshot,
instanceId,
driver: DRIVER_KIND,
...(displayName ? { displayName } : {}),
...(accentColor ? { accentColor } : {}),
continuation: { groupKey: continuationIdentity.continuationKey },
});
const adapter = yield* makePiAdapter({
binaryPath: effectiveConfig.binaryPath,
providerInstanceId: instanceId,
stateDir: serverConfig.stateDir,
attachmentsDir: serverConfig.attachmentsDir,
environment: processEnv,
}).pipe(
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: cause.message,
cause,
}),
),
);
const textGeneration = yield* makePiTextGeneration(effectiveConfig, processEnv);
const maintenanceCapabilities = makeManualOnlyProviderMaintenanceCapabilities({
provider: DRIVER_KIND,
packageName: null,
});
const source = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<PiSettings>>({
maintenanceCapabilities,
getSettings: source.getSettings,
streamSettings: source.streamSettings,
haveSettingsChanged: haveProviderSnapshotSettingsChanged,
initialSnapshot: (settings) =>
makePendingPiProvider(settings.provider).pipe(Effect.map(stamp)),
checkProvider: checkPiProviderStatus(effectiveConfig, processEnv).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.map(stamp),
),
}).pipe(
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to build Pi snapshot: ${String(cause)}`,
cause,
}),
),
);
return {
instanceId,
driverKind: DRIVER_KIND,
continuationIdentity,
displayName,
accentColor,
enabled,
snapshot,
adapter,
textGeneration,
} satisfies ProviderInstance;
}),
};
Loading
Loading