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
This commit is contained in:
Evan Simkowitz 2024-04-04 19:29:43 -07:00 committed by GitHub
parent 0fe767cdf3
commit 1c23701181
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 724 additions and 795 deletions

View File

@ -42,16 +42,19 @@
--app-bg-color: black; --app-bg-color: black;
--app-accent-color: rgb(88, 193, 66); --app-accent-color: rgb(88, 193, 66);
--app-accent-bg-color: rgba(88, 193, 66, 0.25); --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-color: rgb(211, 215, 207);
--app-text-primary-color: rgb(255, 255, 255); --app-text-primary-color: rgb(255, 255, 255);
--app-text-secondary-color: rgb(195, 200, 194); --app-text-secondary-color: rgb(195, 200, 194);
--app-text-disabled-color: rgb(173, 173, 173); --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-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: rgba(21, 23, 21, 1);
--app-panel-bg-color-dev: rgb(21, 23, 48); --app-panel-bg-color-dev: rgb(21, 23, 48);
--app-icon-color: rgb(139, 145, 138); --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); --app-selected-mask-color: rgba(255, 255, 255, 0.06);
/* global status colors */ /* global status colors */
@ -122,18 +125,18 @@
/* line colors */ /* line colors */
--line-sidebar-message-color: rgb(196, 160, 0); --line-sidebar-message-color: rgb(196, 160, 0);
--line-background: rgba(21, 23, 21, 1); --line-background: var(--app-panel-bg-color);
--line-avatar-color: #eceeec; --line-avatar-color: rgb(236, 238, 236);
--line-text-color: rgb(211, 215, 207); --line-text-color: rgb(211, 215, 207);
--line-svg-fill-color: rgb(150, 152, 150); --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-separator-color: rgb(126, 126, 126);
--line-error-color: var(--app-error-color); --line-error-color: var(--app-error-color);
--line-warning-color: var(--app-warning-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-active-border-color: var(--app-accent-color);
--line-selected-bg-color: rgba(255, 255, 255, 0.05); --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-border-color: rgba(204, 0, 0, 0.8);
--line-selected-error-bg-color: rgb(19, 4, 3); --line-selected-error-bg-color: rgb(19, 4, 3);
--line-error-bg-color: rgba(200, 0, 0, 0.1); --line-error-bg-color: rgba(200, 0, 0, 0.1);
@ -152,22 +155,21 @@
/* table colors */ /* table colors */
--table-border-color: rgba(241, 246, 243, 0.15); --table-border-color: rgba(241, 246, 243, 0.15);
--table-thead-border-top-color: rgba(250, 250, 250, 0.1); --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-thead-bg-color: rgba(250, 250, 250, 0.02);
--table-tr-border-bottom-color: rgba(241, 246, 243, 0.15); --table-tr-border-bottom-color: rgba(241, 246, 243, 0.15);
--table-tr-hover-bg-color: rgba(255, 255, 255, 0.06); --table-tr-hover-bg-color: rgba(255, 255, 255, 0.06);
--table-tr-selected-bg-color: #222; --table-tr-selected-bg-color: rgb(34, 34, 34);
--table-tr-selected-hover-bg-color: #333; --table-tr-selected-hover-bg-color: var(--app-maincontent-bg-color);
/* cmdinput colors */ /* cmdinput colors */
--cmdinput-textarea-bg-color: #171717; --cmdinput-bg-color: var(--app-text-bg-color);
--cmdinput-text-error-color: var(--term-red); --cmdinput-text-error-color: var(--term-red);
--cmdinput-history-item-error-color: var(--term-bright-red); --cmdinput-history-item-error-color: var(--term-bright-red);
--cmdinput-history-item-selected-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-button-bg-color: var(--tab-green);
--cmdinput-comment-button-bg-color: rgb(57, 113, 255); --cmdinput-disabled-bg-color: var(--app-text-bg-disabled-color);
--cmdinput-disabled-icon-color: rgb(76, 81, 75, 1); --cmdinput-history-bg-color: var(--app-bg-color);
--cmdinput-history-bg-color: rgb(21, 23, 21, 1);
/* screen view color */ /* screen view color */
--screen-view-text-caption-color: rgb(139, 145, 138); --screen-view-text-caption-color: rgb(139, 145, 138);

View File

@ -5,20 +5,20 @@
@import url("./term-light.css"); @import url("./term-light.css");
:root { :root {
--app-bg-color: #fefefe; --app-bg-color: rgb(254, 254, 254);
--app-accent-color: rgb(75, 166, 57); --app-accent-color: rgb(75, 166, 57);
--app-accent-bg-color: rgba(75, 166, 57, 0.2); --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-primary-color: rgb(0, 0, 0, 0.9);
--app-text-secondary-color: rgb(0, 0, 0, 0.7); --app-text-secondary-color: rgb(0, 0, 0, 0.7);
--app-border-color: rgb(139 145 138); --app-border-color: rgb(139 145 138);
--app-panel-bg-color: #e0e0e0; --app-panel-bg-color: rgb(224, 224, 224);
--app-panel-bg-color-dev: #e0e0e0; --app-panel-bg-color-dev: rgb(224, 224, 224);
--app-icon-color: rgb(80, 80, 80); --app-icon-color: rgb(110, 110, 110);
--app-icon-hover-color: rgb(100, 100, 100); --app-icon-hover-color: rgb(80, 80, 80);
--app-selected-mask-color: rgba(0, 0, 0, 0.06); --app-selected-mask-color: rgba(0, 0, 0, 0.06);
--input-bg-color: #eeeeee; --input-bg-color: rgb(238, 238, 238);
/* tab color */ /* tab color */
--tab-white: rgb(0, 0, 0, 0.6); --tab-white: rgb(0, 0, 0, 0.6);
@ -30,8 +30,8 @@
--table-thead-bg-color: rgba(250, 250, 250, 0.15); --table-thead-bg-color: rgba(250, 250, 250, 0.15);
--table-tr-border-bottom-color: rgba(0, 0, 0, 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-hover-bg-color: rgba(0, 0, 0, 0.15);
--table-tr-selected-bg-color: #dddddd; --table-tr-selected-bg-color: rgb(221, 221, 221);
--table-tr-selected-hover-bg-color: #cccccc; --table-tr-selected-hover-bg-color: rgb(204, 204, 204);
/* form colors */ /* form colors */
--form-element-border-color: rgba(0, 0, 0, 0.3); --form-element-border-color: rgba(0, 0, 0, 0.3);
@ -41,8 +41,8 @@
--form-element-label-color: rgba(0, 0, 0, 0.6); --form-element-label-color: rgba(0, 0, 0, 0.6);
--form-element-secondary-color: rgba(0, 0, 0, 0.09); --form-element-secondary-color: rgba(0, 0, 0, 0.09);
--form-element-icon-color: rgb(0, 0, 0, 0.6); --form-element-icon-color: rgb(0, 0, 0, 0.6);
--form-element-disabled-text-color: #b7b7b7; --form-element-disabled-text-color: rgb(183, 183, 183);
--form-element-placeholder-color: #b7b7b7; --form-element-placeholder-color: rgb(183, 183, 183);
--markdown-bg-color: rgb(0, 0, 0, 0.1); --markdown-bg-color: rgb(0, 0, 0, 0.1);
@ -50,8 +50,6 @@
--modal-header-bottom-border-color: rgba(0, 0, 0, 0.3); --modal-header-bottom-border-color: rgba(0, 0, 0, 0.3);
/* cmd input */ /* cmd input */
--cmdinput-textarea-bg-color: rgba(0, 0, 0, 0.1);
--cmdinput-textarea-border-color: var(--form-element-border-color);
/* scroll colors */ /* scroll colors */
--scrollbar-background-color: var(--app-bg-color); --scrollbar-background-color: var(--app-bg-color);
@ -64,15 +62,15 @@
--line-actions-inactive-color: rgba(0, 0, 0, 0.3); --line-actions-inactive-color: rgba(0, 0, 0, 0.3);
--line-actions-active-color: rgba(0, 0, 0, 1); --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); --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-cell-other-text-color: rgba(0, 0, 0, 0.3);
--datepicker-header-fade-color: rgba(0, 0, 0, 0.4); --datepicker-header-fade-color: rgba(0, 0, 0, 0.4);
--datepicker-year-header-bg-color: #f5f5f5; --datepicker-year-header-bg-color: rgb(245, 245, 245);
--datepicker-year-header-border-color: #dcdcdc; --datepicker-year-header-border-color: rgb(220, 220, 220);
/* toggle colors */ /* toggle colors */
--toggle-thumb-color: var(--app-bg-color); --toggle-thumb-color: var(--app-bg-color);

View File

@ -106,11 +106,6 @@
} }
} }
&.top-border {
border-top: 1px solid #777;
padding: 10px;
}
&:hover .meta .termopts { &:hover .meta .termopts {
display: block; display: block;
} }

View File

@ -242,7 +242,7 @@ class MainSideBar extends React.Component<MainSideBarProps, {}> {
} }
render() { render() {
let mainView = GlobalModel.activeMainView.get(); const mainView = GlobalModel.activeMainView.get();
const historyActive = mainView == "history"; const historyActive = mainView == "history";
const connectionsActive = mainView == "connections"; const connectionsActive = mainView == "connections";
const settingsActive = mainView == "clientsettings"; const settingsActive = mainView == "clientsettings";

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;
}
}
}

View File

@ -5,18 +5,18 @@ import * as React from "react";
import * as mobxReact from "mobx-react"; import * as mobxReact from "mobx-react";
import * as mobx from "mobx"; import * as mobx from "mobx";
import { GlobalModel } from "@/models"; import { GlobalModel } from "@/models";
import { isBlank } from "@/util/util";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import cn from "classnames";
import { If, For } from "tsx-control-statements/components"; import { If, For } from "tsx-control-statements/components";
import { Markdown } from "@/elements"; import { Markdown } from "@/elements";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil"; import { AuxiliaryCmdView } from "./auxview";
import "./aichat.less";
class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> { class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> {
componentDidMount(): void { componentDidMount(): void {
let AIChatObject = this.props.AIChatObject; const AIChatObject = this.props.AIChatObject;
let keybindManager = GlobalModel.keybindManager; const keybindManager = GlobalModel.keybindManager;
let inputModel = GlobalModel.inputModel; const inputModel = GlobalModel.inputModel;
keybindManager.registerKeybinding("pane", "aichat", "generic:confirm", (waveEvent) => { keybindManager.registerKeybinding("pane", "aichat", "generic:confirm", (waveEvent) => {
AIChatObject.onEnterKeyPressed(); AIChatObject.onEnterKeyPressed();
@ -54,10 +54,10 @@ class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> {
@mobxReact.observer @mobxReact.observer
class AIChat extends React.Component<{}, {}> { class AIChat extends React.Component<{}, {}> {
chatListKeyCount: number = 0; chatListKeyCount: number = 0;
textAreaNumLines: mobx.IObservableValue<number> = mobx.observable.box(1, { name: "textAreaNumLines" });
chatWindowScrollRef: React.RefObject<HTMLDivElement>; chatWindowScrollRef: React.RefObject<HTMLDivElement>;
textAreaRef: React.RefObject<HTMLTextAreaElement>; textAreaRef: React.RefObject<HTMLTextAreaElement>;
isFocused: OV<boolean>; isFocused: OV<boolean>;
termFontSize: number = 14;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
@ -69,8 +69,7 @@ class AIChat extends React.Component<{}, {}> {
} }
componentDidMount() { componentDidMount() {
let model = GlobalModel; const inputModel = GlobalModel.inputModel;
let inputModel = model.inputModel;
if (this.chatWindowScrollRef != null && this.chatWindowScrollRef.current != null) { if (this.chatWindowScrollRef != null && this.chatWindowScrollRef.current != null) {
this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight; this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight;
} }
@ -79,6 +78,7 @@ class AIChat extends React.Component<{}, {}> {
inputModel.setCmdInfoChatRefs(this.textAreaRef, this.chatWindowScrollRef); inputModel.setCmdInfoChatRefs(this.textAreaRef, this.chatWindowScrollRef);
} }
this.requestChatUpdate(); this.requestChatUpdate();
this.onTextAreaChange(null);
} }
componentDidUpdate() { componentDidUpdate() {
@ -92,20 +92,18 @@ class AIChat extends React.Component<{}, {}> {
} }
submitChatMessage(messageStr: string) { submitChatMessage(messageStr: string) {
let model = GlobalModel; const curLine = GlobalModel.inputModel.getCurLine();
let inputModel = model.inputModel; const prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
let curLine = inputModel.getCurLine();
let prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
prtn.then((rtn) => { prtn.then((rtn) => {
if (!rtn.success) { if (!rtn.success) {
console.log("submit chat command error: " + rtn.error); console.log("submit chat command error: " + rtn.error);
} }
}).catch((error) => {}); }).catch((_) => {});
} }
getLinePos(elem: any): { numLines: number; linePos: number } { getLinePos(elem: any): { numLines: number; linePos: number } {
let numLines = elem.value.split("\n").length; const numLines = elem.value.split("\n").length;
let linePos = elem.value.substr(0, elem.selectionStart).split("\n").length; const linePos = elem.value.substr(0, elem.selectionStart).split("\n").length;
return { numLines, linePos }; return { numLines, linePos };
} }
@ -121,22 +119,32 @@ class AIChat extends React.Component<{}, {}> {
})(); })();
} }
// Adjust the height of the textarea to fit the text
onTextAreaChange(e: any) { onTextAreaChange(e: any) {
// set height of textarea based on number of newlines // Calculate the bounding height of the text area
mobx.action(() => { const textAreaMaxLines = 4;
this.textAreaNumLines.set(e.target.value.split(/\n/).length); 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(); GlobalModel.inputModel.codeSelectDeselectAll();
})();
} }
onEnterKeyPressed() { onEnterKeyPressed() {
let inputModel = GlobalModel.inputModel; const inputModel = GlobalModel.inputModel;
let currentRef = this.textAreaRef.current; const currentRef = this.textAreaRef.current;
if (currentRef == null) { if (currentRef == null) {
return; return;
} }
if (inputModel.getCodeSelectSelectedIndex() == -1) { if (inputModel.getCodeSelectSelectedIndex() == -1) {
let messageStr = currentRef.value; const messageStr = currentRef.value;
this.submitChatMessage(messageStr); this.submitChatMessage(messageStr);
currentRef.value = ""; currentRef.value = "";
} else { } else {
@ -145,7 +153,7 @@ class AIChat extends React.Component<{}, {}> {
} }
onExpandInputPressed() { onExpandInputPressed() {
let currentRef = this.textAreaRef.current; const currentRef = this.textAreaRef.current;
if (currentRef == null) { if (currentRef == null) {
return; return;
} }
@ -154,7 +162,7 @@ class AIChat extends React.Component<{}, {}> {
} }
onArrowUpPressed(): boolean { onArrowUpPressed(): boolean {
let currentRef = this.textAreaRef.current; const currentRef = this.textAreaRef.current;
if (currentRef == null) { if (currentRef == null) {
return false; return false;
} }
@ -168,8 +176,8 @@ class AIChat extends React.Component<{}, {}> {
} }
onArrowDownPressed(): boolean { onArrowDownPressed(): boolean {
let currentRef = this.textAreaRef.current; const currentRef = this.textAreaRef.current;
let inputModel = GlobalModel.inputModel; const inputModel = GlobalModel.inputModel;
if (currentRef == null) { if (currentRef == null) {
return false; return false;
} }
@ -190,10 +198,10 @@ class AIChat extends React.Component<{}, {}> {
} }
renderChatMessage(chatItem: OpenAICmdInfoChatMessageType): any { renderChatMessage(chatItem: OpenAICmdInfoChatMessageType): any {
let curKey = "chatmsg-" + this.chatListKeyCount; const curKey = "chatmsg-" + this.chatListKeyCount;
this.chatListKeyCount++; this.chatListKeyCount++;
let senderClassName = chatItem.isassistantresponse ? "chat-msg-assistant" : "chat-msg-user"; const senderClassName = chatItem.isassistantresponse ? "chat-msg-assistant" : "chat-msg-user";
let msgClassName = "chat-msg " + senderClassName; const msgClassName = "chat-msg " + senderClassName;
let innerHTML: React.JSX.Element = ( let innerHTML: React.JSX.Element = (
<span> <span>
<div className="chat-msg-header"> <div className="chat-msg-header">
@ -226,31 +234,10 @@ 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() { render() {
let model = GlobalModel; const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
let inputModel = model.inputModel; const chitem: OpenAICmdInfoChatMessageType = null;
const renderKeybindings = mobx
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 renderKeybindings = mobx
.computed(() => { .computed(() => {
return ( return (
this.isFocused.get() || this.isFocused.get() ||
@ -260,27 +247,23 @@ class AIChat extends React.Component<{}, {}> {
); );
}) })
.get(); .get();
return ( return (
<div className="cmd-aichat"> <AuxiliaryCmdView
title="Wave AI"
className="cmd-aichat"
onClose={() => GlobalModel.inputModel.closeAIAssistantChat(true)}
iconClass="fa-sharp fa-solid fa-sparkles"
>
<If condition={renderKeybindings}> <If condition={renderKeybindings}>
<AIChatKeybindings AIChatObject={this}></AIChatKeybindings> <AIChatKeybindings AIChatObject={this}></AIChatKeybindings>
</If> </If>
<div className="cmdinput-titlebar"> <div className="chat-window" ref={this.chatWindowScrollRef}>
<div className="title-icon"> <For each="chitem" index="idx" of={chatMessageItems}>
<i className="fa-sharp fa-solid fa-sparkles" /> {this.renderChatMessage(chitem)}
</For>
</div> </div>
<div className="title-string">Wave AI</div> <div className="chat-input">
<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 <textarea
key="main" key="main"
ref={this.textAreaRef} ref={this.textAreaRef}
@ -291,11 +274,12 @@ class AIChat extends React.Component<{}, {}> {
onBlur={this.onTextAreaBlur.bind(this)} onBlur={this.onTextAreaBlur.bind(this)}
onChange={this.onTextAreaChange.bind(this)} onChange={this.onTextAreaChange.bind(this)}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
style={{ height: textAreaInnerHeight, maxHeight: textAreaMaxHeight, fontSize: termFontSize }} style={{ fontSize: this.termFontSize }}
className={cn("chat-textarea")} className="chat-textarea"
placeholder="Send a Message..." placeholder="Send a Message..."
></textarea> ></textarea>
</div> </div>
</AuxiliaryCmdView>
); );
} }
} }

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);
}
}

View File

@ -0,0 +1,50 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import cn from "classnames";
import { If } from "tsx-control-statements/components";
import "./auxview.less";
export class AuxiliaryCmdView extends React.Component<
{
title: string;
className?: string;
iconClass?: string;
titleBarContents?: React.ReactElement[];
children?: React.ReactNode;
onClose?: React.MouseEventHandler<HTMLDivElement>;
},
{}
> {
render() {
const { title, className, iconClass, titleBarContents, children, onClose } = this.props;
return (
<div className={cn("auxview", className)}>
<div className="auxview-titlebar">
<If condition={iconClass != null}>
<div className="title-icon">
<i className={iconClass} />
</div>
</If>
<div className="title-string">{title}</div>
<If condition={titleBarContents != null}>{titleBarContents}</If>
<div className="flex-spacer"></div>
<If condition={onClose != null}>
<div className="close-button" title="Close (ESC)" onClick={onClose}>
<i className="fa-sharp fa-solid fa-xmark-large" />
</div>
</If>
</div>
<If condition={children != null}>
<div className="auxview-content">{children}</div>
</If>
</div>
);
}
}

View File

@ -7,54 +7,153 @@
position: absolute; position: absolute;
bottom: 0; bottom: 0;
width: 100%; width: 100%;
padding: calc(var(--termpad) * 2) calc(var(--termpad) * 2) calc(var(--termpad) * 2) calc(var(--termpad) * 3 - 1px);
z-index: 100; z-index: 100;
border-top: 2px solid var(--app-border-color); border-top: 2px solid var(--app-border-color);
background-color: var(--app-bg-color); background-color: var(--app-bg-color);
&.has-info, // Apply a border between the base cmdinput and any views shown above it
// TODO: use a generic selector for this
&.has-aichat, &.has-aichat,
&.has-history { &.has-history,
.cmdinput-actions { &.has-info {
display: none; .base-cmdinput {
border-top: 1px solid var(--app-border-color);
} }
} }
.titlebar-spacer { &.has-info {
height: 31px; padding-top: var(--termpad);
} }
.cmdinput-conn { &.has-history,
&.has-aichat {
padding-top: var(--termpad);
height: max(300px, 70%);
}
.remote-status-warning {
display: flex;
flex-direction: row;
color: var(--app-warning-color);
align-items: center;
.wave-button,
.button {
margin-left: 10px;
padding: 4px 10px;
}
}
.cmd-input-grow-spacer {
flex-grow: 1;
}
.base-cmdinput {
position: relative;
// Rather than apply the padding to the whole container, we will apply it to the inner contents directly.
// This is more fragile, but allows us to capture a larger target area for the individual components.
--padding-top: var(--termpad);
--padding-sides: calc(var(--termpad) * 2);
.cmd-input-context {
color: var(--term-bright-white);
white-space: nowrap;
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--termfontfamily);
font-size: var(--termfontsize);
line-height: var(--termlineheight);
// We don't want to pad the bottom or it will push the input field down.
padding: var(--padding-top) var(--padding-sides) 0 var(--padding-sides);
margin-left: 2px;
}
.cmd-input-field {
position: relative;
font-family: var(--termfontfamily);
font-weight: normal;
line-height: var(--termlineheight);
font-size: var(--termfontsize);
border: none;
cursor: text;
// We don't want to pad the top or it will push the input field down.
padding: 0 var(--padding-sides) var(--padding-top) var(--padding-sides);
.cmd-hints {
position: absolute; position: absolute;
top: 0; bottom: -14px;
left: 0; right: 0px;
border-radius: 0 0 4px 0; }
background-color: rgba(88, 193, 66, 0.3); .control {
padding: 2px 10px 4px 10px; padding: 1em 2px;
font-size: calc(var(--termfontsize));
cursor: pointer;
i {
margin-left: 5px;
} }
&:hover { .textareainput-div {
background-color: rgba(88, 193, 66, 0.5); position: relative;
&.control {
padding: var(--termpad) 0;
}
.shelltag {
position: absolute;
// 13px = 10px height + 3px padding. subtract termpad to account for textareainput-div padding (2px not sure?)
bottom: calc(-13px + var(--termpad));
right: 0;
font-size: 10px;
color: var(--app-text-secondary-color);
line-height: 1;
user-select: none;
}
}
textarea {
color: var(--app-text-primary-color);
background-color: var(--app-bg-color);
padding: var(--termpad) 0;
resize: none;
overflow: auto;
overflow-wrap: anywhere;
font-family: var(--termfontfamily);
line-height: var(--termlineheight);
font-size: var(--termfontsize);
border: none;
box-shadow: none;
}
input.history-input {
border: 0;
padding: 0;
height: 0;
}
.cmd-quick-context .button {
background-color: var(--app-bg-color) !important;
color: var(--app-text-color);
}
&.inputmode-global .cmd-quick-context .button {
color: var(--app-bg-color);
background-color: var(--cmdinput-button-bg-color) !important;
}
&.inputmode-comment .cmd-quick-context .button {
color: var(--app-bg-color);
background-color: var(--cmdinput-comment-button-bg-color) !important;
} }
} }
.cmdinput-actions { .cmdinput-actions {
position: absolute; position: absolute;
border-radius: 4px;
padding-left: 4px;
padding-right: 4px;
font-size: calc(var(--termfontsize) + 2px); font-size: calc(var(--termfontsize) + 2px);
line-height: 1.2; line-height: 1.2;
// we want to align to 2nd line of meta. that's 2xPad + 1xLineHightSm // Align to the same bounds as the input field
// height of actions is 1xLineHeight + 8px (2x2px padding on icons) top: var(--padding-top);
top: calc(var(--termpad)); right: var(--padding-sides);
right: calc(var(--termpad) * 2);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -83,7 +182,9 @@
opacity: 1; opacity: 1;
} }
padding: 4px 6px; // This aligns the icons with the prompt field.
// We don't need right padding because the whole input field is already padded.
padding: 2px 0 0 12px;
cursor: pointer; cursor: pointer;
} }
@ -91,467 +192,5 @@
margin-left: 3px; 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);
}
.focus-indicator {
height: 90%;
top: 5%;
left: 4px;
}
&.has-history,
&.has-aichat {
padding-top: var(--termpad);
height: max(300px, 70%);
}
&.has-remote {
max-height: max(300px, 70%);
}
.remote-status-warning {
display: flex;
flex-direction: row;
color: var(--app-warning-color);
align-items: center;
.wave-button,
.button {
margin-left: 10px;
padding: 4px 10px;
}
}
.input-minmax-control {
position: absolute;
top: 5px;
right: 5px;
color: var(--term-foreground);
padding: 5px;
cursor: pointer;
}
.cmd-input-grow-spacer {
flex-grow: 1;
}
.base-cmdinput:not(:first-child) {
margin-top: var(--termpad);
}
.cmd-input-context {
color: var(--term-bright-white);
white-space: nowrap;
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--termfontfamily);
font-size: var(--termfontsize);
line-height: var(--termlineheight);
margin-left: 2px;
}
.cmd-input-filter {
opacity: 0.5;
&:hover {
opacity: 1;
}
.avatar {
display: inline-block;
width: 1em;
height: 1em;
margin: 0 0.5em;
vertical-align: text-top;
fill: var(--term-foreground);
}
.warning {
fill: var(--term-yellow);
}
}
.cmd-input-field {
position: relative;
padding-right: var(--termpad);
font-family: var(--termfontfamily);
font-weight: normal;
line-height: var(--termlineheight);
font-size: var(--termfontsize);
.cmd-hints {
position: absolute;
bottom: -14px;
right: 0px;
}
.control {
padding: 1em 2px;
}
.textareainput-div {
position: relative;
&.control {
padding: var(--termpad) 0;
}
.shelltag {
position: absolute;
// 13px = 10px height + 3px padding. subtract termpad to account for textareainput-div padding (2px not sure?)
bottom: calc(-13px + var(--termpad));
right: 0px;
font-size: 10px;
color: var(--app-text-secondary-color);
line-height: 1;
padding: 1px 8px 3px 8px;
background-color: var(--cmdinput-textarea-bg-color);
border-radius: 0 0 5px 5px;
}
}
textarea {
color: var(--app-text-primary-color);
background-color: var(--cmdinput-textarea-bg-color);
padding: var(--termpad);
resize: none;
overflow: auto;
overflow-wrap: anywhere;
border-color: transparent;
border: none;
font-family: var(--termfontfamily);
line-height: var(--termlineheight);
font-size: var(--termfontsize);
border-radius: 4px 4px 0 4px; // 0 is for shelltag
&.display-disabled {
background-color: #444;
}
&:focus {
border: none;
}
}
input.history-input {
border: 0;
padding: 0;
height: 0;
}
.cmd-quick-context .button {
background-color: #000 !important;
color: var(--app-text-color);
}
&.inputmode-global .cmd-quick-context .button {
color: var(--app-bg-color);
background-color: var(--cmdinput-button-bg-color) !important;
}
&.inputmode-comment .cmd-quick-context .button {
color: var(--app-bg-color);
background-color: var(--cmdinput-comment-button-bg-color) !important;
}
.cmd-exec {
position: absolute;
right: 0;
.icon {
vertical-align: bottom;
margin-right: 1em;
width: 2.5em;
height: 2.5em;
cursor: pointer;
border-radius: 50%;
fill: var(--app-accent-color);
padding: 0.25em;
}
.icon.disabled {
fill: var(--cmdinput-disabled-icon-color);
cursor: default;
}
.cmd-btn {
display: inline-block;
margin-right: 0;
padding: 0.2em 0.7rem;
border-radius: 4px;
vertical-align: super;
.hint-elem {
cursor: pointer;
opacity: 0.5;
&:hover {
opacity: 1;
}
}
}
}
}
.cmd-aichat {
display: flex;
justify-content: flex-end;
flex-flow: column nowrap;
margin-bottom: 10px;
flex-shrink: 1;
overflow-y: auto;
.chat-window {
overflow-y: auto;
margin-bottom: 5px;
flex-shrink: 1;
flex-direction: column-reverse;
}
.chat-textarea {
color: var(--app-text-primary-color);
background-color: var(--cmdinput-textarea-bg);
padding: 0.5em;
resize: none;
overflow: auto;
overflow-wrap: anywhere;
border-color: transparent;
border: none;
font-family: var(--termfontfamily);
font-weight: normal;
flex-shrink: 0;
flex-grow: 1;
border-radius: 4px;
&:focus {
border: none;
outline: none;
}
}
.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;
}
.grow-spacer {
flex: 1 0 10px;
}
}
.cmd-history {
color: var(--app-text-color);
font-family: var(--termfontfamily);
font-size: var(--termfontsize);
overflow: auto;
flex-shrink: 1;
.history-title {
div:first-child {
margin-left: var(--termpad);
}
.history-opt {
white-space: nowrap;
}
.history-clickable-opt {
cursor: pointer;
&:hover {
color: var(--app-text-primary-color);
}
}
.history-clickable-opt {
white-space: nowrap;
cursor: pointer;
}
}
.history-items {
color: var(--app-text-color);
padding-bottom: 6px;
.history-line {
white-space: pre;
}
.history-item.history-haderror {
color: var(--cmdinput-history-item-error-color);
}
.history-line:first-child {
margin-left: 0 !important;
}
.history-item {
padding-left: 5px;
cursor: pointer;
&:hover {
background-color: #222;
}
}
.history-item.is-selected {
font-weight: bold;
color: var(--app-text-primary-color);
background-color: #444;
}
.history-item.is-selected.history-haderror {
color: var(--cmdinput-history-item-selected-error-color);
}
}
}
.cmd-input-info {
flex-shrink: 1;
overflow-y: auto;
margin-bottom: var(--termpad);
font-family: var(--termfontfamily);
font-size: var(--termfontsize);
line-height: var(--termlineheight);
.info-title {
position: absolute;
z-index: 102;
top: 5px;
left: 0;
font-size: calc(var(--termfontsize) + 2px);
background-color: var(--app-bg-color);
color: var(--term-blue);
padding-bottom: 4px;
padding-left: calc(var(--termpad) * 2);
display: flex;
flex-direction: row;
width: 100%;
overflow-x: auto;
border-bottom: 1px solid var(--app-border-color);
}
.info-title + .info-msg,
.info-title + .info-lines,
.info-title + .info-comps,
.info-title + .info-error {
margin-top: 26px;
}
.info-msg {
color: var(--term-blue);
padding-bottom: 2px;
a {
color: var(--term-blue);
}
}
.info-lines {
color: var(--app-text-color);
white-space: pre;
padding-bottom: 6px;
}
.info-comps {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding-bottom: 5px;
font-weight: normal;
font-family: var(--termfontfamily);
font-size: var(--termfontsize);
.info-comp {
min-width: 200px;
color: var(--term-foreground);
margin-right: 10px;
&.has-space {
text-decoration: underline dotted #777;
}
}
.metacmd-comp {
color: var(--term-bright-green);
}
}
.info-error {
color: var(--cmdinput-text-error-color);
padding-bottom: 2px;
}
} }
} }

View File

@ -61,12 +61,17 @@ class CmdInput extends React.Component<{}, {}> {
} }
@boundMethod @boundMethod
cmdInputClick(e: any): void { baseCmdInputClick(e: React.MouseEvent): void {
if (this.promptRef.current != null) { if (this.promptRef.current != null) {
if (this.promptRef.current.contains(e.target)) { if (this.promptRef.current.contains(e.target)) {
return; return;
} }
} }
if ((e.target as HTMLDivElement).classList.contains("cmd-input-context")) {
e.stopPropagation();
return;
}
GlobalModel.inputModel.setHistoryFocus(false);
GlobalModel.inputModel.giveFocus(); GlobalModel.inputModel.giveFocus();
} }
@ -74,9 +79,13 @@ class CmdInput extends React.Component<{}, {}> {
clickAIAction(e: any): void { clickAIAction(e: any): void {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
let inputModel = GlobalModel.inputModel; const inputModel = GlobalModel.inputModel;
if (inputModel.aIChatShow.get()) {
inputModel.closeAIAssistantChat(true);
} else {
inputModel.openAIAssistantChat(); inputModel.openAIAssistantChat();
} }
}
@boundMethod @boundMethod
clickHistoryAction(e: any): void { clickHistoryAction(e: any): void {
@ -160,6 +169,9 @@ class CmdInput extends React.Component<{}, {}> {
} }
let shellInitMsg: string = null; let shellInitMsg: string = null;
let hidePrompt = false; let hidePrompt = false;
const openView = inputModel.getOpenView();
const hasOpenView = openView ? `has-${openView}` : null;
if (ri == null) { if (ri == null) {
let shellStr = "shell"; let shellStr = "shell";
if (!util.isBlank(remote?.defaultshelltype)) { if (!util.isBlank(remote?.defaultshelltype)) {
@ -172,42 +184,7 @@ class CmdInput extends React.Component<{}, {}> {
} }
} }
return ( return (
<div <div ref={this.cmdInputRef} className={cn("cmd-input", hasOpenView, { active: focusVal })}>
ref={this.cmdInputRef}
className={cn(
"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)}
>
<CenteredIcon>{numRunningLines}</CenteredIcon>{" "}
<CenteredIcon>
<RotateIcon className="rotate warning spin" />
</CenteredIcon>
</div>
</If>
<div key="ai" title="Wave AI (Ctrl-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}> <If condition={historyShow}>
<div className="cmd-input-grow-spacer"></div> <div className="cmd-input-grow-spacer"></div>
<HistoryInfo /> <HistoryInfo />
@ -252,7 +229,38 @@ class CmdInput extends React.Component<{}, {}> {
</Button> </Button>
</div> </div>
</If> </If>
<div key="base-cmdinput" className="base-cmdinput"> <div key="base-cmdinput" className="base-cmdinput" onClick={this.baseCmdInputClick}>
<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)}
>
<CenteredIcon>{numRunningLines}</CenteredIcon>{" "}
<CenteredIcon>
<RotateIcon className="rotate warning spin" />
</CenteredIcon>
</div>
</If>
<div
key="ai"
title="Wave AI (Ctrl-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={!hidePrompt}> <If condition={!hidePrompt}>
<div key="prompt" className="cmd-input-context"> <div key="prompt" className="cmd-input-context">
<div className="has-text-white"> <div className="has-text-white">

View File

@ -0,0 +1,60 @@
.cmd-history {
color: var(--app-text-color);
font-family: var(--termfontfamily);
font-size: var(--termfontsize);
.auxview-titlebar {
.history-opt {
white-space: nowrap;
}
.history-clickable-opt {
cursor: pointer;
white-space: nowrap;
cursor: pointer;
&:hover {
color: var(--app-text-primary-color);
}
}
}
.history-items {
color: var(--app-text-color);
width: 100%;
.history-item {
cursor: pointer;
border-radius: 5px;
.history-line {
white-space: pre;
&:first-child {
margin-left: 0 !important;
}
}
&:hover {
background-color: var(--table-tr-hover-bg-color);
}
&.history-haderror {
color: var(--cmdinput-history-item-error-color);
}
&.is-selected {
font-weight: bold;
color: var(--app-text-primary-color);
background-color: var(--table-tr-selected-bg-color);
&:hover {
background-color: var(--table-tr-selected-hover-bg-color);
}
}
&.is-selected.history-haderror {
color: var(--cmdinput-history-item-selected-error-color);
}
}
}
}

View File

@ -13,6 +13,9 @@ import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "@/models"; import { GlobalModel } from "@/models";
import { isBlank } from "@/util/util"; import { isBlank } from "@/util/util";
import "./historyinfo.less";
import { AuxiliaryCmdView } from "./auxview";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
const TDots = "⋮"; const TDots = "⋮";
@ -43,7 +46,7 @@ class HItem extends React.Component<
if (hitem.remote == null || isBlank(hitem.remote.remoteid)) { if (hitem.remote == null || isBlank(hitem.remote.remoteid)) {
return sprintf("%-15s ", ""); return sprintf("%-15s ", "");
} }
let r = GlobalModel.getRemote(hitem.remote.remoteid); const r = GlobalModel.getRemote(hitem.remote.remoteid);
if (r == null) { if (r == null) {
return sprintf("%-15s ", "???"); return sprintf("%-15s ", "???");
} }
@ -71,15 +74,15 @@ class HItem extends React.Component<
if (!opts.limitRemote) { if (!opts.limitRemote) {
remoteStr = this.renderRemote(hitem); remoteStr = this.renderRemote(hitem);
} }
let selectedStr = isSelected ? "*" : " "; const selectedStr = isSelected ? "*" : " ";
let lineNumStr = hitem.linenum > 0 ? "(" + hitem.linenum + ")" : ""; const lineNumStr = hitem.linenum > 0 ? "(" + hitem.linenum + ")" : "";
if (isBlank(opts.queryType) || opts.queryType == "screen") { if (isBlank(opts.queryType) || opts.queryType == "screen") {
return selectedStr + sprintf("%7s", lineNumStr) + " " + remoteStr; return selectedStr + sprintf("%7s", lineNumStr) + " " + remoteStr;
} }
if (opts.queryType == "session") { if (opts.queryType == "session") {
let screenStr = ""; let screenStr = "";
if (!isBlank(hitem.screenid)) { if (!isBlank(hitem.screenid)) {
let scrName = scrNames[hitem.screenid]; const scrName = scrNames[hitem.screenid];
if (scrName != null) { if (scrName != null) {
screenStr = "[" + truncateWithTDots(scrName, 15) + "]"; screenStr = "[" + truncateWithTDots(scrName, 15) + "]";
} }
@ -89,19 +92,18 @@ class HItem extends React.Component<
if (opts.queryType == "global") { if (opts.queryType == "global") {
let sessionStr = ""; let sessionStr = "";
if (!isBlank(hitem.sessionid)) { if (!isBlank(hitem.sessionid)) {
let sessionName = snames[hitem.sessionid]; const sessionName = snames[hitem.sessionid];
if (sessionName != null) { if (sessionName != null) {
sessionStr = "#" + truncateWithTDots(sessionName, 15); sessionStr = "#" + truncateWithTDots(sessionName, 15);
} }
} }
let screenStr = ""; let screenStr = "";
if (!isBlank(hitem.screenid)) { if (!isBlank(hitem.screenid)) {
let scrName = scrNames[hitem.screenid]; const scrName = scrNames[hitem.screenid];
if (scrName != null) { if (scrName != null) {
screenStr = "[" + truncateWithTDots(scrName, 13) + "]"; screenStr = "[" + truncateWithTDots(scrName, 13) + "]";
} }
} }
let ssStr = sessionStr + screenStr;
return ( return (
selectedStr + selectedStr +
sprintf("%15s ", sessionStr) + sprintf("%15s ", sessionStr) +
@ -116,12 +118,12 @@ class HItem extends React.Component<
} }
render() { render() {
let { hitem, isSelected, opts, snames, scrNames } = this.props; const { hitem, isSelected, opts, snames, scrNames } = this.props;
let lines = hitem.cmdstr.split("\n"); const lines = hitem.cmdstr.split("\n");
let line: string = ""; let line: string = "";
let idx = 0; let idx = 0;
let infoText = this.renderHInfoText(hitem, opts, isSelected, snames, scrNames); const infoText = this.renderHInfoText(hitem, opts, isSelected, snames, scrNames);
let infoTextSpacer = sprintf("%" + infoText.length + "s", ""); const infoTextSpacer = sprintf("%" + infoText.length + "s", "");
return ( return (
<div <div
key={hitem.historynum} key={hitem.historynum}
@ -153,7 +155,7 @@ class HistoryInfo extends React.Component<{}, {}> {
containingText: mobx.IObservableValue<string> = mobx.observable.box(""); containingText: mobx.IObservableValue<string> = mobx.observable.box("");
componentDidMount() { componentDidMount() {
let inputModel = GlobalModel.inputModel; const inputModel = GlobalModel.inputModel;
let hitem = inputModel.getHistorySelectedItem(); let hitem = inputModel.getHistorySelectedItem();
if (hitem == null) { if (hitem == null) {
hitem = inputModel.getFirstHistoryItem(); hitem = inputModel.getFirstHistoryItem();
@ -170,15 +172,15 @@ class HistoryInfo extends React.Component<{}, {}> {
@boundMethod @boundMethod
handleItemClick(hitem: HistoryItem) { handleItemClick(hitem: HistoryItem) {
let inputModel = GlobalModel.inputModel; const inputModel = GlobalModel.inputModel;
let selItem = inputModel.getHistorySelectedItem(); const selItem = inputModel.getHistorySelectedItem();
inputModel.setHistoryFocus(true);
if (this.lastClickHNum == hitem.historynum && selItem != null && selItem.historynum == hitem.historynum) { if (this.lastClickHNum == hitem.historynum && selItem != null && selItem.historynum == hitem.historynum) {
inputModel.grabSelectedHistoryItem(); inputModel.grabSelectedHistoryItem();
return; return;
} }
inputModel.giveFocus();
inputModel.setHistorySelectionNum(hitem.historynum); inputModel.setHistorySelectionNum(hitem.historynum);
let now = Date.now(); const now = Date.now();
this.lastClickHNum = hitem.historynum; this.lastClickHNum = hitem.historynum;
this.lastClickTs = now; this.lastClickTs = now;
setTimeout(() => { setTimeout(() => {
@ -191,24 +193,41 @@ class HistoryInfo extends React.Component<{}, {}> {
@boundMethod @boundMethod
handleClickType() { handleClickType() {
let inputModel = GlobalModel.inputModel; const inputModel = GlobalModel.inputModel;
inputModel.setHistoryFocus(true);
inputModel.toggleHistoryType(); inputModel.toggleHistoryType();
} }
@boundMethod @boundMethod
handleClickRemote() { handleClickRemote() {
let inputModel = GlobalModel.inputModel; const inputModel = GlobalModel.inputModel;
inputModel.setHistoryFocus(true);
inputModel.toggleRemoteType(); inputModel.toggleRemoteType();
} }
@boundMethod
getTitleBarContents(): React.ReactElement[] {
const opts = GlobalModel.inputModel.historyQueryOpts.get();
return [
<div className="history-opt history-clickable-opt" key="screen" onClick={this.handleClickType}>
[for {opts.queryType} &#x2318;S]
</div>,
<div className="history-opt" key="query-str" title="type to search">
[containing '{opts.queryStr}']
</div>,
<div className="history-opt history-clickable-opt" key="remote" onClick={this.handleClickRemote}>
[{opts.limitRemote ? "this" : "any"} remote &#x2318;R]
</div>,
];
}
render() { render() {
let inputModel = GlobalModel.inputModel; const inputModel = GlobalModel.inputModel;
let idx: number = 0; const selItem = inputModel.getHistorySelectedItem();
let selItem = inputModel.getHistorySelectedItem(); const hitems = inputModel.getFilteredHistoryItems().slice().reverse();
let hitems = inputModel.getFilteredHistoryItems(); const opts = inputModel.historyQueryOpts.get();
hitems = hitems.slice().reverse();
let hitem: HistoryItem = null; let hitem: HistoryItem = null;
let opts = inputModel.historyQueryOpts.get();
let snames: Record<string, string> = {}; let snames: Record<string, string> = {};
let scrNames: Record<string, string> = {}; let scrNames: Record<string, string> = {};
if (opts.queryType == "global") { if (opts.queryType == "global") {
@ -218,29 +237,13 @@ class HistoryInfo extends React.Component<{}, {}> {
scrNames = GlobalModel.getScreenNames(); scrNames = GlobalModel.getScreenNames();
} }
return ( return (
<div className="cmd-history hide-scrollbar"> <AuxiliaryCmdView
<div className="cmdinput-titlebar history-title"> title="History"
<div className="title-icon"> className="cmd-history hide-scrollbar"
<i className="fa-sharp fa-solid fa-clock-rotate-left" /> onClose={this.handleClose}
</div> titleBarContents={this.getTitleBarContents()}
<div className="title-string">History</div> iconClass="fa-sharp fa-solid fa-clock-rotate-left"
<div className="spacer"></div> >
<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 <div
className={cn( className={cn(
"history-items", "history-items",
@ -248,7 +251,6 @@ class HistoryInfo extends React.Component<{}, {}> {
{ "show-sessions": opts.queryType == "global" } { "show-sessions": opts.queryType == "global" }
)} )}
> >
<div className="titlebar-spacer" />
<If condition={hitems.length == 0}>[no history]</If> <If condition={hitems.length == 0}>[no history]</If>
<If condition={hitems.length > 0}> <If condition={hitems.length > 0}>
<For each="hitem" index="idx" of={hitems}> <For each="hitem" index="idx" of={hitems}>
@ -264,7 +266,7 @@ class HistoryInfo extends React.Component<{}, {}> {
</For> </For>
</If> </If>
</div> </div>
</div> </AuxiliaryCmdView>
); );
} }
} }

View File

@ -0,0 +1,49 @@
.cmd-input-info {
font-family: var(--termfontfamily);
font-size: var(--termfontsize);
line-height: var(--termlineheight);
.info-msg {
color: var(--term-blue);
padding-bottom: 2px;
a {
color: var(--term-blue);
}
}
.info-lines {
color: var(--app-text-color);
white-space: pre;
padding-bottom: 6px;
}
.info-comps {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding-bottom: 5px;
font-weight: normal;
font-family: var(--termfontfamily);
font-size: var(--termfontsize);
.info-comp {
min-width: 200px;
color: var(--term-foreground);
margin-right: 10px;
&.has-space {
text-decoration: underline dotted #777;
}
}
.metacmd-comp {
color: var(--term-bright-green);
}
}
.info-error {
color: var(--cmdinput-text-error-color);
padding-bottom: 2px;
}
}

View File

@ -9,6 +9,9 @@ import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel } from "@/models"; import { GlobalModel } from "@/models";
import * as appconst from "@/app/appconst"; import * as appconst from "@/app/appconst";
import { AuxiliaryCmdView } from "./auxview";
import "./infomsg.less";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
@ -40,29 +43,22 @@ class InfoMsg extends React.Component<{}, {}> {
} }
render() { render() {
let model = GlobalModel; const inputModel = GlobalModel.inputModel;
let inputModel = model.inputModel; const infoMsg: InfoType = inputModel.infoMsg.get();
let infoMsg = inputModel.infoMsg.get(); const infoShow: boolean = inputModel.infoShow.get();
let infoShow = inputModel.infoShow.get();
let line: string = null; let line: string = null;
let istr: string = null; let istr: string = null;
let idx: number = 0; let idx: number = 0;
let titleStr = null; let titleStr = null;
let remoteEditKey = "inforemoteedit";
if (infoMsg != null) { if (infoMsg != null) {
titleStr = infoMsg.infotitle; titleStr = infoMsg.infotitle;
} }
let activeScreen = model.getActiveScreen();
if (!infoShow) { if (!infoShow) {
return null; return null;
} }
return ( return (
<div className="cmd-input-info" style={{ display: infoShow ? "block" : "none" }}> <AuxiliaryCmdView title={titleStr} className="cmd-input-info">
<If condition={infoMsg?.infotitle}>
<div key="infotitle" className="info-title">
{titleStr}
</div>
</If>
<If condition={infoMsg?.infomsg}> <If condition={infoMsg?.infomsg}>
<div key="infomsg" className="info-msg"> <div key="infomsg" className="info-msg">
<If condition={infoMsg.infomsghtml}> <If condition={infoMsg.infomsghtml}>
@ -108,7 +104,7 @@ class InfoMsg extends React.Component<{}, {}> {
<div className="info-error">to reset, run: /reset:cwd</div> <div className="info-error">to reset, run: /reset:cwd</div>
</If> </If>
</If> </If>
</div> </AuxiliaryCmdView>
); );
} }
} }

View File

@ -287,7 +287,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
setFocus(): void { setFocus(): void {
const inputModel = GlobalModel.inputModel; const inputModel = GlobalModel.inputModel;
if (inputModel.historyShow.get()) { if (inputModel.historyFocus.get()) {
this.historyInputRef.current.focus(); this.historyInputRef.current.focus();
} else { } else {
this.mainInputRef.current.focus(); this.mainInputRef.current.focus();
@ -551,7 +551,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
@boundMethod @boundMethod
handleMainFocus(e: any) { handleMainFocus(e: any) {
const inputModel = GlobalModel.inputModel; const inputModel = GlobalModel.inputModel;
if (inputModel.historyShow.get()) { if (inputModel.historyFocus.get()) {
e.preventDefault(); e.preventDefault();
if (this.historyInputRef.current != null) { if (this.historyInputRef.current != null) {
this.historyInputRef.current.focus(); this.historyInputRef.current.focus();
@ -578,7 +578,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
@boundMethod @boundMethod
handleHistoryFocus(e: any) { handleHistoryFocus(e: any) {
const inputModel = GlobalModel.inputModel; const inputModel = GlobalModel.inputModel;
if (!inputModel.historyShow.get()) { if (!inputModel.historyFocus.get()) {
e.preventDefault(); e.preventDefault();
if (this.mainInputRef.current != null) { if (this.mainInputRef.current != null) {
this.mainInputRef.current.focus(); this.mainInputRef.current.focus();
@ -616,7 +616,9 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
if (numLines > 1 || longLine || inputModel.inputExpanded.get()) { if (numLines > 1 || longLine || inputModel.inputExpanded.get()) {
displayLines = 5; displayLines = 5;
} }
const disabled = inputModel.historyShow.get();
// TODO: invert logic here. We should track focus on the main textarea and assume aux view is focused if not.
const disabled = inputModel.historyFocus.get();
if (disabled) { if (disabled) {
displayLines = 1; displayLines = 1;
} }
@ -661,7 +663,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
<HistoryKeybindings inputObject={this}></HistoryKeybindings> <HistoryKeybindings inputObject={this}></HistoryKeybindings>
</If> </If>
<If condition={!disabled && !util.isBlank(shellType)}> <If condition={!util.isBlank(shellType)}>
<div className="shelltag">{shellType}</div> <div className="shelltag">{shellType}</div>
</If> </If>
<textarea <textarea
@ -678,6 +680,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
onChange={this.onChange} onChange={this.onChange}
onSelect={this.onSelect} onSelect={this.onSelect}
placeholder="Type here..."
className={cn("textarea", { "display-disabled": disabled })} className={cn("textarea", { "display-disabled": disabled })}
></textarea> ></textarea>
<input <input

View File

@ -25,6 +25,7 @@ function getDefaultHistoryQueryOpts(): HistoryQueryOpts {
class InputModel { class InputModel {
globalModel: Model; globalModel: Model;
historyShow: OV<boolean> = mobx.observable.box(false); historyShow: OV<boolean> = mobx.observable.box(false);
historyFocus: OV<boolean> = mobx.observable.box(false);
infoShow: OV<boolean> = mobx.observable.box(false); infoShow: OV<boolean> = mobx.observable.box(false);
aIChatShow: OV<boolean> = mobx.observable.box(false); aIChatShow: OV<boolean> = mobx.observable.box(false);
cmdInputHeight: OV<number> = mobx.observable.box(0); cmdInputHeight: OV<number> = mobx.observable.box(0);
@ -92,7 +93,7 @@ class InputModel {
} }
toggleHistoryType(): void { toggleHistoryType(): void {
let opts = mobx.toJS(this.historyQueryOpts.get()); const opts = mobx.toJS(this.historyQueryOpts.get());
let htype = opts.queryType; let htype = opts.queryType;
if (htype == "screen") { if (htype == "screen") {
htype = "session"; htype = "session";
@ -105,7 +106,7 @@ class InputModel {
} }
toggleRemoteType(): void { toggleRemoteType(): void {
let opts = mobx.toJS(this.historyQueryOpts.get()); const opts = mobx.toJS(this.historyQueryOpts.get());
if (opts.limitRemote) { if (opts.limitRemote) {
opts.limitRemote = false; opts.limitRemote = false;
opts.limitRemoteInstance = false; opts.limitRemoteInstance = false;
@ -139,21 +140,21 @@ class InputModel {
} }
_focusCmdInput(): void { _focusCmdInput(): void {
let elem = document.getElementById("main-cmd-input"); const elem = document.getElementById("main-cmd-input");
if (elem != null) { if (elem != null) {
elem.focus(); elem.focus();
} }
} }
_focusHistoryInput(): void { _focusHistoryInput(): void {
let elem: HTMLElement = document.querySelector(".cmd-input input.history-input"); const elem: HTMLElement = document.querySelector(".cmd-input input.history-input");
if (elem != null) { if (elem != null) {
elem.focus(); elem.focus();
} }
} }
giveFocus(): void { giveFocus(): void {
if (this.historyShow.get()) { if (this.historyFocus.get()) {
this._focusHistoryInput(); this._focusHistoryInput();
} else { } else {
this._focusCmdInput(); this._focusCmdInput();
@ -165,7 +166,7 @@ class InputModel {
this.physicalInputFocused.set(isFocused); this.physicalInputFocused.set(isFocused);
})(); })();
if (isFocused) { if (isFocused) {
let screen = this.globalModel.getActiveScreen(); const screen = this.globalModel.getActiveScreen();
if (screen != null) { if (screen != null) {
if (screen.focusType.get() != "input") { if (screen.focusType.get() != "input") {
GlobalCommandRunner.screenSetFocus("input"); GlobalCommandRunner.screenSetFocus("input");
@ -175,11 +176,11 @@ class InputModel {
} }
hasFocus(): boolean { hasFocus(): boolean {
let mainInputElem = document.getElementById("main-cmd-input"); const mainInputElem = document.getElementById("main-cmd-input");
if (document.activeElement == mainInputElem) { if (document.activeElement == mainInputElem) {
return true; return true;
} }
let historyInputElem = document.querySelector(".cmd-input input.history-input"); const historyInputElem = document.querySelector(".cmd-input input.history-input");
if (document.activeElement == historyInputElem) { if (document.activeElement == historyInputElem) {
return true; return true;
} }
@ -190,6 +191,19 @@ class InputModel {
return false; return false;
} }
getOpenView(): string {
if (this.historyShow.get()) {
return "history";
}
if (this.aIChatShow.get()) {
return "aichat";
}
if (this.infoShow.get()) {
return "info";
}
return null;
}
setHistoryType(htype: HistoryTypeStrs): void { setHistoryType(htype: HistoryTypeStrs): void {
if (this.historyQueryOpts.get().queryType == htype) { if (this.historyQueryOpts.get().queryType == htype) {
return; return;
@ -201,20 +215,19 @@ class InputModel {
if (oldItem == null) { if (oldItem == null) {
return 0; return 0;
} }
let newItems = this.getFilteredHistoryItems(); const newItems = this.getFilteredHistoryItems();
if (newItems.length == 0) { if (newItems.length == 0) {
return 0; return 0;
} }
let bestIdx = 0; let bestIdx = 0;
for (let i = 0; i < newItems.length; i++) { for (const [i, item] of newItems.entries()) {
// still start at i=0 to catch the historynum equality case // still start at i=0 to catch the historynum equality case
let item = newItems[i];
if (item.historynum == oldItem.historynum) { if (item.historynum == oldItem.historynum) {
bestIdx = i; bestIdx = i;
break; break;
} }
let bestTsDiff = Math.abs(item.ts - newItems[bestIdx].ts); const bestTsDiff = Math.abs(item.ts - newItems[bestIdx].ts);
let curTsDiff = Math.abs(item.ts - oldItem.ts); const curTsDiff = Math.abs(item.ts - oldItem.ts);
if (curTsDiff < bestTsDiff) { if (curTsDiff < bestTsDiff) {
bestIdx = i; bestIdx = i;
} }
@ -224,9 +237,9 @@ class InputModel {
setHistoryQueryOpts(opts: HistoryQueryOpts): void { setHistoryQueryOpts(opts: HistoryQueryOpts): void {
mobx.action(() => { mobx.action(() => {
let oldItem = this.getHistorySelectedItem(); const oldItem = this.getHistorySelectedItem();
this.historyQueryOpts.set(opts); this.historyQueryOpts.set(opts);
let bestIndex = this.findBestNewIndex(oldItem); const bestIndex = this.findBestNewIndex(oldItem);
setTimeout(() => this.setHistoryIndex(bestIndex, true), 10); setTimeout(() => this.setHistoryIndex(bestIndex, true), 10);
})(); })();
} }
@ -253,17 +266,28 @@ class InputModel {
this.setInputPopUpType("none"); this.setInputPopUpType("none");
} }
this.historyShow.set(show); this.historyShow.set(show);
this.historyFocus.set(show);
if (this.hasFocus()) { if (this.hasFocus()) {
this.giveFocus(); this.giveFocus();
} }
})(); })();
} }
setHistoryFocus(focus: boolean): void {
if (this.historyFocus.get() == focus) {
return;
}
mobx.action(() => {
this.historyFocus.set(focus);
this.giveFocus();
})();
}
isHistoryLoaded(): boolean { isHistoryLoaded(): boolean {
if (this.historyLoading.get()) { if (this.historyLoading.get()) {
return false; return false;
} }
let hitems = this.historyItems.get(); const hitems = this.historyItems.get();
return hitems != null; return hitems != null;
} }
@ -294,6 +318,7 @@ class InputModel {
if (!this.historyShow.get()) { if (!this.historyShow.get()) {
mobx.action(() => { mobx.action(() => {
this.setHistoryShow(true); this.setHistoryShow(true);
this.aIChatShow.set(false);
this.infoShow.set(false); this.infoShow.set(false);
this.dropModHistory(true); this.dropModHistory(true);
this.giveFocus(); this.giveFocus();
@ -311,11 +336,11 @@ class InputModel {
} }
getHistorySelectedItem(): HistoryItem { getHistorySelectedItem(): HistoryItem {
let hidx = this.historyIndex.get(); const hidx = this.historyIndex.get();
if (hidx == 0) { if (hidx == 0) {
return null; return null;
} }
let hitems = this.getFilteredHistoryItems(); const hitems = this.getFilteredHistoryItems();
if (hidx > hitems.length) { if (hidx > hitems.length) {
return null; return null;
} }
@ -323,7 +348,7 @@ class InputModel {
} }
getFirstHistoryItem(): HistoryItem { getFirstHistoryItem(): HistoryItem {
let hitems = this.getFilteredHistoryItems(); const hitems = this.getFilteredHistoryItems();
if (hitems.length == 0) { if (hitems.length == 0) {
return null; return null;
} }
@ -331,9 +356,9 @@ class InputModel {
} }
setHistorySelectionNum(hnum: string): void { setHistorySelectionNum(hnum: string): void {
let hitems = this.getFilteredHistoryItems(); const hitems = this.getFilteredHistoryItems();
for (let i = 0; i < hitems.length; i++) { for (const [i, hitem] of hitems.entries()) {
if (hitems[i].historynum == hnum) { if (hitem.historynum == hnum) {
this.setHistoryIndex(i + 1); this.setHistoryIndex(i + 1);
return; return;
} }
@ -342,8 +367,8 @@ class InputModel {
setHistoryInfo(hinfo: HistoryInfoType): void { setHistoryInfo(hinfo: HistoryInfoType): void {
mobx.action(() => { mobx.action(() => {
let oldItem = this.getHistorySelectedItem(); const oldItem = this.getHistorySelectedItem();
let hitems: HistoryItem[] = hinfo.items ?? []; const hitems: HistoryItem[] = hinfo.items ?? [];
this.historyItems.set(hitems); this.historyItems.set(hitems);
this.historyLoading.set(false); this.historyLoading.set(false);
this.historyQueryOpts.get().queryType = hinfo.historytype; this.historyQueryOpts.get().queryType = hinfo.historytype;
@ -352,7 +377,7 @@ class InputModel {
this.historyQueryOpts.get().limitRemoteInstance = false; this.historyQueryOpts.get().limitRemoteInstance = false;
} }
if (this.historyAfterLoadIndex == -1) { if (this.historyAfterLoadIndex == -1) {
let bestIndex = this.findBestNewIndex(oldItem); const bestIndex = this.findBestNewIndex(oldItem);
setTimeout(() => this.setHistoryIndex(bestIndex, true), 100); setTimeout(() => this.setHistoryIndex(bestIndex, true), 100);
} else if (this.historyAfterLoadIndex) { } else if (this.historyAfterLoadIndex) {
if (hitems.length >= this.historyAfterLoadIndex) { if (hitems.length >= this.historyAfterLoadIndex) {
@ -371,10 +396,10 @@ class InputModel {
} }
_getFilteredHistoryItems(): HistoryItem[] { _getFilteredHistoryItems(): HistoryItem[] {
let hitems: HistoryItem[] = this.historyItems.get() ?? []; const hitems: HistoryItem[] = this.historyItems.get() ?? [];
let rtn: HistoryItem[] = []; const rtn: HistoryItem[] = [];
let opts = mobx.toJS(this.historyQueryOpts.get()); const opts: HistoryQueryOpts = mobx.toJS(this.historyQueryOpts.get());
let ctx = this.globalModel.getUIContext(); const ctx = this.globalModel.getUIContext();
let curRemote: RemotePtrType = ctx.remote; let curRemote: RemotePtrType = ctx.remote;
if (curRemote == null) { if (curRemote == null) {
curRemote = { ownerid: "", name: "", remoteid: "" }; curRemote = { ownerid: "", name: "", remoteid: "" };
@ -411,7 +436,7 @@ class InputModel {
if (isBlank(hitem.cmdstr)) { if (isBlank(hitem.cmdstr)) {
continue; continue;
} }
let idx = hitem.cmdstr.indexOf(opts.queryStr); const idx = hitem.cmdstr.indexOf(opts.queryStr);
if (idx == -1) { if (idx == -1) {
continue; continue;
} }
@ -423,24 +448,24 @@ class InputModel {
} }
scrollHistoryItemIntoView(hnum: string): void { scrollHistoryItemIntoView(hnum: string): void {
let elem: HTMLElement = document.querySelector(".cmd-history .hnum-" + hnum); const elem: HTMLElement = document.querySelector(".cmd-history .hnum-" + hnum);
if (elem == null) { if (elem == null) {
return; return;
} }
let historyDiv = elem.closest(".cmd-history"); const historyDiv = elem.closest(".cmd-history");
if (historyDiv == null) { if (historyDiv == null) {
return; return;
} }
let buffer = 15; const buffer = 15;
let titleHeight = 24; let titleHeight = 24;
let titleDiv: HTMLElement = document.querySelector(".cmd-history .history-title"); const titleDiv: HTMLElement = document.querySelector(".cmd-history .history-title");
if (titleDiv != null) { if (titleDiv != null) {
titleHeight = titleDiv.offsetHeight + 2; titleHeight = titleDiv.offsetHeight + 2;
} }
let elemOffset = elem.offsetTop; const elemOffset = elem.offsetTop;
let elemHeight = elem.clientHeight; const elemHeight = elem.clientHeight;
let topPos = historyDiv.scrollTop; const topPos = historyDiv.scrollTop;
let endPos = topPos + historyDiv.clientHeight; const endPos = topPos + historyDiv.clientHeight;
if (elemOffset + elemHeight + buffer > endPos) { if (elemOffset + elemHeight + buffer > endPos) {
if (elemHeight + buffer > historyDiv.clientHeight - titleHeight) { if (elemHeight + buffer > historyDiv.clientHeight - titleHeight) {
historyDiv.scrollTop = elemOffset - titleHeight; historyDiv.scrollTop = elemOffset - titleHeight;
@ -459,7 +484,7 @@ class InputModel {
} }
grabSelectedHistoryItem(): void { grabSelectedHistoryItem(): void {
let hitem = this.getHistorySelectedItem(); const hitem = this.getHistorySelectedItem();
if (hitem == null) { if (hitem == null) {
this.resetHistory(); this.resetHistory();
return; return;
@ -498,9 +523,8 @@ class InputModel {
if (!this.isHistoryLoaded()) { if (!this.isHistoryLoaded()) {
return; return;
} }
let hitems = this.getFilteredHistoryItems(); const hitems = this.getFilteredHistoryItems();
let idx = this.historyIndex.get(); let idx = this.historyIndex.get() + amt;
idx += amt;
if (idx < 0) { if (idx < 0) {
idx = 0; idx = 0;
} }
@ -540,7 +564,6 @@ class InputModel {
} }
setAIChatFocus() { setAIChatFocus() {
console.log("setting ai chat focus");
if (this.aiChatTextAreaRef?.current != null) { if (this.aiChatTextAreaRef?.current != null) {
this.aiChatTextAreaRef.current.focus(); this.aiChatTextAreaRef.current.focus();
} }
@ -551,9 +574,8 @@ class InputModel {
this.codeSelectSelectedIndex.get() >= 0 && this.codeSelectSelectedIndex.get() >= 0 &&
this.codeSelectSelectedIndex.get() < this.codeSelectBlockRefArray.length this.codeSelectSelectedIndex.get() < this.codeSelectBlockRefArray.length
) { ) {
let curBlockRef = this.codeSelectBlockRefArray[this.codeSelectSelectedIndex.get()]; const curBlockRef = this.codeSelectBlockRefArray[this.codeSelectSelectedIndex.get()];
let codeText = curBlockRef.current.innerText; const codeText = curBlockRef.current.innerText.replace(/\n$/, ""); // remove trailing newline
codeText = codeText.replace(/\n$/, ""); // remove trailing newline
this.setCurLine(codeText); this.setCurLine(codeText);
this.giveFocus(); this.giveFocus();
} }
@ -574,14 +596,13 @@ class InputModel {
mobx.action(() => { mobx.action(() => {
if (blockIndex >= 0 && blockIndex < this.codeSelectBlockRefArray.length) { if (blockIndex >= 0 && blockIndex < this.codeSelectBlockRefArray.length) {
this.codeSelectSelectedIndex.set(blockIndex); this.codeSelectSelectedIndex.set(blockIndex);
let currentRef = this.codeSelectBlockRefArray[blockIndex].current; const currentRef = this.codeSelectBlockRefArray[blockIndex].current;
if (currentRef != null) { if (currentRef != null && this.aiChatWindowRef?.current != null) {
if (this.aiChatWindowRef?.current != null) { const chatWindowTop = this.aiChatWindowRef.current.scrollTop;
let chatWindowTop = this.aiChatWindowRef.current.scrollTop; const chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100;
let chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100; const elemTop = currentRef.offsetTop;
let elemTop = currentRef.offsetTop;
let elemBottom = elemTop - currentRef.offsetHeight; let elemBottom = elemTop - currentRef.offsetHeight;
let elementIsInView = elemBottom < chatWindowBottom && elemTop > chatWindowTop; const elementIsInView = elemBottom < chatWindowBottom && elemTop > chatWindowTop;
if (!elementIsInView) { if (!elementIsInView) {
this.aiChatWindowRef.current.scrollTop = this.aiChatWindowRef.current.scrollTop =
elemBottom - this.aiChatWindowRef.current.clientHeight / 3; elemBottom - this.aiChatWindowRef.current.clientHeight / 3;
@ -590,7 +611,6 @@ class InputModel {
} }
this.codeSelectBlockRefArray = []; this.codeSelectBlockRefArray = [];
this.setAIChatFocus(); this.setAIChatFocus();
}
})(); })();
} }
@ -603,7 +623,7 @@ class InputModel {
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) { } else if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
return; return;
} }
let incBlockIndex = this.codeSelectSelectedIndex.get() + 1; const incBlockIndex = this.codeSelectSelectedIndex.get() + 1;
if (this.codeSelectSelectedIndex.get() == this.codeSelectBlockRefArray.length - 1) { if (this.codeSelectSelectedIndex.get() == this.codeSelectBlockRefArray.length - 1) {
this.codeSelectDeselectAll(); this.codeSelectDeselectAll();
if (this.aiChatWindowRef?.current != null) { if (this.aiChatWindowRef?.current != null) {
@ -627,7 +647,7 @@ class InputModel {
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) { } else if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
return; return;
} }
let decBlockIndex = this.codeSelectSelectedIndex.get() - 1; const decBlockIndex = this.codeSelectSelectedIndex.get() - 1;
if (decBlockIndex < 0) { if (decBlockIndex < 0) {
this.codeSelectDeselectAll(this.codeSelectTop); this.codeSelectDeselectAll(this.codeSelectTop);
if (this.aiChatWindowRef?.current != null) { if (this.aiChatWindowRef?.current != null) {
@ -666,6 +686,8 @@ class InputModel {
mobx.action(() => { mobx.action(() => {
this.setInputPopUpType("aichat"); this.setInputPopUpType("aichat");
this.aIChatShow.set(true); this.aIChatShow.set(true);
this.historyShow.set(false);
this.infoShow.set(false);
this.setAIChatFocus(); this.setAIChatFocus();
})(); })();
} }
@ -686,7 +708,7 @@ class InputModel {
} }
clearAIAssistantChat(): void { clearAIAssistantChat(): void {
let prtn = this.globalModel.submitChatInfoCommand("", "", true); const prtn = this.globalModel.submitChatInfoCommand("", "", true);
prtn.then((rtn) => { prtn.then((rtn) => {
if (!rtn.success) { if (!rtn.success) {
console.log("submit chat command error: " + rtn.error); console.log("submit chat command error: " + rtn.error);
@ -700,11 +722,11 @@ class InputModel {
if (!this.infoShow.get()) { if (!this.infoShow.get()) {
return false; return false;
} }
let info = this.infoMsg.get(); const info = this.infoMsg.get();
if (info == null) { if (info == null) {
return false; return false;
} }
let div = document.querySelector(".cmd-input-info"); const div = document.querySelector(".cmd-input-info");
if (div == null) { if (div == null) {
return false; return false;
} }
@ -736,7 +758,7 @@ class InputModel {
this.setHistoryShow(false); this.setHistoryShow(false);
return; return;
} }
let isShowing = this.infoShow.get(); const isShowing = this.infoShow.get();
if (isShowing) { if (isShowing) {
this.infoShow.set(false); this.infoShow.set(false);
} else { } else {
@ -750,7 +772,7 @@ class InputModel {
@boundMethod @boundMethod
uiSubmitCommand(): void { uiSubmitCommand(): void {
mobx.action(() => { mobx.action(() => {
let commandStr = this.getCurLine(); const commandStr = this.getCurLine();
if (commandStr.trim() == "") { if (commandStr.trim() == "") {
return; return;
} }
@ -771,7 +793,7 @@ class InputModel {
} }
setCurLine(val: string): void { setCurLine(val: string): void {
let hidx = this.historyIndex.get(); const hidx = this.historyIndex.get();
mobx.action(() => { mobx.action(() => {
if (this.modHistory.length <= hidx) { if (this.modHistory.length <= hidx) {
this.modHistory.length = hidx + 1; this.modHistory.length = hidx + 1;
@ -803,15 +825,15 @@ class InputModel {
} }
getCurLine(): string { getCurLine(): string {
let hidx = this.historyIndex.get(); const hidx = this.historyIndex.get();
if (hidx < this.modHistory.length && this.modHistory[hidx] != null) { if (hidx < this.modHistory.length && this.modHistory[hidx] != null) {
return this.modHistory[hidx]; return this.modHistory[hidx];
} }
let hitems = this.getFilteredHistoryItems(); const hitems = this.getFilteredHistoryItems();
if (hidx == 0 || hitems == null || hidx > hitems.length) { if (hidx == 0 || hitems == null || hidx > hitems.length) {
return ""; return "";
} }
let hitem = hitems[hidx - 1]; const hitem = hitems[hidx - 1];
if (hitem == null) { if (hitem == null) {
return ""; return "";
} }

View File

@ -23,7 +23,7 @@ function getMonoFontSize(fontSize: number): MonoFontSize {
if (MonoFontSizes[fontSize] != null) { if (MonoFontSizes[fontSize] != null) {
return MonoFontSizes[fontSize]; return MonoFontSizes[fontSize];
} }
let size = measureText("W", { pre: true, mono: true, fontSize: fontSize }); const size = measureText("W", { pre: true, mono: true, fontSize: fontSize });
if (size.height != 0 && size.width != 0) { if (size.height != 0 && size.width != 0) {
MonoFontSizes[fontSize] = size; MonoFontSizes[fontSize] = size;
} }
@ -38,7 +38,7 @@ function measureText(text: string, textOpts: { pre?: boolean; mono?: boolean; fo
if (textOpts == null) { if (textOpts == null) {
throw new Error("invalid textOpts passed to measureText (null)"); throw new Error("invalid textOpts passed to measureText (null)");
} }
let textElem = document.createElement("span"); const textElem = document.createElement("span");
if (textOpts.pre) { if (textOpts.pre) {
textElem.classList.add("pre"); textElem.classList.add("pre");
} }
@ -53,19 +53,19 @@ function measureText(text: string, textOpts: { pre?: boolean; mono?: boolean; fo
} }
} }
textElem.innerText = text; textElem.innerText = text;
let measureDiv = document.getElementById("measure"); const measureDiv = document.getElementById("measure");
if (measureDiv == null) { if (measureDiv == null) {
throw new Error("cannot measure text, no #measure div"); throw new Error("cannot measure text, no #measure div");
} }
measureDiv.replaceChildren(textElem); measureDiv.replaceChildren(textElem);
let height = Math.ceil(textElem.offsetHeight); const height = Math.ceil(textElem.offsetHeight);
let width = textElem.offsetWidth; const width = textElem.offsetWidth;
let pad = Math.floor(height / 2); const pad = Math.floor(height / 2);
return { width, height, pad, fontSize: textOpts.fontSize }; return { width, height, pad, fontSize: textOpts.fontSize };
} }
function windowWidthToCols(width: number, fontSize: number): number { function windowWidthToCols(width: number, fontSize: number): number {
let dr = getMonoFontSize(fontSize); const dr = getMonoFontSize(fontSize);
let cols = Math.trunc((width - MagicLayout.ScreenMaxContentWidthBuffer) / dr.width) - 1; let cols = Math.trunc((width - MagicLayout.ScreenMaxContentWidthBuffer) / dr.width) - 1;
cols = boundInt(cols, MinTermCols, MaxTermCols); cols = boundInt(cols, MinTermCols, MaxTermCols);
return cols; return cols;
@ -80,7 +80,7 @@ function windowHeightToRows(lhe: LineHeightEnv, height: number): number {
} }
function termWidthFromCols(cols: number, fontSize: number): number { function termWidthFromCols(cols: number, fontSize: number): number {
let dr = getMonoFontSize(fontSize); const dr = getMonoFontSize(fontSize);
return Math.ceil(dr.width * cols) + MagicLayout.TermWidthBuffer; return Math.ceil(dr.width * cols) + MagicLayout.TermWidthBuffer;
} }
@ -89,12 +89,12 @@ function termWidthFromCols(cols: number, fontSize: number): number {
// works out to `realHeight = round(ceil(height * dpr) * rows / dpr) / rows` // works out to `realHeight = round(ceil(height * dpr) * rows / dpr) / rows`
// their calculation is based off the "totalRows" (so that argument has been added) // their calculation is based off the "totalRows" (so that argument has been added)
function termHeightFromRows(rows: number, fontSize: number, totalRows: number): number { function termHeightFromRows(rows: number, fontSize: number, totalRows: number): number {
let dr = getMonoFontSize(fontSize); const dr = getMonoFontSize(fontSize);
const dpr = window.devicePixelRatio; const dpr = window.devicePixelRatio;
if (totalRows == null || totalRows == 0) { if (totalRows == null || totalRows == 0) {
totalRows = rows > 25 ? rows : 25; totalRows = rows > 25 ? rows : 25;
} }
let realHeight = Math.round((Math.ceil(dr.height * dpr) * totalRows) / dpr) / totalRows; const realHeight = Math.round((Math.ceil(dr.height * dpr) * totalRows) / dpr) / totalRows;
return Math.ceil(realHeight * rows); return Math.ceil(realHeight * rows);
} }