fix: include plain URLs and tags in memo snippet generation (#5688)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
memoclaw 2026-03-05 22:03:45 +08:00 committed by GitHub
parent 92d937b1aa
commit 3d4f793f97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 99 additions and 35 deletions

View File

@ -244,15 +244,20 @@ func (s *service) GenerateSnippet(content []byte, maxLength int) (string, error)
lastNodeWasBlock = false
// Only extract plain text nodes
if textNode, ok := n.(*gast.Text); ok {
segment := textNode.Segment
// Extract text from various node types
switch node := n.(type) {
case *gast.Text:
segment := node.Segment
buf.Write(segment.Value(content))
// Add space if this is a soft line break
if textNode.SoftLineBreak() {
if node.SoftLineBreak() {
buf.WriteByte(' ')
}
case *gast.AutoLink:
buf.Write(node.URL(content))
return gast.WalkSkipChildren, nil
case *mast.TagNode:
buf.WriteByte('#')
buf.Write(node.Tag)
}
// Stop walking if we've exceeded double the max length

View File

@ -94,6 +94,18 @@ func TestGenerateSnippet(t *testing.T) {
maxLength: 100,
expected: "Item 1 Item 2 Item 3",
},
{
name: "plain URL autolink",
content: "https://usememos.com",
maxLength: 100,
expected: "https://usememos.com",
},
{
name: "text with plain URL",
content: "Check out https://usememos.com for more info.",
maxLength: 100,
expected: "Check out https://usememos.com for more info.",
},
}
for _, tt := range tests {
@ -103,6 +115,35 @@ func TestGenerateSnippet(t *testing.T) {
assert.Equal(t, tt.expected, snippet)
})
}
// Test with tag extension enabled (matches production config).
svcWithTags := NewService(WithTagExtension())
tagTests := []struct {
name string
content string
maxLength int
expected string
}{
{
name: "tag only",
content: "#todo",
maxLength: 100,
expected: "#todo",
},
{
name: "text with tags",
content: "Remember to #review the #code",
maxLength: 100,
expected: "Remember to #review the #code",
},
}
for _, tt := range tagTests {
t.Run(tt.name, func(t *testing.T) {
snippet, err := svcWithTags.GenerateSnippet([]byte(tt.content), tt.maxLength)
require.NoError(t, err)
assert.Equal(t, tt.expected, snippet)
})
}
}
func TestExtractProperties(t *testing.T) {

View File

@ -141,7 +141,7 @@ const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [],
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
<div className="flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/30">
<PaperclipIcon className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-xs text-foreground">Attachments ({items.length})</span>
<span className="text-xs text-muted-foreground">Attachments ({items.length})</span>
</div>
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">

View File

@ -78,7 +78,7 @@ const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, par
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
<div className="flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/30">
<LinkIcon className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-xs text-foreground">Relations ({referenceRelations.length})</span>
<span className="text-xs text-muted-foreground">Relations ({referenceRelations.length})</span>
</div>
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">

View File

@ -3,6 +3,7 @@ import { Link } from "react-router-dom";
import { extractMemoIdFromName } from "@/helpers/resource-names";
import { useMemoComments } from "@/hooks/useMemoQueries";
import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext";
import MemoSnippetLink from "./MemoSnippetLink";
const MemoCommentListView: React.FC = () => {
const { memo } = useMemoViewContext();
@ -23,7 +24,7 @@ const MemoCommentListView: React.FC = () => {
<span className="text-xs text-muted-foreground">Comments{commentAmount > 1 ? ` (${commentAmount})` : ""}</span>
<Link
to={`/${memo.name}#comments`}
className="flex items-center gap-0.5 text-xs text-muted-foreground hover:text-foreground hover:underline underline-offset-2 transition-colors"
className="flex items-center gap-0.5 text-xs text-muted-foreground/80 hover:underline underline-offset-2 transition-colors"
>
View all
<ArrowUpRightIcon className="w-3 h-3" />
@ -32,13 +33,13 @@ const MemoCommentListView: React.FC = () => {
{displayedComments.map((comment) => {
const uid = extractMemoIdFromName(comment.name);
return (
<Link
<MemoSnippetLink
key={comment.name}
name={comment.name}
snippet={comment.snippet || comment.content}
to={`/${memo.name}#${uid}`}
className="bg-muted/60 rounded-md px-2 py-1 text-xs text-muted-foreground truncate leading-relaxed hover:bg-muted transition-colors block"
>
{comment.content}
</Link>
className="bg-muted/40 rounded-md"
/>
);
})}
</div>

View File

@ -0,0 +1,34 @@
import { Link } from "react-router-dom";
import { extractMemoIdFromName } from "@/helpers/resource-names";
import { cn } from "@/lib/utils";
interface MemoSnippetLinkProps {
name: string;
snippet: string;
to: string;
state?: object;
className?: string;
}
const MemoSnippetLink = ({ name, snippet, to, state, className }: MemoSnippetLinkProps) => {
const memoId = extractMemoIdFromName(name);
return (
<Link
className={cn(
"flex items-center gap-1 px-1 py-1 rounded text-xs text-muted-foreground hover:text-foreground hover:bg-accent/20 transition-colors group",
className,
)}
to={to}
viewTransition
state={state}
>
<span className="text-[8px] font-mono px-1 py-0.5 rounded border border-border bg-muted/40 group-hover:bg-accent/30 transition-colors shrink-0">
{memoId.slice(0, 6)}
</span>
<span className="truncate">{snippet}</span>
</Link>
);
};
export default MemoSnippetLink;

View File

@ -1,7 +1,5 @@
import { Link } from "react-router-dom";
import { extractMemoIdFromName } from "@/helpers/resource-names";
import { cn } from "@/lib/utils";
import type { MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb";
import MemoSnippetLink from "../MemoSnippetLink";
interface RelationCardProps {
memo: MemoRelation_Memo;
@ -10,23 +8,8 @@ interface RelationCardProps {
}
const RelationCard = ({ memo, parentPage, className }: RelationCardProps) => {
const memoId = extractMemoIdFromName(memo.name);
return (
<Link
className={cn(
"flex items-center gap-1 px-1 py-1 rounded text-xs text-muted-foreground hover:text-foreground hover:bg-accent/20 transition-colors group",
className,
)}
to={`/${memo.name}`}
viewTransition
state={{ from: parentPage }}
>
<span className="text-[8px] font-mono px-1 py-0.5 rounded border border-border bg-muted/40 group-hover:bg-accent/30 transition-colors shrink-0">
{memoId.slice(0, 6)}
</span>
<span className="truncate">{memo.snippet}</span>
</Link>
<MemoSnippetLink name={memo.name} snippet={memo.snippet} to={`/${memo.name}`} state={{ from: parentPage }} className={className} />
);
};

View File

@ -27,7 +27,7 @@ const SectionHeader = ({ icon: Icon, title, count, tabs }: SectionHeaderProps) =
onClick={tab.onClick}
className={cn(
"text-xs px-0 py-0 transition-colors",
tab.active ? "text-foreground" : "text-muted-foreground hover:text-foreground",
tab.active ? "text-muted-foreground" : "text-muted-foreground/60 hover:text-muted-foreground",
)}
>
{tab.label} ({tab.count})
@ -37,7 +37,7 @@ const SectionHeader = ({ icon: Icon, title, count, tabs }: SectionHeaderProps) =
))}
</div>
) : (
<span className="text-xs text-foreground">
<span className="text-xs text-muted-foreground">
{title} ({count})
</span>
)}