-
Notifications
You must be signed in to change notification settings - Fork 25
Add @mention resolution for comments and descriptions #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
319e339
c0b727e
dc96a14
f84f9d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -170,6 +170,50 @@ func (c *Client) Delete(path string) (*APIResponse, error) { | |||||||||||
| return c.request("DELETE", path, nil) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // GetHTML performs a GET request expecting an HTML response. | ||||||||||||
| // Unlike Get, it sets Accept: text/html and does not attempt JSON parsing. | ||||||||||||
| func (c *Client) GetHTML(path string) (*APIResponse, error) { | ||||||||||||
| requestURL := c.buildURL(path) | ||||||||||||
| req, err := http.NewRequestWithContext(context.Background(), "GET", requestURL, nil) | ||||||||||||
| if err != nil { | ||||||||||||
| return nil, errors.NewNetworkError(fmt.Sprintf("Failed to create request: %v", err)) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| req.Header.Set("Authorization", "Bearer "+c.Token) | ||||||||||||
| req.Header.Set("Accept", "text/html") | ||||||||||||
| req.Header.Set("User-Agent", "fizzy-cli/1.0") | ||||||||||||
|
||||||||||||
| req.Header.Set("Authorization", "Bearer "+c.Token) | |
| req.Header.Set("Accept", "text/html") | |
| req.Header.Set("User-Agent", "fizzy-cli/1.0") | |
| c.setHeaders(req) | |
| req.Header.Set("Accept", "text/html") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in c0b727e. GetHTML() now calls c.setHeaders(req) and only overrides the Accept header to text/html.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -293,9 +293,9 @@ var cardCreateCmd = &cobra.Command{ | |
| if descErr != nil { | ||
| return descErr | ||
| } | ||
| description = markdownToHTML(string(descContent)) | ||
| description = markdownToHTML(resolveMentions(string(descContent), getClient())) | ||
| } else if cardCreateDescription != "" { | ||
| description = markdownToHTML(cardCreateDescription) | ||
| description = markdownToHTML(resolveMentions(cardCreateDescription, getClient())) | ||
| } | ||
|
||
|
|
||
| ac := getSDK() | ||
|
|
@@ -384,9 +384,9 @@ var cardUpdateCmd = &cobra.Command{ | |
| if err != nil { | ||
| return err | ||
| } | ||
| description = markdownToHTML(string(content)) | ||
| description = markdownToHTML(resolveMentions(string(content), getClient())) | ||
| } else if cardUpdateDescription != "" { | ||
| description = markdownToHTML(cardUpdateDescription) | ||
| description = markdownToHTML(resolveMentions(cardUpdateDescription, getClient())) | ||
| } | ||
|
||
|
|
||
| // Build breadcrumbs | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -149,9 +149,9 @@ var commentCreateCmd = &cobra.Command{ | |
| if err != nil { | ||
| return err | ||
| } | ||
| body = markdownToHTML(string(content)) | ||
| body = markdownToHTML(resolveMentions(string(content), getClient())) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Mention resolution is applied to raw markdown text using regex, so Prompt for AI agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in c0b727e. |
||
| } else if commentCreateBody != "" { | ||
| body = markdownToHTML(commentCreateBody) | ||
| body = markdownToHTML(resolveMentions(commentCreateBody, getClient())) | ||
| } else { | ||
|
||
| return newRequiredFlagError("body or body_file") | ||
| } | ||
|
|
@@ -209,9 +209,9 @@ var commentUpdateCmd = &cobra.Command{ | |
| if err != nil { | ||
| return err | ||
| } | ||
| body = markdownToHTML(string(content)) | ||
| body = markdownToHTML(resolveMentions(string(content), getClient())) | ||
| } else if commentUpdateBody != "" { | ||
| body = markdownToHTML(commentUpdateBody) | ||
| body = markdownToHTML(resolveMentions(commentUpdateBody, getClient())) | ||
| } | ||
|
||
|
|
||
| commentID := args[0] | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,207 @@ | ||||||||
| package commands | ||||||||
|
|
||||||||
| import ( | ||||||||
| "fmt" | ||||||||
| "os" | ||||||||
| "regexp" | ||||||||
| "strings" | ||||||||
| "sync" | ||||||||
| "unicode" | ||||||||
|
|
||||||||
| "github.com/basecamp/fizzy-cli/internal/client" | ||||||||
| ) | ||||||||
|
|
||||||||
| // mentionUser represents a mentionable user parsed from the /prompts/users endpoint. | ||||||||
| type mentionUser struct { | ||||||||
| FirstName string // e.g. "Wayne" | ||||||||
| FullName string // e.g. "Wayne Smith" | ||||||||
| SGID string // signed global ID for ActionText | ||||||||
| AvatarSrc string // e.g. "/6103476/users/03f5awg7.../avatar" | ||||||||
| } | ||||||||
|
|
||||||||
| // Package-level cache: populated once per CLI invocation. | ||||||||
| var ( | ||||||||
| mentionUsers []mentionUser | ||||||||
| mentionOnce sync.Once | ||||||||
| mentionErr error | ||||||||
| ) | ||||||||
|
|
||||||||
| // resetMentionCache resets the cache for testing. | ||||||||
| func resetMentionCache() { | ||||||||
| mentionOnce = sync.Once{} | ||||||||
| mentionUsers = nil | ||||||||
| mentionErr = nil | ||||||||
| } | ||||||||
|
|
||||||||
| // mentionRegex matches @Name patterns not preceded by word characters or dots | ||||||||
| // (to avoid matching emails like user@example.com). | ||||||||
| var mentionRegex = regexp.MustCompile(`(?:^|[^a-zA-Z0-9_.])@([a-zA-Z][a-zA-Z0-9_]*)`) | ||||||||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||||||||
|
|
||||||||
| // promptItemRegex parses <lexxy-prompt-item> tags from the /prompts/users HTML. | ||||||||
| var promptItemRegex = regexp.MustCompile(`<lexxy-prompt-item\s+search="([^"]+)"\s+sgid="([^"]+)"[^>]*>`) | ||||||||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||||||||
| var promptItemRegex = regexp.MustCompile(`<lexxy-prompt-item\s+search="([^"]+)"\s+sgid="([^"]+)"[^>]*>`) | |
| // It is robust to attribute order and extra attributes, while capturing search and sgid. | |
| var promptItemRegex = regexp.MustCompile(`<lexxy-prompt-item(?=[^>]*\ssearch="([^"]+)")(?=[^>]*\ssgid="([^"]+)")[^>]*>`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in c0b727e. Note: the suggested lookahead regex ((?=...)) is not supported by Go's RE2 engine — it panics at compile time. Instead, replaced the single regex with separate searchAttrRegex and sgidAttrRegex patterns applied independently to each matched tag. This handles any attribute order without requiring PCRE features.
Copilot
AI
Mar 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolveMentions runs on the raw markdown input before markdownToHTML(), so it will also rewrite @Name occurrences inside markdown code spans / fenced code blocks (e.g., @Sarah), changing what the user intended to show as literal code into an escaped <action-text-attachment ...> snippet. Add logic to skip mention resolution inside markdown code (or move mention resolution to operate on the post-markdown HTML while ignoring /
content), and add a unit test for the backtick/code-fence case.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in c0b727e. Code spans and fenced code blocks are now replaced with null-byte placeholders before mention resolution runs, then restored afterward. This prevents @name inside backticks or code fences from being transformed. Three new test cases cover this.
Copilot
AI
Mar 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
buildMentionHTML interpolates SGID, FullName, AvatarSrc, and FirstName directly into HTML attributes / text without escaping. User names can legally contain characters like quotes or '<', which can break the generated markup or introduce HTML injection in the rich-text body. Use proper HTML escaping for attribute values and inner text (e.g., html.EscapeString) before formatting the attachment HTML, and add a test covering a name containing quotes or '<' to prevent regressions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in c0b727e. All user-controlled values (SGID, FullName, AvatarSrc, FirstName) are now escaped with html.EscapeString() before interpolation. Added TestBuildMentionHTMLEscaping covering names with quotes and <script> tags.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GetHTML()is a new request path with important behavior differences fromGet()(differentAcceptheader and no JSON parsing). Sinceinternal/clienthas extensive request/response tests already, it would be good to add a focused unit test forGetHTML()to prevent regressions (e.g., assert the server seesAccept: text/html, the raw body is returned, and non-2xx responses are surfaced viaerrorFromResponse).