fix: update with fixes from new version

This commit is contained in:
Sylvia Crowe 2024-09-17 13:18:34 -07:00
commit 33f2532ae8
26 changed files with 1966 additions and 1703 deletions

View File

@ -16,6 +16,10 @@ jobs:
arch: "amd64" arch: "amd64"
runner: "ubuntu-latest" runner: "ubuntu-latest"
scripthaus: "build-package-linux" scripthaus: "build-package-linux"
- platform: "linux"
arch: "arm64"
runner: ubuntu-24.04-arm64-16core
scripthaus: "build-package-linux"
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -28,6 +32,8 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install --no-install-recommends -y libarchive-tools libopenjp2-tools rpm sudo apt-get install --no-install-recommends -y libarchive-tools libopenjp2-tools rpm
- name: Install FPM # The version of FPM that comes bundled with electron-builder doesn't include a Linux ARM target. Installing Gems onto the runner is super quick so we'll just do this for all targets.
run: sudo gem install fpm
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: ${{env.GO_VERSION}} go-version: ${{env.GO_VERSION}}
@ -58,6 +64,7 @@ jobs:
run: scripthaus run ${{ matrix.scripthaus }} run: scripthaus run ${{ matrix.scripthaus }}
env: env:
GOARCH: ${{ matrix.arch }} GOARCH: ${{ matrix.arch }}
USE_SYSTEM_FPM: true # Ensure that the installed version of FPM is used rather than the bundled one.
CSC_LINK: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE}} CSC_LINK: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE}}
CSC_KEY_PASSWORD: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE_PWD }} CSC_KEY_PASSWORD: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE_PWD }}
APPLE_ID: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} APPLE_ID: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}

View File

@ -1,4 +1,4 @@
name: TestDriver.ai Regression Testing name: TestDriver.ai Regression Testing - Waveterm
on: on:
push: push:
branches: branches:
@ -14,17 +14,19 @@ permissions:
contents: read # To allow the action to read repository contents contents: read # To allow the action to read repository contents
pull-requests: write # To allow the action to create/update pull request comments pull-requests: write # To allow the action to create/update pull request comments
jobs: jobs:
test: test:
name: TestDriver name: "TestDriver"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dashcamio/testdriver@main - uses: dashcamio/testdriver@main
id: testdriver id: testdriver
with: with:
version: v2.12.12 version: v3.9.0
key: ${{secrets.DASHCAM_API}}
os: mac
prerun: | prerun: |
rm ~/Desktop/WITH-LOVE-FROM-AMERICA.txt
cd ~/actions-runner/_work/testdriver/testdriver/ cd ~/actions-runner/_work/testdriver/testdriver/
brew install go brew install go
brew tap scripthaus-dev/scripthaus brew tap scripthaus-dev/scripthaus
@ -42,25 +44,8 @@ jobs:
echo "Starting Electron" echo "Starting Electron"
scripthaus run electron 1>/dev/null 2>&1 & scripthaus run electron 1>/dev/null 2>&1 &
echo "Electron Done" echo "Electron Done"
cd /Users/ec2-user/Downloads/td/
npm rebuild
exit exit
prompt: | prompt: |
1. wait 10 seconds 1. /run /Users/ec2-user/actions-runner/_work/testdriver/testdriver/.testdriver/wave1.yml
1. click "Get Started"
1. validate that overlapping text does not appear in the application
1. focus the Wave input with the keyboard shorcut Command + I
1. type 'ls' into the input
1. press return
1. 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.markdown }}
${{ steps.testdriver.outputs.summary }}
reactions: |
+1
-1

View File

@ -1,18 +0,0 @@
rm ~/Desktop/WITH-LOVE-FROM-AMERICA.txt
cd ~/actions-runner/_work/testdriver/testdriver/
brew install go
brew tap scripthaus-dev/scripthaus
brew install scripthaus
corepack enable
yarn install
scripthaus run build-backend
echo "Yarn"
yarn
echo "Rebuild"
scripthaus run electron-rebuild
echo "Webpack"
scripthaus run webpack-build
echo "Starting Electron"
scripthaus run electron 1>/dev/null 2>&1 &
echo "Electron Done"
exit

37
.testdriver/wave1.yml Normal file
View File

@ -0,0 +1,37 @@
version: 3.8.0
steps:
- prompt: "Focus electron"
commands:
- command: focus-application
name: Electron
- command: hover-text
description: Get started CTA
text: Get Started
action: click
- command: hover-text
description: Settings button
text: Settings
action: click
- command: hover-text
description: font size 13
text: 13px
action: click
- command: hover-text
description: font size 12
text: 12px
action: click
- command: hover-text
description: theme selector
text: Dark
action: click
- command: hover-text
description: theme color white
text: Light
action: click
- command: hover-text
description: workspace
text: workspace-1
action: click
- command: assert
expect: the terminal is white

View File

@ -309,10 +309,6 @@
"command": "aichat:clearHistory", "command": "aichat:clearHistory",
"keys": ["Ctrl:l"] "keys": ["Ctrl:l"]
}, },
{
"command": "aichat:setCmdInputValue",
"keys": ["Ctrl:Shift:e"]
},
{ {
"command": "terminal:copy", "command": "terminal:copy",
"keys": ["Ctrl:Shift:c"] "keys": ["Ctrl:Shift:c"]

View File

@ -34,20 +34,23 @@ You'll now have to move the built `scripthaus` binary to a directory in your pat
sudo cp scripthaus /usr/local/bin sudo cp scripthaus /usr/local/bin
``` ```
## Install nodejs, npm, and yarn ## Install nodejs and yarn
We use [nvm](https://github.com/nvm-sh/nvm) to install nodejs on Linux (you can use an alternate installer if you wish). You must have a relatively recent version of node in order to build the terminal. Different distributions and shells will require different setup instructions. These instructions work for Ubuntu 22 using bash (will install node v20.8.1): You also need a relatively modern nodejs with npm and yarn installed.
``` Node can be installed from [https://nodejs.org](https://nodejs.org).
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
source ~/.bashrc
nvm install v20.8.1
```
Now we can install yarn: We use Yarn Modern to manage our packages. The recommended way to install Yarn Modern is using Corepack, a new utility shipped by NodeJS that lets you manage your package manager versioning as you would any packages.
``` If you installed NodeJS from the official feed (via the website or using NVM), this should come preinstalled. If you use Homebrew or some other feed, you may need to manually install Corepack using `npm install -g corepack`.
npm install -g yarn
For more information on Corepack, check out [this link](https://yarnpkg.com/corepack).
Once you've verified that you have Corepack installed, run the following script to set up Yarn for the repository:
```sh
corepack enable
yarn install
``` ```
## Clone the Wave Repo ## Clone the Wave Repo

View File

@ -18,95 +18,95 @@
"appId": "dev.commandline.waveterm" "appId": "dev.commandline.waveterm"
}, },
"dependencies": { "dependencies": {
"@lexical/react": "^0.14.3", "@lexical/react": "^0.17.0",
"@monaco-editor/react": "^4.5.1", "@monaco-editor/react": "^4.6.0",
"@table-nav/core": "^0.0.7", "@table-nav/core": "^0.0.7",
"@table-nav/react": "^0.0.7", "@table-nav/react": "^0.0.7",
"@tanstack/match-sorter-utils": "^8.8.4", "@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-table": "^8.10.3", "@tanstack/react-table": "^8.20.1",
"@withfig/autocomplete": "^2.652.3", "@withfig/autocomplete": "^2.672.0",
"autobind-decorator": "^2.4.0", "autobind-decorator": "^2.4.0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.3", "dayjs": "^1.11.12",
"dompurify": "^3.0.2", "dompurify": "^3.1.6",
"electron-squirrel-startup": "^1.0.0", "electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.1.8", "electron-updater": "^6.3.2",
"framer-motion": "^10.16.16", "framer-motion": "^10.18.0",
"lexical": "0.14.5", "lexical": "0.14.5",
"mobx": "6.12", "mobx": "6.12.5",
"mobx-react": "^7.5.0", "mobx-react": "^7.6.0",
"monaco-editor": "0.48.0", "monaco-editor": "0.48.0",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"node-fetch": "^3.2.10", "node-fetch": "^3.3.2",
"overlayscrollbars": "^2.6.1", "overlayscrollbars": "^2.10.0",
"overlayscrollbars-react": "^0.5.5", "overlayscrollbars-react": "^0.5.6",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"react": "^18.1.0", "react": "^18.3.1",
"react-dom": "^18.1.0", "react-dom": "^18.3.1",
"react-markdown": "^9.0.0", "react-markdown": "^9.0.1",
"remark": "^15.0.1", "remark": "^15.0.1",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"sprintf-js": "^1.1.2", "sprintf-js": "^1.1.3",
"throttle-debounce": "^5.0.0", "throttle-debounce": "^5.0.2",
"tinycolor2": "^1.6.0", "tinycolor2": "^1.6.0",
"tsx-control-statements": "^5.1.1", "tsx-control-statements": "^5.1.1",
"uuid": "^9.0.0", "uuid": "^9.0.1",
"winston": "^3.8.2", "winston": "^3.13.1",
"xterm": "^5.3.0", "xterm": "^5.3.0",
"xterm-addon-serialize": "^0.11.0", "xterm-addon-serialize": "^0.11.0",
"xterm-addon-web-links": "^0.9.0", "xterm-addon-web-links": "^0.9.0",
"xterm-addon-webgl": "^0.16.0" "xterm-addon-webgl": "^0.16.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.17.10", "@babel/cli": "^7.24.8",
"@babel/core": "^7.18.2", "@babel/core": "^7.25.2",
"@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.18.2", "@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/plugin-transform-react-jsx": "^7.17.12", "@babel/plugin-transform-react-jsx": "^7.25.2",
"@babel/plugin-transform-runtime": "^7.23.4", "@babel/plugin-transform-runtime": "^7.24.7",
"@babel/preset-env": "^7.18.2", "@babel/preset-env": "^7.25.3",
"@babel/preset-react": "^7.23.3", "@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.17.12", "@babel/preset-typescript": "^7.24.7",
"@electron/rebuild": "^3.6.0", "@electron/rebuild": "^3.6.0",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@types/electron": "^1.6.10", "@types/electron": "^1.6.10",
"@types/node": "^20.11.0", "@types/node": "^22.1.0",
"@types/papaparse": "^5.3.10", "@types/papaparse": "^5.3.14",
"@types/react": "^18.0.12", "@types/react": "^18.3.3",
"@types/semver": "^7.5.6", "@types/semver": "^7.5.8",
"@types/sprintf-js": "^1.1.3", "@types/sprintf-js": "^1.1.4",
"@types/throttle-debounce": "^5.0.1", "@types/throttle-debounce": "^5.0.2",
"@types/tinycolor2": "^1", "@types/tinycolor2": "^1.4.6",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.8",
"@types/webpack-env": "^1.18.3", "@types/webpack-env": "^1.18.5",
"@withfig/autocomplete-types": "^1.30.0", "@withfig/autocomplete-types": "^1.31.0",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"babel-plugin-jsx-control-statements": "^4.1.2", "babel-plugin-jsx-control-statements": "^4.1.2",
"copy-webpack-plugin": "^12.0.0", "copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.0", "css-loader": "^7.1.2",
"electron": "^30.0.8", "electron": "^31.3.1",
"electron-builder": "^24.13.3", "electron-builder": "^24.13.3",
"electron-builder-squirrel-windows": "^24.13.3", "electron-builder-squirrel-windows": "25.0.0-alpha.10",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"http-server": "^14.1.1", "http-server": "^14.1.1",
"less": "^4.1.2", "less": "^4.2.0",
"less-loader": "^12.0.0", "less-loader": "^12.2.0",
"lodash-webpack-plugin": "^0.11.6", "lodash-webpack-plugin": "^0.11.6",
"mini-css-extract-plugin": "^2.6.0", "mini-css-extract-plugin": "^2.9.0",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"react-split-it": "^2.0.0", "react-split-it": "^2.0.0",
"style-loader": "4.0.0", "style-loader": "4.0.0",
"typescript": "^5.0.0", "typescript": "^5.5.4",
"webpack": "^5.73.0", "webpack": "^5.94.0",
"webpack-bundle-analyzer": "^4.10.1", "webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4", "webpack-dev-server": "^5.0.4",
"webpack-merge": "^5.8.0", "webpack-merge": "^5.10.0",
"yaml": "^2.4.0" "yaml": "^2.5.0"
}, },
"scripts": { "scripts": {
"postinstall": "electron-builder install-app-deps" "postinstall": "electron-builder install-app-deps"

View File

@ -170,6 +170,7 @@
--cmdinput-button-bg-color: var(--tab-green); --cmdinput-button-bg-color: var(--tab-green);
--cmdinput-disabled-bg-color: var(--app-text-bg-disabled-color); --cmdinput-disabled-bg-color: var(--app-text-bg-disabled-color);
--cmdinput-history-bg-color: var(--app-bg-color); --cmdinput-history-bg-color: var(--app-bg-color);
--cmdinput-ghost-text-color: rgb(145, 150, 144);
/* screen view color */ /* screen view color */
--screen-view-text-caption-color: rgb(139, 145, 138); --screen-view-text-caption-color: rgb(139, 145, 138);

View File

@ -50,6 +50,7 @@
--modal-header-bottom-border-color: rgba(0, 0, 0, 0.3); --modal-header-bottom-border-color: rgba(0, 0, 0, 0.3);
/* cmd input */ /* cmd input */
--cmdinput-ghost-text-color: rgb(116, 116, 116);
/* scroll colors */ /* scroll colors */
--scrollbar-background-color: var(--app-bg-color); --scrollbar-background-color: var(--app-bg-color);

View File

@ -7,7 +7,6 @@ export { InlineSettingsTextEdit } from "./inlinesettingstextedit";
export { InputDecoration } from "./inputdecoration"; export { InputDecoration } from "./inputdecoration";
export { LinkButton } from "./linkbutton"; export { LinkButton } from "./linkbutton";
export { Markdown } from "./markdown"; export { Markdown } from "./markdown";
export { Markdown2 } from "./markdown2";
export { Modal } from "./modal"; export { Modal } from "./modal";
export { PasswordField } from "./passwordfield"; export { PasswordField } from "./passwordfield";
export { ResizableSidebar } from "./resizablesidebar"; export { ResizableSidebar } from "./resizablesidebar";

View File

@ -46,7 +46,7 @@
padding: 2px 4px 2px 6px; padding: 2px 4px 2px 6px;
} }
pre.codeblock { pre {
background-color: var(--markdown-bg-color); background-color: var(--markdown-bg-color);
margin: 4px 10px; margin: 4px 10px;
padding: 0.4em 0.7em; padding: 0.4em 0.7em;

View File

@ -5,14 +5,14 @@ import * as React from "react";
import * as mobxReact from "mobx-react"; import * as mobxReact from "mobx-react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { CopyButton } from "@/elements";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { GlobalModel } from "@/models"; import * as mobx from "mobx";
import { v4 as uuidv4 } from "uuid"; import { If } from "tsx-control-statements/components";
import "./markdown.less"; import "./markdown.less";
import { boundMethod } from "autobind-decorator";
function LinkRenderer(props: any): any { function Link(props: any): JSX.Element {
let newUrl = "https://extern?" + encodeURIComponent(props.href); let newUrl = "https://extern?" + encodeURIComponent(props.href);
return ( return (
<a href={newUrl} target="_blank" rel={"noopener"}> <a href={newUrl} target="_blank" rel={"noopener"}>
@ -21,99 +21,86 @@ function LinkRenderer(props: any): any {
); );
} }
function HeaderRenderer(props: any, hnum: number): any { function Header(props: any, hnum: number): JSX.Element {
return <div className={clsx("title", "is-" + hnum)}>{props.children}</div>; return <div className={clsx("title", "is-" + hnum)}>{props.children}</div>;
} }
function CodeRenderer(props: any): any { function Code(props: any): JSX.Element {
return <code>{props.children}</code>; return <code>{props.children}</code>;
} }
@mobxReact.observer const CodeBlock = mobxReact.observer(
class CodeBlockMarkdown extends React.Component< (props: { children: React.ReactNode; onClickExecute?: (cmd: string) => void }): JSX.Element => {
{ children: React.ReactNode; codeSelectSelectedIndex?: number; uuid: string }, const copied: OV<boolean> = mobx.observable.box(false, { name: "copied" });
{}
> {
blockIndex: number;
blockRef: React.RefObject<HTMLPreElement>;
constructor(props) { const getTextContent = (children: any) => {
super(props); if (typeof children === "string") {
this.blockRef = React.createRef(); return children;
this.blockIndex = GlobalModel.inputModel.addCodeBlockToCodeSelect(this.blockRef, this.props.uuid); } else if (Array.isArray(children)) {
return children.map(getTextContent).join("");
} else if (children.props && children.props.children) {
return getTextContent(children.props.children);
} }
return "";
};
render() { const handleCopy = async (e: React.MouseEvent) => {
let clickHandler: (e: React.MouseEvent<HTMLElement>, blockIndex: number) => void; let textToCopy = getTextContent(props.children);
let inputModel = GlobalModel.inputModel; textToCopy = textToCopy.replace(/\n$/, ""); // remove trailing newline
clickHandler = (e: React.MouseEvent<HTMLElement>, blockIndex: number) => { await navigator.clipboard.writeText(textToCopy);
const sel = window.getSelection(); copied.set(true);
if (sel?.toString().length == 0) { setTimeout(() => copied.set(false), 2000); // Reset copied state after 2 seconds
inputModel.setCodeSelectSelectedCodeBlock(blockIndex); };
const handleExecute = (e: React.MouseEvent) => {
let textToCopy = getTextContent(props.children);
textToCopy = textToCopy.replace(/\n$/, ""); // remove trailing newline
if (props.onClickExecute) {
props.onClickExecute(textToCopy);
return;
} }
}; };
let selected = this.blockIndex == this.props.codeSelectSelectedIndex;
return ( return (
<pre <pre className="codeblock">
ref={this.blockRef} {props.children}
className={clsx({ selected: selected })} <div className="codeblock-actions">
onClick={(event) => clickHandler(event, this.blockIndex)} <CopyButton className="copy-button" onClick={handleCopy} title="Copy" />
> <If condition={props.onClickExecute}>
{this.props.children} <i className="fa-regular fa-square-terminal" onClick={handleExecute}></i>
</If>
</div>
</pre> </pre>
); );
} }
} );
@mobxReact.observer @mobxReact.observer
class Markdown extends React.Component< class Markdown extends React.Component<
{ text: string; style?: any; extraClassName?: string; codeSelect?: boolean }, {
text: string;
style?: any;
className?: string;
onClickExecute?: (cmd: string) => void;
},
{} {}
> { > {
curUuid: string;
constructor(props) {
super(props);
this.curUuid = uuidv4();
}
@boundMethod
CodeBlockRenderer(props: any, codeSelect: boolean, codeSelectIndex: number, curUuid: string): any {
if (codeSelect) {
return (
<CodeBlockMarkdown codeSelectSelectedIndex={codeSelectIndex} uuid={curUuid}>
{props.children}
</CodeBlockMarkdown>
);
} else {
const clickHandler = (e: React.MouseEvent<HTMLElement>) => {
let blockText = (e.target as HTMLElement).innerText;
if (blockText) {
blockText = blockText.replace(/\n$/, ""); // remove trailing newline
navigator.clipboard.writeText(blockText);
}
};
return <pre onClick={(event) => clickHandler(event)}>{props.children}</pre>;
}
}
render() { render() {
let text = this.props.text; let { text, className, onClickExecute } = this.props;
let codeSelect = this.props.codeSelect;
let curCodeSelectIndex = GlobalModel.inputModel.getCodeSelectSelectedIndex();
let markdownComponents = { let markdownComponents = {
a: LinkRenderer, a: Link,
h1: (props) => HeaderRenderer(props, 1), h1: (props) => <Header {...props} hnum={1} />,
h2: (props) => HeaderRenderer(props, 2), h2: (props) => <Header {...props} hnum={2} />,
h3: (props) => HeaderRenderer(props, 3), h3: (props) => <Header {...props} hnum={3} />,
h4: (props) => HeaderRenderer(props, 4), h4: (props) => <Header {...props} hnum={4} />,
h5: (props) => HeaderRenderer(props, 5), h5: (props) => <Header {...props} hnum={5} />,
h6: (props) => HeaderRenderer(props, 6), h6: (props) => <Header {...props} hnum={6} />,
code: (props) => CodeRenderer(props), code: Code,
pre: (props) => this.CodeBlockRenderer(props, codeSelect, curCodeSelectIndex, this.curUuid), pre: (props) => <CodeBlock {...props} onClickExecute={onClickExecute} />,
}; };
return ( return (
<div className={clsx("markdown content", this.props.extraClassName)} style={this.props.style}> <div className={clsx("markdown content", className)} style={this.props.style}>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}> <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{text} {text}
</ReactMarkdown> </ReactMarkdown>

View File

@ -1,112 +0,0 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobxReact from "mobx-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { CopyButton } from "@/elements";
import { clsx } from "clsx";
import * as mobx from "mobx";
import { If } from "tsx-control-statements/components";
import "./markdown.less";
function Link(props: any): JSX.Element {
let newUrl = "https://extern?" + encodeURIComponent(props.href);
return (
<a href={newUrl} target="_blank" rel={"noopener"}>
{props.children}
</a>
);
}
function Header(props: any, hnum: number): JSX.Element {
return <div className={clsx("title", "is-" + hnum)}>{props.children}</div>;
}
function Code(props: any): JSX.Element {
return <code>{props.children}</code>;
}
const CodeBlock = mobxReact.observer(
(props: { children: React.ReactNode; onClickExecute?: (cmd: string) => void }): JSX.Element => {
const copied: OV<boolean> = mobx.observable.box(false, { name: "copied" });
const getTextContent = (children: any) => {
if (typeof children === "string") {
return children;
} else if (Array.isArray(children)) {
return children.map(getTextContent).join("");
} else if (children.props && children.props.children) {
return getTextContent(children.props.children);
}
return "";
};
const handleCopy = async (e: React.MouseEvent) => {
let textToCopy = getTextContent(props.children);
textToCopy = textToCopy.replace(/\n$/, ""); // remove trailing newline
await navigator.clipboard.writeText(textToCopy);
copied.set(true);
setTimeout(() => copied.set(false), 2000); // Reset copied state after 2 seconds
};
const handleExecute = (e: React.MouseEvent) => {
let textToCopy = getTextContent(props.children);
textToCopy = textToCopy.replace(/\n$/, ""); // remove trailing newline
if (props.onClickExecute) {
props.onClickExecute(textToCopy);
return;
}
};
return (
<pre className="codeblock">
{props.children}
<div className="codeblock-actions">
<CopyButton className="copy-button" onClick={handleCopy} title="Copy" />
<If condition={props.onClickExecute}>
<i className="fa-regular fa-square-terminal" onClick={handleExecute}></i>
</If>
</div>
</pre>
);
}
);
@mobxReact.observer
class Markdown2 extends React.Component<
{
text: string;
style?: any;
className?: string;
onClickExecute?: (cmd: string) => void;
},
{}
> {
render() {
let { text, className, onClickExecute } = this.props;
let markdownComponents = {
a: Link,
h1: (props) => <Header {...props} hnum={1} />,
h2: (props) => <Header {...props} hnum={2} />,
h3: (props) => <Header {...props} hnum={3} />,
h4: (props) => <Header {...props} hnum={4} />,
h5: (props) => <Header {...props} hnum={5} />,
h6: (props) => <Header {...props} hnum={6} />,
code: Code,
pre: (props) => <CodeBlock {...props} onClickExecute={onClickExecute} />,
};
return (
<div className={clsx("markdown content", className)} style={this.props.style}>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{text}
</ReactMarkdown>
</div>
);
}
}
export { Markdown2 };

View File

@ -178,7 +178,7 @@ class LineActions extends React.Component<{ screen: LineContainerType; line: Lin
<div className="line-actions"> <div className="line-actions">
<Choose> <Choose>
<When condition={containerType == appconst.LineContainer_Main}> <When condition={containerType == appconst.LineContainer_Main}>
<div key="chat" title="Restart Command" className="line-icon" onClick={this.clickChat}> <div key="chat" title="Ask Wave AI" className="line-icon" onClick={this.clickChat}>
<i className="fa-sharp fa-regular fa-sparkles fa-fw" /> <i className="fa-sharp fa-regular fa-sparkles fa-fw" />
</div> </div>
<div key="restart" title="Restart Command" className="line-icon" onClick={this.clickRestart}> <div key="restart" title="Restart Command" className="line-icon" onClick={this.clickRestart}>

View File

@ -7,7 +7,7 @@ import * as mobx from "mobx";
import { GlobalModel } from "@/models"; import { GlobalModel } from "@/models";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { For, If } from "tsx-control-statements/components"; import { For, If } from "tsx-control-statements/components";
import { Markdown2, TypingIndicator } from "@/elements"; import { Markdown, TypingIndicator } from "@/elements";
import type { OverlayScrollbars } from "overlayscrollbars"; import type { OverlayScrollbars } from "overlayscrollbars";
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react"; import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import tinycolor from "tinycolor2"; import tinycolor from "tinycolor2";
@ -40,9 +40,6 @@ class ChatKeyBindings extends React.Component<{ component: ChatSidebar }, {}> {
keybindManager.registerKeybinding("pane", "aichat", "generic:selectBelow", (waveEvent) => { keybindManager.registerKeybinding("pane", "aichat", "generic:selectBelow", (waveEvent) => {
return component.onArrowDownPressed(); return component.onArrowDownPressed();
}); });
keybindManager.registerKeybinding("pane", "aichat", "aichat:setCmdInputValue", (waveEvent) => {
return component.onSetCmdInputValue();
});
} }
componentWillUnmount(): void { componentWillUnmount(): void {
@ -75,7 +72,7 @@ class ChatItem extends React.Component<
<div className="chat-msg-header"> <div className="chat-msg-header">
<i className="fa-sharp fa-solid fa-user"></i> <i className="fa-sharp fa-solid fa-user"></i>
</div> </div>
<Markdown2 className="msg-text" text={chatItem.userquery} /> <Markdown className="msg-text" text={chatItem.userquery} />
</> </>
); );
if (isassistantresponse) { if (isassistantresponse) {
@ -97,7 +94,7 @@ class ChatItem extends React.Component<
<div className="chat-msg-header"> <div className="chat-msg-header">
<i className="fa-sharp fa-solid fa-sparkles"></i> <i className="fa-sharp fa-solid fa-sparkles"></i>
</div> </div>
<Markdown2 text={assistantresponse.message} onClickExecute={onSetCmdInputValue} /> <Markdown text={assistantresponse.message} onClickExecute={onSetCmdInputValue} />
</> </>
); );
} }
@ -195,7 +192,6 @@ class ChatSidebar extends React.Component<{}, {}> {
value: OV<string> = mobx.observable.box("", { deep: false, name: "value" }); value: OV<string> = mobx.observable.box("", { deep: false, name: "value" });
osInstance: OverlayScrollbars; osInstance: OverlayScrollbars;
termFontSize: number = 14; termFontSize: number = 14;
blockIndex: number;
disposeReaction: () => void; disposeReaction: () => void;
constructor(props) { constructor(props) {
@ -227,32 +223,21 @@ class ChatSidebar extends React.Component<{}, {}> {
} }
); );
if (this.sidebarRef.current) { if (this.sidebarRef.current) {
this.sidebarRef.current.addEventListener("click", this.handleSidebarClick); this.sidebarRef.current.addEventListener("click", this.onSidebarClick);
} }
document.addEventListener("click", this.handleClickOutside);
this.requestChatUpdate(); this.requestChatUpdate();
} }
componentWillUnmount() { componentWillUnmount() {
if (this.sidebarRef.current) { if (this.sidebarRef.current) {
this.sidebarRef.current.removeEventListener("click", this.handleSidebarClick); this.sidebarRef.current.removeEventListener("click", this.onSidebarClick);
} }
document.removeEventListener("click", this.handleClickOutside);
GlobalModel.sidebarchatModel.resetFocus(); GlobalModel.sidebarchatModel.resetFocus();
if (this.disposeReaction) { if (this.disposeReaction) {
this.disposeReaction(); this.disposeReaction();
} }
} }
@mobx.action.bound
handleClickOutside(e: MouseEvent) {
const sidebar = this.sidebarRef.current;
if (sidebar && !sidebar.contains(e.target as Node)) {
GlobalModel.sidebarchatModel.resetFocus();
GlobalModel.inputModel.giveFocus();
}
}
requestChatUpdate() { requestChatUpdate() {
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice(); const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
if (chatMessageItems == null || chatMessageItems.length === 0) { if (chatMessageItems == null || chatMessageItems.length === 0) {
@ -309,10 +294,16 @@ class ChatSidebar extends React.Component<{}, {}> {
@mobx.action.bound @mobx.action.bound
onEnterKeyPressed() { onEnterKeyPressed() {
const blockIndex = GlobalModel.sidebarchatModel.getSelectedCodeBlockIndex();
if (blockIndex != null) {
this.onSetCmdInputValue();
return true;
}
const messageStr = this.value.get(); const messageStr = this.value.get();
this.submitChatMessage(messageStr); this.submitChatMessage(messageStr);
this.value.set(""); this.value.set("");
GlobalModel.sidebarchatModel.resetCmdAndOutput(); GlobalModel.sidebarchatModel.resetCmdAndOutput();
return true;
} }
@mobx.action.bound @mobx.action.bound
@ -324,6 +315,11 @@ class ChatSidebar extends React.Component<{}, {}> {
currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end"); currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end");
} }
@mobx.action.bound
onBlur() {
GlobalModel.sidebarchatModel.resetFocus();
}
updatePreTagOutline(clickedPre?) { updatePreTagOutline(clickedPre?) {
const pres = this.chatWindowRef.current?.querySelectorAll("pre"); const pres = this.chatWindowRef.current?.querySelectorAll("pre");
if (pres == null) { if (pres == null) {
@ -340,7 +336,7 @@ class ChatSidebar extends React.Component<{}, {}> {
} }
@mobx.action.bound @mobx.action.bound
handleSidebarClick(event) { onSidebarClick(event) {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if ( if (
target.closest(".copy-button") || target.closest(".copy-button") ||
@ -526,7 +522,6 @@ class ChatSidebar extends React.Component<{}, {}> {
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice(); const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
const renderAIChatKeybindings = GlobalModel.sidebarchatModel.hasFocus(); const renderAIChatKeybindings = GlobalModel.sidebarchatModel.hasFocus();
const textAreaValue = this.value.get(); const textAreaValue = this.value.get();
return ( return (
<div ref={this.sidebarRef} className="sidebarchat"> <div ref={this.sidebarRef} className="sidebarchat">
<If condition={renderAIChatKeybindings}> <If condition={renderAIChatKeybindings}>
@ -546,6 +541,7 @@ class ChatSidebar extends React.Component<{}, {}> {
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
className="sidebarchat-input chat-textarea" className="sidebarchat-input chat-textarea"
onBlur={this.onBlur}
onFocus={this.onTextAreaFocus} onFocus={this.onTextAreaFocus}
onMouseDown={this.onTextAreaMouseDown} // When the user clicks on the textarea onMouseDown={this.onTextAreaMouseDown} // When the user clicks on the textarea
onChange={this.onTextAreaChange} onChange={this.onTextAreaChange}

View File

@ -1,72 +0,0 @@
.cmd-aichat {
padding-bottom: 0 !important;
.chat-window {
display: flex;
overflow-y: auto;
margin-bottom: 5px;
flex-direction: column;
height: 100%;
// This is the filler that will push the chat messages to the bottom until the chat window is full
.filler {
flex: 1 1 auto;
}
}
.chat-input {
padding: 0.5em 0.5em 0.5em 0.5em;
flex: 0 0 auto;
.chat-textarea {
color: var(--app-text-primary-color);
background-color: var(--cmdinput-textarea-bg);
resize: none;
width: 100%;
border: transparent;
outline: none;
overflow: auto;
overflow-wrap: anywhere;
font-family: var(--termfontfamily);
font-weight: normal;
line-height: var(--termlineheight);
}
}
.chat-msg {
margin-top: calc(var(--termpad) * 2);
margin-bottom: calc(var(--termpad) * 2);
.chat-msg-header {
display: flex;
margin-bottom: 2px;
i {
margin-right: 0.5em;
}
.chat-username {
font-weight: bold;
margin-right: 5px;
}
}
}
.chat-msg-assistant {
color: var(--app-text-color);
}
.chat-msg-user {
.msg-text {
font-family: var(--markdown-font);
font-size: 14px;
white-space: pre-wrap;
}
}
.chat-msg-error {
color: var(--cmdinput-text-error);
font-family: var(--markdown-font);
font-size: 14px;
}
}

View File

@ -1,282 +0,0 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { GlobalModel } from "@/models";
import { boundMethod } from "autobind-decorator";
import { If, For } from "tsx-control-statements/components";
import { Markdown } from "@/elements";
import { AuxiliaryCmdView } from "./auxview";
import * as appconst from "@/app/appconst";
import "./aichat.less";
class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> {
componentDidMount(): void {
const AIChatObject = this.props.AIChatObject;
const keybindManager = GlobalModel.keybindManager;
const inputModel = GlobalModel.inputModel;
keybindManager.registerKeybinding("pane", "aichat", "generic:confirm", (waveEvent) => {
AIChatObject.onEnterKeyPressed();
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "generic:expandTextInput", (waveEvent) => {
AIChatObject.onExpandInputPressed();
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "generic:cancel", (waveEvent) => {
inputModel.closeAuxView();
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "aichat:clearHistory", (waveEvent) => {
inputModel.clearAIAssistantChat();
return true;
});
keybindManager.registerKeybinding("pane", "aichat", "generic:selectAbove", (waveEvent) => {
return AIChatObject.onArrowUpPressed();
});
keybindManager.registerKeybinding("pane", "aichat", "generic:selectBelow", (waveEvent) => {
return AIChatObject.onArrowDownPressed();
});
}
componentWillUnmount(): void {
GlobalModel.keybindManager.unregisterDomain("aichat");
}
render() {
return null;
}
}
@mobxReact.observer
class AIChat extends React.Component<{}, {}> {
chatListKeyCount: number = 0;
chatWindowScrollRef: React.RefObject<HTMLDivElement>;
textAreaRef: React.RefObject<HTMLTextAreaElement>;
termFontSize: number = 14;
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) {
this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight;
}
if (this.textAreaRef.current != null) {
this.textAreaRef.current.focus();
inputModel.setCmdInfoChatRefs(this.textAreaRef, this.chatWindowScrollRef);
}
this.requestChatUpdate();
this.onTextAreaChange(null);
}
componentDidUpdate() {
if (this.chatWindowScrollRef?.current != null) {
this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight;
}
}
requestChatUpdate() {
this.submitChatMessage("");
}
submitChatMessage(messageStr: string) {
const curLine = GlobalModel.inputModel.curLine;
const prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
prtn.then((rtn) => {
if (!rtn.success) {
console.log("submit chat command error: " + rtn.error);
}
}).catch((_) => {});
}
getLinePos(elem: any): { numLines: number; linePos: number } {
const numLines = elem.value.split("\n").length;
const linePos = elem.value.substr(0, elem.selectionStart).split("\n").length;
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;
const textAreaLineHeight = this.termFontSize * 1.5;
const textAreaMinHeight = textAreaLineHeight;
const textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines;
// Get the height of the wrapped text area content. Courtesy of https://stackoverflow.com/questions/995168/textarea-to-resize-based-on-content-length
this.textAreaRef.current.style.height = "1px";
const scrollHeight: number = this.textAreaRef.current.scrollHeight;
// Set the new height of the text area, bounded by the min and max height.
const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight);
this.textAreaRef.current.style.height = newHeight + "px";
}
onTextAreaInput(e: any) {
GlobalModel.inputModel.codeSelectDeselectAll();
}
onEnterKeyPressed() {
const inputModel = GlobalModel.inputModel;
const currentRef = this.textAreaRef.current;
if (currentRef == null) {
return;
}
if (inputModel.getCodeSelectSelectedIndex() == -1) {
const messageStr = currentRef.value;
this.submitChatMessage(messageStr);
currentRef.value = "";
} else {
mobx.action(() => {
inputModel.grabCodeSelectSelection();
inputModel.setAuxViewFocus(false);
})();
}
}
onExpandInputPressed() {
const currentRef = this.textAreaRef.current;
if (currentRef == null) {
return;
}
currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end");
GlobalModel.inputModel.codeSelectDeselectAll();
}
onArrowUpPressed(): boolean {
const currentRef = this.textAreaRef.current;
if (currentRef == null) {
return false;
}
if (this.getLinePos(currentRef).linePos > 1) {
// normal up arrow
GlobalModel.inputModel.codeSelectDeselectAll();
return false;
}
GlobalModel.inputModel.codeSelectSelectNextOldestCodeBlock();
return true;
}
onArrowDownPressed(): boolean {
const currentRef = this.textAreaRef.current;
const inputModel = GlobalModel.inputModel;
if (currentRef == null) {
return false;
}
if (inputModel.getCodeSelectSelectedIndex() == inputModel.codeSelectBottom) {
GlobalModel.inputModel.codeSelectDeselectAll();
return false;
}
inputModel.codeSelectSelectNextNewestCodeBlock();
return true;
}
@boundMethod
onKeyDown(e: any) {}
renderError(err: string): any {
return <div className="chat-msg-error">{err}</div>;
}
renderChatMessage(chatItem: OpenAICmdInfoChatMessageType): any {
const curKey = "chatmsg-" + this.chatListKeyCount;
this.chatListKeyCount++;
const senderClassName = chatItem.isassistantresponse ? "chat-msg-assistant" : "chat-msg-user";
const msgClassName = "chat-msg " + senderClassName;
let innerHTML: React.JSX.Element = (
<span>
<div className="chat-msg-header">
<i className="fa-sharp fa-solid fa-user"></i>
<div className="chat-username">You</div>
</div>
<p className="msg-text">{chatItem.userquery}</p>
</span>
);
if (chatItem.isassistantresponse) {
if (chatItem.assistantresponse.error != null && chatItem.assistantresponse.error != "") {
innerHTML = this.renderError(chatItem.assistantresponse.error);
} else {
innerHTML = (
<span>
<div className="chat-msg-header">
<i className="fa-sharp fa-solid fa-sparkles"></i>
<div className="chat-username">AI Assistant</div>
</div>
<Markdown text={chatItem.assistantresponse.message} codeSelect />
</span>
);
}
}
return (
<div className={msgClassName} key={curKey}>
{innerHTML}
</div>
);
}
render() {
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
const chitem: OpenAICmdInfoChatMessageType = null;
const renderKeybindings = GlobalModel.inputModel.shouldRenderAuxViewKeybindings(appconst.InputAuxView_AIChat);
return (
<AuxiliaryCmdView
title="Wave AI"
className="cmd-aichat"
onClose={() => GlobalModel.inputModel.closeAuxView()}
iconClass="fa-sharp fa-solid fa-sparkles"
>
<If condition={renderKeybindings}>
<AIChatKeybindings AIChatObject={this}></AIChatKeybindings>
</If>
<div className="chat-window" ref={this.chatWindowScrollRef}>
<div className="filler"></div>
<For each="chitem" index="idx" of={chatMessageItems}>
{this.renderChatMessage(chitem)}
</For>
</div>
<div className="chat-input">
<textarea
key="main"
ref={this.textAreaRef}
autoComplete="off"
autoCorrect="off"
id="chat-cmd-input"
onFocus={this.onTextAreaFocused}
onBlur={this.onTextAreaBlur}
onChange={this.onTextAreaChange}
onInput={this.onTextAreaInput}
onKeyDown={this.onKeyDown}
style={{ fontSize: this.termFontSize }}
className="chat-textarea"
placeholder="Send a Message..."
></textarea>
</div>
</AuxiliaryCmdView>
);
}
}
export { AIChat };

View File

@ -131,7 +131,7 @@
} }
.textarea-ghost { .textarea-ghost {
color: var(--app-text-secondary-color); color: var(--cmdinput-ghost-text-color);
z-index: 1; z-index: 1;
} }

View File

@ -16,7 +16,6 @@ import { InfoMsg } from "./infomsg";
import { HistoryInfo } from "./historyinfo"; import { HistoryInfo } from "./historyinfo";
import { Prompt } from "@/common/prompt/prompt"; import { Prompt } from "@/common/prompt/prompt";
import { CenteredIcon, RotateIcon } from "@/common/icons/icons"; import { CenteredIcon, RotateIcon } from "@/common/icons/icons";
import { AIChat } from "./aichat";
import * as util from "@/util/util"; import * as util from "@/util/util";
import * as appconst from "@/app/appconst"; import * as appconst from "@/app/appconst";
import { AutocompleteSuggestionView } from "./suggestionview"; import { AutocompleteSuggestionView } from "./suggestionview";
@ -29,6 +28,7 @@ dayjs.extend(localizedFormat);
class CmdInput extends React.Component<{}, {}> { class CmdInput extends React.Component<{}, {}> {
cmdInputRef: React.RefObject<any> = React.createRef(); cmdInputRef: React.RefObject<any> = React.createRef();
promptRef: React.RefObject<any> = React.createRef(); promptRef: React.RefObject<any> = React.createRef();
sbcTimeoutId: NodeJS.Timeout = null;
constructor(props) { constructor(props) {
super(props); super(props);
@ -57,6 +57,13 @@ class CmdInput extends React.Component<{}, {}> {
this.updateCmdInputHeight(); this.updateCmdInputHeight();
} }
componentWillUnmount() {
if (this.sbcTimeoutId) {
clearTimeout(this.sbcTimeoutId);
this.sbcTimeoutId = null;
}
}
@boundMethod @boundMethod
handleInnerHeightUpdate(): void { handleInnerHeightUpdate(): void {
this.updateCmdInputHeight(); this.updateCmdInputHeight();
@ -96,9 +103,15 @@ class CmdInput extends React.Component<{}, {}> {
@mobx.action.bound @mobx.action.bound
clickAIChatAction(e: any): void { clickAIChatAction(e: any): void {
const rightSidebarModel = GlobalModel.rightSidebarModel; const isCollapsed = GlobalModel.rightSidebarModel.getCollapsed();
const width = rightSidebarModel.getWidth(true); GlobalModel.rightSidebarModel.setCollapsed(!isCollapsed);
rightSidebarModel.saveState(width, false); if (isCollapsed) {
this.sbcTimeoutId = setTimeout(() => {
GlobalModel.inputModel.setChatSidebarFocus();
}, 100);
} else {
GlobalModel.inputModel.setChatSidebarFocus(false);
}
} }
@boundMethod @boundMethod
@ -187,10 +200,6 @@ class CmdInput extends React.Component<{}, {}> {
<div className="cmd-input-grow-spacer"></div> <div className="cmd-input-grow-spacer"></div>
<HistoryInfo /> <HistoryInfo />
</When> </When>
<When condition={openView === appconst.InputAuxView_AIChat}>
<div className="cmd-input-grow-spacer"></div>
<AIChat />
</When>
<When condition={openView === appconst.InputAuxView_Info}> <When condition={openView === appconst.InputAuxView_Info}>
<InfoMsg key="infomsg" /> <InfoMsg key="infomsg" />
</When> </When>

View File

@ -187,9 +187,7 @@ export class AutocompleteModel {
* @see getPrimarySuggestionIndex * @see getPrimarySuggestionIndex
*/ */
getPrimarySuggestionCompletion(): string { getPrimarySuggestionCompletion(): string {
if (!this.isEnabled) { if (!this.isEnabled || !this.globalModel.inputModel.curLine) return null;
return null;
}
const suggestionIndex = this.getPrimarySuggestionIndex(); const suggestionIndex = this.getPrimarySuggestionIndex();
const retVal = this.getSuggestionCompletion(suggestionIndex); const retVal = this.getSuggestionCompletion(suggestionIndex);
if (retVal) { if (retVal) {

View File

@ -234,10 +234,21 @@ class Screen {
} }
refocusLine(sdata: ScreenDataType, oldFocusType: string, oldSelectedLine: number): void { refocusLine(sdata: ScreenDataType, oldFocusType: string, oldSelectedLine: number): void {
if (this.globalModel.activeMainView.get() != "session") {
return;
}
let isCmdFocus = sdata.focustype == "cmd"; let isCmdFocus = sdata.focustype == "cmd";
if (!isCmdFocus) { if (!isCmdFocus) {
return; return;
} }
if (document.activeElement != null) {
if (document.activeElement.nodeName == "INPUT" || document.activeElement.nodeName == "TEXTAREA") {
return;
}
}
if (this.globalModel.modalsModel.hasOpenModals()) {
return;
}
let curLineFocus = this.globalModel.getFocusedLine(); let curLineFocus = this.globalModel.getFocusedLine();
let sline: LineType = null; let sline: LineType = null;
if (sdata.selectedline != 0) { if (sdata.selectedline != 0) {

View File

@ -6,7 +6,6 @@ import * as mobx from "mobx";
import * as mobxReact from "mobx-react"; import * as mobxReact from "mobx-react";
import { sprintf } from "sprintf-js"; import { sprintf } from "sprintf-js";
import { Markdown } from "@/elements"; import { Markdown } from "@/elements";
import { GlobalModel } from "@/models/global";
import "./markdown.less"; import "./markdown.less";

View File

@ -18,6 +18,7 @@ import (
"os/exec" "os/exec"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"syscall" "syscall"
"unicode/utf8" "unicode/utf8"
@ -673,3 +674,26 @@ func GetFirstLine(s string) string {
} }
return s[0:idx] return s[0:idx]
} }
func TrimQuotes(s string) (string, bool) {
if len(s) > 2 && s[0] == '"' {
trimmed, err := strconv.Unquote(s)
if err != nil {
return s, false
}
return trimmed, true
}
return s, false
}
func TryTrimQuotes(s string) string {
trimmed, _ := TrimQuotes(s)
return trimmed
}
func ReplaceQuotes(s string, shouldReplace bool) string {
if shouldReplace {
return strconv.Quote(s)
}
return s
}

View File

@ -132,7 +132,7 @@ var SetVarScopes = []SetVarScope{
{ScopeName: "remote", VarNames: []string{}}, {ScopeName: "remote", VarNames: []string{}},
} }
var userHostRe = regexp.MustCompile(`^(sudo@)?([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$`) var userHostRe = regexp.MustCompile(`^(sudo@)?([a-zA-Z0-9][a-zA-Z0-9._@:\\-]*@)?([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$`)
var remoteAliasRe = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9._-]*$") var remoteAliasRe = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9._-]*$")
var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$") var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$")
var rendererRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_.:-]*$") var rendererRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_.:-]*$")
@ -769,7 +769,7 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.U
} else { } else {
return nil, fmt.Errorf("error in Eval Meta Command: %w", rtnErr) return nil, fmt.Errorf("error in Eval Meta Command: %w", rtnErr)
} }
if !resolveBool(pk.Kwargs[KwArgNoHist], false) { if !resolveBool(pk.Kwargs[KwArgNoHist], false) && pk.EphemeralOpts == nil {
// TODO should this be "pk" or "newPk" (2nd arg) // TODO should this be "pk" or "newPk" (2nd arg)
err := addToHistory(ctx, pk, historyContext, (newPk.MetaCmd != "run"), (rtnErr != nil)) err := addToHistory(ctx, pk, historyContext, (newPk.MetaCmd != "run"), (rtnErr != nil))
if err != nil { if err != nil {

View File

@ -23,6 +23,7 @@ import (
"github.com/kevinburke/ssh_config" "github.com/kevinburke/ssh_config"
"github.com/wavetermdev/waveterm/waveshell/pkg/base" "github.com/wavetermdev/waveterm/waveshell/pkg/base"
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus" "github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore" "github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
"github.com/wavetermdev/waveterm/wavesrv/pkg/userinput" "github.com/wavetermdev/waveterm/wavesrv/pkg/userinput"
@ -110,10 +111,6 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
} }
unencryptedPrivateKey, err := ssh.ParseRawPrivateKey(privateKey) unencryptedPrivateKey, err := ssh.ParseRawPrivateKey(privateKey)
if _, ok := err.(*ssh.PassphraseMissingError); !ok {
// skip this key and try with the next
return createDummySigner()
}
if err == nil { if err == nil {
signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey) signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey)
if err == nil { if err == nil {
@ -124,7 +121,10 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
} }
return []ssh.Signer{signer}, err return []ssh.Signer{signer}, err
} }
}
if _, ok := err.(*ssh.PassphraseMissingError); !ok {
// skip this key and try with the next
return createDummySigner()
} }
signer, err := ssh.ParsePrivateKey(privateKey) signer, err := ssh.ParsePrivateKey(privateKey)
@ -137,10 +137,6 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
} }
unencryptedPrivateKey, err = ssh.ParseRawPrivateKeyWithPassphrase(privateKey, []byte(passphrase)) unencryptedPrivateKey, err = ssh.ParseRawPrivateKeyWithPassphrase(privateKey, []byte(passphrase))
if err != x509.IncorrectPasswordError && err.Error() != "bcrypt_pbkdf: empty password" {
// skip this key and try with the next
return createDummySigner()
}
if err == nil { if err == nil {
signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey) signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey)
if err == nil { if err == nil {
@ -152,18 +148,10 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
return []ssh.Signer{signer}, err return []ssh.Signer{signer}, err
} }
} }
/*
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(passphrase))
if err == nil {
log.Printf("with passphrase %v\n", signer.PublicKey().Marshal())
return []ssh.Signer{signer}, err
}
if err != x509.IncorrectPasswordError && err.Error() != "bcrypt_pbkdf: empty password" { if err != x509.IncorrectPasswordError && err.Error() != "bcrypt_pbkdf: empty password" {
// skip this key and try with the next // skip this key and try with the next
return createDummySigner() return createDummySigner()
} }
*/
// batch mode deactivates user input // batch mode deactivates user input
if sshKeywords.BatchMode { if sshKeywords.BatchMode {
@ -201,16 +189,6 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
}) })
} }
return []ssh.Signer{signer}, err return []ssh.Signer{signer}, err
/*
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(response.Text))
if err != nil {
// skip this key and try with the next
return createDummySigner()
}
log.Printf("with passphrase %v\n", signer.PublicKey().Marshal())
return []ssh.Signer{signer}, err
*/
} }
} }
@ -809,9 +787,9 @@ func findSshConfigKeywords(hostPattern string, sshAuthSock string) (*SshKeywords
return nil, err return nil, err
} }
if identityAgentRaw == "" { if identityAgentRaw == "" {
sshKeywords.IdentityAgent = sshAuthSock sshKeywords.IdentityAgent = base.ExpandHomeDir(utilfn.TryTrimQuotes(strings.TrimSpace(string(sshAuthSock))))
} else { } else {
sshKeywords.IdentityAgent = identityAgentRaw sshKeywords.IdentityAgent = base.ExpandHomeDir(utilfn.TryTrimQuotes(identityAgentRaw))
} }
return sshKeywords, nil return sshKeywords, nil

2628
yarn.lock

File diff suppressed because it is too large Load Diff