Skip to content

Fix/ai link loss - #458

Merged
abhinavxd merged 7 commits into
mainfrom
fix/ai-link-loss
Jul 31, 2026
Merged

Fix/ai link loss#458
abhinavxd merged 7 commits into
mainfrom
fix/ai-link-loss

Conversation

@abhinavxd

@abhinavxd abhinavxd commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • AI-generated responses now render as formatted HTML while preserving line breaks.
    • Conversation history and transcripts preserve links alongside their URLs.
    • Assistant summaries and responses automatically remove unnecessary Markdown code fences.
    • Drafted replies begin directly with the response text, without unnecessary introductions.
  • Bug Fixes

    • Improved handling of quoted HTML content and links in messages.
    • Preserved readable transcript content when messages contain empty or unsupported HTML.
    • Improved consistency when displaying generated content across conversations.
    • Improved validation when loading configured language files.

AI grammar fix rewrote the draft as plain text, so links in the reply box were dropped. It now sends the editor HTML and asks the model to keep tags. Conversation transcripts, the AI agent history and copilot context also stripped link URLs when converting HTML to text, so the model never saw them. They now keep links as "text ( url )".
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

AI responses are stored as markdown and returned as HTML. Valid outer Markdown code fences are removed. HTML-to-text conversion now preserves link URLs for transcripts and quote processing.

Changes

AI content normalization

Layer / File(s) Summary
Link-preserving HTML-to-text conversion
internal/stringutil/htmllinks.go, internal/stringutil/stringutil.go, internal/stringutil/emailquote.go, internal/stringutil/*_test.go
The shared conversion helper preserves link URLs. Quote conversion and tests use the link-preserving behavior.
AI response fence normalization
internal/ai/ai.go, internal/ai/agent.go, internal/ai/copilot.go, internal/ai/ai_test.go
Completion, agent responses, and summaries remove valid outer code fences. Completion converts plain text to escaped HTML. Tests cover matching and nonmatching fence formats.
Markdown persistence and HTML delivery
cmd/ai.go, frontend/apps/main/src/features/conversation/ReplyBox.vue
Copilot replies are stored as markdown and returned as HTML. The reply box sends htmlContent for prompt completion.
Transcript and quote link handling
internal/aiagent/worker.go, internal/conversation/models/models.go, internal/conversation/models/models_test.go
Transcript and message comparisons use link-preserving conversion. Tests cover links, empty messages, sender labels, and message limits.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Provider
  participant Completion
  participant Copilot
  participant ConversationStore
  participant ConversationPanel
  Provider->>Completion: return generated response
  Completion->>Completion: strip valid outer code fence
  Completion->>Completion: normalize response as HTML fragment
  Completion->>Copilot: return normalized response
  Copilot->>ConversationStore: persist markdown response
  Copilot->>ConversationPanel: return HTML response
  ConversationStore->>ConversationPanel: provide history
  ConversationPanel->>ConversationPanel: convert assistant markdown to HTML
Loading

Possibly related PRs

  • abhinavxd/libredesk#359: Updates transcript construction and HTML-to-text conversion in overlapping conversation model paths.
  • abhinavxd/libredesk#446: Modifies AI reply rendering in the same command, frontend, and Copilot paths.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: fixing AI-generated link loss and preserving links in rendered content.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ai-link-loss

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/ai.go`:
- Around line 439-444: Update handleGetCopilotMessages so assistant messages
that already contain legacy HTML are detected or marked before Markdown2HTML is
applied, preserving their existing markup while still converting Markdown
assistant content. Use the message format metadata or established legacy-row
detection mechanism rather than treating every assistant message as Markdown.

In `@frontend/apps/main/src/features/conversation/ReplyBox.vue`:
- Around line 225-226: Update the htmlContent assignment in ReplyBox.vue to
avoid treating arbitrary angle-bracketed text, including email addresses, as
HTML. Prefer an explicit API response-format indicator when available; otherwise
use a strict HTML fragment matcher that only accepts valid expected tags, and
add a regression case ensuring text such as “Contact <customer@example.com>”
remains plain text.

In `@internal/ai/ai.go`:
- Line 43: The codeFenceOpenRe matcher in internal/ai/ai.go at lines 43-43 must
accept punctuation-bearing language labels such as c++ and text/plain while
still rejecting additional opening-line content; update the pattern accordingly.
Add matching test cases in internal/ai/ai_test.go at lines 11-50 covering both
labels.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 87dc77d1-6927-4420-aa63-4864d48c32b8

📥 Commits

Reviewing files that changed from the base of the PR and between 418a82e and a5a029d.

📒 Files selected for processing (13)
  • cmd/ai.go
  • frontend/apps/main/src/features/conversation/ReplyBox.vue
  • internal/ai/agent.go
  • internal/ai/ai.go
  • internal/ai/ai_test.go
  • internal/ai/copilot.go
  • internal/aiagent/worker.go
  • internal/conversation/models/models.go
  • internal/conversation/models/models_test.go
  • internal/stringutil/emailquote.go
  • internal/stringutil/emailquote_test.go
  • internal/stringutil/stringutil.go
  • internal/stringutil/stringutil_test.go

Comment thread cmd/ai.go
Comment thread frontend/apps/main/src/features/conversation/ReplyBox.vue Outdated
Comment thread internal/ai/ai.go Outdated
Transcripts flattened links to "text ( url )", so the model copied that shape and drafts came back with the URL in brackets instead of a link. HTML to text for the LLM now writes "[text](url)", which Markdown2HTML turns back into an anchor.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/stringutil/htmllinks.go`:
- Around line 41-42: Update the href filtering condition in the HTML link
conversion logic to detect the cid: scheme case-insensitively, preserving
plain-text handling for values such as CID:image-1. Add a test case covering an
uppercase CID: prefix alongside the existing href exclusion tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eec7587d-7ae4-4a3c-9313-7632e78999f0

📥 Commits

Reviewing files that changed from the base of the PR and between da378e7 and 2e89586.

📒 Files selected for processing (8)
  • internal/aiagent/worker.go
  • internal/conversation/models/models.go
  • internal/conversation/models/models_test.go
  • internal/stringutil/emailquote.go
  • internal/stringutil/emailquote_test.go
  • internal/stringutil/htmllinks.go
  • internal/stringutil/stringutil.go
  • internal/stringutil/stringutil_test.go
💤 Files with no reviewable changes (1)
  • internal/stringutil/stringutil.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/stringutil/emailquote_test.go
  • internal/aiagent/worker.go
  • internal/stringutil/emailquote.go
  • internal/conversation/models/models.go
  • internal/conversation/models/models_test.go

Comment on lines +41 to +42
if href == "" || strings.HasPrefix(href, "#") || strings.HasPrefix(href, "cid:") {
return ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle cid: schemes without case sensitivity.

Line 41 only excludes lowercase cid: values. URI schemes are case-insensitive, so CID:image-1 becomes a Markdown link instead of remaining plain text. Use a case-insensitive prefix check. Add a CID: test case.

Proposed fix
-	if href == "" || strings.HasPrefix(href, "#") || strings.HasPrefix(href, "cid:") {
+	if href == "" || strings.HasPrefix(href, "#") ||
+		(len(href) >= len("cid:") && strings.EqualFold(href[:len("cid:")], "cid:")) {
 		return ""
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if href == "" || strings.HasPrefix(href, "#") || strings.HasPrefix(href, "cid:") {
return ""
if href == "" || strings.HasPrefix(href, "#") ||
(len(href) >= len("cid:") && strings.EqualFold(href[:len("cid:")], "cid:")) {
return ""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/stringutil/htmllinks.go` around lines 41 - 42, Update the href
filtering condition in the HTML link conversion logic to detect the cid: scheme
case-insensitively, preserving plain-text handling for values such as
CID:image-1. Add a test case covering an uppercase CID: prefix alongside the
existing href exclusion tests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/stringutil/htmllinks.go (1)

44-51: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Escape other Markdown-significant characters in link text.

markdownLink escapes only [ and ] in the anchor text. If the text contains _, *, or `, the returned Markdown can be misinterpreted as emphasis, strong emphasis, or code spans when re-rendered, corrupting the visible link text. Extend the replacer to cover these characters too.

♻️ Proposed fix
-	return "[" + strings.NewReplacer("[", `\[`, "]", `\]`).Replace(text) + "](" + href + ")"
+	escaper := strings.NewReplacer("\\", `\\`, "[", `\[`, "]", `\]`, "_", `\_`, "*", `\*`, "`", "\\`")
+	return "[" + escaper.Replace(text) + "](" + href + ")"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/stringutil/htmllinks.go` around lines 44 - 51, The
strings.NewReplacer call in the return statement of markdownLink only escapes
brackets but allows other Markdown-significant characters like underscore,
asterisk, and backtick to pass through unescaped, causing potential
misinterpretation when the link text is re-rendered. Extend the
strings.NewReplacer to include escape sequences for underscore, asterisk, and
backtick characters in addition to the existing bracket escaping to preserve the
intended link text appearance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/init.go`:
- Around line 871-879: Change initI18n to return the initialization error
instead of calling log.Fatalf for file-read or loadI18nLang failures, and update
every caller to handle the returned error. Preserve fatal startup behavior at
the startup caller, while handleUpdateGeneralSettings must return an appropriate
HTTP error response and leave the running server alive when a runtime language
update fails.
- Around line 871-873: Validate req.Lang in handleUpdateGeneralSettings against
the available i18n language files before calling app.setting.Update(req),
rejecting unsupported values. In initI18n, validate the persisted app.lang
similarly and fall back to defLang (or reject it) before constructing the
/i18n/{lang}.json path.

---

Nitpick comments:
In `@internal/stringutil/htmllinks.go`:
- Around line 44-51: The strings.NewReplacer call in the return statement of
markdownLink only escapes brackets but allows other Markdown-significant
characters like underscore, asterisk, and backtick to pass through unescaped,
causing potential misinterpretation when the link text is re-rendered. Extend
the strings.NewReplacer to include escape sequences for underscore, asterisk,
and backtick characters in addition to the existing bracket escaping to preserve
the intended link text appearance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a2085f34-e768-403f-931b-fb1bed23432b

📥 Commits

Reviewing files that changed from the base of the PR and between da378e7 and 95b9993.

📒 Files selected for processing (9)
  • cmd/init.go
  • internal/aiagent/worker.go
  • internal/conversation/models/models.go
  • internal/conversation/models/models_test.go
  • internal/stringutil/emailquote.go
  • internal/stringutil/emailquote_test.go
  • internal/stringutil/htmllinks.go
  • internal/stringutil/stringutil.go
  • internal/stringutil/stringutil_test.go
💤 Files with no reviewable changes (1)
  • internal/stringutil/stringutil.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • internal/conversation/models/models.go
  • internal/stringutil/emailquote.go
  • internal/stringutil/emailquote_test.go
  • internal/conversation/models/models_test.go
  • internal/aiagent/worker.go
  • internal/stringutil/stringutil_test.go

Comment thread cmd/init.go
Comment on lines +871 to +873
lang := cmp.Or(ko.String("app.lang"), defLang)
log.Printf("loading i18n language file: %s", lang)
if _, err := fs.Read("/i18n/" + lang + ".json"); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether app.lang is validated against a known language list before persistence.
rg -n -C5 '"lang"|Lang\b' internal/ cmd/ --type=go

Repository: abhinavxd/libredesk

Length of output: 5842


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^cmd/(init|settings|i18n|handlers)\.go$|internal/settings|internal/setting)'

echo
echo "== settings model and handlers =="
cat -n internal/setting/models/models.go
echo
cat -n cmd/settings.go | sed -n '1,120p'
echo
echo "== setting update implementation candidates =="
rg -n -C4 'type .*Setting|func .*Update|UpdateSettings|settings.*Update|ko\.String\("app\.lang"\)|app\.lang' internal cmd --type=go

echo
echo "== handlers i18n relevant =="
cat -n cmd/i18n.go | sed -n '1,180p'

Repository: abhinavxd/libredesk

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== internal setting implementation =="
cat -n internal/setting/setting.go | sed -n '1,220p'

echo
echo "== setting SQL for app.lang =="
cat -n internal/setting/queries.sql | sed -n '1,220p'

echo
echo "== available lang endpoint/handlers =="
cat -n cmd/i18n.go | sed -n '33,75p'

echo
echo "== migrations mentioning app.lang and lang map =="
cat -n internal/migrations/v2.3.0.go | sed -n '1,120p'
rg -n "langMap|en-US|en_GB|de-DE|fr-FR|es-ES|pt-PT|zh-CN|zh-Hans|ar-SA|tr-TR|ru-RU" internal migrations cmd --type=go --iglob '*.go'

Repository: abhinavxd/libredesk

Length of output: 11913


Validate app.lang before saving it to settings.

handleUpdateGeneralSettings accepts any req.Lang value and passes it to app.setting.Update(req), which only marshals and persists the JSON without checking supported i18n codes. Validate req.Lang against the available lang files before calling Update, and have initI18n fall back to defLang or reject an invalid persisted value instead of using it directly in "/i18n/" + lang + ".json".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/init.go` around lines 871 - 873, Validate req.Lang in
handleUpdateGeneralSettings against the available i18n language files before
calling app.setting.Update(req), rejecting unsupported values. In initI18n,
validate the persisted app.lang similarly and fall back to defLang (or reject
it) before constructing the /i18n/{lang}.json path.

Comment thread cmd/init.go
Comment on lines +871 to 879
lang := cmp.Or(ko.String("app.lang"), defLang)
log.Printf("loading i18n language file: %s", lang)
if _, err := fs.Read("/i18n/" + lang + ".json"); err != nil {
log.Fatalf("error reading i18n language file `%s` : %v", lang, err)
}
i18n, err := i18n.New(file.ReadBytes())
i18n, err := loadI18nLang(lang, fs)
if err != nil {
log.Fatalf("error initializing i18n: %v", err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

log.Fatalf inside initI18n can crash the running server on a runtime settings update.

initI18n calls log.Fatalf when the file read fails (Line 874) or when loadI18nLang fails (Line 878). log.Fatalf calls os.Exit(1) and terminates the whole process.

cmd/settings.go calls app.i18n = initI18n(app.fs) from handleUpdateGeneralSettings whenever an admin changes the language setting at runtime. If the corresponding /i18n/{lang}.json file is missing, unreadable, or malformed at that point, the entire server process exits, not just the request. This turns a single misconfigured admin update into a full outage for all users.

Change initI18n to return an error to its callers, and let each caller decide the behavior: fatal at startup, but return an HTTP error response (without crashing) from handleUpdateGeneralSettings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/init.go` around lines 871 - 879, Change initI18n to return the
initialization error instead of calling log.Fatalf for file-read or loadI18nLang
failures, and update every caller to handle the returned error. Preserve fatal
startup behavior at the startup caller, while handleUpdateGeneralSettings must
return an appropriate HTTP error response and leave the running server alive
when a runtime language update fails.

@abhinavxd
abhinavxd merged commit 5c56112 into main Jul 31, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant