@ykurilov/agent-chat-react 0.68.0 → 0.69.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @ykurilov/agent-chat-react
2
2
 
3
+ ## 0.69.1
4
+
5
+ No changes in this release.
6
+
7
+ ## 0.69.0
8
+
9
+ ### Minor Changes
10
+
11
+ - e6c03dc: Значки шагов и заголовки раздумий (106): в простом виде — ряд значков у «Сделано N шагов» и список «значок + фраза» (`StepIcon`, `icon` в `stepPhrases`, `guessStepIcon`); `thoughtTitles` у службы чата — заголовок раздумья от провайдера или пересказ дешёвой моделью в фоне, хранится в части раздумья, в запрос основной модели не идёт, расход `thought_title`; договор скина 28 (`WorkStepsView.live.icon`, `done.items`, `icons`, `moreIcons`, `Reasoning.title`); фраза навыка с именем.
12
+
3
13
  ## 0.68.0
4
14
 
5
15
  No changes in this release.
package/README.md CHANGED
@@ -66,6 +66,10 @@ import { AgentChat, ChatLabelsProvider, russianLabels } from "@ykurilov/agent-ch
66
66
  поправку. Скину — `ChatSkin.steering`, `Composer.canSteer` и `UserMessage.pending` (договор v21, [docs/skin-v21.md](../../docs/skin-v21.md)).
67
67
  - `liveDemos` — живые демонстрации: блок ` ```html-demo ` в ответе запускается в изолированной рамке во всю ширину ответа (на сервере —
68
68
  `agent.liveDemos: true`). Скину — `AgentText.renderDemo` и `ChatSkin.demoColors` (договор v22, [docs/live-demos.md](../../docs/live-demos.md)).
69
+ - Значки шагов и заголовки раздумий (с 0.69, договор скина v28, [docs/skin-v28.md](../../docs/skin-v28.md)): в простом виде
70
+ ряд значков у «Сделано N шагов» и список «значок + фраза»; значок своему инструменту — `icon` рядом с фразами
71
+ `stepPhrases` (`StepIcon`), без него — `guessStepIcon` по имени. Заголовки раздумий приходят с сервера
72
+ (`thoughtTitles`) и показываются сами — в простом и подробном виде ([docs/work-view.md](../../docs/work-view.md)).
69
73
  - Английский интерфейс: `<ChatLabelsProvider labels={englishLabels}>` — полный словарь (см. docs/getting-started.md, «Надписи»).
70
74
  - `onTurnFinished({ chatId, messageId, status })` — ход закончен на экране: один раз на ход, когда ответ получил итог, включая остановку
71
75
  и ошибку. Ответ, закрытый ради поправки на лету, получает своё событие перед ответом на поправку. Открытие чата с готовой историей события не даёт. На сервере то же — `onTurnFinished` у `createChatHandler`.
@@ -296,6 +296,8 @@ var russianWorkView = {
296
296
  stepUnfinishedUnknown: "\u0428\u0430\u0433 \u043D\u0435 \u0437\u0430\u043A\u043E\u043D\u0447\u0435\u043D",
297
297
  stepRepeated: (phrase, count) => `${phrase} \xD7${count}`,
298
298
  thinking: "\u0414\u0443\u043C\u0430\u044E",
299
+ thought: (durationMs) => durationMs === null ? "\u041F\u043E\u0434\u0443\u043C\u0430\u043B" : `\u0414\u0443\u043C\u0430\u043B ${Math.max(1, Math.round(durationMs / 1e3))} \u0441\u0435\u043A`,
300
+ moreSteps: (count) => `+${count}`,
299
301
  live: withEllipsis,
300
302
  stepsDone: (count) => `${russianPlural.select(count) === "one" ? "\u0421\u0434\u0435\u043B\u0430\u043D" : "\u0421\u0434\u0435\u043B\u0430\u043D\u043E"} ${russianCount(count, { one: "\u0448\u0430\u0433", few: "\u0448\u0430\u0433\u0430", many: "\u0448\u0430\u0433\u043E\u0432" })}`,
301
303
  helperRunning: (name) => `\u041F\u043E\u043C\u043E\u0449\u043D\u0438\u043A \xAB${name}\xBB \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442`,
@@ -310,6 +312,8 @@ var englishWorkViewLabels = {
310
312
  stepUnfinishedUnknown: "A step wasn\u2019t finished",
311
313
  stepRepeated: (phrase, count) => `${phrase} \xD7${count}`,
312
314
  thinking: "Thinking",
315
+ thought: (durationMs) => durationMs === null ? "Thought" : `Thought for ${Math.max(1, Math.round(durationMs / 1e3))} s`,
316
+ moreSteps: (count) => `+${count}`,
313
317
  live: withEllipsis,
314
318
  stepsDone: (count) => `${count} ${count === 1 ? "step" : "steps"} done`,
315
319
  helperRunning: (name) => `Helper \u201C${name}\u201D is working`,
@@ -4004,6 +4008,54 @@ function useThinkingStatus(active = true) {
4004
4008
  };
4005
4009
  }
4006
4010
 
4011
+ // src/work-view/step-icons.ts
4012
+ var SDK_TOOL_ICONS = {
4013
+ use_skill: "skill",
4014
+ web_search: "web",
4015
+ read_page: "web",
4016
+ page_screenshot: "snapshot",
4017
+ manage_memory: "memory",
4018
+ search_earlier_conversation: "search",
4019
+ check_tasks: "task",
4020
+ cancel_task: "task",
4021
+ generate_images: "image",
4022
+ show_image: "image",
4023
+ show_video: "video"
4024
+ };
4025
+ var WORD_ICONS = [
4026
+ ["snapshot", ["screenshot", "snapshot", "capture"]],
4027
+ ["image", ["image", "images", "picture", "photo", "draw"]],
4028
+ ["video", ["video"]],
4029
+ ["check", ["test", "tests", "check", "verify", "lint", "validate", "typecheck", "review"]],
4030
+ ["edit", ["edit", "write", "patch", "replace", "update", "create", "save", "apply", "delete", "remove", "rename", "move", "insert"]],
4031
+ ["search", ["search", "find", "grep", "lookup", "query"]],
4032
+ ["read", ["read", "open", "view", "look", "get", "load", "list", "cat", "show", "inspect"]],
4033
+ ["web", ["web", "browse", "browser", "url", "http", "page", "fetch"]],
4034
+ ["run", ["run", "exec", "execute", "bash", "shell", "command", "terminal", "build", "install", "deploy"]],
4035
+ ["skill", ["skill", "skills"]],
4036
+ ["memory", ["memory", "remember", "note", "notes"]]
4037
+ ];
4038
+ var wordsOf = (toolName) => toolName.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
4039
+ function guessStepIcon(toolName) {
4040
+ const known = SDK_TOOL_ICONS[toolName];
4041
+ if (known) return known;
4042
+ if (toolName.startsWith("helper_")) return "helper";
4043
+ if (toolName.includes("__")) return "mcp";
4044
+ const words = wordsOf(toolName);
4045
+ return WORD_ICONS.find(([, keys]) => words.some((word) => keys.includes(word)))?.[0] ?? "tool";
4046
+ }
4047
+ var iconOfText = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
4048
+ function stepIconOf(call, { phrases, fallback } = {}) {
4049
+ const entry = phrases && Object.hasOwn(phrases, call.toolName) ? phrases[call.toolName] : void 0;
4050
+ let own;
4051
+ try {
4052
+ own = typeof entry === "function" ? entry(call)?.icon : entry?.icon;
4053
+ } catch {
4054
+ own = void 0;
4055
+ }
4056
+ return iconOfText(own) ?? iconOfText(fallback?.icon) ?? guessStepIcon(call.toolName);
4057
+ }
4058
+
4007
4059
  // src/agent-chat.tsx
4008
4060
  import { getToolName as getToolName5, isToolUIPart as isToolUIPart3 } from "ai";
4009
4061
  import { Fragment, memo as memo4, useCallback as useCallback17, useEffect as useEffect14, useId as useId2, useMemo as useMemo16, useRef as useRef17, useState as useState18 } from "react";
@@ -5706,7 +5758,8 @@ function stepPhraseOf(call, {
5706
5758
  }
5707
5759
 
5708
5760
  // src/work-view/work-layout.ts
5709
- function workLayoutOf(entries, { streaming, phrases, labels }) {
5761
+ var STEP_ICON_ROW_MAX = 8;
5762
+ function workLayoutOf(entries, { streaming, phrases, labels, thoughts = false }) {
5710
5763
  const lastShown = entries.findLastIndex((entry) => entry.kind === "shown" && !entry.blank);
5711
5764
  const layout = [];
5712
5765
  let index = 0;
@@ -5729,39 +5782,56 @@ function workLayoutOf(entries, { streaming, phrases, labels }) {
5729
5782
  } else if (next.blank) blanks.push({ kind: "shown", node: next.node });
5730
5783
  else break;
5731
5784
  }
5732
- if (streaming && lastStep > lastShown) layout.push({ kind: "live", key, phrase: labels.live(livePhraseOf(group, phrases, labels)) });
5733
- else {
5734
- const done = doneLinesOf(group, phrases, labels, !streaming);
5785
+ if (streaming && lastStep > lastShown) {
5786
+ const live = liveStepOf(group, phrases, labels);
5787
+ layout.push({ kind: "live", key, phrase: labels.live(live.phrase), icon: live.icon });
5788
+ } else {
5789
+ const done = doneLinesOf(thoughts ? group : group.filter((step) => step.kind === "tool"), phrases, labels, !streaming);
5735
5790
  if (done.count > 0) layout.push({ kind: "done", key, ...done });
5736
5791
  }
5737
5792
  layout.push(...blanks);
5738
5793
  }
5739
5794
  return layout;
5740
5795
  }
5741
- function livePhraseOf(group, phrases, labels) {
5796
+ function liveStepOf(group, phrases, labels) {
5742
5797
  const last = group.at(-1);
5743
5798
  const running = group.findLast((step) => step.kind === "tool" && step.call.state === "running");
5744
- if (last.kind === "tool" && running?.kind === "tool") return phraseOf(running, phrases, labels);
5745
- return last.kind === "thinking" ? labels.thinking : labels.stepRunning;
5799
+ if (last.kind === "tool" && running?.kind === "tool") return { phrase: phraseOf(running, phrases, labels), icon: iconOf(running, phrases) };
5800
+ if (last.kind === "thinking") return { phrase: last.title ?? labels.thinking, icon: "thinking" };
5801
+ return { phrase: labels.stepRunning, icon: iconOf(last, phrases) };
5802
+ }
5803
+ function iconOf(step, phrases) {
5804
+ return stepIconOf(step.call, { phrases, fallback: step.fallback });
5746
5805
  }
5747
5806
  function phraseOf(step, phrases, labels, unfinished = false) {
5748
5807
  return stepPhraseOf({ ...step.call, ...step.title !== void 0 && { title: step.title } }, { phrases, labels, fallback: step.fallback, unfinished });
5749
5808
  }
5750
5809
  function doneLinesOf(group, phrases, labels, finished2) {
5751
- const lines = [];
5752
- let count = 0;
5753
- for (const step of group) {
5754
- if (step.kind !== "tool") continue;
5755
- count++;
5756
- const unfinished = finished2 && step.call.state === "running";
5757
- const phrase = phraseOf(step, phrases, labels, unfinished);
5758
- const failed = step.call.state === "error" || unfinished;
5759
- const previous = lines.at(-1);
5760
- if (previous?.phrase === phrase && previous.failed === failed) previous.repeats++;
5761
- else lines.push({ phrase, failed, repeats: 1 });
5810
+ const all = group.map(
5811
+ (step) => step.kind === "tool" ? { ...toolLineOf(step, phrases, labels, finished2), tool: true } : { icon: "thinking", phrase: step.title ?? labels.thought(step.durationMs ?? null), failed: false, tool: false }
5812
+ );
5813
+ const calls = all.filter((line) => line.tool);
5814
+ const icons = all.map((line) => line.icon);
5815
+ return {
5816
+ steps: collapsed(calls, labels).map(({ phrase, failed }) => ({ phrase, failed })),
5817
+ count: calls.length,
5818
+ items: collapsed(all, labels),
5819
+ icons: icons.slice(0, STEP_ICON_ROW_MAX),
5820
+ moreIcons: Math.max(0, icons.length - STEP_ICON_ROW_MAX)
5821
+ };
5822
+ }
5823
+ function collapsed(lines, labels) {
5824
+ const runs = [];
5825
+ for (const line of lines) {
5826
+ const previous = runs.at(-1);
5827
+ if (previous?.phrase === line.phrase && previous.failed === line.failed && previous.icon === line.icon) previous.repeats++;
5828
+ else runs.push({ icon: line.icon, phrase: line.phrase, failed: line.failed, repeats: 1 });
5762
5829
  }
5763
- const steps = lines.map(({ phrase, failed, repeats }) => ({ phrase: repeats > 1 ? labels.stepRepeated(phrase, repeats) : phrase, failed }));
5764
- return { steps, count };
5830
+ return runs.map(({ repeats, ...line }) => repeats > 1 ? { ...line, phrase: labels.stepRepeated(line.phrase, repeats) } : line);
5831
+ }
5832
+ function toolLineOf(step, phrases, labels, finished2) {
5833
+ const unfinished = finished2 && step.call.state === "running";
5834
+ return { icon: iconOf(step, phrases), phrase: phraseOf(step, phrases, labels, unfinished), failed: step.call.state === "error" || unfinished };
5765
5835
  }
5766
5836
 
5767
5837
  // src/work-view/work-steps-part.tsx
@@ -5776,7 +5846,19 @@ function WorkStepsPart({
5776
5846
  const labels = useChatLabels();
5777
5847
  const [open, setOpen] = usePartOpen(expanded, `${item.key}:work`);
5778
5848
  if (skin.WorkSteps) {
5779
- return item.kind === "live" ? /* @__PURE__ */ jsx8(skin.WorkSteps, { state: "live", phrase: item.phrase }) : /* @__PURE__ */ jsx8(skin.WorkSteps, { state: "done", steps: item.steps, count: item.count, open, onOpenChange: setOpen });
5849
+ return item.kind === "live" ? /* @__PURE__ */ jsx8(skin.WorkSteps, { state: "live", phrase: item.phrase, icon: item.icon }) : /* @__PURE__ */ jsx8(
5850
+ skin.WorkSteps,
5851
+ {
5852
+ state: "done",
5853
+ steps: item.steps,
5854
+ count: item.count,
5855
+ items: item.items,
5856
+ icons: item.icons,
5857
+ moreIcons: item.moreIcons,
5858
+ open,
5859
+ onOpenChange: setOpen
5860
+ }
5861
+ );
5780
5862
  }
5781
5863
  if (item.kind === "live") return /* @__PURE__ */ jsx8(skin.ToolCallRow, { toolName: item.phrase, title: item.phrase, state: "running", open: false, onOpenChange: keepClosed });
5782
5864
  const title = labels.workView.stepsDone(item.count);
@@ -6394,6 +6476,7 @@ var AgentAnswer = memo4(function AgentAnswer2({
6394
6476
  }) {
6395
6477
  const labels = useChatLabels();
6396
6478
  const durations = message.metadata?.reasoningDurationsMs ?? {};
6479
+ const thoughtTitles = message.metadata?.thoughtTitles ?? {};
6397
6480
  let reasoningIndex = 0;
6398
6481
  const simple = extensions.workView === "simple";
6399
6482
  const entries = [];
@@ -6415,10 +6498,12 @@ var AgentAnswer = memo4(function AgentAnswer2({
6415
6498
  if (part.type === "reasoning") {
6416
6499
  const ordinal = String(reasoningIndex++);
6417
6500
  const streaming = part.state === "streaming";
6418
- if (!streaming && part.text.trim().length === 0) return;
6501
+ const durationMs = streaming ? null : durations[ordinal] ?? null;
6502
+ const title = streaming ? void 0 : thoughtTitles[ordinal];
6503
+ if (!streaming && part.text.trim().length === 0 && (title === void 0 || !simple)) return;
6419
6504
  step(
6420
6505
  index,
6421
- { kind: "thinking" },
6506
+ { kind: "thinking", durationMs, ...title !== void 0 && { title } },
6422
6507
  /* @__PURE__ */ jsx9(
6423
6508
  ExpandablePart,
6424
6509
  {
@@ -6427,7 +6512,8 @@ var AgentAnswer = memo4(function AgentAnswer2({
6427
6512
  partId: partKey(message.id, index),
6428
6513
  text: part.text,
6429
6514
  streaming,
6430
- durationMs: streaming ? null : durations[ordinal] ?? null
6515
+ durationMs,
6516
+ ...title !== void 0 && { title }
6431
6517
  },
6432
6518
  key
6433
6519
  )
@@ -6535,7 +6621,7 @@ var AgentAnswer = memo4(function AgentAnswer2({
6535
6621
  else step(index, toolStepOf(part), row);
6536
6622
  }
6537
6623
  });
6538
- const parts = simple ? workLayoutOf(entries, { streaming: status === "streaming", phrases: extensions.stepPhrases, labels: labels.workView }).map(
6624
+ const parts = simple ? workLayoutOf(entries, { streaming: status === "streaming", phrases: extensions.stepPhrases, labels: labels.workView, thoughts: Object.keys(thoughtTitles).length > 0 }).map(
6539
6625
  (item) => item.kind === "shown" ? item.node : /* @__PURE__ */ jsx9(WorkStepsPart, { skin, expanded, item }, `${item.key}:work`)
6540
6626
  ) : entries.map((entry) => entry.kind === "shown" ? entry.node : null);
6541
6627
  const text = messageText(message, { voiceAnswer });
@@ -7078,10 +7164,12 @@ export {
7078
7164
  useAgentChat,
7079
7165
  useModelChoice,
7080
7166
  useThinkingStatus,
7167
+ guessStepIcon,
7168
+ stepIconOf,
7081
7169
  AgentChat,
7082
7170
  useChatList,
7083
7171
  useChatListView,
7084
7172
  activeChatTitle,
7085
7173
  useWorkspaceListSync
7086
7174
  };
7087
- //# sourceMappingURL=chunk-JLGBVDAR.js.map
7175
+ //# sourceMappingURL=chunk-Z7X5D4OF.js.map