Skip to content

Commit e840c02

Browse files
committed
Release v0.22.1 with decision workspace updates
- Add decision workspace projections, persistence, and RPC wiring - Improve OpenClaw gateway diagnostics and handshake handling - Bump app versions and add release notes for 0.22.1
1 parent 55604da commit e840c02

36 files changed

Lines changed: 602 additions & 142 deletions

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.22.1] - 2026-04-10
11+
12+
See [docs/releases/v0.22.1.md](docs/releases/v0.22.1.md) for full notes and [docs/releases/v0.22.1/assets.md](docs/releases/v0.22.1/assets.md) for release asset inventory.
13+
14+
### Added
15+
16+
- Add decision workspace contracts, projections, persistence tables, and WebSocket wiring groundwork.
17+
- Add pending user input projections plus thread overview and detail queries.
18+
- Add sidebar density controls, connection test controls, and expandable notification diagnostics with copy support.
19+
- Add companion pairing contracts and mobile pairing stubs.
20+
- Add stop support for pending git actions and external GitHub link opening from the preview popout.
21+
- Add OpenClaw maintainer workflow skills.
22+
23+
### Changed
24+
25+
- Switch SME Claude flows to Claude Code CLI.
26+
- Extract the OpenClaw gateway client with auth fallback and modernize the gateway handshake flow.
27+
- Refresh theme tokens, default typography, and VS Code icon manifests.
28+
- Preserve thread routes in desktop pop-out windows and widen preview viewport inputs.
29+
- Render SME replies as markdown and replace the draft upload icon with a close action.
30+
31+
### Fixed
32+
33+
- Ignore expected redacted auth shutdown noise in Codex logs.
34+
- Normalize React language ids for syntax highlighting.
35+
- Defer the empty diff guard until after hook setup.
36+
1037
## [0.22.0] - 2026-04-09
1138

1239
See [docs/releases/v0.22.0.md](docs/releases/v0.22.0.md) for full notes and [docs/releases/v0.22.0/assets.md](docs/releases/v0.22.0/assets.md) for release asset inventory.
@@ -658,3 +685,4 @@ First public version tag. See [docs/releases/v0.0.1.md](docs/releases/v0.0.1.md)
658685
[0.20.0]: https://github.com/OpenKnots/okcode/releases/tag/v0.20.0
659686
[0.21.0]: https://github.com/OpenKnots/okcode/releases/tag/v0.21.0
660687
[0.22.0]: https://github.com/OpenKnots/okcode/releases/tag/v0.22.0
688+
[0.22.1]: https://github.com/OpenKnots/okcode/releases/tag/v0.22.1

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@okcode/desktop",
3-
"version": "0.22.0",
3+
"version": "0.22.1",
44
"private": true,
55
"main": "dist-electron/main.js",
66
"scripts": {

apps/mobile/android/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ android {
88
minSdkVersion rootProject.ext.minSdkVersion
99
targetSdkVersion rootProject.ext.targetSdkVersion
1010
versionCode 1
11-
versionName "0.22.0"
11+
versionName "0.22.1"
1212
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1313
aaptOptions {
1414
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

apps/mobile/ios/App/App.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@
306306
"$(inherited)",
307307
"@executable_path/Frameworks",
308308
);
309-
MARKETING_VERSION = 0.22.0;
309+
MARKETING_VERSION = 0.22.1;
310310
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
311311
PRODUCT_BUNDLE_IDENTIFIER = com.openknots.okcode.mobile;
312312
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -331,7 +331,7 @@
331331
"$(inherited)",
332332
"@executable_path/Frameworks",
333333
);
334-
MARKETING_VERSION = 0.22.0;
334+
MARKETING_VERSION = 0.22.1;
335335
PRODUCT_BUNDLE_IDENTIFIER = com.openknots.okcode.mobile;
336336
PRODUCT_NAME = "$(TARGET_NAME)";
337337
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

apps/mobile/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@okcode/mobile",
3-
"version": "0.22.0",
3+
"version": "0.22.1",
44
"private": true,
55
"type": "module",
66
"scripts": {

apps/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "okcodes",
3-
"version": "0.22.0",
3+
"version": "0.22.1",
44
"license": "MIT",
55
"repository": {
66
"type": "git",

apps/server/src/decision/Services/DecisionProjection.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ export interface DecisionProjectionShape {
2222
readonly consultation: DecisionConsultation;
2323
readonly questions: ReadonlyArray<DecisionConsultationQuestion>;
2424
}) => Effect.Effect<void, DecisionWorkspaceServiceError>;
25-
readonly getConsultation: (input: {
26-
readonly consultationId: string;
27-
}) => Effect.Effect<
28-
{ consultation: DecisionConsultation; questions: ReadonlyArray<DecisionConsultationQuestion> } | null,
25+
readonly getConsultation: (input: { readonly consultationId: string }) => Effect.Effect<
26+
{
27+
consultation: DecisionConsultation;
28+
questions: ReadonlyArray<DecisionConsultationQuestion>;
29+
} | null,
2930
DecisionWorkspaceServiceError
3031
>;
3132
readonly listConsultationsByCaseId: (input: {

apps/server/src/main.test.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import { mkdtempSync } from "node:fs";
12
import * as Http from "node:http";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
25
import * as NodeServices from "@effect/platform-node/NodeServices";
36
import { assert, it, vi } from "@effect/vitest";
47
import * as ConfigProvider from "effect/ConfigProvider";
@@ -17,6 +20,7 @@ import { Server, type ServerShape } from "./wsServer";
1720
const start = vi.fn(() => undefined);
1821
const stop = vi.fn(() => undefined);
1922
let resolvedConfig: ServerConfigShape | null = null;
23+
let testWorkspaceRoot = "";
2024
const serverStart = Effect.acquireRelease(
2125
Effect.gen(function* () {
2226
resolvedConfig = yield* ServerConfig;
@@ -29,11 +33,17 @@ const findAvailablePort = vi.fn((preferred: number) => Effect.succeed(preferred)
2933

3034
// Shared service layer used by this CLI test suite.
3135
const testLayer = Layer.mergeAll(
32-
Layer.succeed(CliConfig, {
33-
cwd: "/tmp/t3-test-workspace",
34-
fixPath: Effect.void,
35-
resolveStaticDir: Effect.undefined,
36-
} satisfies CliConfigShape),
36+
Layer.effect(
37+
CliConfig,
38+
Effect.sync(
39+
() =>
40+
({
41+
cwd: testWorkspaceRoot,
42+
fixPath: Effect.void,
43+
resolveStaticDir: Effect.undefined,
44+
}) satisfies CliConfigShape,
45+
),
46+
),
3747
Layer.succeed(NetService, {
3848
canListenOnHost: () => Effect.succeed(true),
3949
isPortAvailableOnLoopback: () => Effect.succeed(true),
@@ -74,6 +84,7 @@ const runCli = (
7484
beforeEach(() => {
7585
vi.clearAllMocks();
7686
resolvedConfig = null;
87+
testWorkspaceRoot = mkdtempSync(join(tmpdir(), "okcode-main-test-"));
7788
start.mockImplementation(() => undefined);
7889
stop.mockImplementation(() => undefined);
7990
findAvailablePort.mockImplementation((preferred: number) => Effect.succeed(preferred));

apps/server/src/openclawGatewayTest.ts

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ interface MutableGatewayDiagnostics {
4646
hints: string[];
4747
}
4848

49+
interface RunOpenclawGatewayTestOptions {
50+
readonly stateDir?: string | undefined;
51+
}
52+
4953
interface OpenClawGatewayErrorLike {
5054
readonly message: string;
5155
readonly code?: string;
@@ -75,10 +79,6 @@ function toMessage(cause: unknown, fallback: string): string {
7579
return fallback;
7680
}
7781

78-
function readString(value: unknown): string | undefined {
79-
return typeof value === "string" && value.length > 0 ? value : undefined;
80-
}
81-
8282
function applyGatewayError(
8383
diagnostics: MutableGatewayDiagnostics,
8484
error: OpenClawGatewayErrorLike | undefined,
@@ -87,24 +87,40 @@ function applyGatewayError(
8787
return;
8888
}
8989

90-
diagnostics.gatewayErrorCode = error.code;
90+
if (typeof error.code === "string") {
91+
diagnostics.gatewayErrorCode = error.code;
92+
}
9193
const details = error.details ?? {};
92-
diagnostics.gatewayErrorDetailCode = typeof details.code === "string" ? details.code : undefined;
93-
diagnostics.gatewayErrorDetailReason =
94-
typeof details.reason === "string" ? details.reason : undefined;
95-
diagnostics.gatewayRecommendedNextStep =
96-
typeof details.recommendedNextStep === "string" ? details.recommendedNextStep : undefined;
97-
diagnostics.gatewayCanRetryWithDeviceToken =
98-
typeof details.canRetryWithDeviceToken === "boolean"
99-
? details.canRetryWithDeviceToken
100-
: undefined;
94+
if (typeof details.code === "string") {
95+
diagnostics.gatewayErrorDetailCode = details.code;
96+
}
97+
if (typeof details.reason === "string") {
98+
diagnostics.gatewayErrorDetailReason = details.reason;
99+
}
100+
if (typeof details.recommendedNextStep === "string") {
101+
diagnostics.gatewayRecommendedNextStep = details.recommendedNextStep;
102+
}
103+
if (typeof details.canRetryWithDeviceToken === "boolean") {
104+
diagnostics.gatewayCanRetryWithDeviceToken = details.canRetryWithDeviceToken;
105+
}
101106
}
102107

103108
function pushUnique(items: string[], value: string): void {
104109
if (items.includes(value) || items.length >= MAX_CAPTURED_NOTIFICATIONS) return;
105110
items.push(value);
106111
}
107112

113+
function formatGatewayFailureDetail(
114+
detail: string,
115+
diagnostics: Pick<MutableGatewayDiagnostics, "gatewayErrorDetailCode">,
116+
): string {
117+
const code = diagnostics.gatewayErrorDetailCode;
118+
if (!code || detail.includes(code)) {
119+
return detail;
120+
}
121+
return `${detail} (${code})`;
122+
}
123+
108124
function isLoopbackHost(host: string): boolean {
109125
const normalized = host.toLowerCase();
110126
return (
@@ -245,11 +261,6 @@ async function probeHealth(parsedUrl: URL): Promise<GatewayHealthProbe> {
245261
}
246262
}
247263

248-
function formatSocketClose(code: number | undefined, reason: string | undefined): string | null {
249-
if (code === undefined) return null;
250-
return reason && reason.length > 0 ? `code ${code}: ${reason}` : `code ${code}`;
251-
}
252-
253264
function buildHints(
254265
parsedUrl: URL,
255266
diagnostics: Pick<
@@ -413,6 +424,7 @@ export async function runOpenclawGatewayTest(
413424
const steps: TestOpenclawGatewayStep[] = [];
414425
const diagnostics: MutableGatewayDiagnostics = createDiagnostics();
415426
let parsedUrlForHints: URL | null = null;
427+
let connection: Awaited<ReturnType<typeof connectOpenClawGateway>> | undefined;
416428

417429
const pushStep = (
418430
name: string,
@@ -504,11 +516,9 @@ export async function runOpenclawGatewayTest(
504516
diagnostics.hostKind = classifyGatewayHost(parsedUrl.hostname, diagnostics.resolvedAddresses);
505517

506518
const connectStart = Date.now();
507-
let connection: Awaited<ReturnType<typeof connectOpenClawGateway>> | undefined;
508519
try {
509520
connection = await connectOpenClawGateway({
510521
gatewayUrl,
511-
stateDir: options?.stateDir,
512522
sessionKey: "okcode:gateway-test",
513523
role: "operator",
514524
scopes: [...OPENCLAW_OPERATOR_SCOPES],
@@ -525,7 +535,8 @@ export async function runOpenclawGatewayTest(
525535
},
526536
userAgent: `okcode/${serverBuildInfo.version}`,
527537
locale: Intl.DateTimeFormat().resolvedOptions().locale || "en-US",
528-
password: sharedSecret,
538+
...(options?.stateDir ? { stateDir: options.stateDir } : {}),
539+
...(sharedSecret ? { password: sharedSecret } : {}),
529540
onEvent: (event) => {
530541
pushUnique(diagnostics.observedNotifications, event.event);
531542
},
@@ -543,8 +554,27 @@ export async function runOpenclawGatewayTest(
543554
cause instanceof Error
544555
? (cause as Error & { readonly gatewayError?: OpenClawGatewayErrorLike }).gatewayError
545556
: undefined;
557+
const connectionStage =
558+
cause instanceof Error
559+
? (cause as Error & { readonly openClawConnectionStage?: "websocket" | "handshake" })
560+
.openClawConnectionStage
561+
: undefined;
546562
applyGatewayError(diagnostics, gatewayError);
547-
const detail = toMessage(cause, "Connection failed.");
563+
const detail = formatGatewayFailureDetail(
564+
toMessage(cause, "Connection failed."),
565+
diagnostics,
566+
);
567+
if (connectionStage === "handshake") {
568+
pushStep(
569+
"WebSocket connect",
570+
"pass",
571+
Date.now() - connectStart,
572+
`Connected in ${Date.now() - connectStart}ms`,
573+
);
574+
applyHealthProbe(await healthPromise);
575+
pushStep("Gateway handshake", "fail", 0, detail);
576+
return finalize(false, detail, "Gateway handshake");
577+
}
548578
pushStep("WebSocket connect", "fail", Date.now() - connectStart, detail);
549579
applyHealthProbe(await healthPromise);
550580
return finalize(false, detail, "WebSocket connect");

apps/server/src/orchestration/Layers/ProjectionOverviewQuery.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,21 @@ import {
3131
import { ProjectionState } from "../../persistence/Services/ProjectionState.ts";
3232
import { ProjectionProject } from "../../persistence/Services/ProjectionProjects.ts";
3333
import { ProjectionThread } from "../../persistence/Services/ProjectionThreads.ts";
34-
import { ProjectionThreadProposedPlan } from "../../persistence/Services/ProjectionThreadProposedPlans.ts";
3534
import { ProjectionThreadSession } from "../../persistence/Services/ProjectionThreadSessions.ts";
3635
import { ORCHESTRATION_PROJECTOR_NAMES } from "./ProjectionPipeline.ts";
3736

38-
const ProjectionProjectOverviewRow = ProjectionProject.mapFields({
37+
const ProjectionProjectOverviewRow = Schema.Struct({
38+
...ProjectionProject.fields,
3939
scripts: Schema.fromJsonString(Schema.Array(ProjectScript)),
40-
}).pipe(
41-
Schema.extend(
42-
Schema.Struct({
43-
activeThreadCount: NonNegativeInt,
44-
}),
45-
),
46-
);
40+
activeThreadCount: NonNegativeInt,
41+
});
4742

48-
const ProjectionThreadOverviewRow = ProjectionThread.pipe(
49-
Schema.extend(
50-
Schema.Struct({
51-
lastUserMessageAt: Schema.NullOr(IsoDateTime),
52-
pendingApprovalCount: NonNegativeInt,
53-
pendingUserInputCount: NonNegativeInt,
54-
}),
55-
),
56-
);
43+
const ProjectionThreadOverviewRow = Schema.Struct({
44+
...ProjectionThread.fields,
45+
lastUserMessageAt: Schema.NullOr(IsoDateTime),
46+
pendingApprovalCount: NonNegativeInt,
47+
pendingUserInputCount: NonNegativeInt,
48+
});
5749

5850
const ProjectionLatestTurnDbRowSchema = Schema.Struct({
5951
threadId: ProjectionThread.fields.threadId,
@@ -419,7 +411,7 @@ const makeProjectionOverviewQuery = Effect.gen(function* () {
419411
projects,
420412
threads,
421413
updatedAt:
422-
updatedAtCandidates.sort((left, right) =>
414+
updatedAtCandidates.toSorted((left, right) =>
423415
left < right ? 1 : left > right ? -1 : 0,
424416
)[0] ?? new Date(0).toISOString(),
425417
});

0 commit comments

Comments
 (0)