Compare commits

...

97 Commits

Author SHA1 Message Date
Evan Simkowitz 0d40d6fff3 looking good 2024-04-22 12:15:47 -07:00
Evan Simkowitz 86cbba1f79 Got the results up to parity, next step is to work on performance 2024-04-19 19:01:11 -07:00
Evan Simkowitz bdf17f0133 move Newton parser to its own file 2024-04-19 16:27:19 -07:00
Evan Simkowitz f5f0791b32 Merge branch 'evan/inshellisense' into evan/inshellisense-newton 2024-04-19 15:08:40 -07:00
Evan Simkowitz 899347e90f Merge branch 'main' into evan/inshellisense 2024-04-19 15:08:27 -07:00
Evan Simkowitz fbc5e1606b added suggestions for options 2024-04-19 15:06:26 -07:00
Sylvie Crowe 39ee41921f fix: add a new line when writing to knownhosts (#582)
This fixes issue #476 by ensuring that when waveterm writes to a
knownhosts file, a newline character is appended at the end.
2024-04-18 14:51:11 -07:00
Evan Simkowitz fde2ce97d2 save work 2024-04-17 10:29:32 -07:00
Evan Simkowitz 93845c405d Got mostly to parity with suggestions in the parser! 2024-04-16 18:01:18 -07:00
Sylvie Crowe 2913babea7 Sudo Caching (#573)
* feat: share sudo between pty sessions

This is a first pass at a feature to cache the sudo password and share
it between different pty sessions. This makes it possible to not require
manual password entry every time sudo is used.

* feat: allow error handling and canceling sudo cmds

This adds the missing functionality that prevented failed sudo commands
from automatically closing.

* feat: restrict sudo caching to dev mode for now

* modify fullCmdStr not pk.Command

* refactor: condense ecdh encryptor creation

This refactors the common pieces needed to create an encryptor from an
ecdh key pair into a common function.

* refactor: rename promptenc to waveenc

* feat: add command to clear sudo password

We currently do not provide use of the sudo -k and sudo -K commands to
clear the sudo password. This adds a /sudo:clear command to handle it in
the meantime.

* feat: add kwarg to force sudo

In cases where parsing for sudo doesn't work, this provides an alternate
wave kwarg to use instead. It can be used with [sudo=1] at the beginning
of a command.

* refactor: simplify sudoArg parsing

* feat: allow user to clear all sudo passwords

This introduces the "all" kwarg for the sudo:clear command in order to
clear all sudo passwords.

* fix: handle deadline with real time

Golang's time module uses monatomic time by default, but that is not
desired for the password timeout since we want the timer to continue
even if the computer is asleep. We now avoid this by directly comparing
the unix timestamps.

* fix: remove sudo restriction to dev mode

This allows it to be used in regular builds as well.

* fix: switch to password timeout without wait group

This removes an unnecessary waiting period for sudo password entry.

* fix: update deadline in sudo:clear

This allows sudo:clear to cancel the goroutine for watching the password
timer.

* fix: pluralize sudo:clear message when all=1

This changes the output message for /sudo:clear to indicate multiple
passwords cleared if the all=1 kwarg is used.

* fix: use GetRemoteMap for getting remotes in clear

The sudo:clear command was directly looping over the GlobalStore.Map
which is not thread safe. Switched to GetRemoteMap which uses a lock
internally.

* fix: allow sudo metacmd to set sudo false

This fixes the logic for resolving if a command is a sudo command. This
change makes it possible for the sudo metacmd kwarg to force sudo to be
false.
2024-04-16 16:58:17 -07:00
Mike Sawka a787a1a934 add k8s context/namespace to prompt (#576)
* k8s in prompt

* test/fix formatting
2024-04-16 13:22:58 -07:00
Cole Lashley bf447c60ce Custom keybind commands (#574)
* added custom keybindings

* removed logs
2024-04-16 12:46:27 -07:00
Evan Simkowitz f2b322c7d7 working to merge my parser into the inshellisense model 2024-04-15 20:22:47 -07:00
Evan Simkowitz c1bd5f5765 fix command execution 2024-04-15 17:22:25 -07:00
Evan Simkowitz b97c7e186d remove unnecessary debugs 2024-04-12 16:07:49 -07:00
Evan Simkowitz 0442fc84d4 update debugs to be more helpful 2024-04-12 16:04:05 -07:00
Evan Simkowitz 27efae3ad5 make the logic less gross 2024-04-12 16:03:27 -07:00
Evan Simkowitz 4d8c2cd242 remove unused import 2024-04-12 15:57:17 -07:00
Evan Simkowitz a560499b58 return filepaths when in doubt 2024-04-12 15:53:54 -07:00
Evan Simkowitz e8b489bcd9 add history and root spec 2024-04-12 15:35:45 -07:00
Evan Simkowitz 31c12c4705 remove and modify debugs 2024-04-11 18:25:35 -07:00
Evan Simkowitz 787bcbc9d8 replace logs with debugs 2024-04-11 18:23:34 -07:00
Evan Simkowitz 985304c9aa demote dotfiles and dotdirs 2024-04-11 18:19:00 -07:00
Evan Simkowitz 13cbfdeb6e fix isEnabled 2024-04-11 18:16:28 -07:00
Evan Simkowitz 53290b47e1 fix bad merge 2024-04-11 18:14:05 -07:00
Evan Simkowitz 4bdd092d33 remove lastCurLine from textareainput 2024-04-11 18:09:28 -07:00
Evan Simkowitz fb2b3dfc6a move import 2024-04-11 18:07:56 -07:00
Evan Simkowitz fa27964d49 move import 2024-04-11 18:06:58 -07:00
Evan Simkowitz 95132011e4 remove import 2024-04-11 18:06:43 -07:00
Evan Simkowitz dae4210b72 rename stuff and add comments 2024-04-11 18:03:04 -07:00
Evan Simkowitz e394ba70a6 move keybinding 2024-04-11 17:55:28 -07:00
Evan Simkowitz 483b2eff6f add right arrow to apply suggestion 2024-04-11 17:54:54 -07:00
Evan Simkowitz 2f4a6fa6e2 Merge branch 'main' into evan/inshellisense 2024-04-11 17:45:24 -07:00
Evan Simkowitz 4af1b19ffd fix lastCurLine 2024-04-11 17:41:11 -07:00
Evan Simkowitz 8b1d930cd6 move import 2024-04-11 17:38:24 -07:00
Evan Simkowitz a4e8648968 add placeholder div if no suggestions 2024-04-11 17:37:50 -07:00
Evan Simkowitz 429ea37419 change onclose 2024-04-11 17:23:16 -07:00
Evan Simkowitz a8f3e356f6 add keybindings 2024-04-11 17:22:51 -07:00
Evan Simkowitz 85235aaad8 move autocomplete to its own model, add more styling to suggestion view 2024-04-11 17:16:22 -07:00
Evan Simkowitz 619cfe4931 rudimentary show all autocomplete 2024-04-11 16:00:52 -07:00
Evan Simkowitz 11f1c3bb70 remove finished todos 2024-04-11 15:50:46 -07:00
Evan Simkowitz 8d140c9920 Add file and folder completions 2024-04-11 15:49:46 -07:00
Evan Simkowitz 205a9d1e2e Merge branch 'main' into evan/inshellisense 2024-04-10 23:11:14 -07:00
Evan Simkowitz d3139d51cf add new license 2024-04-10 22:08:08 -07:00
Evan Simkowitz 5c931de40c fix caching 2024-04-10 21:48:23 -07:00
Evan Simkowitz 497a96bfc2 adds trigger and begins to add caching 2024-04-10 17:20:51 -07:00
Evan Simkowitz 427447c592 Merge branch 'main' into evan/inshellisense 2024-04-09 17:11:25 -07:00
Evan Simkowitz 1fe5d425da set non-transparent colors for light mode, fix fuzzy letters issue with ghost prompt 2024-04-09 17:07:15 -07:00
Evan Simkowitz a395043a1c enforce setting 2024-04-09 16:40:47 -07:00
Evan Simkowitz ff9f9b43b6 Add client setting for autocomplete 2024-04-09 16:29:59 -07:00
Evan Simkowitz 98ba0b46ac clear suggestions in more places 2024-04-09 12:33:16 -07:00
Evan Simkowitz fb3253d018 cache last command 2024-04-09 12:22:27 -07:00
Evan Simkowitz 8d91d156aa handle flicker when deleting chars 2024-04-09 12:09:42 -07:00
Evan Simkowitz b7eec15853 Adding ghost text 2024-04-09 11:57:07 -07:00
Evan Simkowitz 38ff6183a2 get suggestions on typing 2024-04-08 18:47:35 -07:00
Evan Simkowitz 1b1e6edaee Merge branch 'main' into evan/inshellisense 2024-04-08 13:47:17 -07:00
Evan Simkowitz f4aeb92d32 Merge branch 'main' into evan/inshellisense 2024-04-08 13:16:01 -07:00
Evan Simkowitz b780d5b244 Merge branch 'main' into evan/inshellisense 2024-04-08 11:19:25 -07:00
Evan Simkowitz d81a6ed480 Merge branch 'main' into evan/inshellisense 2024-04-05 10:49:22 -07:00
Evan Simkowitz d8393ae50b Merge branch 'main' into evan/inshellisense 2024-04-02 18:48:59 -07:00
Evan Simkowitz 79deb01ea4 save work 2024-04-02 18:48:38 -07:00
Evan Simkowitz 12f9a50686 beginning to add autocomplete to textareainput 2024-04-02 18:35:12 -07:00
Evan Simkowitz 239933be3e Got autocomplete working! 2024-04-02 18:14:49 -07:00
Evan Simkowitz 4692cf698b hook up runtime 2024-04-02 17:13:54 -07:00
Evan Simkowitz 907e87c217 expand cwd 2024-04-02 17:05:14 -07:00
Evan Simkowitz f97bb699a1 plumb timeout to waveshell 2024-04-02 16:57:34 -07:00
Evan Simkowitz 077545ed93 bad merrge 2024-04-02 15:50:46 -07:00
Evan Simkowitz 60d0f0e976 Merge branch 'main' into evan/inshellisense 2024-04-02 15:49:32 -07:00
Evan Simkowitz cd4671e5bb store pointer to syncs 2024-04-02 14:58:39 -07:00
Evan Simkowitz d8a714dd4a ensure only one downstream writer can read from the buffer 2024-04-02 14:28:15 -07:00
Evan Simkowitz e0fa1b2560 document UsePty 2024-04-02 14:19:46 -07:00
Evan Simkowitz 2dffcc9237 add UsePty to EphemeralRunOpts 2024-04-02 14:17:12 -07:00
Evan Simkowitz 70090b8684 add package comment 2024-04-02 14:08:10 -07:00
Evan Simkowitz 3f8952caf4 more naming changes 2024-04-02 14:06:51 -07:00
Evan Simkowitz 15a2486e94 delete unused package 2024-04-02 14:01:57 -07:00
Evan Simkowitz 38aa7016b8 comment 2024-04-02 14:01:03 -07:00
Evan Simkowitz cff353ca94 formatting 2024-04-02 13:59:59 -07:00
Evan Simkowitz 5b440ace65 Making EphemeralWriteCloser into a more generic BufferedPipe 2024-04-02 13:58:56 -07:00
Evan Simkowitz 87de0dbc44 revert waveshell changes 2024-04-01 22:17:44 -07:00
Evan Simkowitz 74839b8e1b env vars are now working 2024-04-01 22:12:58 -07:00
Evan Simkowitz 8a24953a88 fix stdout/stderr 2024-04-01 20:55:23 -07:00
Evan Simkowitz a95854514a Add separate paths for stdout and stderr writers 2024-04-01 20:51:26 -07:00
Evan Simkowitz 229fcf8cd4 got cwd override working 2024-04-01 18:27:18 -07:00
Evan Simkowitz 83542ddab8 clean up code 2024-04-01 17:42:03 -07:00
Evan Simkowitz b520242f04 fixing spaghetti 2024-04-01 17:12:51 -07:00
Evan Simkowitz bc73ed1b6c save debug statements 2024-04-01 16:04:42 -07:00
Evan Simkowitz c4647945a4 fix bad merge 2024-04-01 14:22:23 -07:00
Evan Simkowitz 3db40b1c63 Merge branch 'main' into evan/inshellisense 2024-04-01 14:22:17 -07:00
Evan Simkowitz f6d7c37915 save debugging 2024-03-29 16:54:29 -07:00
Evan Simkowitz a41e866b1f Command pipes thru, triggers infinite loop 2024-03-28 18:44:37 -07:00
Evan Simkowitz a772715ae4 Add EphemeralWriteCloser 2024-03-28 18:02:27 -07:00
Evan Simkowitz d32e2aae05 Merge branch 'main' into evan/inshellisense 2024-03-28 16:24:27 -07:00
Evan Simkowitz 3f9d52a7e4 save work 2024-03-26 16:04:37 -07:00
Evan Simkowitz a239a8d70e Merge branch 'main' into evan/inshellisense 2024-03-26 13:00:21 -07:00
Evan Simkowitz a1bddd971f save work 2024-03-25 17:43:02 -07:00
Evan Simkowitz 722318f57d save work, starting to add backend types 2024-03-25 10:54:27 -07:00
Evan Simkowitz 3b633328a1 initial 2024-03-22 16:01:08 -07:00
55 changed files with 3849 additions and 163 deletions
+2
View File
@@ -24,6 +24,7 @@
"@table-nav/react": "^0.0.7",
"@tanstack/match-sorter-utils": "^8.8.4",
"@tanstack/react-table": "^8.10.3",
"@withfig/autocomplete": "^2.652.3",
"autobind-decorator": "^2.4.0",
"base64-js": "^1.5.1",
"classnames": "^2.3.1",
@@ -79,6 +80,7 @@
"@types/throttle-debounce": "^5.0.1",
"@types/uuid": "^9.0.7",
"@types/webpack-env": "^1.18.3",
"@withfig/autocomplete-types": "^1.30.0",
"babel-loader": "^9.1.3",
"babel-plugin-jsx-control-statements": "^4.1.2",
"copy-webpack-plugin": "^12.0.0",
+2 -2
View File
@@ -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);
+1
View File
@@ -68,3 +68,4 @@ export const ErrorCode_InvalidCwd = "ERRCWD";
export const InputAuxView_History = "history";
export const InputAuxView_Info = "info";
export const InputAuxView_AIChat = "aichat";
export const InputAuxView_Suggestions = "suggestions";
+16 -1
View File
@@ -11,9 +11,9 @@ import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "@/commo
import { commandRtnHandler, isBlank } from "@/util/util";
import { getTermThemes } from "@/util/themeutil";
import * as appconst from "@/app/appconst";
import { MainView } from "@/common/elements/mainview";
import "./clientsettings.less";
import { MainView } from "../common/elements/mainview";
class ClientSettingsKeybindings extends React.Component<{}, {}> {
componentDidMount() {
@@ -110,6 +110,12 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
GlobalModel.getElectronApi().changeAutoUpdate(val);
}
@boundMethod
handleChangeAutocompleteEnabled(val: boolean): void {
const prtn: Promise<CommandRtnType> = GlobalCommandRunner.setAutocompleteEnabled(val);
commandRtnHandler(prtn, this.errorMessage);
}
getFontSizes(): DropdownItem[] {
const availableFontSizes: DropdownItem[] = [];
for (let s = appconst.MinFontSize; s <= appconst.MaxFontSize; s++) {
@@ -354,6 +360,15 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">Command Autocomplete</div>
<div className="settings-input">
<Toggle
checked={cdata.clientopts.autocompleteenabled ?? false}
onChange={this.handleChangeAutocompleteEnabled}
/>
</div>
</div>
<SettingsError errorMessage={this.errorMessage} />
</div>
</MainView>
+4
View File
@@ -58,4 +58,8 @@
.term-prompt-python {
color: var(--term-magenta);
}
.term-prompt-k8s {
color: var(--term-magenta);
}
}
+12 -1
View File
@@ -146,6 +146,7 @@ class Prompt extends React.Component<
let branchElem = null;
let pythonElem = null;
let condaElem = null;
let k8sElem = null;
if (!isBlank(festate["PROMPTVAR_GITBRANCH"])) {
const branchName = festate["PROMPTVAR_GITBRANCH"];
branchElem = (
@@ -171,9 +172,19 @@ class Prompt extends React.Component<
</span>
);
}
if (!isBlank(festate["K8SCONTEXT"])) {
const k8sContext = festate["K8SCONTEXT"];
const k8sNs = festate["K8SNAMESPACE"];
k8sElem = (
<span title="k8s context:namespace" className="term-prompt-k8s">
k8s:({k8sContext}
{isBlank(k8sNs) ? "" : ":" + k8sNs}){" "}
</span>
);
}
return (
<span className={termClassNames}>
{remoteElem} {cwdElem} {branchElem} {condaElem} {pythonElem}
{remoteElem} {cwdElem} {branchElem} {condaElem} {pythonElem} {k8sElem}
</span>
);
}
+1 -1
View File
@@ -92,7 +92,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) {
+33 -35
View File
@@ -4,47 +4,45 @@
import * as React from "react";
import cn from "classnames";
import { If } from "tsx-control-statements/components";
import { observer } from "mobx-react";
import "./auxview.less";
export class AuxiliaryCmdView extends React.Component<
{
title: string;
className?: string;
iconClass?: string;
titleBarContents?: React.ReactElement[];
children?: React.ReactNode;
onClose?: React.MouseEventHandler<HTMLDivElement>;
},
{}
> {
render() {
const { title, className, iconClass, titleBarContents, children, onClose } = this.props;
interface AuxiliaryCmdViewProps {
title: string;
className?: string;
iconClass?: string;
titleBarContents?: React.ReactElement[];
children?: React.ReactNode;
onClose?: React.MouseEventHandler<HTMLDivElement>;
}
return (
<div className={cn("auxview", className)}>
<div className="auxview-titlebar">
<If condition={iconClass != null}>
<div className="title-icon">
<i className={iconClass} />
</div>
</If>
<div className="title-string">{title}</div>
export const AuxiliaryCmdView: React.FC<AuxiliaryCmdViewProps> = observer((props) => {
const { title, className, iconClass, titleBarContents, children, onClose } = props;
<If condition={titleBarContents != null}>{titleBarContents}</If>
return (
<div className={cn("auxview", className)}>
<div className="auxview-titlebar">
<If condition={iconClass != null}>
<div className="title-icon">
<i className={iconClass} />
</div>
</If>
<div className="title-string">{title}</div>
<div className="flex-spacer"></div>
<If condition={titleBarContents != null}>{titleBarContents}</If>
<If condition={onClose != null}>
<div className="close-button" title="Close (ESC)" onClick={onClose}>
<i className="fa-sharp fa-solid fa-xmark-large" />
</div>
</If>
</div>
<If condition={children != null}>
<div className="auxview-content">{children}</div>
<div className="flex-spacer"></div>
<If condition={onClose != null}>
<div className="close-button" title="Close (ESC)" onClick={onClose}>
<i className="fa-sharp fa-solid fa-xmark-large" />
</div>
</If>
</div>
);
}
}
<If condition={children != null}>
<div className="auxview-content">{children}</div>
</If>
</div>
);
});
+18 -4
View File
@@ -14,7 +14,8 @@
// TODO: use a generic selector for this
&.has-aichat,
&.has-history,
&.has-info {
&.has-info,
&.has-suggestions {
.base-cmdinput {
border-top: 1px solid var(--app-border-color);
}
@@ -109,9 +110,11 @@
}
}
textarea {
color: var(--app-text-primary-color);
background-color: var(--app-bg-color);
.textarea,
.textarea-ghost {
position: absolute;
top: 0;
left: 0;
padding: var(--termpad) 0;
resize: none;
overflow: auto;
@@ -119,10 +122,21 @@
font-family: var(--termfontfamily);
line-height: var(--termlineheight);
font-size: var(--termfontsize);
background-color: transparent;
border: none;
box-shadow: none;
}
.textarea {
color: var(--app-text-primary-color);
z-index: 2;
}
.textarea-ghost {
color: var(--app-text-secondary-color);
z-index: 1;
}
input.history-input {
border: 0;
padding: 0;
+5
View File
@@ -19,6 +19,7 @@ import { CenteredIcon, RotateIcon } from "@/common/icons/icons";
import { AIChat } from "./aichat";
import * as util from "@/util/util";
import * as appconst from "@/app/appconst";
import { SuggestionView } from "./suggestionview";
import "./cmdinput.less";
@@ -181,6 +182,7 @@ class CmdInput extends React.Component<{}, {}> {
hidePrompt = true;
}
}
return (
<div ref={this.cmdInputRef} className={cn("cmd-input", hasOpenView, { active: focusVal })}>
<Choose>
@@ -195,6 +197,9 @@ class CmdInput extends React.Component<{}, {}> {
<When condition={openView === appconst.InputAuxView_Info}>
<InfoMsg key="infomsg" />
</When>
<When condition={openView === appconst.InputAuxView_Suggestions}>
<SuggestionView />
</When>
</Choose>
<If condition={remote && remote.status != "connected"}>
<div className="remote-status-warning">
@@ -11,7 +11,6 @@
.history-clickable-opt {
cursor: pointer;
white-space: nowrap;
cursor: pointer;
&:hover {
color: var(--app-text-primary-color);
+1 -1
View File
@@ -225,7 +225,7 @@ class HistoryInfo extends React.Component<{}, {}> {
render() {
const inputModel = GlobalModel.inputModel;
const selItem = inputModel.getHistorySelectedItem();
const hitems = inputModel.getFilteredHistoryItems().slice().reverse();
const hitems = inputModel.filteredHistoryItems.slice().reverse();
const opts = inputModel.historyQueryOpts.get();
let hitem: HistoryItem = null;
let snames: Record<string, string> = {};
@@ -0,0 +1,25 @@
.suggestions-view .auxview-content {
display: flex;
flex-direction: column;
min-height: 1em;
.suggestion-item {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
cursor: pointer;
border-radius: 5px;
&:hover {
background-color: var(--table-tr-hover-bg-color);
}
&.is-selected {
font-weight: bold;
color: var(--app-text-primary-color);
background-color: var(--table-tr-selected-bg-color);
&:hover {
background-color: var(--table-tr-selected-hover-bg-color);
}
}
}
}
@@ -0,0 +1,77 @@
import { SuggestionBlob } from "@/autocomplete/runtime/model";
import { AuxiliaryCmdView } from "./auxview";
import { GlobalModel } from "@/models";
import cn from "classnames";
import React, { useEffect } from "react";
import { observer } from "mobx-react";
import { If } from "tsx-control-statements/components";
import "./suggestionview.less";
import { getAll, getFirst } from "@/autocomplete/runtime/utils";
export const SuggestionView: React.FC = observer(() => {
const [selectedSuggestion, setSelectedSuggestion] = React.useState<number>(0);
useEffect(() => {
const keybindManager = GlobalModel.keybindManager;
keybindManager.registerKeybinding("pane", "aichat", "generic:confirm", (waveEvent) => {
setSuggestion(selectedSuggestion);
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "generic:cancel", (waveEvent) => {
closeView();
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "generic:selectAbove", (waveEvent) => {
setSelectedSuggestion(Math.max(0, selectedSuggestion - 1));
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "generic:selectBelow", (waveEvent) => {
setSelectedSuggestion(Math.min(suggestions?.suggestions.length - 1, selectedSuggestion + 1));
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "generic:tab", (waveEvent) => {
setSelectedSuggestion(Math.min(suggestions?.suggestions.length - 1, selectedSuggestion + 1));
return true;
});
return () => {
GlobalModel.keybindManager.unregisterDomain("aichat");
};
});
const inputModel = GlobalModel.inputModel;
const autocompleteModel = GlobalModel.autocompleteModel;
const suggestions: SuggestionBlob = autocompleteModel.getSuggestions();
const closeView = () => {
inputModel.closeAuxView();
};
const setSuggestion = (idx: number) => {
autocompleteModel.applySuggestion(idx);
autocompleteModel.loadSuggestions();
closeView();
};
return (
<AuxiliaryCmdView title="Suggestions" className="suggestions-view" onClose={closeView}>
<If condition={!suggestions}>
<div className="no-suggestions">No suggestions</div>
</If>
{suggestions?.suggestions.map((suggestion, idx) => (
<div
key={getFirst(suggestion.name)}
title={suggestion.description}
className={cn("suggestion-item", { "is-selected": selectedSuggestion === idx })}
onClick={() => {
setSuggestion(idx);
}}
>
{suggestion.icon} {getAll(suggestion.name).join(",")}{" "}
{suggestion.description ? `- ${suggestion.description}` : ""}
</div>
))}
</AuxiliaryCmdView>
);
});
+34 -26
View File
@@ -11,6 +11,8 @@ import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, Screen } from "@/models";
import { getMonoFontSize } from "@/util/textmeasure";
import * as appconst from "@/app/appconst";
import { Shell, getSuggestions } from "@/autocomplete";
import { SuggestionBlob } from "@/autocomplete/runtime/model";
type OV<T> = mobx.IObservableValue<T>;
@@ -38,7 +40,7 @@ function scrollDiv(div: any, amt: number) {
div.scrollTo({ top: newScrollTop, behavior: "smooth" });
}
class HistoryKeybindings extends React.Component<{ inputObject: TextAreaInput }, {}> {
class HistoryKeybindings extends React.Component<{}, {}> {
componentDidMount(): void {
if (GlobalModel.activeMainView != "session") {
return;
@@ -101,38 +103,25 @@ class HistoryKeybindings extends React.Component<{ inputObject: TextAreaInput },
}
class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput }, {}> {
lastTab: boolean;
curPress: string;
lastTab: boolean;
componentDidMount() {
if (GlobalModel.activeMainView != "session") {
return;
}
const inputObject = this.props.inputObject;
this.lastTab = false;
const keybindManager = GlobalModel.keybindManager;
const inputModel = GlobalModel.inputModel;
keybindManager.registerKeybinding("pane", "cmdinput", "cmdinput:autocomplete", (waveEvent) => {
const lastTab = this.lastTab;
this.lastTab = true;
this.curPress = "tab";
const curLine = inputModel.getCurLine();
if (lastTab) {
GlobalModel.submitCommand(
"_compgen",
null,
[curLine],
{ comppos: String(curLine.length), compshow: "1", nohist: "1" },
true
);
if (this.lastTab) {
const curLine = inputModel.curLine;
if (curLine != "") {
inputModel.setActiveAuxView(appconst.InputAuxView_Suggestions);
}
} else {
GlobalModel.submitCommand(
"_compgen",
null,
[curLine],
{ comppos: String(curLine.length), nohist: "1" },
true
);
this.lastTab = true;
}
return true;
});
@@ -203,6 +192,10 @@ class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput }
const rtn = inputObject.arrowDownPressed();
return rtn;
});
keybindManager.registerKeybinding("pane", "cmdinput", "generic:selectRight", (waveEvent) => {
inputObject.arrowRightPressed();
return true;
});
keybindManager.registerKeybinding("pane", "cmdinput", "generic:selectPageAbove", (waveEvent) => {
this.curPress = "historyupdown";
inputObject.scrollPage(true);
@@ -240,9 +233,7 @@ class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput }
@mobxReact.observer
class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () => void }, {}> {
lastTab: boolean = false;
lastHistoryUpDown: boolean = false;
lastTabCurLine: OV<string> = mobx.observable.box(null);
lastFocusType: string = null;
mainInputRef: React.RefObject<HTMLTextAreaElement> = React.createRef();
historyInputRef: React.RefObject<HTMLInputElement> = React.createRef();
@@ -399,6 +390,11 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
return true;
}
@boundMethod
arrowRightPressed() {
GlobalModel.autocompleteModel.applyPrimarySuggestion();
}
scrollPage(up: boolean) {
const inputModel = GlobalModel.inputModel;
const infoScroll = inputModel.hasScrollingInfoMsg();
@@ -562,7 +558,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();
@@ -605,6 +601,10 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
}
}
const isHistoryFocused = auxViewFocused && inputModel.getActiveAuxView() == appconst.InputAuxView_History;
// Will be null if the feature is disabled
const primaryAutocompleteSuggestion = GlobalModel.autocompleteModel.getPrimarySuggestionCompletion();
return (
<div
className="textareainput-div control is-expanded"
@@ -612,15 +612,23 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
style={{ height: computedOuterHeight }}
>
<If condition={!auxViewFocused}>
<CmdInputKeybindings inputObject={this}></CmdInputKeybindings>
<CmdInputKeybindings inputObject={this} />
</If>
<If condition={isHistoryFocused}>
<HistoryKeybindings inputObject={this}></HistoryKeybindings>
<HistoryKeybindings />
</If>
<If condition={!util.isBlank(shellType)}>
<div className="shelltag">{shellType}</div>
</If>
<If condition={primaryAutocompleteSuggestion}>
<div
className="textarea-ghost"
style={{ height: computedInnerHeight, minHeight: computedInnerHeight, fontSize: termFontSize }}
>
{`${"\xa0".repeat(curLine.length)}${primaryAutocompleteSuggestion}`}
</div>
</If>
<textarea
key="main"
ref={this.mainInputRef}
+3
View File
@@ -0,0 +1,3 @@
# Autocomplete parser
This is the autocomplete parser for Wave. Much of the runtime for this parser is forked from the [@microsoft/inshellisense project](https://github.com/microsoft/inshellisense). We've modified the exec code to proxy commands to the active `waveshell` instance. All suggestions, as with inshellisense, come from the [@withfig/autocomplete project](https://github.com/withfig/autocomplete). We will be supplementing these with some of our own autocomplete for our own `/slashcommands` and `metacommands`.
+5
View File
@@ -0,0 +1,5 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
export * from "./runtime/runtime";
export * from "./utils/shell";
+164
View File
@@ -0,0 +1,164 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// Modified from https://github.com/microsoft/inshellisense/blob/main/src/runtime/generator.ts
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { GlobalModel } from "@/models";
import log from "../utils/log";
import { runTemplates } from "./template";
import { buildExecuteShellCommand } from "./utils";
const getGeneratorContext = async (cwd: string): Promise<Fig.GeneratorContext> => {
const resp = await GlobalModel.submitEphemeralCommand("eval", null, ["env"], null, false, {
expectsresponse: true,
overridecwd: cwd,
env: null,
});
const { stdout, stderr } = await GlobalModel.getEphemeralCommandOutput(resp);
if (stderr) {
log.debug({ msg: "failed to get environment variables", stderr });
}
const env = {};
stdout
.split("\n")
.filter((s) => s.length > 0)
.forEach((line) => {
const [key, value] = line.split("=");
env[key] = value;
});
return {
environmentVariables: env,
currentWorkingDirectory: cwd,
currentProcess: "", // TODO: define current process
sshPrefix: "", // deprecated, should be empty
isDangerous: false,
searchTerm: "", // TODO: define search term
};
};
let lastFirstToken = "";
let lastFinalToken = "";
let cachedSuggestions: Fig.Suggestion[] = [];
// TODO: add support getQueryTerm
export const runGenerator = async (
generator: Fig.Generator,
tokens: string[],
cwd: string
): Promise<Fig.Suggestion[]> => {
const { script, postProcess, scriptTimeout, splitOn, custom, template, filterTemplateSuggestions, trigger } =
generator;
const newToken = tokens.at(-1) ?? "";
if (lastFirstToken == tokens.at(0) && trigger && cachedSuggestions.length > 0) {
log.debug("trigger", trigger);
if (typeof trigger === "string") {
if (!newToken?.includes(trigger)) {
log.debug("trigger string", newToken, trigger);
return cachedSuggestions;
}
} else if (typeof trigger === "function") {
log.debug("trigger function", "newToken:", newToken, "lastToken: ", lastFinalToken);
if (!trigger(newToken, lastFinalToken ?? "")) {
log.debug("trigger function false");
return cachedSuggestions;
} else {
log.debug("trigger function true");
}
} else {
switch (trigger.on) {
case "change": {
log.debug("trigger change", newToken, lastFinalToken);
if (lastFinalToken && newToken && lastFinalToken === newToken) {
log.debug("trigger change false");
return cachedSuggestions;
} else {
log.debug("trigger change true");
}
break;
}
case "match": {
if (Array.isArray(trigger.string)) {
log.debug("trigger match array", newToken, trigger.string);
if (!trigger.string.some((t) => newToken === t)) {
log.debug("trigger match false");
return cachedSuggestions;
} else {
log.debug("trigger match true");
}
} else if (trigger.string !== newToken) {
log.debug("trigger match single true", newToken, trigger.string);
return cachedSuggestions;
} else {
log.debug("trigger match single false", newToken, trigger.string);
}
break;
}
case "threshold": {
log.debug("trigger threshold", newToken, lastFinalToken, trigger.length);
if (Math.abs(newToken.length - lastFinalToken.length) < trigger.length) {
log.debug("trigger threshold false");
return cachedSuggestions;
} else {
log.debug("trigger threshold true");
}
break;
}
}
}
} else if (lastFirstToken === tokens.at(0) && newToken && lastFinalToken === newToken) {
log.debug("lastToken === newToken", lastFinalToken, newToken);
return cachedSuggestions;
}
log.debug("lastToken !== newToken", lastFinalToken, newToken);
const executeShellCommand = buildExecuteShellCommand(scriptTimeout ?? 5000);
const suggestions = [];
lastFinalToken = tokens[-1];
lastFirstToken = tokens[0];
try {
if (script) {
const shellInput = typeof script === "function" ? script(tokens) : script;
const scriptOutput = Array.isArray(shellInput)
? await executeShellCommand({ command: shellInput.at(0) ?? "", args: shellInput.slice(1), cwd })
: await executeShellCommand({ ...shellInput, cwd });
const scriptStdout = scriptOutput.stdout.trim();
const scriptStderr = scriptOutput.stderr.trim();
if (scriptStderr) {
log.debug("script error, skipping processing", scriptStderr);
} else if (postProcess) {
suggestions.push(...postProcess(scriptStdout, tokens));
} else if (splitOn) {
suggestions.push(...scriptStdout.split(splitOn).map((s) => ({ name: s })));
}
}
if (custom) {
log.debug("custom", custom);
const customSuggestions = await custom(tokens, executeShellCommand, await getGeneratorContext(cwd));
log.debug("customSuggestions", customSuggestions);
suggestions.push(...customSuggestions);
}
if (template != null) {
const templateSuggestions = await runTemplates(template, cwd);
if (filterTemplateSuggestions) {
suggestions.push(...filterTemplateSuggestions(templateSuggestions));
} else {
suggestions.push(...templateSuggestions);
}
}
cachedSuggestions = suggestions;
return suggestions;
} catch (e) {
const err = typeof e === "string" ? e : e instanceof Error ? e.message : e;
log.debug({ msg: "generator failed", err, script, splitOn, template });
}
return suggestions;
};
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// Modified from https://github.com/microsoft/inshellisense/blob/main/src/runtime/model.ts
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
export type Suggestion = {
name: string;
allNames: string[];
description?: string;
icon: string;
priority: number;
insertValue?: string;
};
export type SuggestionBlob = {
suggestions: Fig.Suggestion[];
argumentDescription?: string;
charactersToDrop?: number;
};
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More