@nextclaw/ui 0.27.4 → 0.27.5-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/.vite/manifest.json +45 -45
  3. package/dist/assets/{account-panel-Cfx3ocPY.js → account-panel-DgqicWMm.js} +1 -1
  4. package/dist/assets/{agents-BdeFtIOV.js → agents-CTSR4ZKb.js} +1 -1
  5. package/dist/assets/agents-i7167DcF.js +1 -0
  6. package/dist/assets/{app-presenter-provider-BB3Xl8b0.js → app-presenter-provider-DdD2Q7jZ.js} +17 -16
  7. package/dist/assets/{appearance-settings-page-DiSRSDN3.js → appearance-settings-page-CSWYLrsR.js} +1 -1
  8. package/dist/assets/apps-DUP3aPpu.js +1 -0
  9. package/dist/assets/cron-DPBUHn7j.js +1 -0
  10. package/dist/assets/doc-browser-C7kvXGc_.js +1 -0
  11. package/dist/assets/inbox-C09MQO9k.js +1 -0
  12. package/dist/assets/{keyboard-shortcuts-settings-page-BFBXVf20.js → keyboard-shortcuts-settings-page-BCxB9m69.js} +1 -1
  13. package/dist/assets/{main-Bp1iozNB.js → main-C6VGoYNO.js} +2 -2
  14. package/dist/assets/{marketplace-BTDWnHP7.js → marketplace-CXCLbyho.js} +1 -1
  15. package/dist/assets/mcp-marketplace-page-ZAm380SB.js +1 -0
  16. package/dist/assets/panel-apps-BQlyeAId.js +1 -0
  17. package/dist/assets/{projects-fPTOn2Wz.js → projects-BWM-oEo0.js} +1 -1
  18. package/dist/assets/remote-DngM8iep.js +1 -0
  19. package/dist/assets/{runtime-config-page-P5RUq3YK.js → runtime-config-page-Dx6lWn3_.js} +1 -1
  20. package/dist/assets/{service-apps-panel-BReYr_ni.js → service-apps-panel-9khaUSFI.js} +1 -1
  21. package/dist/index.html +3 -3
  22. package/package.json +7 -7
  23. package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +16 -0
  24. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +17 -0
  25. package/src/features/chat/features/message/utils/chat-message-timeline.utils.ts +3 -3
  26. package/src/features/chat/features/message/utils/chat-message.utils.ts +11 -4
  27. package/src/features/chat/features/message/utils/chat-user-question-timeline.utils.ts +42 -0
  28. package/dist/assets/agents-p_wcGhC3.js +0 -1
  29. package/dist/assets/apps-CKFrWb00.js +0 -1
  30. package/dist/assets/cron-UqsFT41c.js +0 -1
  31. package/dist/assets/doc-browser-2VjSkax1.js +0 -1
  32. package/dist/assets/inbox-D62apR2j.js +0 -1
  33. package/dist/assets/mcp-marketplace-page-oHCbBkS-.js +0 -1
  34. package/dist/assets/panel-apps-CniMi7QW.js +0 -1
  35. package/dist/assets/remote-CsJMF8OR.js +0 -1
@@ -20,6 +20,22 @@ const visibleMessage = {
20
20
  } satisfies NcpMessage;
21
21
 
22
22
  describe("chat message timeline visibility", () => {
23
+ it("places an async question at its tool call between assistant text parts", () => {
24
+ const source = { ...visibleMessage, parts: [
25
+ { type: "text" as const, text: "先说明。" },
26
+ { type: "tool-invocation" as const, toolCallId: "call-1", toolName: "request_user_input_async", state: "result" as const,
27
+ args: "{}", result: { accepted: true, questionIds: ["q-1"] } },
28
+ { type: "text" as const, text: "稍后继续。" },
29
+ ] } satisfies NcpMessage;
30
+ const question = { ...visibleMessage, id: "assistant-question-1", parts: [
31
+ { type: "text" as const, text: "城市? (北京 / 上海)" },
32
+ { type: "extension" as const, extensionType: "nextclaw.user-question", data: { questions: [{ id: "q-1", title: "城市?", options: ["北京", "上海"] }] } },
33
+ ] } satisfies NcpMessage;
34
+ const projected = projectVisibleChatMessages([source, question]);
35
+ expect(projected).toHaveLength(1);
36
+ expect(projected[0]?.parts.map((part) => part.type)).toEqual(["text", "extension", "text"]);
37
+ expect(projected[0]?.parts[1]).toMatchObject({ data: { questions: [{ id: "q-1" }] } });
38
+ });
23
39
  it("places context compaction between surrounding visible messages", () => {
24
40
  const afterMessage = {
25
41
  ...visibleMessage,
@@ -118,6 +118,23 @@ it("maps context compaction extensions into stable process parts", () => {
118
118
  }]);
119
119
  });
120
120
 
121
+ it("keeps assistant prose on both sides of an unanchored question fallback", () => {
122
+ const data = { questions: [{ id: "question-1", title: "Which format?", options: ["PDF", "DOCX"] }] };
123
+ const adapted = adapt([{
124
+ id: "assistant-question-older",
125
+ role: "assistant",
126
+ parts: [
127
+ { type: "text", text: "Before." },
128
+ { type: "text", text: "Which format? (PDF / DOCX)" },
129
+ { type: "extension", extensionType: "nextclaw.user-question", data },
130
+ { type: "text", text: "After." },
131
+ ],
132
+ }]);
133
+ expect(adapted[0]?.parts.map((part) => part.type)).toEqual(["markdown", "custom", "markdown"]);
134
+ expect(adapted[0]?.parts[0]).toMatchObject({ text: "Before." });
135
+ expect(adapted[0]?.parts[2]).toMatchObject({ text: "After." });
136
+ });
137
+
121
138
  it("renders a structured user question at its message position without repeating fallback text", () => {
122
139
  const data = { questions: [{ id: "question-1", title: "Which format?", options: ["PDF", "DOCX"] }] };
123
140
  const adapted = adapt([{
@@ -13,10 +13,10 @@ import {
13
13
  readObservationEventPartData,
14
14
  type ObservationEventPartData,
15
15
  } from "@/features/chat/features/message/utils/chat-message-observation-event.utils";
16
+ import { placeUserQuestionsInAssistantBody } from "@/features/chat/features/message/utils/chat-user-question-timeline.utils";
16
17
 
17
18
  const INHERITED_FROM_SESSION_METADATA_KEY = "inherited_from_session_id";
18
- export const CONTEXT_COMPACTION_PART_EXTENSION_TYPE =
19
- "nextclaw.context-compaction";
19
+ export const CONTEXT_COMPACTION_PART_EXTENSION_TYPE = "nextclaw.context-compaction";
20
20
 
21
21
  export type ContextCompactionPartData = {
22
22
  id: string;
@@ -280,7 +280,7 @@ function projectVisibleChatMessageState(
280
280
  status: "pending",
281
281
  };
282
282
  }
283
- return { messages, inlineCompactionMessageIds, observationEvents };
283
+ return { ...placeUserQuestionsInAssistantBody(messages, observationEvents), inlineCompactionMessageIds };
284
284
  }
285
285
 
286
286
  export function projectVisibleChatMessages(
@@ -122,16 +122,23 @@ export function adaptChatMessage(
122
122
  texts: params.texts,
123
123
  }))
124
124
  .filter((part) => part !== null);
125
- const hasQuestionPart = parts.some(
126
- (part) => part.type === "custom" && part.customType === "nextclaw.user-question",
127
- );
128
125
  const reply = message.meta?.userQuestionReply;
129
126
  const displayParts: ChatMessageViewModel["parts"] = reply
130
127
  ? [
131
128
  { type: "custom", id: reply.questionId, customType: "nextclaw.user-question-reply", data: reply },
132
129
  ...(reply.answer ? [{ type: "markdown" as const, text: reply.answer }] : []),
133
130
  ]
134
- : hasQuestionPart ? parts.filter((part) => part.type !== "markdown") : parts;
131
+ : parts.filter((part, index) => {
132
+ const next = parts[index + 1];
133
+ if (part.type !== "markdown" || next?.type !== "custom" || next.customType !== "nextclaw.user-question") return true;
134
+ const entries = next.data && typeof next.data === "object" && "questions" in next.data ? next.data.questions : null;
135
+ if (!Array.isArray(entries)) return true;
136
+ const fallback = entries.map((entry) => {
137
+ if (!entry || typeof entry.title !== "string") return "";
138
+ return `${entry.title}${Array.isArray(entry.options) && entry.options.length ? ` (${entry.options.join(" / ")})` : ""}`;
139
+ }).join("\n");
140
+ return part.text !== fallback;
141
+ });
135
142
  return {
136
143
  id: message.id,
137
144
  role: resolveUiRole(message.role),
@@ -0,0 +1,42 @@
1
+ import type { NcpMessage } from "@nextclaw/ncp";
2
+
3
+ function questionIdsFromToolPart(part: NcpMessage["parts"][number]): string[] {
4
+ if (part.type !== "tool-invocation" || part.toolName !== "request_user_input_async" ||
5
+ !part.result || typeof part.result !== "object" || Array.isArray(part.result)) return [];
6
+ const { accepted, questionIds } = part.result as { accepted?: unknown; questionIds?: unknown };
7
+ return accepted === true && Array.isArray(questionIds) && questionIds.every((id) => typeof id === "string")
8
+ ? questionIds : [];
9
+ }
10
+
11
+ function questionEntries(part: NcpMessage["parts"][number]): Array<{ id: string }> {
12
+ if (part.type !== "extension" || part.extensionType !== "nextclaw.user-question") return [];
13
+ const entries = part.data && typeof part.data === "object" && "questions" in part.data ? part.data.questions : null;
14
+ return Array.isArray(entries) ? entries.filter((entry): entry is { id: string } => Boolean(entry && typeof entry.id === "string")) : [];
15
+ }
16
+
17
+ export function placeUserQuestionsInAssistantBody<T extends { boundaryIndex: number }>(
18
+ messages: NcpMessage[], observationEvents: T[],
19
+ ): { messages: NcpMessage[]; observationEvents: T[] } {
20
+ const entriesById = new Map(messages.flatMap((message) => message.parts.flatMap((part) => questionEntries(part).map((entry) => [entry.id, entry] as const))));
21
+ const anchoredIds = new Set(messages.flatMap((message) => message.parts.flatMap((part) =>
22
+ questionIdsFromToolPart(part).filter((id) => entriesById.has(id)))));
23
+ if (!anchoredIds.size) return { messages, observationEvents };
24
+ const removedIndices: number[] = [];
25
+ const projected = messages.flatMap((message, index) => {
26
+ const questionPart = message.parts.find((part) => questionEntries(part).length);
27
+ if (questionPart && message.id.startsWith("assistant-question-")) {
28
+ const remaining = questionEntries(questionPart).filter((entry) => !anchoredIds.has(entry.id));
29
+ if (!remaining.length) { removedIndices.push(index); return []; }
30
+ return [{ ...message, parts: [{ type: "extension" as const, extensionType: "nextclaw.user-question", data: { questions: remaining } }] }];
31
+ }
32
+ const parts = message.parts.flatMap((part): NcpMessage["parts"] => {
33
+ const questions = questionIdsFromToolPart(part).flatMap((id) => entriesById.get(id) ?? []);
34
+ return questions.length
35
+ ? [{ type: "extension", extensionType: "nextclaw.user-question", data: { questions } }]
36
+ : [part];
37
+ });
38
+ return [{ ...message, parts }];
39
+ });
40
+ return { messages: projected, observationEvents: observationEvents.map((entry) => ({ ...entry,
41
+ boundaryIndex: entry.boundaryIndex - removedIndices.filter((index) => index < entry.boundaryIndex).length })) };
42
+ }
@@ -1 +0,0 @@
1
- import{t as e}from"./agents-BdeFtIOV.js";export{e as AgentsPage};
@@ -1 +0,0 @@
1
- import{ot as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as AppsPanel};
@@ -1 +0,0 @@
1
- import{K as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as CronJobResource};
@@ -1 +0,0 @@
1
- import{St as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as DocBrowser};
@@ -1 +0,0 @@
1
- import{z as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as InboxPage};
@@ -1 +0,0 @@
1
- import{st as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as McpMarketplacePage};
@@ -1 +0,0 @@
1
- import{s as e,v as t}from"./theme-provider-BsJ6HxeX.js";import"./app-presenter-provider-BB3Xl8b0.js";export{t as PanelAppHostProvider,e as PanelAppRuntimeSurface};
@@ -1 +0,0 @@
1
- import{F as e}from"./app-presenter-provider-BB3Xl8b0.js";export{e as RemoteAccessPage};