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
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.18.0] - 2026-04-07

See [docs/releases/v0.18.0.md](docs/releases/v0.18.0.md) for full notes and [docs/releases/v0.18.0/assets.md](docs/releases/v0.18.0/assets.md) for release asset inventory.

### Added

- Add a unified workspace panel to the right sidebar.
- Add bulk delete-all worktree cleanup action.
- Add preview layout modes and pop-out controls.
- Add project sidebar expand-all toggle.
- Add tab snapshot capture for the browser preview.
- Add support for binary project writes.

### Changed

- Make CLI publishing optional in the coordinated release workflow.
- Open diff files in the integrated viewer by default.
- Cache sidebar thread and project lookups for faster navigation.
- Lock the browser preview to the top dock.
- Collapse the workspace tree when binary project writes are shown.
- Move sidebar branding to the footer and drop the chat project badge.
- Clean up chat header panel toggles.

### Fixed

- Reduce preview and diff flicker during active workspace updates.
- Resolve web release warnings before shipping v0.18.0.

## [0.17.0] - 2026-04-07

See [docs/releases/v0.17.0.md](docs/releases/v0.17.0.md) for full notes and [docs/releases/v0.17.0/assets.md](docs/releases/v0.17.0/assets.md) for release asset inventory.
Expand Down Expand Up @@ -552,3 +580,4 @@ First public version tag. See [docs/releases/v0.0.1.md](docs/releases/v0.0.1.md)
[0.13.0]: https://github.com/OpenKnots/okcode/releases/tag/v0.13.0
[0.16.1]: https://github.com/OpenKnots/okcode/releases/tag/v0.16.1
[0.17.0]: https://github.com/OpenKnots/okcode/releases/tag/v0.17.0
[0.18.0]: https://github.com/OpenKnots/okcode/releases/tag/v0.18.0
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@okcode/desktop",
"version": "0.17.0",
"version": "0.18.0",
"private": true,
"main": "dist-electron/main.js",
"scripts": {
Expand Down
11 changes: 5 additions & 6 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1447,7 +1447,7 @@ function registerIpcHandlers(): void {
});

// Fill the pop-out window with the preview surface.
const [popOutWidth, popOutHeight] = popOut.getContentSize();
const [popOutWidth = 0, popOutHeight = 0] = popOut.getContentSize();
popOutController.setBounds({
x: 0,
y: 0,
Expand All @@ -1461,7 +1461,7 @@ function registerIpcHandlers(): void {
// Keep preview surface filling the pop-out window on resize.
popOut.on("resize", () => {
if (popOut.isDestroyed()) return;
const [w, h] = popOut.getContentSize();
const [w = 0, h = 0] = popOut.getContentSize();
popOutController.setBounds({
x: 0,
y: 0,
Expand All @@ -1488,10 +1488,9 @@ function registerIpcHandlers(): void {
if (isDevelopment && process.env.VITE_DEV_SERVER_URL) {
void popOut.loadURL(`${process.env.VITE_DEV_SERVER_URL}?popout=true`);
} else {
void popOut.loadFile(
Path.join(__dirname, "../../web/dist/index.html"),
{ query: { popout: "true" } },
);
void popOut.loadFile(Path.join(__dirname, "../../web/dist/index.html"), {
query: { popout: "true" },
});
}
});

Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "0.17.0"
versionName "0.18.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/ios/App/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.17.0;
MARKETING_VERSION = 0.18.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.openknots.okcode.mobile;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -325,7 +325,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.17.0;
MARKETING_VERSION = 0.18.0;
PRODUCT_BUNDLE_IDENTIFIER = com.openknots.okcode.mobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@okcode/mobile",
"version": "0.17.0",
"version": "0.18.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "okcodes",
"version": "0.17.0",
"version": "0.18.0",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
16 changes: 14 additions & 2 deletions apps/server/src/provider/Layers/OpenClawAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,14 +796,26 @@ function makeOpenClawAdapter(options?: OpenClawAdapterLiveOptions) {
id: context.nextRpcId++,
}),
),
catch: (e) => e as Error,
catch: (cause) =>
new ProviderAdapterProcessError({
provider: PROVIDER,
threadId: context.session.threadId,
detail: "Failed to send session.stop during teardown.",
cause,
}),
}).pipe(Effect.orElseSucceed(() => undefined));
}

// Close WebSocket.
yield* Effect.try({
try: () => context.ws.close(),
catch: (e) => e as Error,
catch: (cause) =>
new ProviderAdapterProcessError({
provider: PROVIDER,
threadId: context.session.threadId,
detail: "Failed to close websocket during teardown.",
cause,
}),
}).pipe(Effect.orElseSucceed(() => undefined));

// Interrupt stream fiber if active.
Expand Down
19 changes: 17 additions & 2 deletions apps/server/src/provider/Layers/ProviderHealth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,18 @@ import type {
ServerProviderStatus,
ServerProviderStatusState,
} from "@okcode/contracts";
import { Array, Effect, Fiber, FileSystem, Layer, Option, Path, Result, Stream } from "effect";
import {
Array,
Data,
Effect,
Fiber,
FileSystem,
Layer,
Option,
Path,
Result,
Stream,
} from "effect";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import {
Expand All @@ -28,6 +39,10 @@ const DEFAULT_TIMEOUT_MS = 4_000;
const CODEX_PROVIDER = "codex" as const;
const CLAUDE_AGENT_PROVIDER = "claudeAgent" as const;

class OpenClawHealthProbeError extends Data.TaggedError("OpenClawHealthProbeError")<{
cause: unknown;
}> {}

// ── Pure helpers ────────────────────────────────────────────────────

export interface CommandResult {
Expand Down Expand Up @@ -630,7 +645,7 @@ const checkOpenClawProviderStatus: Effect.Effect<ServerProviderStatus, never, ne
clearTimeout(timeout);
}
},
catch: (cause) => cause as Error,
catch: (cause) => new OpenClawHealthProbeError({ cause }),
}).pipe(Effect.result);

if (Result.isFailure(probeResult)) {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@okcode/web",
"version": "0.17.0",
"version": "0.18.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
10 changes: 6 additions & 4 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -423,18 +423,20 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
const previewDock = usePreviewStateStore((state) =>
activeProjectId ? (state.dockByProjectId[activeProjectId] ?? "top") : "top",
);
const previewLayoutMode = usePreviewStateStore((state) =>
activeProjectId ? (state.layoutModeByProjectId[activeProjectId] ?? "top") : "top",
);
const previewSizeDefault =
previewLayoutMode === "side" ? PREVIEW_SPLIT_SIDE_DEFAULT_SIZE_PX : PREVIEW_SPLIT_DEFAULT_SIZE_PX;
previewLayoutMode === "side"
? PREVIEW_SPLIT_SIDE_DEFAULT_SIZE_PX
: PREVIEW_SPLIT_DEFAULT_SIZE_PX;
const previewSize = usePreviewStateStore((state) =>
activeProjectId
? (state.sizeByProjectId[activeProjectId] ?? previewSizeDefault)
: previewSizeDefault,
);
const togglePreviewLayout = usePreviewStateStore((state) => state.toggleProjectLayout);
const setPreviewSize = usePreviewStateStore((state) => state.setProjectSize);
const previewLayoutMode = usePreviewStateStore((state) =>
activeProjectId ? (state.layoutModeByProjectId[activeProjectId] ?? "top") : "top",
);
const previewSplitRef = useRef<HTMLDivElement | null>(null);
const previewResizeStateRef = useRef<{
pointerId: number;
Expand Down
12 changes: 2 additions & 10 deletions apps/web/src/components/PreviewLayoutSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import type { ProjectId } from "@okcode/contracts";
import {
Maximize2Icon,
PanelRightIcon,
PanelTopIcon,
PictureInPicture2Icon,
} from "lucide-react";
import { Maximize2Icon, PanelRightIcon, PanelTopIcon, PictureInPicture2Icon } from "lucide-react";

import { readDesktopPreviewBridge } from "~/desktopPreview";
import { cn } from "~/lib/utils";
import {
type PreviewLayoutMode,
usePreviewStateStore,
} from "~/previewStateStore";
import { type PreviewLayoutMode, usePreviewStateStore } from "~/previewStateStore";

import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";

Expand Down
6 changes: 1 addition & 5 deletions apps/web/src/components/PreviewPanel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,7 @@ describe("PreviewLayoutSwitcher", () => {
});

describe("previewStateStore layout mode", () => {
const projectId = "test-project" as unknown as Parameters<
typeof usePreviewStateStore.getState
>[0] extends undefined
? string
: string;
const projectId = "test-project";

it("defaults layout mode to 'top'", () => {
const state = usePreviewStateStore.getState();
Expand Down
26 changes: 14 additions & 12 deletions apps/web/src/components/PreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -635,18 +635,20 @@ export function PreviewPanel({ projectId, threadId, onClose }: PreviewPanelProps
{/* Exit fullscreen shortcut button */}
{layoutMode === "fullscreen" && (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
size="icon-xs"
variant="ghost"
className="text-muted-foreground/55 hover:bg-transparent hover:text-foreground"
aria-label="Exit full screen"
onClick={() => toggleFullscreen(projectId)}
>
<Minimize2Icon className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipTrigger
render={
<Button
type="button"
size="icon-xs"
variant="ghost"
className="text-muted-foreground/55 hover:bg-transparent hover:text-foreground"
aria-label="Exit full screen"
onClick={() => toggleFullscreen(projectId)}
>
<Minimize2Icon className="size-3.5" />
</Button>
}
/>
<TooltipPopup side="bottom" sideOffset={6}>
Exit full screen
</TooltipPopup>
Expand Down
4 changes: 1 addition & 3 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1649,9 +1649,7 @@ export default function Sidebar() {
});
}, []);

const wordmark = (
<SidebarTrigger className="shrink-0 md:hidden" />
);
const wordmark = <SidebarTrigger className="shrink-0 md:hidden" />;

return (
<>
Expand Down
8 changes: 1 addition & 7 deletions apps/web/src/components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,16 @@ import type {
import { useQuery } from "@tanstack/react-query";
import { ChevronDownIcon, ExternalLinkIcon, GitPullRequestIcon } from "lucide-react";
import { memo, useCallback, useEffect } from "react";
import { useTheme } from "~/hooks/useTheme";
import { useThreadTitleEditor } from "~/hooks/useThreadTitleEditor";
import { shortcutLabelsForCommand } from "~/keybindings";
import type { ClientMode } from "~/lib/clientMode";
import { gitStatusQueryOptions } from "~/lib/gitReactQuery";
import { ensureNativeApi } from "~/nativeApi";
import type { PreviewDock } from "~/previewStateStore";
import { useProjectColor } from "~/projectColors";
import type { ProjectScriptDraft } from "~/projectScriptDefaults";
import { EditableThreadTitle } from "../EditableThreadTitle";
import GitActionsControl from "../GitActionsControl";
import ProjectScriptsControl, { type NewProjectScriptInput } from "../ProjectScriptsControl";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Kbd } from "../ui/kbd";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
Expand Down Expand Up @@ -61,7 +58,7 @@ interface ChatHeaderProps {
export const ChatHeader = memo(function ChatHeader({
activeThreadId,
activeThreadTitle,
activeProjectId,
activeProjectId: _activeProjectId,
activeProjectName,
activeProjectCwd,
isLocalDraftThread,
Expand Down Expand Up @@ -91,9 +88,6 @@ export const ChatHeader = memo(function ChatHeader({
onMinimize,
}: ChatHeaderProps) {
const isMobileCompanion = clientMode === "mobile";
const projectColor = useProjectColor(activeProjectId);
const { resolvedTheme } = useTheme();
const isDark = resolvedTheme === "dark";
const {
editingThreadId,
draftTitle,
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/chat/InlineDiffBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ function basename(filePath: string): string {
/** Max lines to show before collapsing with a "show more" toggle. */
const MAX_VISIBLE_LINES = 18;

function DiffLineRow(props: { line: DiffLine; html?: string }) {
function DiffLineRow(props: { line: DiffLine; html?: string | undefined }) {
const { line, html } = props;

return (
Expand Down Expand Up @@ -202,7 +202,7 @@ function DiffLineRow(props: { line: DiffLine; html?: string }) {
/** Shared diff-lines rendering used by both the main path and the error
* boundary fallback, eliminating the previous triplication of JSX. */
function DiffLinesContent(props: {
lines: Array<{ key: string; line: DiffLine; html?: string }>;
lines: Array<{ key: string; line: DiffLine; html?: string | undefined }>;
needsTruncation: boolean;
hiddenCount: number;
isExpanded: boolean;
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/hooks/useLayoutActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ export function useLayoutActions(): UseLayoutActionsResult {

const previewDock = projectId ? (previewState.dockByProjectId[projectId] ?? null) : null;
const previewSize = projectId ? (previewState.sizeByProjectId[projectId] ?? null) : null;
const previewLayoutMode = projectId
? (previewState.layoutModeByProjectId[projectId] ?? null)
: null;

const now = Date.now();
return {
Expand All @@ -164,6 +167,7 @@ export function useLayoutActions(): UseLayoutActionsResult {
sidebarWidths: readSidebarWidths(),
previewDock,
previewSize,
previewLayoutMode,
};
},
[],
Expand Down Expand Up @@ -214,6 +218,9 @@ export function useLayoutActions(): UseLayoutActionsResult {
if (layout.previewSize !== null) {
usePreviewStateStore.getState().setProjectSize(projectId, layout.previewSize);
}
if (layout.previewLayoutMode !== null) {
usePreviewStateStore.getState().setProjectLayoutMode(projectId, layout.previewLayoutMode);
}
}

// ── 4. Apply terminal state ────────────────────────────────
Expand Down Expand Up @@ -257,6 +264,7 @@ export function useLayoutActions(): UseLayoutActionsResult {
sidebarWidths: snapshot.sidebarWidths,
previewDock: snapshot.previewDock,
previewSize: snapshot.previewSize,
previewLayoutMode: snapshot.previewLayoutMode,
});
},
[captureCurrentLayout, updateLayoutInStore],
Expand Down
Loading
Loading