Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ out/
make/
artifacts/
mikework/
aiplans/
manifests/
.env
out
Expand Down
21 changes: 13 additions & 8 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,18 @@ tasks:
- cmd: powershell -NoProfile -NonInteractive -Command "Remove-Item -Force -ErrorAction SilentlyContinue -Path dist/bin/wsh*"
platforms: [windows]
ignore_error: true
- task: build:wsh:parallel
deps:
- go:mod:tidy
- generate
sources:
- "cmd/wsh/**/*.go"
- "pkg/**/*.go"
generates:
- "dist/bin/wsh*"

build:wsh:parallel:
deps:
- task: build:wsh:internal
vars:
GOOS: darwin
Expand Down Expand Up @@ -314,14 +326,7 @@ tasks:
vars:
GOOS: windows
GOARCH: arm64
deps:
- go:mod:tidy
- generate
sources:
- "cmd/wsh/**/*.go"
- "pkg/**/*.go"
generates:
- "dist/bin/wsh*"
internal: true

build:wsh:internal:
vars:
Expand Down
2 changes: 1 addition & 1 deletion cmd/wsh/cmd/wshcmd-root.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2025, Command Line Inc.
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd
Expand Down
1 change: 1 addition & 0 deletions docs/docs/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ wsh editconfig
| app:disablectrlshiftarrows <VersionBadge version="v0.14" /> | bool | Set to true to disable Ctrl+Shift block-navigation keybindings (`Arrow` and `h/j/k/l`) (defaults to false) |
| app:disablectrlshiftdisplay <VersionBadge version="v0.14" /> | bool | Set to true to disable the Ctrl+Shift visual indicator display (defaults to false) |
| app:focusfollowscursor <VersionBadge version="v0.14" /> | string | Controls whether block focus follows cursor movement: `"off"` (default), `"on"` (all blocks), or `"term"` (terminal blocks only) |
| app:tabbar <VersionBadge version="v0.14.4" /> | string | Controls the position of the tab bar: `"top"` (default) for a horizontal tab bar at the top of the window, or `"left"` for a vertical tab bar on the left side of the window |
| ai:preset | string | the default AI preset to use |
| ai:baseurl | string | Set the AI Base Url (must be OpenAI compatible) |
| ai:apitoken | string | your AI api token |
Expand Down
15 changes: 12 additions & 3 deletions frontend/app/aipanel/aipanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,11 @@ const ConfigChangeModeFixer = memo(() => {

ConfigChangeModeFixer.displayName = "ConfigChangeModeFixer";

const AIPanelComponentInner = memo(() => {
type AIPanelComponentInnerProps = {
roundTopLeft: boolean;
};

const AIPanelComponentInner = memo(({ roundTopLeft }: AIPanelComponentInnerProps) => {
const [isDragOver, setIsDragOver] = useState(false);
const [isReactDndDragOver, setIsReactDndDragOver] = useState(false);
const [initialLoadDone, setInitialLoadDone] = useState(false);
Expand Down Expand Up @@ -554,6 +558,7 @@ const AIPanelComponentInner = memo(() => {
isFocused ? "border-2 border-accent" : "border-2 border-transparent"
)}
style={{
borderTopLeftRadius: roundTopLeft ? 10 : 0,
borderTopRightRadius: model.inBuilder ? 0 : 10,
borderBottomRightRadius: model.inBuilder ? 0 : 10,
borderBottomLeftRadius: 10,
Expand Down Expand Up @@ -607,10 +612,14 @@ const AIPanelComponentInner = memo(() => {

AIPanelComponentInner.displayName = "AIPanelInner";

const AIPanelComponent = () => {
type AIPanelComponentProps = {
roundTopLeft: boolean;
};

const AIPanelComponent = ({ roundTopLeft }: AIPanelComponentProps) => {
return (
<ErrorBoundary>
<AIPanelComponentInner />
<AIPanelComponentInner roundTopLeft={roundTopLeft} />
</ErrorBoundary>
);
};
Expand Down
6 changes: 6 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,12 @@ export class RpcApiType {
return client.wshRpcCall("listalleditableapps", null, opts);
}

// command "macosversion" [call]
MacOSVersionCommand(client: WshClient, opts?: RpcOpts): Promise<string> {
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "macosversion", null, opts);
return client.wshRpcCall("macosversion", null, opts);
}

// command "makedraftfromlocal" [call]
MakeDraftFromLocalCommand(client: WshClient, data: CommandMakeDraftFromLocalData, opts?: RpcOpts): Promise<CommandMakeDraftFromLocalRtnData> {
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "makedraftfromlocal", data, opts);
Expand Down
89 changes: 5 additions & 84 deletions frontend/app/tab/tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { getTabBadgeAtom } from "@/app/store/badge";
import { getOrefMetaKeyAtom, globalStore, recordTEvent, refocusNode } from "@/app/store/global";
import { refocusNode } from "@/app/store/global";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { WaveEnv, WaveEnvSubset, useWaveEnv } from "@/app/waveenv/waveenv";
import { Button } from "@/element/button";
Expand All @@ -14,17 +14,20 @@ import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef,
import { makeORef } from "../store/wos";
import { TabBadges } from "./tabbadges";
import "./tab.scss";
import { buildTabContextMenu } from "./tabcontextmenu";

type TabEnv = WaveEnvSubset<{
export type TabEnv = WaveEnvSubset<{
rpc: {
ActivityCommand: WaveEnv["rpc"]["ActivityCommand"];
SetConfigCommand: WaveEnv["rpc"]["SetConfigCommand"];
SetMetaCommand: WaveEnv["rpc"]["SetMetaCommand"];
UpdateTabNameCommand: WaveEnv["rpc"]["UpdateTabNameCommand"];
};
atoms: {
fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"];
};
wos: WaveEnv["wos"];
getSettingsKeyAtom: WaveEnv["getSettingsKeyAtom"];
showContextMenu: WaveEnv["showContextMenu"];
}>;

Expand Down Expand Up @@ -216,88 +219,6 @@ const TabV = forwardRef<HTMLDivElement, TabVProps>((props, ref) => {

TabV.displayName = "TabV";

const FlagColors: { label: string; value: string }[] = [
{ label: "Green", value: "#58C142" },
{ label: "Teal", value: "#00FFDB" },
{ label: "Blue", value: "#429DFF" },
{ label: "Purple", value: "#BF55EC" },
{ label: "Red", value: "#FF453A" },
{ label: "Orange", value: "#FF9500" },
{ label: "Yellow", value: "#FFE900" },
];

function buildTabContextMenu(
id: string,
renameRef: React.RefObject<(() => void) | null>,
onClose: (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null) => void,
env: TabEnv
): ContextMenuItem[] {
const menu: ContextMenuItem[] = [];
menu.push(
{ label: "Rename Tab", click: () => renameRef.current?.() },
{
label: "Copy TabId",
click: () => fireAndForget(() => navigator.clipboard.writeText(id)),
},
{ type: "separator" }
);
const tabORef = makeORef("tab", id);
const currentFlagColor = globalStore.get(getOrefMetaKeyAtom(tabORef, "tab:flagcolor")) ?? null;
const flagSubmenu: ContextMenuItem[] = [
{
label: "None",
type: "checkbox",
checked: currentFlagColor == null,
click: () =>
fireAndForget(() =>
env.rpc.SetMetaCommand(TabRpcClient, { oref: tabORef, meta: { "tab:flagcolor": null } })
),
},
...FlagColors.map((fc) => ({
label: fc.label,
type: "checkbox" as const,
checked: currentFlagColor === fc.value,
click: () =>
fireAndForget(() =>
env.rpc.SetMetaCommand(TabRpcClient, { oref: tabORef, meta: { "tab:flagcolor": fc.value } })
),
})),
];
menu.push({ label: "Flag Tab", type: "submenu", submenu: flagSubmenu }, { type: "separator" });
const fullConfig = globalStore.get(env.atoms.fullConfigAtom);
const bgPresets: string[] = [];
for (const key in fullConfig?.presets ?? {}) {
if (key.startsWith("bg@") && fullConfig.presets[key] != null) {
bgPresets.push(key);
}
}
bgPresets.sort((a, b) => {
const aOrder = fullConfig.presets[a]["display:order"] ?? 0;
const bOrder = fullConfig.presets[b]["display:order"] ?? 0;
return aOrder - bOrder;
});
if (bgPresets.length > 0) {
const submenu: ContextMenuItem[] = [];
const oref = makeORef("tab", id);
for (const presetName of bgPresets) {
// preset cannot be null (filtered above)
const preset = fullConfig.presets[presetName];
submenu.push({
label: preset["display:name"] ?? presetName,
click: () =>
fireAndForget(async () => {
await env.rpc.SetMetaCommand(TabRpcClient, { oref, meta: preset });
env.rpc.ActivityCommand(TabRpcClient, { settabtheme: 1 }, { noresponse: true });
recordTEvent("action:settabtheme");
}),
});
}
menu.push({ label: "Backgrounds", type: "submenu", submenu }, { type: "separator" });
}
menu.push({ label: "Close Tab", click: () => onClose(null) });
return menu;
}

interface TabProps {
id: string;
active: boolean;
Expand Down
66 changes: 41 additions & 25 deletions frontend/app/tab/tabbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TabRpcClient } from "@/app/store/wshrpcutil";
import { useWaveEnv } from "@/app/waveenv/waveenv";
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
import { deleteLayoutModelForTab } from "@/layout/index";
import { isMacOSTahoeOrLater } from "@/util/platformutil";
import { fireAndForget } from "@/util/util";
import { useAtomValue } from "jotai";
import { OverlayScrollbars } from "overlayscrollbars";
Expand All @@ -20,6 +21,9 @@ import { WorkspaceSwitcher } from "./workspaceswitcher";

const TabDefaultWidth = 130;
const TabMinWidth = 100;
const MacOSTrafficLightsWidth = 74;
const MacOSTahoeTrafficLightsWidth = 80;

const OSOptions = {
overflow: {
x: "scroll",
Expand All @@ -39,6 +43,7 @@ const OSOptions = {

interface TabBarProps {
workspace: Workspace;
noTabs?: boolean;
}

const WaveAIButton = memo(({ divRef }: { divRef?: React.RefObject<HTMLDivElement> }) => {
Expand Down Expand Up @@ -152,7 +157,7 @@ function strArrayIsEqual(a: string[], b: string[]) {
return true;
}

const TabBar = memo(({ workspace }: TabBarProps) => {
const TabBar = memo(({ workspace, noTabs }: TabBarProps) => {
const env = useWaveEnv<TabBarEnv>();
const [tabIds, setTabIds] = useState<string[]>([]);
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
Expand Down Expand Up @@ -635,10 +640,13 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
// Calculate window drag left width based on platform and state
let windowDragLeftWidth = 10;
if (env.isMacOS() && !isFullScreen) {
const trafficLightsWidth = isMacOSTahoeOrLater()
? MacOSTahoeTrafficLightsWidth
: MacOSTrafficLightsWidth;
if (zoomFactor > 0) {
windowDragLeftWidth = 74 / zoomFactor;
windowDragLeftWidth = trafficLightsWidth / zoomFactor;
} else {
windowDragLeftWidth = 74;
windowDragLeftWidth = trafficLightsWidth;
}
}

Expand Down Expand Up @@ -680,33 +688,41 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
<WorkspaceSwitcher />
</Tooltip>
<div className="tab-bar" ref={tabBarRef} data-overlayscrollbars-initialize>
<div className="tabs-wrapper" ref={tabsWrapperRef} style={{ width: `${tabsWrapperWidth}px` }}>
{tabIds.map((tabId, index) => {
const isActive = activeTabId === tabId;
const showDivider = index !== 0 && !isActive && index !== activeTabIndex + 1;
return (
<Tab
key={tabId}
ref={tabRefs.current[index]}
id={tabId}
showDivider={showDivider}
onSelect={() => handleSelectTab(tabId)}
active={isActive}
onDragStart={(event) => handleDragStart(event, tabId, tabRefs.current[index])}
onClose={(event) => handleCloseTab(event, tabId)}
onLoaded={() => handleTabLoaded(tabId)}
isDragging={draggingTab === tabId}
tabWidth={tabWidthRef.current}
isNew={tabId === newTabId}
/>
);
})}
<div
className="tabs-wrapper"
ref={tabsWrapperRef}
style={{
width: noTabs ? 0 : tabsWrapperWidth,
...(noTabs ? ({ WebkitAppRegion: "drag" } as React.CSSProperties) : {}),
}}
>
{!noTabs &&
tabIds.map((tabId, index) => {
const isActive = activeTabId === tabId;
const showDivider = index !== 0 && !isActive && index !== activeTabIndex + 1;
return (
<Tab
key={tabId}
ref={tabRefs.current[index]}
id={tabId}
showDivider={showDivider}
onSelect={() => handleSelectTab(tabId)}
active={isActive}
onDragStart={(event) => handleDragStart(event, tabId, tabRefs.current[index])}
onClose={(event) => handleCloseTab(event, tabId)}
onLoaded={() => handleTabLoaded(tabId)}
isDragging={draggingTab === tabId}
tabWidth={tabWidthRef.current}
isNew={tabId === newTabId}
/>
);
})}
</div>
</div>
<button
ref={addBtnRef}
title="Add Tab"
className="flex h-[22px] px-2 mb-1 mx-1 items-center rounded-md box-border cursor-pointer hover:bg-hoverbg transition-colors text-[12px] text-secondary hover:text-primary"
className={`flex h-[22px] px-2 mb-1 mx-1 items-center rounded-md box-border cursor-pointer hover:bg-hoverbg transition-colors text-[12px] text-secondary hover:text-primary${noTabs ? " invisible" : ""}`}
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
onClick={handleAddTab}
>
Expand Down
7 changes: 6 additions & 1 deletion frontend/app/tab/tabbarenv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export type TabBarEnv = WaveEnvSubset<{
installAppUpdate: WaveEnv["electron"]["installAppUpdate"];
};
rpc: {
ActivityCommand: WaveEnv["rpc"]["ActivityCommand"];
SetConfigCommand: WaveEnv["rpc"]["SetConfigCommand"];
SetMetaCommand: WaveEnv["rpc"]["SetMetaCommand"];
UpdateTabNameCommand: WaveEnv["rpc"]["UpdateTabNameCommand"];
UpdateWorkspaceTabIdsCommand: WaveEnv["rpc"]["UpdateWorkspaceTabIdsCommand"];
};
atoms: {
Expand All @@ -24,7 +28,8 @@ export type TabBarEnv = WaveEnvSubset<{
updaterStatusAtom: WaveEnv["atoms"]["updaterStatusAtom"];
};
wos: WaveEnv["wos"];
getSettingsKeyAtom: SettingsKeyAtomFnType<"app:hideaibutton" | "tab:confirmclose" | "window:showmenubar">;
getSettingsKeyAtom: SettingsKeyAtomFnType<"app:hideaibutton" | "app:tabbar" | "tab:confirmclose" | "window:showmenubar">;
showContextMenu: WaveEnv["showContextMenu"];
mockSetWaveObj: WaveEnv["mockSetWaveObj"];
isWindows: WaveEnv["isWindows"];
isMacOS: WaveEnv["isMacOS"];
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/tab/tabcontent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const tileGapSizeAtom = atom((get) => {
return settings["window:tilegapsize"];
});

const TabContent = React.memo(({ tabId }: { tabId: string }) => {
const TabContent = React.memo(({ tabId, noTopPadding }: { tabId: string; noTopPadding?: boolean }) => {
const oref = useMemo(() => WOS.makeORef("tab", tabId), [tabId]);
const loadingAtom = useMemo(() => WOS.getWaveObjectLoadingAtom(oref), [oref]);
const tabLoading = useAtomValue(loadingAtom);
Expand Down Expand Up @@ -67,7 +67,7 @@ const TabContent = React.memo(({ tabId }: { tabId: string }) => {
}

return (
<div className="flex flex-row flex-grow min-h-0 w-full items-center justify-center overflow-hidden relative pt-[3px] pr-[3px]">
<div className={`flex flex-row flex-grow min-h-0 w-full items-center justify-center overflow-hidden relative ${noTopPadding ? "" : "pt-[3px]"} pr-[3px]`}>
{innerContent}
</div>
);
Expand Down
Loading
Loading