Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/evidence/current-chat-search/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Current chat search verification

Captured on Windows with Chrome against the production Go API and `web/dist`,
using `cmd/workbenchjourney` with a disposable workspace and its offline provider.
The two sample chats have different titles and bodies containing `请求超时`.
Screenshots use the real search endpoint and saved message navigation.

| Screenshot | Build and observed behavior |
| --- | --- |
| [Before](before-global-search.png) | Base `96ba29bf`, 1440 × 1000. Global search shows both chats' titles and messages: four matches. |
| [Current chat](after-current-search.png) | This change, 1440 × 1000. Current scope shows one matching body; selecting it opens that message's history window. |
| [Narrow window](after-current-search-narrow.png) | This change, 390 × 844. Ctrl+F reopens the hidden sidebar, focuses the retained query and keeps the scope controls within the viewport. |

The baseline UI bundle digest was
`beedd22bc9c451fb4d4a60d914275544d0fbb4a6b134b5eb52bac1b9ff9bc1f4`;
the final bundle digest was
`78d6b88f6c424bb8718cf4779e7a4fc6f3cc24cd7c861580b0471563a14ddea6`.
Ctrl+K returned to all chats and four results. Ctrl+F selected the current chat.
The narrow viewport had no horizontal document overflow.

Focused checks passed:

- Go search, full-message lookup and OpenAPI tests in `internal/httpapi` and
`internal/store`, including a real branch with an inherited long assistant reply,
cross-Run compacted history, scoped cursors, lifecycle filters and redaction.
- The affected project-move tests in `internal/application`.
- React search, sidebar, keyboard settings and app tests: 93 tests. The final
drawer-focus adjustment also passed the 19 search tests.
- `npm run check:api`, `npm run build`, `git diff --check`, and
`go run ./cmd/protocolregistry -check -baseline 96ba29bff4ae2a6b92e563fe0e5e5dbf93a2840d`.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 11 additions & 1 deletion docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -27639,7 +27639,7 @@
"get": {
"operationId": "searchSavedThreadMessages",
"summary": "Search saved conversation titles and public message bodies",
"description": "Searches complete saved user and assistant text, including compacted history, before pagination. Uses literal ASCII case-insensitive substrings, with exact matching for other Unicode. Redacts and checks the existing public transcript projection before returning a match. Excludes deleted conversations, tools, peer messages and internal events. Defaults to active conversations; include_archived also includes read-only archived history. Results use creation-time and identity cursors scoped to the normalized query and archive filter.",
"description": "Searches complete saved user and assistant text, including compacted history, public model progress and inherited branch replies, before pagination. With thread_id, searches only message bodies in that exact conversation; otherwise also searches conversation titles. Uses literal ASCII case-insensitive substrings, with exact matching for other Unicode. Redacts and checks the existing public transcript projection before returning a match. Excludes deleted conversations, tools, peer messages and internal events. Defaults to active conversations; include_archived also includes read-only archived history. Results use creation-time and identity cursors scoped to the normalized query, Thread and archive filter.",
"tags": [
"Threads"
],
Expand Down Expand Up @@ -27676,6 +27676,16 @@
"type": "string"
}
},
{
"name": "thread_id",
"in": "query",
"description": "Search saved public message bodies in this exact conversation",
"schema": {
"maxLength": 256,
"minLength": 1,
"type": "string"
}
},
{
"name": "include_archived",
"in": "query",
Expand Down
19 changes: 11 additions & 8 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -887,15 +887,18 @@ During an attempt, `run events` may contain at most 32 ordered `model.delta` rec

### Unified Thread transcript / 统一 Thread 主工作面

侧栏搜索通过 `GET /api/v1/threads?status=active&q=...` 检索全部未归档任务的标题,
无需先加载旧任务;不搜索消息正文,归档任务仍在设置中查看。查询会去掉首尾空白,最多
256 个 Unicode 字符,ASCII 字母不区分大小写,其他 Unicode 字符精确匹配;`%`、`_`
和反斜杠按普通字符匹配。空查询与不传 `q` 相同。

结果按创建时间倒序排列,同一时间按任务 ID 倒序。继续使用响应的 `next_cursor` 获取
更早匹配项;修改查询或生命周期筛选后须从第一页开始。并发新增较新任务不会移动旧页,
侧栏搜索通过 `GET /api/v1/threads/search?q=...` 检索已保存的标题与用户、助手正文,
包括压缩前及尚未加载的历史、公开进度和分支继承的回复。选择「当前聊天」时附带 `thread_id`,只搜索当前聊天的正文;
「所有聊天」恢复跨聊天搜索。默认不包含已归档聊天,勾选「包含已归档对话」后可只读查找。
打开聊天后按 `Ctrl/Cmd+F` 进入当前聊天搜索,`Ctrl/Cmd+K` 进入全局搜索;可在快捷键设置中修改。
查询会去掉首尾空白,最多 256 个 Unicode 字符,ASCII 字母不区分大小写,其他 Unicode 字符
精确匹配;`%`、`_` 和反斜杠按普通字符匹配。清空查询返回任务列表。

结果按创建时间倒序排列,同一时间按记录 ID 倒序。继续使用响应的 `next_cursor` 获取
更早匹配项;修改查询、聊天范围或归档筛选后须从第一页开始。并发新增较新消息不会移动旧页,
但重命名、归档或删除会改变后续查询的结果集。搜索、翻页与清空搜索不会切换当前任务,
也不会覆盖正在编辑的草稿。
也不会覆盖正在编辑的草稿。点击消息结果会直接定位到原消息所在的历史窗口,包括较早 Run。
搜索沿用公开记录检查和脱敏规则,不返回工具结果、内部事件、其他聊天的协作消息或已删除聊天。

列表同时返回 Go 投影的 `execution_state`,区分执行中、停止中、等待审批、暂停、空闲和
终态。输入框可编辑(`composer_state=ready`)不表示空闲。状态读取失败、缺少执行观察源
Expand Down
2 changes: 1 addition & 1 deletion internal/application/thread_project_move_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func TestThreadProjectMoveHistoryBranchAndUndoStayAtSource(t *testing.T) {
if err != nil || len(page.Records) == 0 {
t.Fatalf("history=%+v %v", page, err)
}
matches, err := st.SearchThreadMessages(t.Context(), "SOURCE_HISTORY_CANARY", false, time.Time{}, "", 20)
matches, err := st.SearchThreadMessages(t.Context(), "SOURCE_HISTORY_CANARY", "", false, time.Time{}, "", 20)
if err != nil || len(matches) != 1 || matches[0].ThreadID != moved.Thread.ID {
t.Fatalf("saved search=%+v %v", matches, err)
}
Expand Down
3 changes: 2 additions & 1 deletion internal/httpapi/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -1074,10 +1074,11 @@ func openAPIOperationSpecs() []openAPIOperationSpec {
}},
{Path: "/api/v1/threads/search", OperationID: "searchSavedThreadMessages",
Summary: "Search saved conversation titles and public message bodies", Tag: "Threads",
Description: "Searches complete saved user and assistant text, including compacted history, before pagination. Uses literal ASCII case-insensitive substrings, with exact matching for other Unicode. Redacts and checks the existing public transcript projection before returning a match. Excludes deleted conversations, tools, peer messages and internal events. Defaults to active conversations; include_archived also includes read-only archived history. Results use creation-time and identity cursors scoped to the normalized query and archive filter.",
Description: "Searches complete saved user and assistant text, including compacted history, public model progress and inherited branch replies, before pagination. With thread_id, searches only message bodies in that exact conversation; otherwise also searches conversation titles. Uses literal ASCII case-insensitive substrings, with exact matching for other Unicode. Redacts and checks the existing public transcript projection before returning a match. Excludes deleted conversations, tools, peer messages and internal events. Defaults to active conversations; include_archived also includes read-only archived history. Results use creation-time and identity cursors scoped to the normalized query, Thread and archive filter.",
DataType: reflect.TypeOf(ThreadSearchResultView{}), Collection: true, Paginated: true,
Parameters: append(paginationParameters(),
openAPIParameter{Name: "q", In: "query", Required: true, Schema: map[string]any{"type": "string", "minLength": 1, "maxLength": 256}},
openAPIParameter{Name: "thread_id", In: "query", Description: "Search saved public message bodies in this exact conversation", Schema: map[string]any{"type": "string", "minLength": 1, "maxLength": 256}},
booleanQueryParameter("include_archived", "Include archived conversations"))},
{Path: "/api/v1/threads/{thread_id}/messages/{message_id}", OperationID: "getSavedThreadMessage",
Summary: "Locate a saved public message in its conversation", Tag: "Threads",
Expand Down
12 changes: 9 additions & 3 deletions internal/httpapi/thread_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
)

type threadSearchStore interface {
SearchThreadMessages(context.Context, string, bool, time.Time, string, int) ([]domain.ThreadSearchMatch, error)
SearchThreadMessages(context.Context, string, string, bool, time.Time, string, int) ([]domain.ThreadSearchMatch, error)
GetThreadSearchMessage(context.Context, string, string) (domain.ThreadSearchMatch, error)
}

Expand All @@ -39,7 +39,7 @@ type ThreadSavedMessageView struct {

func (a *API) searchThreadMessages(request *http.Request) (any, *Page, error) {
values := request.URL.Query()
if err := validateSingleQueryValues(values, "q", "include_archived", "limit", "cursor"); err != nil {
if err := validateSingleQueryValues(values, "q", "thread_id", "include_archived", "limit", "cursor"); err != nil {
return nil, nil, err
}
for key, entries := range values {
Expand All @@ -52,6 +52,12 @@ func (a *API) searchThreadMessages(request *http.Request) (any, *Page, error) {
return nil, nil, apperror.New(apperror.CodeInvalidArgument, "Search requires between 1 and 256 characters")
}
values.Set("q", query)
threadID := values.Get("thread_id")
if _, scoped := values["thread_id"]; scoped {
if err := validateIdentity(threadID, "search Thread identity"); err != nil {
return nil, nil, err
}
}
includeArchived := false
if raw, exists := singleQueryValue(values, "include_archived"); exists {
includeArchived, err = strconv.ParseBool(raw)
Expand All @@ -68,7 +74,7 @@ func (a *API) searchThreadMessages(request *http.Request) (any, *Page, error) {
if !ok {
return nil, nil, apperror.New(apperror.CodeNotFound, "Saved message search is unavailable")
}
items, err := reader.SearchThreadMessages(request.Context(), query, includeArchived,
items, err := reader.SearchThreadMessages(request.Context(), query, threadID, includeArchived,
pageRequest.Anchor.BeforeCreatedAt, pageRequest.Anchor.BeforeID, stableListStoreLimit(pageRequest))
if err != nil {
return nil, nil, err
Expand Down
Loading
Loading