Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64629eaf15 |
@@ -167,7 +167,6 @@ type CreateOptions struct {
|
||||
WorkspaceID uuid.NullUUID
|
||||
ParentChatID uuid.NullUUID
|
||||
RootChatID uuid.NullUUID
|
||||
Title string
|
||||
ModelConfigID uuid.UUID
|
||||
SystemPrompt string
|
||||
InitialUserContent []fantasy.Content
|
||||
@@ -238,9 +237,6 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C
|
||||
if opts.OwnerID == uuid.Nil {
|
||||
return database.Chat{}, xerrors.New("owner_id is required")
|
||||
}
|
||||
if strings.TrimSpace(opts.Title) == "" {
|
||||
return database.Chat{}, xerrors.New("title is required")
|
||||
}
|
||||
if len(opts.InitialUserContent) == 0 {
|
||||
return database.Chat{}, xerrors.New("initial user content is required")
|
||||
}
|
||||
@@ -253,7 +249,6 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C
|
||||
ParentChatID: opts.ParentChatID,
|
||||
RootChatID: opts.RootChatID,
|
||||
LastModelConfigID: opts.ModelConfigID,
|
||||
Title: opts.Title,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert chat: %w", err)
|
||||
@@ -1601,11 +1596,13 @@ func (p *Server) publishChatPubsubEvent(chat database.Chat, kind coderdpubsub.Ch
|
||||
sdkChat := codersdk.Chat{
|
||||
ID: chat.ID,
|
||||
OwnerID: chat.OwnerID,
|
||||
Title: chat.Title,
|
||||
Status: codersdk.ChatStatus(chat.Status),
|
||||
CreatedAt: chat.CreatedAt,
|
||||
UpdatedAt: chat.UpdatedAt,
|
||||
}
|
||||
if chat.Title.Valid {
|
||||
sdkChat.Title = &chat.Title.String
|
||||
}
|
||||
if chat.ParentChatID.Valid {
|
||||
parentChatID := chat.ParentChatID.UUID
|
||||
sdkChat.ParentChatID = &parentChatID
|
||||
@@ -2030,7 +2027,7 @@ func (p *Server) processChat(ctx context.Context, chat database.Chat) {
|
||||
if p.webpushDispatcher != nil && p.webpushDispatcher.PublicKey() != "" && !chat.ParentChatID.Valid && !wasInterrupted {
|
||||
if status == database.ChatStatusWaiting || status == database.ChatStatusError {
|
||||
pushMsg := codersdk.WebpushMessage{
|
||||
Title: chat.Title,
|
||||
Title: chat.Title.String,
|
||||
Body: "Agent has finished running.",
|
||||
Icon: "/favicon.ico",
|
||||
Data: map[string]string{"url": fmt.Sprintf("/agents/%s", chat.ID)},
|
||||
|
||||
@@ -46,7 +46,6 @@ func TestInterruptChatBroadcastsStatusAcrossInstances(t *testing.T) {
|
||||
|
||||
chat, err := replicaA.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "interrupt-me",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -249,7 +248,6 @@ func TestInterruptChatClearsWorkerInDatabase(t *testing.T) {
|
||||
|
||||
chat, err := replica.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "db-transition",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -285,7 +283,6 @@ func TestUpdateChatHeartbeatRequiresOwnership(t *testing.T) {
|
||||
|
||||
chat, err := replica.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "heartbeat-ownership",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -327,7 +324,6 @@ func TestSendMessageQueueBehaviorQueuesWhenBusy(t *testing.T) {
|
||||
|
||||
chat, err := replica.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "queue-when-busy",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -378,7 +374,6 @@ func TestSendMessageInterruptBehaviorQueuesAndInterruptsWhenBusy(t *testing.T) {
|
||||
|
||||
chat, err := replica.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "interrupt-when-busy",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -437,7 +432,6 @@ func TestEditMessageUpdatesAndTruncatesAndClearsQueue(t *testing.T) {
|
||||
|
||||
chat, err := replica.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "edit-message",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "original"}},
|
||||
})
|
||||
@@ -525,7 +519,6 @@ func TestEditMessageRejectsMissingMessage(t *testing.T) {
|
||||
|
||||
chat, err := replica.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "missing-edited-message",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -551,7 +544,6 @@ func TestEditMessageRejectsNonUserMessage(t *testing.T) {
|
||||
|
||||
chat, err := replica.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "non-user-edited-message",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -603,7 +595,6 @@ func TestRecoverStaleChatsPeriodically(t *testing.T) {
|
||||
deadWorkerID := uuid.New()
|
||||
chat, err := db.InsertChat(ctx, database.InsertChatParams{
|
||||
OwnerID: user.ID,
|
||||
Title: "stale-recovery-periodic",
|
||||
LastModelConfigID: model.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -648,7 +639,6 @@ func TestRecoverStaleChatsPeriodically(t *testing.T) {
|
||||
deadWorkerID2 := uuid.New()
|
||||
chat2, err := db.InsertChat(ctx, database.InsertChatParams{
|
||||
OwnerID: user.ID,
|
||||
Title: "stale-recovery-periodic-2",
|
||||
LastModelConfigID: model.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -686,7 +676,6 @@ func TestNewReplicaRecoversStaleChatFromDeadReplica(t *testing.T) {
|
||||
deadReplicaID := uuid.New()
|
||||
chat, err := db.InsertChat(ctx, database.InsertChatParams{
|
||||
OwnerID: user.ID,
|
||||
Title: "orphaned-chat",
|
||||
LastModelConfigID: model.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -728,7 +717,6 @@ func TestWaitingChatsAreNotRecoveredAsStale(t *testing.T) {
|
||||
// by stale recovery.
|
||||
chat, err := db.InsertChat(ctx, database.InsertChatParams{
|
||||
OwnerID: user.ID,
|
||||
Title: "waiting-chat",
|
||||
LastModelConfigID: model.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -770,7 +758,6 @@ func TestUpdateChatStatusPersistsLastError(t *testing.T) {
|
||||
|
||||
chat, err := db.InsertChat(ctx, database.InsertChatParams{
|
||||
OwnerID: user.ID,
|
||||
Title: "error-persisted",
|
||||
LastModelConfigID: model.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -825,7 +812,6 @@ func TestSubscribeSnapshotIncludesStatusEvent(t *testing.T) {
|
||||
|
||||
chat, err := replica.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "status-snapshot",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -854,7 +840,6 @@ func TestSubscribeNoPubsubNoDuplicateMessageParts(t *testing.T) {
|
||||
|
||||
chat, err := replica.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "no-dup-parts",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -894,7 +879,6 @@ func TestSubscribeAfterMessageID(t *testing.T) {
|
||||
// Create a chat — this inserts one initial "user" message.
|
||||
chat, err := replica.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "after-id-test",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "first"}},
|
||||
})
|
||||
@@ -1423,7 +1407,6 @@ func TestInterruptChatDoesNotSendWebPushNotification(t *testing.T) {
|
||||
|
||||
chat, err := server.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "interrupt-no-push",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -1528,7 +1511,6 @@ func TestSuccessfulChatSendsWebPushWithNavigationData(t *testing.T) {
|
||||
|
||||
chat, err := server.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "push-nav-test",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -1612,7 +1594,6 @@ func TestCloseDuringShutdownContextCanceledShouldRetryOnNewReplica(t *testing.T)
|
||||
|
||||
chat, err := serverA.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "shutdown-retry",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
|
||||
@@ -35,7 +35,6 @@ func TestStartWorkspace(t *testing.T) {
|
||||
chat, err := db.InsertChat(ctx, database.InsertChatParams{
|
||||
OwnerID: user.ID,
|
||||
LastModelConfigID: modelCfg.ID,
|
||||
Title: "test-no-workspace",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -78,7 +77,6 @@ func TestStartWorkspace(t *testing.T) {
|
||||
OwnerID: user.ID,
|
||||
WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true},
|
||||
LastModelConfigID: modelCfg.ID,
|
||||
Title: "test-already-running",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -133,7 +131,6 @@ func TestStartWorkspace(t *testing.T) {
|
||||
OwnerID: user.ID,
|
||||
WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true},
|
||||
LastModelConfigID: modelCfg.ID,
|
||||
Title: "test-stopped-workspace",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ func (p *Server) subagentTools(currentChat func() database.Chat) []fantasy.Agent
|
||||
|
||||
return toolJSONResponse(map[string]any{
|
||||
"chat_id": childChat.ID.String(),
|
||||
"title": childChat.Title,
|
||||
"title": childChat.Title.String,
|
||||
"status": string(childChat.Status),
|
||||
}), nil
|
||||
},
|
||||
@@ -128,7 +128,7 @@ func (p *Server) subagentTools(currentChat func() database.Chat) []fantasy.Agent
|
||||
|
||||
return toolJSONResponse(map[string]any{
|
||||
"chat_id": targetChatID.String(),
|
||||
"title": targetChat.Title,
|
||||
"title": targetChat.Title.String,
|
||||
"report": report,
|
||||
"status": string(targetChat.Status),
|
||||
}), nil
|
||||
@@ -169,7 +169,7 @@ func (p *Server) subagentTools(currentChat func() database.Chat) []fantasy.Agent
|
||||
|
||||
return toolJSONResponse(map[string]any{
|
||||
"chat_id": targetChatID.String(),
|
||||
"title": targetChat.Title,
|
||||
"title": targetChat.Title.String,
|
||||
"status": string(targetChat.Status),
|
||||
"interrupted": args.Interrupt,
|
||||
}), nil
|
||||
@@ -202,7 +202,7 @@ func (p *Server) subagentTools(currentChat func() database.Chat) []fantasy.Agent
|
||||
|
||||
return toolJSONResponse(map[string]any{
|
||||
"chat_id": targetChatID.String(),
|
||||
"title": targetChat.Title,
|
||||
"title": targetChat.Title.String,
|
||||
"terminated": true,
|
||||
"status": string(targetChat.Status),
|
||||
}), nil
|
||||
@@ -223,7 +223,7 @@ func (p *Server) createChildSubagentChat(
|
||||
ctx context.Context,
|
||||
parent database.Chat,
|
||||
prompt string,
|
||||
title string,
|
||||
_ string,
|
||||
) (database.Chat, error) {
|
||||
if parent.ParentChatID.Valid {
|
||||
return database.Chat{}, xerrors.New("delegated chats cannot create child subagents")
|
||||
@@ -234,11 +234,6 @@ func (p *Server) createChildSubagentChat(
|
||||
return database.Chat{}, xerrors.New("prompt is required")
|
||||
}
|
||||
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" {
|
||||
title = subagentFallbackChatTitle(prompt)
|
||||
}
|
||||
|
||||
rootChatID := parent.ID
|
||||
if parent.RootChatID.Valid {
|
||||
rootChatID = parent.RootChatID.UUID
|
||||
@@ -259,7 +254,6 @@ func (p *Server) createChildSubagentChat(
|
||||
Valid: true,
|
||||
},
|
||||
ModelConfigID: parent.LastModelConfigID,
|
||||
Title: title,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: prompt}},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -494,42 +488,6 @@ func listSubagentDescendants(
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func subagentFallbackChatTitle(message string) string {
|
||||
const maxWords = 6
|
||||
const maxRunes = 80
|
||||
|
||||
words := strings.Fields(message)
|
||||
if len(words) == 0 {
|
||||
return "New Chat"
|
||||
}
|
||||
|
||||
truncated := false
|
||||
if len(words) > maxWords {
|
||||
words = words[:maxWords]
|
||||
truncated = true
|
||||
}
|
||||
|
||||
title := strings.Join(words, " ")
|
||||
if truncated {
|
||||
title += "..."
|
||||
}
|
||||
|
||||
return subagentTruncateRunes(title, maxRunes)
|
||||
}
|
||||
|
||||
func subagentTruncateRunes(value string, maxRunes int) string {
|
||||
if maxRunes <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
runes := []rune(value)
|
||||
if len(runes) <= maxRunes {
|
||||
return value
|
||||
}
|
||||
|
||||
return string(runes[:maxRunes])
|
||||
}
|
||||
|
||||
func toolJSONResponse(result map[string]any) fantasy.ToolResponse {
|
||||
data, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
|
||||
+12
-38
@@ -2,6 +2,7 @@ package chatd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -92,13 +93,13 @@ func (p *Server) maybeGenerateChatTitle(
|
||||
)
|
||||
continue
|
||||
}
|
||||
if title == "" || title == chat.Title {
|
||||
if title == "" {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = p.db.UpdateChatByID(ctx, database.UpdateChatByIDParams{
|
||||
ID: chat.ID,
|
||||
Title: title,
|
||||
Title: sql.NullString{String: title, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn(ctx, "failed to update generated chat title",
|
||||
@@ -107,7 +108,7 @@ func (p *Server) maybeGenerateChatTitle(
|
||||
)
|
||||
return
|
||||
}
|
||||
chat.Title = title
|
||||
chat.Title = sql.NullString{String: title, Valid: true}
|
||||
p.publishChatPubsubEvent(chat, coderdpubsub.ChatEventKindTitleChange)
|
||||
return
|
||||
}
|
||||
@@ -167,12 +168,17 @@ func generateTitle(
|
||||
|
||||
// titleInput returns the first user message text and whether title
|
||||
// generation should proceed. It returns false when the chat already
|
||||
// has assistant/tool replies, has more than one visible user message,
|
||||
// or the current title doesn't look like a candidate for replacement.
|
||||
// has a title set, has assistant/tool replies, or no user text is
|
||||
// available.
|
||||
func titleInput(
|
||||
chat database.Chat,
|
||||
messages []database.ChatMessage,
|
||||
) (string, bool) {
|
||||
// Only generate a title if one hasn't been set yet.
|
||||
if chat.Title.Valid {
|
||||
return "", false
|
||||
}
|
||||
|
||||
userCount := 0
|
||||
firstUserText := ""
|
||||
|
||||
@@ -200,16 +206,7 @@ func titleInput(
|
||||
}
|
||||
}
|
||||
|
||||
if userCount != 1 || firstUserText == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
currentTitle := strings.TrimSpace(chat.Title)
|
||||
if currentTitle == "" {
|
||||
return firstUserText, true
|
||||
}
|
||||
|
||||
if currentTitle != fallbackChatTitle(firstUserText) {
|
||||
if userCount == 0 || firstUserText == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -227,29 +224,6 @@ func normalizeTitleOutput(title string) string {
|
||||
return truncateRunes(title, 80)
|
||||
}
|
||||
|
||||
func fallbackChatTitle(message string) string {
|
||||
const maxWords = 6
|
||||
const maxRunes = 80
|
||||
|
||||
words := strings.Fields(message)
|
||||
if len(words) == 0 {
|
||||
return "New Chat"
|
||||
}
|
||||
|
||||
truncated := false
|
||||
if len(words) > maxWords {
|
||||
words = words[:maxWords]
|
||||
truncated = true
|
||||
}
|
||||
|
||||
title := strings.Join(words, " ")
|
||||
if truncated {
|
||||
title += "…"
|
||||
}
|
||||
|
||||
return truncateRunes(title, maxRunes)
|
||||
}
|
||||
|
||||
// contentBlocksToText concatenates the text parts of content blocks
|
||||
// into a single space-separated string.
|
||||
func contentBlocksToText(content []fantasy.Content) string {
|
||||
|
||||
+4
-37
@@ -262,7 +262,7 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
contentBlocks, contentFileIDs, titleSource, inputError := createChatInputFromRequest(ctx, api.Database, req)
|
||||
contentBlocks, contentFileIDs, _, inputError := createChatInputFromRequest(ctx, api.Database, req)
|
||||
if inputError != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, *inputError)
|
||||
return
|
||||
@@ -274,8 +274,6 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
title := chatTitleFromMessage(titleSource)
|
||||
|
||||
if api.chatDaemon == nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Chat processor is unavailable.",
|
||||
@@ -293,7 +291,6 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
|
||||
chat, err := api.chatDaemon.CreateChat(ctx, chatd.CreateOptions{
|
||||
OwnerID: apiKey.UserID,
|
||||
WorkspaceID: workspaceSelection.WorkspaceID,
|
||||
Title: title,
|
||||
ModelConfigID: modelConfigID,
|
||||
SystemPrompt: api.resolvedChatSystemPrompt(ctx),
|
||||
InitialUserContent: contentBlocks,
|
||||
@@ -2586,49 +2583,19 @@ func createChatInputFromParts(
|
||||
return content, fileIDs, titleSource, nil
|
||||
}
|
||||
|
||||
func chatTitleFromMessage(message string) string {
|
||||
const maxWords = 6
|
||||
const maxRunes = 80
|
||||
words := strings.Fields(message)
|
||||
if len(words) == 0 {
|
||||
return "New Chat"
|
||||
}
|
||||
truncated := false
|
||||
if len(words) > maxWords {
|
||||
words = words[:maxWords]
|
||||
truncated = true
|
||||
}
|
||||
title := strings.Join(words, " ")
|
||||
if truncated {
|
||||
title += "…"
|
||||
}
|
||||
return truncateRunes(title, maxRunes)
|
||||
}
|
||||
|
||||
func truncateRunes(value string, maxLen int) string {
|
||||
if maxLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
runes := []rune(value)
|
||||
if len(runes) <= maxLen {
|
||||
return value
|
||||
}
|
||||
|
||||
return string(runes[:maxLen])
|
||||
}
|
||||
|
||||
func convertChat(c database.Chat, diffStatus *database.ChatDiffStatus) codersdk.Chat {
|
||||
chat := codersdk.Chat{
|
||||
ID: c.ID,
|
||||
OwnerID: c.OwnerID,
|
||||
LastModelConfigID: c.LastModelConfigID,
|
||||
Title: c.Title,
|
||||
Status: codersdk.ChatStatus(c.Status),
|
||||
Archived: c.Archived,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
if c.Title.Valid {
|
||||
chat.Title = &c.Title.String
|
||||
}
|
||||
if c.LastError.Valid {
|
||||
chat.LastError = &c.LastError.String
|
||||
}
|
||||
|
||||
+11
-21
@@ -75,7 +75,7 @@ func TestPostChats(t *testing.T) {
|
||||
require.NotEqual(t, uuid.Nil, chat.ID)
|
||||
require.Equal(t, user.UserID, chat.OwnerID)
|
||||
require.Equal(t, modelConfig.ID, chat.LastModelConfigID)
|
||||
require.Equal(t, "hello from chats route tests", chat.Title)
|
||||
require.Nil(t, chat.Title)
|
||||
require.Equal(t, codersdk.ChatStatusPending, chat.Status)
|
||||
require.NotZero(t, chat.CreatedAt)
|
||||
require.NotZero(t, chat.UpdatedAt)
|
||||
@@ -321,7 +321,6 @@ func TestListChats(t *testing.T) {
|
||||
memberDBChat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
|
||||
OwnerID: member.ID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "member chat only",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -351,8 +350,8 @@ func TestListChats(t *testing.T) {
|
||||
require.Contains(t, chatsByID, firstChatA.ID)
|
||||
require.Contains(t, chatsByID, firstChatB.ID)
|
||||
require.NotContains(t, chatsByID, memberDBChat.ID)
|
||||
require.Equal(t, "first owner chat", chatsByID[firstChatA.ID].Title)
|
||||
require.Equal(t, "second owner chat", chatsByID[firstChatB.ID].Title)
|
||||
require.Nil(t, chatsByID[firstChatA.ID].Title)
|
||||
require.Nil(t, chatsByID[firstChatB.ID].Title)
|
||||
|
||||
for i := 1; i < len(chats); i++ {
|
||||
require.False(t, chats[i-1].UpdatedAt.Before(chats[i].UpdatedAt))
|
||||
@@ -369,7 +368,7 @@ func TestListChats(t *testing.T) {
|
||||
require.Len(t, memberChats, 1)
|
||||
require.Equal(t, memberDBChat.ID, memberChats[0].ID)
|
||||
require.Equal(t, member.ID, memberChats[0].OwnerID)
|
||||
require.Equal(t, "member chat only", memberChats[0].Title)
|
||||
require.Nil(t, memberChats[0].Title)
|
||||
require.NotNil(t, memberChats[0].RootChatID)
|
||||
require.Equal(t, memberChats[0].ID, *memberChats[0].RootChatID)
|
||||
require.NotNil(t, memberChats[0].DiffStatus)
|
||||
@@ -1196,7 +1195,7 @@ func TestGetChat(t *testing.T) {
|
||||
require.Equal(t, createdChat.ID, chatWithMessages.Chat.ID)
|
||||
require.Equal(t, firstUser.UserID, chatWithMessages.Chat.OwnerID)
|
||||
require.Equal(t, modelConfig.ID, chatWithMessages.Chat.LastModelConfigID)
|
||||
require.Equal(t, "get chat route payload", chatWithMessages.Chat.Title)
|
||||
require.Nil(t, chatWithMessages.Chat.Title)
|
||||
require.NotZero(t, chatWithMessages.Chat.CreatedAt)
|
||||
require.NotZero(t, chatWithMessages.Chat.UpdatedAt)
|
||||
require.NotEmpty(t, chatWithMessages.Messages)
|
||||
@@ -1337,7 +1336,6 @@ func TestArchiveChat(t *testing.T) {
|
||||
child1, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
|
||||
OwnerID: user.UserID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "child 1",
|
||||
ParentChatID: uuid.NullUUID{UUID: parentChat.ID, Valid: true},
|
||||
RootChatID: uuid.NullUUID{UUID: parentChat.ID, Valid: true},
|
||||
})
|
||||
@@ -1346,7 +1344,6 @@ func TestArchiveChat(t *testing.T) {
|
||||
child2, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
|
||||
OwnerID: user.UserID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "child 2",
|
||||
ParentChatID: uuid.NullUUID{UUID: parentChat.ID, Valid: true},
|
||||
RootChatID: uuid.NullUUID{UUID: parentChat.ID, Valid: true},
|
||||
})
|
||||
@@ -1971,9 +1968,9 @@ func TestChatMessageWithFileReferences(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, uuid.Nil, chat.ID)
|
||||
|
||||
// Title is derived from the text parts. For file-references
|
||||
// the formatted text becomes the title source.
|
||||
require.NotEmpty(t, chat.Title)
|
||||
// Title is not set on creation; it is populated
|
||||
// asynchronously by the chat daemon.
|
||||
require.Nil(t, chat.Title)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2108,8 +2105,9 @@ func TestChatMessageWithFiles(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// With no text, chatTitleFromMessage("") returns "New Chat".
|
||||
require.Equal(t, "New Chat", chat.Title)
|
||||
// Title is not set on creation; it is populated
|
||||
// asynchronously by the chat daemon.
|
||||
require.Nil(t, chat.Title)
|
||||
})
|
||||
|
||||
t.Run("InvalidFileID", func(t *testing.T) {
|
||||
@@ -2476,7 +2474,6 @@ func TestInterruptChat(t *testing.T) {
|
||||
chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
|
||||
OwnerID: user.UserID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "interrupt route test",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -2544,7 +2541,6 @@ func TestGetChatDiffStatus(t *testing.T) {
|
||||
noCachedStatusChat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
|
||||
OwnerID: user.UserID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "get diff status route no cache",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -2563,7 +2559,6 @@ func TestGetChatDiffStatus(t *testing.T) {
|
||||
cachedStatusChat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
|
||||
OwnerID: user.UserID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "get diff status route cached",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -2666,7 +2661,6 @@ func TestGetChatDiffContents(t *testing.T) {
|
||||
chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
|
||||
OwnerID: user.UserID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "diff contents with cached repository reference",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -2761,7 +2755,6 @@ func TestDeleteChatQueuedMessage(t *testing.T) {
|
||||
chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
|
||||
OwnerID: user.UserID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "delete queued message route test",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -2808,7 +2801,6 @@ func TestDeleteChatQueuedMessage(t *testing.T) {
|
||||
chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
|
||||
OwnerID: user.UserID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "delete queued invalid id",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -2842,7 +2834,6 @@ func TestPromoteChatQueuedMessage(t *testing.T) {
|
||||
chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
|
||||
OwnerID: user.UserID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "promote queued message route test",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -2907,7 +2898,6 @@ func TestPromoteChatQueuedMessage(t *testing.T) {
|
||||
chat, err := db.InsertChat(dbauthz.AsSystemRestricted(ctx), database.InsertChatParams{
|
||||
OwnerID: user.UserID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "promote queued invalid id",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -648,7 +648,7 @@ func (s *MethodTestSuite) TestChats() {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
arg := database.UpdateChatByIDParams{
|
||||
ID: chat.ID,
|
||||
Title: "Updated title",
|
||||
Title: sql.NullString{String: "Updated title", Valid: true},
|
||||
}
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateChatByID(gomock.Any(), arg).Return(chat, nil).AnyTimes()
|
||||
|
||||
Generated
+1
-1
@@ -1283,7 +1283,7 @@ CREATE TABLE chats (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
workspace_id uuid,
|
||||
title text DEFAULT 'New Chat'::text NOT NULL,
|
||||
title text,
|
||||
status chat_status DEFAULT 'waiting'::chat_status NOT NULL,
|
||||
worker_id uuid,
|
||||
started_at timestamp with time zone,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
UPDATE chats SET title = 'New Chat' WHERE title IS NULL;
|
||||
ALTER TABLE chats ALTER COLUMN title SET NOT NULL;
|
||||
ALTER TABLE chats ALTER COLUMN title SET DEFAULT 'New Chat';
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE chats ALTER COLUMN title DROP NOT NULL;
|
||||
ALTER TABLE chats ALTER COLUMN title DROP DEFAULT;
|
||||
@@ -3896,7 +3896,7 @@ type Chat struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
|
||||
Title string `db:"title" json:"title"`
|
||||
Title sql.NullString `db:"title" json:"title"`
|
||||
Status ChatStatus `db:"status" json:"status"`
|
||||
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
|
||||
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
|
||||
|
||||
@@ -8957,7 +8957,6 @@ func TestGetChatMessagesForPromptByChatID(t *testing.T) {
|
||||
chat, err := db.InsertChat(ctx, database.InsertChatParams{
|
||||
OwnerID: user.ID,
|
||||
LastModelConfigID: modelCfg.ID,
|
||||
Title: "test-chat-" + uuid.NewString(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return chat
|
||||
|
||||
@@ -3593,15 +3593,13 @@ INSERT INTO chats (
|
||||
workspace_id,
|
||||
parent_chat_id,
|
||||
root_chat_id,
|
||||
last_model_config_id,
|
||||
title
|
||||
last_model_config_id
|
||||
) VALUES (
|
||||
$1::uuid,
|
||||
$2::uuid,
|
||||
$3::uuid,
|
||||
$4::uuid,
|
||||
$5::uuid,
|
||||
$6::text
|
||||
$5::uuid
|
||||
)
|
||||
RETURNING
|
||||
id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error
|
||||
@@ -3613,7 +3611,6 @@ type InsertChatParams struct {
|
||||
ParentChatID uuid.NullUUID `db:"parent_chat_id" json:"parent_chat_id"`
|
||||
RootChatID uuid.NullUUID `db:"root_chat_id" json:"root_chat_id"`
|
||||
LastModelConfigID uuid.UUID `db:"last_model_config_id" json:"last_model_config_id"`
|
||||
Title string `db:"title" json:"title"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat, error) {
|
||||
@@ -3623,7 +3620,6 @@ func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat
|
||||
arg.ParentChatID,
|
||||
arg.RootChatID,
|
||||
arg.LastModelConfigID,
|
||||
arg.Title,
|
||||
)
|
||||
var i Chat
|
||||
err := row.Scan(
|
||||
@@ -3910,8 +3906,8 @@ RETURNING
|
||||
`
|
||||
|
||||
type UpdateChatByIDParams struct {
|
||||
Title string `db:"title" json:"title"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Title sql.NullString `db:"title" json:"title"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateChatByID(ctx context.Context, arg UpdateChatByIDParams) (Chat, error) {
|
||||
|
||||
@@ -167,15 +167,13 @@ INSERT INTO chats (
|
||||
workspace_id,
|
||||
parent_chat_id,
|
||||
root_chat_id,
|
||||
last_model_config_id,
|
||||
title
|
||||
last_model_config_id
|
||||
) VALUES (
|
||||
@owner_id::uuid,
|
||||
sqlc.narg('workspace_id')::uuid,
|
||||
sqlc.narg('parent_chat_id')::uuid,
|
||||
sqlc.narg('root_chat_id')::uuid,
|
||||
@last_model_config_id::uuid,
|
||||
@title::text
|
||||
@last_model_config_id::uuid
|
||||
)
|
||||
RETURNING
|
||||
*;
|
||||
@@ -237,7 +235,7 @@ RETURNING
|
||||
UPDATE
|
||||
chats
|
||||
SET
|
||||
title = @title::text,
|
||||
title = sqlc.narg('title')::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
|
||||
@@ -67,7 +67,6 @@ func TestChatParam(t *testing.T) {
|
||||
ParentChatID: uuid.NullUUID{},
|
||||
RootChatID: uuid.NullUUID{},
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "Test chat",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ type Chat struct {
|
||||
ParentChatID *uuid.UUID `json:"parent_chat_id,omitempty" format:"uuid"`
|
||||
RootChatID *uuid.UUID `json:"root_chat_id,omitempty" format:"uuid"`
|
||||
LastModelConfigID uuid.UUID `json:"last_model_config_id" format:"uuid"`
|
||||
Title string `json:"title"`
|
||||
Title *string `json:"title"`
|
||||
Status ChatStatus `json:"status"`
|
||||
LastError *string `json:"last_error"`
|
||||
DiffStatus *ChatDiffStatus `json:"diff_status,omitempty"`
|
||||
|
||||
@@ -150,7 +150,6 @@ func TestSubscribeRelayReconnectsOnDrop(t *testing.T) {
|
||||
// Create a chat and mark it as running on a remote worker.
|
||||
chat, err := subscriber.CreateChat(ctx, osschatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "relay-reconnect",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -244,7 +243,6 @@ func TestSubscribeRelayAsyncDoesNotBlock(t *testing.T) {
|
||||
// Create a chat in pending status.
|
||||
chat, err := subscriber.CreateChat(ctx, osschatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "relay-async-nonblock",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -351,7 +349,6 @@ func TestSubscribeRelaySnapshotDelivered(t *testing.T) {
|
||||
// Create a chat already running on a remote worker.
|
||||
chat, err := subscriber.CreateChat(ctx, osschatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "relay-snapshot",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -464,7 +461,6 @@ func TestSubscribeRelayStaleDialDiscardedAfterInterrupt(t *testing.T) {
|
||||
|
||||
chat, err := subscriber.CreateChat(ctx, osschatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "stale-dial-test",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -627,7 +623,6 @@ func TestSubscribeCancelDuringInFlightDial(t *testing.T) {
|
||||
|
||||
chat, err := subscriber.CreateChat(ctx, osschatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "cancel-inflight-dial",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -725,7 +720,6 @@ func TestSubscribeRelayRunningToRunningSwitch(t *testing.T) {
|
||||
|
||||
chat, err := subscriber.CreateChat(ctx, osschatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "running-to-running",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -847,7 +841,6 @@ func TestSubscribeRelayFailedDialRetries(t *testing.T) {
|
||||
// synchronous relay.
|
||||
chat, err := subscriber.CreateChat(ctx, osschatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "failed-dial-retry",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -957,7 +950,6 @@ func TestSubscribeRunningLocalWorkerClosesRelay(t *testing.T) {
|
||||
// opens a synchronous relay.
|
||||
chat, err := subscriber.CreateChat(ctx, osschatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "local-worker-closes-relay",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
@@ -1065,7 +1057,6 @@ func TestSubscribeRelayMultipleReconnects(t *testing.T) {
|
||||
// Subscribe opens a synchronous relay immediately.
|
||||
chat, err := subscriber.CreateChat(ctx, osschatd.CreateOptions{
|
||||
OwnerID: user.ID,
|
||||
Title: "multiple-reconnects",
|
||||
ModelConfigID: model.ID,
|
||||
InitialUserContent: []fantasy.Content{fantasy.TextContent{Text: "hello"}},
|
||||
})
|
||||
|
||||
Generated
+1
-1
@@ -1059,7 +1059,7 @@ export interface Chat {
|
||||
readonly parent_chat_id?: string;
|
||||
readonly root_chat_id?: string;
|
||||
readonly last_model_config_id: string;
|
||||
readonly title: string;
|
||||
readonly title: string | null;
|
||||
readonly status: ChatStatus;
|
||||
readonly last_error: string | null;
|
||||
readonly diff_status?: ChatDiffStatus;
|
||||
|
||||
@@ -957,7 +957,7 @@ const AgentDetail: FC = () => {
|
||||
inputValueRef,
|
||||
});
|
||||
|
||||
const chatTitle = chatQuery.data?.chat?.title;
|
||||
const chatTitle = chatQuery.data?.chat?.title ?? undefined;
|
||||
|
||||
const titleElement = (
|
||||
<title>
|
||||
|
||||
@@ -107,7 +107,9 @@ export const AgentDetailTopBar: FC<AgentDetailTopBarProps> = ({
|
||||
className="h-auto max-w-[16rem] rounded-sm px-1 py-0.5 text-xs text-content-secondary shadow-none hover:bg-transparent hover:text-content-primary"
|
||||
onClick={() => onOpenParentChat(parentChat.id)}
|
||||
>
|
||||
<span className="truncate">{parentChat.title}</span>
|
||||
<span className="truncate">
|
||||
{parentChat.title ?? "Untitled"}
|
||||
</span>
|
||||
</Button>
|
||||
<ChevronRightIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary/70" />
|
||||
</>
|
||||
|
||||
@@ -215,7 +215,7 @@ const collectVisibleChatIDs = ({
|
||||
}
|
||||
|
||||
const matchedChatIDs = chats
|
||||
.filter((chat) => chat.title.toLowerCase().includes(search))
|
||||
.filter((chat) => (chat.title ?? "").toLowerCase().includes(search))
|
||||
.map((chat) => chat.id);
|
||||
if (matchedChatIDs.length === 0) {
|
||||
return new Set<string>();
|
||||
@@ -413,7 +413,9 @@ const ChatTreeNode = memo<ChatTreeNodeProps>(({ chat, isChildNode }) => {
|
||||
isActive && "font-medium",
|
||||
)}
|
||||
>
|
||||
{chat.title}
|
||||
{chat.title ?? (
|
||||
<span className="inline-block h-3.5 w-24 animate-pulse rounded bg-surface-tertiary" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
@@ -460,7 +462,7 @@ const ChatTreeNode = memo<ChatTreeNodeProps>(({ chat, isChildNode }) => {
|
||||
size="icon"
|
||||
variant="subtle"
|
||||
className="absolute inset-0 flex h-6 w-7 min-w-0 justify-end rounded-none px-0 opacity-0 text-content-secondary hover:text-content-primary [@media(hover:hover)]:group-hover:opacity-100 data-[state=open]:opacity-100"
|
||||
aria-label={`Open actions for ${chat.title}`}
|
||||
aria-label={`Open actions for ${chat.title ?? "Untitled"}`}
|
||||
>
|
||||
<EllipsisIcon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
|
||||
@@ -44,12 +44,15 @@ self.addEventListener("push", (event) => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return self.registration.showNotification(payload.title, {
|
||||
body: payload.body || "",
|
||||
icon: payload.icon || "/favicon.ico",
|
||||
data: payload.data,
|
||||
tag: payload.tag,
|
||||
});
|
||||
return self.registration.showNotification(
|
||||
payload.title ?? "New notification",
|
||||
{
|
||||
body: payload.body || "",
|
||||
icon: payload.icon || "/favicon.ico",
|
||||
data: payload.data,
|
||||
tag: payload.tag,
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user