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
6 changes: 6 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ A cockpit is the user-facing control surface for coding-agent work. In JCode thi

The cockpit coordinates local tools and provider runtimes; it should not leak raw provider protocol details directly into ordinary user-facing concepts.

### OpenClaw Gateway Provider

The OpenClaw Gateway Provider is a first-class JCode Provider that connects to a user-configured OpenClaw Gateway URL and presents OpenClaw chat inside normal JCode Threads.

Its first version should treat the OpenClaw Gateway as a provider runtime rather than a separate external chat launcher. JCode owns the Settings configuration, provider health, thread-to-session mapping, and runtime-event translation while keeping OpenClaw protocol details behind the Provider boundary.

### Skill Library

The Skill Library is the settings-native surface for discovering and managing coding-agent skills across providers such as OpenCode and Codex.
Expand Down
35 changes: 24 additions & 11 deletions apps/server/integration/orchestrationEngine.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,25 @@ const APPROVAL_REQUEST_ID = asApprovalRequestId("req-approval-1");
const itLiveUnlessCi = (process.env.CI ? it.skip : it.live) as typeof it.live;
type IntegrationProvider = ProviderKind;

function defaultModelSelectionFor(
provider: Exclude<ProviderKind, "openclaw" | "pi">,
): ModelSelection {
switch (provider) {
case "codex":
return { provider, model: DEFAULT_MODEL_BY_PROVIDER.codex };
case "claudeAgent":
return { provider, model: DEFAULT_MODEL_BY_PROVIDER.claudeAgent };
case "cursor":
return { provider, model: DEFAULT_MODEL_BY_PROVIDER.cursor };
case "gemini":
return { provider, model: DEFAULT_MODEL_BY_PROVIDER.gemini };
case "kilo":
return { provider, model: DEFAULT_MODEL_BY_PROVIDER.kilo };
case "opencode":
return { provider, model: DEFAULT_MODEL_BY_PROVIDER.opencode };
}
}

function nowIso() {
return new Date().toISOString();
}
Expand Down Expand Up @@ -109,21 +128,18 @@ const seedProjectAndThread = (harness: OrchestrationIntegrationHarness) =>
Effect.gen(function* () {
const createdAt = nowIso();
const provider = harness.adapterHarness?.provider ?? "codex";
if (provider === "pi") {
throw new Error("Pi integration tests require an explicit model selection.");
if (provider === "openclaw" || provider === "pi") {
throw new Error("OpenClaw and Pi integration tests require an explicit model selection.");
}
const defaultModel = DEFAULT_MODEL_BY_PROVIDER[provider];
const defaultModelSelection = defaultModelSelectionFor(provider);

yield* harness.engine.dispatch({
type: "project.create",
commandId: CommandId.makeUnsafe("cmd-project-create"),
projectId: PROJECT_ID,
title: "Integration Project",
workspaceRoot: harness.workspaceDir,
defaultModelSelection: {
provider,
model: defaultModel,
},
defaultModelSelection,
createdAt,
});

Expand All @@ -133,10 +149,7 @@ const seedProjectAndThread = (harness: OrchestrationIntegrationHarness) =>
threadId: THREAD_ID,
projectId: PROJECT_ID,
title: "Integration Thread",
modelSelection: {
provider,
model: defaultModel,
},
modelSelection: defaultModelSelection,
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
branch: null,
Expand Down
12 changes: 12 additions & 0 deletions apps/server/src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { Open, type OpenShape } from "./open";
import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery";
import { AnalyticsService } from "./telemetry/Services/AnalyticsService";
import { Server, type ServerShape } from "./effectServer";
import { ServerSettingsService } from "./serverSettings";
import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore";

vi.mock("./threadRetention", async () => {
const Effect = await import("effect/Effect");
Expand All @@ -37,6 +39,14 @@ const serverStart = Effect.acquireRelease(
() => Effect.sync(() => stop()),
);
const findAvailablePort = vi.fn((preferred: number) => Effect.succeed(preferred));
const serverSecretStoreLayer = ServerSecretStoreLive.pipe(
Layer.provide(
ServerConfig.layerTest(process.cwd(), {
prefix: "jcode-main-test-",
}),
),
Layer.provide(NodeServices.layer),
);

// Shared service layer used by this CLI test suite.
const testLayer = Layer.mergeAll(
Expand All @@ -62,6 +72,8 @@ const testLayer = Layer.mergeAll(
AnalyticsService.layerTest,
FetchHttpClient.layer,
NodeServices.layer,
ServerSettingsService.layerTest(),
serverSecretStoreLayer,
);

const runCli = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,7 @@ const make = Effect.gen(function* () {
input.modelSelection ?? threadModelSelections.get(input.threadId) ?? thread.modelSelection;
const modelForTurn =
sessionModelSwitch === "unsupported"
? activeSession?.model !== undefined
? activeSession?.model !== undefined && requestedModelSelection.provider !== "openclaw"
? {
...requestedModelSelection,
model: activeSession.model,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1715,9 +1715,11 @@ const make = Effect.gen(function* () {
(entry) => entry.id === childThreadId,
);
const resolvedModelSelection =
identity?.model && identity.modelIsRequestedHint !== true
identity?.model &&
identity.modelIsRequestedHint !== true &&
parentThread.modelSelection.provider !== "openclaw"
? {
provider: parentThread.modelSelection.provider,
...parentThread.modelSelection,
model: identity.model,
}
: undefined;
Expand Down
Loading
Loading