Skip to content

Commit 792ac24

Browse files
authored
Merge pull request #23 from BrunoV21/process-patch
Process patch
2 parents ac91a8a + 3cabf96 commit 792ac24

File tree

13 files changed

+701
-106
lines changed

13 files changed

+701
-106
lines changed

codetide/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,4 +577,19 @@ def get(
577577
as_string=as_string,
578578
as_list_str=as_string_list,
579579
preloaded_files=requested_files
580-
)
580+
)
581+
582+
def _as_file_paths(self, code_identifiers: Union[str, List[str]])->List[str]:
583+
if isinstance(code_identifiers, str):
584+
code_identifiers = [code_identifiers]
585+
586+
as_file_paths = []
587+
for code_identifier in code_identifiers:
588+
if self.rootpath / code_identifier in self.files:
589+
as_file_paths.append(code_identifier)
590+
elif element := self.codebase.cached_elements.get(code_identifier):
591+
as_file_paths.append(element.file_path)
592+
else: ### covers new files
593+
as_file_paths.append(element)
594+
595+
return as_file_paths

codetide/agents/tide/agent.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
from functools import partial
22
from codetide import CodeTide
3-
from ...mcp.tools.patch_code import file_exists, open_file, process_patch, remove_file, write_file
3+
from ...mcp.tools.patch_code import file_exists, open_file, process_patch, remove_file, write_file, parse_patch_blocks
44
from ...core.defaults import DEFAULT_ENCODING, DEFAULT_STORAGE_PATH
55
from ...autocomplete import AutoComplete
66
from .models import Steps
77
from .prompts import (
88
AGENT_TIDE_SYSTEM_PROMPT, GET_CODE_IDENTIFIERS_SYSTEM_PROMPT, REJECT_PATCH_FEEDBACK_TEMPLATE,
99
STAGED_DIFFS_TEMPLATE, STEPS_SYSTEM_PROMPT, WRITE_PATCH_SYSTEM_PROMPT
1010
)
11-
from .utils import delete_file, parse_commit_blocks, parse_patch_blocks, parse_steps_markdown, trim_to_patch_section
11+
from .utils import delete_file, parse_blocks, parse_steps_markdown, trim_to_patch_section
1212
from .consts import AGENT_TIDE_ASCII_ART
1313

1414
try:
@@ -49,6 +49,10 @@ class AgentTide(BaseModel):
4949
changed_paths :List[str]=Field(default_factory=list)
5050
request_human_confirmation :bool=False
5151

52+
contextIdentifiers :Optional[List[str]]=None
53+
modifyIdentifiers :Optional[List[str]]=None
54+
reasoning :Optional[str]=None
55+
5256
_skip_context_retrieval :bool=False
5357
_last_code_identifers :Optional[Set[str]]=set()
5458
_last_code_context :Optional[str] = None
@@ -104,14 +108,29 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None):
104108
)
105109

106110
if codeIdentifiers is None and not self._skip_context_retrieval:
107-
codeIdentifiers = await self.llm.acomplete(
111+
context_response = await self.llm.acomplete(
108112
self.history,
109113
system_prompt=[GET_CODE_IDENTIFIERS_SYSTEM_PROMPT.format(DATE=TODAY)],
110114
prefix_prompt=repo_tree,
111-
stream=False,
112-
json_output=True
115+
stream=False
116+
# json_output=True
113117
)
114118

119+
contextIdentifiers = parse_blocks(context_response, block_word="Context Identifiers", multiple=False)
120+
modifyIdentifiers = parse_blocks(context_response, block_word="Modify Identifiers", multiple=False)
121+
122+
reasoning = context_response.split("*** Begin")
123+
if not reasoning:
124+
reasoning = [context_response]
125+
self.reasoning = reasoning[0].strip()
126+
127+
self.contextIdentifiers = contextIdentifiers.splitlines() if isinstance(contextIdentifiers, str) else None
128+
self.modifyIdentifiers = modifyIdentifiers.splitlines() if isinstance(modifyIdentifiers, str) else None
129+
codeIdentifiers = self.contextIdentifiers or []
130+
131+
if self.modifyIdentifiers:
132+
codeIdentifiers.extend(self.tide._as_file_paths(self.modifyIdentifiers))
133+
115134
codeContext = None
116135
if codeIdentifiers:
117136
autocomplete = AutoComplete(self.tide.cached_ids)
@@ -144,7 +163,7 @@ async def agent_loop(self, codeIdentifiers :Optional[List[str]]=None):
144163
if not self.request_human_confirmation:
145164
self.approve()
146165

147-
commitMessage = parse_commit_blocks(response, multiple=False)
166+
commitMessage = parse_blocks(response, multiple=False, block_word="Commit")
148167
if commitMessage:
149168
self.commit(commitMessage)
150169

codetide/agents/tide/models.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
1+
from typing import Callable, Dict, List, Optional
22
from pydantic import BaseModel, RootModel
3-
from typing import Dict, List, Optional
43

54
STEP_INSTRUCTION_TEMPLATE = """
65
## Step {step}:
@@ -23,6 +22,7 @@ class Step(BaseModel):
2322
description :str
2423
instructions :str
2524
context_identifiers :Optional[List[str]]=None
25+
modify_identifiers: Optional[List[str]]=None
2626

2727
def as_instruction(self)->str:
2828
return STEP_INSTRUCTION_TEMPLATE.format(
@@ -31,6 +31,14 @@ def as_instruction(self)->str:
3131
instructions=self.instructions
3232
)
3333

34+
def get_code_identifiers(self, validate_identifiers_fn :Callable)->Optional[List[str]]:
35+
code_identifiers = self.context_identifiers or []
36+
37+
if self.modify_identifiers:
38+
code_identifiers.extend(validate_identifiers_fn(code_identifiers))
39+
40+
return None or code_identifiers
41+
3442
class Steps(RootModel):
3543
root :List[Step]
3644

codetide/agents/tide/prompts.py

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -61,34 +61,45 @@
6161
**Instructions:**
6262
6363
1. Carefully read and interpret the user's request, identifying any references to files, modules, submodules, or code elements—either explicit or implied.
64-
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.
65-
3. Only include full file paths (relative to the repository root) if:
64+
2. **Segregate identifiers into two categories:**
65+
- **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.
66+
- **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.
67+
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:
6668
- The user explicitly requests file-level operations (such as adding, deleting, or renaming files), or
6769
- No valid or relevant code identifiers can be determined for the request.
6870
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.
69-
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.
71+
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.
7072
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.
71-
7. If no relevant code identifiers or file paths can be confidently identified, return an empty list.
73+
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.
7274
7375
---
7476
75-
**Output Format (Strict JSON Only):**
77+
**Output Format:**
7678
77-
Return a JSON array of strings. Each string must be:
78-
- A fully qualified code identifier using dot notation (e.g., `module.submodule.Class.method`), without file extensions, or
79-
- A valid file path relative to the repository root (only if explicitly required or no code identifiers are available).
79+
Your response must include:
8080
81-
Your output must be a pure JSON list of strings. Do **not** include any explanation, comments, or formatting outside the JSON block.
81+
1. A brief explanation (1-3 sentences) describing your reasoning and search process for selecting the identifiers.
82+
2. The following delimited sections, each containing a newline-separated list of identifiers (or left empty if none):
83+
84+
*** Begin Context Identifiers
85+
<one per line, or empty>
86+
*** End Context Identifiers
87+
88+
*** Begin Modify Identifiers
89+
<one per line, or empty>
90+
*** End Modify Identifiers
91+
92+
Do **not** include any additional commentary, formatting, or output outside these sections.
8293
8394
---
8495
8596
**Evaluation Criteria:**
8697
87-
- You must identify all code identifiers directly referenced or implied in the user request, prioritizing them over file paths.
98+
- You must identify all code identifiers directly referenced or implied in the user request, and correctly categorize them as context or modify identifiers.
8899
- You must include any internal code elements that are clearly involved or required for the task.
89100
- You must consider logical dependencies that may need to be modified together (e.g., helper modules, config files, related class methods).
90101
- 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.
91-
- You must return a clean and complete list of all relevant code identifiers and, if necessary, file paths.
102+
- You must return a clean and complete list of all relevant code identifiers and, if necessary, file paths, in the correct section.
92103
- Do not over-include; be minimal but thorough. Return only what is truly required.
93104
94105
"""
@@ -288,12 +299,16 @@
288299
1. **step_description**
289300
**instructions**: precise instructions of the task to be implemented in this step
290301
**context_identifiers**:
291-
- fully qualified code identifiers or file paths (as taken from the repo_tree) that this step touches, depends on, or must update
302+
- fully qualified code identifiers or file paths (as taken from the repo_tree) that this step depends on for context (read/reference only)
303+
**modify_identifiers**:
304+
- fully qualified code identifiers or file paths (as taken from the repo_tree) that this step will directly modify or update
292305
---
293306
2. **next_step_description**
294307
**instructions**: ...
295308
**context_identifiers**:
296309
- ...
310+
**modify_identifiers**:
311+
- ...
297312
---
298313
...
299314
*** End Steps
@@ -308,22 +323,17 @@
308323
309324
4. **Granularity:** Break complex requirements into logical sub-steps. Order them so dependencies are respected (e.g., setup → implementation → validation → integration).
310325
311-
5. **Traceability:** Each steps `context_identifiers` must clearly tie that step to specific code areas; this enables downstream mapping to actual implementation targets.
326+
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.
312327
313328
6. **Single-Responsibility per Step:** Aim for each numbered step to encapsulate a coherent unit of work. Avoid mixing unrelated concerns in one step.
314329
315330
7. **Decision Points:** If a step involves a choice or alternative, surface the options in the instructions and, if necessary, flag which you assume unless the user directs otherwise.
316331
317332
8. **Testing & Validation:** Where appropriate, include in steps the need for testing, how to validate success, and any edge cases to cover.
318333
319-
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.
334+
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.
320335
321336
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.
322-
323-
---
324-
325-
`repo_tree`
326-
{REPO_TREE}
327337
"""
328338

329339
CMD_TRIGGER_PLANNING_STEPS = """
@@ -343,18 +353,17 @@
343353

344354
CMD_COMMIT_PROMPT = """
345355
Generate a conventional commit message that summarizes the work done since the previous commit.
346-
The message should have a clear subject line and a body explaining the problem solved and the implementation approach.
347-
348-
Important Instructions:
349356
350-
Place the commit message inside exactly this format:
351-
*** Begin Commit
352-
[commit message]
353-
*** End Commit
354-
355-
You may include additional comments about the changes made outside of this block
357+
**Instructions:**
356358
357-
If no diffs for staged files are provided in the context, reply that there's nothing to commit
359+
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.
360+
2. Then, place the commit subject line (only) inside the commit block, using this format:
361+
*** Begin Commit
362+
[subject line only, up to 3 lines, straight to the point and descriptive of the broad changes]
363+
*** End Commit
364+
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.
365+
4. You may include additional comments about the changes made outside of this block, if needed.
366+
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
358367
359368
The commit message should follow conventional commit format with a clear type/scope prefix
360369
"""

codetide/agents/tide/ui/app.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import asyncio
3838
import json
3939
import yaml
40+
import time
4041

4142
@cl.password_auth_callback
4243
def auth():
@@ -172,6 +173,7 @@ async def on_execute_steps(action :cl.Action):
172173
latest_step_message :cl.Message = cl.user_session.get("latest_step_message")
173174
if latest_step_message and latest_step_message.id == action.payload.get("msg_id"):
174175
await latest_step_message.remove_actions()
176+
await latest_step_message.send() # close message ?
175177

176178
if agent_tide_ui.current_step is None:
177179
task_list = cl.TaskList("Steps")
@@ -213,7 +215,7 @@ async def on_execute_steps(action :cl.Action):
213215
author="Agent Tide"
214216
).send()
215217

216-
await agent_loop(step_instructions_msg, codeIdentifiers=step.context_identifiers, agent_tide_ui=agent_tide_ui)
218+
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)
217219

218220
task_list.status = f"Waiting feedback on step {current_task_idx}"
219221
await task_list.send()
@@ -225,6 +227,7 @@ async def on_stop_steps(action :cl.Action):
225227
latest_step_message :cl.Message = cl.user_session.get("latest_step_message")
226228
if latest_step_message and latest_step_message.id == action.payload.get("msg_id"):
227229
await latest_step_message.remove_actions()
230+
await latest_step_message.send() # close message ?
228231

229232
task_list = cl.user_session.get("StepsTaskList")
230233
if task_list:
@@ -262,9 +265,45 @@ async def on_inspect_context(action :cl.Action):
262265

263266
await inspect_msg.send()
264267

268+
async def send_reasoning_msg(loading_msg :cl.message, context_msg :cl.Message, agent_tide_ui :AgentTideUi, st :float)->bool:
269+
await loading_msg.remove()
270+
271+
context_data = {
272+
key: value for key in ["contextIdentifiers", "modifyIdentifiers"]
273+
if (value := getattr(agent_tide_ui.agent_tide, key, None))
274+
}
275+
context_msg.elements.append(
276+
cl.CustomElement(
277+
name="ReasoningMessage",
278+
props={
279+
"reasoning": agent_tide_ui.agent_tide.reasoning,
280+
"data": context_data,
281+
"title": f"Thought for {time.time()-st:.2f} seconds",
282+
"defaultExpanded": False,
283+
"showControls": False
284+
}
285+
)
286+
)
287+
await context_msg.send()
288+
return True
265289

266290
@cl.on_message
267291
async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Optional[list] = None, agent_tide_ui :Optional[AgentTideUi]=None):
292+
293+
loading_msg = await cl.Message(
294+
content="",
295+
elements=[
296+
cl.CustomElement(
297+
name="LoadingMessage",
298+
props={
299+
"messages": ["Working", "Syncing CodeTide", "Thinking", "Looking for context"],
300+
"interval": 1500, # 1.5 seconds between messages
301+
"showIcon": True
302+
}
303+
)
304+
]
305+
).send()
306+
268307
if agent_tide_ui is None:
269308
agent_tide_ui = await loadAgentTideUi()
270309

@@ -278,7 +317,8 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option
278317

279318
chat_history.append({"role": "user", "content": message.content})
280319
await agent_tide_ui.add_to_history(message.content)
281-
320+
321+
context_msg = cl.Message(content="", author="AgentTide")
282322
msg = cl.Message(content="", author="Agent Tide")
283323
async with cl.Step("ApplyPatch", type="tool") as diff_step:
284324
await diff_step.remove()
@@ -311,11 +351,17 @@ async def agent_loop(message: Optional[cl.Message]=None, codeIdentifiers: Option
311351
global_fallback_msg=msg
312352
)
313353

354+
st = time.time()
355+
is_reasonig_sent = False
314356
async for chunk in run_concurrent_tasks(agent_tide_ui, codeIdentifiers):
315357
if chunk == STREAM_START_TOKEN:
358+
is_reasonig_sent = await send_reasoning_msg(loading_msg, context_msg, agent_tide_ui, st)
316359
continue
317360

318-
if chunk == STREAM_END_TOKEN:
361+
elif not is_reasonig_sent:
362+
is_reasonig_sent = await send_reasoning_msg(loading_msg, context_msg, agent_tide_ui, st)
363+
364+
elif chunk == STREAM_END_TOKEN:
319365
# Handle any remaining content
320366
await stream_processor.finalize()
321367
break

0 commit comments

Comments
 (0)