fix(frontend): encode thread IDs in chat routes (#4302)

This commit is contained in:
Ryker_Feng
2026-07-19 22:03:25 +08:00
committed by GitHub
parent d075be0277
commit 532111b2e6
4 changed files with 18 additions and 2 deletions
+2
View File
@@ -682,6 +682,8 @@ Interrupted first-turn runs still persist a fallback conversation title, so stop
In the Web UI, completed assistant turns can be branched into a new main conversation. The new thread starts from that turn's checkpoint. Because workspace files are not checkpointed, the branch only receives a best-effort copy of the current workspace when you branch from the latest turn; branching from an older turn keeps just the restored message history so the branch never inherits files that were created in a later part of the conversation.
Web UI chat links percent-encode custom thread identifiers before placing them in route segments, so reserved URL characters such as `#` and `?` do not change which conversation is opened.
```
# Paths inside the sandbox container
/mnt/skills/public
+1
View File
@@ -82,6 +82,7 @@ Tool-calling AI messages can contain user-visible text as well as `tool_calls`.
- **Server Components by default**, `"use client"` only for interactive components
- **Thread hooks** (`useThreadStream`, `useSubmitThread`, `useThreads`) are the primary API interface
- **Thread routes** — construct Web UI chat paths through `core/threads/utils.ts::pathOfThread()`, which percent-encodes both custom agent names and thread IDs before inserting them into route segments
- **LangGraph client** is a singleton obtained via `getAPIClient()` in `core/api/`
- **Environment validation** uses `@t3-oss/env-nextjs` with Zod schemas (`src/env.js`). Skip with `SKIP_ENV_VALIDATION=1`
- **Subtask step history and runtime metadata** (`core/tasks/`) — the subtask card shows a subagent's full step timeline (#3779): its assistant reasoning turns interleaved with the tools it ran. `Subtask.steps[]` is accumulated live from `task_running` events (appended via `mergeSteps`, not overwritten) and backfilled on expand for historical runs by `fetchSubtaskSteps`, which pages the events endpoint scoped to one task (GET `/runs/{runId}/events?event_types=subagent.step&task_id=…&after_seq=…`) until a short page, so the run-wide limit can't truncate the timeline. `task_started` carries the effective `model_name`; `task_running` carries a cumulative usage snapshot after each completed LLM call. `core/tasks/lifecycle.ts` normalizes these additive events, and `computeNextSubtask` keeps the largest cumulative total so replayed or late SSE frames cannot double-count or roll the folded card backward. Terminal ToolMessage metadata (`subagent_model_name` / `subagent_token_usage`) restores the same values from normal history after reload; no per-card event fetch is needed. `core/tasks/steps.ts` is the pure step model: `messageToStep` (live), `eventsToSteps` (reload), `mergeSteps` (dedup by `message_index`), and `stepsForDisplay` (what the card renders — keeps tool steps + AI steps with text, drops the trailing final-answer AI step when completed since it's shown as `result`). `core/tasks/context.tsx`'s `useUpdateSubtask` applies updates against a `tasksRef` mirroring the latest state (not a closure snapshot), so a late-resolving `fetchSubtaskSteps` backfill merges into current state instead of clobbering SSE steps or sibling subtasks that arrived meanwhile. The owning `run_id` is carried onto history content messages in `buildVisibleHistoryMessages` so the card can resolve the events endpoint.
+3 -2
View File
@@ -21,6 +21,7 @@ export function pathOfThread(
context?: Pick<AgentThreadContext, "agent_name"> | null,
) {
const threadId = typeof thread === "string" ? thread : thread.thread_id;
const encodedThreadId = encodeURIComponent(threadId);
let agentName: string | undefined;
if (typeof thread === "string") {
agentName = context?.agent_name;
@@ -35,8 +36,8 @@ export function pathOfThread(
}
return agentName
? `/workspace/agents/${encodeURIComponent(agentName)}/chats/${threadId}`
: `/workspace/chats/${threadId}`;
? `/workspace/agents/${encodeURIComponent(agentName)}/chats/${encodedThreadId}`
: `/workspace/chats/${encodedThreadId}`;
}
export function textOfMessage(message: Message) {
@@ -16,6 +16,18 @@ test("uses standard chat route when thread has no agent context", () => {
).toBe("/workspace/chats/thread-123");
});
test("encodes thread ids in standard chat routes", () => {
expect(pathOfThread("thread#1?draft")).toBe(
"/workspace/chats/thread%231%3Fdraft",
);
});
test("encodes thread ids in agent chat routes", () => {
expect(pathOfThread("thread#1?draft", { agent_name: "researcher" })).toBe(
"/workspace/agents/researcher/chats/thread%231%3Fdraft",
);
});
test("uses agent chat route when thread context has agent_name", () => {
expect(
pathOfThread({