diff --git a/.github/pr-assets/730/after-chat-first-click-raw.png b/.github/pr-assets/730/after-chat-first-click-raw.png new file mode 100644 index 00000000..038bbb5f Binary files /dev/null and b/.github/pr-assets/730/after-chat-first-click-raw.png differ diff --git a/.github/pr-assets/730/after-chat-first-click.png b/.github/pr-assets/730/after-chat-first-click.png new file mode 100644 index 00000000..47d8e9f2 Binary files /dev/null and b/.github/pr-assets/730/after-chat-first-click.png differ diff --git a/.github/pr-assets/730/before-chat-first-click.png b/.github/pr-assets/730/before-chat-first-click.png new file mode 100644 index 00000000..ff3679dd Binary files /dev/null and b/.github/pr-assets/730/before-chat-first-click.png differ diff --git a/.github/pr-assets/730/index.html b/.github/pr-assets/730/index.html new file mode 100644 index 00000000..7e33c9b5 --- /dev/null +++ b/.github/pr-assets/730/index.html @@ -0,0 +1,71 @@ + + +
+ + +PR #730 — Closes #729
+ +First conversation click from clean state shows empty placeholder instead of messages
+skipNextHistoryRef unconditionally set on empty→non-empty transition, skipping loadHistory()
Guard skip with expectingRunRef.current — only active during new-chat send flow
+ Before: Clicking "Chat a95c8ffa" from initial state — conversation highlighted but main area shows "Start a conversation" placeholder. Messages never load.
+
+ After: Same click — conversation messages load immediately. The 5-message history is displayed correctly on first click.
+PR #748 — visual proof pack for issue #739
+ +Active workspace files under deny-roots were rejected by exec even when they belonged to the current upload/team scope.
Path exemptions were static and non-shell-aware, so quoted, prefixed, and legacy screenshot-era upload paths still matched the deny root.
+Shell-aware parsing + canonical path checks now exempt the active personal .uploads subtree, the legacy uploads subtree, and the active team workspace.
claw:3000 backed by the unfixed /opt/goclaw deployment (v2.7.1). After source: temporary PR deployment on claw:3001, backed by goclaw-pr748:local built from the fixed code commit 83fa1342. Both UI captures use fresh chats with the same user and GLM thục bô. Callout border marks the reviewed exec row.
+
+ Stable claw release, fresh chat with GLM thục bô: the agent runs the exact absolute /app/.goclaw/.../.uploads/...jpg copy command and the exec row fails with the safety-policy .goclaw/ denial.
+ Temporary fixed deployment on claw:3001, fresh chat with the same agent: the same absolute /app/.goclaw/.../.uploads/...jpg copy command now completes successfully and returns /tmp/partner-abs.png.
$ ssh claw 'docker run --rm -v /tmp/goclaw-739-before:/src -w /src golang:1.26 sh -lc "/usr/local/go/bin/go test ./cmd -run TestSetupToolRegistryExecWorkspacePaths -count=1 -v"' +=== RUN TestSetupToolRegistryExecWorkspacePaths +=== RUN TestSetupToolRegistryExecWorkspacePaths/personal_workspace_prefixed_uploads_allowed +gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths.../001 +=== RUN TestSetupToolRegistryExecWorkspacePaths/team_workspace_quoted_input_allowed +gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths.../001 +=== RUN TestSetupToolRegistryExecWorkspacePaths/legacy_dotgoclaw_uploads_layout_allowed +gateway_setup_exec_workspace_test.go:133: denied = true, want false; output = command denied by safety policy: matches pattern /tmp/TestSetupToolRegistryExecWorkspacePaths.../001 +FAIL+
Baseline source on claw: the exact gateway wiring still rejects all three representative workspace-path forms.
$ go test ./cmd -run TestSetupToolRegistryExecWorkspacePaths -count=1 -v +=== RUN TestSetupToolRegistryExecWorkspacePaths +=== RUN TestSetupToolRegistryExecWorkspacePaths/personal_workspace_prefixed_uploads_allowed +=== RUN TestSetupToolRegistryExecWorkspacePaths/team_workspace_quoted_input_allowed +=== RUN TestSetupToolRegistryExecWorkspacePaths/legacy_dotgoclaw_uploads_layout_allowed +--- PASS: TestSetupToolRegistryExecWorkspacePaths (0.01s) + --- PASS: personal_workspace_prefixed_uploads_allowed + --- PASS: team_workspace_quoted_input_allowed + --- PASS: legacy_dotgoclaw_uploads_layout_allowed +PASS+
Current worktree: the same gateway path handling now passes for personal uploads, team workspace paths, and the screenshot-era legacy .goclaw/.../uploads layout.
claw
+
+ This earlier live run documents the same production agent and workspace context on stable claw. The report’s primary Before/After pair above is the clean isolated proof you asked for; this screenshot is kept as supporting context for the reproduction journey.
+
.uploads subtree, the legacy .goclaw/.../uploads subtree, or the active team workspace.dataDir files, and workspace-local .goclaw/* remain denied.claw:3000 Before and temporary fixed claw:3001 After, both using fresh conversations.([\s\S]*?)`), "`${1}`"},
+ {regexp.MustCompile(`(?i)]*>([\s\S]*?)`), "[${2}](${1})"},
+}
+
+func htmlTagToWaMd(text string) string {
+ for _, r := range htmlToWaMdReplacers {
+ text = r.re.ReplaceAllString(text, r.repl)
+ }
+ return text
+}
+
+type waCodeBlockMatch struct {
+ text string
+ codes []string
+}
+
+// waExtractCodeBlocks pulls fenced code blocks out of text and replaces them with
+// \x00CB{n}\x00 placeholders so other regex passes don't mangle their contents.
+func waExtractCodeBlocks(text string) waCodeBlockMatch {
+ re := regexp.MustCompile("```[\\w]*\\n?([\\s\\S]*?)```")
+ matches := re.FindAllStringSubmatch(text, -1)
+
+ codes := make([]string, 0, len(matches))
+ for _, m := range matches {
+ codes = append(codes, m[1])
+ }
+
+ i := 0
+ text = re.ReplaceAllStringFunc(text, func(_ string) string {
+ placeholder := fmt.Sprintf("\x00CB%d\x00", i)
+ i++
+ return placeholder
+ })
+
+ return waCodeBlockMatch{text: text, codes: codes}
+}
+
+// chunkText splits text into pieces that fit within maxLen,
+// preferring to split at paragraph (\n\n) or line (\n) boundaries.
+func chunkText(text string, maxLen int) []string {
+ if len(text) <= maxLen {
+ return []string{text}
+ }
+
+ var chunks []string
+ for len(text) > 0 {
+ if len(text) <= maxLen {
+ chunks = append(chunks, text)
+ break
+ }
+ // Find the best split point: paragraph > line > space > hard cut.
+ cutAt := maxLen
+ if idx := strings.LastIndex(text[:maxLen], "\n\n"); idx > 0 {
+ cutAt = idx
+ } else if idx := strings.LastIndex(text[:maxLen], "\n"); idx > 0 {
+ cutAt = idx
+ } else if idx := strings.LastIndex(text[:maxLen], " "); idx > 0 {
+ cutAt = idx
+ }
+ chunks = append(chunks, strings.TrimRight(text[:cutAt], " \n"))
+ text = strings.TrimLeft(text[cutAt:], " \n")
+ }
+ return chunks
+}
diff --git a/internal/channels/whatsapp/format_test.go b/internal/channels/whatsapp/format_test.go
new file mode 100644
index 00000000..9b93ba86
--- /dev/null
+++ b/internal/channels/whatsapp/format_test.go
@@ -0,0 +1,49 @@
+package whatsapp
+
+import "testing"
+
+func TestMarkdownToWhatsApp(t *testing.T) {
+ tests := []struct {
+ name string
+ in string
+ want string
+ }{
+ {"empty", "", ""},
+ {"plain text", "hello world", "hello world"},
+ {"header h1", "# Title", "*Title*"},
+ {"header h3", "### Sub", "*Sub*"},
+ {"bold stars", "this is **bold** text", "this is *bold* text"},
+ {"bold underscores", "this is __bold__ text", "this is *bold* text"},
+ {"strikethrough", "~~deleted~~", "~deleted~"},
+ {"inline code", "use `fmt.Println`", "use ```fmt.Println```"},
+ {"link", "[Go](https://go.dev)", "Go https://go.dev"},
+ {"unordered list dash", "- item one\n- item two", "• item one\n• item two"},
+ {"unordered list star", "* item one\n* item two", "• item one\n• item two"},
+ {"blockquote", "> quoted text", "quoted text"},
+ {
+ "fenced code block preserved",
+ "```go\nfmt.Println(\"hi\")\n```",
+ "```\nfmt.Println(\"hi\")\n```",
+ },
+ {
+ "code block not mangled by bold regex",
+ "```\n**not bold**\n```",
+ "```\n**not bold**\n```",
+ },
+ {"collapse blank lines", "a\n\n\n\nb", "a\n\nb"},
+ {"html bold", "bold", "*bold*"},
+ {"html italic", "italic", "_italic_"},
+ {"html strikethrough", "{t(`${s}.shareMemoryTip`)}
+{t(`${s}.memoryGroupDescription`)}
+ {t(`${s}.shareMemoryNote`)} +
+ )} ++ {t(`${s}.shareKGNote`)} +
+ )}- {t(`${s}.shareMemoryNote`)} -
- )}+ {t("whatsapp.loginSuccessLoading")} +
+ )} + {status === "error" && ( +{errorMsg}
+ )} + {status === "waiting" && !qrPng && ( ++ {t("whatsapp.waitingForQr")} +
+ )} + {status === "waiting" && qrPng && ( + <> ++ {t("whatsapp.scanHint")} +
+ > + )} + {status === "idle" && ( +{t("whatsapp.initializing")}
+ )} +