Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3615f95
feat(providers): add Kimi provider with in-app OAuth sign-in
ItsJazii Aug 19, 2026
732aa4e
fix(kimi): retain healthy snapshot on transient probe timeout
ItsJazii Aug 19, 2026
0cfc291
feat(kimi): discover and switch models dynamically
ItsJazii Aug 20, 2026
9ea0264
feat(kimi): map interaction controls to native modes
ItsJazii Aug 20, 2026
59a8928
feat(kimi): discover model-specific thinking effort
ItsJazii Aug 20, 2026
31a9efb
fix(kimi): defer probes during turns and prefer official binary
ItsJazii Aug 20, 2026
ab2de32
fix(kimi): make the provider opt-in like sibling ACP providers
ItsJazii Aug 20, 2026
8251e2e
test(kimi): cover adapter turn lifecycle and pre-prompt chain
ItsJazii Aug 20, 2026
ebd23f0
fix(kimi): implement ACP client terminal support
ItsJazii Aug 20, 2026
b10ee5b
fix(kimi): kill session terminals on turn interrupt
ItsJazii Aug 20, 2026
6d15825
fix(kimi): repair approval and plan-mode flows
ItsJazii Aug 20, 2026
aa151a0
fix(kimi): make plan decisions native to the CLI
ItsJazii Aug 22, 2026
1df3639
Merge branch 'main' into feat/kimi-provider
ItsJazii Aug 22, 2026
f09f54a
fix(kimi): harden provider runtime state
ItsJazii Aug 22, 2026
c60584c
fix(kimi): harden per-instance authentication
ItsJazii Aug 22, 2026
3b778b6
fix(kimi): isolate authentication controls by instance
ItsJazii Aug 22, 2026
9e6577f
Merge branch 'fix/pr7908-review' into feat/kimi-provider
ItsJazii Aug 22, 2026
9145cc5
fix(kimi): make terminal creation interruption-safe
ItsJazii Aug 22, 2026
4c2c0c2
fix(web): announce Kimi sign-in status
ItsJazii Aug 22, 2026
be96ae6
fix(kimi): model authentication failures explicitly
ItsJazii Aug 22, 2026
21ca899
fix(kimi): settle prompt slots when a queued steer fiber is dropped
ItsJazii Aug 22, 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
15 changes: 14 additions & 1 deletion apps/mobile/src/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Path, Svg } from "react-native-svg";
import { Circle, Path, Svg } from "react-native-svg";
import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";

type ProviderIconProps = {
Expand Down Expand Up @@ -39,6 +39,19 @@ export function ProviderIcon(props: ProviderIconProps) {
);
}

if (props.provider === "kimi") {
const fill = isDarkMode ? "#F5F5F5" : "#0F0F0F";
return (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
{/* Stylized "K" mark matching the web KimiIcon: upright stem plus two
angled strokes, with the upper arm ending in Kimi's dot accent. */}
<Path fill={fill} d="M4 3h3.6v18H4z" />
<Path fill={fill} d="M9.4 13.2 16.6 21h4.6l-8.9-9.7 2.5-2.7-2.4-2.6-8.5 9.2h4.9z" />
<Circle fill={fill} cx="18.8" cy="5.2" r="2.2" />
</Svg>
);
}

if (props.provider === "cursor") {
return (
<Svg width={size} height={size} viewBox="0 0 466.73 532.09" fill="none">
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.serverGetBackgroundPolicy]: AuthOrchestrationReadScope,
[WS_METHODS.cloudGetRelayClientStatus]: AuthRelayReadScope,
[WS_METHODS.cloudInstallRelayClient]: AuthRelayWriteScope,
// Authentication mutates provider credentials on the host.
[WS_METHODS.kimiAuthSignIn]: AuthOrchestrationOperateScope,
[WS_METHODS.kimiAuthSignOut]: AuthOrchestrationOperateScope,
[WS_METHODS.pullRequestsList]: AuthOrchestrationReadScope,
[WS_METHODS.pullRequestsListStats]: AuthOrchestrationReadScope,
[WS_METHODS.pullRequestsDetail]: AuthOrchestrationReadScope,
Expand Down
268 changes: 268 additions & 0 deletions apps/server/src/provider/Drivers/KimiDriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import * as NodeOS from "node:os";

import { KimiSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
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 Ref from "effect/Ref";
import * as Schema from "effect/Schema";
import { HttpClient } from "effect/unstable/http";
import { ChildProcessSpawner } from "effect/unstable/process";

import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { makeKimiTextGeneration } from "../../textGeneration/KimiTextGeneration.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeKimiTurnActivity, type KimiTurnActivity } from "../acp/KimiAcpSupport.ts";
import { makeKimiAdapter } from "../Layers/KimiAdapter.ts";
import {
buildInitialKimiProviderSnapshot,
enrichKimiSnapshot,
isTransientKimiProbeClassification,
probeKimiProviderStatus,
type KimiModelDiscoveryCache,
type KimiProviderProbeResult,
} from "../Layers/KimiProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.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,
makeStaticProviderMaintenanceResolver,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import {
haveProviderSnapshotSettingsChanged,
makeProviderSnapshotSettingsSource,
type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";
const decodeKimiSettings = Schema.decodeSync(KimiSettings);

export const resolveKimiDriverBinaryPath = Effect.fn("resolveKimiDriverBinaryPath")(function* (
config: Pick<KimiSettings, "binaryPath">,
options?: { readonly homeDirectory?: string },
) {
const configuredBinaryPath = config.binaryPath.trim();
if (configuredBinaryPath && configuredBinaryPath !== "kimi") {
return configuredBinaryPath;
}
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const platform = yield* HostProcessPlatform;
const officialBinaryPath = path.join(
options?.homeDirectory ?? NodeOS.homedir(),
".kimi-code",
"bin",
platform === "win32" ? "kimi.exe" : "kimi",
);
const officialBinaryExists = yield* fileSystem
.exists(officialBinaryPath)
.pipe(Effect.orElseSucceed(() => false));
return officialBinaryExists ? officialBinaryPath : configuredBinaryPath || "kimi";
});

export function runKimiProbeWithActiveTurnDeferral<A, E, R>(input: {
readonly turnActivity: KimiTurnActivity;
readonly probe: Effect.Effect<A, E, R>;
}): Effect.Effect<A, E, R> {
return Effect.gen(function* () {
while (!(yield* input.turnActivity.beginProbeIfIdle)) {
const activeCount = yield* input.turnActivity.activeCount;
yield* Effect.logDebug("Deferring Kimi provider probe until active turns settle.", {
activeTurnCount: activeCount,
});
yield* input.turnActivity.awaitIdle;
}
return yield* input.probe.pipe(Effect.ensuring(input.turnActivity.endProbe));
});
}

const DRIVER_KIND = ProviderDriverKind.make("kimi");
// The Kimi CLI installs via Moonshot's install script (or a global npm
// package the script manages itself), so no T3-managed update path applies;
// updates stay manual.
const UPDATE = makeStaticProviderMaintenanceResolver(
makeManualOnlyProviderMaintenanceCapabilities({
provider: DRIVER_KIND,
packageName: null,
}),
);

export type KimiDriverEnv =
| BackgroundPolicy.BackgroundPolicy
| ChildProcessSpawner.ChildProcessSpawner
| Crypto.Crypto
| FileSystem.FileSystem
| HttpClient.HttpClient
| Path.Path
| ProviderEventLoggers
| ServerConfig
| ServerSettingsService;

const withInstanceIdentity =
(input: {
readonly instanceId: ProviderInstance["instanceId"];
readonly displayName: string | undefined;
readonly accentColor: string | undefined;
readonly continuationGroupKey: string;
}) =>
(snapshot: ServerProviderDraft): ServerProvider => ({
...snapshot,
instanceId: input.instanceId,
driver: DRIVER_KIND,
...(input.displayName ? { displayName: input.displayName } : {}),
...(input.accentColor ? { accentColor: input.accentColor } : {}),
continuation: { groupKey: input.continuationGroupKey },
});

export const stabilizeKimiProviderProbe = Effect.fn("stabilizeKimiProviderProbe")(function* (
lastKnownGoodRef: Ref.Ref<ServerProvider | null>,
result: KimiProviderProbeResult<ServerProvider>,
) {
if (result.classification._tag === "healthy") {
yield* Ref.set(lastKnownGoodRef, result.snapshot);
return result.snapshot;
}
if (isTransientKimiProbeClassification(result.classification)) {
return (yield* Ref.get(lastKnownGoodRef)) ?? result.snapshot;
}
yield* Ref.set(lastKnownGoodRef, null);
return result.snapshot;
});

export const KimiDriver: ProviderDriver<KimiSettings, KimiDriverEnv> = {
driverKind: DRIVER_KIND,
metadata: {
displayName: "Kimi",
supportsMultipleInstances: true,
},
configSchema: KimiSettings,
defaultConfig: (): KimiSettings => decodeKimiSettings({}),
create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
Effect.gen(function* () {
const crypto = yield* Crypto.Crypto;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const httpClient = yield* HttpClient.HttpClient;
const serverSettings = yield* ServerSettingsService;
const eventLoggers = yield* ProviderEventLoggers;
const processEnv = mergeProviderInstanceEnvironment(environment);
const continuationIdentity = defaultProviderContinuationIdentity({
driverKind: DRIVER_KIND,
instanceId,
});
const stampIdentity = withInstanceIdentity({
instanceId,
displayName,
accentColor,
continuationGroupKey: continuationIdentity.continuationKey,
});
const resolvedBinaryPath = yield* resolveKimiDriverBinaryPath(config);
const effectiveConfig = {
...config,
binaryPath: resolvedBinaryPath,
enabled,
} satisfies KimiSettings;
const turnActivity = yield* makeKimiTurnActivity;
const lastKnownGoodRef = yield* Ref.make<ServerProvider | null>(null);
const discoveryCacheRef = yield* Ref.make<KimiModelDiscoveryCache | undefined>(undefined);
const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
});

const adapter = yield* makeKimiAdapter(effectiveConfig, {
environment: processEnv,
...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
instanceId,
turnActivity,
});
const textGeneration = yield* makeKimiTextGeneration(effectiveConfig, processEnv);

const probeProvider = Effect.gen(function* () {
const discoveryCache = yield* Ref.get(discoveryCacheRef);
const result = yield* probeKimiProviderStatus(
effectiveConfig,
processEnv,
discoveryCache ? { discoveryCache } : {},
);
if (result.classification._tag === "healthy") {
yield* Ref.set(discoveryCacheRef, result.discoveryCache);
} else if (!isTransientKimiProbeClassification(result.classification)) {
yield* Ref.set(discoveryCacheRef, undefined);
}
return yield* stabilizeKimiProviderProbe(lastKnownGoodRef, {
...result,
snapshot: stampIdentity(result.snapshot),
});
}).pipe(
Effect.provideService(Crypto.Crypto, crypto),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
);
const checkProvider = runKimiProbeWithActiveTurnDeferral({
turnActivity,
probe: probeProvider,
});

const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
const managedSnapshot = yield* makeManagedServerProvider<
ProviderSnapshotSettings<KimiSettings>
>({
maintenanceCapabilities,
getSettings: snapshotSettings.getSettings,
streamSettings: snapshotSettings.streamSettings,
haveSettingsChanged: haveProviderSnapshotSettingsChanged,
initialSnapshot: (settings) =>
buildInitialKimiProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)),
checkProvider,
enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
enrichKimiSnapshot({
snapshot: currentSnapshot,
maintenanceCapabilities,
enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
publishSnapshot,
httpClient,
}),
}).pipe(
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to build Kimi snapshot: ${cause.message ?? String(cause)}`,
cause,
}),
),
);
const snapshot = {
maintenanceCapabilities: managedSnapshot.maintenanceCapabilities,
getSnapshot: managedSnapshot.getSnapshot,
refresh: Ref.set(discoveryCacheRef, undefined).pipe(
Effect.andThen(managedSnapshot.refresh),
),
get streamChanges() {
return managedSnapshot.streamChanges;
},
};

return {
instanceId,
driverKind: DRIVER_KIND,
continuationIdentity,
displayName,
accentColor,
enabled,
snapshot,
adapter,
textGeneration,
} satisfies ProviderInstance;
}),
};
Loading
Loading