Compare commits

..

1 Commits

Author SHA1 Message Date
Evan Simkowitz 4d95d45a45 save 2024-04-05 10:46:07 -07:00
52 changed files with 773 additions and 2160 deletions
+1 -3
View File
@@ -23,7 +23,6 @@
"@table-nav/react": "^0.0.7",
"@tanstack/match-sorter-utils": "^8.8.4",
"@tanstack/react-table": "^8.10.3",
"@withfig/autocomplete": "^2.652.3",
"autobind-decorator": "^2.4.0",
"base64-js": "^1.5.1",
"classnames": "^2.3.1",
@@ -78,7 +77,6 @@
"@types/throttle-debounce": "^5.0.1",
"@types/uuid": "^9.0.7",
"@types/webpack-env": "^1.18.3",
"@withfig/autocomplete-types": "^1.30.0",
"babel-loader": "^9.1.3",
"babel-plugin-jsx-control-statements": "^4.1.2",
"copy-webpack-plugin": "^12.0.0",
@@ -107,4 +105,4 @@
"scripts": {
"postinstall": "electron-builder install-app-deps"
}
}
}
-4
View File
@@ -64,7 +64,3 @@ export enum StatusIndicatorLevel {
// matches packet.go
export const ErrorCode_InvalidCwd = "ERRCWD";
export const InputAuxView_History = "history";
export const InputAuxView_Info = "info";
export const InputAuxView_AIChat = "aichat";
+2 -2
View File
@@ -7,13 +7,13 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, RemotesModel, getApi } from "@/models";
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown, Button } from "@/common/elements";
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "@/common/elements";
import { commandRtnHandler, isBlank } from "@/util/util";
import { getTermThemes } from "@/util/themeutil";
import * as appconst from "@/app/appconst";
import "./clientsettings.less";
import { MainView } from "@/common/elements/mainview";
import { MainView } from "../common/elements/mainview";
class ClientSettingsKeybindings extends React.Component<{}, {}> {
componentDidMount() {
+4 -6
View File
@@ -6,7 +6,7 @@ import "./button.less";
interface ButtonProps {
children: React.ReactNode;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onClick?: () => void;
disabled?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
@@ -14,7 +14,6 @@ interface ButtonProps {
autoFocus?: boolean;
className?: string;
termInline?: boolean;
title?: string;
}
class Button extends React.Component<ButtonProps> {
@@ -24,14 +23,14 @@ class Button extends React.Component<ButtonProps> {
};
@boundMethod
handleClick(e) {
handleClick() {
if (this.props.onClick && !this.props.disabled) {
this.props.onClick(e);
this.props.onClick();
}
}
render() {
const { leftIcon, rightIcon, children, disabled, style, autoFocus, termInline, className, title } = this.props;
const { leftIcon, rightIcon, children, disabled, style, autoFocus, termInline, className } = this.props;
return (
<button
@@ -40,7 +39,6 @@ class Button extends React.Component<ButtonProps> {
disabled={disabled}
style={style}
autoFocus={autoFocus}
title={title}
>
{leftIcon && <span className="icon-left">{leftIcon}</span>}
{children}
-7
View File
@@ -1,7 +0,0 @@
.copy-button {
padding: 5px 5px;
.fa-check {
color: var(--app-success-color);
}
}
-51
View File
@@ -1,51 +0,0 @@
import * as React from "react";
import { Button } from "./button";
import { boundMethod } from "autobind-decorator";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import "./copybutton.less";
type CopyButtonProps = {
title: string;
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
};
@mobxReact.observer
class CopyButton extends React.Component<CopyButtonProps, {}> {
isCopied: OV<boolean> = mobx.observable.box(false, { name: "isCopied" });
@boundMethod
handleOnClick(e: React.MouseEvent<HTMLButtonElement>) {
if (this.isCopied.get()) {
return;
}
mobx.action(() => {
this.isCopied.set(true);
})();
setTimeout(() => {
mobx.action(() => {
this.isCopied.set(false);
})();
}, 2000);
if (this.props.onClick) {
this.props.onClick(e);
}
}
render() {
const { title, onClick } = this.props;
const isCopied = this.isCopied.get();
return (
<Button onClick={this.handleOnClick} className="copy-button secondary ghost" title={title}>
{isCopied ? (
<i className="fa-sharp fa-solid fa-check"></i>
) : (
<i className="fa-sharp fa-solid fa-copy"></i>
)}
</Button>
);
}
}
export { CopyButton };
-1
View File
@@ -18,4 +18,3 @@ export { Toggle } from "./toggle";
export { Tooltip } from "./tooltip";
export { TabIcon } from "./tabicon";
export { DatePicker } from "./datepicker";
export { CopyButton } from "./copybutton";
+16
View File
@@ -0,0 +1,16 @@
import { OverlayScrollbarsComponentProps, useOverlayScrollbars } from "overlayscrollbars-react";
import React from "react";
export const ScrollbarsComponent = (props: {
options?: OverlayScrollbarsComponentProps["options"];
children: React.ReactNode;
childrenRef: React.RefObject<any>;
}) => {
const [initialize, instance] = useOverlayScrollbars({ options: props.options });
React.useEffect(() => {
initialize(props.childrenRef.current);
}, [initialize, props.childrenRef.current]);
return <div ref={props.childrenRef}>{props.children}</div>;
};
+1 -1
View File
@@ -312,7 +312,7 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
endDecoration: (
<InputDecoration>
<Tooltip
message={`(Required) The path to your ssh private key file.`}
message={`(Required) The path to your ssh key file.`}
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
>
<i className="fa-sharp fa-regular fa-circle-question" />
+1 -1
View File
@@ -338,7 +338,7 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
endDecoration: (
<InputDecoration>
<Tooltip
message={`(Required) The path to your ssh private key file.`}
message={`(Required) The path to your ssh key file.`}
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
>
<i className="fa-sharp fa-regular fa-circle-question" />
-4
View File
@@ -357,10 +357,6 @@
cursor: pointer;
}
.wave-button {
padding: 5px 5px;
}
visibility: hidden;
}
+27 -7
View File
@@ -14,7 +14,7 @@ import localizedFormat from "dayjs/plugin/localizedFormat";
import customParseFormat from "dayjs/plugin/customParseFormat";
import { Line } from "@/app/line/linecomps";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
import { TextField, Dropdown, Button, CopyButton } from "@/elements";
import { TextField, Dropdown, Button, DatePicker } from "@/elements";
import { ReactComponent as ChevronLeftIcon } from "@/assets/icons/history/chevron-left.svg";
import { ReactComponent as ChevronRightIcon } from "@/assets/icons/history/chevron-right.svg";
@@ -22,6 +22,8 @@ import { ReactComponent as RightIcon } from "@/assets/icons/history/right.svg";
import { ReactComponent as SearchIcon } from "@/assets/icons/history/search.svg";
import { ReactComponent as TrashIcon } from "@/assets/icons/trash.svg";
import { ReactComponent as CheckedCheckbox } from "@/assets/icons/checked-checkbox.svg";
import { ReactComponent as CheckIcon } from "@/assets/icons/line/check.svg";
import { ReactComponent as CopyIcon } from "@/assets/icons/history/copy.svg";
import "./history.less";
import { MainView } from "../common/elements/mainview";
@@ -113,6 +115,7 @@ class HistoryCmdStr extends React.Component<
cmdstr: string;
onUse: () => void;
onCopy: () => void;
isCopied: boolean;
fontSize: "normal" | "large";
limitHeight: boolean;
},
@@ -135,17 +138,24 @@ class HistoryCmdStr extends React.Component<
}
render() {
const { cmdstr, fontSize, limitHeight } = this.props;
const { isCopied, cmdstr, fontSize, limitHeight } = this.props;
return (
<div className={cn("cmdstr-code", { "is-large": fontSize == "large" }, { "limit-height": limitHeight })}>
<If condition={isCopied}>
<div key="copied" className="copied-indicator">
<div>copied</div>
</div>
</If>
<div key="code" className="code-div">
<code>{cmdstr}</code>
</div>
<div key="copy" className="actions-block">
<CopyButton onClick={this.handleCopy} title="Copy" />
<Button className="secondary ghost" title="Use Command" onClick={this.handleUse}>
<i className="fa-sharp fa-solid fa-play"></i>
</Button>
<div className="action-item" onClick={this.handleCopy} title="copy">
<CopyIcon className="icon" />
</div>
<div key="use" className="action-item" title="Use Command" onClick={this.handleUse}>
<CheckIcon className="icon" />
</div>
</div>
</div>
);
@@ -180,6 +190,7 @@ class HistoryView extends React.Component<{}, {}> {
tableRszObs: ResizeObserver;
sessionDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "sessionDropdownActive" });
remoteDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "remoteDropdownActive" });
copiedItemId: OV<string> = mobx.observable.box(null, { name: "copiedItemId" });
@boundMethod
handleNext() {
@@ -366,6 +377,14 @@ class HistoryView extends React.Component<{}, {}> {
return;
}
navigator.clipboard.writeText(item.cmdstr);
mobx.action(() => {
this.copiedItemId.set(item.historyid);
})();
setTimeout(() => {
mobx.action(() => {
this.copiedItemId.set(null);
})();
}, 600);
}
@boundMethod
@@ -375,7 +394,7 @@ class HistoryView extends React.Component<{}, {}> {
}
mobx.action(() => {
GlobalModel.showSessionView();
GlobalModel.inputModel.updateCmdLine({ str: item.cmdstr, pos: item.cmdstr.length });
GlobalModel.inputModel.setCurLine(item.cmdstr);
setTimeout(() => GlobalModel.inputModel.giveFocus(), 50);
})();
}
@@ -550,6 +569,7 @@ class HistoryView extends React.Component<{}, {}> {
cmdstr={item.cmdstr}
onUse={() => this.handleUse(item)}
onCopy={() => this.handleCopy(item)}
isCopied={this.copiedItemId.get() == item.historyid}
fontSize="normal"
limitHeight={true}
/>
+14 -7
View File
@@ -11,6 +11,8 @@ import { Markdown } from "@/elements";
import { AuxiliaryCmdView } from "./auxview";
import "./aichat.less";
import { UseOverlayScrollbarsInitialization, useOverlayScrollbars } from "overlayscrollbars-react";
import { ScrollbarsComponent } from "@/app/common/elements/scrollbars";
class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> {
componentDidMount(): void {
@@ -27,7 +29,7 @@ class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> {
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "generic:cancel", (waveEvent) => {
inputModel.closeAuxView();
inputModel.closeAIAssistantChat(true);
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "aichat:clearHistory", (waveEvent) => {
@@ -252,17 +254,22 @@ class AIChat extends React.Component<{}, {}> {
<AuxiliaryCmdView
title="Wave AI"
className="cmd-aichat"
onClose={() => GlobalModel.inputModel.closeAuxView()}
onClose={() => GlobalModel.inputModel.closeAIAssistantChat(true)}
iconClass="fa-sharp fa-solid fa-sparkles"
>
<If condition={renderKeybindings}>
<AIChatKeybindings AIChatObject={this}></AIChatKeybindings>
</If>
<div className="chat-window" ref={this.chatWindowScrollRef}>
<For each="chitem" index="idx" of={chatMessageItems}>
{this.renderChatMessage(chitem)}
</For>
</div>
<ScrollbarsComponent
childrenRef={this.chatWindowScrollRef}
options={{ scrollbars: { autoHide: "leave" } }}
>
<div className="chat-window" ref={this.chatWindowScrollRef}>
<For each="chitem" index="idx" of={chatMessageItems}>
{this.renderChatMessage(chitem)}
</For>
</div>
</ScrollbarsComponent>
<div className="chat-input">
<textarea
key="main"
@@ -1,28 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { SuggestionBlob } from "@/autocomplete/runtime/model";
import { GlobalModel } from "@/models";
import cn from "classnames";
import { useMemo } from "react";
export const AutocompleteSuggestions = async (props: { curLine: string }) => {
const inputModel = GlobalModel.inputModel;
const suggestions = await useMemo(async () => await inputModel.getSuggestions(), [props.curLine]);
if (!suggestions) {
return null;
}
const items = suggestions.suggestions.map((s) => `${s.icon} ${s.name}`);
return (
<div className="autocomplete-suggestions">
{items.map((item, idx) => (
<div key={idx} className={cn("autocomplete-item")}>
{item}
</div>
))}
</div>
);
};
+2 -8
View File
@@ -4,11 +4,12 @@
max-height: max(300px, 40%);
display: flex;
flex-direction: column;
position: absolute;
bottom: 0;
width: 100%;
z-index: 100;
border-top: 2px solid var(--app-border-color);
background-color: var(--app-bg-color);
position: relative;
// Apply a border between the base cmdinput and any views shown above it
// TODO: use a generic selector for this
@@ -92,8 +93,6 @@
.textareainput-div {
position: relative;
display: flex;
flex-direction: column;
&.control {
padding: var(--termpad) 0;
@@ -123,11 +122,6 @@
font-size: var(--termfontsize);
border: none;
box-shadow: none;
width: fit-content;
}
.suggestion {
width: fit-content;
}
input.history-input {
+20 -27
View File
@@ -5,7 +5,7 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { Choose, If, When } from "tsx-control-statements/components";
import { If } from "tsx-control-statements/components";
import cn from "classnames";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
@@ -18,7 +18,6 @@ import { Prompt } from "@/common/prompt/prompt";
import { CenteredIcon, RotateIcon } from "@/common/icons/icons";
import { AIChat } from "./aichat";
import * as util from "@/util/util";
import * as appconst from "@/app/appconst";
import "./cmdinput.less";
@@ -62,7 +61,7 @@ class CmdInput extends React.Component<{}, {}> {
}
@boundMethod
baseCmdInputClick(e: React.SyntheticEvent): void {
baseCmdInputClick(e: React.MouseEvent): void {
if (this.promptRef.current != null) {
if (this.promptRef.current.contains(e.target)) {
return;
@@ -72,7 +71,7 @@ class CmdInput extends React.Component<{}, {}> {
e.stopPropagation();
return;
}
GlobalModel.inputModel.setAuxViewFocus(false);
GlobalModel.inputModel.setHistoryFocus(false);
GlobalModel.inputModel.giveFocus();
}
@@ -81,8 +80,8 @@ class CmdInput extends React.Component<{}, {}> {
e.preventDefault();
e.stopPropagation();
const inputModel = GlobalModel.inputModel;
if (inputModel.getActiveAuxView() === appconst.InputAuxView_AIChat) {
inputModel.closeAuxView();
if (inputModel.aIChatShow.get()) {
inputModel.closeAIAssistantChat(true);
} else {
inputModel.openAIAssistantChat();
}
@@ -94,7 +93,7 @@ class CmdInput extends React.Component<{}, {}> {
e.stopPropagation();
const inputModel = GlobalModel.inputModel;
if (inputModel.getActiveAuxView() === appconst.InputAuxView_History) {
if (inputModel.historyShow.get()) {
inputModel.resetHistory();
} else {
inputModel.openHistory();
@@ -156,6 +155,9 @@ class CmdInput extends React.Component<{}, {}> {
remote = GlobalModel.getRemote(rptr.remoteid);
}
feState = feState || {};
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;
@@ -168,7 +170,7 @@ class CmdInput extends React.Component<{}, {}> {
let shellInitMsg: string = null;
let hidePrompt = false;
const openView = inputModel.getActiveAuxView();
const openView = inputModel.getOpenView();
const hasOpenView = openView ? `has-${openView}` : null;
if (ri == null) {
let shellStr = "shell";
@@ -183,19 +185,15 @@ class CmdInput extends React.Component<{}, {}> {
}
return (
<div ref={this.cmdInputRef} className={cn("cmd-input", hasOpenView, { active: focusVal })}>
<Choose>
<When condition={openView === appconst.InputAuxView_History}>
<div className="cmd-input-grow-spacer"></div>
<HistoryInfo />
</When>
<When condition={openView === appconst.InputAuxView_AIChat}>
<div className="cmd-input-grow-spacer"></div>
<AIChat />
</When>
<When condition={openView === appconst.InputAuxView_Info}>
<InfoMsg key="infomsg" />
</When>
</Choose>
<If condition={historyShow}>
<div className="cmd-input-grow-spacer"></div>
<HistoryInfo />
</If>
<If condition={aiChatShow}>
<div className="cmd-input-grow-spacer"></div>
<AIChat />
</If>
<InfoMsg key="infomsg" />
<If condition={remote && remote.status != "connected"}>
<div className="remote-status-warning">
WARNING:&nbsp;
@@ -231,12 +229,7 @@ class CmdInput extends React.Component<{}, {}> {
</Button>
</div>
</If>
<div
key="base-cmdinput"
className="base-cmdinput"
onClick={this.baseCmdInputClick}
onSelect={this.baseCmdInputClick}
>
<div key="base-cmdinput" className="base-cmdinput" onClick={this.baseCmdInputClick}>
<div className="cmdinput-actions">
<If condition={numRunningLines > 0}>
<div
+34 -26
View File
@@ -15,6 +15,7 @@ import { isBlank } from "@/util/util";
import "./historyinfo.less";
import { AuxiliaryCmdView } from "./auxview";
import { ScrollbarsComponent } from "@/app/common/elements/scrollbars";
dayjs.extend(localizedFormat);
@@ -153,6 +154,7 @@ class HistoryInfo extends React.Component<{}, {}> {
lastClickHNum: string = null;
lastClickTs: number = 0;
containingText: mobx.IObservableValue<string> = mobx.observable.box("");
historyItemsRef: React.RefObject<HTMLDivElement> = React.createRef();
componentDidMount() {
const inputModel = GlobalModel.inputModel;
@@ -167,14 +169,14 @@ class HistoryInfo extends React.Component<{}, {}> {
@boundMethod
handleClose() {
GlobalModel.inputModel.closeAuxView();
GlobalModel.inputModel.toggleInfoMsg();
}
@boundMethod
handleItemClick(hitem: HistoryItem) {
const inputModel = GlobalModel.inputModel;
const selItem = inputModel.getHistorySelectedItem();
inputModel.setAuxViewFocus(false);
inputModel.setHistoryFocus(true);
if (this.lastClickHNum == hitem.historynum && selItem != null && selItem.historynum == hitem.historynum) {
inputModel.grabSelectedHistoryItem();
return;
@@ -194,14 +196,14 @@ class HistoryInfo extends React.Component<{}, {}> {
@boundMethod
handleClickType() {
const inputModel = GlobalModel.inputModel;
inputModel.setAuxViewFocus(true);
inputModel.setHistoryFocus(true);
inputModel.toggleHistoryType();
}
@boundMethod
handleClickRemote() {
const inputModel = GlobalModel.inputModel;
inputModel.setAuxViewFocus(true);
inputModel.setHistoryFocus(true);
inputModel.toggleRemoteType();
}
@@ -239,33 +241,39 @@ class HistoryInfo extends React.Component<{}, {}> {
return (
<AuxiliaryCmdView
title="History"
className="cmd-history hide-scrollbar"
className="cmd-history"
onClose={this.handleClose}
titleBarContents={this.getTitleBarContents()}
iconClass="fa-sharp fa-solid fa-clock-rotate-left"
>
<div
className={cn(
"history-items",
{ "show-remotes": !opts.limitRemote },
{ "show-sessions": opts.queryType == "global" }
)}
<ScrollbarsComponent
childrenRef={this.historyItemsRef}
options={{ scrollbars: { autoHide: "scroll" } }}
>
<If condition={hitems.length == 0}>[no history]</If>
<If condition={hitems.length > 0}>
<For each="hitem" index="idx" of={hitems}>
<HItem
key={hitem.historyid}
hitem={hitem}
isSelected={hitem == selItem}
opts={opts}
snames={snames}
scrNames={scrNames}
onClick={this.handleItemClick}
></HItem>
</For>
</If>
</div>
<div
className={cn(
"history-items",
{ "show-remotes": !opts.limitRemote },
{ "show-sessions": opts.queryType == "global" }
)}
ref={this.historyItemsRef}
>
<If condition={hitems.length == 0}>[no history]</If>
<If condition={hitems.length > 0}>
<For each="hitem" index="idx" of={hitems}>
<HItem
key={hitem.historyid}
hitem={hitem}
isSelected={hitem == selItem}
opts={opts}
snames={snames}
scrNames={scrNames}
onClick={this.handleItemClick}
></HItem>
</For>
</If>
</div>
</ScrollbarsComponent>
</AuxiliaryCmdView>
);
}
+1 -1
View File
@@ -45,7 +45,7 @@ class InfoMsg extends React.Component<{}, {}> {
render() {
const inputModel = GlobalModel.inputModel;
const infoMsg: InfoType = inputModel.infoMsg.get();
const infoShow = inputModel.getActiveAuxView() == appconst.InputAuxView_Info;
const infoShow: boolean = inputModel.infoShow.get();
let line: string = null;
let istr: string = null;
let idx: number = 0;
File diff suppressed because it is too large Load Diff

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