From 87eb27087fbc3d31582cb4045331bc51e25783be Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Fri, 5 Sep 2025 20:37:57 +0100 Subject: [PATCH 1/8] feat(ui/commands): add brainstorm command and prompt for code-free solution discussion --- codetide/agents/tide/prompts.py | 8 ++++++++ codetide/agents/tide/ui/agent_tide_ui.py | 8 +++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index c3287a6..dec1929 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -407,6 +407,14 @@ Ensure high coverage by including unit, integration, and end-to-end tests that address edge cases and follow best practices. """ + +CMD_BRAINSTORM_PROMPT = """ +You are strictly prohibited from writing or generating any code until the user explicitly asks you to do so. +For now, you must put on the hat of a solutions architect: your role is to discuss, brainstorm, and collaboratively explore possible solutions, architectures, and implementation strategies with the user. +Ask clarifying questions, propose alternatives, and help the user refine requirements or approaches. +Maintain a conversational flow, encourage user input, and do not proceed to code generation under any circumstances until the user gives a clear instruction to generate code. +""" + CMD_CODE_REVIEW_PROMPT = """ Review the following code submission for bugs, style inconsistencies, and performance issues. Provide specific, actionable feedback to improve code quality, maintainability, and adherence to established coding standards. diff --git a/codetide/agents/tide/ui/agent_tide_ui.py b/codetide/agents/tide/ui/agent_tide_ui.py index f2e24f8..58a4b86 100644 --- a/codetide/agents/tide/ui/agent_tide_ui.py +++ b/codetide/agents/tide/ui/agent_tide_ui.py @@ -9,7 +9,7 @@ "Install it with: pip install codetide[agents-ui]" ) from e -from codetide.agents.tide.prompts import CMD_CODE_REVIEW_PROMPT, CMD_COMMIT_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT +from codetide.agents.tide.prompts import CMD_BRAINSTORM_PROMPT, CMD_CODE_REVIEW_PROMPT, CMD_COMMIT_PROMPT, CMD_TRIGGER_PLANNING_STEPS, CMD_WRITE_TESTS_PROMPT from codetide.agents.tide.defaults import DEFAULT_AGENT_TIDE_LLM_CONFIG_PATH from codetide.agents.tide.ui.defaults import PLACEHOLDER_LLM_CONFIG from codetide.agents.tide.agent import AgentTide @@ -42,7 +42,8 @@ def __init__(self, project_path: Path = Path("./"), history :Optional[list]=None "plan": CMD_TRIGGER_PLANNING_STEPS, "review": CMD_CODE_REVIEW_PROMPT, "test": CMD_WRITE_TESTS_PROMPT, - "commit": CMD_COMMIT_PROMPT + "commit": CMD_COMMIT_PROMPT, + "brainstorm": CMD_BRAINSTORM_PROMPT } self.session_id = session_id if session_id else ulid() @@ -50,7 +51,8 @@ def __init__(self, project_path: Path = Path("./"), history :Optional[list]=None {"id": "review", "icon": "search-check", "description": "Review file(s) or object(s)"}, {"id": "test", "icon": "flask-conical", "description": "Test file(s) or object(s)"}, {"id": "commit", "icon": "git-commit", "description": "Commit changed files"}, - {"id": "plan", "icon": "notepad-text-dashed", "description": "Create a step-by-step task plan"} + {"id": "plan", "icon": "notepad-text-dashed", "description": "Create a step-by-step task plan"}, + {"id": "brainstorm", "icon": "brain-circuit", "description": "Brainstorm and discuss solutions (no code generation)"} ] async def load(self): From e2d0e5fdccc19338cc1d758da3fdf867c1f5e6d7 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Fri, 5 Sep 2025 21:57:42 +0100 Subject: [PATCH 2/8] feat(ui/app): improve CLI help with detailed command descriptions in help output --- codetide/agents/tide/ui/app.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index b53be52..13c8b01 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -448,7 +448,23 @@ def serve( def main(): - parser = argparse.ArgumentParser(description="Launch the Tide UI server.") + parser = argparse.ArgumentParser( + description="Launch the Tide UI server.", + epilog=( + "\nAvailable commands and what they do:\n" + " --host Host to bind to (default: None)\n" + " --port Port to bind to (default: 9753)\n" + " --root-path Root path for the app (default: None)\n" + " --ssl-certfile Path to SSL certificate file (default: None)\n" + " --ssl-keyfile Path to SSL key file (default: None)\n" + " --ws-per-message-deflate WebSocket per-message deflate (true/false, default: true)\n" + " --ws-protocol WebSocket protocol (default: auto)\n" + " --project-path Path to the project directory (default: ./)\n" + " --config-path Path to the config file (default: .agent_tide_config.yml)\n" + " -h, --help Show this help message and exit\n" + ), + formatter_class=argparse.RawDescriptionHelpFormatter + ) parser.add_argument("--host", type=str, default=None, help="Host to bind to") parser.add_argument("--port", type=int, default=AGENT_TIDE_PORT, help="Port to bind to") parser.add_argument("--root-path", type=str, default=None, help="Root path for the app") From ebb071bf623ab6c4edfe86cd73e16070346d9d0e Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Fri, 5 Sep 2025 22:33:07 +0100 Subject: [PATCH 3/8] feat(ui): add __init__.py to expose serve function in ui package --- codetide/agents/tide/ui/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 codetide/agents/tide/ui/__init__.py diff --git a/codetide/agents/tide/ui/__init__.py b/codetide/agents/tide/ui/__init__.py new file mode 100644 index 0000000..767ebea --- /dev/null +++ b/codetide/agents/tide/ui/__init__.py @@ -0,0 +1,5 @@ +from .app import serve + +__all__ = [ + "serve" +] \ No newline at end of file From b62cf8619789516fda6a8f4209c43c085d0e6868 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Fri, 5 Sep 2025 23:37:15 +0100 Subject: [PATCH 4/8] improved typsecriptp parser imports robustness --- codetide/parsers/typescript_parser.py | 178 ++++++++++++++++++-------- 1 file changed, 122 insertions(+), 56 deletions(-) diff --git a/codetide/parsers/typescript_parser.py b/codetide/parsers/typescript_parser.py index a362689..14b4151 100644 --- a/codetide/parsers/typescript_parser.py +++ b/codetide/parsers/typescript_parser.py @@ -172,85 +172,151 @@ def _process_node(cls, node: Node, code: bytes, codeFile: CodeFileModel): cls._process_node(child, code, codeFile) @classmethod - def _process_import_clause_node(cls, node: Node, code: bytes) -> Tuple[List[str], List[Optional[str]]]: - names = [] - aliases = [] + def _process_import_node(cls, node: Node, code: bytes, codeFile: CodeFileModel): + """ + Process different types of import statements: + 1. Side-effect imports: import './App.css' + 2. Default imports: import React from 'react' + 3. Named imports: import { useState } from 'react' + 4. Namespace imports: import * as React from 'react' + 5. Type-only imports: import type { User } from './types' + 6. Mixed imports: import React, { useState } from 'react' + """ - for child in node.children: - if child.type == "named_imports": - for import_child in child.children: - if import_child.type == "import_specifier": - current_name = None - current_alias = None - next_is_alias = False - - for alias_child in import_child.children: - if alias_child.type == "identifier" and not next_is_alias: - current_name = cls._get_content(code, alias_child) - elif alias_child.type == "as": - next_is_alias = True - elif alias_child.type == "identifier" and next_is_alias: - current_alias = cls._get_content(code, alias_child) - next_is_alias = False - - if current_name: - names.append(current_name) - aliases.append(current_alias) - - elif child.type == "identifier": - name = cls._get_content(code, child) - if name: - names.append(name) - aliases.append(None) + # Debug: Print the full import statement + import_text = cls._get_content(code, node) + print(f"Processing import: {import_text}") - return names, aliases - - @classmethod - def _process_import_node(cls, node: Node, code: bytes, codeFile: CodeFileModel): + # Initialize variables source = None names = [] aliases = [] - next_is_from_import = False - next_is_import = False + is_type_only = False + is_namespace = False + namespace_alias = None + # First pass: identify import type and extract source for child in node.children: - if child.type == "import": - next_is_import = True - elif child.type == "import_clause" and next_is_import: - names, aliases = cls._process_import_clause_node(child, code) - next_is_import = False - elif next_is_import: - source = cls._get_content(code, child) - next_is_import = False - elif child.type == "from": - next_is_from_import = True - elif child.type == "string" and next_is_from_import: - source = cls._get_content(code, child) - if names and source is None: - source = names[0] if len(names) == 1 else None - if source: - names = [] - aliases = [] - + child_text = cls._get_content(code, child) + print(f" Child: {child.type} = '{child_text}'") + + # Check for type-only import + if child.type == "type" or child_text == "type": + is_type_only = True + + # Extract source (the string literal) + elif child.type == "string": + source = child_text.strip("'\"") # Remove quotes + + # Second pass: process import clause if it exists + import_clause = None + for child in node.children: + if child.type == "import_clause": + import_clause = child + break + + if import_clause: + names, aliases, is_namespace, namespace_alias = cls._process_import_clause_node( + import_clause, code + ) + + # Handle different import types if source: - if names: + if is_namespace and namespace_alias: + # Namespace import: import * as React from 'react' + importStatement = ImportStatement( + source=source, + name="*", # Indicates namespace import + alias=namespace_alias, + is_type_only=is_type_only + ) + codeFile.add_import(importStatement) + cls._generate_unique_import_id(codeFile.imports[-1]) + + elif names: + # Named imports or default + named imports for name, alias in zip(names, aliases): importStatement = ImportStatement( source=source, name=name, - alias=alias + alias=alias, + is_type_only=is_type_only ) codeFile.add_import(importStatement) cls._generate_unique_import_id(codeFile.imports[-1]) else: + # Side-effect import: import './App.css' importStatement = ImportStatement( source=source, name=None, - alias=None + alias=None, + is_type_only=is_type_only, + is_side_effect=True ) codeFile.add_import(importStatement) cls._generate_unique_import_id(codeFile.imports[-1]) + @classmethod + def _process_import_clause_node(cls, node: Node, code: bytes) -> Tuple[List[str], List[str], bool, Optional[str]]: + """ + Process import_clause node to extract names and aliases. + Returns: (names, aliases, is_namespace, namespace_alias) + """ + names = [] + aliases = [] + is_namespace = False + namespace_alias = None + + def process_node_recursively(current_node: Node): + nonlocal names, aliases, is_namespace, namespace_alias + + node_text = cls._get_content(code, current_node) + print(f" Processing clause node: {current_node.type} = '{node_text}'") + + if current_node.type == "namespace_import": + # Handle: * as React + is_namespace = True + for child in current_node.children: + if child.type == "identifier": + namespace_alias = cls._get_content(code, child) + + elif current_node.type == "identifier": + # Default import or named import identifier + identifier = cls._get_content(code, current_node) + names.append(identifier) + aliases.append(None) # No alias by default + + elif current_node.type == "import_specifier": + # Named import with possible alias: { name } or { name as alias } + name = None + alias = None + + for child in current_node.children: + child_text = cls._get_content(code, child) + if child.type == "identifier": + if name is None: + name = child_text + else: + alias = child_text # This is the alias part + + if name: + names.append(name) + aliases.append(alias) + + elif current_node.type == "named_imports": + # Process children of named_imports (the part inside {}) + for child in current_node.children: + if child.type not in ["{", "}", ","]: # Skip punctuation + process_node_recursively(child) + + else: + # Recursively process children for other node types + for child in current_node.children: + process_node_recursively(child) + + process_node_recursively(node) + return names, aliases, is_namespace, namespace_alias + @classmethod def _process_class_node(cls, node: Node, code: bytes, codeFile: CodeFileModel, node_type :Literal["class", "interface", "type"]="class"): # TODO add support for modifiers at variables, classes i.e From d612269eff816ff0ced119404b93e3603760eefc Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Fri, 5 Sep 2025 23:38:42 +0100 Subject: [PATCH 5/8] added debug statements --- codetide/__init__.py | 4 +++- codetide/parsers/typescript_parser.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/codetide/__init__.py b/codetide/__init__.py index 8674046..870c494 100644 --- a/codetide/__init__.py +++ b/codetide/__init__.py @@ -15,6 +15,7 @@ from typing import Optional, List, Tuple, Union, Dict from datetime import datetime, timezone from pathlib import Path +import traceback import asyncio import pygit2 import time @@ -292,7 +293,7 @@ async def _process_single_file( logger.debug(f"Processing file: {filepath}") return await parser.parse_file(filepath, self.rootpath) except Exception as e: - logger.warning(f"Failed to process {filepath}: {str(e)}") + logger.warning(f"Failed to process {filepath}: {str(e)}\n\n{traceback.format_exc()}") return None def _add_results_to_codebase( @@ -419,6 +420,7 @@ def _get_changed_files(self) -> Tuple[List[Path], bool]: # Check for deleted files for stored_file_path in self.files: if stored_file_path not in files: + logger.info(f"detected deletion: {stored_file_path}") file_deletion_detected = True break diff --git a/codetide/parsers/typescript_parser.py b/codetide/parsers/typescript_parser.py index 14b4151..11b5be1 100644 --- a/codetide/parsers/typescript_parser.py +++ b/codetide/parsers/typescript_parser.py @@ -457,6 +457,7 @@ def _process_variable_declaration(cls, node: Node, code: bytes, codeFile: CodeFi @classmethod def _process_variable_declarator(cls, node: Node, code: bytes, codeFile: CodeFileModel): + # TODO debug this with GitRecap name = None type_hint = None value = None From 698cacbb2147dc15b13cda2900c50fe681612514 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 7 Sep 2025 23:08:24 +0100 Subject: [PATCH 6/8] added missing init file --- codetide/search/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 codetide/search/__init__.py diff --git a/codetide/search/__init__.py b/codetide/search/__init__.py new file mode 100644 index 0000000..e69de29 From c44dad37b85806a34ba227e9c68fc1f70b3a807c Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 7 Sep 2025 23:17:56 +0100 Subject: [PATCH 7/8] added flake ignore --- codetide/parsers/typescript_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codetide/parsers/typescript_parser.py b/codetide/parsers/typescript_parser.py index 11b5be1..e0f7bad 100644 --- a/codetide/parsers/typescript_parser.py +++ b/codetide/parsers/typescript_parser.py @@ -268,7 +268,7 @@ def _process_import_clause_node(cls, node: Node, code: bytes) -> Tuple[List[str] namespace_alias = None def process_node_recursively(current_node: Node): - nonlocal names, aliases, is_namespace, namespace_alias + nonlocal names, aliases, is_namespace, namespace_alias # noqa: F824 node_text = cls._get_content(code, current_node) print(f" Processing clause node: {current_node.type} = '{node_text}'") From dec7e7317d2ee02820f87f76cd3216ffa8f28563 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 7 Sep 2025 23:20:59 +0100 Subject: [PATCH 8/8] reverted ts parser --- codetide/parsers/typescript_parser.py | 179 ++++++++------------------ 1 file changed, 56 insertions(+), 123 deletions(-) diff --git a/codetide/parsers/typescript_parser.py b/codetide/parsers/typescript_parser.py index e0f7bad..a362689 100644 --- a/codetide/parsers/typescript_parser.py +++ b/codetide/parsers/typescript_parser.py @@ -172,151 +172,85 @@ def _process_node(cls, node: Node, code: bytes, codeFile: CodeFileModel): cls._process_node(child, code, codeFile) @classmethod - def _process_import_node(cls, node: Node, code: bytes, codeFile: CodeFileModel): - """ - Process different types of import statements: - 1. Side-effect imports: import './App.css' - 2. Default imports: import React from 'react' - 3. Named imports: import { useState } from 'react' - 4. Namespace imports: import * as React from 'react' - 5. Type-only imports: import type { User } from './types' - 6. Mixed imports: import React, { useState } from 'react' - """ + def _process_import_clause_node(cls, node: Node, code: bytes) -> Tuple[List[str], List[Optional[str]]]: + names = [] + aliases = [] - # Debug: Print the full import statement - import_text = cls._get_content(code, node) - print(f"Processing import: {import_text}") + for child in node.children: + if child.type == "named_imports": + for import_child in child.children: + if import_child.type == "import_specifier": + current_name = None + current_alias = None + next_is_alias = False + + for alias_child in import_child.children: + if alias_child.type == "identifier" and not next_is_alias: + current_name = cls._get_content(code, alias_child) + elif alias_child.type == "as": + next_is_alias = True + elif alias_child.type == "identifier" and next_is_alias: + current_alias = cls._get_content(code, alias_child) + next_is_alias = False + + if current_name: + names.append(current_name) + aliases.append(current_alias) + + elif child.type == "identifier": + name = cls._get_content(code, child) + if name: + names.append(name) + aliases.append(None) - # Initialize variables + return names, aliases + + @classmethod + def _process_import_node(cls, node: Node, code: bytes, codeFile: CodeFileModel): source = None names = [] aliases = [] - is_type_only = False - is_namespace = False - namespace_alias = None + next_is_from_import = False + next_is_import = False - # First pass: identify import type and extract source for child in node.children: - child_text = cls._get_content(code, child) - print(f" Child: {child.type} = '{child_text}'") - - # Check for type-only import - if child.type == "type" or child_text == "type": - is_type_only = True - - # Extract source (the string literal) - elif child.type == "string": - source = child_text.strip("'\"") # Remove quotes - - # Second pass: process import clause if it exists - import_clause = None - for child in node.children: - if child.type == "import_clause": - import_clause = child - break - - if import_clause: - names, aliases, is_namespace, namespace_alias = cls._process_import_clause_node( - import_clause, code - ) - - # Handle different import types + if child.type == "import": + next_is_import = True + elif child.type == "import_clause" and next_is_import: + names, aliases = cls._process_import_clause_node(child, code) + next_is_import = False + elif next_is_import: + source = cls._get_content(code, child) + next_is_import = False + elif child.type == "from": + next_is_from_import = True + elif child.type == "string" and next_is_from_import: + source = cls._get_content(code, child) + if names and source is None: + source = names[0] if len(names) == 1 else None + if source: + names = [] + aliases = [] + if source: - if is_namespace and namespace_alias: - # Namespace import: import * as React from 'react' - importStatement = ImportStatement( - source=source, - name="*", # Indicates namespace import - alias=namespace_alias, - is_type_only=is_type_only - ) - codeFile.add_import(importStatement) - cls._generate_unique_import_id(codeFile.imports[-1]) - - elif names: - # Named imports or default + named imports + if names: for name, alias in zip(names, aliases): importStatement = ImportStatement( source=source, name=name, - alias=alias, - is_type_only=is_type_only + alias=alias ) codeFile.add_import(importStatement) cls._generate_unique_import_id(codeFile.imports[-1]) else: - # Side-effect import: import './App.css' importStatement = ImportStatement( source=source, name=None, - alias=None, - is_type_only=is_type_only, - is_side_effect=True + alias=None ) codeFile.add_import(importStatement) cls._generate_unique_import_id(codeFile.imports[-1]) - @classmethod - def _process_import_clause_node(cls, node: Node, code: bytes) -> Tuple[List[str], List[str], bool, Optional[str]]: - """ - Process import_clause node to extract names and aliases. - Returns: (names, aliases, is_namespace, namespace_alias) - """ - names = [] - aliases = [] - is_namespace = False - namespace_alias = None - - def process_node_recursively(current_node: Node): - nonlocal names, aliases, is_namespace, namespace_alias # noqa: F824 - - node_text = cls._get_content(code, current_node) - print(f" Processing clause node: {current_node.type} = '{node_text}'") - - if current_node.type == "namespace_import": - # Handle: * as React - is_namespace = True - for child in current_node.children: - if child.type == "identifier": - namespace_alias = cls._get_content(code, child) - - elif current_node.type == "identifier": - # Default import or named import identifier - identifier = cls._get_content(code, current_node) - names.append(identifier) - aliases.append(None) # No alias by default - - elif current_node.type == "import_specifier": - # Named import with possible alias: { name } or { name as alias } - name = None - alias = None - - for child in current_node.children: - child_text = cls._get_content(code, child) - if child.type == "identifier": - if name is None: - name = child_text - else: - alias = child_text # This is the alias part - - if name: - names.append(name) - aliases.append(alias) - - elif current_node.type == "named_imports": - # Process children of named_imports (the part inside {}) - for child in current_node.children: - if child.type not in ["{", "}", ","]: # Skip punctuation - process_node_recursively(child) - - else: - # Recursively process children for other node types - for child in current_node.children: - process_node_recursively(child) - - process_node_recursively(node) - return names, aliases, is_namespace, namespace_alias - @classmethod def _process_class_node(cls, node: Node, code: bytes, codeFile: CodeFileModel, node_type :Literal["class", "interface", "type"]="class"): # TODO add support for modifiers at variables, classes i.e @@ -457,7 +391,6 @@ def _process_variable_declaration(cls, node: Node, code: bytes, codeFile: CodeFi @classmethod def _process_variable_declarator(cls, node: Node, code: bytes, codeFile: CodeFileModel): - # TODO debug this with GitRecap name = None type_hint = None value = None