From c6a8797ddd2b3efc4b0be1acd3d14e47ca677ff8 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 29 Apr 2024 18:29:27 -0700 Subject: [PATCH 01/12] 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 30a9194e..68aa518a 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 8b5e7692..ee67c773 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 11d29223..865ef728 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 cfa2326d..b19a26a7 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 333a7b36..d2c9c4b6 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 a30cd215..614afeff 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 60c1f90e..ed3e3385 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 9f1978da..67b28b7c 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 361f729d..02346804 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 7789fce4..ea0b2a2e 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 78dbf6e6..6887324d 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 67b9dee4..a7ae780b 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 3263182b..84c627e5 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 c6a00242..0dfb13b3 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 070ce803..801ab5f7 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) { From e6df2c216d6e257215b526a6dfc5b3ef07916032 Mon Sep 17 00:00:00 2001 From: Cole Lashley Date: Tue, 30 Apr 2024 11:37:01 -0700 Subject: [PATCH 02/12] Client show defaults small fix (#614) --- wavesrv/pkg/cmdrunner/cmdrunner.go | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index e2deea74..caadecc9 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -6091,6 +6091,26 @@ func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s if pk.UIContext != nil && pk.UIContext.Build != "" { clientVersion = pk.UIContext.Build } + aiModel := clientData.OpenAIOpts.Model + if aiModel == "" { + aiModel = "(default) " + openai.DefaultModel + } + aiMaxTokens := fmt.Sprintf("%d", clientData.OpenAIOpts.MaxTokens) + if clientData.OpenAIOpts.MaxTokens == 0 { + aiMaxTokens = fmt.Sprintf("(default) %d", openai.DefaultMaxTokens) + } + aiMaxChoices := fmt.Sprintf("%d", clientData.OpenAIOpts.MaxChoices) + if clientData.OpenAIOpts.MaxChoices == 0 { + aiMaxChoices = "(not set)" + } + aiBaseUrl := clientData.OpenAIOpts.BaseURL + if aiBaseUrl == "" { + aiBaseUrl = "(openai default)" + } + aiTimeout := fmt.Sprintf("(default) %d", (OpenAIPacketTimeout / 1000)) + if clientData.OpenAIOpts.Timeout != 0 { + aiTimeout = strconv.FormatFloat((float64(clientData.OpenAIOpts.Timeout) / 1000.0), 'f', -1, 64) + } var buf bytes.Buffer buf.WriteString(fmt.Sprintf(" %-15s %s\n", "userid", clientData.UserId)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "clientid", clientData.ClientId)) @@ -6104,11 +6124,11 @@ func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s buf.WriteString(fmt.Sprintf(" %-15s %s\n", "termfontfamily", clientData.FeOpts.TermFontFamily)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "termfontfamily", clientData.FeOpts.Theme)) buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aiapitoken", clientData.OpenAIOpts.APIToken)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aimodel", clientData.OpenAIOpts.Model)) - buf.WriteString(fmt.Sprintf(" %-15s %d\n", "aimaxtokens", clientData.OpenAIOpts.MaxTokens)) - buf.WriteString(fmt.Sprintf(" %-15s %d\n", "aimaxchoices", clientData.OpenAIOpts.MaxChoices)) - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aibaseurl", clientData.OpenAIOpts.BaseURL)) - buf.WriteString(fmt.Sprintf(" %-15s %ss\n", "aitimeout", strconv.FormatFloat((float64(clientData.OpenAIOpts.Timeout)/1000.0), 'f', -1, 64))) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aimodel", aiModel)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aimaxtokens", aiMaxTokens)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aimaxchoices", aiMaxChoices)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "aibaseurl", aiBaseUrl)) + buf.WriteString(fmt.Sprintf(" %-15s %ss\n", "aitimeout", aiTimeout)) update := scbus.MakeUpdatePacket() update.AddUpdate(sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("client info"), From c53e0a99f68e249672817884aaf38c0b9115b25d Mon Sep 17 00:00:00 2001 From: Ian Jennings Date: Tue, 30 Apr 2024 14:37:22 -0500 Subject: [PATCH 03/12] TestDriver.ai: Add comments to PRs (#498) --- .github/workflows/regression.yml | 33 ++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index 0a50189f..ebaac111 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -1,22 +1,26 @@ -name: TestDriver.ai Regression Testing (test) - +name: TestDriver.ai Regression Testing on: push: - branches: ["main"] + branches: + - main pull_request: - branches: ["main"] + branches: + - main schedule: - - cron: "0 21 * * *" # every day at 9pm - workflow_dispatch: + - cron: 0 21 * * * + workflow_dispatch: null + +permissions: + contents: read # To allow the action to read repository contents + pull-requests: write # To allow the action to create/update pull request comments jobs: test: - name: "TestDriver" + name: TestDriver runs-on: ubuntu-latest steps: - uses: dashcamio/testdriver@main id: testdriver - # note that .testdriver/prerun.sh runs before this, so the app has launched already with: version: v2.9.4 prerun: | @@ -38,8 +42,21 @@ jobs: echo "Electron Done" exit prompt: | + 1. click "Continue" 2. click "Create new tab" 2. focus the Wave input with the keyboard shorcut Command + I 3. type 'ls' into the input 4. press return 5. validate Wave shows the result of 'ls' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: peter-evans/create-or-update-comment@v4 + if: ${{always()}} + with: + issue-number: ${{ github.event.pull_request.number }} + body: | + ## TestDriver Summary + ${{ steps.testdriver.outputs.summary }} + reactions: | + +1 + -1 \ No newline at end of file From 35e0488f7b2b4df8e95c70e6f1e589c685d9a75b Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 30 Apr 2024 13:58:54 -0700 Subject: [PATCH 04/12] Call makeObservable for all cmdInput classes (#619) --- src/app/workspace/cmdinput/aichat.tsx | 2 ++ src/app/workspace/cmdinput/cmdinput.tsx | 5 +++++ src/app/workspace/cmdinput/historyinfo.tsx | 5 +++++ src/app/workspace/cmdinput/infomsg.tsx | 6 ++++++ src/app/workspace/cmdinput/textareainput.tsx | 13 +++++++++++-- src/models/input.ts | 5 +---- 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/app/workspace/cmdinput/aichat.tsx b/src/app/workspace/cmdinput/aichat.tsx index ee67c773..8ddc099a 100644 --- a/src/app/workspace/cmdinput/aichat.tsx +++ b/src/app/workspace/cmdinput/aichat.tsx @@ -61,9 +61,11 @@ class AIChat extends React.Component<{}, {}> { constructor(props: any) { super(props); + mobx.makeObservable(this); this.chatWindowScrollRef = React.createRef(); this.textAreaRef = React.createRef(); } + componentDidMount() { const inputModel = GlobalModel.inputModel; if (this.chatWindowScrollRef?.current != null) { diff --git a/src/app/workspace/cmdinput/cmdinput.tsx b/src/app/workspace/cmdinput/cmdinput.tsx index b19a26a7..c6b108bf 100644 --- a/src/app/workspace/cmdinput/cmdinput.tsx +++ b/src/app/workspace/cmdinput/cmdinput.tsx @@ -29,6 +29,11 @@ class CmdInput extends React.Component<{}, {}> { cmdInputRef: React.RefObject = React.createRef(); promptRef: React.RefObject = React.createRef(); + constructor(props) { + super(props); + mobx.makeObservable(this); + } + componentDidMount() { this.updateCmdInputHeight(); } diff --git a/src/app/workspace/cmdinput/historyinfo.tsx b/src/app/workspace/cmdinput/historyinfo.tsx index 614afeff..7572ac28 100644 --- a/src/app/workspace/cmdinput/historyinfo.tsx +++ b/src/app/workspace/cmdinput/historyinfo.tsx @@ -42,6 +42,11 @@ class HItem extends React.Component< }, {} > { + constructor(props) { + super(props); + mobx.makeObservable(this); + } + renderRemote(hitem: HistoryItem): any { if (hitem.remote == null || isBlank(hitem.remote.remoteid)) { return sprintf("%-15s ", ""); diff --git a/src/app/workspace/cmdinput/infomsg.tsx b/src/app/workspace/cmdinput/infomsg.tsx index f7143f91..70bec936 100644 --- a/src/app/workspace/cmdinput/infomsg.tsx +++ b/src/app/workspace/cmdinput/infomsg.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import * as mobxReact from "mobx-react"; +import * as mobx from "mobx"; import { If, For } from "tsx-control-statements/components"; import cn from "classnames"; import dayjs from "dayjs"; @@ -17,6 +18,11 @@ dayjs.extend(localizedFormat); @mobxReact.observer class InfoMsg extends React.Component<{}, {}> { + constructor(props) { + super(props); + mobx.makeObservable(this); + } + getAfterSlash(s: string): string { if (s.startsWith("^/")) { return s.substring(1); diff --git a/src/app/workspace/cmdinput/textareainput.tsx b/src/app/workspace/cmdinput/textareainput.tsx index ed3e3385..2d2eb4ff 100644 --- a/src/app/workspace/cmdinput/textareainput.tsx +++ b/src/app/workspace/cmdinput/textareainput.tsx @@ -250,6 +250,11 @@ 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 + constructor(props) { + super(props); + mobx.makeObservable(this); + } + @mobx.action incVersion(): void { const v = this.version.get(); @@ -313,8 +318,8 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () } } - @mobx.action - componentDidMount() { + @mobx.action.bound + handleComponentDidMount() { const activeScreen = GlobalModel.getActiveScreen(); if (activeScreen != null) { const focusType = activeScreen.focusType.get(); @@ -327,6 +332,10 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () this.updateSP(); } + componentDidMount() { + this.handleComponentDidMount(); + } + @mobx.action componentDidUpdate() { const activeScreen = GlobalModel.getActiveScreen(); diff --git a/src/models/input.ts b/src/models/input.ts index 67b28b7c..5fa96110 100644 --- a/src/models/input.ts +++ b/src/models/input.ts @@ -3,7 +3,6 @@ import type React from "react"; import * as mobx from "mobx"; -import { boundMethod } from "autobind-decorator"; import { isBlank } from "@/util/util"; import * as appconst from "@/app/appconst"; import type { Model } from "./model"; @@ -75,6 +74,7 @@ class InputModel { constructor(globalModel: Model) { this.globalModel = globalModel; + mobx.makeObservable(this); mobx.action(() => { this.codeSelectSelectedIndex.set(-1); this.codeSelectBlockRefArray = []; @@ -459,7 +459,6 @@ class InputModel { this.giveFocus(); } - @mobx.computed shouldRenderAuxViewKeybindings(view: InputAuxViewType): boolean { if (view != null && this.getActiveAuxView() != view) { return false; @@ -723,7 +722,6 @@ class InputModel { } } - @boundMethod uiSubmitCommand(): void { const commandStr = this.curLine; if (commandStr.trim() == "") { @@ -757,7 +755,6 @@ class InputModel { } @mobx.action - @boundMethod toggleExpandInput(): void { this.inputExpanded.set(!this.inputExpanded.get()); this.forceInputFocus = true; From 8c63be4ea02fdfbddb08db61b9fcff911b914599 Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Tue, 30 Apr 2024 14:28:52 -0700 Subject: [PATCH 05/12] fix cmdinput cursor positioning on mount (#620) --- src/app/workspace/cmdinput/textareainput.tsx | 23 ++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/app/workspace/cmdinput/textareainput.tsx b/src/app/workspace/cmdinput/textareainput.tsx index 2d2eb4ff..a12628dd 100644 --- a/src/app/workspace/cmdinput/textareainput.tsx +++ b/src/app/workspace/cmdinput/textareainput.tsx @@ -334,6 +334,19 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () componentDidMount() { this.handleComponentDidMount(); + this.updateCursorPosIfForced(); + } + + updateCursorPosIfForced() { + const inputModel = GlobalModel.inputModel; + const fcpos = inputModel.forceCursorPos.get(); + if (fcpos != null && fcpos != appconst.NoStrPos) { + if (this.mainInputRef.current != null) { + this.mainInputRef.current.selectionStart = fcpos; + this.mainInputRef.current.selectionEnd = fcpos; + } + inputModel.forceCursorPos.set(null); + } } @mobx.action @@ -347,14 +360,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () this.lastFocusType = focusType; } const inputModel = GlobalModel.inputModel; - const fcpos = inputModel.forceCursorPos.get(); - if (fcpos != null && fcpos != appconst.NoStrPos) { - if (this.mainInputRef.current != null) { - this.mainInputRef.current.selectionStart = fcpos; - this.mainInputRef.current.selectionEnd = fcpos; - } - inputModel.forceCursorPos.set(null); - } + this.updateCursorPosIfForced(); if (inputModel.forceInputFocus) { inputModel.forceInputFocus = false; this.setFocus(); @@ -614,7 +620,6 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () const renderCmdInputKeybindings = inputModel.shouldRenderAuxViewKeybindings(null); const renderHistoryKeybindings = inputModel.shouldRenderAuxViewKeybindings(appconst.InputAuxView_History); - return (
Date: Tue, 30 Apr 2024 16:05:49 -0700 Subject: [PATCH 06/12] Make Client Settings and connections views scrollable, fix regression in unused bookmarks feature (#621) Makes clientsettings scrollable, adds an option for mainview to wrap the mainview-content in an OverlayScrollbarsComponent --- public/themes/default.css | 2 +- public/themes/light.css | 4 + src/app/clientsettings/clientsettings.less | 3 +- src/app/clientsettings/clientsettings.tsx | 8 +- src/app/common/elements/mainview.less | 3 +- src/app/common/elements/mainview.tsx | 20 +++- src/app/connections/connections.less | 19 +++- src/app/connections/connections.tsx | 105 +++++++++++---------- src/models/bookmarks.ts | 2 +- 9 files changed, 109 insertions(+), 57 deletions(-) diff --git a/public/themes/default.css b/public/themes/default.css index 9233a226..4842fde4 100644 --- a/public/themes/default.css +++ b/public/themes/default.css @@ -156,7 +156,7 @@ --table-border-color: rgba(241, 246, 243, 0.15); --table-thead-border-top-color: rgba(250, 250, 250, 0.1); --table-thead-bright-border-color: rgb(204, 204, 204); - --table-thead-bg-color: rgba(250, 250, 250, 0.02); + --table-thead-bg-color: rgb(5, 5, 5); --table-tr-border-bottom-color: rgba(241, 246, 243, 0.15); --table-tr-hover-bg-color: rgba(255, 255, 255, 0.06); --table-tr-selected-bg-color: rgb(34, 34, 34); diff --git a/public/themes/light.css b/public/themes/light.css index 68aa518a..f7eb3361 100644 --- a/public/themes/light.css +++ b/public/themes/light.css @@ -57,6 +57,10 @@ --scrollbar-thumb-hover-color: rgba(0, 0, 0, 0.4); --scrollbar-thumb-active-color: rgba(0, 0, 0, 0.5); + /* table colors */ + --table-thead-bg-color: rgb(253, 253, 253); + --table-tr-selected-bg-color: rgb(216, 216, 216); + /* line color */ --line-actions-bg-color: rgba(0, 0, 0, 0.1); --line-actions-inactive-color: rgba(0, 0, 0, 0.3); diff --git a/src/app/clientsettings/clientsettings.less b/src/app/clientsettings/clientsettings.less index c5bdd86d..1bca4294 100644 --- a/src/app/clientsettings/clientsettings.less +++ b/src/app/clientsettings/clientsettings.less @@ -1,6 +1,7 @@ .clientsettings-view { .content { - padding: 14px 18px 0 30px; + padding: 14px 18px 14px 30px; + overflow-y: scroll; } .wave-dropdown { diff --git a/src/app/clientsettings/clientsettings.tsx b/src/app/clientsettings/clientsettings.tsx index fa243c05..77bc3bd6 100644 --- a/src/app/clientsettings/clientsettings.tsx +++ b/src/app/clientsettings/clientsettings.tsx @@ -14,6 +14,7 @@ import * as appconst from "@/app/appconst"; import "./clientsettings.less"; import { MainView } from "../common/elements/mainview"; +import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; class ClientSettingsKeybindings extends React.Component<{}, {}> { componentDidMount() { @@ -251,7 +252,12 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove const curSudoPwClearOnSleep = GlobalModel.getSudoPwClearOnSleep(); return ( - + diff --git a/src/app/common/elements/mainview.less b/src/app/common/elements/mainview.less index 811f4d07..8707ed1b 100644 --- a/src/app/common/elements/mainview.less +++ b/src/app/common/elements/mainview.less @@ -40,7 +40,8 @@ } .mainview-content { - padding-top: 14px; + display: flex; + flex-direction: column; } } diff --git a/src/app/common/elements/mainview.tsx b/src/app/common/elements/mainview.tsx index 65a3ef65..e4243ceb 100644 --- a/src/app/common/elements/mainview.tsx +++ b/src/app/common/elements/mainview.tsx @@ -7,6 +7,8 @@ import cn from "classnames"; import { GlobalModel } from "@/models"; import "./mainview.less"; +import { Choose, If, Otherwise, When } from "tsx-control-statements/components"; +import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; @mobxReact.observer class MainView extends React.Component<{ @@ -14,6 +16,8 @@ class MainView extends React.Component<{ onClose: () => void; children: React.ReactNode; className?: string; + scrollable?: boolean; + onScrollbarInitialized?: () => void; }> { render() { const sidebarModel = GlobalModel.mainSidebarModel; @@ -31,7 +35,21 @@ class MainView extends React.Component<{
-
{this.props.children}
+ + + + {this.props.children} + + + +
{this.props.children}
+
+
); } diff --git a/src/app/connections/connections.less b/src/app/connections/connections.less index 8cc98cf2..ae97a87e 100644 --- a/src/app/connections/connections.less +++ b/src/app/connections/connections.less @@ -9,10 +9,20 @@ margin: 20px 50px 20px 20px; } + .connections-table-container { + display: flex; + flex-direction: column; + flex-grow: 1; + overflow-y: scroll; + max-height: 85vh; + width: fit-content; + max-width: 970px; + } + .connections-table { margin: 0px 10px 10px 10px; table-layout: fixed; - max-width: 970px; + position: relative; colgroup { .first-col { @@ -28,13 +38,16 @@ thead { border-radius: var(--sizing-2-xs, 4px); - border-bottom: 2px solid var(--table-thead-bright-border-color); user-select: none; th { + position: sticky; + top: 0; height: 32px; padding: 5px 15px 5px 10px; color: var(--app-text-color); + border-bottom: 2px solid var(--table-thead-bright-border-color); + background: var(--table-thead-bg-color); } } @@ -78,8 +91,10 @@ footer { margin-left: 10px; + margin-top: 10px; display: flex; flex-direction: row; + flex-shrink: 0; gap: 8px; } diff --git a/src/app/connections/connections.tsx b/src/app/connections/connections.tsx index 0cdc45c8..edb34d47 100644 --- a/src/app/connections/connections.tsx +++ b/src/app/connections/connections.tsx @@ -13,6 +13,7 @@ import * as util from "@/util/util"; import "./connections.less"; import { MainView } from "../common/elements/mainview"; +import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; class ConnectionsKeybindings extends React.Component<{}, {}> { componentDidMount() { @@ -149,57 +150,63 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered - - - - - - - - - - - - - - - - this.handleRead(item.remoteid)} // Moved onClick here - > - - - +
-
Name
-
-
Type
-
-
Status
-
- - {this.getName(item)} {this.getImportSymbol(item)} - -
{item.remotetype}
-
-
- -
-
+ + + + + + + + + + - - -
+
Name
+
+
Type
+
+
Status
+
+ + + + this.handleRead(item.remoteid)} // Moved onClick here + > + + + {this.getName(item)} {this.getImportSymbol(item)} + + +
{item.remotetype}
+ + +
+ +
+ + +
+ + +