Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c147d8be78 | |||
| 9ffaa18efe | |||
| d53f3fe207 | |||
| 4f1d757dd8 |
22
HISTORY.md
22
HISTORY.md
@@ -5,10 +5,32 @@ Changelog
|
||||
(unreleased)
|
||||
------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Project association improvements, refs NOISSUE. [Simon Diesenreiter]
|
||||
|
||||
|
||||
0.9.10 (2026-04-11)
|
||||
-------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- More git integration fixes, refs NOISSUE. [Simon Diesenreiter]
|
||||
|
||||
Other
|
||||
~~~~~
|
||||
|
||||
|
||||
0.9.9 (2026-04-11)
|
||||
------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Add missing git binary, refs NOISSUE. [Simon Diesenreiter]
|
||||
|
||||
Other
|
||||
~~~~~
|
||||
|
||||
|
||||
0.9.8 (2026-04-11)
|
||||
------------------
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.9.9
|
||||
0.9.11
|
||||
|
||||
@@ -58,6 +58,18 @@ class GiteaAPI:
|
||||
"""Build a Gitea API URL from a relative path."""
|
||||
return f"{self.base_url}/api/v1/{path.lstrip('/')}"
|
||||
|
||||
def _normalize_pull_request_head(self, head: str | None, owner: str | None = None) -> str | None:
|
||||
"""Return a Gitea-compatible head ref for pull request creation."""
|
||||
normalized = (head or '').strip()
|
||||
if not normalized:
|
||||
return None
|
||||
if ':' in normalized:
|
||||
return normalized
|
||||
effective_owner = (owner or self.owner or '').strip()
|
||||
if not effective_owner:
|
||||
return normalized
|
||||
return f"{effective_owner}:{normalized}"
|
||||
|
||||
def build_repo_git_url(self, owner: str | None = None, repo: str | None = None) -> str | None:
|
||||
"""Build the clone URL for a repository."""
|
||||
_owner = owner or self.owner
|
||||
@@ -222,11 +234,12 @@ class GiteaAPI:
|
||||
"""Create a pull request."""
|
||||
_owner = owner or self.owner
|
||||
_repo = repo or self.repo
|
||||
normalized_head = self._normalize_pull_request_head(head, _owner)
|
||||
payload = {
|
||||
"title": title,
|
||||
"body": body,
|
||||
"base": base,
|
||||
"head": head or f"{_owner}-{_repo}-ai-gen-{hash(title) % 10000}",
|
||||
"head": normalized_head or f"{_owner}:{_owner}-{_repo}-ai-gen-{hash(title) % 10000}",
|
||||
}
|
||||
return await self._request("POST", f"repos/{_owner}/{_repo}/pulls", payload)
|
||||
|
||||
@@ -242,11 +255,12 @@ class GiteaAPI:
|
||||
"""Synchronously create a pull request."""
|
||||
_owner = owner or self.owner
|
||||
_repo = repo or self.repo
|
||||
normalized_head = self._normalize_pull_request_head(head, _owner)
|
||||
payload = {
|
||||
"title": title,
|
||||
"body": body,
|
||||
"base": base,
|
||||
"head": head or f"{_owner}-{_repo}-ai-gen-{hash(title) % 10000}",
|
||||
"head": normalized_head or f"{_owner}:{_owner}-{_repo}-ai-gen-{hash(title) % 10000}",
|
||||
}
|
||||
return self._request_sync("POST", f"repos/{_owner}/{_repo}/pulls", payload)
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ class AgentOrchestrator:
|
||||
features=self.features,
|
||||
tech_stack=self.tech_stack,
|
||||
actor_name=self.prompt_actor,
|
||||
source=self.prompt_actor,
|
||||
related_issue={'number': self.related_issue_number} if self.related_issue_number is not None else None,
|
||||
source_context=self.prompt_source_context,
|
||||
routing=self.prompt_routing,
|
||||
@@ -593,6 +594,16 @@ class AgentOrchestrator:
|
||||
f"Prompt: {self.prompt_text or self.description}\n\n"
|
||||
f"Branch: {self.branch_name}"
|
||||
)
|
||||
pull_request_debug = self.ui_manager.ui_data.setdefault('git', {}).setdefault('pull_request_debug', {})
|
||||
pull_request_request = {
|
||||
'owner': self.repo_owner,
|
||||
'repo': self.repo_name,
|
||||
'title': title,
|
||||
'body': body,
|
||||
'base': 'main',
|
||||
'head': self.gitea_api._normalize_pull_request_head(self.branch_name, self.repo_owner) or self.branch_name,
|
||||
}
|
||||
pull_request_debug['request'] = pull_request_request
|
||||
result = await self.gitea_api.create_pull_request(
|
||||
title=title,
|
||||
body=body,
|
||||
@@ -601,7 +612,9 @@ class AgentOrchestrator:
|
||||
base='main',
|
||||
head=self.branch_name,
|
||||
)
|
||||
pull_request_debug['response'] = result
|
||||
if result.get('error'):
|
||||
pull_request_debug['status'] = 'error'
|
||||
raise RuntimeError(f"Unable to create pull request: {result.get('error')}")
|
||||
|
||||
pr_number = result.get('number') or result.get('id') or 0
|
||||
@@ -616,6 +629,8 @@ class AgentOrchestrator:
|
||||
'merged': bool(result.get('merged')),
|
||||
'pr_state': result.get('state', 'open'),
|
||||
}
|
||||
pull_request_debug['status'] = 'created'
|
||||
pull_request_debug['resolved'] = pr_data
|
||||
if self.db_manager and self.history:
|
||||
self.db_manager.save_pr_data(self.history.id, pr_data)
|
||||
self.active_pull_request = self.db_manager.get_open_pull_request(project_id=self.project_id) if self.db_manager else pr_data
|
||||
|
||||
@@ -31,6 +31,11 @@ class RequestInterpreter:
|
||||
PLACEHOLDER_PROJECT_NAME_WORDS = {
|
||||
'generated project', 'new project', 'project', 'temporary name', 'temp name', 'placeholder', 'untitled project',
|
||||
}
|
||||
ROUTING_STOPWORDS = REPO_NOISE_WORDS | GENERIC_PROJECT_NAME_WORDS | {
|
||||
'about', 'after', 'again', 'appropriate', 'before', 'best', 'details', 'follow', 'following', 'implement',
|
||||
'integration', 'instance', 'instances', 'later', 'make', 'now', 'primary', 'primarily', 'probably',
|
||||
'remember', 'specific', 'suite', 'tearing', 'testing', 'through', 'used', 'using', 'workflow', 'workflows',
|
||||
}
|
||||
|
||||
def __init__(self, ollama_url: str | None = None, model: str | None = None):
|
||||
self.ollama_url = (ollama_url or settings.ollama_url).rstrip('/')
|
||||
@@ -468,6 +473,7 @@ class RequestInterpreter:
|
||||
projects = context.get('projects', [])
|
||||
last_project_id = recent_history[0].get('project_id') if recent_history else None
|
||||
last_issue = ((recent_history[0].get('related_issue') or {}).get('number') if recent_history else None)
|
||||
last_project = next((project for project in projects if project.get('project_id') == last_project_id), None) if last_project_id else None
|
||||
|
||||
matched_project = None
|
||||
for project in projects:
|
||||
@@ -481,8 +487,24 @@ class RequestInterpreter:
|
||||
break
|
||||
if matched_project is None and not explicit_new:
|
||||
follow_up_tokens = ['also', 'continue', 'for this project', 'for that project', 'work on this', 'work on that', 'fix that', 'add this']
|
||||
if any(token in lowered for token in follow_up_tokens) and last_project_id:
|
||||
matched_project = next((project for project in projects if project.get('project_id') == last_project_id), None)
|
||||
leading_follow_up = lowered.startswith(('also', 'now', 'continue', 'remember', 'then'))
|
||||
recent_overlap = 0
|
||||
if last_project is not None:
|
||||
recent_prompt_text = recent_history[0].get('prompt_text') or ''
|
||||
project_reference_text = ' '.join(
|
||||
part for part in [
|
||||
last_project.get('name') or '',
|
||||
last_project.get('description') or '',
|
||||
((last_project.get('repository') or {}).get('name') or ''),
|
||||
]
|
||||
if part
|
||||
)
|
||||
recent_overlap = len(
|
||||
self._routing_tokens(prompt_text)
|
||||
& (self._routing_tokens(recent_prompt_text) | self._routing_tokens(project_reference_text))
|
||||
)
|
||||
if last_project_id and (leading_follow_up or any(token in lowered for token in follow_up_tokens) or recent_overlap >= 2):
|
||||
matched_project = last_project
|
||||
issue_number = referenced_issue
|
||||
if issue_number is None and any(token in lowered for token in ['that issue', 'this issue', 'the issue']) and last_issue is not None:
|
||||
issue_number = last_issue
|
||||
@@ -497,6 +519,14 @@ class RequestInterpreter:
|
||||
'reasoning_summary': 'Heuristic routing from chat history and project names.',
|
||||
}
|
||||
|
||||
def _routing_tokens(self, text: str) -> set[str]:
|
||||
"""Extract meaningful tokens for heuristic continuation matching."""
|
||||
cleaned = re.sub(r'[^a-z0-9]+', ' ', (text or '').lower())
|
||||
return {
|
||||
token for token in cleaned.split()
|
||||
if len(token) >= 4 and token not in self.ROUTING_STOPWORDS
|
||||
}
|
||||
|
||||
def _extract_issue_number(self, prompt_text: str) -> int | None:
|
||||
match = re.search(r'(?:#|issue\s+)(\d+)', prompt_text, flags=re.IGNORECASE)
|
||||
return int(match.group(1)) if match else None
|
||||
@@ -261,6 +261,21 @@ def _render_generation_diagnostics(ui_data: dict | None) -> None:
|
||||
ui.label(f"Remote push error: {git_debug['remote_error']}").classes('factory-code')
|
||||
if git_debug.get('error'):
|
||||
ui.label(f"Git error: {git_debug['error']}").classes('factory-code')
|
||||
pull_request_debug = git_debug.get('pull_request_debug') if isinstance(git_debug.get('pull_request_debug'), dict) else {}
|
||||
if pull_request_debug:
|
||||
ui.label('Pull request creation').style('font-weight: 700; color: #2f241d;')
|
||||
if pull_request_debug.get('status'):
|
||||
ui.label(str(pull_request_debug['status'])).classes('factory-chip')
|
||||
if pull_request_debug.get('request'):
|
||||
with ui.expansion('PR request payload').classes('w-full q-mt-sm'):
|
||||
ui.label(json.dumps(pull_request_debug['request'], indent=2, sort_keys=True)).classes('factory-code')
|
||||
if pull_request_debug.get('response'):
|
||||
with ui.expansion('PR API response').classes('w-full q-mt-sm'):
|
||||
ui.label(json.dumps(pull_request_debug['response'], indent=2, sort_keys=True)).classes('factory-code')
|
||||
if pull_request_debug.get('resolved'):
|
||||
resolved = pull_request_debug['resolved']
|
||||
if resolved.get('pr_url'):
|
||||
ui.link('Open pull request', resolved['pr_url'], new_tab=True).classes('factory-code')
|
||||
|
||||
|
||||
def _render_timeline(events: list[dict]) -> None:
|
||||
|
||||
@@ -241,6 +241,17 @@ def _serialize_project_log(log: ProjectLog) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _ensure_summary_mentions_pull_request(summary_message: str, pull_request: dict | None) -> str:
|
||||
"""Append the pull request URL to chat summaries when one exists."""
|
||||
if not isinstance(pull_request, dict):
|
||||
return summary_message
|
||||
pr_url = (pull_request.get('pr_url') or '').strip()
|
||||
if not pr_url or pr_url in summary_message:
|
||||
return summary_message
|
||||
separator = '' if summary_message.endswith(('.', '!', '?')) else '.'
|
||||
return f"{summary_message}{separator} Review PR: {pr_url}"
|
||||
|
||||
|
||||
def _serialize_system_log(log: SystemLog) -> dict:
|
||||
"""Serialize a system log row."""
|
||||
return {
|
||||
@@ -391,6 +402,7 @@ async def _run_generation(
|
||||
'logs': [log.get('message', '') for log in response_data.get('logs', []) if isinstance(log, dict)],
|
||||
}
|
||||
summary_message, summary_trace = await ChangeSummaryGenerator().summarize_with_trace(summary_context)
|
||||
summary_message = _ensure_summary_mentions_pull_request(summary_message, response_data.get('pull_request'))
|
||||
if orchestrator.db_manager and orchestrator.history and orchestrator.prompt_audit:
|
||||
orchestrator.db_manager.log_llm_trace(
|
||||
project_id=project_id,
|
||||
|
||||
Reference in New Issue
Block a user