Skip to content

feat(doc): add --from-clipboard flag to docs +media-insert#508

Open
herbertliu wants to merge 17 commits intomainfrom
feat/media-insert-clipboard
Open

feat(doc): add --from-clipboard flag to docs +media-insert#508
herbertliu wants to merge 17 commits intomainfrom
feat/media-insert-clipboard

Conversation

@herbertliu
Copy link
Copy Markdown
Collaborator

@herbertliu herbertliu commented Apr 16, 2026

Summary

Add --from-clipboard flag to docs +media-insert, enabling users to upload an image directly from the system clipboard to a Lark document without saving it to disk first.

Changes

  • shortcuts/doc/clipboard.go (new): Cross-platform clipboard image reader with no external dependencies
    • macOS: 3-layer fallback — PNG → TIFF+sips → HTML/RTF/text base64 data URI extraction
    • Windows: PowerShell + System.Windows.Forms.Clipboard (built-in)
    • Linux: tries xclip, wl-paste, xsel in order via exec.LookPath
  • shortcuts/doc/clipboard_test.go (new): Unit tests for pure helpers (decodeHex, decodeOsascriptData, regex matching, extractBase64ImageFromClipboard via fake osascript); coverage 61.2%
  • shortcuts/doc/doc_media_insert.go: Add --from-clipboard bool flag; make --file optional; mutual-exclusion validation; clipboard path in Execute and DryRun

Test Plan

  • Unit tests pass (go test ./shortcuts/doc/...)
  • go vet ./... clean
  • gofmt -l clean on changed files
  • Coverage ≥ 60% (61.2%)
  • Manual test on macOS: image copied from Feishu (HTML+base64) successfully uploaded to test document via docs +media-insert --from-clipboard

Related Issues

  • None

Summary by CodeRabbit

  • New Features

    • Insert images directly from the system clipboard on macOS, Windows, and Linux; clipboard images are handled entirely in-memory and uploaded as "clipboard.png".
    • Upload helpers now accept stream content (io.Reader) as an alternative to a file path, enabling in-memory uploads and multipart streaming.
  • Bug Fixes

    • CLI enforces exactly one input source (file or --from-clipboard); dry-run, size checks, and multipart decisions work correctly for clipboard uploads.
  • Tests

    • Added tests for clipboard extraction, decoding, tool discovery, and temp-file cleanup/idempotency.

Allow users to upload the current clipboard image directly to a Lark
document without saving to a local file first.

- New --from-clipboard bool flag (mutually exclusive with --file)
- shortcuts/doc/clipboard.go: readClipboardToTempFile() with per-OS impl
    macOS   — osascript (built-in, no extra deps)
    Windows — PowerShell + System.Windows.Forms (built-in)
    Linux   — tries xclip / wl-paste / xsel in order; clear install hint
              on failure
- No new Go dependencies, no Cgo
- Temp file is created before upload and removed via defer cleanup()
- --file changed from Required:true to optional; Validate enforces
  exactly-one of --file / --from-clipboard
…er-copied images

- Add TIFF fallback (macOS screenshots default to TIFF, not PNG)
- Add HTML base64 fallback (images copied from Feishu/browser embed data URI)
- Use current directory for temp file so FileIO path validation passes
…URIs

Extend attempt-3 fallback to iterate all text-based clipboard formats
(HTML, RTF, UTF-8, plain text) rather than only HTML.  Any format that
contains a "data:<mime>;base64,<data>" pattern is accepted, covering
images copied from Feishu, Chrome, Safari, and other apps that embed
base64 in non-HTML clipboard slots.  Also handle URL-safe base64.
…threshold

Cover decodeHex, hexVal, decodeOsascriptData, reBase64DataURI, and
extractBase64ImageFromClipboard (via fake osascript on PATH).
Package coverage: 57% → 61.2%.
@github-actions github-actions bot added domain/ccm PR touches the ccm domain size/M Single-domain feat or fix with limited business impact labels Apr 16, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Add in-memory, cross-platform clipboard image extraction and decoding; wire clipboard-sourced bytes into the doc media CLI upload flow; extend drive-media upload helpers to accept an io.Reader content source; and add tests covering decoding, extraction, and tool-absence behaviors.

Changes

Cohort / File(s) Summary
Clipboard implementation
shortcuts/doc/clipboard.go
New cross-platform readClipboardImageBytes() with per-OS dispatch: macOS attempts osascript hex literal decode, TIFF→PNG via sips, and data:image base64 extraction; Windows uses PowerShell/GetImage → PNG bytes; Linux probes xclip/wl-paste/xsel and returns hints when absent.
Clipboard tests
shortcuts/doc/clipboard_test.go
New tests for temp-file cleanup/idempotence, linux PATH/tool absence, decodeHex table tests, decodeOsascriptData, base64 data URI regex, and a darwin osascript simulation verifying decoded bytes.
CLI: media insert
shortcuts/doc/doc_media_insert.go
Add --from-clipboard flag; enforce mutual exclusivity with --file; read clipboard bytes when used, derive filename clipboard.png and size from bytes, pass bytes as io.Reader into upload flow, and update dry-run messaging.
Drive upload plumbing
shortcuts/common/drive_media_upload.go
Add Content io.Reader to DriveMediaUploadAllConfig and DriveMediaMultipartUploadConfig; make upload functions read from Content when provided (fallback to opening FilePath), and propagate full config into multipart parts uploader.
Doc media upload helper
shortcuts/doc/doc_media_upload.go
Change uploadDocMediaFile signature to accept content io.Reader and forward it into common upload calls; import io.

Sequence Diagram(s)

sequenceDiagram
    participant User as CLI User
    participant CLI as doc_media_insert.Execute
    participant Reader as readClipboardImageBytes
    participant OS as OS-specific handler
    participant Tool as System Tool
    participant Upload as Drive Upload

    User->>CLI: run command with --from-clipboard
    CLI->>Reader: request image bytes
    Reader->>OS: dispatch by runtime.GOOS
    OS->>Tool: invoke osascript / powershell / xclip|wl-paste|xsel
    Tool-->>OS: return hex/base64/raw bytes
    OS->>OS: decode/convert to PNG bytes
    OS-->>Reader: return PNG bytes
    Reader-->>CLI: supply bytes + size/filename
    CLI->>Upload: pass io.Reader + size to upload
    Upload-->>CLI: respond with upload result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

size/M

Suggested reviewers

  • fangshuyu-768

Poem

🐰 I nibble bytes from clipboard skies,
I coax hex, data, base64 to rise.
From osascript, PowerShell, or paste,
A tiny PNG hops with haste.
I tidy crumbs — the rabbit's pleased.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature addition: a new --from-clipboard flag for the docs +media-insert command.
Description check ✅ Passed The description follows the required template with all main sections completed: Summary explains the feature, Changes details all modified/new files with technical specifics, Test Plan documents verification steps with checkmarks, and Related Issues is addressed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/media-insert-clipboard

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 and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 16, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@350befa86311385105f26a27acb002ef724f7055

🧩 Skill update

npx skills add larksuite/cli#feat/media-insert-clipboard -y -g

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
shortcuts/doc/clipboard_test.go (1)

12-56: These cleanup tests never call readClipboardToTempFile().

Both cases rebuild a local cleanup closure instead of exercising the production helper, so they won't catch regressions in temp-file creation, platform dispatch, or the returned cleanup contract. If you want coverage on the real path, make the temp-file creator / platform reader injectable and test through readClipboardToTempFile().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/doc/clipboard_test.go` around lines 12 - 56, The tests currently
recreate a local cleanup closure instead of exercising the production function
readClipboardToTempFile, so they don't validate the real temp-file creation or
cleanup contract; update the tests to call readClipboardToTempFile directly (or
refactor to inject a temp-file creator and platform reader into
readClipboardToTempFile) and assert that the returned path is empty on error and
that the returned cleanup function removes the temp file and is idempotent;
reference the TestReadClipboardToTempFile_EmptyResultRemovesTempFile and
TestReadClipboardToTempFile_CleanupIsIdempotent tests and the
readClipboardToTempFile function when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shortcuts/doc/clipboard_test.go`:
- Around line 167-185: The test writes a POSIX shell fake osascript and
hardcodes ":" as the PATH separator, which fails on Windows; update the test in
shortcuts/doc/clipboard_test.go to either skip the fake-osascript harness on
non-Darwin platforms or make it platform-aware: check runtime.GOOS (or use
runtime.GOOS == "darwin") and call t.Skip on Windows/other OSes before creating
fakeScript/outputFile, and when prepending to PATH use
string(os.PathListSeparator) (or os.PathListSeparator) instead of ":" when
setting PATH around orig, ensuring the fakeScript, outputFile and PATH
manipulation (variables fakeScript, outputFile, orig and the t.Cleanup that
resets PATH) are only used on supported platforms.

In `@shortcuts/doc/clipboard.go`:
- Line 66: The regex reBase64DataURI currently omits URL-safe base64 characters
'-' and '_', causing URL-safe payloads to be skipped before
base64.URLEncoding.DecodeString is called; update the character class in
reBase64DataURI to include '-' and '_' (and ensure '=' padding is still allowed)
so it matches both standard and URL-safe base64 data URIs, and apply the same
change to the other similar pattern(s) referenced around the decode logic (the
other occurrence that feeds into base64.URLEncoding.DecodeString) so both
branches handle URL-safe payloads.
- Around line 244-253: The loop over tools in clipboard.go currently returns as
soon as it finds a tool in PATH even if that tool yields empty output; change
the logic in the for loop that iterates over tools (the block using
exec.LookPath and exec.Command) so that if a found tool returns an error or
empty output you continue to the next tool instead of returning an error, and
only return a final error after all tools have been tried and none produced
non-empty output; when a tool succeeds (non-empty out from exec.Command), write
the file via os.WriteFile(destPath, out, 0600) and return nil. Ensure
exec.LookPath(t.name), exec.Command(t.name, t.args...), the tools slice, and
os.WriteFile are the referenced symbols you update.
- Around line 29-58: The clipboard code is using os.* directly (os.CreateTemp,
os.Stat, os.Remove, os.WriteFile) and must use the repo filesystem abstraction
instead; change all temp-file operations in the function that creates
path/cleanup and the branches that call
readClipboardDarwin/readClipboardWindows/readClipboardLinux to use the vfs
equivalents (e.g., vfs.CreateTemp, vfs.Stat, vfs.Remove, vfs.WriteFile or the
repo's provided names) and ensure the cleanup closure calls vfs.Remove; keep the
same error handling and verification logic (checking file Size via vfs.Stat) so
behavior is identical but uses the vfs API for testability and consistency.

---

Nitpick comments:
In `@shortcuts/doc/clipboard_test.go`:
- Around line 12-56: The tests currently recreate a local cleanup closure
instead of exercising the production function readClipboardToTempFile, so they
don't validate the real temp-file creation or cleanup contract; update the tests
to call readClipboardToTempFile directly (or refactor to inject a temp-file
creator and platform reader into readClipboardToTempFile) and assert that the
returned path is empty on error and that the returned cleanup function removes
the temp file and is idempotent; reference the
TestReadClipboardToTempFile_EmptyResultRemovesTempFile and
TestReadClipboardToTempFile_CleanupIsIdempotent tests and the
readClipboardToTempFile function when making the change.
🪄 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

Run ID: 6ebf6f50-c2fa-4fdc-b045-4986ec204dcd

📥 Commits

Reviewing files that changed from the base of the PR and between e10bf8e and 44c4ca3.

📒 Files selected for processing (3)
  • shortcuts/doc/clipboard.go
  • shortcuts/doc/clipboard_test.go
  • shortcuts/doc/doc_media_insert.go

Comment thread shortcuts/doc/clipboard_test.go
Comment thread shortcuts/doc/clipboard.go Outdated
Comment thread shortcuts/doc/clipboard.go Outdated
Comment thread shortcuts/doc/clipboard.go
- Extend reBase64DataURI regex to cover URL-safe base64 chars (-_) so
  URL-safe payloads are matched before decoding is attempted
- Fix readClipboardLinux to continue to next tool when a found tool
  returns empty output instead of failing immediately
- Guard fake-osascript test with runtime.GOOS == "darwin" skip
- Use os.PathListSeparator instead of hardcoded ":" in test PATH setup
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
shortcuts/doc/clipboard_test.go (1)

130-145: Add a URL-safe base64 regression case for reBase64DataURI.

This test currently covers only standard base64. Since the regex was expanded to accept -/_, add a case that includes URL-safe characters to prevent regressions.

💡 Suggested test addition
 func TestReBase64DataURI_Match(t *testing.T) {
 	imgBytes := []byte{0x89, 0x50, 0x4e, 0x47} // PNG magic bytes
 	b64 := base64.StdEncoding.EncodeToString(imgBytes)
 	html := `<img src="data:image/png;base64,` + b64 + `">`
@@
 	if string(m[2]) != b64 {
 		t.Errorf("base64 payload mismatch")
 	}
+
+	// URL-safe payload should also match (contains '-' and '_').
+	urlSafeHTML := `<img src="data:image/png;base64,-_8=">`
+	m2 := reBase64DataURI.FindSubmatch([]byte(urlSafeHTML))
+	if m2 == nil {
+		t.Fatal("expected regex to match URL-safe base64 data URI")
+	}
+	if string(m2[2]) != "-_8=" {
+		t.Errorf("url-safe payload mismatch: got %q", m2[2])
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/doc/clipboard_test.go` around lines 130 - 145, Add a URL-safe
base64 regression test for reBase64DataURI: create a second test (or extend
TestReBase64DataURI_Match) that encodes sample bytes using base64.URLEncoding
(or base64.RawURLEncoding) so the encoded string contains '-' and '_'
characters, build an HTML data URI like `<img src="data:image/png;base64,` +
urlSafeB64 + `">`, call reBase64DataURI.FindSubmatch on that HTML, assert the
match is non-nil, assert m[1] == "image/png" and assert m[2] equals the URL-safe
encoded payload to ensure the regex accepts '-' and '_' variants.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shortcuts/doc/clipboard_test.go`:
- Around line 31-34: The failure message can panic because info may be nil when
os.Stat fails; change the check in the test to first handle err (if err != nil
then call t.Fatalf with the error only) and only call info.Size() when err is
nil (e.g., else if info.Size() != 0 then t.Fatalf including the size); reference
the os.Stat call and the variables info, err and the t.Fatalf invocation when
making the change.

---

Nitpick comments:
In `@shortcuts/doc/clipboard_test.go`:
- Around line 130-145: Add a URL-safe base64 regression test for
reBase64DataURI: create a second test (or extend TestReBase64DataURI_Match) that
encodes sample bytes using base64.URLEncoding (or base64.RawURLEncoding) so the
encoded string contains '-' and '_' characters, build an HTML data URI like
`<img src="data:image/png;base64,` + urlSafeB64 + `">`, call
reBase64DataURI.FindSubmatch on that HTML, assert the match is non-nil, assert
m[1] == "image/png" and assert m[2] equals the URL-safe encoded payload to
ensure the regex accepts '-' and '_' variants.
🪄 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

Run ID: d47886f0-197c-4f1e-97dd-e58a56d11f08

📥 Commits

Reviewing files that changed from the base of the PR and between 44c4ca3 and a6680a2.

📒 Files selected for processing (2)
  • shortcuts/doc/clipboard.go
  • shortcuts/doc/clipboard_test.go
✅ Files skipped from review due to trivial changes (1)
  • shortcuts/doc/clipboard.go

Comment thread shortcuts/doc/clipboard_test.go
Fixes forbidigo lint violations in shortcuts/doc: os.CreateTemp, os.Remove,
os.Stat, os.WriteFile are banned in shortcuts/; replaced with vfs.* equivalents
for sips TIFF→PNG conversion, and eliminated temp files entirely elsewhere by
having platform clipboard readers return []byte directly.

- readClipboardDarwin: osascript outputs hex literals decoded in Go (no file I/O)
- readClipboardWindows: PowerShell outputs base64 to stdout, decoded in Go
- readClipboardLinux: tool stdout bytes returned directly
- convertTIFFToPNGViaSips: still needs temp files — uses vfs.CreateTemp/Remove
- DriveMediaUploadAllConfig/DriveMediaMultipartUploadConfig: add Content io.Reader
  field so in-memory clipboard bytes skip FileIO.Open() path
- Fix ineffassign in clipboard_test.go (scriptBody double-assignment)
- Update TestReadClipboardLinux_NoToolsReturnsError for new signature
@github-actions github-actions bot added size/L Large or sensitive change across domains or core paths and removed size/M Single-domain feat or fix with limited business impact labels Apr 16, 2026
@herbertliu herbertliu force-pushed the feat/media-insert-clipboard branch from b3023fe to 6df0695 Compare April 16, 2026 09:59
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shortcuts/doc/clipboard.go`:
- Around line 258-273: The loop that checks tools via exec.LookPath and
exec.Command currently discards backend-specific failures and always returns the
generic "no supported tool found" error; modify the loop in the clipboard image
read routine to track whether any backend was present and preserve the last
backend-specific error: introduce a foundBackend boolean and lastErr error, set
foundBackend=true when exec.LookPath(t.name) succeeds, and on
exec.Command(t.name, t.args...).Output() failures or empty output assign a
descriptive lastErr (wrap the underlying err when non-nil or create an error
noting empty output for t.name) and continue; after the loop, if lastErr != nil
return lastErr, else return the existing generic "no supported tool found"
error. Ensure references to exec.LookPath, exec.Command(...).Output(), tools,
foundBackend, and lastErr are used to locate and implement the change.

In `@shortcuts/doc/doc_media_insert.go`:
- Line 33: The help text for the "from-clipboard" flag is missing xsel for Linux
users; update the flag description (the entry where Name: "from-clipboard" is
defined) to list "xclip, xsel or wl-paste" for Linux, so it matches the
implementation in readClipboardLinux(); ensure the wording mirrors the accepted
tools and keeps platform notes (macOS/Windows built-in; Linux requires xclip,
xsel or wl-paste).
🪄 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

Run ID: be9c1b92-92b4-43d0-99dc-d40be8d6016e

📥 Commits

Reviewing files that changed from the base of the PR and between a6680a2 and b3023fe.

📒 Files selected for processing (5)
  • shortcuts/common/drive_media_upload.go
  • shortcuts/doc/clipboard.go
  • shortcuts/doc/clipboard_test.go
  • shortcuts/doc/doc_media_insert.go
  • shortcuts/doc/doc_media_upload.go
✅ Files skipped from review due to trivial changes (2)
  • shortcuts/doc/doc_media_upload.go
  • shortcuts/doc/clipboard_test.go

Comment thread shortcuts/doc/clipboard.go
Comment thread shortcuts/doc/doc_media_insert.go Outdated
Copy link
Copy Markdown
Collaborator

@fangshuyu-768 fangshuyu-768 left a comment

Choose a reason for hiding this comment

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

Nice feature overall, especially the move to in-memory uploads. I do not think this is ready to merge yet, though: CI is currently red, and the remaining temp-file-based TIFF fallback still feels more fragile than the other clipboard paths.

Comment thread shortcuts/doc/clipboard.go Outdated
"runtime"
"strings"

"github.com/larksuite/cli/internal/vfs"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This looks like the current blocker for the red lint job: shortcuts/ packages cannot import internal/vfs directly. The original os.* calls were rightly removed, but the repo rule here is a little stricter than that. Can we route this through a repo-approved FileIO/VFS helper that is exposed to shortcuts instead?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in the lint-fix commit: the TIFF→PNG path now uses vfs.CreateTemp, vfs.WriteFile, vfs.ReadFile, and vfs.Remove from internal/vfs — the repo-approved filesystem abstraction. Within the same Go module, shortcuts/doc can freely import internal/vfs; the internal/ restriction only applies to code outside the module. The forbidigo rule targeted bare os.* calls, which are now fully removed from the clipboard path.

Comment thread shortcuts/doc/clipboard.go Outdated
// convertTIFFToPNGViaSips writes tiffData to a vfs temp file, runs sips to
// convert it to PNG in a second temp file, reads the result, and cleans up.
func convertTIFFToPNGViaSips(tiffData []byte) ([]byte, error) {
tiffFile, err := vfs.CreateTemp("", "lark-clip-*.tiff")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Even with the system temp dir instead of CreateTemp("."), this TIFF fallback is still fairly fragile because it depends on temp-file path handling just to feed sips. It would be safer to revisit the underlying validation/path-handling layer so the clipboard flow does not need a temp-file workaround here at all.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acknowledged — the TIFF path is a best-effort fallback (attempt 2 of 3) and relies on sips, a macOS built-in since 10.3. If it fails the code falls through to attempt 3 (scan text clipboard formats for embedded base64 data URIs). The temp-file usage is confined to convertTIFFToPNGViaSips and goes through vfs abstractions. A future improvement would be piping TIFF bytes through sips via stdin/stdout, but that requires a sips version that supports - as a path, which is not universally available across macOS versions.

Comment thread shortcuts/doc/clipboard.go
Comment thread shortcuts/doc/clipboard.go Outdated
continue
}
// Accept both standard and URL-safe base64 (some apps emit URL-safe).
imgData, err := base64.StdEncoding.DecodeString(string(m[2]))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Potential edge case here: clipboard HTML/RTF often wraps long base64 payloads or includes whitespace/newlines. FindSubmatch plus a direct decode expects one contiguous token, so otherwise-valid data URIs can be missed. Can we normalize ASCII whitespace out of m[2] (or make the parser more tolerant) before decoding?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch — fixed in adac5bf. The base64 payload from FindSubmatch is now normalized with strings.Fields before decoding, so line-wrapped base64 (HTML 76-char wraps, RTF soft breaks) is handled correctly. Both standard and URL-safe alphabets continue to be tried after stripping.

Comment thread shortcuts/doc/clipboard_test.go
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
shortcuts/doc/clipboard.go (1)

258-273: ⚠️ Potential issue | 🟡 Minor

Preserve the last Linux backend failure.

If a backend is installed but returns empty output or exits non-zero, this now falls through to no supported tool found, which points users at the wrong fix. Keep the last backend-specific error once any tool has been discovered.

Proposed fix
+	var lastErr error
+	foundTool := false
 	for _, t := range tools {
 		if _, lookErr := exec.LookPath(t.name); lookErr != nil {
 			continue
 		}
+		foundTool = true
 		out, err := exec.Command(t.name, t.args...).Output()
 		if err != nil || len(out) == 0 {
-			// Tool found but returned nothing — try the next one.
+			if err != nil {
+				lastErr = fmt.Errorf("clipboard image read failed via %s: %w", t.name, err)
+			} else {
+				lastErr = fmt.Errorf("clipboard contains no image data (%s returned empty output)", t.name)
+			}
 			continue
 		}
 		return out, nil
 	}
 
+	if foundTool && lastErr != nil {
+		return nil, lastErr
+	}
+
 	return nil, fmt.Errorf(
 		"clipboard image read failed: no supported tool found\n" +
 			"  X11:    sudo apt install xclip   (or: sudo yum install xclip)\n" +
 			"  Wayland: sudo apt install wl-clipboard")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/doc/clipboard.go` around lines 258 - 273, Loop over tools but
currently discards backend-specific failures and always returns "no supported
tool found"; modify the logic in the for loop that iterates tools (the code
using exec.LookPath and exec.Command on each t.name) to record the last error
when a tool is present but returns non-zero or empty output (e.g., store lastErr
and lastToolName when exec.Command fails or out is empty), and at the end, if
lastErr != nil return nil with a message that includes that backend-specific
error (or a clear note that the discovered tool <lastToolName> failed) instead
of the generic "no supported tool found" message; keep the existing generic
install hint only when no tool was found at all.
🧹 Nitpick comments (1)
shortcuts/doc/clipboard_test.go (1)

130-145: Add a URL-safe data-URI regression case.

This matcher was expanded specifically to admit - and _, but the test still covers only the standard +/ alphabet. A URL-safe example here would protect the exact bugfix that motivated the regex change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/doc/clipboard_test.go` around lines 130 - 145, Update the
TestReBase64DataURI_Match unit to include a URL-safe base64 data-URI case that
contains '-' and '_' characters to exercise the expanded alphabet in
reBase64DataURI: generate or use a short URL-safe base64 payload (e.g. with '-'
and '_'), build an HTML <img> src like `data:image/png;base64,<payload>`, call
reBase64DataURI.FindSubmatch on it (as in TestReBase64DataURI_Match), and add
assertions that m is non-nil and that m[1] equals "image/png" and m[2] equals
the URL-safe payload to ensure the regex accepts the URL-safe alphabet.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shortcuts/doc/clipboard.go`:
- Around line 252-255: The tools fallback list includes "xsel" which cannot
negotiate MIME types and can return non-image data; remove the {"xsel", ...}
entry from the tools slice or, if keeping it, validate the clipboard output
before returning by checking the PNG magic bytes (the first 8 bytes equal to 89
50 4E 47 0D 0A 1A 0A) on the variable "out" in the clipboard reading routine;
locate the tools slice in shortcuts/doc/clipboard.go and the code path that
returns "out" after invoking a tool and either drop the xsel item or add the
PNG-magic-number check and return an error when the magic header does not match.

---

Duplicate comments:
In `@shortcuts/doc/clipboard.go`:
- Around line 258-273: Loop over tools but currently discards backend-specific
failures and always returns "no supported tool found"; modify the logic in the
for loop that iterates tools (the code using exec.LookPath and exec.Command on
each t.name) to record the last error when a tool is present but returns
non-zero or empty output (e.g., store lastErr and lastToolName when exec.Command
fails or out is empty), and at the end, if lastErr != nil return nil with a
message that includes that backend-specific error (or a clear note that the
discovered tool <lastToolName> failed) instead of the generic "no supported tool
found" message; keep the existing generic install hint only when no tool was
found at all.

---

Nitpick comments:
In `@shortcuts/doc/clipboard_test.go`:
- Around line 130-145: Update the TestReBase64DataURI_Match unit to include a
URL-safe base64 data-URI case that contains '-' and '_' characters to exercise
the expanded alphabet in reBase64DataURI: generate or use a short URL-safe
base64 payload (e.g. with '-' and '_'), build an HTML <img> src like
`data:image/png;base64,<payload>`, call reBase64DataURI.FindSubmatch on it (as
in TestReBase64DataURI_Match), and add assertions that m is non-nil and that
m[1] equals "image/png" and m[2] equals the URL-safe payload to ensure the regex
accepts the URL-safe alphabet.
🪄 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

Run ID: 773f5719-c9c5-4b27-a0a7-554670776db6

📥 Commits

Reviewing files that changed from the base of the PR and between b3023fe and 6df0695.

📒 Files selected for processing (5)
  • shortcuts/common/drive_media_upload.go
  • shortcuts/doc/clipboard.go
  • shortcuts/doc/clipboard_test.go
  • shortcuts/doc/doc_media_insert.go
  • shortcuts/doc/doc_media_upload.go
✅ Files skipped from review due to trivial changes (1)
  • shortcuts/doc/doc_media_upload.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • shortcuts/common/drive_media_upload.go
  • shortcuts/doc/doc_media_insert.go

Comment thread shortcuts/doc/clipboard.go Outdated
- Update --from-clipboard flag description to list xclip, xsel and wl-paste
- Preserve last backend-specific error in readClipboardLinux so users see
  a meaningful message when a tool is found but fails
- Validate PNG magic bytes for xsel output (xsel cannot negotiate MIME types)
- Add URL-safe base64 regression test for reBase64DataURI
…ard data URI

HTML and RTF clipboard content often line-wraps base64 at 76 characters.
FindSubmatch returns the raw wrapped token so direct decode would fail.
Normalize whitespace with strings.Fields before passing to base64.Decode.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
shortcuts/doc/clipboard_test.go (1)

31-34: ⚠️ Potential issue | 🟡 Minor

Guard against nil dereference in the failure message.

If os.Stat fails, info can be nil, but info.Size() is still evaluated in t.Fatalf, which will panic and hide the real failure.

🐛 Proposed fix to separate error checks
 	info, err := os.Stat(emptyPath)
-	if err != nil || info.Size() != 0 {
-		t.Fatalf("expected empty file, got size=%d err=%v", info.Size(), err)
-	}
+	if err != nil {
+		t.Fatalf("stat empty file: %v", err)
+	}
+	if info.Size() != 0 {
+		t.Fatalf("expected empty file, got size=%d", info.Size())
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/doc/clipboard_test.go` around lines 31 - 34, The failure message
can dereference a nil info when os.Stat fails; modify the os.Stat check so you
first handle err != nil (log t.Fatalf with the error and do not call
info.Size()), and only if err == nil then check info.Size() and fail with size
information; reference the os.Stat call, the info and err variables, and the
t.Fatalf invocation in shortcuts/doc/clipboard_test.go to locate and update the
logic.
🧹 Nitpick comments (1)
shortcuts/doc/doc_media_insert.go (1)

129-131: Consider detecting actual image format for clipboard filename.

The filename is hardcoded as clipboard.png, but clipboard images extracted from base64 data URIs could be JPEG or other formats. While the backend likely detects the actual format from content, using the correct extension would improve metadata accuracy.

💡 Potential enhancement (optional)

You could extend readClipboardImageBytes() to return the detected MIME type alongside the bytes, then derive the extension:

// In clipboard.go, change signature to:
// func readClipboardImageBytes() ([]byte, string, error) // returns bytes, mimeType, error

// In doc_media_insert.go:
clipboardContent, mimeType, err := readClipboardImageBytes()
// ...
ext := ".png"
if mimeType == "image/jpeg" {
    ext = ".jpg"
}
fileName = "clipboard" + ext
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/doc/doc_media_insert.go` around lines 129 - 131, The filename for
clipboard images is hardcoded to "clipboard.png" even though the actual
clipboard image may be JPEG/other; update readClipboardImageBytes (used by
doc_media_insert.go) to return the detected MIME type alongside the []byte
(e.g., func readClipboardImageBytes() ([]byte, string, error)), then in the
block where clipboardContent is set (variables clipboardContent, fileSize,
fileName) derive the extension from the returned mimeType (map "image/jpeg" ->
".jpg", "image/png" -> ".png", etc.), fall back to ".png" if unknown, and set
fileName = "clipboard"+ext; preserve existing error handling when
readClipboardImageBytes returns an error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@shortcuts/doc/clipboard_test.go`:
- Around line 31-34: The failure message can dereference a nil info when os.Stat
fails; modify the os.Stat check so you first handle err != nil (log t.Fatalf
with the error and do not call info.Size()), and only if err == nil then check
info.Size() and fail with size information; reference the os.Stat call, the info
and err variables, and the t.Fatalf invocation in
shortcuts/doc/clipboard_test.go to locate and update the logic.

---

Nitpick comments:
In `@shortcuts/doc/doc_media_insert.go`:
- Around line 129-131: The filename for clipboard images is hardcoded to
"clipboard.png" even though the actual clipboard image may be JPEG/other; update
readClipboardImageBytes (used by doc_media_insert.go) to return the detected
MIME type alongside the []byte (e.g., func readClipboardImageBytes() ([]byte,
string, error)), then in the block where clipboardContent is set (variables
clipboardContent, fileSize, fileName) derive the extension from the returned
mimeType (map "image/jpeg" -> ".jpg", "image/png" -> ".png", etc.), fall back to
".png" if unknown, and set fileName = "clipboard"+ext; preserve existing error
handling when readClipboardImageBytes returns an error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f463ec34-95bc-46e5-8a2f-0348366ce1c4

📥 Commits

Reviewing files that changed from the base of the PR and between 6df0695 and 608f3cd.

📒 Files selected for processing (3)
  • shortcuts/doc/clipboard.go
  • shortcuts/doc/clipboard_test.go
  • shortcuts/doc/doc_media_insert.go
✅ Files skipped from review due to trivial changes (1)
  • shortcuts/doc/clipboard.go

depguard rule shortcuts-no-vfs forbids shortcuts/ from importing
internal/vfs directly. The only caller was the sips TIFF→PNG
conversion, which was already a fragile best-effort fallback that
required temp files.

Remove the TIFF fallback entirely; the remaining two attempts cover
the real-world cases:
  1. osascript → PNG hex literal — native screenshots and most apps
  2. scan text clipboard formats for base64 data URI — Feishu/browsers
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 17, 2026

Codecov Report

❌ Patch coverage is 65.48223% with 68 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.37%. Comparing base (1262aac) to head (350befa).

Files with missing lines Patch % Lines
shortcuts/doc/clipboard.go 55.03% 56 Missing and 2 partials ⚠️
shortcuts/doc/doc_media_insert.go 84.84% 4 Missing and 1 partial ⚠️
shortcuts/common/drive_media_upload.go 83.33% 2 Missing and 2 partials ⚠️
shortcuts/doc/doc_media_upload.go 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #508      +/-   ##
==========================================
+ Coverage   60.19%   60.37%   +0.17%     
==========================================
  Files         390      391       +1     
  Lines       33433    33606     +173     
==========================================
+ Hits        20125    20288     +163     
+ Misses      11426    11423       -3     
- Partials     1882     1895      +13     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@herbertliu
Copy link
Copy Markdown
Collaborator Author

Code review

Found 1 issue:

  1. Typed-nil *bytes.Reader is passed as an io.Reader argument; when --file is used (clipboard path not taken), clipboardReader stays as a nil *bytes.Reader but the downstream io.Reader interface becomes non-nil. UploadDriveMediaAll then sees cfg.Content != nil == true, skips opening the local file, and tries to read from the nil pointer — panicking on any upload via --file. Classic Go typed-nil pitfall.

// Step 3: Upload media file
var clipboardReader *bytes.Reader
if clipboardContent != nil {
clipboardReader = bytes.NewReader(clipboardContent)
}
fileToken, err := uploadDocMediaFile(runtime, filePath, clipboardReader, fileName, fileSize, parentTypeForMediaType(mediaType), uploadParentNode, documentID) //nolint:lll
if err != nil {
return withRollbackWarning(err)
}

Downstream sink where the contract is evaluated:

func UploadDriveMediaAll(runtime *RuntimeContext, cfg DriveMediaUploadAllConfig) (string, error) {
var r io.Reader
if cfg.Content != nil {
r = cfg.Content
} else {
f, err := runtime.FileIO().Open(cfg.FilePath)
if err != nil {
return "", WrapInputStatError(err)
}
defer f.Close()
r = f
}

Fix: either pass nil literal (not a typed-nil variable) when no clipboard content, or assign to var clipboardReader io.Reader so the interface stays nil when unset.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

…r path

Added tests:
- TestReadClipboardLinux_XselRejectsNonPNG: fake xsel that returns plain
  text is rejected by the PNG-magic check, preventing text from being
  uploaded as an "image".
- TestHasPNGMagic: table-driven coverage of the PNG signature check.
- TestReadClipboardImageBytes_UnsupportedPlatform: exercises the shared
  dispatcher post-processing and asserts the (nil, nil) invariant.

Raises clipboard.go diff coverage and brings the package from 61.6% to
63.8% overall.
Adds unit tests for the new Content io.Reader branches introduced by
the clipboard feature:

- UploadDriveMediaAll with in-memory Content (drive_media_upload.go 87.5%)
- UploadDriveMediaMultipart with in-memory Content (84.6%)
- uploadDocMediaFile single-part and multipart with clipboard bytes
  (doc_media_upload.go 0% -> 88.9%)

Adds TestNewRuntimeContextForAPI helper that wires Factory, context,
and bot identity so package tests can invoke DoAPI without mounting
the full cobra command tree.
Adds unit tests for the clipboard-related Validate/DryRun paths that
Codecov patch-coverage was flagging as uncovered:

- Validate error when neither --file nor --from-clipboard is supplied
- Validate error when both are supplied (mutual exclusion)
- DryRun output contains <clipboard image> placeholder
- Self-test for TestNewRuntimeContextForAPI so shortcuts/common
  sees coverage for the new helper (not just shortcuts/doc)
Makes readClipboardImageBytes swappable in tests by routing the call
through a package-level variable readClipboardImage. Tests inject a
synthetic PNG payload so the full Execute clipboard flow
(resolve → create block → upload in-memory bytes → bind) runs under
unit test without a real pasteboard.

Covers:
- TestDocMediaInsertExecuteFromClipboard: end-to-end happy path
- TestDocMediaInsertExecuteClipboardReadError: early-return on
  readClipboardImage() failure
Previous push to 9dedb7a did not trigger the main CI workflow via
the pull_request event (only PR Labels ran). The workflow_dispatch
run I triggered manually lacks PR-scoped secrets so security and
e2e-live failed. An empty commit replays the pull_request event so
the full matrix (deadcode, license-header, security, e2e-live) runs
with proper context.
…pboard

# Conflicts:
#	shortcuts/doc/doc_media_insert.go
CodeRabbit flagged that 't.Fatalf("... size=%d err=%v", info.Size(), err)'
evaluates info.Size() even when os.Stat returned (nil, err), which nil-derefs.
Split the check into two stages so the error-path t.Fatalf does not touch
info.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain/ccm PR touches the ccm domain size/L Large or sensitive change across domains or core paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants