cmdinput actions (filter, ai, and history) (#395)

* clientsettings fixups -- border top, padding, and remove theme dropdown for prod

* new cmdinput actions, get the filter commands showing, titlebars for info history and info aichat, toggles for history
This commit is contained in:
Mike Sawka
2024-03-06 19:15:51 -08:00
committed by GitHub
parent 3a119ae575
commit 29acdc6eff
12 changed files with 257 additions and 93 deletions
+1
View File
@@ -46,6 +46,7 @@
/* global colors */
--app-bg-color: black;
--app-accent-color: rgb(88, 193, 66);
--app-accent-bg-color: rgba(88, 193, 66, 0.2);
--app-error-color: rgb(204, 0, 0);
--app-warning-color: rgb(255, 165, 0);
--app-success-color: rgb(38, 97, 26);
+2 -1
View File
@@ -1,6 +1,7 @@
.clientsettings-view {
.content {
padding: 0 18px 0 30px;
padding: 14px 18px 0 30px;
border-top: 1px solid var(--app-border-color);
}
.wave-dropdown {
+13 -10
View File
@@ -5,6 +5,7 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, RemotesModel, getApi } from "@/models";
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "@/common/elements";
@@ -191,17 +192,19 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Theme</div>
<div className="settings-input">
<Dropdown
className="theme-dropdown"
options={this.getThemes()}
defaultValue={curTheme}
onChange={this.handleChangeTheme}
/>
<If condition={GlobalModel.isDev}>
<div className="settings-field">
<div className="settings-label">Theme</div>
<div className="settings-input">
<Dropdown
className="theme-dropdown"
options={this.getThemes()}
defaultValue={curTheme}
onChange={this.handleChangeTheme}
/>
</div>
</div>
</div>
</If>
<div className="settings-field">
<div className="settings-label">Client ID</div>
<div className="settings-input">{cdata.clientid}</div>
+1 -1
View File
@@ -171,7 +171,7 @@
}
&:hover {
color: var(--line-actions-active-color);
color: var(--app-icon-hover-color);
}
padding: 4px 4px;
+18 -3
View File
@@ -152,8 +152,8 @@ class AIChat extends React.Component<{}, {}> {
innerHTML = (
<span>
<div className="chat-msg-header">
<i className="fa-sharp fa-solid fa-headset"></i>
<div className="chat-username">ChatGPT</div>
<i className="fa-sharp fa-solid fa-sparkles"></i>
<div className="chat-username">AI Assistant</div>
</div>
<Markdown text={chatItem.assistantresponse.message} codeSelect />
</span>
@@ -195,6 +195,21 @@ class AIChat extends React.Component<{}, {}> {
return (
<div className="cmd-aichat">
<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" />
</div>
</div>
<div className="titlebar-spacer" />
{this.renderChatWindow()}
<textarea
key="main"
@@ -205,7 +220,7 @@ class AIChat extends React.Component<{}, {}> {
onKeyDown={this.onKeyDown}
style={{ height: textAreaInnerHeight, maxHeight: textAreaMaxHeight, fontSize: termFontSize }}
className={cn("chat-textarea")}
placeholder="Send a Message to ChatGPT..."
placeholder="Send a Message..."
></textarea>
</div>
);
+105 -29
View File
@@ -13,6 +13,99 @@
&.active {
}
&.has-info,
&.has-aichat,
&.has-history {
.cmdinput-actions {
display: none;
}
}
.titlebar-spacer {
height: 31px;
}
.cmdinput-actions {
position: absolute;
border-radius: 4px;
padding-left: 4px;
padding-right: 4px;
font-size: calc(var(--termfontsize) + 2px);
line-height: 1.2;
// we want to align to 2nd line of meta. that's 2xPad + 1xLineHightSm
// height of actions is 1xLineHeight + 8px (2x2px padding on icons)
top: calc(var(--termpad));
right: calc(var(--termpad) * 2);
display: flex;
flex-direction: row;
align-items: center;
.cmdinput-icon {
color: var(--app-icon-hover-color);
opacity: 0.5;
&.running-cmds i {
color: var(--app-warning-color);
}
&.active {
opacity: 1;
}
&:hover {
opacity: 1;
}
padding: 4px 6px;
cursor: pointer;
}
.line-icon + .line-icon:not(.line-icon-shrink-left) {
margin-left: 3px;
}
}
.cmdinput-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%;
overflow-x: auto;
border-bottom: 1px solid var(--app-border-color);
font: var(--base-font);
.title-icon {
margin-right: 10px;
}
.title-string {
font-weight: bold;
}
.close-button {
cursor: pointer;
i {
color: var(--app-icon-color);
&:hover {
color: var(--app-icon-hover-color);
}
}
}
.spacer {
flex: 0 0 10px;
}
}
&.has-info {
padding-top: var(--termpad);
}
@@ -23,23 +116,20 @@
left: 4px;
}
&.has-history {
&.has-history,
&.has-aichat {
padding-top: var(--termpad);
height: max(300px, 40%);
height: max(300px, 70%);
}
&.has-remote {
max-height: max(300px, 70%);
}
&.has-aichat {
max-height: max(300px, 70%);
}
.remote-status-warning {
display: flex;
flex-direction: row;
color: var(--term-yellow);
color: var(--app-warning-color);
align-items: center;
.button {
@@ -300,19 +390,6 @@
flex-shrink: 1;
.history-title {
position: absolute;
z-index: 102;
top: 5px;
left: 0;
background-color: var(--app-bg-color);
color: var(--term-blue);
padding-bottom: 4px;
display: flex;
flex-direction: row;
width: 100%;
overflow-x: auto;
border-bottom: 1px solid var(--app-border-color);
div:first-child {
margin-left: var(--termpad);
}
@@ -321,22 +398,21 @@
white-space: nowrap;
}
.history-clickable-opt {
cursor: pointer;
&:hover {
color: var(--app-text-primary-color);
}
}
.history-clickable-opt {
white-space: nowrap;
cursor: pointer;
}
.grow-spacer {
flex: 1 0 10px;
}
.spacer {
flex: 0 0 10px;
}
}
.history-items {
margin-top: 24px;
color: var(--app-text-color);
padding-bottom: 6px;
+27 -10
View File
@@ -70,7 +70,7 @@ class CmdInput extends React.Component<{}, {}> {
}
@boundMethod
clickAIHint(e: any): void {
clickAIAction(e: any): void {
e.preventDefault();
e.stopPropagation();
let inputModel = GlobalModel.inputModel;
@@ -78,7 +78,7 @@ class CmdInput extends React.Component<{}, {}> {
}
@boundMethod
clickHistoryHint(e: any): void {
clickHistoryAction(e: any): void {
e.preventDefault();
e.stopPropagation();
@@ -131,6 +131,7 @@ class CmdInput extends React.Component<{}, {}> {
const inputMode: string = inputModel.inputMode.get();
const textAreaInputKey = screen == null ? "null" : screen.screenId;
const win = GlobalModel.getScreenLinesById(screen.screenId);
const filterRunning = screen.filterRunning.get();
let numRunningLines = 0;
if (win != null) {
numRunningLines = mobx.computed(() => win.getRunningCmdLines().length).get();
@@ -142,9 +143,33 @@ class CmdInput extends React.Component<{}, {}> {
"cmd-input",
{ "has-info": infoShow },
{ "has-aichat": aiChatShow },
{ "has-history": historyShow },
{ active: focusVal }
)}
>
<div className="cmdinput-actions">
<If condition={numRunningLines > 0}>
<div
key="running"
className={cn("cmdinput-icon", "running-cmds", { active: filterRunning })}
title="Filter for Running Commands"
onClick={() => this.toggleFilter(screen)}
>
{numRunningLines} <i className="fa-sharp fa-regular fa-rotate fa-spin fa-fw" />
</div>
</If>
<div key="ai" title="Wave AI (Cmd-Space)" className="cmdinput-icon" onClick={this.clickAIAction}>
<i className="fa-sharp fa-regular fa-sparkles fa-fw" />
</div>
<div
key="history"
title="Tab History (Ctrl-R)"
className="cmdinput-icon"
onClick={this.clickHistoryAction}
>
<i className="fa-sharp fa-regular fa-clock-rotate-left fa-fw" />
</div>
</div>
<If condition={historyShow}>
<div className="cmd-input-grow-spacer"></div>
<HistoryInfo />
@@ -188,14 +213,6 @@ class CmdInput extends React.Component<{}, {}> {
<Prompt rptr={rptr} festate={feState} color={true} />
</span>
</div>
<If condition={numRunningLines > 0}>
<div onClick={() => this.toggleFilter(screen)} className="cmd-input-filter">
{numRunningLines}
<div className="avatar">
<RotateIcon className="warning spin" />
</div>
</div>
</If>
</div>
<div
key="input"
+31 -10
View File
@@ -189,6 +189,18 @@ class HistoryInfo extends React.Component<{}, {}> {
}, 3000);
}
@boundMethod
handleClickType() {
let inputModel = GlobalModel.inputModel;
inputModel.toggleHistoryType();
}
@boundMethod
handleClickRemote() {
let inputModel = GlobalModel.inputModel;
inputModel.toggleRemoteType();
}
render() {
let inputModel = GlobalModel.inputModel;
let idx: number = 0;
@@ -207,19 +219,27 @@ class HistoryInfo extends React.Component<{}, {}> {
}
return (
<div className="cmd-history hide-scrollbar">
<div className="history-title">
<div className="history-label">history</div>
<div className="cmdinput-titlebar history-title">
<div className="title-icon">
<i className="fa-sharp fa-solid fa-clock-rotate-left" />
</div>
<div className="title-string">History</div>
<div className="spacer"></div>
<div className="history-opt">[for {opts.queryType} &#x2318;S]</div>
<div className="spacer"></div>
<div className="history-opt">[containing '{opts.queryStr}']</div>
<div className="spacer"></div>
<div className="history-opt">[{opts.limitRemote ? "this" : "any"} remote &#x2318;R]</div>
<div className="grow-spacer"></div>
<div className="history-clickable-opt" onClick={this.handleClose}>
(ESC)
<div className="history-opt history-clickable-opt" onClick={this.handleClickType}>
[for {opts.queryType} &#x2318;S]
</div>
<div className="spacer"></div>
<div className="history-opt" title="type to search">
[containing '{opts.queryStr}']
</div>
<div className="spacer"></div>
<div className="history-opt history-clickable-opt" onClick={this.handleClickRemote}>
[{opts.limitRemote ? "this" : "any"} remote &#x2318;R]
</div>
<div className="flex-spacer"></div>
<div className="close-button" title="Close (ESC)">
<i className="fa-sharp fa-solid fa-xmark-large" onClick={this.handleClose}></i>
</div>
</div>
<div
className={cn(
@@ -228,6 +248,7 @@ class HistoryInfo extends React.Component<{}, {}> {
{ "show-sessions": opts.queryType == "global" }
)}
>
<div className="titlebar-spacer" />
<If condition={hitems.length == 0}>[no history]</If>
<If condition={hitems.length > 0}>
<For each="hitem" index="idx" of={hitems}>
+4 -19
View File
@@ -362,29 +362,14 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
}
if (checkKeyPressed(waveEvent, "Cmd:r") || checkKeyPressed(waveEvent, "Ctrl:r")) {
e.preventDefault();
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
if (opts.limitRemote) {
opts.limitRemote = false;
opts.limitRemoteInstance = false;
} else {
opts.limitRemote = true;
opts.limitRemoteInstance = true;
}
inputModel.setHistoryQueryOpts(opts);
e.stopPropagation();
inputModel.toggleRemoteType();
return;
}
if (checkKeyPressed(waveEvent, "Cmd:s") || checkKeyPressed(waveEvent, "Ctrl:s")) {
e.preventDefault();
let opts = mobx.toJS(inputModel.historyQueryOpts.get());
let htype = opts.queryType;
if (htype == "screen") {
htype = "session";
} else if (htype == "session") {
htype = "global";
} else {
htype = "screen";
}
inputModel.setHistoryType(htype);
e.stopPropagation();
inputModel.toggleHistoryType();
return;
}
if (checkKeyPressed(waveEvent, "Tab")) {
+26 -2
View File
@@ -181,8 +181,32 @@
}
.filter-running {
margin: auto 1rem 0 1rem;
align-self: center;
position: relative;
display: flex;
flex-direction: row;
width: 100%;
border-top: 1px solid var(--app-border-color);
padding: calc(var(--termpad) + 2px) var(--termpad) calc(var(--termpad) + 2px) var(--termpad);
align-items: center;
justify-content: center;
.filter-content {
cursor: pointer;
padding: 2px;
color: var(--app-text-primary-color);
z-index: 2;
}
.filter-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--app-accent-bg-color);
z-index: 1;
pointer-events: none;
}
}
}
+4 -8
View File
@@ -763,15 +763,11 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
</If>
<If condition={screen.filterRunning.get()}>
<div className="filter-running">
<Button
variant="outlined"
color="color-yellow"
style={{ borderRadius: "999px" }}
onClick={this.disableFilter}
>
<div className="filter-mask" />
<div className="filter-content" onClick={this.disableFilter}>
Showing Running Commands &nbsp;
<i className="fa-sharp fa-solid fa-xmark" />
</Button>
<i className="fa-sharp fa-solid fa-xmark-large" />
</div>
</div>
</If>
</div>
+25
View File
@@ -88,6 +88,31 @@ class InputModel {
})();
}
toggleHistoryType(): void {
let opts = mobx.toJS(this.historyQueryOpts.get());
let htype = opts.queryType;
if (htype == "screen") {
htype = "session";
} else if (htype == "session") {
htype = "global";
} else {
htype = "screen";
}
this.setHistoryType(htype);
}
toggleRemoteType(): void {
let opts = mobx.toJS(this.historyQueryOpts.get());
if (opts.limitRemote) {
opts.limitRemote = false;
opts.limitRemoteInstance = false;
} else {
opts.limitRemote = true;
opts.limitRemoteInstance = true;
}
this.setHistoryQueryOpts(opts);
}
onInputFocus(isFocused: boolean): void {
mobx.action(() => {
if (isFocused) {