Skip to content

Commit e556a81

Browse files
authored
Feat/channel tool feedback animation (sipeed#2569)
* feat(channels): unify tool feedback animation across discord telegram and feishu * fix(tool-feedback): unify fallback and single-message delivery * fix(channels): finalize tool feedback in place * fix ci * feat: improve tool feedback
1 parent 8461c99 commit e556a81

35 files changed

Lines changed: 3318 additions & 170 deletions

cmd/picoclaw/internal/auth/wecom_test.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package auth
33
import (
44
"bytes"
55
"context"
6+
"net"
67
"net/http"
78
"net/http/httptest"
89
"net/url"
@@ -19,6 +20,19 @@ import (
1920
"github.com/sipeed/picoclaw/pkg/config"
2021
)
2122

23+
func newIPv4TestServer(t *testing.T, handler http.Handler) *httptest.Server {
24+
t.Helper()
25+
26+
server := httptest.NewUnstartedServer(handler)
27+
listener, err := net.Listen("tcp4", "127.0.0.1:0")
28+
require.NoError(t, err)
29+
30+
server.Listener = listener
31+
server.Start()
32+
t.Cleanup(server.Close)
33+
return server
34+
}
35+
2236
func TestNewWeComCommand(t *testing.T) {
2337
cmd := newWeComCommand()
2438

@@ -53,15 +67,14 @@ func TestBuildWeComQRCodePageURL(t *testing.T) {
5367
}
5468

5569
func TestFetchWeComQRCode(t *testing.T) {
56-
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
70+
server := newIPv4TestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
5771
assert.Equal(t, "/generate", r.URL.Path)
5872
assert.Equal(t, wecomQRSourceID, r.URL.Query().Get("source"))
5973
assert.Equal(t, wecomQRSourceID, r.URL.Query().Get("sourceID"))
6074
assert.Equal(t, strconv.Itoa(wecomPlatformCode()), r.URL.Query().Get("plat"))
6175
w.Header().Set("Content-Type", "application/json")
6276
_, _ = w.Write([]byte(`{"data":{"scode":"scode-1","auth_url":"https://example.com/qr"}}`))
6377
}))
64-
defer server.Close()
6578

6679
opts := normalizeWeComQRFlowOptions(wecomQRFlowOptions{
6780
HTTPClient: server.Client(),
@@ -78,7 +91,7 @@ func TestFetchWeComQRCode(t *testing.T) {
7891
func TestPollWeComQRCodeResult(t *testing.T) {
7992
var calls atomic.Int32
8093

81-
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
94+
server := newIPv4TestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
8295
call := calls.Add(1)
8396
assert.Equal(t, "/query", r.URL.Path)
8497
assert.Equal(t, "scode-1", r.URL.Query().Get("scode"))
@@ -92,7 +105,6 @@ func TestPollWeComQRCodeResult(t *testing.T) {
92105
_, _ = w.Write([]byte(`{"data":{"status":"success","bot_info":{"botid":"bot-1","secret":"secret-1"}}}`))
93106
}
94107
}))
95-
defer server.Close()
96108

97109
var output bytes.Buffer
98110
opts := normalizeWeComQRFlowOptions(wecomQRFlowOptions{

docs/channels/discord/README.md

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,56 @@ Discord is a free voice, video, and text chat application designed for communiti
88

99
```json
1010
{
11+
"agents": {
12+
"defaults": {
13+
"tool_feedback": {
14+
"enabled": true,
15+
"max_args_length": 300
16+
}
17+
}
18+
},
1119
"channel_list": {
1220
"discord": {
1321
"enabled": true,
1422
"type": "discord",
1523
"token": "YOUR_BOT_TOKEN",
1624
"allow_from": ["YOUR_USER_ID"],
25+
"placeholder": {
26+
"enabled": true,
27+
"text": ["Thinking... 💭"]
28+
},
1729
"group_trigger": {
1830
"mention_only": false
19-
}
31+
},
32+
"reasoning_channel_id": ""
2033
}
2134
}
2235
}
2336
```
2437

25-
| Field | Type | Required | Description |
26-
| ------------- | ------ | -------- | --------------------------------------------------------------------------- |
27-
| enabled | bool | Yes | Whether to enable the Discord channel |
28-
| token | string | Yes | Discord Bot Token |
29-
| allow_from | array | No | Allowlist of user IDs; empty means all users are allowed |
30-
| group_trigger | object | No | Group trigger settings (example: { "mention_only": false }) |
38+
| Field | Type | Required | Description |
39+
| -------------------- | ------ | -------- | --------------------------------------------------------------------------- |
40+
| enabled | bool | Yes | Whether to enable the Discord channel |
41+
| token | string | Yes | Discord Bot Token |
42+
| allow_from | array | No | Allowlist of user IDs; empty means all users are allowed |
43+
| placeholder | object | No | Placeholder message config shown while the agent is working |
44+
| group_trigger | object | No | Group trigger settings (example: { "mention_only": false }) |
45+
| reasoning_channel_id | string | No | Optional target channel ID for reasoning/thinking output |
46+
47+
## Visible Execution Feedback
48+
49+
Discord can show three different kinds of "working" feedback:
50+
51+
1. Typing indicator: automatic, no extra config needed.
52+
2. Placeholder message: enable `channel_list.discord.placeholder.enabled` to send a visible `Thinking...` message that is later edited into the final reply.
53+
3. Tool execution feedback: enable `agents.defaults.tool_feedback.enabled` to send a short message before each tool call, for example:
54+
55+
```text
56+
🔧 `web_search`
57+
Checking the latest PicoClaw release notes before I answer.
58+
```
59+
60+
If you only see `Bot is typing`, check that `placeholder.enabled` or `tool_feedback.enabled` is actually set in your runtime config.
3161

3262
## Setup
3363

pkg/agent/loop.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ const (
112112
pendingTurnPrefix = "pending-"
113113
metadataKeyMessageKind = "message_kind"
114114
messageKindThought = "thought"
115+
messageKindToolFeedback = "tool_feedback"
115116
metadataKeyAccountID = "account_id"
116117
metadataKeyGuildID = "guild_id"
117118
metadataKeyTeamID = "team_id"

0 commit comments

Comments
 (0)