diff --git a/src/app/appconst.ts b/src/app/appconst.ts index 0acecdfc..062b36e9 100644 --- a/src/app/appconst.ts +++ b/src/app/appconst.ts @@ -64,3 +64,7 @@ export enum StatusIndicatorLevel { // matches packet.go export const ErrorCode_InvalidCwd = "ERRCWD"; + +export const InputAuxView_History = "history"; +export const InputAuxView_Info = "info"; +export const InputAuxView_AIChat = "aichat"; diff --git a/src/app/workspace/cmdinput/aichat.tsx b/src/app/workspace/cmdinput/aichat.tsx index 5adfbf56..72e1072d 100644 --- a/src/app/workspace/cmdinput/aichat.tsx +++ b/src/app/workspace/cmdinput/aichat.tsx @@ -27,7 +27,7 @@ class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> { return true; }); keybindManager.registerKeybinding("pane", "aichat", "generic:cancel", (waveEvent) => { - inputModel.closeAIAssistantChat(true); + inputModel.closeAuxView(); return true; }); keybindManager.registerKeybinding("pane", "aichat", "aichat:clearHistory", (waveEvent) => { @@ -70,7 +70,7 @@ class AIChat extends React.Component<{}, {}> { componentDidMount() { const inputModel = GlobalModel.inputModel; - if (this.chatWindowScrollRef != null && this.chatWindowScrollRef.current != null) { + if (this.chatWindowScrollRef?.current != null) { this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight; } if (this.textAreaRef.current != null) { @@ -82,7 +82,7 @@ class AIChat extends React.Component<{}, {}> { } componentDidUpdate() { - if (this.chatWindowScrollRef != null && this.chatWindowScrollRef.current != null) { + if (this.chatWindowScrollRef?.current != null) { this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight; } } @@ -252,7 +252,7 @@ class AIChat extends React.Component<{}, {}> { GlobalModel.inputModel.closeAIAssistantChat(true)} + onClose={() => GlobalModel.inputModel.closeAuxView()} iconClass="fa-sharp fa-solid fa-sparkles" > diff --git a/src/app/workspace/cmdinput/cmdinput.less b/src/app/workspace/cmdinput/cmdinput.less index 97d02351..66785d4e 100644 --- a/src/app/workspace/cmdinput/cmdinput.less +++ b/src/app/workspace/cmdinput/cmdinput.less @@ -4,12 +4,11 @@ max-height: max(300px, 40%); display: flex; flex-direction: column; - position: absolute; - bottom: 0; width: 100%; z-index: 100; border-top: 2px solid var(--app-border-color); background-color: var(--app-bg-color); + position: relative; // Apply a border between the base cmdinput and any views shown above it // TODO: use a generic selector for this diff --git a/src/app/workspace/cmdinput/cmdinput.tsx b/src/app/workspace/cmdinput/cmdinput.tsx index 6410f56a..002116d8 100644 --- a/src/app/workspace/cmdinput/cmdinput.tsx +++ b/src/app/workspace/cmdinput/cmdinput.tsx @@ -5,7 +5,7 @@ import * as React from "react"; import * as mobxReact from "mobx-react"; import * as mobx from "mobx"; import { boundMethod } from "autobind-decorator"; -import { If } from "tsx-control-statements/components"; +import { Choose, If, When } from "tsx-control-statements/components"; import cn from "classnames"; import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; @@ -18,6 +18,7 @@ import { Prompt } from "@/common/prompt/prompt"; import { CenteredIcon, RotateIcon } from "@/common/icons/icons"; import { AIChat } from "./aichat"; import * as util from "@/util/util"; +import * as appconst from "@/app/appconst"; import "./cmdinput.less"; @@ -71,7 +72,7 @@ class CmdInput extends React.Component<{}, {}> { e.stopPropagation(); return; } - GlobalModel.inputModel.setHistoryFocus(false); + GlobalModel.inputModel.setAuxViewFocus(false); GlobalModel.inputModel.giveFocus(); } @@ -80,8 +81,8 @@ class CmdInput extends React.Component<{}, {}> { e.preventDefault(); e.stopPropagation(); const inputModel = GlobalModel.inputModel; - if (inputModel.aIChatShow.get()) { - inputModel.closeAIAssistantChat(true); + if (inputModel.getActiveAuxView() === appconst.InputAuxView_AIChat) { + inputModel.closeAuxView(); } else { inputModel.openAIAssistantChat(); } @@ -93,7 +94,7 @@ class CmdInput extends React.Component<{}, {}> { e.stopPropagation(); const inputModel = GlobalModel.inputModel; - if (inputModel.historyShow.get()) { + if (inputModel.getActiveAuxView() === appconst.InputAuxView_History) { inputModel.resetHistory(); } else { inputModel.openHistory(); @@ -155,9 +156,6 @@ class CmdInput extends React.Component<{}, {}> { remote = GlobalModel.getRemote(rptr.remoteid); } feState = feState || {}; - const infoShow = inputModel.infoShow.get(); - const historyShow = !infoShow && inputModel.historyShow.get(); - const aiChatShow = inputModel.aIChatShow.get(); const focusVal = inputModel.physicalInputFocused.get(); const inputMode: string = inputModel.inputMode.get(); const textAreaInputKey = screen == null ? "null" : screen.screenId; @@ -170,7 +168,7 @@ class CmdInput extends React.Component<{}, {}> { let shellInitMsg: string = null; let hidePrompt = false; - const openView = inputModel.getOpenView(); + const openView = inputModel.getActiveAuxView(); const hasOpenView = openView ? `has-${openView}` : null; if (ri == null) { let shellStr = "shell"; @@ -185,15 +183,19 @@ class CmdInput extends React.Component<{}, {}> { } return (
- -
- -
- -
- -
- + + +
+ +
+ +
+ +
+ + + +
WARNING:  diff --git a/src/app/workspace/cmdinput/historyinfo.tsx b/src/app/workspace/cmdinput/historyinfo.tsx index e188139b..cbe6cdf8 100644 --- a/src/app/workspace/cmdinput/historyinfo.tsx +++ b/src/app/workspace/cmdinput/historyinfo.tsx @@ -167,14 +167,14 @@ class HistoryInfo extends React.Component<{}, {}> { @boundMethod handleClose() { - GlobalModel.inputModel.toggleInfoMsg(); + GlobalModel.inputModel.closeAuxView(); } @boundMethod handleItemClick(hitem: HistoryItem) { const inputModel = GlobalModel.inputModel; const selItem = inputModel.getHistorySelectedItem(); - inputModel.setHistoryFocus(true); + inputModel.setAuxViewFocus(false); if (this.lastClickHNum == hitem.historynum && selItem != null && selItem.historynum == hitem.historynum) { inputModel.grabSelectedHistoryItem(); return; @@ -194,14 +194,14 @@ class HistoryInfo extends React.Component<{}, {}> { @boundMethod handleClickType() { const inputModel = GlobalModel.inputModel; - inputModel.setHistoryFocus(true); + inputModel.setAuxViewFocus(true); inputModel.toggleHistoryType(); } @boundMethod handleClickRemote() { const inputModel = GlobalModel.inputModel; - inputModel.setHistoryFocus(true); + inputModel.setAuxViewFocus(true); inputModel.toggleRemoteType(); } diff --git a/src/app/workspace/cmdinput/infomsg.tsx b/src/app/workspace/cmdinput/infomsg.tsx index 6c21864e..f7143f91 100644 --- a/src/app/workspace/cmdinput/infomsg.tsx +++ b/src/app/workspace/cmdinput/infomsg.tsx @@ -45,7 +45,7 @@ class InfoMsg extends React.Component<{}, {}> { render() { const inputModel = GlobalModel.inputModel; const infoMsg: InfoType = inputModel.infoMsg.get(); - const infoShow: boolean = inputModel.infoShow.get(); + const infoShow = inputModel.getActiveAuxView() == appconst.InputAuxView_Info; let line: string = null; let istr: string = null; let idx: number = 0; diff --git a/src/app/workspace/cmdinput/textareainput.tsx b/src/app/workspace/cmdinput/textareainput.tsx index 354cae50..b0fe2630 100644 --- a/src/app/workspace/cmdinput/textareainput.tsx +++ b/src/app/workspace/cmdinput/textareainput.tsx @@ -152,11 +152,7 @@ class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput } }); keybindManager.registerKeybinding("pane", "cmdinput", "generic:cancel", (waveEvent) => { GlobalModel.closeTabSettings(); - inputModel.toggleInfoMsg(); - if (inputModel.inputMode.get() != null) { - inputModel.resetInputMode(); - } - inputModel.closeAIAssistantChat(true); + inputModel.closeAuxView(); return true; }); keybindManager.registerKeybinding("pane", "cmdinput", "cmdinput:expandInput", (waveEvent) => { @@ -254,8 +250,6 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () lastHeight: number = 0; lastSP: StrWithPos = { str: "", pos: appconst.NoStrPos }; version: OV = mobx.observable.box(0, { name: "textAreaInput-version" }); // forces render updates - mainInputFocused: OV = mobx.observable.box(true, { name: "textAreaInput-mainInputFocused" }); - historyFocused: OV = mobx.observable.box(false, { name: "textAreaInput-historyFocused" }); incVersion(): void { const v = this.version.get(); @@ -286,16 +280,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () } setFocus(): void { - const inputModel = GlobalModel.inputModel; - if (inputModel.historyFocus.get()) { - if (this.historyInputRef.current != null && document.activeElement != this.historyInputRef.current) { - this.historyInputRef.current.focus(); - } - } else { - if (this.mainInputRef.current != null && document.activeElement != this.mainInputRef.current) { - this.mainInputRef.current.focus(); - } - } + GlobalModel.inputModel.giveFocus(); } getTextAreaMaxCols(): number { @@ -536,7 +521,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () if (selStart > value.length || selEnd > value.length) { return; } - const newValue = value.substr(0, selStart) + clipText + value.substr(selEnd); + const newValue = value.substring(0, selStart) + clipText + value.substring(selEnd); const cmdLineUpdate = { str: newValue, pos: selStart + clipText.length }; GlobalModel.inputModel.updateCmdLine(cmdLineUpdate); }); @@ -553,19 +538,9 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () } @boundMethod - handleMainFocus(e: any) { - const inputModel = GlobalModel.inputModel; - if (inputModel.historyFocus.get()) { - e.preventDefault(); - if (this.historyInputRef.current != null) { - this.historyInputRef.current.focus(); - } - return; - } - inputModel.setPhysicalInputFocused(true); - mobx.action(() => { - this.mainInputFocused.set(true); - })(); + handleFocus(e: any) { + e.preventDefault(); + GlobalModel.inputModel.giveFocus(); } @boundMethod @@ -574,25 +549,6 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () return; } GlobalModel.inputModel.setPhysicalInputFocused(false); - mobx.action(() => { - this.mainInputFocused.set(false); - })(); - } - - @boundMethod - handleHistoryFocus(e: any) { - const inputModel = GlobalModel.inputModel; - if (!inputModel.historyFocus.get()) { - e.preventDefault(); - if (this.mainInputRef.current != null) { - this.mainInputRef.current.focus(); - } - return; - } - inputModel.setPhysicalInputFocused(true); - mobx.action(() => { - this.historyFocused.set(true); - })(); } @boundMethod @@ -601,9 +557,6 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () return; } GlobalModel.inputModel.setPhysicalInputFocused(false); - mobx.action(() => { - this.historyFocused.set(false); - })(); } render() { @@ -621,9 +574,8 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () displayLines = 5; } - // 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) { + const auxViewFocused = inputModel.getAuxViewFocus(); + if (auxViewFocused) { displayLines = 1; } const activeScreen = GlobalModel.getActiveScreen(); @@ -639,7 +591,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () const screen = GlobalModel.getActiveScreen(); if (screen != null) { const ri = screen.getCurRemoteInstance(); - if (ri != null && ri.shelltype != null) { + if (ri?.shelltype != null) { shellType = ri.shelltype; } if (shellType == "") { @@ -652,15 +604,14 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () } } } - const isMainInputFocused = inputModel.hasFocus() && this.mainInputFocused.get(); - const isHistoryFocused = this.historyFocused.get(); + const isHistoryFocused = auxViewFocused && inputModel.getActiveAuxView() == appconst.InputAuxView_History; return (
- + @@ -677,7 +628,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () autoComplete="off" autoCorrect="off" id="main-cmd-input" - onFocus={this.handleMainFocus} + onFocus={this.handleFocus} onBlur={this.handleMainBlur} style={{ height: computedInnerHeight, minHeight: computedInnerHeight, fontSize: termFontSize }} value={curLine} @@ -685,7 +636,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () onChange={this.onChange} onSelect={this.onSelect} placeholder="Type here..." - className={cn("textarea", { "display-disabled": disabled })} + className={cn("textarea", { "display-disabled": auxViewFocused })} > { session={session} screen={activeScreen} /> -
diff --git a/src/models/input.ts b/src/models/input.ts index 88b34fb4..3957fa2e 100644 --- a/src/models/input.ts +++ b/src/models/input.ts @@ -8,6 +8,7 @@ import { isBlank } from "@/util/util"; import * as appconst from "@/app/appconst"; import { Model } from "./model"; import { GlobalCommandRunner } from "./global"; +import { app } from "electron"; function getDefaultHistoryQueryOpts(): HistoryQueryOpts { return { @@ -24,10 +25,8 @@ function getDefaultHistoryQueryOpts(): HistoryQueryOpts { class InputModel { globalModel: Model; - historyShow: OV = mobx.observable.box(false); - historyFocus: OV = mobx.observable.box(false); - infoShow: OV = mobx.observable.box(false); - aIChatShow: OV = mobx.observable.box(false); + activeAuxView: OV = mobx.observable.box(null); + auxViewFocus: OV = mobx.observable.box(false); cmdInputHeight: OV = mobx.observable.box(0); aiChatTextAreaRef: React.RefObject; aiChatWindowRef: React.RefObject; @@ -139,26 +138,39 @@ class InputModel { })(); } - _focusCmdInput(): void { - const elem = document.getElementById("main-cmd-input"); - if (elem != null) { - elem.focus(); - } - } - - _focusHistoryInput(): void { - const elem: HTMLElement = document.querySelector(".cmd-input input.history-input"); - if (elem != null) { - elem.focus(); - } - } - + // Focuses the main input or the auxiliary view, depending on the active auxiliary view giveFocus(): void { - if (this.historyFocus.get()) { - this._focusHistoryInput(); - } else { - this._focusCmdInput(); - } + // Override active view to the main input if aux view does not have focus + const activeAuxView = this.getAuxViewFocus() ? this.getActiveAuxView() : null; + mobx.action(() => { + switch (activeAuxView) { + case appconst.InputAuxView_History: { + const elem: HTMLElement = document.querySelector(".cmd-input input.history-input"); + if (elem != null) { + elem.focus(); + } + break; + } + case "aichat": + this.setAIChatFocus(); + break; + case null: { + const elem = document.getElementById("main-cmd-input"); + if (elem != null) { + elem.focus(); + } + this.setPhysicalInputFocused(true); + break; + } + default: { + const elem: HTMLElement = document.querySelector(".cmd-input .auxview"); + if (elem != null) { + elem.focus(); + } + break; + } + } + })(); } setPhysicalInputFocused(isFocused: boolean): void { @@ -191,19 +203,6 @@ class InputModel { 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 { if (this.historyQueryOpts.get().queryType == htype) { return; @@ -244,45 +243,11 @@ class InputModel { })(); } - setInputPopUpType(type: string) { - this.inputPopUpType = type; - this.aIChatShow.set(type == "aichat"); - this.historyShow.set(type == "history"); - } - setOpenAICmdInfoChat(chat: OpenAICmdInfoChatMessageType[]): void { this.AICmdInfoChatItems.replace(chat); this.codeSelectBlockRefArray = []; } - setHistoryShow(show: boolean): void { - if (this.historyShow.get() == show) { - return; - } - mobx.action(() => { - if (show) { - this.setInputPopUpType("history"); - } else { - this.setInputPopUpType("none"); - } - this.historyShow.set(show); - this.historyFocus.set(show); - if (this.hasFocus()) { - this.giveFocus(); - } - })(); - } - - setHistoryFocus(focus: boolean): void { - if (this.historyFocus.get() == focus) { - return; - } - mobx.action(() => { - this.historyFocus.set(focus); - this.giveFocus(); - })(); - } - isHistoryLoaded(): boolean { if (this.historyLoading.get()) { return false; @@ -315,14 +280,9 @@ class InputModel { this.loadHistory(true, 0, "screen"); return; } - if (!this.historyShow.get()) { - mobx.action(() => { - this.setHistoryShow(true); - this.aIChatShow.set(false); - this.infoShow.set(false); - this.dropModHistory(true); - this.giveFocus(); - })(); + if (this.getActiveAuxView() != appconst.InputAuxView_History) { + this.dropModHistory(true); + this.setActiveAuxView(appconst.InputAuxView_History); } } @@ -495,6 +455,51 @@ class InputModel { })(); } + // Closes the auxiliary view if it is open, focuses the main input + closeAuxView(): void { + if (this.activeAuxView.get() == null) { + return; + } + this.setActiveAuxView(null); + } + + // Gets the active auxiliary view, or null if none + getActiveAuxView(): InputAuxViewType { + return this.activeAuxView.get(); + } + + // Sets the active auxiliary view + setActiveAuxView(view: InputAuxViewType): void { + if (view == this.activeAuxView.get()) { + return; + } + mobx.action(() => { + this.auxViewFocus.set(view != null); + this.activeAuxView.set(view); + })(); + this.giveFocus(); + } + + // Gets the focus state of the auxiliary view. If true, the view will get focus. Otherwise, the main input will get focus. + // If the auxiliary view is not open, this will return false. + getAuxViewFocus(): boolean { + if (this.getActiveAuxView() == null) { + return false; + } + return this.auxViewFocus.get(); + } + + // Sets the focus state of the auxiliary view. If true, the view will get focus. Otherwise, the main input will get focus. + setAuxViewFocus(focus: boolean): void { + if (this.getAuxViewFocus() == focus) { + return; + } + mobx.action(() => { + this.auxViewFocus.set(focus); + })(); + this.giveFocus(); + } + setHistoryIndex(hidx: number, force?: boolean): void { if (hidx < 0) { return; @@ -504,7 +509,7 @@ class InputModel { } mobx.action(() => { this.historyIndex.set(hidx); - if (this.historyShow.get()) { + if (this.getActiveAuxView() == appconst.InputAuxView_History) { let hitem = this.getHistorySelectedItem(); if (hitem == null) { hitem = this.getFirstHistoryItem(); @@ -538,16 +543,18 @@ class InputModel { this._clearInfoTimeout(); mobx.action(() => { this.infoMsg.set(info); - if (info == null) { - this.infoShow.set(false); - } else { - this.infoShow.set(true); - this.setHistoryShow(false); - } })(); + + if (info == null && this.getActiveAuxView() == appconst.InputAuxView_Info) { + this.setActiveAuxView(null); + } else { + this.setActiveAuxView(appconst.InputAuxView_Info); + } + if (info != null && timeoutMs) { this.infoTimeoutId = setTimeout(() => { - if (this.historyShow.get()) { + console.log("clearing info msg"); + if (this.activeAuxView.get() != appconst.InputAuxView_Info) { return; } this.clearInfoMsg(false); @@ -683,28 +690,7 @@ class InputModel { } openAIAssistantChat(): void { - mobx.action(() => { - this.setInputPopUpType("aichat"); - this.aIChatShow.set(true); - this.historyShow.set(false); - this.infoShow.set(false); - this.setAIChatFocus(); - })(); - } - - // pass true to give focus to the input (e.g. if this is an 'active' close of the chat) - // when resetting the input (when switching screens, don't give focus) - closeAIAssistantChat(giveFocus: boolean): void { - if (!this.aIChatShow.get()) { - return; - } - mobx.action(() => { - this.setInputPopUpType("none"); - this.aIChatShow.set(false); - if (giveFocus) { - this.giveFocus(); - } - })(); + this.setActiveAuxView(appconst.InputAuxView_AIChat); } clearAIAssistantChat(): void { @@ -719,7 +705,7 @@ class InputModel { } hasScrollingInfoMsg(): boolean { - if (!this.infoShow.get()) { + if (this.activeAuxView.get() !== appconst.InputAuxView_Info) { return false; } const info = this.infoMsg.get(); @@ -742,9 +728,11 @@ class InputModel { clearInfoMsg(setNull: boolean): void { this._clearInfoTimeout(); + + if (this.getActiveAuxView() == appconst.InputAuxView_Info) { + this.setActiveAuxView(null); + } mobx.action(() => { - this.setHistoryShow(false); - this.infoShow.set(false); if (setNull) { this.infoMsg.set(null); } @@ -753,20 +741,11 @@ class InputModel { toggleInfoMsg(): void { this._clearInfoTimeout(); - mobx.action(() => { - if (this.historyShow.get()) { - this.setHistoryShow(false); - return; - } - const isShowing = this.infoShow.get(); - if (isShowing) { - this.infoShow.set(false); - } else { - if (this.infoMsg.get() != null) { - this.infoShow.set(true); - } - } - })(); + if (this.activeAuxView.get() == appconst.InputAuxView_Info) { + this.setActiveAuxView(null); + } else if (this.infoMsg.get() != null) { + this.setActiveAuxView(appconst.InputAuxView_Info); + } } @boundMethod @@ -804,9 +783,7 @@ class InputModel { resetInput(): void { mobx.action(() => { - this.setHistoryShow(false); - this.closeAIAssistantChat(false); - this.infoShow.set(false); + this.setActiveAuxView(null); this.inputMode.set(null); this.resetHistory(); this.dropModHistory(false); @@ -854,7 +831,9 @@ class InputModel { resetHistory(): void { mobx.action(() => { - this.setHistoryShow(false); + if (this.getActiveAuxView() == appconst.InputAuxView_History) { + this.setActiveAuxView(null); + } this.historyLoading.set(false); this.historyType.set("screen"); this.historyItems.set(null); diff --git a/src/models/model.ts b/src/models/model.ts index 8175de32..0bf49e82 100644 --- a/src/models/model.ts +++ b/src/models/model.ts @@ -1082,7 +1082,7 @@ class Model { this.ws.watchScreen(newActiveSessionId, newActiveScreenId); this.closeTabSettings(); const activeScreen = this.getActiveScreen(); - if (activeScreen != null && activeScreen.getCurRemoteInstance() != null) { + if (activeScreen?.getCurRemoteInstance() != null) { setTimeout(() => { GlobalCommandRunner.syncShellState(); }, 100); diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts index b7c0a6bd..30003e0a 100644 --- a/src/types/custom.d.ts +++ b/src/types/custom.d.ts @@ -15,6 +15,7 @@ declare global { type LineContainerStrs = "main" | "sidebar" | "history"; type AppUpdateStatusType = "unavailable" | "ready"; type NativeThemeSource = "system" | "light" | "dark"; + type InputAuxViewType = null | "history" | "info" | "aichat"; type OV = mobx.IObservableValue; type OArr = mobx.IObservableArray;