Compare commits

...

22 Commits

Author SHA1 Message Date
Evan Simkowitz 4d95d45a45 save 2024-04-05 10:46:07 -07:00
Evan Simkowitz 1c23701181 Clean up styling and focus behavior for cmdinput (#546)
* Clean up cmdinput

* Remove unused css styles, clicking on textarea will focus back to textarea without closing history

* cleanup logic for activating textarea

* actions buttons should always show, should properly disable inactive views

* clicking actions toggles the view

* remove titlebar spacer, clean up padding

* Make AIChat and HistoryInfo share a common layout

* fix ai chat scroll

* clean up formatting

* fix chat textarea resizing

* align prompt and input

* update infomsg to use auxview

* update comments

* fix widths and key error

* add todo

* adjust padding for input, remove debug

* Don't capture clicks on the prompt area
2024-04-04 19:29:43 -07:00
Cole Lashley 0fe767cdf3 Bugfixes for ai chat code select (#537)
* added uuid to code select to fix some render related bugs

* added input popup type, and fixed aichat computed condition

* fixed stash artifacts
2024-04-04 16:58:26 -07:00
Mike Sawka 097623ab51 have initial run-command return faster to the frontend for quicker updating (#549)
* have initial run-command return faster to the frontend for quicker updating.  cuts time from 70-80ms down to 20ms for an average command

* remove wlogs

* more logging cleanup

* fix focus for when start cmd returns an error
2024-04-04 15:08:45 -07:00
Red J Adaya 9de25e4869 truncate labels (#544) 2024-04-03 09:55:36 -07:00
Evan Simkowitz a139633375 Send NumRunningCommands updates via the main bus, regardless of where other updates are being sent (#545) 2024-04-02 18:47:54 -07:00
Evan Simkowitz ccc63937b6 Support running ephemeral commands (#543)
* initial

* save work, starting to add backend types

* save work

* save work

* Add EphemeralWriteCloser

* Command pipes thru, triggers infinite loop

* save debugging

* fix bad merge

* save debug statements

* fixing spaghetti

* clean up code

* got cwd override working

* Add separate paths for stdout and stderr writers

* fix stdout/stderr

* env vars are now working

* revert waveshell changes

* Making EphemeralWriteCloser into a more generic BufferedPipe

* formatting

* comment

* delete unused package

* more naming changes

* add package comment

* add UsePty to EphemeralRunOpts

* document UsePty

* ensure only one downstream writer can read from the buffer

* store pointer to syncs

* remove inshellisense stuff for now

* remove debugs

* revert yarn

* remove unnecessary debugs in main-server

* more debugging removed

* revert tsconfig
2024-04-02 15:46:32 -07:00
Evan Simkowitz dc7fc2c823 Pull in linting fixes from preact branch (#538)
* Pull in linting fixes from preact branch

* more consts

* one more const

* Revert PureComponent

* revert preact artifavt

* revert another change
2024-04-02 12:30:17 -07:00
Red J Adaya ca5117cda0 Terminal theming (#485)
* init

* use setStyleVar

* backend implementation. scrope level terminal theming.

* only invoke this.applyTermTheme for keys that are updated. command runner for global term theme

* invoke applyTermTheme for global terminal themes as well

* fix nil error

* fix issue were theme can't be found

* fix issue where selected termtheme is not set as default value

* term theme switcher for session

* do not force reload after setting css vars

* fix issues. screenview terminal theme switcher

* remove debugging code

* move getTermThemes to util

* fix global theme reset

* fix workspace theme reset

* fix screenview terminal theme reset issue

* cleanup

* do not apply theme if theme hasn't changed

* do not apply theme if theme hasn't changed in workspace view

* cleanup

* cleanup

* force reload terminal

* fix inconsistency

* fix reset issue

* add a mobx reaction so that theming working when switching sessions

* workig reset

* simplify and cleanup

* refactor

* working global and session terminal theming

* add check

* perf improvement

* more perf improvements

* put reaction componentDidUpdate to make sure ref is already associated to the element

* cleanup

* fix issue where session theme is overriden by global theme on reload

* reduce flickering on reload

* more on reducing flickering on reload

* cleanup

* more cleanup

* fix file not found when no global theme is set

* screen level terminal theming

* update comment

* re-render terminal in history view. cleanup.

* cleanup

* merge main
2024-04-01 23:41:24 -07:00
Cole Lashley f41ac1d5e3 Added this.lastHistoryUpDown = false to cmdinput domain callback (#533)
* added more cmdinput-anykey behavior

* fixed lasthistoryupdown

* more bug fixes

* fixed history bugs hopefully fully fixed
2024-03-29 13:28:49 -07:00
Mike Sawka 0024f0f3e8 fix xterm.js versions and addon versions (#534)
* make xterm packages consistent

* add serializeAddon, fix varsUpdated
2024-03-29 10:52:18 -07:00
Mike Sawka 8a3d9628ba add addons-webgl, protected with clientsettings flag (#532) 2024-03-28 23:19:39 -07:00
Mike Sawka 7c2265f3e3 add debug command to see remote-instance state (#531) 2024-03-28 18:53:16 -07:00
Cole Lashley 4f6c2ee39b Other key bind bug fixes (#529) 2024-03-28 17:53:03 -07:00
Mike Sawka a1e4e807cc update statediff algorithm for wavesrv / remote instances (#530)
* remote statemap from waveshell server (diff against initial state)

* move ShellStatePtr from sstore to packet so it can be passed over the wire

* add finalstatebaseptr to cmddone

* much improved diff computation code on wavesrv side

* fix displayname -- now using hash

* add comments, change a couple msh.WriteToPtyBuffer calls to log.Printfs
2024-03-28 16:56:39 -07:00
Cole Lashley 5c85b2b786 Cmdinput keybinding bug fixes (#524)
* added domain callback for every layer

* added domain callback for every layer

* switched dump logs to false

* dropdown fixes

* moved uuid back to constructor
2024-03-28 13:19:20 -07:00
Cole Lashley 4879e90e91 fixed 2 more switches for baseurl (#527) 2024-03-28 13:14:05 -07:00
Evan Simkowitz a61310afc3 Remove datepicker from history until we have a proper date range selection (#526) 2024-03-28 12:21:58 -07:00
Cole Lashley 80c781bf4c added check for no baseurl (#525) 2024-03-28 12:20:58 -07:00
Mike Sawka 33400e65ff restore delete screen button (#522) 2024-03-27 22:42:37 -07:00
Sylvie Crowe 7b58949027 fix: this prevents possible loops while connecting (#521)
A recent change made it possible to get stuck in a loop when connecting
to a remote. This reverts the part of it that caused this while
retaining the other behavior. This makes it possible to add to blank
known_host files again. It also adds a printout to display when a
connection is complete.
2024-03-27 21:19:35 -07:00
Cole Lashley 3be96c949d Debug pane and lookup key (#518)
* added new util functions for logging keyutils and added debug pane

* clean up the formatting
2024-03-27 18:33:29 -07:00
66 changed files with 3195 additions and 1616 deletions
+4 -2
View File
@@ -49,8 +49,10 @@
"tsx-control-statements": "^5.1.1",
"uuid": "^9.0.0",
"winston": "^3.8.2",
"xterm": "^5.0.0",
"xterm-addon-web-links": "^0.9.0"
"xterm": "^5.3.0",
"xterm-addon-web-links": "^0.9.0",
"xterm-addon-serialize": "^0.11.0",
"xterm-addon-webgl": "^0.16.0"
},
"devDependencies": {
"@babel/cli": "^7.17.10",
+18 -16
View File
@@ -42,16 +42,19 @@
--app-bg-color: black;
--app-accent-color: rgb(88, 193, 66);
--app-accent-bg-color: rgba(88, 193, 66, 0.25);
--app-accent-bg-darker-color: rgba(88, 193, 66, 0.5);
--app-text-color: rgb(211, 215, 207);
--app-text-primary-color: rgb(255, 255, 255);
--app-text-secondary-color: rgb(195, 200, 194);
--app-text-disabled-color: rgb(173, 173, 173);
--app-text-bg-color: rgb(23, 23, 23);
--app-text-bg-disabled-color: rgb(51, 51, 51);
--app-border-color: rgb(51, 51, 51);
--app-maincontent-bg-color: #333;
--app-maincontent-bg-color: rgb(51, 51, 51);
--app-panel-bg-color: rgba(21, 23, 21, 1);
--app-panel-bg-color-dev: rgb(21, 23, 48);
--app-icon-color: rgb(139, 145, 138);
--app-icon-hover-color: #fff;
--app-icon-hover-color: rgb(255, 255, 255);
--app-selected-mask-color: rgba(255, 255, 255, 0.06);
/* global status colors */
@@ -87,7 +90,7 @@
--form-element-border-color: rgba(241, 246, 243, 0.15);
--form-element-bg-color: var(--app-bg-color);
--form-element-text-color: var(--app-text-primary-color);
--form-element-primary-text-color: var(--form-element-text-color);
--form-element-primary-text-color: var(--app-text-primary-color);
--form-element-label-color: var(--app-text-secondary-color);
--form-element-primary-color: var(--app-accent-color);
--form-element-secondary-color: rgba(255, 255, 255, 0.2);
@@ -122,18 +125,18 @@
/* line colors */
--line-sidebar-message-color: rgb(196, 160, 0);
--line-background: rgba(21, 23, 21, 1);
--line-avatar-color: #eceeec;
--line-background: var(--app-panel-bg-color);
--line-avatar-color: rgb(236, 238, 236);
--line-text-color: rgb(211, 215, 207);
--line-svg-fill-color: rgb(150, 152, 150);
--line-svg-hover-fill-color: #eceeec;
--line-svg-hover-fill-color: rgb(236, 238, 236);
--line-separator-color: rgb(126, 126, 126);
--line-error-color: var(--app-error-color);
--line-warning-color: var(--app-warning-color);
--line-base-soft-blue-color: #729fcf;
--line-base-soft-blue-color: rgb(114, 159, 207);
--line-active-border-color: var(--app-accent-color);
--line-selected-bg-color: rgba(255, 255, 255, 0.05);
--line-selected-border-left-color: #777777;
--line-selected-border-left-color: rgb(119, 119, 119);
--line-selected-error-border-color: rgba(204, 0, 0, 0.8);
--line-selected-error-bg-color: rgb(19, 4, 3);
--line-error-bg-color: rgba(200, 0, 0, 0.1);
@@ -152,22 +155,21 @@
/* table colors */
--table-border-color: rgba(241, 246, 243, 0.15);
--table-thead-border-top-color: rgba(250, 250, 250, 0.1);
--table-thead-bright-border-color: #ccc;
--table-thead-bright-border-color: rgb(204, 204, 204);
--table-thead-bg-color: rgba(250, 250, 250, 0.02);
--table-tr-border-bottom-color: rgba(241, 246, 243, 0.15);
--table-tr-hover-bg-color: rgba(255, 255, 255, 0.06);
--table-tr-selected-bg-color: #222;
--table-tr-selected-hover-bg-color: #333;
--table-tr-selected-bg-color: rgb(34, 34, 34);
--table-tr-selected-hover-bg-color: var(--app-maincontent-bg-color);
/* cmdinput colors */
--cmdinput-textarea-bg-color: #171717;
--cmdinput-bg-color: var(--app-text-bg-color);
--cmdinput-text-error-color: var(--term-red);
--cmdinput-history-item-error-color: var(--term-bright-red);
--cmdinput-history-item-selected-error-color: var(--term-bright-red);
--cmdinput-button-bg-color: rgb(88, 193, 66);
--cmdinput-comment-button-bg-color: rgb(57, 113, 255);
--cmdinput-disabled-icon-color: rgb(76, 81, 75, 1);
--cmdinput-history-bg-color: rgb(21, 23, 21, 1);
--cmdinput-button-bg-color: var(--tab-green);
--cmdinput-disabled-bg-color: var(--app-text-bg-disabled-color);
--cmdinput-history-bg-color: var(--app-bg-color);
/* screen view color */
--screen-view-text-caption-color: rgb(139, 145, 138);
+17 -19
View File
@@ -5,20 +5,20 @@
@import url("./term-light.css");
:root {
--app-bg-color: #fefefe;
--app-bg-color: rgb(254, 254, 254);
--app-accent-color: rgb(75, 166, 57);
--app-accent-bg-color: rgba(75, 166, 57, 0.2);
--app-text-color: #000;
--app-text-color: rgb(0, 0, 0);
--app-text-primary-color: rgb(0, 0, 0, 0.9);
--app-text-secondary-color: rgb(0, 0, 0, 0.7);
--app-border-color: rgb(139 145 138);
--app-panel-bg-color: #e0e0e0;
--app-panel-bg-color-dev: #e0e0e0;
--app-icon-color: rgb(80, 80, 80);
--app-icon-hover-color: rgb(100, 100, 100);
--app-panel-bg-color: rgb(224, 224, 224);
--app-panel-bg-color-dev: rgb(224, 224, 224);
--app-icon-color: rgb(110, 110, 110);
--app-icon-hover-color: rgb(80, 80, 80);
--app-selected-mask-color: rgba(0, 0, 0, 0.06);
--input-bg-color: #eeeeee;
--input-bg-color: rgb(238, 238, 238);
/* tab color */
--tab-white: rgb(0, 0, 0, 0.6);
@@ -30,19 +30,19 @@
--table-thead-bg-color: rgba(250, 250, 250, 0.15);
--table-tr-border-bottom-color: rgba(0, 0, 0, 0.15);
--table-tr-hover-bg-color: rgba(0, 0, 0, 0.15);
--table-tr-selected-bg-color: #dddddd;
--table-tr-selected-hover-bg-color: #cccccc;
--table-tr-selected-bg-color: rgb(221, 221, 221);
--table-tr-selected-hover-bg-color: rgb(204, 204, 204);
/* form colors */
--form-element-border-color: rgba(0, 0, 0, 0.3);
--form-element-bg-color: var(--app-bg-color);
--form-element-text-color: var(--app-text-color);
--form-element-primary-text-color: var(--app-bg-color);
--form-element-text-color: var(--app-text-primary-color);
--form-element-primary-text-color: var(--app-text-primary-color);
--form-element-label-color: rgba(0, 0, 0, 0.6);
--form-element-secondary-color: rgba(0, 0, 0, 0.09);
--form-element-icon-color: rgb(0, 0, 0, 0.6);
--form-element-disabled-text-color: #b7b7b7;
--form-element-placeholder-color: #b7b7b7;
--form-element-disabled-text-color: rgb(183, 183, 183);
--form-element-placeholder-color: rgb(183, 183, 183);
--markdown-bg-color: rgb(0, 0, 0, 0.1);
@@ -50,8 +50,6 @@
--modal-header-bottom-border-color: rgba(0, 0, 0, 0.3);
/* cmd input */
--cmdinput-textarea-bg-color: rgba(0, 0, 0, 0.1);
--cmdinput-textarea-border-color: var(--form-element-border-color);
/* scroll colors */
--scrollbar-background-color: var(--app-bg-color);
@@ -64,15 +62,15 @@
--line-actions-inactive-color: rgba(0, 0, 0, 0.3);
--line-actions-active-color: rgba(0, 0, 0, 1);
--logo-button-hover-bg-color: #f0f0f0;
--logo-button-hover-bg-color: rgb(240, 240, 240);
--xterm-viewport-border-color: rgba(0, 0, 0, 0.3);
--datepicker-cell-hover-bg-color: #f0f0f0;
--datepicker-cell-hover-bg-color: rgb(240, 240, 240);
--datepicker-cell-other-text-color: rgba(0, 0, 0, 0.3);
--datepicker-header-fade-color: rgba(0, 0, 0, 0.4);
--datepicker-year-header-bg-color: #f5f5f5;
--datepicker-year-header-border-color: #dcdcdc;
--datepicker-year-header-bg-color: rgb(245, 245, 245);
--datepicker-year-header-border-color: rgb(220, 220, 220);
/* toggle colors */
--toggle-thumb-color: var(--app-bg-color);
+29
View File
@@ -9,6 +9,7 @@ import { If } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, RemotesModel, getApi } from "@/models";
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";
@@ -73,6 +74,19 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeTermTheme(theme: string): void {
// For global terminal theme, the key is global, otherwise it's either
// sessionId or screenId.
const currTheme = GlobalModel.getTermTheme()["global"];
if (currTheme == theme) {
return;
}
const prtn = GlobalCommandRunner.setGlobalTermTheme(theme, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
handleChangeTelemetry(val: boolean): void {
let prtn: Promise<CommandRtnType> = null;
@@ -193,6 +207,8 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
const curFontSize = GlobalModel.getTermFontSize();
const curFontFamily = GlobalModel.getTermFontFamily();
const curTheme = GlobalModel.getThemeSource();
const termThemes = getTermThemes(GlobalModel.termThemes, "Wave Default");
const currTermTheme = GlobalModel.getTermTheme()["global"] ?? termThemes[0].label;
return (
<MainView className="clientsettings-view" title="Client Settings" onClose={this.handleClose}>
@@ -233,6 +249,19 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
/>
</div>
</div>
<If condition={termThemes.length > 0}>
<div className="settings-field">
<div className="settings-label">Terminal Theme</div>
<div className="settings-input">
<Dropdown
className="terminal-theme-dropdown"
options={termThemes}
defaultValue={currTermTheme}
onChange={this.handleChangeTermTheme}
/>
</div>
</div>
</If>
<div className="settings-field">
<div className="settings-label">Client ID</div>
<div className="settings-input">{cdata.clientid}</div>
+47
View File
@@ -27,6 +27,10 @@
}
}
&.primary.danger {
background: var(--app-error-color);
}
&.primary.outlined {
background: none;
border: 1px solid var(--form-element-primary-color);
@@ -36,6 +40,37 @@
}
}
&.primary.greyoutlined {
background: none;
border: 1px solid var(--app-text-secondary-color);
i {
fill: var(--app-text-secondary-color);
}
}
&.primary.outlined,
&.primary.greyoutlined {
&.hover-danger:hover {
color: var(--form-element-primary-text-color);
border: 1px solid var(--app-error-color);
background: var(--app-error-color);
}
}
&.primary.outlined.danger {
background: none;
border: 1px solid var(--app-error-color);
i {
fill: var(--app-error-color);
}
}
&.greytext {
color: var(--app-text-secondary-color);
}
&.primary.ghost {
background: none;
i {
@@ -43,6 +78,13 @@
}
}
&.primary.ghost.danger {
background: none;
i {
fill: var(--app-error-color);
}
}
&.secondary {
color: var(--form-element-text-color);
background: var(--form-element-secondary-color);
@@ -56,6 +98,11 @@
border: 1px solid var(--form-element-text-color);
}
&.secondary.outlined.danger {
background: none;
border: 1px solid var(--app-error-color);
}
&.secondary.ghost {
background: none;
}
+9 -1
View File
@@ -53,7 +53,7 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
};
this.wrapperRef = React.createRef();
this.menuRef = React.createRef();
this.curUuid == uuidv4();
this.curUuid = uuidv4();
}
componentDidMount() {
@@ -100,6 +100,9 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
@boundMethod
handleClick() {
if (!this.state.isOpen || !this.state.isTouched) {
this.registerKeybindings();
}
this.toggleDropdown();
}
@@ -109,6 +112,7 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
this.registerKeybindings();
}
@boundMethod
registerKeybindings() {
let keybindManager = GlobalModel.keybindManager;
let domain = "dropdown-" + this.curUuid;
@@ -122,6 +126,7 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
});
keybindManager.registerKeybinding("control", domain, "generic:cancel", (waveEvent) => {
this.setState({ isOpen: false });
this.unregisterKeybindings();
return true;
});
keybindManager.registerKeybinding("control", domain, "generic:selectAbove", (waveEvent) => {
@@ -165,10 +170,12 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
}
}
@boundMethod
handleBlur() {
this.unregisterKeybindings();
}
@boundMethod
unregisterKeybindings() {
let domain = "dropdown-" + this.curUuid;
GlobalModel.keybindManager.unregisterDomain(domain);
@@ -189,6 +196,7 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
}
onChange(value);
this.setState({ isOpen: false, isTouched: true });
this.unregisterKeybindings();
}
@boundMethod
+22 -5
View File
@@ -7,8 +7,10 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import cn from "classnames";
import { GlobalModel } from "@/models";
import { v4 as uuidv4 } from "uuid";
import "./markdown.less";
import { boundMethod } from "autobind-decorator";
function LinkRenderer(props: any): any {
let newUrl = "https://extern?" + encodeURIComponent(props.href);
@@ -28,14 +30,17 @@ function CodeRenderer(props: any): any {
}
@mobxReact.observer
class CodeBlockMarkdown extends React.Component<{ children: React.ReactNode; codeSelectSelectedIndex?: number }, {}> {
class CodeBlockMarkdown extends React.Component<
{ children: React.ReactNode; codeSelectSelectedIndex?: number; uuid: string },
{}
> {
blockIndex: number;
blockRef: React.RefObject<HTMLPreElement>;
constructor(props) {
super(props);
this.blockRef = React.createRef();
this.blockIndex = GlobalModel.inputModel.addCodeBlockToCodeSelect(this.blockRef);
this.blockIndex = GlobalModel.inputModel.addCodeBlockToCodeSelect(this.blockRef, this.props.uuid);
}
render() {
@@ -62,9 +67,21 @@ class Markdown extends React.Component<
{ text: string; style?: any; extraClassName?: string; codeSelect?: boolean },
{}
> {
CodeBlockRenderer(props: any, codeSelect: boolean, codeSelectIndex: number): any {
curUuid: string;
constructor(props) {
super(props);
this.curUuid = uuidv4();
}
@boundMethod
CodeBlockRenderer(props: any, codeSelect: boolean, codeSelectIndex: number, curUuid: string): any {
if (codeSelect) {
return <CodeBlockMarkdown codeSelectSelectedIndex={codeSelectIndex}>{props.children}</CodeBlockMarkdown>;
return (
<CodeBlockMarkdown codeSelectSelectedIndex={codeSelectIndex} uuid={curUuid}>
{props.children}
</CodeBlockMarkdown>
);
} else {
const clickHandler = (e: React.MouseEvent<HTMLElement>) => {
let blockText = (e.target as HTMLElement).innerText;
@@ -90,7 +107,7 @@ class Markdown extends React.Component<
h5: (props) => HeaderRenderer(props, 5),
h6: (props) => HeaderRenderer(props, 6),
code: (props) => CodeRenderer(props),
pre: (props) => this.CodeBlockRenderer(props, codeSelect, curCodeSelectIndex),
pre: (props) => this.CodeBlockRenderer(props, codeSelect, curCodeSelectIndex, this.curUuid),
};
return (
<div className={cn("markdown content", this.props.extraClassName)} style={this.props.style}>
+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>;
};
+39 -2
View File
@@ -11,8 +11,9 @@ import { GlobalModel, GlobalCommandRunner, Screen } from "@/models";
import { SettingsError, Modal, Dropdown, Tooltip } from "@/elements";
import * as util from "@/util/util";
import { Button } from "@/elements";
import { ReactComponent as GlobeIcon } from "@/assets/icons/globe.svg";
import { ReactComponent as StatusCircleIcon } from "@/assets/icons/statuscircle.svg";
import { If } from "tsx-control-statements/components";
import { commandRtnHandler } from "@/util/util";
import { getTermThemes } from "@/util/themeutil";
import {
TabColorSelector,
TabIconSelector,
@@ -153,11 +154,34 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
});
}
@boundMethod
handleChangeTermTheme(theme: string): void {
const currTheme = GlobalModel.getTermTheme()[this.screenId];
if (currTheme == theme) {
return;
}
const prtn = GlobalCommandRunner.setScreenTermTheme(this.screenId, theme, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
selectRemote(cname: string): void {
let prtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
util.commandRtnHandler(prtn, this.errorMessage);
}
render() {
const screen = this.screen;
if (screen == null) {
return null;
}
let color: string = null;
let icon: string = null;
let index: number = 0;
const curRemote = GlobalModel.getRemote(GlobalModel.getActiveScreen().getCurRemoteInstance().remoteid);
const termThemes = getTermThemes(GlobalModel.termThemes);
const currTermTheme = GlobalModel.getTermTheme()[this.screenId] ?? termThemes[0].label;
return (
<Modal className="screen-settings-modal">
<Modal.Header onClose={this.closeModal} title={`Tab Settings (${screen.name.get()})`} />
@@ -186,6 +210,19 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
<TabIconSelector screen={screen} errorMessage={this.errorMessage} />
</div>
</div>
<If condition={termThemes.length > 0}>
<div className="settings-field">
<div className="settings-label">Terminal Theme</div>
<div className="settings-input">
<Dropdown
className="terminal-theme-dropdown"
options={termThemes}
defaultValue={currTermTheme}
onChange={this.handleChangeTermTheme}
/>
</div>
</div>
</If>
<div className="settings-field">
<div className="settings-label actions-label">
<div>Actions</div>
+30 -2
View File
@@ -6,8 +6,10 @@ import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { GlobalModel, GlobalCommandRunner, Session } from "@/models";
import { Button } from "@/elements";
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Tooltip } from "@/elements";
import { If } from "tsx-control-statements/components";
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Tooltip, Button, Dropdown } from "@/elements";
import { commandRtnHandler } from "@/util/util";
import { getTermThemes } from "@/util/themeutil";
import * as util from "@/util/util";
import "./sessionsettings.less";
@@ -76,6 +78,16 @@ class SessionSettingsModal extends React.Component<{}, {}> {
});
}
@boundMethod
handleChangeTermTheme(theme: string): void {
const currTheme = GlobalModel.getTermTheme()[this.sessionId];
if (currTheme == theme) {
return;
}
const prtn = GlobalCommandRunner.setSessionTermTheme(this.sessionId, theme, false);
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
dismissError(): void {
mobx.action(() => {
@@ -87,6 +99,9 @@ class SessionSettingsModal extends React.Component<{}, {}> {
if (this.session == null) {
return null;
}
const termThemes = getTermThemes(GlobalModel.termThemes);
const currTermTheme = GlobalModel.getTermTheme()[this.sessionId] ?? termThemes[0].label;
return (
<Modal className="session-settings-modal">
<Modal.Header onClose={this.closeModal} title={`Workspace Settings (${this.session.name.get()})`} />
@@ -104,6 +119,19 @@ class SessionSettingsModal extends React.Component<{}, {}> {
/>
</div>
</div>
<If condition={termThemes.length > 0}>
<div className="settings-field">
<div className="settings-label">Terminal Theme</div>
<div className="settings-input">
<Dropdown
className="terminal-theme-dropdown"
options={termThemes}
defaultValue={currTermTheme}
onChange={this.handleChangeTermTheme}
/>
</div>
</div>
</If>
<div className="settings-field">
<div className="settings-label">
<div>Archived</div>
+9 -4
View File
@@ -308,7 +308,6 @@ class HistoryView extends React.Component<{}, {}> {
hvm.setFromDate(null);
return;
}
console.log;
hvm.setFromDate(newDate);
}
@@ -486,10 +485,10 @@ class HistoryView extends React.Component<{}, {}> {
options={this.getRemoteItems(rnames, remoteIds)}
onChange={this.clickLimitRemote}
/>
<div className="fromts">
{/* <div className="fromts">
<div className="fromts-text">From:&nbsp;</div>
<DatePicker selectedDate={new Date()} onSelectDate={this.handleFromTsChange} />
</div>
</div> */}
<div
className="filter-cmds search-checkbox hoverEffect"
title="Filter common commands like 'ls' and 'cd' from the results"
@@ -670,6 +669,11 @@ class LineContainer extends React.Component<{ historyId: string; width: number }
this.line = hvm.getLineById(this.historyItem.lineid);
}
componentDidMount(): void {
GlobalModel.bumpTermRenderVersion();
GlobalModel.termThemeSrcEl.set(null);
}
@boundMethod
handleHeightChange(lineNum: number, newHeight: number, oldHeight: number): void {
return;
@@ -710,8 +714,9 @@ class LineContainer extends React.Component<{ historyId: string; width: number }
ssStr = sprintf("#%s[%s]", session.name.get(), screen.name.get());
canViewInContext = true;
}
const termRenderVersion = GlobalModel.termRenderVersion.get();
return (
<div className="line-container">
<div className="line-container" key={termRenderVersion}>
<If condition={canViewInContext}>
<div className="line-context">
<div title="View in Context" className="vic-btn" onClick={this.viewInContext}>
-5
View File
@@ -106,11 +106,6 @@
}
}
&.top-border {
border-top: 1px solid #777;
padding: 10px;
}
&:hover .meta .termopts {
display: block;
}
+156 -145
View File
@@ -8,7 +8,7 @@ import { sprintf } from "sprintf-js";
import { boundMethod } from "autobind-decorator";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { If } from "tsx-control-statements/components";
import { Choose, If, Otherwise, When } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, Cmd } from "@/models";
import { termHeightFromRows } from "@/util/textmeasure";
import cn from "classnames";
@@ -25,23 +25,22 @@ import * as lineutil from "./lineutil";
import { ErrorBoundary } from "@/common/error/errorboundary";
import * as appconst from "@/app/appconst";
import * as util from "@/util/util";
import * as textmeasure from "@/util/textmeasure";
import "./line.less";
import { CenteredIcon, RotateIcon } from "../common/icons/icons";
import { RotateIcon } from "../common/icons/icons";
const DebugHeightProblems = false;
const MinLine = 0;
const MaxLine = 1000;
let heightLog = {};
const heightLog = {};
(window as any).heightLog = heightLog;
(window as any).findHeightProblems = function () {
for (let linenum in heightLog) {
let lh = heightLog[linenum];
for (const linenum in heightLog) {
const lh = heightLog[linenum];
if (lh.heightArr == null || lh.heightArr.length < 2) {
continue;
}
let firstHeight = lh.heightArr[0];
const firstHeight = lh.heightArr[0];
for (let i = 1; i < lh.heightArr.length; i++) {
if (lh.heightArr[i] != firstHeight) {
console.log("line", linenum, "heights", lh.heightArr);
@@ -57,7 +56,7 @@ function cmdShouldMarkError(cmd: Cmd): boolean {
if (cmd.getStatus() == "error") {
return true;
}
let exitCode = cmd.getExitCode();
const exitCode = cmd.getExitCode();
// 0, SIGINT, or SIGPIPE
if (exitCode == 0 || exitCode == 130 || exitCode == 141) {
return false;
@@ -142,7 +141,7 @@ class LineActions extends React.Component<{ screen: LineContainerType; line: Lin
handleLineSettings(e: any): void {
e.preventDefault();
e.stopPropagation();
let { line } = this.props;
const { line } = this.props;
if (line != null) {
mobx.action(() => {
GlobalModel.lineSettingsModal.set(line.linenum);
@@ -152,74 +151,89 @@ class LineActions extends React.Component<{ screen: LineContainerType; line: Lin
}
render() {
let { line, screen } = this.props;
const { line, screen } = this.props;
const isMinimized = line.linestate["wave:min"];
const containerType = screen.getContainerType();
return (
<div className="line-actions">
<If condition={containerType == appconst.LineContainer_Main}>
<div key="restart" title="Restart Command" className="line-icon" onClick={this.clickRestart}>
<i className="fa-sharp fa-regular fa-arrows-rotate fa-fw" />
</div>
<div key="delete" title="Delete Line (&#x2318;D)" className="line-icon" onClick={this.clickDelete}>
<i className="fa-sharp fa-regular fa-trash fa-fw" />
</div>
<div
key="bookmark"
title="Bookmark"
className={cn("line-icon", "line-bookmark")}
onClick={this.clickBookmark}
>
<i className="fa-sharp fa-regular fa-bookmark fa-fw" />
</div>
<div
key="minimize"
title={`${isMinimized ? "Show Output" : "Hide Output"}`}
className={cn("line-icon", isMinimized ? "active" : "")}
onClick={this.clickMinimize}
>
<If condition={isMinimized}>
<i className="fa-sharp fa-regular fa-circle-plus fa-fw" />
</If>
<If condition={!isMinimized}>
<i className="fa-sharp fa-regular fa-circle-minus fa-fw" />
</If>
</div>
<div className="line-icon line-sidebar" onClick={this.clickMoveToSidebar} title="Move to Sidebar">
<i className="fa-sharp fa-solid fa-right-to-line fa-fw" />
</div>
<div
key="settings"
title="Line Settings"
className="line-icon line-icon-shrink-left"
onClick={this.handleLineSettings}
>
<i className="fa-sharp fa-regular fa-ellipsis-vertical fa-fw" />
</div>
</If>
<If condition={containerType == appconst.LineContainer_Sidebar}>
<div key="restart" title="Restart Command" className="line-icon" onClick={this.clickRestart}>
<i className="fa-sharp fa-regular fa-arrows-rotate fa-fw" />
</div>
<div key="delete" title="Delete Line (&#x2318;D)" className="line-icon" onClick={this.clickDelete}>
<i className="fa-sharp fa-regular fa-trash fa-fw" />
</div>
<div
key="bookmark"
title="Bookmark"
className={cn("line-icon", "line-bookmark")}
onClick={this.clickBookmark}
>
<i className="fa-sharp fa-regular fa-bookmark fa-fw" />
</div>
<div
className="line-icon line-sidebar"
onClick={this.clickRemoveFromSidebar}
title="Move to Sidebar"
>
<i className="fa-sharp fa-solid fa-left-to-line fa-fw" />
</div>
</If>
<Choose>
<When condition={containerType == appconst.LineContainer_Main}>
<div key="restart" title="Restart Command" className="line-icon" onClick={this.clickRestart}>
<i className="fa-sharp fa-regular fa-arrows-rotate fa-fw" />
</div>
<div
key="delete"
title="Delete Line (&#x2318;D)"
className="line-icon"
onClick={this.clickDelete}
>
<i className="fa-sharp fa-regular fa-trash fa-fw" />
</div>
<div
key="bookmark"
title="Bookmark"
className={cn("line-icon", "line-bookmark")}
onClick={this.clickBookmark}
>
<i className="fa-sharp fa-regular fa-bookmark fa-fw" />
</div>
<div
key="minimize"
title={`${isMinimized ? "Show Output" : "Hide Output"}`}
className={cn("line-icon", isMinimized ? "active" : "")}
onClick={this.clickMinimize}
>
<Choose>
<When condition={isMinimized}>
<i className="fa-sharp fa-regular fa-circle-plus fa-fw" />
</When>
<Otherwise>
<i className="fa-sharp fa-regular fa-circle-minus fa-fw" />
</Otherwise>
</Choose>
</div>
<div
className="line-icon line-sidebar"
onClick={this.clickMoveToSidebar}
title="Move to Sidebar"
>
<i className="fa-sharp fa-solid fa-right-to-line fa-fw" />
</div>
<div
key="settings"
title="Line Settings"
className="line-icon line-icon-shrink-left"
onClick={this.handleLineSettings}
>
<i className="fa-sharp fa-regular fa-ellipsis-vertical fa-fw" />
</div>
</When>
<When condition={containerType == appconst.LineContainer_Sidebar}>
<div
key="delete"
title="Delete Line (&#x2318;D)"
className="line-icon"
onClick={this.clickDelete}
>
<i className="fa-sharp fa-regular fa-trash fa-fw" />
</div>
<div
key="bookmark"
title="Bookmark"
className={cn("line-icon", "line-bookmark")}
onClick={this.clickBookmark}
>
<i className="fa-sharp fa-regular fa-bookmark fa-fw" />
</div>
<div
className="line-icon line-sidebar"
onClick={this.clickRemoveFromSidebar}
title="Move to Sidebar"
>
<i className="fa-sharp fa-solid fa-left-to-line fa-fw" />
</div>
</When>
</Choose>
</div>
);
}
@@ -254,9 +268,9 @@ class LineHeader extends React.Component<{ screen: LineContainerType; line: Line
}
renderMeta1(cmd: Cmd) {
let { line } = this.props;
const { line } = this.props;
let formattedTime: string = "";
let restartTs = cmd.getRestartTs();
const restartTs = cmd.getRestartTs();
let timeTitle: string = null;
if (restartTs != null && restartTs > 0) {
formattedTime = "restarted @ " + lineutil.getLineDateTimeStr(restartTs);
@@ -264,8 +278,8 @@ class LineHeader extends React.Component<{ screen: LineContainerType; line: Line
} else {
formattedTime = lineutil.getLineDateTimeStr(line.ts);
}
let renderer = line.renderer;
let durationMs = cmd.getDurationMs();
const renderer = line.renderer;
const durationMs = cmd.getDurationMs();
return (
<div key="meta1" className="meta meta-line1">
<SmallLineAvatar line={line} cmd={cmd} />
@@ -287,7 +301,7 @@ class LineHeader extends React.Component<{ screen: LineContainerType; line: Line
}
render() {
let { line, cmd } = this.props;
const { line, cmd } = this.props;
const hidePrompt = getIsHidePrompt(line);
return (
<div key="header" className={cn("line-header", { "hide-prompt": hidePrompt })}>
@@ -303,7 +317,7 @@ class SmallLineAvatar extends React.Component<{ line: LineType; cmd: Cmd; onRigh
render() {
const { line, cmd } = this.props;
const lineNumStr = (line.linenumtemp ? "~" : "#") + String(line.linenum);
let status = cmd != null ? cmd.getStatus() : "done";
const status = cmd != null ? cmd.getStatus() : "done";
const exitcode = cmd != null ? cmd.getExitCode() : 0;
const isComment = line.linetype == "text";
let icon = null;
@@ -359,7 +373,7 @@ class RtnState extends React.Component<{ cmd: Cmd; line: LineType }> {
}
checkStateDiffLoad(): void {
let cmd = this.props.cmd;
const cmd = this.props.cmd;
if (cmd == null || !cmd.getRtnState() || this.rtnStateDiffFetched) {
return;
}
@@ -406,7 +420,7 @@ class RtnState extends React.Component<{ cmd: Cmd; line: LineType }> {
}
render() {
let { cmd } = this.props;
const { cmd } = this.props;
const rsdiff = this.rtnStateDiff.get();
const termFontSize = GlobalModel.getTermFontSize();
let rtnStateDiffSize = termFontSize - 2;
@@ -488,7 +502,7 @@ class LineCmd extends React.Component<
if (elem != null) {
curHeight = elem.offsetHeight;
}
let linenum = line.linenum;
const linenum = line.linenum;
if (DebugHeightProblems && linenum >= MinLine && linenum <= MaxLine) {
heightLog[linenum] = heightLog[linenum] || {};
heightLog[linenum].heightArr = heightLog[linenum].heightArr || [];
@@ -685,7 +699,7 @@ class LineCmd extends React.Component<
const isFocused = mobx
.computed(
() => {
let screenFocusType = screen.getFocusType();
const screenFocusType = screen.getFocusType();
return isPhysicalFocused && screenFocusType == "cmd";
},
{ name: "computed-isFocused" }
@@ -694,7 +708,7 @@ class LineCmd extends React.Component<
const shouldCmdFocus = mobx
.computed(
() => {
let screenFocusType = screen.getFocusType();
const screenFocusType = screen.getFocusType();
return isSelected && screenFocusType == "cmd";
},
{ name: "computed-shouldCmdFocus" }
@@ -725,16 +739,9 @@ class LineCmd extends React.Component<
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
}
const rendererType = lineutil.getRendererType(line);
const hidePrompt = rendererPlugin?.hidePrompt;
const termFontSize = GlobalModel.getTermFontSize();
const containerType = screen.getContainerType();
const isMinimized = line.linestate["wave:min"] && containerType == appconst.LineContainer_Main;
const lhv: LineChromeHeightVars = {
numCmdLines: lineutil.countCmdLines(cmd.getCmdStr()),
zeroHeight: isMinimized,
hasLine2: !hidePrompt,
};
const chromeHeight = textmeasure.calcLineChromeHeight(GlobalModel.lineHeightEnv, lhv);
return (
<div
className={mainDivCn}
@@ -749,55 +756,64 @@ class LineCmd extends React.Component<
</If>
<LineActions screen={screen} line={line} cmd={cmd} />
<LineHeader screen={screen} line={line} cmd={cmd} />
<If condition={!isMinimized && isInSidebar}>
<div className="sidebar-message" style={{ fontSize: termFontSize }}>
&nbsp;&nbsp;showing in sidebar =&gt;
</div>
</If>
<If condition={!isMinimized && !isInSidebar}>
<ErrorBoundary plugin={rendererPlugin?.name} lineContext={lineutil.getRendererContext(line)}>
<If condition={rendererPlugin == null && !isNoneRenderer}>
<TerminalRenderer
screen={screen}
line={line}
width={width}
staticRender={staticRender}
visible={visible}
onHeightChange={this.handleHeightChange}
collapsed={false}
/>
</If>
<If condition={rendererPlugin != null && rendererPlugin.rendererType == "simple"}>
<SimpleBlobRenderer
rendererContainer={screen}
lineId={line.lineid}
plugin={rendererPlugin}
onHeightChange={this.handleHeightChange}
initParams={this.makeRendererModelInitializeParams()}
scrollToBringIntoViewport={this.scrollToBringIntoViewport}
isSelected={isSelected}
shouldFocus={shouldCmdFocus}
/>
</If>
<If condition={rendererPlugin != null && rendererPlugin.rendererType == "full"}>
<IncrementalRenderer
rendererContainer={screen}
lineId={line.lineid}
plugin={rendererPlugin}
onHeightChange={this.handleHeightChange}
initParams={this.makeRendererModelInitializeParams()}
isSelected={isSelected}
/>
</If>
</ErrorBoundary>
<If condition={isRtnState}>
<RtnState cmd={cmd} line={line} />
</If>
<If condition={isSelected && !isFocused && rendererType == "terminal"}>
<div className="cmd-hints">
<div className="hint-item color-nohover-white">focus line ({renderCmdText("L")})</div>
</div>
</If>
<If condition={!isMinimized}>
<Choose>
<When condition={isInSidebar}>
<div className="sidebar-message" style={{ fontSize: termFontSize }}>
&nbsp;&nbsp;showing in sidebar =&gt;
</div>
</When>
<Otherwise>
<ErrorBoundary
plugin={rendererPlugin?.name}
lineContext={lineutil.getRendererContext(line)}
>
<If condition={rendererPlugin == null && !isNoneRenderer}>
<TerminalRenderer
screen={screen}
line={line}
width={width}
staticRender={staticRender}
visible={visible}
onHeightChange={this.handleHeightChange}
collapsed={false}
/>
</If>
<If condition={rendererPlugin != null && rendererPlugin.rendererType == "simple"}>
<SimpleBlobRenderer
rendererContainer={screen}
lineId={line.lineid}
plugin={rendererPlugin}
onHeightChange={this.handleHeightChange}
initParams={this.makeRendererModelInitializeParams()}
scrollToBringIntoViewport={this.scrollToBringIntoViewport}
isSelected={isSelected}
shouldFocus={shouldCmdFocus}
/>
</If>
<If condition={rendererPlugin != null && rendererPlugin.rendererType == "full"}>
<IncrementalRenderer
rendererContainer={screen}
lineId={line.lineid}
plugin={rendererPlugin}
onHeightChange={this.handleHeightChange}
initParams={this.makeRendererModelInitializeParams()}
isSelected={isSelected}
/>
</If>
</ErrorBoundary>
<If condition={isRtnState}>
<RtnState cmd={cmd} line={line} />
</If>
<If condition={isSelected && !isFocused && rendererType == "terminal"}>
<div className="cmd-hints">
<div className="hint-item color-nohover-white">
focus line ({renderCmdText("L")})
</div>
</div>
</If>
</Otherwise>
</Choose>
</If>
</div>
);
@@ -877,11 +893,6 @@ class LineText extends React.Component<
name: "computed-isSelected",
})
.get();
const isFocused = mobx
.computed(() => screen.getFocusType() == "cmd", {
name: "computed-isFocused",
})
.get();
const mainClass = cn("line", "line-text", "focus-parent", { selected: isSelected });
return (
<div
+1
View File
@@ -15,6 +15,7 @@ import * as util from "@/util/util";
import * as lineutil from "./lineutil";
import "./lines.less";
import { GlobalModel } from "@/models";
dayjs.extend(localizedFormat);
+1 -1
View File
@@ -242,7 +242,7 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
}
render() {
let mainView = GlobalModel.activeMainView.get();
const mainView = GlobalModel.activeMainView.get();
const historyActive = mainView == "history";
const connectionsActive = mainView == "connections";
const settingsActive = mainView == "clientsettings";
+25
View File
@@ -25,4 +25,29 @@
&.collapsed {
display: none;
}
.keybind-debug-pane {
padding: 10px;
overflow: hidden;
width: 100%;
.keybind-pane-title {
font-size: 18px;
font-weight: bold;
padding-bottom: 5px;
}
.keybind-level {
margin-top: 10px;
font-weight: bold;
font-size: 16px;
}
.keybind-domain {
font-size: 14px;
margin-left: 20px;
white-space: nowrap;
overflow: hidden;
}
}
}
+39
View File
@@ -4,6 +4,7 @@
import * as React from "react";
import * as mobxReact from "mobx-react";
import dayjs from "dayjs";
import { If, For } from "tsx-control-statements/components";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "@/models";
@@ -17,6 +18,41 @@ interface RightSideBarProps {
parentRef: React.RefObject<HTMLElement>;
}
@mobxReact.observer
class KeybindDevPane extends React.Component<{}, {}> {
render() {
let curActiveKeybinds: Array<{ name: string; domains: Array<string> }> =
GlobalModel.keybindManager.getActiveKeybindings();
let keybindLevel: { name: string; domains: Array<string> } = null;
let domain: string = null;
let curVersion = GlobalModel.keybindManager.getActiveKeybindsVersion().get();
let levelIdx: number = 0;
let domainIdx: number = 0;
let lastKeyData = GlobalModel.keybindManager.getLastKeyData();
return (
<div className="keybind-debug-pane">
<div className="keybind-pane-title">Keybind Manager</div>
<For index="levelIdx" each="keybindLevel" of={curActiveKeybinds}>
<div className="keybind-level" key={"level-" + curVersion + levelIdx}>
{keybindLevel.name}
</div>
<For index="domainIdx" each="domain" of={keybindLevel.domains}>
<div className="keybind-domain" key={"domain-" + curVersion + domainIdx}>
{domain}
</div>
</For>
</For>
<br />
<br />
<div>
<h1>Last KeyPress Domain: {lastKeyData.domain}</h1>
<h1>Last KeyPress key: {lastKeyData.keyPress}</h1>
</div>
</div>
);
}
}
@mobxReact.observer
class RightSideBar extends React.Component<RightSideBarProps, {}> {
render() {
@@ -35,6 +71,9 @@ class RightSideBar extends React.Component<RightSideBarProps, {}> {
<i className="fa-sharp fa-regular fa-xmark"></i>
</Button>
</div>
<If condition={GlobalModel.isDev}>
<KeybindDevPane></KeybindDevPane>
</If>
</React.Fragment>
)}
</ResizableSidebar>
+70
View File
@@ -0,0 +1,70 @@
.cmd-aichat {
padding-bottom: 0 !important;
.auxview-content {
flex-flow: column nowrap;
height: 100%;
.chat-window {
overflow-y: auto;
margin-bottom: 5px;
flex: 1 1 auto;
flex-direction: column-reverse;
}
.chat-input {
padding: 0.5em 0.5em 0.5em 0.5em;
flex: 1 0 auto;
.chat-textarea {
color: var(--app-text-primary-color);
background-color: var(--cmdinput-textarea-bg);
resize: none;
width: 100%;
border: transparent;
outline: none;
overflow: auto;
overflow-wrap: anywhere;
font-family: var(--termfontfamily);
font-weight: normal;
line-height: var(--termlineheight);
}
}
.chat-msg {
margin-top: calc(var(--termpad) * 2);
margin-bottom: calc(var(--termpad) * 2);
.chat-msg-header {
display: flex;
margin-bottom: 2px;
i {
margin-right: 0.5em;
}
.chat-username {
font-weight: bold;
margin-right: 5px;
}
}
}
.chat-msg-assistant {
color: var(--app-text-color);
}
.chat-msg-user {
.msg-text {
font-family: var(--markdown-font);
font-size: 14px;
white-space: pre-wrap;
}
}
.chat-msg-error {
color: var(--cmdinput-text-error);
font-family: var(--markdown-font);
font-size: 14px;
}
}
}
+87 -88
View File
@@ -5,18 +5,20 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { GlobalModel } from "@/models";
import { isBlank } from "@/util/util";
import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { If, For } from "tsx-control-statements/components";
import { Markdown } from "@/elements";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
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 {
let AIChatObject = this.props.AIChatObject;
let keybindManager = GlobalModel.keybindManager;
let inputModel = GlobalModel.inputModel;
const AIChatObject = this.props.AIChatObject;
const keybindManager = GlobalModel.keybindManager;
const inputModel = GlobalModel.inputModel;
keybindManager.registerKeybinding("pane", "aichat", "generic:confirm", (waveEvent) => {
AIChatObject.onEnterKeyPressed();
@@ -54,10 +56,10 @@ class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> {
@mobxReact.observer
class AIChat extends React.Component<{}, {}> {
chatListKeyCount: number = 0;
textAreaNumLines: mobx.IObservableValue<number> = mobx.observable.box(1, { name: "textAreaNumLines" });
chatWindowScrollRef: React.RefObject<HTMLDivElement>;
textAreaRef: React.RefObject<HTMLTextAreaElement>;
isFocused: OV<boolean>;
termFontSize: number = 14;
constructor(props: any) {
super(props);
@@ -69,9 +71,8 @@ class AIChat extends React.Component<{}, {}> {
}
componentDidMount() {
let model = GlobalModel;
let inputModel = model.inputModel;
if (this.chatWindowScrollRef != null && this.chatWindowScrollRef.current != null) {
const inputModel = GlobalModel.inputModel;
if (this.chatWindowScrollRef?.current != null) {
this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight;
}
if (this.textAreaRef.current != null) {
@@ -79,10 +80,11 @@ class AIChat extends React.Component<{}, {}> {
inputModel.setCmdInfoChatRefs(this.textAreaRef, this.chatWindowScrollRef);
}
this.requestChatUpdate();
this.onTextAreaChange(null);
}
componentDidUpdate() {
if (this.chatWindowScrollRef != null && this.chatWindowScrollRef.current != null) {
if (this.chatWindowScrollRef?.current != null) {
this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight;
}
}
@@ -92,20 +94,18 @@ class AIChat extends React.Component<{}, {}> {
}
submitChatMessage(messageStr: string) {
let model = GlobalModel;
let inputModel = model.inputModel;
let curLine = inputModel.getCurLine();
let prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
const curLine = GlobalModel.inputModel.getCurLine();
const prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
prtn.then((rtn) => {
if (!rtn.success) {
console.log("submit chat command error: " + rtn.error);
}
}).catch((error) => {});
}).catch((_) => {});
}
getLinePos(elem: any): { numLines: number; linePos: number } {
let numLines = elem.value.split("\n").length;
let linePos = elem.value.substr(0, elem.selectionStart).split("\n").length;
const numLines = elem.value.split("\n").length;
const linePos = elem.value.substr(0, elem.selectionStart).split("\n").length;
return { numLines, linePos };
}
@@ -121,22 +121,32 @@ class AIChat extends React.Component<{}, {}> {
})();
}
// Adjust the height of the textarea to fit the text
onTextAreaChange(e: any) {
// set height of textarea based on number of newlines
mobx.action(() => {
this.textAreaNumLines.set(e.target.value.split(/\n/).length);
GlobalModel.inputModel.codeSelectDeselectAll();
})();
// Calculate the bounding height of the text area
const textAreaMaxLines = 4;
const textAreaLineHeight = this.termFontSize * 1.5;
const textAreaMinHeight = textAreaLineHeight;
const textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines;
// Get the height of the wrapped text area content. Courtesy of https://stackoverflow.com/questions/995168/textarea-to-resize-based-on-content-length
this.textAreaRef.current.style.height = "1px";
const scrollHeight: number = this.textAreaRef.current.scrollHeight;
// Set the new height of the text area, bounded by the min and max height.
const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight);
this.textAreaRef.current.style.height = newHeight + "px";
GlobalModel.inputModel.codeSelectDeselectAll();
}
onEnterKeyPressed() {
let inputModel = GlobalModel.inputModel;
let currentRef = this.textAreaRef.current;
const inputModel = GlobalModel.inputModel;
const currentRef = this.textAreaRef.current;
if (currentRef == null) {
return;
}
if (inputModel.getCodeSelectSelectedIndex() == -1) {
let messageStr = currentRef.value;
const messageStr = currentRef.value;
this.submitChatMessage(messageStr);
currentRef.value = "";
} else {
@@ -145,7 +155,7 @@ class AIChat extends React.Component<{}, {}> {
}
onExpandInputPressed() {
let currentRef = this.textAreaRef.current;
const currentRef = this.textAreaRef.current;
if (currentRef == null) {
return;
}
@@ -154,7 +164,7 @@ class AIChat extends React.Component<{}, {}> {
}
onArrowUpPressed(): boolean {
let currentRef = this.textAreaRef.current;
const currentRef = this.textAreaRef.current;
if (currentRef == null) {
return false;
}
@@ -168,8 +178,8 @@ class AIChat extends React.Component<{}, {}> {
}
onArrowDownPressed(): boolean {
let currentRef = this.textAreaRef.current;
let inputModel = GlobalModel.inputModel;
const currentRef = this.textAreaRef.current;
const inputModel = GlobalModel.inputModel;
if (currentRef == null) {
return false;
}
@@ -190,10 +200,10 @@ class AIChat extends React.Component<{}, {}> {
}
renderChatMessage(chatItem: OpenAICmdInfoChatMessageType): any {
let curKey = "chatmsg-" + this.chatListKeyCount;
const curKey = "chatmsg-" + this.chatListKeyCount;
this.chatListKeyCount++;
let senderClassName = chatItem.isassistantresponse ? "chat-msg-assistant" : "chat-msg-user";
let msgClassName = "chat-msg " + senderClassName;
const senderClassName = chatItem.isassistantresponse ? "chat-msg-assistant" : "chat-msg-user";
const msgClassName = "chat-msg " + senderClassName;
let innerHTML: React.JSX.Element = (
<span>
<div className="chat-msg-header">
@@ -226,68 +236,57 @@ class AIChat extends React.Component<{}, {}> {
);
}
renderChatWindow(): any {
let model = GlobalModel;
let inputModel = model.inputModel;
let chatMessageItems = inputModel.AICmdInfoChatItems.slice();
let chitem: OpenAICmdInfoChatMessageType = null;
return (
<div className="chat-window" ref={this.chatWindowScrollRef}>
<For each="chitem" index="idx" of={chatMessageItems}>
{this.renderChatMessage(chitem)}
</For>
</div>
);
}
render() {
let model = GlobalModel;
let inputModel = model.inputModel;
const termFontSize = 14;
const textAreaMaxLines = 4;
const textAreaLineHeight = termFontSize * 1.5;
const textAreaPadding = 2 * 0.5 * termFontSize;
let textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines + textAreaPadding;
let textAreaInnerHeight = this.textAreaNumLines.get() * textAreaLineHeight + textAreaPadding;
let isFocused = this.isFocused.get();
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
const chitem: OpenAICmdInfoChatMessageType = null;
const renderKeybindings = mobx
.computed(() => {
return (
this.isFocused.get() ||
GlobalModel.inputModel.hasFocus() ||
(GlobalModel.getActiveScreen().getFocusType() == "input" &&
GlobalModel.activeMainView.get() == "session")
);
})
.get();
return (
<div className="cmd-aichat">
<If condition={isFocused}>
<AuxiliaryCmdView
title="Wave AI"
className="cmd-aichat"
onClose={() => GlobalModel.inputModel.closeAIAssistantChat(true)}
iconClass="fa-sharp fa-solid fa-sparkles"
>
<If condition={renderKeybindings}>
<AIChatKeybindings AIChatObject={this}></AIChatKeybindings>
</If>
<div className="cmdinput-titlebar">
<div className="title-icon">
<i className="fa-sharp fa-solid fa-sparkles" />
</div>
<div className="title-string">Wave AI</div>
<div className="flex-spacer"></div>
<div
className="close-button"
title="Close (ESC)"
onClick={() => inputModel.closeAIAssistantChat(true)}
>
<i className="fa-sharp fa-solid fa-xmark-large" />
<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"
ref={this.textAreaRef}
autoComplete="off"
autoCorrect="off"
id="chat-cmd-input"
onFocus={this.onTextAreaFocused.bind(this)}
onBlur={this.onTextAreaBlur.bind(this)}
onChange={this.onTextAreaChange.bind(this)}
onKeyDown={this.onKeyDown}
style={{ fontSize: this.termFontSize }}
className="chat-textarea"
placeholder="Send a Message..."
></textarea>
</div>
<div className="titlebar-spacer" />
{this.renderChatWindow()}
<textarea
key="main"
ref={this.textAreaRef}
autoComplete="off"
autoCorrect="off"
id="chat-cmd-input"
onFocus={this.onTextAreaFocused.bind(this)}
onBlur={this.onTextAreaBlur.bind(this)}
onChange={this.onTextAreaChange.bind(this)}
onKeyDown={this.onKeyDown}
style={{ height: textAreaInnerHeight, maxHeight: textAreaMaxHeight, fontSize: termFontSize }}
className={cn("chat-textarea")}
placeholder="Send a Message..."
></textarea>
</div>
</AuxiliaryCmdView>
);
}
}
+51
View File
@@ -0,0 +1,51 @@
// For the additonal views, we want less padding on the top and bottom than we want for the base-cmdinput div
.auxview {
padding: var(--termpad) calc(var(--termpad) * 2) var(--termpad) calc(var(--termpad) * 3 - 1px);
overflow: auto;
flex-shrink: 1;
width: 100%;
--auxview-titlebar-height: 18px;
.auxview-titlebar {
position: absolute;
z-index: 22;
top: 0;
left: 0;
background-color: var(--app-panel-bg-color);
color: var(--term-blue);
padding: 6px 10px 6px 10px;
display: flex;
flex-direction: row;
width: 100%;
border-bottom: 1px solid var(--app-border-color);
font: var(--base-font);
user-select: none;
cursor: default;
line-height: var(--auxview-titlebar-height);
.title-string {
font-weight: bold;
}
.close-button {
cursor: pointer;
i {
color: var(--app-icon-color);
&:hover {
color: var(--app-icon-hover-color);
}
}
}
div:not(.close-button, .flex-spacer) {
margin-right: 10px;
}
}
.auxview-content {
display: flex;
padding-top: calc(var(--auxview-titlebar-height) + 6px);
}
}

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