From 18b17c1cb56044a3956c87c2d50a5e73148d691d Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 30 Aug 2025 23:35:50 +0100 Subject: [PATCH 01/28] added two html based custom elements --- .../tide/ui/public/elements/HtmlRenderer.jsx | 71 ++++++++++++ .../ui/public/elements/ReasoningMessage.jsx | 103 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 codetide/agents/tide/ui/public/elements/HtmlRenderer.jsx create mode 100644 codetide/agents/tide/ui/public/elements/ReasoningMessage.jsx diff --git a/codetide/agents/tide/ui/public/elements/HtmlRenderer.jsx b/codetide/agents/tide/ui/public/elements/HtmlRenderer.jsx new file mode 100644 index 0000000..22e29de --- /dev/null +++ b/codetide/agents/tide/ui/public/elements/HtmlRenderer.jsx @@ -0,0 +1,71 @@ +import { Card, CardContent } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { X, RefreshCw } from "lucide-react" + +export default function HtmlRenderer() { + // Function to sanitize HTML (basic approach) + const sanitizeHtml = (html) => { + // Create a temporary div to parse the HTML + const temp = document.createElement('div'); + temp.innerHTML = html; + + // Remove potentially dangerous elements + const dangerousElements = temp.querySelectorAll('script, object, embed, iframe'); + dangerousElements.forEach(el => el.remove()); + + return temp.innerHTML; + }; + + const handleRefresh = () => { + // Re-render the component by updating props + updateElement(props); + }; + + const handleRemove = () => { + deleteElement(); + }; + + // Get the HTML content from props + const htmlContent = props.html || props.content || '

No HTML content provided

'; + const title = props.title || 'HTML Content'; + const showControls = props.showControls !== false; // Default to true + + return ( +
+ {showControls && ( +
+

{title}

+
+ + +
+
+ )} + + + +
+ + +
+ ); +} \ No newline at end of file diff --git a/codetide/agents/tide/ui/public/elements/ReasoningMessage.jsx b/codetide/agents/tide/ui/public/elements/ReasoningMessage.jsx new file mode 100644 index 0000000..306abbe --- /dev/null +++ b/codetide/agents/tide/ui/public/elements/ReasoningMessage.jsx @@ -0,0 +1,103 @@ +import { Card, CardContent } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { ChevronDown, ChevronRight } from "lucide-react" +import { useState } from "react" + +export default function ReasoningMessage() { + const [isExpanded, setIsExpanded] = useState(props.defaultExpanded !== false); + + const toggleExpanded = () => { + setIsExpanded(!isExpanded); + }; + + // Get data from props + const reasoning = props.reasoning || "No reasoning provided"; + const data = props.data || {}; + const title = props.title || "Analysis Result"; + const summaryText = props.summaryText || "Click to view details"; + + // Convert data object to array and take first two entries + const dataEntries = Object.entries(data).slice(0, 2); + + // Generate HTML content + const generateHtml = () => { + let html = ` +
+
+
+

Reasoning

+
+ ${reasoning} +
+
`; + + // Add key-value entries if they exist + if (dataEntries.length > 0) { + html += `
`; + + dataEntries.forEach(([key, value], index) => { + html += ` +
+
${key}
+
+ ${typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)} +
+
`; + }); + + html += `
`; + } + + html += ` +
+
`; + + return html; + }; + + return ( +
+
+

{title}

+ + + + + + +

{summaryText}

+
+
+
+
+ + + + {isExpanded && ( +
+ )} + + + + +
+ ); +} \ No newline at end of file From 49c70ed39b6a09b94313c9fb034554e22c0ab916 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 30 Aug 2025 23:40:56 +0100 Subject: [PATCH 02/28] updated GET_CODE_IDENTIFIERS_SYSTEM_PROMPT --- codetide/agents/tide/prompts.py | 36 ++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index b44d7ac..0ca95f8 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -61,34 +61,46 @@ **Instructions:** 1. Carefully read and interpret the user's request, identifying any references to files, modules, submodules, or code elements—either explicit or implied. -2. **Prioritize returning fully qualified code identifiers** (such as functions, classes, methods, variables, or attributes) that are directly related to the user's request or are elements of interest. The identifier format must use dot notation to represent the path-like structure, e.g., `module.submodule.Class.method` or `module.function`, without file extensions. -3. Only include full file paths (relative to the repository root) if: +2. **Segregate identifiers into two categories:** + - **Context Identifiers:** Code elements (functions, classes, methods, variables, attributes, or file paths) that are required to understand, reference, or provide context for the requested change, but are not themselves expected to be modified. + - **Modify Identifiers:** Code elements (functions, classes, methods, variables, attributes, or file paths) that are likely to require direct modification to fulfill the user's request. +3. **Prioritize returning fully qualified code identifiers** (using dot notation, e.g., `module.submodule.Class.method`), without file extensions. Only include file paths (relative to the repository root) if: - The user explicitly requests file-level operations (such as adding, deleting, or renaming files), or - No valid or relevant code identifiers can be determined for the request. 4. If the user refers to a file by name or path and the request is about code elements within that file, extract and include the relevant code identifiers from that file instead of the file path, unless the user specifically asks for the file path. -5. If fulfilling the request would likely depend on additional symbols or files—based on naming, structure, required context from other files/modules, or conventional design patterns—include those code identifiers as well. -6. Only include identifiers or paths that are present in the provided tree structure. Never fabricate or guess paths or names that do not exist. -7. If no relevant code identifiers or file paths can be confidently identified, return an empty list. +5. If fulfilling the request would likely depend on additional symbols or files—based on naming, structure, required context from other files/modules, or conventional design patterns—include those code identifiers as context identifiers. +6. Only include identifiers or2025-08-30 18:45:26 - Translation file for pt-PT not found. Using default translation en-US. + paths that are present in the provided tree structure. Never fabricate or guess paths or names that do not exist. +7. If no relevant code identifiers or file paths can be confidently identified, leave the relevant section(s) empty. --- -**Output Format (Strict JSON Only):** +**Output Format:** -Return a JSON array of strings. Each string must be: -- A fully qualified code identifier using dot notation (e.g., `module.submodule.Class.method`), without file extensions, or -- A valid file path relative to the repository root (only if explicitly required or no code identifiers are available). +Your response must include: -Your output must be a pure JSON list of strings. Do **not** include any explanation, comments, or formatting outside the JSON block. +1. A brief explanation (1-3 sentences) describing your reasoning and search process for selecting the identifiers. +2. The following delimited sections, each containing a newline-separated list of identifiers (or left empty if none): + +*** Begin Context Identifiers + +*** End Context Identifiers + +*** Begin Modify Identifiers + +*** End Modify Identifiers + +Do **not** include any additional commentary, formatting, or output outside these sections. --- **Evaluation Criteria:** -- You must identify all code identifiers directly referenced or implied in the user request, prioritizing them over file paths. +- You must identify all code identifiers directly referenced or implied in the user request, and correctly categorize them as context or modify identifiers. - You must include any internal code elements that are clearly involved or required for the task. - You must consider logical dependencies that may need to be modified together (e.g., helper modules, config files, related class methods). - You must consider files that can be relevant as context to complete the user request, but only include their paths if code identifiers are not available or explicitly requested. -- You must return a clean and complete list of all relevant code identifiers and, if necessary, file paths. +- You must return a clean and complete list of all relevant code identifiers and, if necessary, file paths, in the correct section. - Do not over-include; be minimal but thorough. Return only what is truly required. """ From aa842843710a7a3b70b4a6e81642795b9603700e Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 30 Aug 2025 23:41:31 +0100 Subject: [PATCH 03/28] refactored parse_commit_blocks to more general parse_blocks --- codetide/agents/tide/utils.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/codetide/agents/tide/utils.py b/codetide/agents/tide/utils.py index 7fca8cc..b5a1e48 100644 --- a/codetide/agents/tide/utils.py +++ b/codetide/agents/tide/utils.py @@ -55,26 +55,34 @@ def parse_patch_blocks(text: str, multiple: bool = True) -> Union[str, List[str] return matches if multiple else matches[0] -def parse_commit_blocks(text: str, multiple: bool = True) -> Union[str, List[str], None]: +def parse_blocks(text: str, block_word: str = "Commit", multiple: bool = True) -> Union[str, List[str], None]: """ - Extract content between *** Begin Commit and *** End Commit markers (exclusive), + Extract content between *** Begin and *** End markers (exclusive), ensuring that both markers are at zero indentation (start of line, no leading spaces). Args: - text: Full input text containing one or more commit blocks. - multiple: If True, return a list of all commit blocks. If False, return the first match. + text: Full input text containing one or more blocks. + block_word: The word to use in the block markers (e.g., "Commit", "Section", "Code"). + multiple: If True, return a list of all blocks. If False, return the first match. Returns: - A string (single commit), list of strings (multiple commits), or None if not found. + A string (single block), list of strings (multiple blocks), or None if not found. """ - pattern = r"(?m)^\*\*\* Begin Commit\n([\s\S]*?)^\*\*\* End Commit$" + # Escape the block_word to handle any special regex characters + escaped_word = re.escape(block_word) + + # Create pattern with the parameterized block word + pattern = rf"(?m)^\*\*\* Begin {escaped_word}\n([\s\S]*?)^\*\*\* End {escaped_word}$" matches = re.findall(pattern, text) if not matches: return None - return matches if multiple else matches[0].strip() + if multiple: + return [match.strip() for match in matches] + else: + return matches[0].strip() def parse_steps_markdown(md: str): steps = [] From ffbb95315bcc0cbde5706afcc3e6fe497a07711f Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 30 Aug 2025 23:42:46 +0100 Subject: [PATCH 04/28] updated agent to integrate GET_CODE_IDENTIFIERS_SYSTEM_PROMPT changes and provide require support for chainlit app --- codetide/agents/tide/agent.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 16bd3c9..cce2374 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -8,7 +8,7 @@ AGENT_TIDE_SYSTEM_PROMPT, GET_CODE_IDENTIFIERS_SYSTEM_PROMPT, REJECT_PATCH_FEEDBACK_TEMPLATE, STAGED_DIFFS_TEMPLATE, STEPS_SYSTEM_PROMPT, WRITE_PATCH_SYSTEM_PROMPT ) -from .utils import delete_file, parse_commit_blocks, parse_patch_blocks, parse_steps_markdown, trim_to_patch_section +from .utils import delete_file, parse_blocks, parse_patch_blocks, parse_steps_markdown, trim_to_patch_section from .consts import AGENT_TIDE_ASCII_ART try: @@ -49,6 +49,10 @@ class AgentTide(BaseModel): changed_paths :List[str]=Field(default_factory=list) request_human_confirmation :bool=False + contextIdentifiers :Optional[List[str]]=None + modifyIdentifiers :Optional[List[str]]=None + reasoning :Optional[str]=None + _skip_context_retrieval :bool=False _last_code_identifers :Optional[Set[str]]=set() _last_code_context :Optional[str] = None @@ -104,14 +108,26 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): ) if codeIdentifiers is None and not self._skip_context_retrieval: - codeIdentifiers = await self.llm.acomplete( + context_response = await self.llm.acomplete( self.history, system_prompt=[GET_CODE_IDENTIFIERS_SYSTEM_PROMPT.format(DATE=TODAY)], prefix_prompt=repo_tree, - stream=False, - json_output=True + stream=False + # json_output=True ) + contextIdentifiers = parse_blocks(context_response, block_word="Context Identifiers", multiple=False) + modifyIdentifiers = parse_blocks(context_response, block_word="Modify Identifiers", multiple=False) + + reasoning = context_response.split("*** Begin") + if not reasoning: + reasoning = [context_response] + self.reasoning = reasoning[0].strip() + + self.contextIdentifiers = contextIdentifiers.splitlines() or None + self.modifyIdentifiers = modifyIdentifiers.splitlines() or None + # TODO need fo finsih implementing context and modify identifiers into codetide logic + codeContext = None if codeIdentifiers: autocomplete = AutoComplete(self.tide.cached_ids) @@ -144,7 +160,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): if not self.request_human_confirmation: self.approve() - commitMessage = parse_commit_blocks(response, multiple=False) + commitMessage = parse_blocks(response, multiple=False, block_word="Commit") if commitMessage: self.commit(commitMessage) From f0979abec7040f5a8ef36c02a1450ad290b3f890 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sat, 30 Aug 2025 23:44:47 +0100 Subject: [PATCH 05/28] updarted chainlit app to display custom element with context ideneifities and reasoning --- codetide/agents/tide/ui/app.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 568ec56..9710b43 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -37,6 +37,7 @@ import asyncio import json import yaml +import time @cl.password_auth_callback def auth(): @@ -278,7 +279,9 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option chat_history.append({"role": "user", "content": message.content}) await agent_tide_ui.add_to_history(message.content) - + + context_msg = cl.Message(content="", author="AgentTide") + # context_stream_msg = cl.Message(content="", author="AgentTide") msg = cl.Message(content="", author="Agent Tide") async with cl.Step("ApplyPatch", type="tool") as diff_step: await diff_step.remove() @@ -311,8 +314,28 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option global_fallback_msg=msg ) + st = time.time() async for chunk in run_concurrent_tasks(agent_tide_ui, codeIdentifiers): if chunk == STREAM_START_TOKEN: + + context_data = { + key: value for key in ["contextIdentifiers", "modifyIdentifiers"] + if (value := getattr(agent_tide_ui.agent_tide, key, None)) + } + context_msg.elements.append( + cl.CustomElement( + name="ReasoningMessage", + props={ + "reasoning": agent_tide_ui.agent_tide.reasoning, + "data": context_data, + "title": f"Thought for {time.time()-st:.2f} seconds", + "defaultExpanded": False, + "showControls": False + } + ) + ) + await context_msg.send() + continue if chunk == STREAM_END_TOKEN: From 220f2fef1cc3f977d12f08413bc5cc33dbfab943 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 00:03:00 +0100 Subject: [PATCH 06/28] added loading animation --- codetide/agents/tide/ui/app.py | 19 ++++- .../ui/public/elements/LoadingMessage.jsx | 81 +++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 codetide/agents/tide/ui/public/elements/LoadingMessage.jsx diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 9710b43..09c6894 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -266,6 +266,21 @@ async def on_inspect_context(action :cl.Action): @cl.on_message async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Optional[list] = None, agent_tide_ui :Optional[AgentTideUi]=None): + + loading_msg = await cl.Message( + content="", + elements=[ + cl.CustomElement( + name="LoadingMessage", + props={ + "messages": ["Working", "Syncing CodeTide", "Thinking", "Looking for context"], + "interval": 1500, # 1.5 seconds between messages + "showIcon": True + } + ) + ] + ).send() + if agent_tide_ui is None: agent_tide_ui = await loadAgentTideUi() @@ -281,7 +296,6 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option await agent_tide_ui.add_to_history(message.content) context_msg = cl.Message(content="", author="AgentTide") - # context_stream_msg = cl.Message(content="", author="AgentTide") msg = cl.Message(content="", author="Agent Tide") async with cl.Step("ApplyPatch", type="tool") as diff_step: await diff_step.remove() @@ -317,7 +331,8 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option st = time.time() async for chunk in run_concurrent_tasks(agent_tide_ui, codeIdentifiers): if chunk == STREAM_START_TOKEN: - + await loading_msg.remove() + context_data = { key: value for key in ["contextIdentifiers", "modifyIdentifiers"] if (value := getattr(agent_tide_ui.agent_tide, key, None)) diff --git a/codetide/agents/tide/ui/public/elements/LoadingMessage.jsx b/codetide/agents/tide/ui/public/elements/LoadingMessage.jsx new file mode 100644 index 0000000..aeba6f4 --- /dev/null +++ b/codetide/agents/tide/ui/public/elements/LoadingMessage.jsx @@ -0,0 +1,81 @@ +import React, { useState, useEffect } from 'react'; + +const LoadingMessage = ({ messages = ['Working...', 'Syncing CodeTide', 'Thinking...', 'Looking for context'], interval = 3000 }) => { + const [currentIndex, setCurrentIndex] = useState(0); + + useEffect(() => { + const timer = setInterval(() => { + setCurrentIndex((prevIndex) => (prevIndex + 1) % messages.length); + }, interval); + + return () => clearInterval(timer); + }, [messages.length, interval]); + + /* TODO update styles and fonts to match current - increase size a bit*/ + const loadingWaveStyle = { + display: 'inline-flex', + alignItems: 'center', + gap: '8px', + padding: '8px 12px', + background: 'rgba(255, 255, 255, 0.05)', + border: '1px solid rgba(255, 255, 255, 0.1)', + borderRadius: '20px', + backdropFilter: 'blur(10px)', + color: '#e0e0e0', + fontSize: '12px', + fontWeight: '500', + letterSpacing: '0.3px', + minHeight: '20px', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif' + }; + + const waveContainerStyle = { + display: 'flex', + gap: '2px' + }; + + const waveDotStyle = { + width: '3px', + height: '3px', + background: '#00d4ff', + borderRadius: '50%', + animation: 'wave 1.2s ease-in-out infinite' + }; + + const loadingTextStyle = { + minWidth: '120px', + transition: 'opacity 0.3s ease-in-out' + }; + + return ( + <> + +
+
+
+
+
+
+
+
+ {messages[currentIndex]} +
+
+ + ); +}; + +export default LoadingMessage; \ No newline at end of file From ac994789c62b25a2dbf24a867ba63b668a8e6bfd Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 15:12:27 +0100 Subject: [PATCH 07/28] updated reasoner block --- .../agents/tide/ui/public/elements/ReasoningMessage.jsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/ReasoningMessage.jsx b/codetide/agents/tide/ui/public/elements/ReasoningMessage.jsx index 306abbe..0dbcedb 100644 --- a/codetide/agents/tide/ui/public/elements/ReasoningMessage.jsx +++ b/codetide/agents/tide/ui/public/elements/ReasoningMessage.jsx @@ -25,11 +25,8 @@ export default function ReasoningMessage() { let html = `
-
-

Reasoning

-
- ${reasoning} -
+
+ ${reasoning}
`; // Add key-value entries if they exist From 057960cd3bd15ae2d0f1a8a77c19853ada16f169 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 15:24:55 +0100 Subject: [PATCH 08/28] updated laoding msg custom element --- .../ui/public/elements/LoadingMessage.jsx | 144 ++++++++---------- 1 file changed, 65 insertions(+), 79 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/LoadingMessage.jsx b/codetide/agents/tide/ui/public/elements/LoadingMessage.jsx index aeba6f4..f5de93f 100644 --- a/codetide/agents/tide/ui/public/elements/LoadingMessage.jsx +++ b/codetide/agents/tide/ui/public/elements/LoadingMessage.jsx @@ -1,81 +1,67 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from "react" -const LoadingMessage = ({ messages = ['Working...', 'Syncing CodeTide', 'Thinking...', 'Looking for context'], interval = 3000 }) => { - const [currentIndex, setCurrentIndex] = useState(0); - - useEffect(() => { - const timer = setInterval(() => { - setCurrentIndex((prevIndex) => (prevIndex + 1) % messages.length); - }, interval); - - return () => clearInterval(timer); - }, [messages.length, interval]); - - /* TODO update styles and fonts to match current - increase size a bit*/ - const loadingWaveStyle = { - display: 'inline-flex', - alignItems: 'center', - gap: '8px', - padding: '8px 12px', - background: 'rgba(255, 255, 255, 0.05)', - border: '1px solid rgba(255, 255, 255, 0.1)', - borderRadius: '20px', - backdropFilter: 'blur(10px)', - color: '#e0e0e0', - fontSize: '12px', - fontWeight: '500', - letterSpacing: '0.3px', - minHeight: '20px', - fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif' - }; - - const waveContainerStyle = { - display: 'flex', - gap: '2px' - }; - - const waveDotStyle = { - width: '3px', - height: '3px', - background: '#00d4ff', - borderRadius: '50%', - animation: 'wave 1.2s ease-in-out infinite' - }; - - const loadingTextStyle = { - minWidth: '120px', - transition: 'opacity 0.3s ease-in-out' - }; - - return ( - <> - -
-
-
-
-
-
+export default function LoadingMessages() { + const defaultMessages = [ + "Working", + "Syncing CodeTide", + "Thinking", + "Looking for context" + ]; + + const messages = props.messages || defaultMessages; + const interval = props.interval || 2000; // 2 seconds default + const showIcon = props.showIcon !== false; // default true + + const [currentMessageIndex, setCurrentMessageIndex] = useState(0); + + useEffect(() => { + const timer = setInterval(() => { + setCurrentMessageIndex((prevIndex) => + (prevIndex + 1) % messages.length + ); + }, interval); + + return () => clearInterval(timer); + }, [messages.length, interval]); + + return ( +
+ {showIcon && ( +
+
+
+
+
+ )} + + + {messages[currentMessageIndex]}... + + +
-
- {messages[currentIndex]} -
-
- - ); -}; - -export default LoadingMessage; \ No newline at end of file + ); +} \ No newline at end of file From 77b022fb7c9bad2904fa5fc7573dd09b0063377b Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 16:48:37 +0100 Subject: [PATCH 09/28] added _as_file_paths to ensure full laoding for commits --- codetide/__init__.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/codetide/__init__.py b/codetide/__init__.py index 913c0d7..e08f241 100644 --- a/codetide/__init__.py +++ b/codetide/__init__.py @@ -577,4 +577,17 @@ def get( as_string=as_string, as_list_str=as_string_list, preloaded_files=requested_files - ) \ No newline at end of file + ) + + def _as_file_paths(self, code_identifiers: Union[str, List[str]])->List[str]: + if isinstance(code_identifiers, str): + code_identifiers = [code_identifiers] + + as_file_paths = [] + for code_identifier in code_identifiers: + if self.rootpath / code_identifier in self.files: + as_file_paths.append(code_identifier) + elif element := self.codebase.cached_elements.get(code_identifier): + as_file_paths.append(element.file_path) + + return as_file_paths From c12c3ca02a1c1fff8f66537f101eea71a75c11b7 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 16:49:50 +0100 Subject: [PATCH 10/28] integrated _as_file_paths into codeIdentifiers --- codetide/agents/tide/agent.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index cce2374..bec60a0 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -126,7 +126,10 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): self.contextIdentifiers = contextIdentifiers.splitlines() or None self.modifyIdentifiers = modifyIdentifiers.splitlines() or None - # TODO need fo finsih implementing context and modify identifiers into codetide logic + codeIdentifiers = self.contextIdentifiers + + if self.modifyIdentifiers: + codeIdentifiers.extend(self.tide._as_file_paths(self.modifyIdentifiers)) codeContext = None if codeIdentifiers: From fff6ea08d67a793935fad7959bbb6b46d8397c15 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 17:24:29 +0100 Subject: [PATCH 11/28] added cached_elements property and expanded type annotation --- codetide/core/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/codetide/core/models.py b/codetide/core/models.py index 122d008..d1505aa 100644 --- a/codetide/core/models.py +++ b/codetide/core/models.py @@ -539,7 +539,13 @@ def from_list_of_elements(cls, class CodeBase(BaseModel): """Root model representing complete codebase with file hierarchy and caching.""" root: List[CodeFileModel] = Field(default_factory=list) - _cached_elements :Dict[str, Any] = dict() + _cached_elements :Dict[str, Union[CodeFileModel, ClassDefinition, FunctionDefinition, VariableDeclaration, ImportStatement]] = dict() + + @property + def cached_elements(self)->Dict[str, Union[CodeFileModel, ClassDefinition, FunctionDefinition, VariableDeclaration, ImportStatement]]: + if not self._cached_elements: + self._build_cached_elements() + return self._cached_elements def _build_cached_elements(self, force_update :bool=False): """Builds cache of all elements with unique IDs across entire codebase.""" From f4c650b81c2090a96864b85c9296c911cea6db08 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 18:13:11 +0100 Subject: [PATCH 12/28] updated steps prompt to get context and modify identifiers --- codetide/agents/tide/prompts.py | 8 +++-- codetide/agents/tide/utils.py | 53 ++++++++++++++------------------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 0ca95f8..afe8551 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -300,12 +300,16 @@ 1. **step_description** **instructions**: precise instructions of the task to be implemented in this step **context_identifiers**: - - fully qualified code identifiers or file paths (as taken from the repo_tree) that this step touches, depends on, or must update + - fully qualified code identifiers or file paths (as taken from the repo_tree) that this step depends on for context (read/reference only) + **modify_identifiers**: + - fully qualified code identifiers or file paths (as taken from the repo_tree) that this step will directly modify or update --- 2. **next_step_description** **instructions**: ... **context_identifiers**: - ... + **modify_identifiers**: + - ... --- ... *** End Steps @@ -320,7 +324,7 @@ 4. **Granularity:** Break complex requirements into logical sub-steps. Order them so dependencies are respected (e.g., setup → implementation → validation → integration). -5. **Traceability:** Each step’s `context_identifiers` must clearly tie that step to specific code areas; this enables downstream mapping to actual implementation targets. +5. **Traceability:** Each step’s `context_identifiers` and `modify_identifiers` must clearly tie that step to specific code areas; this enables downstream mapping to actual implementation targets. 6. **Single-Responsibility per Step:** Aim for each numbered step to encapsulate a coherent unit of work. Avoid mixing unrelated concerns in one step. diff --git a/codetide/agents/tide/utils.py b/codetide/agents/tide/utils.py index b5a1e48..12cfdd1 100644 --- a/codetide/agents/tide/utils.py +++ b/codetide/agents/tide/utils.py @@ -85,43 +85,36 @@ def parse_blocks(text: str, block_word: str = "Commit", multiple: bool = True) - return matches[0].strip() def parse_steps_markdown(md: str): + """ + Parse the markdown steps block and return a list of step dicts. + Now supports both context_identifiers and modify_identifiers. + """ steps = [] - - # Extract only content between *** Begin Steps and *** End Steps - match = re.search(r"\*\*\* Begin Steps(.*?)\*\*\* End Steps", md, re.DOTALL) - if not match: - return [] - - steps_block = match.group(1).strip() - - # Split steps by '---' - raw_steps = [s.strip() for s in steps_block.split('---') if s.strip()] - - for raw_step in raw_steps: - # Match step number and description - step_header = re.match(r"(\d+)\.\s+\*\*(.*?)\*\*", raw_step) - if not step_header: + step_blocks = re.split(r'---\s*', md) + for block in step_blocks: + block = block.strip() + if not block or block.startswith("*** End Steps"): continue - - step_num = int(step_header.group(1)) - description = step_header.group(2).strip() - - # Match instructions - instructions_match = re.search(r"\*\*instructions\*\*:\s*(.*?)(?=\*\*context_identifiers\*\*:)", raw_step, re.DOTALL) - instructions = instructions_match.group(1).strip() if instructions_match else "" - - # Match context identifiers - context_match = re.search(r"\*\*context_identifiers\*\*:\s*(.*)", raw_step, re.DOTALL) - context_block = context_match.group(1).strip() if context_match else "" - context_identifiers = re.findall(r"- (.+)", context_block) - + # Parse step number and description + m = re.match( + r'(\d+)\.\s+\*\*(.*?)\*\*\s*\n\s*\*\*instructions\*\*:\s*(.*?)\n\s*\*\*context_identifiers\*\*:\s*((?:- .*\n?)*)\s*\*\*modify_identifiers\*\*:\s*((?:- .*\n?)*)', + block, re.DOTALL) + if not m: + continue + step_num = int(m.group(1)) + description = m.group(2).strip() + instructions = m.group(3).strip() + context_block = m.group(4) + modify_block = m.group(5) + context_identifiers = [line[2:].strip() for line in context_block.strip().splitlines() if line.strip().startswith('-')] + modify_identifiers = [line[2:].strip() for line in modify_block.strip().splitlines() if line.strip().startswith('-')] steps.append({ "step": step_num, "description": description, "instructions": instructions, - "context_identifiers": context_identifiers + "context_identifiers": context_identifiers, + "modify_identifiers": modify_identifiers }) - return steps async def delete_file(file_path: str) -> bool: From 2e9900308cae976f12b4d84f5aee5e25b42411d4 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 18:54:05 +0100 Subject: [PATCH 13/28] updated Step models --- codetide/agents/tide/models.py | 12 ++++++++++-- codetide/agents/tide/ui/app.py | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/codetide/agents/tide/models.py b/codetide/agents/tide/models.py index 62be8d2..84c03a0 100644 --- a/codetide/agents/tide/models.py +++ b/codetide/agents/tide/models.py @@ -1,6 +1,5 @@ - +from typing import Callable, Dict, List, Optional from pydantic import BaseModel, RootModel -from typing import Dict, List, Optional STEP_INSTRUCTION_TEMPLATE = """ ## Step {step}: @@ -23,6 +22,7 @@ class Step(BaseModel): description :str instructions :str context_identifiers :Optional[List[str]]=None + modify_identifiers: Optional[List[str]]=None def as_instruction(self)->str: return STEP_INSTRUCTION_TEMPLATE.format( @@ -31,6 +31,14 @@ def as_instruction(self)->str: instructions=self.instructions ) + def get_code_identifiers(self, validate_identifiers_fn :Callable)->Optional[List[str]]: + code_identifiers = self.context_identifiers or [] + + if self.modify_identifiers: + code_identifiers.extend(validate_identifiers_fn(code_identifiers)) + + return None or code_identifiers + class Steps(RootModel): root :List[Step] diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 09c6894..1363256 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -214,7 +214,7 @@ async def on_execute_steps(action :cl.Action): author="Agent Tide" ).send() - await agent_loop(step_instructions_msg, codeIdentifiers=step.context_identifiers, agent_tide_ui=agent_tide_ui) + await agent_loop(step_instructions_msg, codeIdentifiers=step.get_code_identifiers(agent_tide_ui.agent_tide.tide._as_file_paths), agent_tide_ui=agent_tide_ui) task_list.status = f"Waiting feedback on step {current_task_idx}" await task_list.send() From a8589fdfd50361150d0bf00c216c2f7896df83b2 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 19:31:47 +0100 Subject: [PATCH 14/28] reversed laodingMessage --- .../ui/public/elements/LoadingMessage.jsx | 144 ++++++++++-------- 1 file changed, 79 insertions(+), 65 deletions(-) diff --git a/codetide/agents/tide/ui/public/elements/LoadingMessage.jsx b/codetide/agents/tide/ui/public/elements/LoadingMessage.jsx index f5de93f..aeba6f4 100644 --- a/codetide/agents/tide/ui/public/elements/LoadingMessage.jsx +++ b/codetide/agents/tide/ui/public/elements/LoadingMessage.jsx @@ -1,67 +1,81 @@ -import { useState, useEffect } from "react" +import React, { useState, useEffect } from 'react'; -export default function LoadingMessages() { - const defaultMessages = [ - "Working", - "Syncing CodeTide", - "Thinking", - "Looking for context" - ]; - - const messages = props.messages || defaultMessages; - const interval = props.interval || 2000; // 2 seconds default - const showIcon = props.showIcon !== false; // default true - - const [currentMessageIndex, setCurrentMessageIndex] = useState(0); - - useEffect(() => { - const timer = setInterval(() => { - setCurrentMessageIndex((prevIndex) => - (prevIndex + 1) % messages.length - ); - }, interval); - - return () => clearInterval(timer); - }, [messages.length, interval]); - - return ( -
- {showIcon && ( -
-
-
-
-
- )} - - - {messages[currentMessageIndex]}... - - - +const LoadingMessage = ({ messages = ['Working...', 'Syncing CodeTide', 'Thinking...', 'Looking for context'], interval = 3000 }) => { + const [currentIndex, setCurrentIndex] = useState(0); + + useEffect(() => { + const timer = setInterval(() => { + setCurrentIndex((prevIndex) => (prevIndex + 1) % messages.length); + }, interval); + + return () => clearInterval(timer); + }, [messages.length, interval]); + + /* TODO update styles and fonts to match current - increase size a bit*/ + const loadingWaveStyle = { + display: 'inline-flex', + alignItems: 'center', + gap: '8px', + padding: '8px 12px', + background: 'rgba(255, 255, 255, 0.05)', + border: '1px solid rgba(255, 255, 255, 0.1)', + borderRadius: '20px', + backdropFilter: 'blur(10px)', + color: '#e0e0e0', + fontSize: '12px', + fontWeight: '500', + letterSpacing: '0.3px', + minHeight: '20px', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif' + }; + + const waveContainerStyle = { + display: 'flex', + gap: '2px' + }; + + const waveDotStyle = { + width: '3px', + height: '3px', + background: '#00d4ff', + borderRadius: '50%', + animation: 'wave 1.2s ease-in-out infinite' + }; + + const loadingTextStyle = { + minWidth: '120px', + transition: 'opacity 0.3s ease-in-out' + }; + + return ( + <> + +
+
+
+
+
+
- ); -} \ No newline at end of file +
+ {messages[currentIndex]} +
+
+ + ); +}; + +export default LoadingMessage; \ No newline at end of file From ffb43c1c104b07e812ff4139c3202f735d927f22 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 20:28:00 +0100 Subject: [PATCH 15/28] refactored patch_cide_blocks into patch tool to support process-patch handling multiple patches in single file --- codetide/agents/tide/agent.py | 4 +- codetide/agents/tide/utils.py | 22 +----- codetide/mcp/tools/patch_code/__init__.py | 81 +++++++++++++++-------- 3 files changed, 58 insertions(+), 49 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index bec60a0..506fc97 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -1,6 +1,6 @@ from functools import partial from codetide import CodeTide -from ...mcp.tools.patch_code import file_exists, open_file, process_patch, remove_file, write_file +from ...mcp.tools.patch_code import file_exists, open_file, process_patch, remove_file, write_file, parse_patch_blocks from ...core.defaults import DEFAULT_ENCODING, DEFAULT_STORAGE_PATH from ...autocomplete import AutoComplete from .models import Steps @@ -8,7 +8,7 @@ AGENT_TIDE_SYSTEM_PROMPT, GET_CODE_IDENTIFIERS_SYSTEM_PROMPT, REJECT_PATCH_FEEDBACK_TEMPLATE, STAGED_DIFFS_TEMPLATE, STEPS_SYSTEM_PROMPT, WRITE_PATCH_SYSTEM_PROMPT ) -from .utils import delete_file, parse_blocks, parse_patch_blocks, parse_steps_markdown, trim_to_patch_section +from .utils import delete_file, parse_blocks, parse_steps_markdown, trim_to_patch_section from .consts import AGENT_TIDE_ASCII_ART try: diff --git a/codetide/agents/tide/utils.py b/codetide/agents/tide/utils.py index 12cfdd1..98a712b 100644 --- a/codetide/agents/tide/utils.py +++ b/codetide/agents/tide/utils.py @@ -21,7 +21,7 @@ async def trim_to_patch_section(filename): lines_to_keep.append(line) # Include the begin marker elif '*** End Patch' in line: lines_to_keep.append(line) # Include the end marker - break # Stop after end marker + capturing = False # Stop capturing but continue processing elif capturing: lines_to_keep.append(line) @@ -34,26 +34,6 @@ async def trim_to_patch_section(filename): except FileNotFoundError: pass -def parse_patch_blocks(text: str, multiple: bool = True) -> Union[str, List[str], None]: - """ - Extract content between *** Begin Patch and *** End Patch markers (inclusive), - ensuring that both markers are at zero indentation (start of line, no leading spaces). - - Args: - text: Full input text containing one or more patch blocks. - multiple: If True, return a list of all patch blocks. If False, return the first match. - - Returns: - A string (single patch), list of strings (multiple patches), or None if not found. - """ - - pattern = r"(?m)^(\*\*\* Begin Patch[\s\S]*?^\*\*\* End Patch)$" - matches = re.findall(pattern, text) - - if not matches: - return None - - return matches if multiple else matches[0] def parse_blocks(text: str, block_word: str = "Commit", multiple: bool = True) -> Union[str, List[str], None]: """ diff --git a/codetide/mcp/tools/patch_code/__init__.py b/codetide/mcp/tools/patch_code/__init__.py index 813bbdb..12550e5 100644 --- a/codetide/mcp/tools/patch_code/__init__.py +++ b/codetide/mcp/tools/patch_code/__init__.py @@ -5,8 +5,30 @@ from typing import Dict, Optional, Tuple, List, Callable, Union import pathlib +import re import os +def parse_patch_blocks(text: str, multiple: bool = True) -> Union[str, List[str], None]: + """ + Extract content between *** Begin Patch and *** End Patch markers (inclusive), + ensuring that both markers are at zero indentation (start of line, no leading spaces). + + Args: + text: Full input text containing one or more patch blocks. + multiple: If True, return a list of all patch blocks. If False, return the first match. + + Returns: + A string (single patch), list of strings (multiple patches), or None if not found. + """ + + pattern = r"(?m)^(\*\*\* Begin Patch[\s\S]*?^\*\*\* End Patch)$" + matches = re.findall(pattern, text) + + if not matches: + return None + + return matches if multiple else matches[0] + # --------------------------------------------------------------------------- # # User-facing API # --------------------------------------------------------------------------- # @@ -115,36 +137,43 @@ def process_patch( if not os.path.exists(patch_path): raise DiffError("Patch path {patch_path} does not exist.") + ### TODO might need to update this to process multiple patches in line + if root_path is not None: root_path = pathlib.Path(root_path) # Normalize line endings before processing - text = open_fn(patch_path) - - # FIX: Check for existence of files to be added before parsing. - paths_to_add = identify_files_added(text) - for p in paths_to_add: - if root_path is not None: - p = str(root_path / p) - if exists_fn(p): - raise DiffError(f"Add File Error - file already exists: {p}") - - paths_needed = identify_files_needed(text) - - # Load files with normalized line endings - orig_files = {} - for path in paths_needed: - if root_path is not None: - path = str(root_path / path) - orig_files[path] = open_fn(path) - - patch, _fuzz = text_to_patch(text, orig_files, rootpath=root_path) - commit = patch_to_commit(patch, orig_files) - - apply_commit(commit, write_fn, remove_fn, exists_fn) - - remove_fn(patch_path) - return paths_needed + patches_text = open_fn(patch_path) + patches = parse_patch_blocks(patches_text) + + all_paths_needed = [] + for text in patches: + # FIX: Check for existence of files to be added before parsing. + paths_to_add = identify_files_added(text) + for p in paths_to_add: + if root_path is not None: + p = str(root_path / p) + if exists_fn(p): + raise DiffError(f"Add File Error - file already exists: {p}") + + paths_needed = identify_files_needed(text) + all_paths_needed.extend(paths_needed) + + # Load files with normalized line endings + orig_files = {} + for path in paths_needed: + if root_path is not None: + path = str(root_path / path) + orig_files[path] = open_fn(path) + + patch, _fuzz = text_to_patch(text, orig_files, rootpath=root_path) + commit = patch_to_commit(patch, orig_files) + + apply_commit(commit, write_fn, remove_fn, exists_fn) + + remove_fn(patch_path) + + return all_paths_needed # --------------------------------------------------------------------------- # # Default FS wrappers From 3298ffdde1545aa558d211bc76f7cf8cc69b38ce Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 20:28:27 +0100 Subject: [PATCH 16/28] fixed to allow new files to pass through --- codetide/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codetide/__init__.py b/codetide/__init__.py index e08f241..8674046 100644 --- a/codetide/__init__.py +++ b/codetide/__init__.py @@ -589,5 +589,7 @@ def _as_file_paths(self, code_identifiers: Union[str, List[str]])->List[str]: as_file_paths.append(code_identifier) elif element := self.codebase.cached_elements.get(code_identifier): as_file_paths.append(element.file_path) + else: ### covers new files + as_file_paths.append(element) return as_file_paths From cfa5c0d7af0b697e4f1ecaa23efdcfd384d2b2f9 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 20:28:45 +0100 Subject: [PATCH 17/28] minor improvements promtps --- codetide/agents/tide/prompts.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index afe8551..cc3bc7b 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -69,9 +69,8 @@ - No valid or relevant code identifiers can be determined for the request. 4. If the user refers to a file by name or path and the request is about code elements within that file, extract and include the relevant code identifiers from that file instead of the file path, unless the user specifically asks for the file path. 5. If fulfilling the request would likely depend on additional symbols or files—based on naming, structure, required context from other files/modules, or conventional design patterns—include those code identifiers as context identifiers. -6. Only include identifiers or2025-08-30 18:45:26 - Translation file for pt-PT not found. Using default translation en-US. - paths that are present in the provided tree structure. Never fabricate or guess paths or names that do not exist. -7. If no relevant code identifiers or file paths can be confidently identified, leave the relevant section(s) empty. +6. Only include identifiers or paths that are present in the provided tree structure. Never fabricate or guess paths or names that do not exist. +7. If no relevant code identifiers or file paths can be confidently identified, leave the relevant section(s) empty - without any contents or lines, not even the word empty. --- @@ -324,7 +323,7 @@ 4. **Granularity:** Break complex requirements into logical sub-steps. Order them so dependencies are respected (e.g., setup → implementation → validation → integration). -5. **Traceability:** Each step’s `context_identifiers` and `modify_identifiers` must clearly tie that step to specific code areas; this enables downstream mapping to actual implementation targets. +5. **Traceability:** Each step's `context_identifiers` and `modify_identifiers` must clearly tie that step to specific code areas; this enables downstream mapping to actual implementation targets. 6. **Single-Responsibility per Step:** Aim for each numbered step to encapsulate a coherent unit of work. Avoid mixing unrelated concerns in one step. @@ -332,7 +331,7 @@ 8. **Testing & Validation:** Where appropriate, include in steps the need for testing, how to validate success, and any edge cases to cover. -9. **Failure Modes & Corrections:** If the user’s request implies potential pitfalls (e.g., backward compatibility, race conditions, security), surface those in early steps or in the comments and include remediation as part of the plan. +9. **Failure Modes & Corrections:** If the use's request implies potential pitfalls (e.g., backward compatibility, race conditions, security), surface those in early steps or in the comments and include remediation as part of the plan. 10. **Succinctness of Format:** Strictly adhere to the step formatting with separators (`---`) and the beginning/end markers. Do not add extraneous numbering or narrative outside the prescribed structure. From 67c8deea5a23fb5998b30ed4bf789b49037b2fa5 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 20:29:47 +0100 Subject: [PATCH 18/28] improved loading message / reasoning message robustness --- codetide/agents/tide/agent.py | 2 +- codetide/agents/tide/ui/app.py | 50 +++++++++++++++++++--------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index 506fc97..b618aa3 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -126,7 +126,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): self.contextIdentifiers = contextIdentifiers.splitlines() or None self.modifyIdentifiers = modifyIdentifiers.splitlines() or None - codeIdentifiers = self.contextIdentifiers + codeIdentifiers = self.contextIdentifiers or [] if self.modifyIdentifiers: codeIdentifiers.extend(self.tide._as_file_paths(self.modifyIdentifiers)) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index 1363256..f117c1d 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -263,6 +263,27 @@ async def on_inspect_context(action :cl.Action): await inspect_msg.send() +async def send_reasoning_msg(loading_msg :cl.message, context_msg :cl.Message, agent_tide_ui :AgentTideUi, st :float)->bool: + await loading_msg.remove() + + context_data = { + key: value for key in ["contextIdentifiers", "modifyIdentifiers"] + if (value := getattr(agent_tide_ui.agent_tide, key, None)) + } + context_msg.elements.append( + cl.CustomElement( + name="ReasoningMessage", + props={ + "reasoning": agent_tide_ui.agent_tide.reasoning, + "data": context_data, + "title": f"Thought for {time.time()-st:.2f} seconds", + "defaultExpanded": False, + "showControls": False + } + ) + ) + await context_msg.send() + return True @cl.on_message async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Optional[list] = None, agent_tide_ui :Optional[AgentTideUi]=None): @@ -278,7 +299,7 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option "showIcon": True } ) - ] + ] ).send() if agent_tide_ui is None: @@ -329,31 +350,16 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option ) st = time.time() + is_reasonig_sent = False async for chunk in run_concurrent_tasks(agent_tide_ui, codeIdentifiers): if chunk == STREAM_START_TOKEN: - await loading_msg.remove() - - context_data = { - key: value for key in ["contextIdentifiers", "modifyIdentifiers"] - if (value := getattr(agent_tide_ui.agent_tide, key, None)) - } - context_msg.elements.append( - cl.CustomElement( - name="ReasoningMessage", - props={ - "reasoning": agent_tide_ui.agent_tide.reasoning, - "data": context_data, - "title": f"Thought for {time.time()-st:.2f} seconds", - "defaultExpanded": False, - "showControls": False - } - ) - ) - await context_msg.send() - + is_reasonig_sent = await send_reasoning_msg(loading_msg, context_msg, agent_tide_ui, st) continue - if chunk == STREAM_END_TOKEN: + elif not is_reasonig_sent: + is_reasonig_sent = await send_reasoning_msg(loading_msg, context_msg, agent_tide_ui, st) + + elif chunk == STREAM_END_TOKEN: # Handle any remaining content await stream_processor.finalize() break From 7ac1c3890b5e95129bdd3db3fc15fd68cecc7173 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 20:46:09 +0100 Subject: [PATCH 19/28] included added path in all_paths_needed for process_patch changed files tracking --- codetide/mcp/tools/patch_code/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/codetide/mcp/tools/patch_code/__init__.py b/codetide/mcp/tools/patch_code/__init__.py index 12550e5..162d91a 100644 --- a/codetide/mcp/tools/patch_code/__init__.py +++ b/codetide/mcp/tools/patch_code/__init__.py @@ -157,6 +157,7 @@ def process_patch( raise DiffError(f"Add File Error - file already exists: {p}") paths_needed = identify_files_needed(text) + all_paths_needed.extend(paths_to_add) all_paths_needed.extend(paths_needed) # Load files with normalized line endings From 3c1acdb1be7331431e5e682ea96f20deae48459c Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 22:01:23 +0100 Subject: [PATCH 20/28] update CMD_COMMIT_PROMPT --- codetide/agents/tide/prompts.py | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index cc3bc7b..6555da2 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -332,17 +332,8 @@ 8. **Testing & Validation:** Where appropriate, include in steps the need for testing, how to validate success, and any edge cases to cover. 9. **Failure Modes & Corrections:** If the use's request implies potential pitfalls (e.g., backward compatibility, race conditions, security), surface those in early steps or in the comments and include remediation as part of the plan. - -10. **Succinctness of Format:** Strictly adhere to the step formatting with separators (`---`) and the beginning/end markers. Do not add extraneous numbering or narrative outside the prescribed structure. - ---- - -`repo_tree` -{REPO_TREE} -""" - -CMD_TRIGGER_PLANNING_STEPS = """ -You must operate in a multi-step planning and execution mode: first outline the plan step by step in a sequential way, then ask for my revision. +steps or in the comments and include remediation as part of the plan. +: first outline the plan step by step in a sequential way, then ask for my revision. Do not start implementing the steps without my approval. """ @@ -358,18 +349,17 @@ CMD_COMMIT_PROMPT = """ Generate a conventional commit message that summarizes the work done since the previous commit. -The message should have a clear subject line and a body explaining the problem solved and the implementation approach. - -Important Instructions: -Place the commit message inside exactly this format: -*** Begin Commit -[commit message] -*** End Commit - -You may include additional comments about the changes made outside of this block +**Instructions:** -If no diffs for staged files are provided in the context, reply that there's nothing to commit +1. First, write a body (before the commit block) that explains the problem solved and the implementation approach. This should be clear, concise, and provide context for the change. +2. Then, place the commit subject line (only) inside the commit block, using this format: + *** Begin Commit + [subject line only, up to 3 lines, straight to the point and descriptive of the broad changes] + *** End Commit +3. The subject line should follow the conventional commit format with a clear type/scope prefix, and summarize the broad changes made. Do not include the body or any explanation inside the commit block—only the subject line. +4. You may include additional comments about the changes made outside of this block, if needed. +5. If no diffs for staged files are provided in the context, reply that there's nothing to commit.context, reply that there's nothing to commit The commit message should follow conventional commit format with a clear type/scope prefix """ From 45fe168a0aa3d62941a1ec761dcb39f19b3a935d Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 22:09:33 +0100 Subject: [PATCH 21/28] updated hf example app to latest spec --- codetide/agents/tide/ui/app.py | 2 ++ examples/hf_demo_space/app.py | 34 +++++++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/codetide/agents/tide/ui/app.py b/codetide/agents/tide/ui/app.py index f117c1d..dc16624 100644 --- a/codetide/agents/tide/ui/app.py +++ b/codetide/agents/tide/ui/app.py @@ -173,6 +173,7 @@ async def on_execute_steps(action :cl.Action): latest_step_message :cl.Message = cl.user_session.get("latest_step_message") if latest_step_message and latest_step_message.id == action.payload.get("msg_id"): await latest_step_message.remove_actions() + await latest_step_message.send() # close message ? if agent_tide_ui.current_step is None: task_list = cl.TaskList("Steps") @@ -226,6 +227,7 @@ async def on_stop_steps(action :cl.Action): latest_step_message :cl.Message = cl.user_session.get("latest_step_message") if latest_step_message and latest_step_message.id == action.payload.get("msg_id"): await latest_step_message.remove_actions() + await latest_step_message.send() # close message ? task_list = cl.user_session.get("StepsTaskList") if task_list: diff --git a/examples/hf_demo_space/app.py b/examples/hf_demo_space/app.py index c39892c..bc3bc69 100644 --- a/examples/hf_demo_space/app.py +++ b/examples/hf_demo_space/app.py @@ -9,6 +9,7 @@ from codetide.agents.tide.ui.stream_processor import StreamProcessor, MarkerConfig from codetide.agents.tide.ui.utils import run_concurrent_tasks from codetide.agents.tide.ui.agent_tide_ui import AgentTideUi +from codetide.agents.tide.ui.app import send_reasoning_msg from codetide.core.defaults import DEFAULT_ENCODING from codetide.core.logs import logger from codetide.agents.tide.models import Step @@ -30,12 +31,13 @@ import json import stat import yaml +import time import os DEFAULT_SESSIONS_WORKSPACE = Path(os.getcwd()) / "sessions" @cl.on_chat_start -async def start_chatr(): +async def start_chat(): session_id = ulid() cl.user_session.set("session_id", session_id) await cl.context.emitter.set_commands(AgentTideUi.commands) @@ -199,6 +201,7 @@ async def on_execute_steps(action :cl.Action): latest_step_message :cl.Message = cl.user_session.get("latest_step_message") if latest_step_message and latest_step_message.id == action.payload.get("msg_id"): await latest_step_message.remove_actions() + await latest_step_message.send() # close message ? if agent_tide_ui.current_step is None: task_list = cl.TaskList("Steps") @@ -240,7 +243,7 @@ async def on_execute_steps(action :cl.Action): author="Agent Tide" ).send() - await agent_loop(step_instructions_msg, codeIdentifiers=step.context_identifiers, agent_tide_ui=agent_tide_ui) + await agent_loop(step_instructions_msg, codeIdentifiers=step.get_code_identifiers(agent_tide_ui.agent_tide.tide._as_file_paths), agent_tide_ui=agent_tide_ui) task_list.status = f"Waiting feedback on step {current_task_idx}" await task_list.send() @@ -252,6 +255,7 @@ async def on_stop_steps(action :cl.Action): latest_step_message :cl.Message = cl.user_session.get("latest_step_message") if latest_step_message and latest_step_message.id == action.payload.get("msg_id"): await latest_step_message.remove_actions() + await latest_step_message.send() # close message ? task_list = cl.user_session.get("StepsTaskList") if task_list: @@ -296,6 +300,21 @@ async def on_inspect_context(action :cl.Action): @cl.on_message async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Optional[list] = None, agent_tide_ui :Optional[AgentTideUi]=None): + + loading_msg = await cl.Message( + content="", + elements=[ + cl.CustomElement( + name="LoadingMessage", + props={ + "messages": ["Working", "Syncing CodeTide", "Thinking", "Looking for context"], + "interval": 1500, # 1.5 seconds between messages + "showIcon": True + } + ) + ] + ).send() + if agent_tide_ui is None: agent_tide_ui = cl.user_session.get("AgentTideUi") @@ -310,9 +329,8 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option chat_history.append({"role": "user", "content": message.content}) await agent_tide_ui.add_to_history(message.content) - + context_msg = cl.Message(content="", author="AgentTide") msg = cl.Message(content="", author="Agent Tide") - async with cl.Step("ApplyPatch", type="tool") as diff_step: await diff_step.remove() @@ -344,11 +362,17 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option global_fallback_msg=msg ) + st = time.time() + is_reasonig_sent = False async for chunk in run_concurrent_tasks(agent_tide_ui, codeIdentifiers): if chunk == STREAM_START_TOKEN: + is_reasonig_sent = await send_reasoning_msg(loading_msg, context_msg, agent_tide_ui, st) continue - if chunk == STREAM_END_TOKEN: + elif not is_reasonig_sent: + is_reasonig_sent = await send_reasoning_msg(loading_msg, context_msg, agent_tide_ui, st) + + elif chunk == STREAM_END_TOKEN: # Handle any remaining content await stream_processor.finalize() break From eb1852004c73cd5825cd6a75454ff0ce0ef1cad3 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 22:17:19 +0100 Subject: [PATCH 22/28] added None protection --- codetide/mcp/tools/patch_code/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codetide/mcp/tools/patch_code/__init__.py b/codetide/mcp/tools/patch_code/__init__.py index 162d91a..946aba4 100644 --- a/codetide/mcp/tools/patch_code/__init__.py +++ b/codetide/mcp/tools/patch_code/__init__.py @@ -144,7 +144,7 @@ def process_patch( # Normalize line endings before processing patches_text = open_fn(patch_path) - patches = parse_patch_blocks(patches_text) + patches = parse_patch_blocks(patches_text) or [] all_paths_needed = [] for text in patches: From a577657dbc1817d8c2dc5d5f9c671ccfa33df4d6 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 22:20:13 +0100 Subject: [PATCH 23/28] updated protection --- codetide/mcp/tools/patch_code/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codetide/mcp/tools/patch_code/__init__.py b/codetide/mcp/tools/patch_code/__init__.py index 946aba4..5cd795a 100644 --- a/codetide/mcp/tools/patch_code/__init__.py +++ b/codetide/mcp/tools/patch_code/__init__.py @@ -144,7 +144,7 @@ def process_patch( # Normalize line endings before processing patches_text = open_fn(patch_path) - patches = parse_patch_blocks(patches_text) or [] + patches = parse_patch_blocks(patches_text) or [""] all_paths_needed = [] for text in patches: From 4f0165715f311d07d76c366912f92103e2e558ab Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 22:31:44 +0100 Subject: [PATCH 24/28] improved parse_patch blocks robustness --- codetide/mcp/tools/patch_code/__init__.py | 79 +++++++++++++++++++++-- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/codetide/mcp/tools/patch_code/__init__.py b/codetide/mcp/tools/patch_code/__init__.py index 5cd795a..fe47591 100644 --- a/codetide/mcp/tools/patch_code/__init__.py +++ b/codetide/mcp/tools/patch_code/__init__.py @@ -12,6 +12,10 @@ def parse_patch_blocks(text: str, multiple: bool = True) -> Union[str, List[str] """ Extract content between *** Begin Patch and *** End Patch markers (inclusive), ensuring that both markers are at zero indentation (start of line, no leading spaces). + + If only one identifier is present: + - If only "Begin Patch" exists: returns from Begin Patch to end of text + - If only "End Patch" exists: returns from start of text to End Patch Args: text: Full input text containing one or more patch blocks. @@ -21,13 +25,72 @@ def parse_patch_blocks(text: str, multiple: bool = True) -> Union[str, List[str] A string (single patch), list of strings (multiple patches), or None if not found. """ - pattern = r"(?m)^(\*\*\* Begin Patch[\s\S]*?^\*\*\* End Patch)$" - matches = re.findall(pattern, text) - - if not matches: + # First, try to find complete blocks (both Begin and End markers) + complete_pattern = r"(?m)^(\*\*\* Begin Patch[\s\S]*?^\*\*\* End Patch)$" + complete_matches = re.findall(complete_pattern, text) + + # If we found complete matches, return them (preserving original behavior) + if complete_matches: + return complete_matches if multiple else complete_matches[0] + + # If no complete matches, look for partial identifiers + begin_pattern = r"(?m)^(\*\*\* Begin Patch).*$" + end_pattern = r"(?m)^(\*\*\* End Patch).*$" + + begin_matches = list(re.finditer(begin_pattern, text)) + end_matches = list(re.finditer(end_pattern, text)) + + partial_matches = [] + + # Handle cases with only Begin markers (from Begin to end of text) + if begin_matches and not end_matches: + for match in begin_matches: + start_pos = match.start() + partial_content = text[start_pos:] + partial_matches.append(partial_content) + + # Handle cases with only End markers (from start of text to End) + elif end_matches and not begin_matches: + for match in end_matches: + end_pos = match.end() + partial_content = text[:end_pos] + partial_matches.append(partial_content) + + # Handle mixed cases (some begins without ends, some ends without begins) + elif begin_matches or end_matches: + # Get all Begin positions + begin_positions = [m.start() for m in begin_matches] + end_positions = [m.end() for m in end_matches] + + # For each Begin, try to find corresponding End + for begin_pos in begin_positions: + corresponding_end = None + for end_pos in end_positions: + if end_pos > begin_pos: + corresponding_end = end_pos + break + + if corresponding_end: + # Complete pair found + partial_content = text[begin_pos:corresponding_end] + else: + # Begin without End - go to end of text + partial_content = text[begin_pos:] + + partial_matches.append(partial_content) + + # Handle orphaned End markers (Ends that don't have corresponding Begins) + for end_pos in end_positions: + has_corresponding_begin = any(begin_pos < end_pos for begin_pos in begin_positions) + if not has_corresponding_begin: + # End without Begin - from start of text + partial_content = text[:end_pos] + partial_matches.append(partial_content) + + if not partial_matches: return None - - return matches if multiple else matches[0] + + return partial_matches if multiple else partial_matches[0] # --------------------------------------------------------------------------- # # User-facing API @@ -144,7 +207,9 @@ def process_patch( # Normalize line endings before processing patches_text = open_fn(patch_path) - patches = parse_patch_blocks(patches_text) or [""] + print(f"{patches_text=}") + patches = parse_patch_blocks(patches_text)#or [""] + print(f"{patches=}") all_paths_needed = [] for text in patches: From 6754ae5f3e451cb9de4b7a52fbb256c594462a3f Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 22:37:56 +0100 Subject: [PATCH 25/28] fixed planning prompts --- codetide/agents/tide/prompts.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/prompts.py b/codetide/agents/tide/prompts.py index 6555da2..37cb23f 100644 --- a/codetide/agents/tide/prompts.py +++ b/codetide/agents/tide/prompts.py @@ -332,8 +332,12 @@ 8. **Testing & Validation:** Where appropriate, include in steps the need for testing, how to validate success, and any edge cases to cover. 9. **Failure Modes & Corrections:** If the use's request implies potential pitfalls (e.g., backward compatibility, race conditions, security), surface those in early steps or in the comments and include remediation as part of the plan. -steps or in the comments and include remediation as part of the plan. -: first outline the plan step by step in a sequential way, then ask for my revision. + +10. **Succinctness of Format:** Strictly adhere to the step formatting with separators (`---`) and the beginning/end markers. Do not add extraneous numbering or narrative outside the prescribed structure. +""" + +CMD_TRIGGER_PLANNING_STEPS = """ +You must operate in a multi-step planning and execution mode: first outline the plan step by step in a sequential way, then ask for my revision. Do not start implementing the steps without my approval. """ From 2d295eba93ab99590ae3eb4347af671506ddc9eb Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 22:48:43 +0100 Subject: [PATCH 26/28] adde dprotection --- codetide/agents/tide/agent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codetide/agents/tide/agent.py b/codetide/agents/tide/agent.py index b618aa3..ece0e67 100644 --- a/codetide/agents/tide/agent.py +++ b/codetide/agents/tide/agent.py @@ -124,8 +124,8 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None): reasoning = [context_response] self.reasoning = reasoning[0].strip() - self.contextIdentifiers = contextIdentifiers.splitlines() or None - self.modifyIdentifiers = modifyIdentifiers.splitlines() or None + self.contextIdentifiers = contextIdentifiers.splitlines() if isinstance(contextIdentifiers, str) else None + self.modifyIdentifiers = modifyIdentifiers.splitlines() if isinstance(modifyIdentifiers, str) else None codeIdentifiers = self.contextIdentifiers or [] if self.modifyIdentifiers: From a6fda2455ee8a486f179a6d7e743161c45fefeac Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 23:13:54 +0100 Subject: [PATCH 27/28] improved_parse_steps_markdown --- codetide/agents/tide/utils.py | 59 +++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/codetide/agents/tide/utils.py b/codetide/agents/tide/utils.py index 98a712b..5116532 100644 --- a/codetide/agents/tide/utils.py +++ b/codetide/agents/tide/utils.py @@ -65,36 +65,49 @@ def parse_blocks(text: str, block_word: str = "Commit", multiple: bool = True) - return matches[0].strip() def parse_steps_markdown(md: str): - """ - Parse the markdown steps block and return a list of step dicts. - Now supports both context_identifiers and modify_identifiers. - """ steps = [] - step_blocks = re.split(r'---\s*', md) - for block in step_blocks: - block = block.strip() - if not block or block.startswith("*** End Steps"): - continue - # Parse step number and description - m = re.match( - r'(\d+)\.\s+\*\*(.*?)\*\*\s*\n\s*\*\*instructions\*\*:\s*(.*?)\n\s*\*\*context_identifiers\*\*:\s*((?:- .*\n?)*)\s*\*\*modify_identifiers\*\*:\s*((?:- .*\n?)*)', - block, re.DOTALL) - if not m: + + # Extract only content between *** Begin Steps and *** End Steps + match = re.search(r"\*\*\* Begin Steps(.*?)\*\*\* End Steps", md, re.DOTALL) + if not match: + return [] + + steps_block = match.group(1).strip() + + # Split steps by '---' + raw_steps = [s.strip() for s in steps_block.split('---') if s.strip()] + + for raw_step in raw_steps: + # Match step number and description + step_header = re.match(r"(\d+)\.\s+\*\*(.*?)\*\*", raw_step) + if not step_header: continue - step_num = int(m.group(1)) - description = m.group(2).strip() - instructions = m.group(3).strip() - context_block = m.group(4) - modify_block = m.group(5) - context_identifiers = [line[2:].strip() for line in context_block.strip().splitlines() if line.strip().startswith('-')] - modify_identifiers = [line[2:].strip() for line in modify_block.strip().splitlines() if line.strip().startswith('-')] + + step_num = int(step_header.group(1)) + description = step_header.group(2).strip() + + # Match instructions + instructions_match = re.search(r"\*\*instructions\*\*:\s*(.*?)(?=\*\*context_identifiers\*\*:)", raw_step, re.DOTALL) + instructions = instructions_match.group(1).strip() if instructions_match else "" + + # Match context identifiers + context_match = re.search(r"\*\*context_identifiers\*\*:\s*(.*?)(?=\*\*modify_identifiers\*\*:)", raw_step, re.DOTALL) + context_block = context_match.group(1).strip() if context_match else "" + context_identifiers = re.findall(r"- (.+)", context_block) + + # Match modifying identifiers + modify_match = re.search(r"\*\*modify_identifiers\*\*:\s*(.*)", raw_step, re.DOTALL) + modify_match = modify_match.group(1).strip() if modify_match else "" + modify_identifiers = re.findall(r"- (.+)", modify_match) + steps.append({ "step": step_num, "description": description, "instructions": instructions, - "context_identifiers": context_identifiers, - "modify_identifiers": modify_identifiers + "context_identifiers": [identifier.strip() for identifier in context_identifiers], + "modify_identifiers": [identifier.strip() for identifier in modify_identifiers] }) + return steps async def delete_file(file_path: str) -> bool: From 3cabf96a7e99ad0310464377df7708cf6b7eec10 Mon Sep 17 00:00:00 2001 From: BrunoV21 Date: Sun, 31 Aug 2025 23:14:18 +0100 Subject: [PATCH 28/28] updated / fadded tests --- .../tide/test_utils_parse_steps_markdown.py | 135 +++++++++++++++++- 1 file changed, 131 insertions(+), 4 deletions(-) diff --git a/tests/agents/tide/test_utils_parse_steps_markdown.py b/tests/agents/tide/test_utils_parse_steps_markdown.py index 2da869d..b7e51c6 100644 --- a/tests/agents/tide/test_utils_parse_steps_markdown.py +++ b/tests/agents/tide/test_utils_parse_steps_markdown.py @@ -12,6 +12,9 @@ def generate_steps_md(n): **context_identifiers**: - module.{i} - path/to/file_{i}.py + **modify_identifiers**: + - output/result_{i}.py + - config/settings_{i}.json ---""") steps_md.append("*** End Steps") return "\n".join(steps_md) @@ -25,23 +28,36 @@ def test_parse_multiple_steps(): **context_identifiers**: - db.init_schema - config/db.yaml + **modify_identifiers**: + - database/schema.sql + - config/database.ini --- 1. **Load seed data** **instructions**: Populate tables with initial records from seed_data.json **context_identifiers**: - db.load_seed - data/seed_data.json + **modify_identifiers**: + - database/populated.db + - logs/seed_import.log --- *** End Steps """ steps = parse_steps_markdown(md) + assert len(steps) == 2 + + # Test step 0 assert steps[0]["step"] == 0 assert steps[0]["description"] == "Initialize database" assert "SQLite database" in steps[0]["instructions"] assert steps[0]["context_identifiers"] == ["db.init_schema", "config/db.yaml"] + assert steps[0]["modify_identifiers"] == ["database/schema.sql", "config/database.ini"] + + # Test step 1 assert steps[1]["step"] == 1 assert steps[1]["context_identifiers"][1] == "data/seed_data.json" + assert steps[1]["modify_identifiers"] == ["database/populated.db", "logs/seed_import.log"] def test_single_step(): md = """ @@ -50,6 +66,8 @@ def test_single_step(): **instructions**: Perform all required tasks in a single step. **context_identifiers**: - utils.main_handler + **modify_identifiers**: + - output/final_result.txt --- *** End Steps """ @@ -59,27 +77,62 @@ def test_single_step(): assert steps[0]["description"] == "Do everything" assert "single step" in steps[0]["instructions"] assert steps[0]["context_identifiers"] == ["utils.main_handler"] + assert steps[0]["modify_identifiers"] == ["output/final_result.txt"] -def test_missing_context_identifiers(): +def test_empty_identifiers(): md = """ *** Begin Steps - 0. **No context** + 0. **No identifiers** **instructions**: Just explain something here **context_identifiers**: + **modify_identifiers**: --- *** End Steps """ steps = parse_steps_markdown(md) assert len(steps) == 1 assert steps[0]["context_identifiers"] == [] + assert steps[0]["modify_identifiers"] == [] + +def test_partial_empty_identifiers(): + md = """ + *** Begin Steps + 0. **Only context** + **instructions**: Has context but no modify identifiers + **context_identifiers**: + - some.module + - some/file.py + **modify_identifiers**: + --- + 1. **Only modify** + **instructions**: Has modify but no context identifiers + **context_identifiers**: + **modify_identifiers**: + - output/result.json + - logs/activity.log + --- + *** End Steps + """ + steps = parse_steps_markdown(md) + assert len(steps) == 2 + + # Step with only context + assert steps[0]["context_identifiers"] == ["some.module", "some/file.py"] + assert steps[0]["modify_identifiers"] == [] + + # Step with only modify + assert steps[1]["context_identifiers"] == [] + assert steps[1]["modify_identifiers"] == ["output/result.json", "logs/activity.log"] def test_malformed_but_parsable_step(): md = """ *** Begin Steps 0. **Incomplete step** - **instructions**: This has no context identifiers + **instructions**: This has empty identifier lines **context_identifiers**: - + **modify_identifiers**: + - --- *** End Steps """ @@ -87,8 +140,9 @@ def test_malformed_but_parsable_step(): assert len(steps) == 1 assert steps[0]["description"] == "Incomplete step" assert steps[0]["context_identifiers"] == [] + assert steps[0]["modify_identifiers"] == [] -def test_multiple_hyphen_indented_contexts(): +def test_multiple_hyphen_indented_identifiers(): md = """ *** Begin Steps 0. **Handle multi-line** @@ -97,12 +151,40 @@ def test_multiple_hyphen_indented_contexts(): - module.first - module.second - module.third + **modify_identifiers**: + - output/first.py + - output/second.py + - output/third.py + - logs/process.log --- *** End Steps """ steps = parse_steps_markdown(md) assert len(steps) == 1 assert steps[0]["context_identifiers"] == ["module.first", "module.second", "module.third"] + assert steps[0]["modify_identifiers"] == ["output/first.py", "output/second.py", "output/third.py", "logs/process.log"] + +def test_mixed_whitespace_handling(): + md = """ + *** Begin Steps + 0. **Whitespace test** + **instructions**: Test various whitespace scenarios + **context_identifiers**: + - module.with.spaces + - indented.module + - normal.module + **modify_identifiers**: + - output/spaced.file + - output/indented.file + - output/normal.file + --- + *** End Steps + """ + steps = parse_steps_markdown(md) + assert len(steps) == 1 + # Should strip whitespace from identifier names + assert steps[0]["context_identifiers"] == ["module.with.spaces", "indented.module", "normal.module"] + assert steps[0]["modify_identifiers"] == ["output/spaced.file", "output/indented.file", "output/normal.file"] @pytest.mark.parametrize("count", [5, 10, 50]) def test_large_number_of_steps(count): @@ -116,3 +198,48 @@ def test_large_number_of_steps(count): assert step["description"] == f"Step {i} description" assert f"task {i}" in step["instructions"] assert step["context_identifiers"] == [f"module.{i}", f"path/to/file_{i}.py"] + assert step["modify_identifiers"] == [f"output/result_{i}.py", f"config/settings_{i}.json"] + +def test_complex_real_world_example(): + """Test with a more realistic example that might be seen in practice.""" + md = """ + *** Begin Steps + 0. **Setup authentication system** + **instructions**: Create user authentication with JWT tokens and bcrypt password hashing + **context_identifiers**: + - auth.models.User + - auth.utils.jwt_helper + - config/security.yaml + - requirements.txt + **modify_identifiers**: + - auth/models.py + - auth/views.py + - auth/serializers.py + - config/settings.py + --- + 1. **Implement API endpoints** + **instructions**: Create REST API endpoints for user registration, login, and profile management + **context_identifiers**: + - auth.models.User + - api.base_views + - docs/api_spec.md + **modify_identifiers**: + - api/auth_views.py + - api/urls.py + - tests/test_auth_api.py + --- + *** End Steps + """ + steps = parse_steps_markdown(md) + assert len(steps) == 2 + + # Verify complex step structure + assert steps[0]["description"] == "Setup authentication system" + assert "JWT tokens" in steps[0]["instructions"] + assert len(steps[0]["context_identifiers"]) == 4 + assert len(steps[0]["modify_identifiers"]) == 4 + + assert steps[1]["description"] == "Implement API endpoints" + assert "REST API" in steps[1]["instructions"] + assert len(steps[1]["context_identifiers"]) == 3 + assert len(steps[1]["modify_identifiers"]) == 3 \ No newline at end of file