diff --git a/vscode/.gitignore b/vscode/.gitignore index 7dd7f6fd..e22c86bc 100644 --- a/vscode/.gitignore +++ b/vscode/.gitignore @@ -33,5 +33,4 @@ user_config.json #Ignore lock files package-lock.json -yarn.lock - +yarn.lock \ No newline at end of file diff --git a/vscode/README.md b/vscode/README.md index 4e0cc076..284cc27b 100644 --- a/vscode/README.md +++ b/vscode/README.md @@ -1,8 +1,3 @@ -

- - -

-

CommandDash (Beta)

Your Flutter AI Autopilot with Gemini Code & Vision

@@ -11,23 +6,25 @@ ----------------- -CommandDash is a command-based coding assistant. It has built in agents that not only help you write code, but also auto run and debug it - performing various Flutter development tasks for you. +CommandDash is a command-based coding assistant. It provides built in agents that that can automate various Flutter development tasks for you. ##### ✨ Powered by Gemini ##### 🤝 Dart Analyzer Inside ##### 👨🏼‍💻 For and by Flutter Engineers -Currently in Beta, CommandDash is being built in open-sourced with the community. +Currently in Beta, CommandDash is being built in [open-sourced](https://github.com/CommandDash/commanddash) with the community. ----------------- ## Getting Started -##### 1. Create Free Gemini API Key +##### 1. Create Gemini API Key Visit [Makersuite by Google](https://makersuite.google.com/) and create your free API Key. + +*Note: Gemini offers both free and paid plans.* ##### 2. Add the key in CommandDash Panel -Paste your API key in the input field in CommandDash Panel. +Paste your API key in the input field in Dash Panel. -That's it. You're ready to use Dash AI. ✅ +That's it. You're ready to use CommandDash. ✅ ## Features @@ -36,71 +33,35 @@ That's it. You're ready to use Dash AI. ✅ Multi code chat with VSCode inside Gemini

-Attach multiple code snippets from different files in your inline chat. With full-context passed to Gemini, receive accurate responses and amend code across multiple files. +Select and attach multiple code snippets from different files in your inline chat using **"Attach Snippet to Dash"** from the right-click menu. + +🤝 With full-context passed to Gemini, receive accurate responses and update code across multiple files. ### 🚀 @Agents and /Commands Use built in agents and commands to autopilot different kinds of tasks. -Currently, we support: - -#### 1. `@workspace` agent. -Ask anything related to Flutter or Dart and get instant answers. Query your workspace using `@workspace` command. - -#### 2. `/refactor` command. -

-Multi code chat with VSCode inside Gemini -

- -More, coming very soon. +Currently, we offer following agents and commands: -### ✨ Generate Inline Code +#### 1. `@workspace` +Directly query across your workspace and find relevant files related to a feature. Leverage this command to build an understand of the codebase you are working with. -#### 1. **Code Block Completion** +#### 2. `@test` +Generate unit, widget and integration tests with full-context for your Flutter/Dart project. -Complete methods, classes or any other code blocks by running `Inline Code Generation` from the menu or via `cmd+shift+R`. +✅ Also, attach previously existing tests as references to help Gemini learn your testing style and choice of libraries. -Specify details with comments for better accuracy. For example, +#### 3. `@flutter` +✨ Use `/doc` command answer your Flutter/Dart questions from trusted sources including official docs. -```dart -class Cart { - // Properties - List items = []; +#### 4. `/refactor` and `/document` - void addItem(Item item) { - items.add(item); - } +Modify your existing code with instructions and apply the changes. - void removeItem(Item item) { - items.remove(item); - } - // get total price method - **[cmd+shift+R]** -} -``` - -completes the next lines with: -``` dart - double getTotalPrice() { - double total = 0; - for (Item item in items) { - total += item.price; - } - return total; - } -``` - -#### 2. **Widget from Image or Description** - -Use Gemini's multimodal capabilities to create widget from a image with added description. - -Command: `Dash AI Create: Widget from Image or Description` - -#### 3. **Code from Blueprint** - -Get complete code from a blueprint of a class or function with the behaviour of functions, state management and architecture of your choice. +

+Refactoring code with CommandDash +

-Command: `Dash AI Create: Code from Blueprint` ## FAQs @@ -109,14 +70,14 @@ Command: `Dash AI Create: Code from Blueprint` 2. **Do I need to pay to use CommandDash?** -- Gemini PRO is currently in early access and is completely free to use for upto 60 requests for minute. Please check the [pricing](https://ai.google.dev/pricing) here. +- Gemini PRO offers both free and paid plans. Please check the [pricing](https://ai.google.dev/pricing) here. -3. **I am an Android Studio user. Can I use Dash AI?** +3. **I am an Android Studio user. Can I use CommandDash?** - We are coming soon for IntelliJ-based IDEs. *🤫 Secret: most of our core logic is written in Dart, allowing us to ship on any platform very very fast!* ## Contributing -A coding assistant for all is best built when all of us contribute. You can make contributions to the VSCODE or IntelliJ extension or also to [CommandDash CLI](https://github.com/Welltested-AI/commanddash) shared between the extensions. +A coding assistant for all is best built when all of us contribute. You can make contributions to the VSCODE or IntelliJ extension or also to [agents engine](https://github.com/CommandDash/packages) shared between the extensions. ### Ways to contribute @@ -134,4 +95,4 @@ Connect with like minded people building with Flutter and using AI to do so, eve ## License -Dash AI is released under the Apache License Version 2.0. See the [LICENSE](LICENSE) file for more information. \ No newline at end of file +CommandDash is released under the Apache License Version 2.0. See the [LICENSE](LICENSE) file for more information. \ No newline at end of file diff --git a/vscode/media/agent-provider/agent-provider.js b/vscode/media/agent-provider/agent-provider.js new file mode 100644 index 00000000..8ec68206 --- /dev/null +++ b/vscode/media/agent-provider/agent-provider.js @@ -0,0 +1,15 @@ +class AgentProvider { + constructor(json) { + this.json = json; + } + getInputs(inputString) { + for (const item of this.json) { + for (const command of item.supported_commands) { + if (command.slug === inputString) { + return JSON.parse(JSON.stringify(command)); + } + } + } + return []; + } +} \ No newline at end of file diff --git a/vscode/media/agent-ui-builder/agent-ui-builder.js b/vscode/media/agent-ui-builder/agent-ui-builder.js new file mode 100644 index 00000000..f57ba1cc --- /dev/null +++ b/vscode/media/agent-ui-builder/agent-ui-builder.js @@ -0,0 +1,118 @@ +class AgentUIBuilder { + constructor(ref) { + this.ref = ref; + + this.onStringInput = this.onStringInput.bind(this); + this.buildAgentUI = this.buildAgentUI.bind(this); + + this.container = document.createElement("div"); + + this.codeInputIds = []; + } + + buildAgentUI() { + const { text_field_layout, registered_inputs, slug } = agentInputsJson[0]; + let textHtml = text_field_layout; + registered_inputs.forEach(input => { + const inputElement = this.createInputElement(input); + this.container.appendChild(inputElement); + textHtml = textHtml.replace(`<${input.id}>`, inputElement.outerHTML); + }); + + this.container.innerHTML = `${slug} ${textHtml}`; + this.ref.appendChild(this.container); + this.registerCodeInputListener(); + } + + createInputElement(input) { + const { id, display_text, type, optional } = input; + const _optional = optional ? "(O)" : ""; + if (type === "string_input") { + const inputContainer = document.createElement("span"); + const inputSpan = document.createElement("span"); + inputContainer.innerHTML = `${_optional} ${display_text}`; + inputContainer.classList.add("inline-block"); + + inputSpan.id = id; + inputSpan.contentEditable = true; + inputSpan.tabIndex = 0; + inputSpan.classList.add("px-2", "inline-block", "rounded-tr-[4px]", "rounded-br-[4px]", "string_input", id, "mb-1", "ml-[1px]", "mr-[1px]"); + inputSpan.textContent = '\u200B'; + + this.ref.addEventListener('input', (event) => this.onStringInput(event, id)); + this.ref.addEventListener('paste', () => this.onTextPaste(id)); + + inputContainer.appendChild(inputSpan); + + requestAnimationFrame(() => { + if (!optional) { + const input = document.getElementById(id); + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(input); + range.collapse(false); + selection.removeAllRanges(); + selection.addRange(range); + } + }); + + return inputContainer; + } + + if (type === "code_input") { + const codeContainer = document.createElement("span"); + const codePlaceholder = document.createElement("span"); + + codeContainer.classList.add("code-input-container"); + + codePlaceholder.id = id; + codePlaceholder.contentEditable = "false"; + codePlaceholder.tabIndex = 0; + codePlaceholder.classList.add("ml-1", "mb-1", "px-[7px]", "inline-flex", "cursor-pointer", "rounded-[4px]", "mt-1", "code_input", "items-center"); + codePlaceholder.textContent = `${_optional} ${display_text}`; + codeContainer.id = "code-container"; + codeContainer.appendChild(codePlaceholder); + this.codeInputIds.push(id); + + // this.ref.addEventListener("click", this.onCodeInputClick); + + return codeContainer; + } + } + + registerCodeInputListener() { + this.codeInputIds.forEach((_codeInputId) => { + const codeInput = document.getElementById(_codeInputId); + codeInput.addEventListener("focus", () => { + codeInputId = _codeInputId; + }); + }); + } + + onTextPaste(id) { + const inputSpan = document.getElementById(id); + inputSpan.dispatchEvent(new Event('input', { bubbles: true })); + } + + onStringInput(event, id) { + const sel = window.getSelection(); + const inputSpan = document.getElementById(id); + if (event.target === inputSpan || (sel.anchorNode && sel.anchorNode.parentNode && sel.anchorNode.parentNode.classList.contains(id))) { + const inputIndex = agentInputsJson[0].registered_inputs.findIndex(_input => _input.id === id); + if (inputIndex !== -1) { + agentInputsJson[0].registered_inputs[inputIndex].value = inputSpan.textContent.trim(); + } + } + } + + onCodeInput(chipsData, chipName) { + const firstCodeInput = agentInputsJson[0].registered_inputs.find(input => input.type === "code_input" && ( codeInputId === 0 ? input.value === undefined : input.id === codeInputId)); + + if (firstCodeInput) { + const codeInputSpan = document.getElementById(firstCodeInput.id); + firstCodeInput.value = JSON.stringify(chipsData); + codeInputSpan.innerHTML = `${dartIcon}${chipName}`; + codeInputId = 0; + } + } +} \ No newline at end of file diff --git a/vscode/media/command-deck/command-deck.js b/vscode/media/command-deck/command-deck.js index 7846795e..25ad0ae6 100644 --- a/vscode/media/command-deck/command-deck.js +++ b/vscode/media/command-deck/command-deck.js @@ -75,13 +75,14 @@ function getCaretCoordinates(element, position) { } class CommandDeck { - constructor(ref, menuRef, resolveFn, replaceFn, menuItemFn) { + constructor(ref, menuRef, resolveFn, replaceFn, menuItemFn, agentUIBuilder) { this.ref = ref; this.menuRef = menuRef; this.resolveFn = resolveFn; this.replaceFn = replaceFn; this.menuItemFn = menuItemFn; this.options = []; + this.agentUIBuilder = agentUIBuilder; this.makeOptions = this.makeOptions.bind(this); this.closeMenu = this.closeMenu.bind(this); @@ -97,10 +98,11 @@ class CommandDeck { async makeOptions(query) { let options = []; if (query.startsWith('@')) { - options = await this.resolveFn(query.slice(1), 'at'); + options = await this.resolveFn(query, 'at'); } else if (query.startsWith('/')) { - options = await this.resolveFn(query.slice(1), 'slash'); + options = await this.resolveFn(query, 'slash'); } + if (options.length !== 0) { this.options = options; this.renderMenu(); @@ -122,27 +124,54 @@ class CommandDeck { selectItem(active) { return () => { const option = this.options[active]; - - const commands = this.extractCommands(option, "/") ?? option; - const isSlashOptionAvailable = commandsExecution.hasOwnProperty(commands); - - if (isSlashOptionAvailable) { - commandsExecution[commands].exe(this.ref); + if (!option?.name.startsWith('/')) { + this.ref.textContent = ''; + } + if (option?.name.startsWith('/')) { + const textContent = this.ref.innerHTML; + const atIndex = textContent.lastIndexOf('/'); + this.ref.innerHTML = textContent.substring(0, atIndex) + textContent.substring(atIndex + 1); + } + if (option?.name.startsWith('@')) { + const agentSpan = document.createElement('span'); + const slugSpan = document.createElement('span'); + agentSpan.classList.add("inline-block", "text-[#287CEB]"); + agentSpan.contentEditable = false; + agentSpan.textContent = `${option?.name}\u00A0`; + slugSpan.classList.add("inline-block"); + slugSpan.contentEditable = false; + slugSpan.textContent = "/"; + this.ref.appendChild(agentSpan); + this.ref.appendChild(slugSpan); + activeAgent = true; + currentActiveAgent = option.name; + // this.closeMenu(); + this.makeOptions("/"); + // Move the cursor to the end of the word + this.ref.focus(); + // Move the cursor to the end of the text + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(this.ref); + range.collapse(false); // false means collapse to the end + selection.removeAllRanges(); + selection.addRange(range); } else { - const trigger = this.ref.textContent[this.triggerIdx]; - this.ref.textContent = ""; - const mentionNode = document.createElement("span"); - mentionNode.id = "special-commands"; - mentionNode.classList.add("text-blue-500", "inline-block"); - mentionNode.contentEditable = false; - mentionNode.textContent = `${trigger}${option}\u200B`; - this.ref.appendChild(mentionNode); - this.ref.appendChild(document.createTextNode("\u00A0")); - setCaretToEnd(this.ref); + this.ref.textContent = ''; + const agentUIBuilder = new AgentUIBuilder(this.ref); + const agentProvider = new AgentProvider(data); + // agentInputsJson = agentProvider.getInputs(option); + agentInputsJson.push(agentProvider.getInputs(option.name)); + agentUIBuilder.buildAgentUI(); + + this.ref.focus(); + this.closeMenu(); + + setTimeout(() => { + adjustHeight(); + commandEnable = true; + }, 0); } - - this.ref.focus(); - this.closeMenu(); }; } @@ -163,18 +192,16 @@ class CommandDeck { const textBeforeCaret = this.ref.textContent.slice(0, positionIndex); const tokens = textBeforeCaret.split(/\s/); const lastToken = tokens[tokens.length - 1]; - const triggerIdx = textBeforeCaret.endsWith(lastToken) - ? textBeforeCaret.length - lastToken.length - : -1; + const triggerIdx = textBeforeCaret.endsWith(lastToken) ? textBeforeCaret.length - lastToken.length : -1; const maybeTrigger = textBeforeCaret[triggerIdx]; const keystrokeTriggered = maybeTrigger === '@' || maybeTrigger === '/'; this.ref.style.height = "auto"; this.ref.style.height = this.ref.scrollHeight + "px"; - const isTriggerAtStartOfWord = triggerIdx === 0; + // const iscoAtStartOfWord = triggerIdx === 0; - if (!keystrokeTriggered || !isTriggerAtStartOfWord) { + if (!keystrokeTriggered) { this.closeMenu(); return; } @@ -218,26 +245,22 @@ class CommandDeck { this.selectItem(this.active)(); keyCaught = true; break; - case 'Backspace': - const selection = window.getSelection(); - const range = selection.getRangeAt(0); - const mentionNode = document.getElementById("special-commands"); - - if (mentionNode) { - const prevNode = mentionNode.previousSibling; - const nextNode = mentionNode.nextSibling; - this.ref.removeChild(mentionNode); - - // Restore the cursor position - range.setStartAfter(prevNode || nextNode); - range.collapse(true); - selection.removeAllRanges(); - selection.addRange(range); - } - break; } } + if (ev.key === "Backspace") { + setTimeout(() => { + if (this.ref.textContent.trim() === "") { + activeAgent = false; + commandEnable = false; + currentActiveAgent = ''; + currentActiveSlug = ''; + agentInputsJson.length = 0; + codeInputId = 0; + } + }, 0); + } + if (keyCaught) { ev.preventDefault(); } diff --git a/vscode/media/header.png b/vscode/media/header.png new file mode 100644 index 00000000..8577bfbd Binary files /dev/null and b/vscode/media/header.png differ diff --git a/vscode/media/input.css b/vscode/media/input.css new file mode 100644 index 00000000..bd6213e1 --- /dev/null +++ b/vscode/media/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/vscode/media/onboarding/onboarding.css b/vscode/media/onboarding/onboarding.css index 039c755a..b8c17b64 100644 --- a/vscode/media/onboarding/onboarding.css +++ b/vscode/media/onboarding/onboarding.css @@ -201,11 +201,19 @@ p[contenteditable="true"] { outline: 1px solid #497BEF; } -#text-refactor-input { +.code_input { outline: 1px solid var(--vscode-editor-foreground); } -#text-refactor-input:focus { +.code_input:focus { + outline: 1px solid #497BEF; +} + +.string_input { + outline: 1px solid var(--vscode-editor-foreground); +} + +.string_input:focus { outline: 1px solid #497BEF; } @@ -223,6 +231,12 @@ p[contenteditable="true"] { z-index: 98; } +.questionnaire-card { + background-color: var(--vscode-dropdown-background); + /* color: var(--vscode-editorWidget-foreground); */ +} + + .tippy-box[data-theme~='flutter-blue'] { background-color: #287CEB; color: white; @@ -242,4 +256,23 @@ p[contenteditable="true"] { .tippy-box[data-theme~='flutter-blue'][data-placement^='right']>.tippy-arrow::before { border-right-color: #287CEB; +} + +progress { + height: 7px; +} + +@keyframes progress-animation { + from { + width: 0%; + } + + to { + width: 100%; + } +} + +progress::-webkit-progress-value { + background-color: #287CEB; + animation: progress-animation 2s linear forwards; } \ No newline at end of file diff --git a/vscode/media/onboarding/onboarding.html b/vscode/media/onboarding/onboarding.html index d76f10bb..b7c2d4bd 100644 --- a/vscode/media/onboarding/onboarding.html +++ b/vscode/media/onboarding/onboarding.html @@ -4,20 +4,19 @@ - + - +
- +
-