From c6a8797ddd2b3efc4b0be1acd3d14e47ca677ff8 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 29 Apr 2024 18:29:27 -0700 Subject: [PATCH] Break out non-autocomplete changes from autocomplete PR (#618) This improves the ephemeral command runner to allow for honoring of timeouts and proper handling of overriding the current working directory. It also fixes some partially transparent font colors in light mode, making them solid instead. It also updates the InputModel to be auto-observable and utilize some getters to ensure the cmdinput is getting updated whenever necessary state changes take place. --- public/themes/light.css | 4 +- src/app/workspace/cmdinput/aichat.tsx | 19 +- src/app/workspace/cmdinput/cmdinput.less | 4 +- src/app/workspace/cmdinput/cmdinput.tsx | 12 +- src/app/workspace/cmdinput/historyinfo.less | 1 + src/app/workspace/cmdinput/historyinfo.tsx | 10 +- src/app/workspace/cmdinput/textareainput.tsx | 39 +- src/models/input.ts | 489 +++++++++---------- src/models/model.ts | 60 ++- src/types/custom.d.ts | 1 + waveshell/pkg/packet/packet.go | 1 + waveshell/pkg/shexec/shexec.go | 13 +- wavesrv/pkg/bufferedpipe/bufferedpipe.go | 2 + wavesrv/pkg/remote/remote.go | 27 +- webpack.config.js | 4 +- 15 files changed, 362 insertions(+), 324 deletions(-) diff --git a/public/themes/light.css b/public/themes/light.css index 30a9194ef..68aa518af 100644 --- a/public/themes/light.css +++ b/public/themes/light.css @@ -9,8 +9,8 @@ --app-accent-color: rgb(75, 166, 57); --app-accent-bg-color: rgba(75, 166, 57, 0.2); --app-text-color: rgb(0, 0, 0); - --app-text-primary-color: rgb(0, 0, 0, 0.9); - --app-text-secondary-color: rgb(0, 0, 0, 0.7); + --app-text-primary-color: rgb(23, 23, 23); + --app-text-secondary-color: rgb(76, 76, 76); --app-border-color: rgb(139 145 138); --app-panel-bg-color: rgb(224, 224, 224); --app-panel-bg-color-dev: rgb(224, 224, 224); diff --git a/src/app/workspace/cmdinput/aichat.tsx b/src/app/workspace/cmdinput/aichat.tsx index 8b5e76924..ee67c773f 100644 --- a/src/app/workspace/cmdinput/aichat.tsx +++ b/src/app/workspace/cmdinput/aichat.tsx @@ -88,7 +88,7 @@ class AIChat extends React.Component<{}, {}> { } submitChatMessage(messageStr: string) { - const curLine = GlobalModel.inputModel.getCurLine(); + const curLine = GlobalModel.inputModel.curLine; const prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false); prtn.then((rtn) => { if (!rtn.success) { @@ -103,15 +103,19 @@ class AIChat extends React.Component<{}, {}> { return { numLines, linePos }; } + @mobx.action.bound onTextAreaFocused(e: any) { GlobalModel.inputModel.setAuxViewFocus(true); + this.onTextAreaChange(e); } + @mobx.action.bound onTextAreaBlur(e: any) { GlobalModel.inputModel.setAuxViewFocus(false); } // Adjust the height of the textarea to fit the text + @boundMethod onTextAreaChange(e: any) { // Calculate the bounding height of the text area const textAreaMaxLines = 4; @@ -140,8 +144,10 @@ class AIChat extends React.Component<{}, {}> { this.submitChatMessage(messageStr); currentRef.value = ""; } else { - inputModel.grabCodeSelectSelection(); - inputModel.setAuxViewFocus(false); + mobx.action(() => { + inputModel.grabCodeSelectSelection(); + inputModel.setAuxViewFocus(false); + })(); } } @@ -182,7 +188,6 @@ class AIChat extends React.Component<{}, {}> { return true; } - @mobx.action @boundMethod onKeyDown(e: any) {} @@ -254,9 +259,9 @@ class AIChat extends React.Component<{}, {}> { autoComplete="off" autoCorrect="off" id="chat-cmd-input" - onFocus={this.onTextAreaFocused.bind(this)} - onBlur={this.onTextAreaBlur.bind(this)} - onChange={this.onTextAreaChange.bind(this)} + onFocus={this.onTextAreaFocused} + onBlur={this.onTextAreaBlur} + onChange={this.onTextAreaChange} onKeyDown={this.onKeyDown} style={{ fontSize: this.termFontSize }} className="chat-textarea" diff --git a/src/app/workspace/cmdinput/cmdinput.less b/src/app/workspace/cmdinput/cmdinput.less index 11d292230..865ef728c 100644 --- a/src/app/workspace/cmdinput/cmdinput.less +++ b/src/app/workspace/cmdinput/cmdinput.less @@ -193,8 +193,8 @@ } // 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; + // We don't need right margin because the whole input field is already padded. + margin: 2px 0 0 12px; cursor: pointer; } diff --git a/src/app/workspace/cmdinput/cmdinput.tsx b/src/app/workspace/cmdinput/cmdinput.tsx index cfa2326dc..b19a26a7c 100644 --- a/src/app/workspace/cmdinput/cmdinput.tsx +++ b/src/app/workspace/cmdinput/cmdinput.tsx @@ -56,7 +56,7 @@ class CmdInput extends React.Component<{}, {}> { this.updateCmdInputHeight(); } - @boundMethod + @mobx.action.bound clickFocusInputHint(): void { GlobalModel.inputModel.giveFocus(); } @@ -75,7 +75,7 @@ class CmdInput extends React.Component<{}, {}> { GlobalModel.inputModel.setAuxViewFocus(false); } - @boundMethod + @mobx.action.bound clickAIAction(e: any): void { e.preventDefault(); e.stopPropagation(); @@ -87,7 +87,7 @@ class CmdInput extends React.Component<{}, {}> { } } - @boundMethod + @mobx.action.bound clickHistoryAction(e: any): void { e.preventDefault(); e.stopPropagation(); @@ -105,11 +105,9 @@ class CmdInput extends React.Component<{}, {}> { GlobalCommandRunner.connectRemote(remoteId); } - @boundMethod + @mobx.action.bound toggleFilter(screen: Screen) { - mobx.action(() => { - screen.filterRunning.set(!screen.filterRunning.get()); - })(); + screen.filterRunning.set(!screen.filterRunning.get()); } @boundMethod diff --git a/src/app/workspace/cmdinput/historyinfo.less b/src/app/workspace/cmdinput/historyinfo.less index 333a7b36d..d2c9c4b6a 100644 --- a/src/app/workspace/cmdinput/historyinfo.less +++ b/src/app/workspace/cmdinput/historyinfo.less @@ -22,6 +22,7 @@ color: var(--app-text-color); display: flex; flex-direction: column-reverse; + min-height: 100%; .history-item { cursor: pointer; diff --git a/src/app/workspace/cmdinput/historyinfo.tsx b/src/app/workspace/cmdinput/historyinfo.tsx index a30cd2154..614afeff1 100644 --- a/src/app/workspace/cmdinput/historyinfo.tsx +++ b/src/app/workspace/cmdinput/historyinfo.tsx @@ -169,12 +169,12 @@ class HistoryInfo extends React.Component<{}, {}> { } } - @boundMethod + @mobx.action.bound handleClose() { GlobalModel.inputModel.closeAuxView(); } - @boundMethod + @mobx.action.bound handleItemClick(hitem: HistoryItem) { const inputModel = GlobalModel.inputModel; const selItem = inputModel.getHistorySelectedItem(); @@ -195,14 +195,14 @@ class HistoryInfo extends React.Component<{}, {}> { }, 3000); } - @boundMethod + @mobx.action.bound handleClickType() { const inputModel = GlobalModel.inputModel; inputModel.setAuxViewFocus(true); inputModel.toggleHistoryType(); } - @boundMethod + @mobx.action.bound handleClickRemote() { const inputModel = GlobalModel.inputModel; inputModel.setAuxViewFocus(true); @@ -229,7 +229,7 @@ class HistoryInfo extends React.Component<{}, {}> { render() { const inputModel = GlobalModel.inputModel; const selItem = inputModel.getHistorySelectedItem(); - const hitems = inputModel.getFilteredHistoryItems(); + const hitems = inputModel.filteredHistoryItems; const opts = inputModel.historyQueryOpts.get(); let hitem: HistoryItem = null; let snames: Record = {}; diff --git a/src/app/workspace/cmdinput/textareainput.tsx b/src/app/workspace/cmdinput/textareainput.tsx index 60c1f90e8..ed3e3385f 100644 --- a/src/app/workspace/cmdinput/textareainput.tsx +++ b/src/app/workspace/cmdinput/textareainput.tsx @@ -117,7 +117,7 @@ class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput } const lastTab = this.lastTab; this.lastTab = true; this.curPress = "tab"; - const curLine = inputModel.getCurLine(); + const curLine = inputModel.curLine; if (lastTab) { GlobalModel.submitCommand( "_compgen", @@ -250,9 +250,10 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () lastSP: StrWithPos = { str: "", pos: appconst.NoStrPos }; version: OV = mobx.observable.box(0, { name: "textAreaInput-version" }); // forces render updates + @mobx.action incVersion(): void { const v = this.version.get(); - mobx.action(() => this.version.set(v + 1))(); + this.version.set(v + 1); } getCurSP(): StrWithPos { @@ -278,6 +279,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () GlobalModel.sendCmdInputText(this.props.screen.screenId, curSP); } + @mobx.action setFocus(): void { GlobalModel.inputModel.giveFocus(); } @@ -311,6 +313,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () } } + @mobx.action componentDidMount() { const activeScreen = GlobalModel.getActiveScreen(); if (activeScreen != null) { @@ -324,6 +327,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () this.updateSP(); } + @mobx.action componentDidUpdate() { const activeScreen = GlobalModel.getActiveScreen(); if (activeScreen != null) { @@ -340,7 +344,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () this.mainInputRef.current.selectionStart = fcpos; this.mainInputRef.current.selectionEnd = fcpos; } - mobx.action(() => inputModel.forceCursorPos.set(null))(); + inputModel.forceCursorPos.set(null); } if (inputModel.forceInputFocus) { inputModel.forceInputFocus = false; @@ -414,21 +418,18 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () return; } currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end"); - GlobalModel.inputModel.setCurLine(currentRef.value); + GlobalModel.inputModel.curLine = currentRef.value; } - @mobx.action @boundMethod onKeyDown(e: any) {} - @boundMethod + @mobx.action.bound onChange(e: any) { - mobx.action(() => { - GlobalModel.inputModel.setCurLine(e.target.value); - })(); + GlobalModel.inputModel.curLine = e.target.value; } - @boundMethod + @mobx.action.bound onSelect(e: any) { this.incVersion(); } @@ -453,7 +454,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () GlobalModel.inputModel.updateCmdLine(cmdLineUpdate); } - @boundMethod + @mobx.action.bound controlP() { const inputModel = GlobalModel.inputModel; if (!inputModel.isHistoryLoaded()) { @@ -465,7 +466,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () this.lastHistoryUpDown = true; } - @boundMethod + @mobx.action.bound controlN() { const inputModel = GlobalModel.inputModel; inputModel.moveHistorySelection(-1); @@ -526,17 +527,15 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () }); } - @boundMethod + @mobx.action.bound handleHistoryInput(e: any) { const inputModel = GlobalModel.inputModel; - mobx.action(() => { - const opts = mobx.toJS(inputModel.historyQueryOpts.get()); - opts.queryStr = e.target.value; - inputModel.setHistoryQueryOpts(opts); - })(); + const opts = mobx.toJS(inputModel.historyQueryOpts.get()); + opts.queryStr = e.target.value; + inputModel.setHistoryQueryOpts(opts); } - @boundMethod + @mobx.action.bound handleFocus(e: any) { e.preventDefault(); GlobalModel.inputModel.giveFocus(); @@ -561,7 +560,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () render() { const model = GlobalModel; const inputModel = model.inputModel; - const curLine = inputModel.getCurLine(); + const curLine = inputModel.curLine; let displayLines = 1; const numLines = curLine.split("\n").length; const maxCols = this.getTextAreaMaxCols(); diff --git a/src/models/input.ts b/src/models/input.ts index 9f1978da7..67b28b7c5 100644 --- a/src/models/input.ts +++ b/src/models/input.ts @@ -8,7 +8,6 @@ import { isBlank } from "@/util/util"; import * as appconst from "@/app/appconst"; import type { Model } from "./model"; import { GlobalCommandRunner, GlobalModel } from "./global"; -import { app } from "electron"; function getDefaultHistoryQueryOpts(): HistoryQueryOpts { return { @@ -48,7 +47,6 @@ class InputModel { name: "history-items", deep: false, }); // sorted in reverse (most recent is index 0) - filteredHistoryItems: mobx.IComputedValue = null; historyIndex: mobx.IObservableValue = mobx.observable.box(0, { name: "history-index", }); // 1-indexed (because 0 is current) @@ -73,11 +71,10 @@ class InputModel { physicalInputFocused: OV = mobx.observable.box(false); forceInputFocus: boolean = false; + lastCurLine: string = ""; + constructor(globalModel: Model) { this.globalModel = globalModel; - this.filteredHistoryItems = mobx.computed(() => { - return this._getFilteredHistoryItems(); - }); mobx.action(() => { this.codeSelectSelectedIndex.set(-1); this.codeSelectBlockRefArray = []; @@ -85,12 +82,12 @@ class InputModel { this.codeSelectUuid = ""; } + @mobx.action setInputMode(inputMode: null | "comment" | "global"): void { - mobx.action(() => { - this.inputMode.set(inputMode); - })(); + this.inputMode.set(inputMode); } + @mobx.action toggleHistoryType(): void { const opts = mobx.toJS(this.historyQueryOpts.get()); let htype = opts.queryType; @@ -104,6 +101,7 @@ class InputModel { this.setHistoryType(htype); } + @mobx.action toggleRemoteType(): void { const opts = mobx.toJS(this.historyQueryOpts.get()); if (opts.limitRemote) { @@ -116,67 +114,63 @@ class InputModel { this.setHistoryQueryOpts(opts); } + @mobx.action onInputFocus(isFocused: boolean): void { - mobx.action(() => { - if (isFocused) { - this.inputFocused.set(true); - this.lineFocused.set(false); - } else if (this.inputFocused.get()) { - this.inputFocused.set(false); - } - })(); + if (isFocused) { + this.inputFocused.set(true); + this.lineFocused.set(false); + } else if (this.inputFocused.get()) { + this.inputFocused.set(false); + } } + @mobx.action onLineFocus(isFocused: boolean): void { - mobx.action(() => { - if (isFocused) { - this.inputFocused.set(false); - this.lineFocused.set(true); - } else if (this.lineFocused.get()) { - this.lineFocused.set(false); - } - })(); + if (isFocused) { + this.inputFocused.set(false); + this.lineFocused.set(true); + } else if (this.lineFocused.get()) { + this.lineFocused.set(false); + } } // Focuses the main input or the auxiliary view, depending on the active auxiliary view + @mobx.action giveFocus(): void { // 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 appconst.InputAuxView_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; + switch (activeAuxView) { + case appconst.InputAuxView_History: { + const elem: HTMLElement = document.querySelector(".cmd-input input.history-input"); + if (elem != null) { + elem.focus(); } + break; } - })(); + case appconst.InputAuxView_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; + } + } } + @mobx.action setPhysicalInputFocused(isFocused: boolean): void { - mobx.action(() => { - this.physicalInputFocused.set(isFocused); - })(); + this.physicalInputFocused.set(isFocused); if (isFocused) { const screen = this.globalModel.getActiveScreen(); if (screen != null) { @@ -203,6 +197,7 @@ class InputModel { return false; } + @mobx.action setHistoryType(htype: HistoryTypeStrs): void { if (this.historyQueryOpts.get().queryType == htype) { return; @@ -214,7 +209,7 @@ class InputModel { if (oldItem == null) { return 0; } - const newItems = this.getFilteredHistoryItems(); + const newItems = this.filteredHistoryItems; if (newItems.length == 0) { return 0; } @@ -234,15 +229,15 @@ class InputModel { return bestIdx + 1; } + @mobx.action setHistoryQueryOpts(opts: HistoryQueryOpts): void { - mobx.action(() => { - const oldItem = this.getHistorySelectedItem(); - this.historyQueryOpts.set(opts); - const bestIndex = this.findBestNewIndex(oldItem); - setTimeout(() => this.setHistoryIndex(bestIndex, true), 10); - })(); + const oldItem = this.getHistorySelectedItem(); + this.historyQueryOpts.set(opts); + const bestIndex = this.findBestNewIndex(oldItem); + setTimeout(() => this.setHistoryIndex(bestIndex, true), 10); } + @mobx.action setOpenAICmdInfoChat(chat: OpenAICmdInfoChatMessageType[]): void { this.AICmdInfoChatItems.replace(chat); this.codeSelectBlockRefArray = []; @@ -256,6 +251,7 @@ class InputModel { return hitems != null; } + @mobx.action loadHistory(show: boolean, afterLoadIndex: number, htype: HistoryTypeStrs) { if (this.historyLoading.get()) { return; @@ -266,12 +262,11 @@ class InputModel { } } this.historyAfterLoadIndex = afterLoadIndex; - mobx.action(() => { - this.historyLoading.set(true); - })(); + this.historyLoading.set(true); GlobalCommandRunner.loadHistory(show, htype); } + @mobx.action openHistory(): void { if (this.historyLoading.get()) { return; @@ -287,13 +282,12 @@ class InputModel { } } + @mobx.action updateCmdLine(cmdLine: StrWithPos): void { - mobx.action(() => { - this.setCurLine(cmdLine.str); - if (cmdLine.pos != appconst.NoStrPos) { - this.forceCursorPos.set(cmdLine.pos); - } - })(); + this.curLine = cmdLine.str; + if (cmdLine.pos != appconst.NoStrPos) { + this.forceCursorPos.set(cmdLine.pos); + } } getHistorySelectedItem(): HistoryItem { @@ -301,7 +295,7 @@ class InputModel { if (hidx == 0) { return null; } - const hitems = this.getFilteredHistoryItems(); + const hitems = this.filteredHistoryItems; if (hidx > hitems.length) { return null; } @@ -309,15 +303,16 @@ class InputModel { } getFirstHistoryItem(): HistoryItem { - const hitems = this.getFilteredHistoryItems(); + const hitems = this.filteredHistoryItems; if (hitems.length == 0) { return null; } return hitems[0]; } + @mobx.action setHistorySelectionNum(hnum: string): void { - const hitems = this.getFilteredHistoryItems(); + const hitems = this.filteredHistoryItems; for (const [i, hitem] of hitems.entries()) { if (hitem.historynum == hnum) { this.setHistoryIndex(i + 1); @@ -326,37 +321,33 @@ class InputModel { } } + @mobx.action setHistoryInfo(hinfo: HistoryInfoType): void { - mobx.action(() => { - const oldItem = this.getHistorySelectedItem(); - const hitems: HistoryItem[] = hinfo.items ?? []; - this.historyItems.set(hitems); - this.historyLoading.set(false); - this.historyQueryOpts.get().queryType = hinfo.historytype; - if (hinfo.historytype == "session" || hinfo.historytype == "global") { - this.historyQueryOpts.get().limitRemote = false; - this.historyQueryOpts.get().limitRemoteInstance = false; + const oldItem = this.getHistorySelectedItem(); + const hitems: HistoryItem[] = hinfo.items ?? []; + this.historyItems.set(hitems); + this.historyLoading.set(false); + this.historyQueryOpts.get().queryType = hinfo.historytype; + if (hinfo.historytype == "session" || hinfo.historytype == "global") { + this.historyQueryOpts.get().limitRemote = false; + this.historyQueryOpts.get().limitRemoteInstance = false; + } + if (this.historyAfterLoadIndex == -1) { + const bestIndex = this.findBestNewIndex(oldItem); + setTimeout(() => this.setHistoryIndex(bestIndex, true), 100); + } else if (this.historyAfterLoadIndex) { + if (hitems.length >= this.historyAfterLoadIndex) { + this.setHistoryIndex(this.historyAfterLoadIndex); } - if (this.historyAfterLoadIndex == -1) { - const bestIndex = this.findBestNewIndex(oldItem); - setTimeout(() => this.setHistoryIndex(bestIndex, true), 100); - } else if (this.historyAfterLoadIndex) { - if (hitems.length >= this.historyAfterLoadIndex) { - this.setHistoryIndex(this.historyAfterLoadIndex); - } - } - this.historyAfterLoadIndex = 0; - if (hinfo.show) { - this.openHistory(); - } - })(); + } + this.historyAfterLoadIndex = 0; + if (hinfo.show) { + this.openHistory(); + } } - getFilteredHistoryItems(): HistoryItem[] { - return this.filteredHistoryItems.get(); - } - - _getFilteredHistoryItems(): HistoryItem[] { + @mobx.computed + get filteredHistoryItems(): HistoryItem[] { const hitems: HistoryItem[] = this.historyItems.get() ?? []; const rtn: HistoryItem[] = []; const opts: HistoryQueryOpts = mobx.toJS(this.historyQueryOpts.get()); @@ -416,16 +407,15 @@ class InputModel { elem.scrollIntoView({ block: "nearest" }); } + @mobx.action grabSelectedHistoryItem(): void { const hitem = this.getHistorySelectedItem(); if (hitem == null) { this.resetHistory(); return; } - mobx.action(() => { - this.resetInput(); - this.setCurLine(hitem.cmdstr); - })(); + this.resetInput(); + this.curLine = hitem.cmdstr; } // Closes the auxiliary view if it is open, focuses the main input @@ -449,8 +439,8 @@ class InputModel { mobx.action(() => { this.auxViewFocus.set(view != null); this.activeAuxView.set(view); + this.giveFocus(); })(); - this.giveFocus(); } // Gets the focus state of the auxiliary view. If true, the view will get focus. Otherwise, the main input will get focus. @@ -463,39 +453,33 @@ class InputModel { } // Sets the focus state of the auxiliary view. If true, the view will get focus. Otherwise, the main input will get focus. + @mobx.action setAuxViewFocus(focus: boolean): void { - mobx.action(() => { - this.auxViewFocus.set(focus); - })(); + this.auxViewFocus.set(focus); this.giveFocus(); } + @mobx.computed shouldRenderAuxViewKeybindings(view: InputAuxViewType): boolean { - return mobx - .computed(() => { - if (view != null && this.getActiveAuxView() != view) { - return false; - } - if (view != null && !this.getAuxViewFocus()) { - return false; - } - if (view == null && this.hasFocus() && !this.getAuxViewFocus()) { - return true; - } - if (view != null && this.getAuxViewFocus()) { - return true; - } - if ( - GlobalModel.getActiveScreen().getFocusType() == "input" && - GlobalModel.activeMainView.get() == "session" - ) { - return true; - } - return false; - }) - .get(); + if (view != null && this.getActiveAuxView() != view) { + return false; + } + if (view != null && !this.getAuxViewFocus()) { + return false; + } + if (view == null && this.hasFocus() && !this.getAuxViewFocus()) { + return true; + } + if (view != null && this.getAuxViewFocus()) { + return true; + } + if (GlobalModel.getActiveScreen().getFocusType() == "input" && GlobalModel.activeMainView.get() == "session") { + return true; + } + return false; } + @mobx.action setHistoryIndex(hidx: number, force?: boolean): void { if (hidx < 0) { return; @@ -503,18 +487,16 @@ class InputModel { if (!force && this.historyIndex.get() == hidx) { return; } - mobx.action(() => { - this.historyIndex.set(hidx); - if (this.getActiveAuxView() == appconst.InputAuxView_History) { - let hitem = this.getHistorySelectedItem(); - if (hitem == null) { - hitem = this.getFirstHistoryItem(); - } - if (hitem != null) { - this.scrollHistoryItemIntoView(hitem.historynum); - } + this.historyIndex.set(hidx); + if (this.getActiveAuxView() == appconst.InputAuxView_History) { + let hitem = this.getHistorySelectedItem(); + if (hitem == null) { + hitem = this.getFirstHistoryItem(); } - })(); + if (hitem != null) { + this.scrollHistoryItemIntoView(hitem.historynum); + } + } } moveHistorySelection(amt: number): void { @@ -524,7 +506,7 @@ class InputModel { if (!this.isHistoryLoaded()) { return; } - const hitems = this.getFilteredHistoryItems(); + const hitems = this.filteredHistoryItems; let idx = this.historyIndex.get() + amt; if (idx < 0) { idx = 0; @@ -535,11 +517,10 @@ class InputModel { this.setHistoryIndex(idx); } + @mobx.action flashInfoMsg(info: InfoType, timeoutMs: number): void { this._clearInfoTimeout(); - mobx.action(() => { - this.infoMsg.set(info); - })(); + this.infoMsg.set(info); if (info == null && this.getActiveAuxView() == appconst.InputAuxView_Info) { this.setActiveAuxView(null); @@ -578,7 +559,7 @@ class InputModel { ) { const curBlockRef = this.codeSelectBlockRefArray[this.codeSelectSelectedIndex.get()]; const codeText = curBlockRef.current.innerText.replace(/\n$/, ""); // remove trailing newline - this.setCurLine(codeText); + this.curLine = codeText; this.giveFocus(); } } @@ -594,72 +575,68 @@ class InputModel { return rtn; } + @mobx.action setCodeSelectSelectedCodeBlock(blockIndex: number) { - mobx.action(() => { - if (blockIndex >= 0 && blockIndex < this.codeSelectBlockRefArray.length) { - this.codeSelectSelectedIndex.set(blockIndex); - const currentRef = this.codeSelectBlockRefArray[blockIndex].current; - if (currentRef != null && this.aiChatWindowRef?.current != null) { - const chatWindowTop = this.aiChatWindowRef.current.scrollTop; - const chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100; - const elemTop = currentRef.offsetTop; - let elemBottom = elemTop - currentRef.offsetHeight; - const elementIsInView = elemBottom < chatWindowBottom && elemTop > chatWindowTop; - if (!elementIsInView) { - this.aiChatWindowRef.current.scrollTop = - elemBottom - this.aiChatWindowRef.current.clientHeight / 3; - } + if (blockIndex >= 0 && blockIndex < this.codeSelectBlockRefArray.length) { + this.codeSelectSelectedIndex.set(blockIndex); + const currentRef = this.codeSelectBlockRefArray[blockIndex].current; + if (currentRef != null && this.aiChatWindowRef?.current != null) { + const chatWindowTop = this.aiChatWindowRef.current.scrollTop; + const chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100; + const elemTop = currentRef.offsetTop; + let elemBottom = elemTop - currentRef.offsetHeight; + const elementIsInView = elemBottom < chatWindowBottom && elemTop > chatWindowTop; + if (!elementIsInView) { + this.aiChatWindowRef.current.scrollTop = elemBottom - this.aiChatWindowRef.current.clientHeight / 3; } } - this.codeSelectBlockRefArray = []; - this.setAIChatFocus(); - })(); + } + this.codeSelectBlockRefArray = []; + this.setAIChatFocus(); } + @mobx.action codeSelectSelectNextNewestCodeBlock() { // oldest code block = index 0 in array // this decrements codeSelectSelected index - mobx.action(() => { - if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) { - this.codeSelectSelectedIndex.set(this.codeSelectBottom); - } else if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) { - return; + if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) { + this.codeSelectSelectedIndex.set(this.codeSelectBottom); + } else if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) { + return; + } + const incBlockIndex = this.codeSelectSelectedIndex.get() + 1; + if (this.codeSelectSelectedIndex.get() == this.codeSelectBlockRefArray.length - 1) { + this.codeSelectDeselectAll(); + if (this.aiChatWindowRef?.current != null) { + this.aiChatWindowRef.current.scrollTop = this.aiChatWindowRef.current.scrollHeight; } - const incBlockIndex = this.codeSelectSelectedIndex.get() + 1; - if (this.codeSelectSelectedIndex.get() == this.codeSelectBlockRefArray.length - 1) { - this.codeSelectDeselectAll(); - if (this.aiChatWindowRef?.current != null) { - this.aiChatWindowRef.current.scrollTop = this.aiChatWindowRef.current.scrollHeight; - } - } - if (incBlockIndex >= 0 && incBlockIndex < this.codeSelectBlockRefArray.length) { - this.setCodeSelectSelectedCodeBlock(incBlockIndex); - } - })(); + } + if (incBlockIndex >= 0 && incBlockIndex < this.codeSelectBlockRefArray.length) { + this.setCodeSelectSelectedCodeBlock(incBlockIndex); + } } + @mobx.action codeSelectSelectNextOldestCodeBlock() { - mobx.action(() => { - if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) { - if (this.codeSelectBlockRefArray.length > 0) { - this.codeSelectSelectedIndex.set(this.codeSelectBlockRefArray.length); - } else { - return; - } - } else if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) { + if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) { + if (this.codeSelectBlockRefArray.length > 0) { + this.codeSelectSelectedIndex.set(this.codeSelectBlockRefArray.length); + } else { return; } - const decBlockIndex = this.codeSelectSelectedIndex.get() - 1; - if (decBlockIndex < 0) { - this.codeSelectDeselectAll(this.codeSelectTop); - if (this.aiChatWindowRef?.current != null) { - this.aiChatWindowRef.current.scrollTop = 0; - } + } else if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) { + return; + } + const decBlockIndex = this.codeSelectSelectedIndex.get() - 1; + if (decBlockIndex < 0) { + this.codeSelectDeselectAll(this.codeSelectTop); + if (this.aiChatWindowRef?.current != null) { + this.aiChatWindowRef.current.scrollTop = 0; } - if (decBlockIndex >= 0 && decBlockIndex < this.codeSelectBlockRefArray.length) { - this.setCodeSelectSelectedCodeBlock(decBlockIndex); - } - })(); + } + if (decBlockIndex >= 0 && decBlockIndex < this.codeSelectBlockRefArray.length) { + this.setCodeSelectSelectedCodeBlock(decBlockIndex); + } } getCodeSelectSelectedIndex() { @@ -684,6 +661,7 @@ class InputModel { })(); } + @mobx.action openAIAssistantChat(): void { this.setActiveAuxView(appconst.InputAuxView_AIChat); this.setAuxViewFocus(true); @@ -723,19 +701,19 @@ class InputModel { } } + @mobx.action clearInfoMsg(setNull: boolean): void { this._clearInfoTimeout(); if (this.getActiveAuxView() == appconst.InputAuxView_Info) { this.setActiveAuxView(null); } - mobx.action(() => { - if (setNull) { - this.infoMsg.set(null); - } - })(); + if (setNull) { + this.infoMsg.set(null); + } } + @mobx.action toggleInfoMsg(): void { this._clearInfoTimeout(); if (this.activeAuxView.get() == appconst.InputAuxView_Info) { @@ -747,63 +725,51 @@ class InputModel { @boundMethod uiSubmitCommand(): void { + const commandStr = this.curLine; + if (commandStr.trim() == "") { + return; + } mobx.action(() => { - const commandStr = this.getCurLine(); - if (commandStr.trim() == "") { - return; - } this.resetInput(); - this.globalModel.submitRawCommand(commandStr, true, true); })(); + this.globalModel.submitRawCommand(commandStr, true, true); } isEmpty(): boolean { - return this.getCurLine().trim() == ""; + return this.curLine.trim() == ""; } + @mobx.action resetInputMode(): void { - mobx.action(() => { - this.setInputMode(null); - this.setCurLine(""); - })(); - } - - setCurLine(val: string): void { - const hidx = this.historyIndex.get(); - mobx.action(() => { - if (this.modHistory.length <= hidx) { - this.modHistory.length = hidx + 1; - } - this.modHistory[hidx] = val; - })(); + this.setInputMode(null); + this.curLine = ""; } + @mobx.action resetInput(): void { - mobx.action(() => { - this.setActiveAuxView(null); - this.inputMode.set(null); - this.resetHistory(); - this.dropModHistory(false); - this.infoMsg.set(null); - this.inputExpanded.set(false); - this._clearInfoTimeout(); - })(); + this.setActiveAuxView(null); + this.inputMode.set(null); + this.resetHistory(); + this.dropModHistory(false); + this.infoMsg.set(null); + this.inputExpanded.set(false); + this._clearInfoTimeout(); } + @mobx.action @boundMethod toggleExpandInput(): void { - mobx.action(() => { - this.inputExpanded.set(!this.inputExpanded.get()); - this.forceInputFocus = true; - })(); + this.inputExpanded.set(!this.inputExpanded.get()); + this.forceInputFocus = true; } - getCurLine(): string { + @mobx.computed + get curLine(): string { const hidx = this.historyIndex.get(); if (hidx < this.modHistory.length && this.modHistory[hidx] != null) { return this.modHistory[hidx]; } - const hitems = this.getFilteredHistoryItems(); + const hitems = this.filteredHistoryItems; if (hidx == 0 || hitems == null || hidx > hitems.length) { return ""; } @@ -814,31 +780,40 @@ class InputModel { return hitem.cmdstr; } - dropModHistory(keepLine0: boolean): void { + set curLine(val: string) { + this.lastCurLine = this.curLine; + const hidx = this.historyIndex.get(); mobx.action(() => { - if (keepLine0) { - if (this.modHistory.length > 1) { - this.modHistory.splice(1, this.modHistory.length - 1); - } - } else { - this.modHistory.replace([""]); + if (this.modHistory.length <= hidx) { + this.modHistory.length = hidx + 1; } + this.modHistory[hidx] = val; })(); } - resetHistory(): void { - mobx.action(() => { - if (this.getActiveAuxView() == appconst.InputAuxView_History) { - this.setActiveAuxView(null); + @mobx.action + dropModHistory(keepLine0: boolean): void { + if (keepLine0) { + if (this.modHistory.length > 1) { + this.modHistory.splice(1, this.modHistory.length - 1); } - this.historyLoading.set(false); - this.historyType.set("screen"); - this.historyItems.set(null); - this.historyIndex.set(0); - this.historyQueryOpts.set(getDefaultHistoryQueryOpts()); - this.historyAfterLoadIndex = 0; - this.dropModHistory(true); - })(); + } else { + this.modHistory.replace([""]); + } + } + + @mobx.action + resetHistory(): void { + if (this.getActiveAuxView() == appconst.InputAuxView_History) { + this.setActiveAuxView(null); + } + this.historyLoading.set(false); + this.historyType.set("screen"); + this.historyItems.set(null); + this.historyIndex.set(0); + this.historyQueryOpts.set(getDefaultHistoryQueryOpts()); + this.historyAfterLoadIndex = 0; + this.dropModHistory(true); } } diff --git a/src/models/model.ts b/src/models/model.ts index 361f729d7..023468042 100644 --- a/src/models/model.ts +++ b/src/models/model.ts @@ -37,7 +37,6 @@ import { GlobalCommandRunner } from "./global"; import { clearMonoFontCache, getMonoFontSize } from "@/util/textmeasure"; import type { TermWrap } from "@/plugins/terminal/term"; import * as util from "@/util/util"; -import { url } from "node:inspector"; type SWLinePtr = { line: LineType; @@ -1327,7 +1326,21 @@ class Model { } } - submitCommandPacket(cmdPk: FeCmdPacketType, interactive: boolean): Promise { + /** + * Submits a command packet to the server and processes the response. + * @param cmdPk The command packet to submit. + * @param interactive Whether the command is interactive. + * @param runUpdate Whether to run the update after the command is submitted. If true, the update will be processed and the frontend will be updated. If false, the update will be returned in the promise. + * @returns A promise that resolves to a CommandRtnType. + * @throws An error if the command fails. + * @see CommandRtnType + * @see FeCmdPacketType + **/ + submitCommandPacket( + cmdPk: FeCmdPacketType, + interactive: boolean, + runUpdate: boolean = true + ): Promise { if (this.debugCmds > 0) { console.log("[cmd]", cmdPacketString(cmdPk)); if (this.debugCmds > 1) { @@ -1345,16 +1358,20 @@ class Model { }) .then((resp) => handleJsonFetchResponse(url, resp)) .then((data) => { - mobx.action(() => { + return mobx.action(() => { const update = data.data; if (update != null) { - this.runUpdate(update, interactive); + if (runUpdate) { + this.runUpdate(update, interactive); + } else { + return { success: true, update: update }; + } } if (interactive && !this.isInfoUpdate(update)) { this.inputModel.clearInfoMsg(true); } + return { success: true }; })(); - return { success: true }; }) .catch((err) => { this.errorHandler("calling run-command", err, interactive); @@ -1367,12 +1384,23 @@ class Model { return prtn; } + /** + * Submits a command to the server and processes the response. + * @param metaCmd The meta command to run. + * @param metaSubCmd The meta subcommand to run. + * @param args The arguments to pass to the command. + * @param kwargs The keyword arguments to pass to the command. + * @param interactive Whether the command is interactive. + * @param runUpdate Whether to run the update after the command is submitted. If true, the update will be processed and the frontend will be updated. If false, the update will be returned in the promise. + * @returns A promise that resolves to a CommandRtnType. + */ submitCommand( metaCmd: string, metaSubCmd: string, args: string[], kwargs: Record, - interactive: boolean + interactive: boolean, + runUpdate: boolean = true ): Promise { const pk: FeCmdPacketType = { type: "fecmd", @@ -1393,7 +1421,7 @@ class Model { pk.interactive ); */ - return this.submitCommandPacket(pk, interactive); + return this.submitCommandPacket(pk, interactive, runUpdate); } getSingleEphemeralCommandOutput(url: URL): Promise { @@ -1412,12 +1440,10 @@ class Model { let stderr = ""; if (ephemeralCommandResponse.stdouturl) { const url = new URL(this.getBaseHostPort() + ephemeralCommandResponse.stdouturl); - console.log("stdouturl", url); stdout = await this.getSingleEphemeralCommandOutput(url); } if (ephemeralCommandResponse.stderrurl) { const url = new URL(this.getBaseHostPort() + ephemeralCommandResponse.stderrurl); - console.log("stderrurl", url); stderr = await this.getSingleEphemeralCommandOutput(url); } return { stdout: stdout, stderr: stderr }; @@ -1476,14 +1502,14 @@ class Model { interactive: interactive, ephemeralopts: ephemeralopts, }; - console.log( - "CMD", - pk.metacmd + (pk.metasubcmd != null ? ":" + pk.metasubcmd : ""), - pk.args, - pk.kwargs, - pk.interactive, - pk.ephemeralopts - ); + // console.log( + // "CMD", + // pk.metacmd + (pk.metasubcmd != null ? ":" + pk.metasubcmd : ""), + // pk.args, + // pk.kwargs, + // pk.interactive, + // pk.ephemeralopts + // ); return this.submitEphemeralCommandPacket(pk, interactive); } diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts index 7789fce4c..ea0b2a2e7 100644 --- a/src/types/custom.d.ts +++ b/src/types/custom.d.ts @@ -821,6 +821,7 @@ declare global { type CommandRtnType = { success: boolean; error?: string; + update?: UpdatePacket; }; type EphemeralCommandOutputType = { diff --git a/waveshell/pkg/packet/packet.go b/waveshell/pkg/packet/packet.go index 78dbf6e66..6887324dd 100644 --- a/waveshell/pkg/packet/packet.go +++ b/waveshell/pkg/packet/packet.go @@ -833,6 +833,7 @@ type RunPacketType struct { Detached bool `json:"detached,omitempty"` ReturnState bool `json:"returnstate,omitempty"` IsSudo bool `json:"issudo,omitempty"` + Timeout time.Duration `json:"timeout"` // TODO: added vnext. This is the timeout for the command to run. If the command does not complete in this time, it will be killed. The default zero value will not impose a timeout. } func (*RunPacketType) GetType() string { diff --git a/waveshell/pkg/shexec/shexec.go b/waveshell/pkg/shexec/shexec.go index 67b9dee4f..a7ae780b0 100644 --- a/waveshell/pkg/shexec/shexec.go +++ b/waveshell/pkg/shexec/shexec.go @@ -926,12 +926,11 @@ func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender, fro rcFileName = fmt.Sprintf("/dev/fd/%d", rcFileFdNum) } if cmd.TmpRcFileName != "" { - go func() { + time.AfterFunc(2*time.Second, func() { // cmd.Close() will also remove rcFileName // adding this to also try to proactively clean up after 2-seconds. - time.Sleep(2 * time.Second) os.Remove(cmd.TmpRcFileName) - }() + }) } fullCmdStr := pk.Command if pk.ReturnState { @@ -1109,6 +1108,14 @@ func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender, fro if err != nil { return nil, err } + + if pk.Timeout > 0 { + // Cancel the command if it takes too long + time.AfterFunc(pk.Timeout, func() { + cmd.Cmd.Cancel() + cmd.Close() + }) + } return cmd, nil } diff --git a/wavesrv/pkg/bufferedpipe/bufferedpipe.go b/wavesrv/pkg/bufferedpipe/bufferedpipe.go index 3263182ba..84c627e5c 100644 --- a/wavesrv/pkg/bufferedpipe/bufferedpipe.go +++ b/wavesrv/pkg/bufferedpipe/bufferedpipe.go @@ -15,6 +15,7 @@ import ( "time" "github.com/google/uuid" + "github.com/wavetermdev/waveterm/waveshell/pkg/wlog" "github.com/wavetermdev/waveterm/wavesrv/pkg/scbase" "github.com/wavetermdev/waveterm/wavesrv/pkg/waveenc" ) @@ -110,6 +111,7 @@ func (pipe *BufferedPipe) WriteTo(w io.Writer) (n int64, err error) { // Close the pipe. This will cause any blocking WriteTo calls to return. func (pipe *BufferedPipe) Close() error { + wlog.Logf("closing buffered pipe %s", pipe.Key) defer pipe.bufferDataCond.Broadcast() pipe.closed.Store(true) return nil diff --git a/wavesrv/pkg/remote/remote.go b/wavesrv/pkg/remote/remote.go index c6a002424..0dfb13b31 100644 --- a/wavesrv/pkg/remote/remote.go +++ b/wavesrv/pkg/remote/remote.go @@ -1987,9 +1987,22 @@ func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.Ru // Setting UsePty to false will ensure that the outputs get written to the correct file descriptors to extract stdout and stderr runPacket.UsePty = rcOpts.EphemeralOpts.UsePty - // Ephemeral commands can override the cwd without persisting it to the DB + // Ephemeral commands can override the current working directory. We need to expand the home dir if it's relative. if rcOpts.EphemeralOpts.OverrideCwd != "" { - currentState.Cwd = rcOpts.EphemeralOpts.OverrideCwd + overrideCwd := rcOpts.EphemeralOpts.OverrideCwd + if !strings.HasPrefix(overrideCwd, "/") { + expandedCwd, err := msh.GetRemoteRuntimeState().ExpandHomeDir(overrideCwd) + if err != nil { + return nil, nil, fmt.Errorf("cannot expand home dir for cwd: %w", err) + } + overrideCwd = expandedCwd + } + currentState.Cwd = overrideCwd + } + + // Ephemeral commands can override the timeout + if rcOpts.EphemeralOpts.TimeoutMs > 0 { + runPacket.Timeout = time.Duration(rcOpts.EphemeralOpts.TimeoutMs) * time.Millisecond } // Ephemeral commands can override the env without persisting it to the DB @@ -2405,6 +2418,7 @@ func (msh *MShellProc) handleCmdStartError(rct *RunCmdType, startErr error) { defer msh.RemoveRunningCmd(rct.CK) if rct.EphemeralOpts != nil { // nothing to do for ephemeral commands besides remove the running command + log.Printf("ephemeral command start error: %v\n", startErr) return } ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) @@ -2472,6 +2486,11 @@ func (msh *MShellProc) handleCmdDonePacket(rct *RunCmdType, donePk *packet.CmdDo // Close the ephemeral response writer if it exists if rct.EphemeralOpts != nil && rct.EphemeralOpts.ExpectsResponse { + if donePk.ExitCode != 0 { + // if the command failed, we need to write the error to the response writer + log.Printf("writing error to ephemeral response writer\n") + rct.EphemeralOpts.StderrWriter.Write([]byte(fmt.Sprintf("error: %d\n", donePk.ExitCode))) + } log.Printf("closing ephemeral response writers\n") defer rct.EphemeralOpts.StdoutWriter.Close() defer rct.EphemeralOpts.StderrWriter.Close() @@ -2577,6 +2596,7 @@ func (msh *MShellProc) handleDataPacket(rct *RunCmdType, dataPk *packet.DataPack return } if rct.EphemeralOpts != nil { + log.Printf("ephemeral data packet: %s\n", dataPk.CK) // Write to the response writer if it's set if len(realData) > 0 && rct.EphemeralOpts.ExpectsResponse { switch dataPk.FdNum { @@ -2594,6 +2614,9 @@ func (msh *MShellProc) handleDataPacket(rct *RunCmdType, dataPk *packet.DataPack log.Printf("error handling data packet: invalid fdnum %d\n", dataPk.FdNum) } } + if dataPk.Error != "" { + log.Printf("ephemeral data packet error: %s\n", dataPk.Error) + } ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil) msh.ServerProc.Input.SendPacket(ack) return diff --git a/webpack.config.js b/webpack.config.js index 070ce8030..801ab5f76 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,5 +1,5 @@ -const {webDev, webProd} = require("./webpack/webpack.web.js"); -const {electronDev, electronProd} = require("./webpack/webpack.electron.js"); +const { webDev, webProd } = require("./webpack/webpack.web.js"); +const { electronDev, electronProd } = require("./webpack/webpack.electron.js"); module.exports = (env) => { if (env.prod) {