Compare commits

..

15 Commits

Author SHA1 Message Date
Sylvia Crowe 0cdb63bec8 fix: frontend changes for shell pref in imports
This continues the previous changes to allow the shell to be configured
after importing the ssh_config file. This allows temporary editing of
the shell preference for imported remotes as well as adding a keywork to
allow it to be set in the ssh_config file.
2024-01-31 12:56:26 -08:00
Sylvia Crowe 0417c8a158 fix: set shellpref default settings for backend
The shell preference is a new feature that was never integrated into ssh
config importing. This makes the backend changes to allow that to
happen.
2024-01-31 12:17:33 -08:00
Evan Simkowitz 5ce7b92232 Simplify logic for startResizing width calculation (#269)
* Apply Red's suggestion and move width logic to getWidth

* remove unnecessary

* a little more

* remove comment
2024-01-30 22:34:00 -08:00
Evan Simkowitz 81a9e74991 Fix onMouseDown causing width to snap back to persisted width (#268)
* Fix onMouseDown causing width to snap back to persisted width

* var name change

* remove logs

* remove redundant call
2024-01-30 22:23:00 -08:00
Evan Simkowitz 679e85a84a Fix sidebar width not persisting when un-collapsing (#267)
* Do not persist collapsed width to db

* remove console.logs

* one more

* one more

* remove setTemp from getWidth and add to the startResize call
2024-01-30 22:04:25 -08:00
Evan Simkowitz da69a0f583 Fix broken merge (#266) 2024-01-30 20:56:32 -08:00
Red J Adaya 37ab1bca90 allow resizing of left sidebar (#244)
* wip

* integrate original sidebar content

* ResizableSidebar component

* trigger toggleCollapse

* remove debugging code

* minor refactor. disable text select on mousemove

* replace icons with fontawesome icons. fix alignment issues

* fix session view width when tabs overflow

* prevent index and icon from shifting when resizing

* snap effect

* minor refactor

* apply collapsed mode to sidebar contents

* change default width to 240px

* backend implementation

* fix wrong subcmd

* save collapsed state

* retore sidebar state on reload/launch

* use collapse data form db on first load. use previously saved width on expand.

* persist width as well collapse state

* various fixes and improvements

* bind methods

* refactor

* more refactor

* fix minor bug

* fix merge issues

* various fixes

* refactor

* fixes

* fix issues

* fix all issues

* resolve undefind tempWidth

* fix toggleCollapsed

* use Promise in stopResizing method

* use tempCollapsed to for real time toggling between logos

* minor method name change

* refactor

* remove debugging code

* fix conflict

* fix setting collapsed state via CLI

* minor refactor

* remove debugging code

* create setTempWidthAndTempCollapsed method

* handle invalid width set via cli

* refactor: setbycli not actually needed

* remove unused code
2024-01-30 20:17:49 -08:00
Evan Simkowitz 40757fa7f4 Prevent the status indicator flickering for quick-returning commands (#265)
* add status-indicator-visible

* save

* save

* Prevent the status indicator flickering for quick returns

* flx regression

* reduce delay, reset spinnerVisible when there's no more running commands

* clean up code reuse

* move code around

* slight optimizations to prevent rendering before spinner is visible

* rename var

* revert shouldSync change as it broke the sync
2024-01-30 16:31:38 -08:00
Evan Simkowitz e576f7f07d Sync RotateIcon spin as well (#262)
* Sync RotateIcon spin as well

* make shouldSync a boolean value
2024-01-30 14:16:33 -08:00
Evan Simkowitz 9e804232c2 Fix SidebarItem key error (#263) 2024-01-30 11:54:35 -08:00
sawka d3dbac6e77 fix missing react key 2024-01-30 11:29:12 -08:00
Mike Sawka 198de02a65 code to restore indicator state on refresh (#260)
* code to restore indicator state on refresh

* fix style
2024-01-29 23:51:01 -08:00
Mike Sawka 8f64d6a589 set animation startTime to sync spinners for status indicators (#259)
* set animation startTime to sync spinners for status indicators

* catch animationstart (hover interaction) to restart startTime.

* use const

---------

Co-authored-by: Evan Simkowitz <esimkowitz@users.noreply.github.com>
2024-01-29 23:48:37 -08:00
Evan Simkowitz 1a51d93a54 Fix add-workspace so that only the plus button shows pointer (#258) 2024-01-29 14:10:58 -08:00
sawka 9ab3b2a5b7 only run prettier on js/ts/css/less files 2024-01-29 13:35:09 -08:00
27 changed files with 922 additions and 468 deletions
+19 -2
View File
@@ -1,4 +1,21 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
+12 -4
View File
@@ -30,6 +30,7 @@ type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class App extends React.Component<{}, {}> {
dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" });
mainContentRef: React.RefObject<HTMLDivElement> = React.createRef();
constructor(props: any) {
super(props);
@@ -75,6 +76,13 @@ class App extends React.Component<{}, {}> {
let hasClientStop = GlobalModel.getHasClientStop();
let dcWait = this.dcWait.get();
let platform = GlobalModel.getPlatform();
let clientData = GlobalModel.clientData.get();
// Previously, this is done in sidebar.tsx but it causes flicker when clientData is null cos screen-view shifts around.
// Doing it here fixes the flicker cos app is not rendered until clientData is populated.
if (clientData == null) {
return null;
}
if (disconnected || hasClientStop) {
if (!dcWait) {
@@ -82,8 +90,8 @@ class App extends React.Component<{}, {}> {
}
return (
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div className="main-content">
<MainSideBar />
<div ref={this.mainContentRef} className="main-content">
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<div className="session-view" />
</div>
<If condition={dcWait}>
@@ -102,8 +110,8 @@ class App extends React.Component<{}, {}> {
}
return (
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div className="main-content">
<MainSideBar />
<div ref={this.mainContentRef} className="main-content">
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<ErrorBoundary>
<PluginsView />
<WorkspaceView />
+164 -2
View File
@@ -9,11 +9,12 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import cn from "classnames";
import { If } from "tsx-control-statements/components";
import { RemoteType, StatusIndicatorLevel } from "../../types/types";
import { RemoteType } from "../../types/types";
import ReactDOM from "react-dom";
import { GlobalModel } from "../../model/model";
import { GlobalModel, GlobalCommandRunner } from "../../model/model";
import * as appconst from "../appconst";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
import { MagicLayout } from "../magiclayout";
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
@@ -1265,6 +1266,166 @@ In order to use Wave's advanced features like unified history and persistent ses
});
}
interface ResizableSidebarProps {
parentRef: React.RefObject<HTMLElement>;
position: "left" | "right";
enableSnap?: boolean;
className?: string;
children?: (toggleCollapsed: () => void) => React.ReactNode;
toggleCollapse?: () => void;
}
@mobxReact.observer
class ResizableSidebar extends React.Component<ResizableSidebarProps> {
resizeStartWidth: number = 0;
startX: number = 0;
prevDelta: number = 0;
prevDragDirection: string = null;
disposeReaction: any;
@boundMethod
startResizing(event: React.MouseEvent<HTMLDivElement>) {
event.preventDefault();
const { parentRef, position } = this.props;
const parentRect = parentRef.current?.getBoundingClientRect();
if (!parentRect) return;
if (position === "right") {
this.startX = parentRect.right - event.clientX;
} else {
this.startX = event.clientX - parentRect.left;
}
const mainSidebarModel = GlobalModel.mainSidebarModel;
const collapsed = mainSidebarModel.getCollapsed();
this.resizeStartWidth = mainSidebarModel.getWidth();
document.addEventListener("mousemove", this.onMouseMove);
document.addEventListener("mouseup", this.stopResizing);
document.body.style.cursor = "col-resize";
mobx.action(() => {
mainSidebarModel.setTempWidthAndTempCollapsed(this.resizeStartWidth, collapsed);
mainSidebarModel.isDragging.set(true);
})();
}
@boundMethod
onMouseMove(event: MouseEvent) {
event.preventDefault();
const { parentRef, enableSnap, position } = this.props;
const parentRect = parentRef.current?.getBoundingClientRect();
const mainSidebarModel = GlobalModel.mainSidebarModel;
if (!mainSidebarModel.isDragging.get() || !parentRect) return;
let delta: number, newWidth: number;
if (position === "right") {
delta = parentRect.right - event.clientX - this.startX;
} else {
delta = event.clientX - parentRect.left - this.startX;
}
newWidth = this.resizeStartWidth + delta;
if (enableSnap) {
const minWidth = MagicLayout.MainSidebarMinWidth;
const snapPoint = minWidth + MagicLayout.MainSidebarSnapThreshold;
const dragResistance = MagicLayout.MainSidebarDragResistance;
let dragDirection: string;
if (delta - this.prevDelta > 0) {
dragDirection = "+";
} else if (delta - this.prevDelta == 0) {
if (this.prevDragDirection == "+") {
dragDirection = "+";
} else {
dragDirection = "-";
}
} else {
dragDirection = "-";
}
this.prevDelta = delta;
this.prevDragDirection = dragDirection;
if (newWidth - dragResistance > minWidth && newWidth < snapPoint && dragDirection == "+") {
newWidth = snapPoint;
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
} else if (newWidth + dragResistance < snapPoint && dragDirection == "-") {
newWidth = minWidth;
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
} else if (newWidth > snapPoint) {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
}
} else {
if (newWidth <= MagicLayout.MainSidebarMinWidth) {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
} else {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
}
}
}
@boundMethod
stopResizing() {
let mainSidebarModel = GlobalModel.mainSidebarModel;
GlobalCommandRunner.clientSetSidebar(
mainSidebarModel.tempWidth.get(),
mainSidebarModel.tempCollapsed.get()
).finally(() => {
mobx.action(() => {
mainSidebarModel.isDragging.set(false);
})();
});
document.removeEventListener("mousemove", this.onMouseMove);
document.removeEventListener("mouseup", this.stopResizing);
document.body.style.cursor = "";
}
@boundMethod
toggleCollapsed() {
const mainSidebarModel = GlobalModel.mainSidebarModel;
const tempCollapsed = mainSidebarModel.getCollapsed();
const width = mainSidebarModel.getWidth(true);
mainSidebarModel.setTempWidthAndTempCollapsed(width, !tempCollapsed);
GlobalCommandRunner.clientSetSidebar(width, !tempCollapsed);
}
render() {
const { className, children } = this.props;
const mainSidebarModel = GlobalModel.mainSidebarModel;
const width = mainSidebarModel.getWidth();
const isCollapsed = mainSidebarModel.getCollapsed();
return (
<div className={cn("sidebar", className, { collapsed: isCollapsed })} style={{ width }}>
<div className="sidebar-content">{children(this.toggleCollapsed)}</div>
<div
className="sidebar-handle"
style={{
position: "absolute",
top: 0,
[this.props.position === "left" ? "right" : "left"]: 0,
bottom: 0,
width: "5px",
cursor: "col-resize",
}}
onMouseDown={this.startResizing}
onDoubleClick={this.toggleCollapsed}
></div>
</div>
);
}
}
export {
CmdStrCode,
Toggle,
@@ -1286,5 +1447,6 @@ export {
LinkButton,
Status,
Modal,
ResizableSidebar,
ShowWaveShellInstallPrompt,
};
+21 -1
View File
@@ -48,13 +48,33 @@
}
}
/*
The following accounts for a debounce in the status indicator. We will only display the status indicator icon if the parent indicates that it should be visible AND one of the following is true:
1. There is a status to be shown.
2. There is a spinner to be shown and the required delay has passed.
*/
.status-indicator-visible {
&.spinner-visible,
&.output,
&.error,
&.success {
.positional-icon-visible;
}
// This is set by the timeout in the status indicator component.
&.spinner-visible {
#spinner {
visibility: visible;
}
}
}
.status-indicator {
#spinner,
#indicator {
visibility: hidden;
}
.spin #spinner {
visibility: visible;
stroke: @term-white;
}
&.error #indicator {
+171 -10
View File
@@ -2,17 +2,27 @@ import React from "react";
import { StatusIndicatorLevel } from "../../../types/types";
import cn from "classnames";
import { ReactComponent as SpinnerIndicator } from "../../assets/icons/spinner-indicator.svg";
import { boundMethod } from "autobind-decorator";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import { ReactComponent as RotateIconSvg } from "../../assets/icons/line/rotate.svg";
interface PositionalIconProps {
children?: React.ReactNode;
className?: string;
onClick?: React.MouseEventHandler<HTMLDivElement>;
divRef?: React.RefObject<HTMLDivElement>;
}
export class FrontIcon extends React.Component<PositionalIconProps> {
render() {
return (
<div className={cn("front-icon", "positional-icon", this.props.className)}>
<div
ref={this.props.divRef}
className={cn("front-icon", "positional-icon", this.props.className)}
onClick={this.props.onClick}
>
<div className="positional-icon-inner">{this.props.children}</div>
</div>
);
@@ -22,7 +32,11 @@ export class FrontIcon extends React.Component<PositionalIconProps> {
export class CenteredIcon extends React.Component<PositionalIconProps> {
render() {
return (
<div className={cn("centered-icon", "positional-icon", this.props.className)} onClick={this.props.onClick}>
<div
ref={this.props.divRef}
className={cn("centered-icon", "positional-icon", this.props.className)}
onClick={this.props.onClick}
>
<div className="positional-icon-inner">{this.props.children}</div>
</div>
);
@@ -43,35 +57,182 @@ export class ActionsIcon extends React.Component<ActionsIconProps> {
}
}
class SyncSpin extends React.Component<{
classRef?: React.RefObject<HTMLDivElement>;
children?: React.ReactNode;
shouldSync?: boolean;
}> {
listenerAdded: boolean = false;
componentDidMount() {
this.syncSpinner();
}
componentDidUpdate() {
this.syncSpinner();
}
componentWillUnmount(): void {
const classRef = this.props.classRef;
if (classRef.current != null && this.listenerAdded) {
const elem = classRef.current;
const svgElem = elem.querySelector("svg");
if (svgElem != null) {
svgElem.removeEventListener("animationstart", this.handleAnimationStart);
}
}
}
@boundMethod
handleAnimationStart(e: AnimationEvent) {
const classRef = this.props.classRef;
if (classRef.current == null) {
return;
}
const svgElem = classRef.current.querySelector("svg");
if (svgElem == null) {
return;
}
const animArr = svgElem.getAnimations();
if (animArr == null || animArr.length == 0) {
return;
}
animArr[0].startTime = 0;
}
syncSpinner() {
const { classRef, shouldSync } = this.props;
const shouldSyncVal = shouldSync ?? true;
if (!shouldSyncVal || classRef.current == null) {
return;
}
const elem = classRef.current;
const svgElem = elem.querySelector("svg");
if (svgElem == null) {
return;
}
if (!this.listenerAdded) {
svgElem.addEventListener("animationstart", this.handleAnimationStart);
this.listenerAdded = true;
}
const animArr = svgElem.getAnimations();
if (animArr == null || animArr.length == 0) {
return;
}
animArr[0].startTime = 0;
}
render() {
return this.props.children;
}
}
interface StatusIndicatorProps {
/**
* The level of the status indicator. This will determine the color of the status indicator.
*/
level: StatusIndicatorLevel;
className?: string;
/**
* If true, a spinner will be shown around the status indicator.
*/
runningCommands?: boolean;
}
/**
* This component is used to show the status of a command. It will show a spinner around the status indicator if there are running commands. It will also delay showing the spinner for a short time to prevent flickering.
*/
@mobxReact.observer
export class StatusIndicator extends React.Component<StatusIndicatorProps> {
iconRef: React.RefObject<HTMLDivElement> = React.createRef();
spinnerVisible: mobx.IObservableValue<boolean> = mobx.observable.box(false);
timeout: NodeJS.Timeout;
clearSpinnerTimeout() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
mobx.action(() => {
this.spinnerVisible.set(false);
})();
}
/**
* This will apply a delay after there is a running command before showing the spinner. This prevents flickering for commands that return quickly.
*/
updateMountCallback() {
const runningCommands = this.props.runningCommands ?? false;
if (runningCommands && !this.timeout) {
this.timeout = setTimeout(
mobx.action(() => {
this.spinnerVisible.set(true);
}),
100
);
} else if (!runningCommands) {
this.clearSpinnerTimeout();
}
}
componentDidUpdate(): void {
this.updateMountCallback();
}
componentDidMount(): void {
this.updateMountCallback();
}
componentWillUnmount(): void {
this.clearSpinnerTimeout();
}
render() {
const { level, className, runningCommands } = this.props;
const spinnerVisible = this.spinnerVisible.get();
let statusIndicator = null;
if (level != StatusIndicatorLevel.None || runningCommands) {
let levelClass = null;
if (level != StatusIndicatorLevel.None || spinnerVisible) {
let indicatorLevelClass = null;
switch (level) {
case StatusIndicatorLevel.Output:
levelClass = "output";
indicatorLevelClass = "output";
break;
case StatusIndicatorLevel.Success:
levelClass = "success";
indicatorLevelClass = "success";
break;
case StatusIndicatorLevel.Error:
levelClass = "error";
indicatorLevelClass = "error";
break;
}
const spinnerVisibleClass = spinnerVisible ? "spinner-visible" : null;
statusIndicator = (
<CenteredIcon className={cn(className, levelClass, "status-indicator")}>
<SpinnerIndicator className={runningCommands ? "spin" : null} />
<CenteredIcon
divRef={this.iconRef}
className={cn(className, indicatorLevelClass, spinnerVisibleClass, "status-indicator")}
>
<SpinnerIndicator className={spinnerVisible ? "spin" : null} />
</CenteredIcon>
);
}
return statusIndicator;
return (
<SyncSpin classRef={this.iconRef} shouldSync={runningCommands}>
{statusIndicator}
</SyncSpin>
);
}
}
export class RotateIcon extends React.Component<{
className?: string;
onClick?: React.MouseEventHandler<HTMLDivElement>;
}> {
iconRef: React.RefObject<HTMLDivElement> = React.createRef();
render() {
return (
<SyncSpin classRef={this.iconRef}>
<RotateIconSvg className={this.props.className ?? ""} />
</SyncSpin>
);
}
}
@@ -20,6 +20,12 @@
width: 100%;
}
.import-edit-warning {
display: flex;
flex-direction: row;
align-items: flex-start;
}
.name-actions-section {
margin-bottom: 10px;
display: flex;
+30 -3
View File
@@ -58,6 +58,10 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
return this.selectedRemote?.local;
}
isImportedRemote(): boolean {
return this.selectedRemote?.sshconfigsrc == "sshconfig-import";
}
componentDidMount(): void {
mobx.action(() => {
this.tempAlias.set(this.selectedRemote?.remotealias);
@@ -259,6 +263,27 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
);
}
renderImportedRemoteEditWarning() {
return (
<div className="import-edit-warning">
<Tooltip
message={
<span>
Most options for connections imported from an ssh config file cannot be edited. For these
changes, you must edit the config file and import it again. The shell preference can be
edited, but will return to the default if you import again. It will stay changed if you
follow <a href="https://docs.waveterm.dev/features/sshconfig-imports">this procedure</a>.
</span>
}
icon={<i className="fa-sharp fa-regular fa-fw fa-triangle-exclamation" />}
>
<i className="fa-sharp fa-regular fa-fw fa-triangle-exclamation" />
</Tooltip>
&nbsp;SSH Config Import Behavior
</div>
);
}
renderAuthMode() {
let authMode = this.tempAuthMode.get();
return (
@@ -344,6 +369,7 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
return null;
}
let isLocal = this.isLocalRemote();
let isImported = this.isImportedRemote();
return (
<Modal className="erconn-modal">
<Modal.Header title="Edit Connection" onClose={this.model.closeModal} />
@@ -351,9 +377,10 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
<div className="name-actions-section">
<div className="name text-primary">{util.getRemoteName(this.selectedRemote)}</div>
</div>
<If condition={!isLocal}>{this.renderAlias()}</If>
<If condition={!isLocal}>{this.renderAuthMode()}</If>
<If condition={!isLocal}>{this.renderConnectMode()}</If>
<If condition={!isLocal && !isImported}>{this.renderAlias()}</If>
<If condition={!isLocal && !isImported}>{this.renderAuthMode()}</If>
<If condition={!isLocal && !isImported}>{this.renderConnectMode()}</If>
<If condition={isImported}>{this.renderImportedRemoteEditWarning()}</If>
{this.renderShellPref()}
<If condition={!util.isBlank(this.remoteEdit?.errorstr)}>
<div className="settings-field settings-error">Error: {this.remoteEdit?.errorstr}</div>
-11
View File
@@ -1026,17 +1026,6 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
cancelInstallButton = <></>;
}
if (remote.sshconfigsrc == "sshconfig-import") {
updateAuthButton = (
<Button theme="secondary" disabled={true}>
Edit
<Tooltip
message={`Connections imported from an ssh config file cannot be edited inside waveterm. To edit these, you must edit the config file and import it again.`}
icon={<i className="fa-sharp fa-regular fa-fw fa-ban" />}
>
<i className="fa-sharp fa-regular fa-fw fa-ban" />
</Tooltip>
</Button>
);
archiveButton = (
<Button theme="secondary" onClick={() => this.clickArchive()}>
Delete
@@ -209,17 +209,6 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
cancelInstallButton = <></>;
}
if (remote.sshconfigsrc == "sshconfig-import") {
updateAuthButton = (
<Button theme="secondary" disabled={true}>
Edit
<Tooltip
message={`Connections imported from an ssh config file cannot be edited inside waveterm. To edit these, you must edit the config file and import it again.`}
icon={<i className="fa-sharp fa-regular fa-fw fa-ban" />}
>
<i className="fa-sharp fa-regular fa-fw fa-ban" />
</Tooltip>
</Button>
);
archiveButton = (
<Button theme="secondary" onClick={() => this.clickArchive()}>
Delete
+81 -89
View File
@@ -13,8 +13,6 @@ import { GlobalModel, GlobalCommandRunner, Cmd, getTermPtyData } from "../../mod
import { termHeightFromRows } from "../../util/textmeasure";
import type {
LineType,
RemoteType,
RemotePtrType,
RenderModeType,
RendererOpts,
RendererPluginType,
@@ -24,11 +22,6 @@ import type {
} from "../../types/types";
import cn from "classnames";
import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg";
import { ReactComponent as PinIcon } from "../assets/icons/pin.svg";
import { ReactComponent as PlusIcon } from "../assets/icons/plus.svg";
import { ReactComponent as MinusIcon } from "../assets/icons/minus.svg";
import type { LineContainerModel } from "../../model/model";
import { renderCmdText } from "../common/common";
import { SimpleBlobRenderer } from "../../plugins/core/basicrenderer";
@@ -44,12 +37,13 @@ import * as appconst from "../appconst";
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
import { ReactComponent as CommentIcon } from "../assets/icons/line/comment.svg";
import { ReactComponent as QuestionIcon } from "../assets/icons/line/question.svg";
import { ReactComponent as RotateIcon } from "../assets/icons/line/rotate.svg";
import { ReactComponent as WarningIcon } from "../assets/icons/line/triangle-exclamation.svg";
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
import { ReactComponent as FillIcon } from "../assets/icons/line/fill.svg";
import { ReactComponent as GearIcon } from "../assets/icons/line/gear.svg";
import { RotateIcon } from "../common/icons/icons";
import "./lines.less";
dayjs.extend(localizedFormat);
@@ -59,12 +53,12 @@ type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class SmallLineAvatar extends React.Component<{ line: LineType; cmd: Cmd; onRightClick?: (e: any) => void }, {}> {
render() {
let { line, cmd } = this.props;
let lineNumStr = (line.linenumtemp ? "~" : "#") + String(line.linenum);
let status = cmd != null ? cmd.getStatus() : "done";
let rtnstate = cmd != null ? cmd.getRtnState() : false;
let exitcode = cmd != null ? cmd.getExitCode() : 0;
let isComment = line.linetype == "text";
const { line, cmd } = this.props;
const lineNumStr = (line.linenumtemp ? "~" : "#") + String(line.linenum);
const status = cmd != null ? cmd.getStatus() : "done";
const rtnstate = cmd != null ? cmd.getRtnState() : false;
const exitcode = cmd != null ? cmd.getExitCode() : 0;
const isComment = line.linetype == "text";
let icon = null;
let iconTitle = null;
if (isComment) {
@@ -142,7 +136,7 @@ class LineCmd extends React.Component<
}
checkStateDiffLoad(): void {
let { screen, line, staticRender, visible } = this.props;
const { screen, line, staticRender, visible } = this.props;
if (staticRender) {
return;
}
@@ -153,7 +147,7 @@ class LineCmd extends React.Component<
}
return;
}
let cmd = screen.getCmd(line);
const cmd = screen.getCmd(line);
if (cmd == null || !cmd.getRtnState() || this.rtnStateDiffFetched) {
return;
}
@@ -167,15 +161,15 @@ class LineCmd extends React.Component<
if (this.rtnStateDiffFetched) {
return;
}
let { line } = this.props;
const { line } = this.props;
this.rtnStateDiffFetched = true;
let usp = new URLSearchParams({
const usp = new URLSearchParams({
linenum: String(line.linenum),
screenid: line.screenid,
lineid: line.lineid,
});
let url = GlobalModel.getBaseHostPort() + "/api/rtnstate?" + usp.toString();
let fetchHeaders = GlobalModel.getFetchHeaders();
const url = GlobalModel.getBaseHostPort() + "/api/rtnstate?" + usp.toString();
const fetchHeaders = GlobalModel.getFetchHeaders();
fetch(url, { headers: fetchHeaders })
.then((resp) => {
if (!resp.ok) {
@@ -233,7 +227,7 @@ class LineCmd extends React.Component<
</React.Fragment>
);
}
let isMultiLine = lineutil.isMultiLineCmdText(cmd.getCmdStr());
const isMultiLine = lineutil.isMultiLineCmdText(cmd.getCmdStr());
return (
<div key="meta2" className="meta meta-line2" ref={this.cmdTextRef}>
<div className="metapart-mono cmdtext">
@@ -252,7 +246,7 @@ class LineCmd extends React.Component<
// TODO: this might not be necessary anymore because we're using this.lastHeight
getSnapshotBeforeUpdate(prevProps, prevState): { height: number } {
let elem = this.lineRef.current;
const elem = this.lineRef.current;
if (elem == null) {
return { height: 0 };
}
@@ -266,25 +260,25 @@ class LineCmd extends React.Component<
}
checkCmdText() {
let metaElem = this.cmdTextRef.current;
const metaElem = this.cmdTextRef.current;
if (metaElem == null || metaElem.childNodes.length == 0) {
return;
}
let metaElemWidth = metaElem.offsetWidth;
const metaElemWidth = metaElem.offsetWidth;
if (metaElemWidth == 0) {
return;
}
let metaChild = metaElem.firstChild;
const metaChild = metaElem.firstChild;
if (metaChild == null) {
return;
}
let children = metaChild.childNodes;
const children = metaChild.childNodes;
let childWidth = 0;
for (let i = 0; i < children.length; i++) {
let ch = children[i];
childWidth += ch.offsetWidth;
}
let isOverflow = childWidth > metaElemWidth;
const isOverflow = childWidth > metaElemWidth;
if (isOverflow && isOverflow != this.isOverflow.get()) {
mobx.action(() => {
this.isOverflow.set(isOverflow);
@@ -297,16 +291,16 @@ class LineCmd extends React.Component<
if (this.props.onHeightChange == null) {
return;
}
let { line } = this.props;
const { line } = this.props;
let curHeight = 0;
let elem = this.lineRef.current;
const elem = this.lineRef.current;
if (elem != null) {
curHeight = elem.offsetHeight;
}
if (this.lastHeight == curHeight) {
return;
}
let lastHeight = this.lastHeight;
const lastHeight = this.lastHeight;
this.lastHeight = curHeight;
this.props.onHeightChange(line.linenum, curHeight, lastHeight);
// console.log("line height change: ", line.linenum, lastHeight, "=>", curHeight);
@@ -314,13 +308,13 @@ class LineCmd extends React.Component<
@boundMethod
handleClick() {
let { line, noSelect } = this.props;
const { line, noSelect } = this.props;
if (noSelect) {
return;
}
let sel = window.getSelection();
const sel = window.getSelection();
if (this.lineRef.current != null) {
let selText = sel.toString();
const selText = sel.toString();
if (sel.anchorNode != null && this.lineRef.current.contains(sel.anchorNode) && !isBlank(selText)) {
return;
}
@@ -330,7 +324,7 @@ class LineCmd extends React.Component<
@boundMethod
clickStar() {
let { line } = this.props;
const { line } = this.props;
if (!line.star || line.star == 0) {
GlobalCommandRunner.lineStar(line.lineid, 1);
} else {
@@ -340,7 +334,7 @@ class LineCmd extends React.Component<
@boundMethod
clickPin() {
let { line } = this.props;
const { line } = this.props;
if (!line.pinned) {
GlobalCommandRunner.linePin(line.lineid, true);
} else {
@@ -350,19 +344,19 @@ class LineCmd extends React.Component<
@boundMethod
clickBookmark() {
let { line } = this.props;
const { line } = this.props;
GlobalCommandRunner.lineBookmark(line.lineid);
}
@boundMethod
clickDelete() {
let { line } = this.props;
const { line } = this.props;
GlobalCommandRunner.lineDelete(line.lineid, true);
}
@boundMethod
clickRestart() {
let { line } = this.props;
const { line } = this.props;
GlobalCommandRunner.lineRestart(line.lineid, true);
}
@@ -375,7 +369,7 @@ class LineCmd extends React.Component<
@boundMethod
clickMoveToSidebar() {
let { line } = this.props;
const { line } = this.props;
GlobalCommandRunner.screenSidebarAddLine(line.lineid);
}
@@ -390,20 +384,20 @@ class LineCmd extends React.Component<
}
getIsHidePrompt(): boolean {
let { line } = this.props;
const { line } = this.props;
let rendererPlugin: RendererPluginType = null;
let isNoneRenderer = line.renderer == "none";
const isNoneRenderer = line.renderer == "none";
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
}
let hidePrompt = rendererPlugin != null && rendererPlugin.hidePrompt;
const hidePrompt = rendererPlugin?.hidePrompt;
return hidePrompt;
}
getTerminalRendererHeight(cmd: Cmd): number {
let { screen, line, width, renderMode } = this.props;
const { screen, line, width } = this.props;
let height = 45 + 24; // height of zero height terminal
let usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
if (usedRows > 0) {
height = 48 + 24 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get());
}
@@ -412,7 +406,7 @@ class LineCmd extends React.Component<
@boundMethod
onAvatarRightClick(e: any): void {
let { line, noSelect } = this.props;
const { line, noSelect } = this.props;
if (noSelect) {
return;
}
@@ -426,20 +420,20 @@ class LineCmd extends React.Component<
}
renderSimple() {
let { screen, line } = this.props;
let cmd = screen.getCmd(line);
const { screen, line } = this.props;
const cmd = screen.getCmd(line);
let height: number = 0;
if (isBlank(line.renderer) || line.renderer == "terminal") {
height = this.getTerminalRendererHeight(cmd);
} else {
// header is 16px tall with hide-prompt, 36px otherwise
let { screen, line, width } = this.props;
let hidePrompt = this.getIsHidePrompt();
let usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
const { screen, line, width } = this.props;
const hidePrompt = this.getIsHidePrompt();
const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
height = (hidePrompt ? 16 + 6 : 36 + 6) + usedRows;
}
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
let mainDivCn = cn("line", "line-cmd", "line-simple");
const formattedTime = lineutil.getLineDateTimeStr(line.ts);
const mainDivCn = cn("line", "line-cmd", "line-simple");
return (
<div
className={mainDivCn}
@@ -479,15 +473,16 @@ class LineCmd extends React.Component<
if (restartTs != null && restartTs > 0) {
formattedTime = "restarted @ " + lineutil.getLineDateTimeStr(restartTs);
timeTitle = "original start time " + lineutil.getLineDateTimeStr(line.ts);
}
else {
} else {
formattedTime = lineutil.getLineDateTimeStr(line.ts);
}
let renderer = line.renderer;
return (
<div key="meta1" className="meta meta-line1">
<SmallLineAvatar line={line} cmd={cmd} />
<div title={timeTitle} className="ts">{formattedTime}</div>
<div title={timeTitle} className="ts">
{formattedTime}
</div>
<div>&nbsp;</div>
<If condition={!isBlank(renderer) && renderer != "terminal"}>
<div className="renderer">
@@ -506,7 +501,7 @@ class LineCmd extends React.Component<
}
getRendererOpts(cmd: Cmd): RendererOpts {
let { screen } = this.props;
const { screen } = this.props;
return {
maxSize: screen.getMaxContentSize(),
idealSize: screen.getIdealContentSize(),
@@ -516,9 +511,9 @@ class LineCmd extends React.Component<
}
makeRendererModelInitializeParams(): RendererModelInitializeParams {
let { screen, line } = this.props;
let context = lineutil.getRendererContext(line);
let cmd = screen.getCmd(line); // won't be null
const { screen, line } = this.props;
const context = lineutil.getRendererContext(line);
const cmd = screen.getCmd(line); // won't be null
let savedHeight = screen.getContentHeight(context);
if (savedHeight == null) {
if (line.contentheight != null && line.contentheight != -1) {
@@ -527,7 +522,7 @@ class LineCmd extends React.Component<
savedHeight = 0;
}
}
let api = {
const api = {
saveHeight: (height: number) => {
screen.setContentHeight(lineutil.getRendererContext(line), height);
},
@@ -581,12 +576,12 @@ class LineCmd extends React.Component<
};
render() {
let { screen, line, width, staticRender, visible } = this.props;
let isVisible = visible.get();
const { screen, line, width, staticRender, visible } = this.props;
const isVisible = visible.get();
if (staticRender || !isVisible) {
return this.renderSimple();
}
let cmd = screen.getCmd(line);
const cmd = screen.getCmd(line);
if (cmd == null) {
return (
<div
@@ -600,19 +595,17 @@ class LineCmd extends React.Component<
</div>
);
}
let status = cmd.getStatus();
let lineNumStr = (line.linenumtemp ? "~" : "") + String(line.linenum);
let isSelected = mobx
const isSelected = mobx
.computed(() => screen.getSelectedLine() == line.linenum, {
name: "computed-isSelected",
})
.get();
let isPhysicalFocused = mobx
const isPhysicalFocused = mobx
.computed(() => screen.getIsFocused(line.linenum), {
name: "computed-getIsFocused",
})
.get();
let isFocused = mobx
const isFocused = mobx
.computed(
() => {
let screenFocusType = screen.getFocusType();
@@ -621,7 +614,7 @@ class LineCmd extends React.Component<
{ name: "computed-isFocused" }
)
.get();
let shouldCmdFocus = mobx
const shouldCmdFocus = mobx
.computed(
() => {
let screenFocusType = screen.getFocusType();
@@ -630,7 +623,7 @@ class LineCmd extends React.Component<
{ name: "computed-shouldCmdFocus" }
)
.get();
let isInSidebar = mobx
const isInSidebar = mobx
.computed(
() => {
return screen.isSidebarOpen() && screen.isLineIdInSidebar(line.lineid);
@@ -638,11 +631,10 @@ class LineCmd extends React.Component<
{ name: "computed-isInSidebar" }
)
.get();
let isStatic = staticRender;
let isRunning = cmd.isRunning();
let isExpanded = this.isCmdExpanded.get();
let rsdiff = this.rtnStateDiff.get();
let mainDivCn = cn(
const isRunning = cmd.isRunning();
const isExpanded = this.isCmdExpanded.get();
const rsdiff = this.rtnStateDiff.get();
const mainDivCn = cn(
"line",
"line-cmd",
{ selected: isSelected },
@@ -651,18 +643,18 @@ class LineCmd extends React.Component<
{ "has-rtnstate": cmd.getRtnState() }
);
let rendererPlugin: RendererPluginType = null;
let isNoneRenderer = line.renderer == "none";
const isNoneRenderer = line.renderer == "none";
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
}
let rendererType = lineutil.getRendererType(line);
let hidePrompt = rendererPlugin != null && rendererPlugin.hidePrompt;
let termFontSize = GlobalModel.termFontSize.get();
const rendererType = lineutil.getRendererType(line);
const hidePrompt = rendererPlugin?.hidePrompt;
const termFontSize = GlobalModel.termFontSize.get();
let rtnStateDiffSize = termFontSize - 2;
if (rtnStateDiffSize < 10) {
rtnStateDiffSize = Math.max(termFontSize, 10);
}
let containerType = screen.getContainerType();
const containerType = screen.getContainerType();
return (
<div
className={mainDivCn}
@@ -681,7 +673,7 @@ class LineCmd extends React.Component<
<If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If>
</div>
<div key="restart" title="Restart Command" className="line-icon" onClick={this.clickRestart}>
<i className="fa-sharp fa-regular fa-arrows-rotate"/>
<i className="fa-sharp fa-regular fa-arrows-rotate" />
</div>
<div key="delete" title="Delete Line (&#x2318;D)" className="line-icon" onClick={this.clickDelete}>
<i className="fa-sharp fa-regular fa-trash" />
@@ -821,7 +813,7 @@ class Line extends React.Component<
{}
> {
render() {
let line = this.props.line;
const line = this.props.line;
if (line.archived) {
return null;
}
@@ -847,7 +839,7 @@ class LineText extends React.Component<
> {
@boundMethod
clickHandler() {
let { line, noSelect } = this.props;
const { line, noSelect } = this.props;
if (noSelect) {
return;
}
@@ -856,7 +848,7 @@ class LineText extends React.Component<
@boundMethod
onAvatarRightClick(e: any): void {
let { line, noSelect } = this.props;
const { line, noSelect } = this.props;
if (noSelect) {
return;
}
@@ -870,19 +862,19 @@ class LineText extends React.Component<
}
render() {
let { screen, line, renderMode } = this.props;
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
let isSelected = mobx
const { screen, line } = this.props;
const formattedTime = lineutil.getLineDateTimeStr(line.ts);
const isSelected = mobx
.computed(() => screen.getSelectedLine() == line.linenum, {
name: "computed-isSelected",
})
.get();
let isFocused = mobx
const isFocused = mobx
.computed(() => screen.getFocusType() == "cmd", {
name: "computed-isFocused",
})
.get();
let mainClass = cn("line", "line-text", "focus-parent");
const mainClass = cn("line", "line-text", "focus-parent");
return (
<div
className={mainClass}
+6
View File
@@ -27,6 +27,12 @@ let MagicLayout = {
ScreenSidebarWidthPadding: 5,
ScreenSidebarMinWidth: 200,
ScreenSidebarHeaderHeight: 28,
MainSidebarMinWidth: 75,
MainSidebarMaxWidth: 300,
MainSidebarSnapThreshold: 90,
MainSidebarDragResistance: 50,
MainSidebarDefaultWidth: 240,
};
let m = MagicLayout;
+23 -18
View File
@@ -3,14 +3,13 @@
.main-sidebar {
padding: 0;
min-width: 20rem;
max-width: 20rem;
display: flex;
flex-direction: column;
position: relative;
font-size: 12.5px;
line-height: 20px;
backdrop-filter: blur(4px);
z-index: 20;
.title-bar-drag {
-webkit-app-region: drag;
@@ -24,7 +23,6 @@
&.collapsed {
width: 6em;
min-width: 6em;
.arrow-container,
.collapse-button {
transform: rotate(180deg);
@@ -34,7 +32,7 @@
margin-top: 26px;
.top,
.workspaces-item,
.workspaces,
.middle,
.bottom,
.separator {
@@ -50,7 +48,7 @@
justify-content: center;
align-items: center;
.logo-container img {
.logo-container {
width: 45px;
}
@@ -86,10 +84,14 @@
display: flex;
flex-direction: row;
.logo-container {
flex-shrink: 0;
width: 100px;
}
.spacer {
flex-grow: 1;
}
img {
width: 100px;
}
@@ -127,9 +129,7 @@
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
}
.index {
font-size: 0.8em;
text-align: center;
width: 2em;
font-size: 10px;
}
.hotkey {
float: left !important;
@@ -152,7 +152,6 @@
margin-left: 6px;
border-radius: 4px;
opacity: 1;
transition: opacity 0.1s ease-in-out, visibility 0.1s step-end;
width: inherit;
max-width: inherit;
min-width: inherit;
@@ -179,6 +178,7 @@
float: right;
margin-right: 6px;
letter-spacing: 6px;
margin-left: auto;
}
&:hover {
:not(.disabled) .hotkey {
@@ -190,15 +190,20 @@
}
&:not(:hover) .status-indicator {
.positional-icon-visible;
.status-indicator-visible;
}
.add_workspace {
float: right;
padding: 2px;
margin-right: 6px;
transition: transform 0.3s ease-in-out;
svg {
fill: @base-color;
&.workspaces {
cursor: default;
.add-workspace {
cursor: pointer;
.positional-icon-visible;
float: right;
padding: 2px;
margin-right: 6px;
.fa-plus {
font-size: 13px;
}
}
}
+114 -98
View File
@@ -7,19 +7,19 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import dayjs from "dayjs";
import type { RemoteType } from "../../types/types";
import type { ClientDataType, RemoteType } from "../../types/types";
import { If } from "tsx-control-statements/components";
import { compareLoose } from "semver";
import { ReactComponent as LeftChevronIcon } from "../assets/icons/chevron_left.svg";
import { ReactComponent as AppsIcon } from "../assets/icons/apps.svg";
import { ReactComponent as WorkspacesIcon } from "../assets/icons/workspaces.svg";
import { ReactComponent as AddIcon } from "../assets/icons/add.svg";
import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model";
import { isBlank, openLink } from "../../util/util";
import { ResizableSidebar } from "../common/common";
import * as constants from "../appconst";
import "./sidebar.less";
@@ -27,8 +27,6 @@ import { ActionsIcon, CenteredIcon, FrontIcon, StatusIndicator } from "../common
dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
class SideBarItem extends React.Component<{
frontIcon: React.ReactNode;
contents: React.ReactNode | string;
@@ -60,16 +58,14 @@ class HotKeyIcon extends React.Component<{ hotkey: string }> {
}
}
@mobxReact.observer
class MainSideBar extends React.Component<{}, {}> {
collapsed: mobx.IObservableValue<boolean> = mobx.observable.box(false);
interface MainSideBarProps {
parentRef: React.RefObject<HTMLElement>;
clientData: ClientDataType;
}
@boundMethod
toggleCollapsed() {
mobx.action(() => {
this.collapsed.set(!this.collapsed.get());
})();
}
@mobxReact.observer
class MainSideBar extends React.Component<MainSideBarProps, {}> {
sidebarRef = React.createRef<HTMLDivElement>();
handleSessionClick(sessionId: string) {
GlobalCommandRunner.switchSession(sessionId);
@@ -176,9 +172,9 @@ class MainSideBar extends React.Component<{}, {}> {
getSessions() {
if (!GlobalModel.sessionListLoaded.get()) return <div className="item">loading ...</div>;
let sessionList = [];
let activeSessionId = GlobalModel.activeSessionId.get();
for (let session of GlobalModel.sessionList) {
const sessionList: Session[] = [];
const activeSessionId = GlobalModel.activeSessionId.get();
for (const session of GlobalModel.sessionList) {
if (!session.archived.get() || session.sessionId == activeSessionId) {
sessionList.push(session);
}
@@ -190,6 +186,7 @@ class MainSideBar extends React.Component<{}, {}> {
const sessionRunningCommands = sessionScreens.some((screen) => screen.numRunningCmds.get() > 0);
return (
<SideBarItem
key={session.sessionId}
className={`${isActive ? "active" : ""}`}
frontIcon={<span className="index">{index + 1}</span>}
contents={session.name.get()}
@@ -208,97 +205,116 @@ class MainSideBar extends React.Component<{}, {}> {
}
render() {
let isCollapsed = this.collapsed.get();
let clientData = GlobalModel.clientData.get();
let clientData = this.props.clientData;
let needsUpdate = false;
if (!clientData?.clientopts.noreleasecheck && !isBlank(clientData?.releaseinfo?.latestversion)) {
needsUpdate = compareLoose(VERSION, clientData.releaseinfo.latestversion) < 0;
}
let mainSidebar = GlobalModel.mainSidebarModel;
let isCollapsed = mainSidebar.getCollapsed();
return (
<div className={cn("main-sidebar", { collapsed: isCollapsed }, { "is-dev": GlobalModel.isDev })}>
<div className="title-bar-drag" />
<div className="contents">
<div className="logo">
<If condition={isCollapsed}>
<div className="logo-container" onClick={this.toggleCollapsed}>
<img src="public/logos/wave-logo.png" />
<ResizableSidebar
className="main-sidebar"
position="left"
enableSnap={true}
parentRef={this.props.parentRef}
>
{(toggleCollapse) => (
<React.Fragment>
<div className="title-bar-drag" />
<div className="contents">
<div className="logo">
<If condition={isCollapsed}>
<div className="logo-container" onClick={toggleCollapse}>
<img src="public/logos/wave-logo.png" />
</div>
</If>
<If condition={!isCollapsed}>
<div className="logo-container">
<img src="public/logos/wave-dark.png" />
</div>
<div className="spacer" />
<div className="collapse-button" onClick={toggleCollapse}>
<LeftChevronIcon className="icon" />
</div>
</If>
</div>
</If>
<If condition={!isCollapsed}>
<div className="logo-container">
<img src="public/logos/wave-dark.png" />
<div className="separator" />
<div className="top">
<SideBarItem
key="history"
frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />}
contents="History"
endIcons={[<HotKeyIcon key="hotkey" hotkey="H" />]}
onClick={this.handleHistoryClick}
/>
{/* <SideBarItem className="hoverEffect unselectable" frontIcon={<FavoritesIcon className="icon" />} contents="Favorites" endIcon={<span className="hotkey">&#x2318;B</span>} onClick={this.handleBookmarksClick}/> */}
<SideBarItem
key="connections"
frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
contents="Connections"
onClick={this.handleConnectionsClick}
/>
</div>
<div className="spacer" />
<div className="collapse-button" onClick={this.toggleCollapsed}>
<LeftChevronIcon className="icon" />
</div>
</If>
</div>
<div className="separator" />
<div className="top">
<SideBarItem
frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />}
contents="History"
endIcons={[<HotKeyIcon key="hotkey" hotkey="H" />]}
onClick={this.handleHistoryClick}
/>
{/* <SideBarItem className="hoverEffect unselectable" frontIcon={<FavoritesIcon className="icon" />} contents="Favorites" endIcon={<span className="hotkey">&#x2318;B</span>} onClick={this.handleBookmarksClick}/> */}
<SideBarItem
frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
contents="Connections"
onClick={this.handleConnectionsClick}
/>
</div>
<div className="separator" />
<SideBarItem
frontIcon={<WorkspacesIcon className="icon" />}
contents="Workspaces"
endIcons={[
<div
key="add_workspace"
className="add_workspace hoverEffect"
onClick={this.handleNewSession}
>
<AddIcon />
</div>,
]}
/>
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
<div className="bottom">
<If condition={needsUpdate}>
<div className="separator" />
<SideBarItem
className="updateBanner"
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
contents="Update Available"
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
key="workspaces"
className="workspaces"
frontIcon={<WorkspacesIcon className="icon" />}
contents="Workspaces"
endIcons={[
<CenteredIcon
key="add-workspace"
className="add-workspace hoverEffect"
onClick={this.handleNewSession}
>
<i className="fa-sharp fa-solid fa-plus"></i>
</CenteredIcon>,
]}
/>
</If>
<If condition={GlobalModel.isDev}>
<SideBarItem
frontIcon={<AppsIcon className="icon" />}
contents="Apps"
onClick={this.handlePluginsClick}
endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
/>
</If>
<SideBarItem
frontIcon={<SettingsIcon className="icon" />}
contents="Settings"
onClick={this.handleSettingsClick}
/>
<SideBarItem
frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />}
contents="Documentation"
onClick={() => openLink("https://docs.waveterm.dev")}
/>
<SideBarItem
frontIcon={<i className="fa-brands fa-discord icon" />}
contents="Discord"
onClick={() => openLink("https://discord.gg/XfvZ334gwU")}
/>
</div>
</div>
</div>
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
<div className="bottom">
<If condition={needsUpdate}>
<SideBarItem
key="update-available"
className="updateBanner"
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
contents="Update Available"
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
/>
</If>
<If condition={GlobalModel.isDev}>
<SideBarItem
key="apps"
frontIcon={<AppsIcon className="icon" />}
contents="Apps"
onClick={this.handlePluginsClick}
endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
/>
</If>
<SideBarItem
key="settings"
frontIcon={<SettingsIcon className="icon" />}
contents="Settings"
onClick={this.handleSettingsClick}
/>
<SideBarItem
key="documentation"
frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />}
contents="Documentation"
onClick={() => openLink("https://docs.waveterm.dev")}
/>
<SideBarItem
key="discord"
frontIcon={<i className="fa-brands fa-discord icon" />}
contents="Discord"
onClick={() => openLink("https://discord.gg/XfvZ334gwU")}
/>
</div>
</div>
</React.Fragment>
)}
</ResizableSidebar>
);
}
}
+29 -28
View File
@@ -5,28 +5,25 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If, Choose, When, Otherwise } from "tsx-control-statements/components";
import { If } from "tsx-control-statements/components";
import cn from "classnames";
import dayjs from "dayjs";
import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Screen, ScreenLines } from "../../../model/model";
import { renderCmdText, Button } from "../../common/common";
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
import { renderCmdText } from "../../common/common";
import { TextAreaInput } from "./textareainput";
import { InfoMsg } from "./infomsg";
import { HistoryInfo } from "./historyinfo";
import { Prompt } from "../../common/prompt/prompt";
import { ReactComponent as ExecIcon } from "../../assets/icons/exec.svg";
import { ReactComponent as RotateIcon } from "../../assets/icons/line/rotate.svg";
import "./cmdinput.less";
import { RotateIcon } from "../../common/icons/icons";
import { AIChat } from "./aichat";
import "./cmdinput.less";
dayjs.extend(localizedFormat);
const TDots = "â‹®";
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer
class CmdInput extends React.Component<{}, {}> {
cmdInputRef: React.RefObject<any> = React.createRef();
@@ -37,11 +34,11 @@ class CmdInput extends React.Component<{}, {}> {
}
updateCmdInputHeight() {
let elem = this.cmdInputRef.current;
const elem = this.cmdInputRef.current;
if (elem == null) {
return;
}
let height = elem.offsetHeight;
const height = elem.offsetHeight;
if (height == GlobalModel.inputModel.cmdInputHeight) {
return;
}
@@ -50,7 +47,7 @@ class CmdInput extends React.Component<{}, {}> {
})();
}
componentDidUpdate(prevProps, prevState, snapshot: {}): void {
componentDidUpdate(): void {
this.updateCmdInputHeight();
}
@@ -87,7 +84,7 @@ class CmdInput extends React.Component<{}, {}> {
e.preventDefault();
e.stopPropagation();
let inputModel = GlobalModel.inputModel;
const inputModel = GlobalModel.inputModel;
if (inputModel.historyShow.get()) {
inputModel.resetHistory();
} else {
@@ -113,9 +110,9 @@ class CmdInput extends React.Component<{}, {}> {
}
render() {
let model = GlobalModel;
let inputModel = model.inputModel;
let screen = GlobalModel.getActiveScreen();
const model = GlobalModel;
const inputModel = model.inputModel;
const screen = GlobalModel.getActiveScreen();
let ri: RemoteInstanceType = null;
let rptr: RemotePtrType = null;
if (screen != null) {
@@ -129,15 +126,13 @@ class CmdInput extends React.Component<{}, {}> {
feState = ri.festate;
}
feState = feState || {};
let infoShow = inputModel.infoShow.get();
let historyShow = !infoShow && inputModel.historyShow.get();
let aiChatShow = inputModel.aIChatShow.get();
let infoMsg = inputModel.infoMsg.get();
let hasInfo = infoMsg != null;
let focusVal = inputModel.physicalInputFocused.get();
let inputMode: string = inputModel.inputMode.get();
let textAreaInputKey = screen == null ? "null" : screen.screenId;
let win = GlobalModel.getScreenLinesById(screen.screenId);
const infoShow = inputModel.infoShow.get();
const historyShow = !infoShow && inputModel.historyShow.get();
const aiChatShow = inputModel.aIChatShow.get();
const focusVal = inputModel.physicalInputFocused.get();
const inputMode: string = inputModel.inputMode.get();
const textAreaInputKey = screen == null ? "null" : screen.screenId;
const win = GlobalModel.getScreenLinesById(screen.screenId);
let numRunningLines = 0;
if (win != null) {
numRunningLines = mobx.computed(() => win.getRunningCmdLines().length).get();
@@ -232,11 +227,17 @@ class CmdInput extends React.Component<{}, {}> {
{focusVal && (
<div className="cmd-btn hoverEffect">
<If condition={historyShow}>
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>close (esc)</div>
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>
close (esc)
</div>
</If>
<If condition={!historyShow}>
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>history (ctrl-r)</div>
<div className="hint-elem" onMouseDown={this.clickAIHint}>AI (ctrl-space)</div>
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>
history (ctrl-r)
</div>
<div className="hint-elem" onMouseDown={this.clickAIHint}>
AI (ctrl-space)
</div>
</If>
</div>
)}
+1 -1
View File
@@ -287,7 +287,7 @@
}
&:not(:hover) .status-indicator {
.positional-icon-visible;
.status-indicator-visible;
}
&:hover {
+2 -5
View File
@@ -8,12 +8,12 @@
&.is-hidden {
display: none;
}
max-width: calc(100% - 20.5em);
background: @background-session;
border: 1px solid @base-border;
border-radius: 8px;
transition: width 0.2s ease;
// transition: width 0.2s ease;
margin-bottom: 0.5em;
margin-right: 0.5em;
.center-message {
display: flex;
@@ -24,6 +24,3 @@
color: @text-secondary;
}
}
.collapsed + .session-view {
max-width: calc(100% - 6.7em);
}
+17 -4
View File
@@ -27,20 +27,33 @@ class WorkspaceView extends React.Component<{}, {}> {
if (session == null) {
return (
<div className="session-view">
<div className="center-message"><div>(no active workspace)</div></div>
<div className="center-message">
<div>(no active workspace)</div>
</div>
</div>
);
}
let activeScreen = session.getActiveScreen();
let cmdInputHeight = model.inputModel.cmdInputHeight.get();
if (cmdInputHeight == 0) {
cmdInputHeight = MagicLayout.CmdInputHeight; // this is the base size of cmdInput (measured using devtools)
cmdInputHeight = MagicLayout.CmdInputHeight; // this is the base size of cmdInput (measured using devtools)
}
cmdInputHeight += MagicLayout.CmdInputBottom; // reference to .cmd-input, bottom: 12px
cmdInputHeight += MagicLayout.CmdInputBottom; // reference to .cmd-input, bottom: 12px
let isHidden = GlobalModel.activeMainView.get() != "session";
let mainSidebarModel = GlobalModel.mainSidebarModel;
// Has to calc manually because when tabs overflow, the width of the session view is increased for some reason causing inconsistent width.
// 6px is the right margin of session view.
let width = window.innerWidth - 6 - mainSidebarModel.getWidth();
return (
<div className={cn("session-view", { "is-hidden": isHidden })} data-sessionid={session.sessionId}>
<div
className={cn("session-view", { "is-hidden": isHidden })}
data-sessionid={session.sessionId}
style={{
width: `${width}px`,
}}
>
<ScreenTabs key={"tabs-" + session.sessionId} session={session} />
<ErrorBoundary>
<ScreenView key={"screenview-" + session.sessionId} session={session} screen={activeScreen} />
+86 -23
View File
@@ -7,6 +7,7 @@ import { sprintf } from "sprintf-js";
import { v4 as uuidv4 } from "uuid";
import { boundMethod } from "autobind-decorator";
import { debounce } from "throttle-debounce";
import * as mobxReact from "mobx-react";
import {
handleJsonFetchResponse,
base64ToString,
@@ -50,9 +51,6 @@ import {
HistoryViewDataType,
AlertMessageType,
HistorySearchParams,
UserInputRequest,
UserInputResponse,
UserInputResponsePacket,
FocusTypeStrs,
ScreenLinesType,
HistoryTypeStrs,
@@ -2620,6 +2618,77 @@ class ClientSettingsViewModel {
})();
}
}
class MainSidebarModel {
tempWidth: OV<number> = mobx.observable.box(null, {
name: "MainSidebarModel-tempWidth",
});
tempCollapsed: OV<boolean> = mobx.observable.box(null, {
name: "MainSidebarModel-tempCollapsed",
});
isDragging: OV<boolean> = mobx.observable.box(false, {
name: "MainSidebarModel-isDragging",
});
setTempWidthAndTempCollapsed(newWidth: number, newCollapsed: boolean): void {
const width = Math.max(MagicLayout.MainSidebarMinWidth, Math.min(newWidth, MagicLayout.MainSidebarMaxWidth));
mobx.action(() => {
this.tempWidth.set(width);
this.tempCollapsed.set(newCollapsed);
})();
}
/**
* Gets the intended width for the sidebar. If the sidebar is being dragged, returns the tempWidth. If the sidebar is collapsed, returns the default width.
* @param ignoreCollapse If true, returns the persisted width even if the sidebar is collapsed.
* @returns The intended width for the sidebar or the default width if the sidebar is collapsed. Can be overridden using ignoreCollapse.
*/
getWidth(ignoreCollapse: boolean = false): number {
const clientData = GlobalModel.clientData.get();
let width = clientData?.clientopts?.mainsidebar?.width ?? MagicLayout.MainSidebarDefaultWidth;
if (this.isDragging.get()) {
if (this.tempWidth.get() == null && width == null) {
return MagicLayout.MainSidebarDefaultWidth;
}
if (this.tempWidth.get() == null) {
return width;
}
return this.tempWidth.get();
}
// Set by CLI and collapsed
if (this.getCollapsed()) {
if (ignoreCollapse) {
return width;
} else {
return MagicLayout.MainSidebarMinWidth;
}
} else {
if (width <= MagicLayout.MainSidebarMinWidth) {
width = MagicLayout.MainSidebarDefaultWidth;
}
const snapPoint = MagicLayout.MainSidebarMinWidth + MagicLayout.MainSidebarSnapThreshold;
if (width < snapPoint || width > MagicLayout.MainSidebarMaxWidth) {
width = MagicLayout.MainSidebarDefaultWidth;
}
}
return width;
}
getCollapsed(): boolean {
const clientData = GlobalModel.clientData.get();
const collapsed = clientData?.clientopts?.mainsidebar?.collapsed;
if (this.isDragging.get()) {
if (this.tempCollapsed.get() == null && collapsed == null) {
return false;
}
if (this.tempCollapsed.get() == null) {
return collapsed;
}
return this.tempCollapsed.get();
}
return collapsed;
}
}
class BookmarksModel {
bookmarks: OArr<BookmarkType> = mobx.observable.array([], {
@@ -3390,6 +3459,7 @@ class Model {
connectionViewModel: ConnectionsViewModel;
clientSettingsViewModel: ClientSettingsViewModel;
modalsModel: ModalsModel;
mainSidebarModel: MainSidebarModel;
clientData: OV<ClientDataType> = mobx.observable.box(null, {
name: "clientData",
});
@@ -3416,6 +3486,7 @@ class Model {
this.remotesModalModel = new RemotesModalModel();
this.remotesModel = new RemotesModel();
this.modalsModel = new ModalsModel();
this.mainSidebarModel = new MainSidebarModel();
let isWaveSrvRunning = getApi().getWaveSrvStatus();
this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, {
name: "model-wavesrv-running",
@@ -4103,28 +4174,15 @@ class Model {
if ("openaicmdinfochat" in update) {
this.inputModel.setOpenAICmdInfoChat(update.openaicmdinfochat);
}
if ("screenstatusindicator" in update) {
this.getScreenById_single(update.screenstatusindicator.screenid)?.setStatusIndicator(
update.screenstatusindicator.status
);
if ("screenstatusindicators" in update) {
for (const indicator of update.screenstatusindicators) {
this.getScreenById_single(indicator.screenid)?.setStatusIndicator(indicator.status);
}
}
if ("screennumrunningcommands" in update) {
this.getScreenById_single(update.screennumrunningcommands.screenid)?.setNumRunningCmds(
update.screennumrunningcommands.num
);
}
if ("userinputrequest" in update) {
let userInputRequest: UserInputRequest = update.userinputrequest;
let userInputResponse: UserInputResponse = {
type: "text",
text: "what wonderful weather we're having",
};
let userInputResponsePacket: UserInputResponsePacket = {
type: "userinputresp",
requestid: userInputRequest.requestid,
response: userInputResponse,
};
this.ws.pushMessage(userInputResponsePacket);
for (const snc of update.screennumrunningcommands) {
this.getScreenById_single(snc.screenid)?.setNumRunningCmds(snc.num);
}
}
}
@@ -4996,6 +5054,11 @@ class CommandRunner {
return GlobalModel.submitCommand("client", "setconfirmflag", [flag, valueStr], kwargs, false);
}
clientSetSidebar(width: number, collapsed: boolean): Promise<CommandRtnType> {
let kwargs = { nohist: "1", width: `${width}`, collapsed: collapsed ? "1" : "0" };
return GlobalModel.submitCommand("client", "setsidebar", null, kwargs, false);
}
editBookmark(bookmarkId: string, desc: string, cmdstr: string) {
let kwargs = {
nohist: "1",
+6 -24
View File
@@ -326,9 +326,8 @@ type ModelUpdateType = {
remoteview?: RemoteViewType;
openaicmdinfochat?: OpenAICmdInfoChatMessageType[];
alertmessage?: AlertMessageType;
screenstatusindicator?: ScreenStatusIndicatorUpdateType;
screennumrunningcommands?: ScreenNumRunningCommandsUpdateType;
userinputrequest?: UserInputRequest;
screenstatusindicators?: ScreenStatusIndicatorUpdateType[];
screennumrunningcommands?: ScreenNumRunningCommandsUpdateType[];
};
type HistoryViewDataType = {
@@ -525,6 +524,10 @@ type ClientOptsType = {
noreleasecheck: boolean;
acceptedtos: number;
confirmflags: ConfirmFlagsType;
mainsidebar: {
collapsed: boolean;
width: number;
};
};
type ReleaseInfoType = {
@@ -587,24 +590,6 @@ type HistorySearchParams = {
filterCmds?: boolean;
};
type UserInputRequest = {
requestid: string;
querytext: string;
responsetype: string;
};
type UserInputResponse = {
type: string;
text?: string;
confirm?: boolean;
};
type UserInputResponsePacket = {
type: string;
requestid: string;
response: UserInputResponse;
};
type RenderModeType = "normal" | "collapsed" | "expanded";
type WebScreen = {
@@ -790,9 +775,6 @@ export type {
RenderModeType,
AlertMessageType,
HistorySearchParams,
UserInputRequest,
UserInputResponse,
UserInputResponsePacket,
ScreenLinesType,
FocusTypeStrs,
HistoryTypeStrs,
+70 -4
View File
@@ -92,6 +92,7 @@ var TabIcons = []string{"square", "sparkle", "fire", "ghost", "cloud", "compass"
var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"}
var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}
var ConfirmFlags = []string{"hideshellprompt"}
var SidebarNames = []string{"main"}
var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"}
var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"}
@@ -218,6 +219,7 @@ func init() {
registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand)
registerCmdFn("client:accepttos", ClientAcceptTosCommand)
registerCmdFn("client:setconfirmflag", ClientConfirmFlagCommand)
registerCmdFn("client:setsidebar", ClientSetSidebarCommand)
registerCmdFn("sidebar:open", SidebarOpenCommand)
registerCmdFn("sidebar:close", SidebarCloseCommand)
@@ -1418,9 +1420,6 @@ func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss
if err != nil {
return nil, err
}
if ids.Remote.RState.SSHConfigSrc == sstore.SSHConfigSrcTypeImport {
return nil, fmt.Errorf("/remote:new cannot update imported remote")
}
visualEdit := resolveBool(pk.Kwargs["visual"], false)
isSubmitted := resolveBool(pk.Kwargs["submit"], false)
editArgs, err := parseRemoteEditArgs(false, pk, ids.Remote.MShell.IsLocal())
@@ -1540,6 +1539,7 @@ type HostInfoType struct {
SshKeyFile string
ConnectMode string
Ignore bool
ShellPref string
}
func createSshImportSummary(changeList map[string][]string) string {
@@ -1635,6 +1635,13 @@ func NewHostInfo(hostName string) (*HostInfoType, error) {
connectMode = sstore.ConnectModeManual
}
shellPref := sstore.ShellTypePref_Detect
if cfgWaveOptions["shellpref"] == "bash" {
shellPref = "bash"
} else if cfgWaveOptions["shellpref"] == "zsh" {
shellPref = "zsh"
}
outHostInfo := new(HostInfoType)
outHostInfo.Host = hostName
outHostInfo.User = userName
@@ -1643,6 +1650,7 @@ func NewHostInfo(hostName string) (*HostInfoType, error) {
outHostInfo.SshKeyFile = sshKeyFile
outHostInfo.ConnectMode = connectMode
outHostInfo.Ignore = shouldIgnore
outHostInfo.ShellPref = shellPref
return outHostInfo, nil
}
@@ -1707,6 +1715,7 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
if hostInfo.SshKeyFile != "" {
editMap[sstore.RemoteField_SSHKey] = hostInfo.SshKeyFile
}
editMap[sstore.RemoteField_ShellPref] = hostInfo.ShellPref
msh := remote.GetRemoteById(previouslyImportedRemote.RemoteId)
if msh == nil {
remoteChangeList["updateErr"] = append(remoteChangeList["updateErr"], hostInfo.CanonicalName)
@@ -1714,7 +1723,7 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
continue
}
if msh.Remote.ConnectMode == hostInfo.ConnectMode && msh.Remote.SSHOpts.SSHIdentity == hostInfo.SshKeyFile && msh.Remote.RemoteAlias == hostInfo.Host {
if msh.Remote.ConnectMode == hostInfo.ConnectMode && msh.Remote.SSHOpts.SSHIdentity == hostInfo.SshKeyFile && msh.Remote.RemoteAlias == hostInfo.Host && msh.Remote.ShellPref == hostInfo.ShellPref {
// silently skip this one. it didn't fail, but no changes were needed
continue
}
@@ -1751,6 +1760,7 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
AutoInstall: true,
SSHOpts: sshOpts,
SSHConfigSrc: sstore.SSHConfigSrcTypeImport,
ShellPref: sstore.ShellTypePref_Detect,
}
err := remote.AddRemote(ctx, r, false)
if err != nil {
@@ -4463,6 +4473,62 @@ func ClientConfirmFlagCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
return update, nil
}
func ClientSetSidebarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
clientData, err := sstore.EnsureClientData(ctx)
if err != nil {
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
}
// Handle collapsed
collapsed, ok := pk.Kwargs["collapsed"]
if !ok {
return nil, fmt.Errorf("collapsed key not provided")
}
collapsedValue := resolveBool(collapsed, false)
// Handle width
var width int
if w, exists := pk.Kwargs["width"]; exists {
width, err = resolveNonNegInt(w, 0)
if err != nil {
return nil, fmt.Errorf("error resolving width: %v", err)
}
} else if clientData.ClientOpts.MainSidebar != nil {
width = clientData.ClientOpts.MainSidebar.Width
}
// Initialize SidebarCollapsed if it's nil
if clientData.ClientOpts.MainSidebar == nil {
clientData.ClientOpts.MainSidebar = new(sstore.SidebarValueType)
}
// Set the sidebar values
var sv sstore.SidebarValueType
sv.Collapsed = collapsedValue
if width != 0 {
sv.Width = width
}
clientData.ClientOpts.MainSidebar = &sv
// Update client data
err = sstore.SetClientOpts(ctx, clientData.ClientOpts)
if err != nil {
return nil, fmt.Errorf("error updating client data: %v", err)
}
// Retrieve updated client data
clientData, err = sstore.EnsureClientData(ctx)
if err != nil {
return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)
}
update := &sstore.ModelUpdate{
ClientData: clientData,
}
return update, nil
}
func validateOpenAIAPIToken(key string) error {
if len(key) > MaxOpenAIAPITokenLen {
return fmt.Errorf("invalid openai token, too long")

Some files were not shown because too many files have changed in this diff Show More