@adecore/agents-react 0.39.0 → 0.40.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.
Files changed (317) hide show
  1. package/dist/agents/AgentModelLabel.js +2 -1
  2. package/dist/agents/model-name.js +2 -1
  3. package/dist/agents/provider-paths.d.ts +2 -1
  4. package/dist/agents/provider-paths.js +7 -3
  5. package/dist/chat/account-choice.js +57 -28
  6. package/dist/chat/bookmarks.js +2 -1
  7. package/dist/chat/drafts.d.ts +0 -1
  8. package/dist/chat/drafts.js +9 -28
  9. package/dist/chat/logic/picker-entries.js +2 -8
  10. package/dist/chat/logic/resume-compaction.js +2 -5
  11. package/dist/chat/logic/timeline-copy.js +2 -8
  12. package/dist/chat/logic/timeline.js +9 -10
  13. package/dist/chat/logic/workflow.js +2 -4
  14. package/dist/chat/subagent-conversation.js +2 -1
  15. package/dist/chat/ui/GoalDock.js +2 -1
  16. package/dist/chat/ui/LimitState.js +4 -6
  17. package/dist/chat/ui/composer/use-continuable-turn.js +0 -1
  18. package/dist/chat/ui/composer/use-notice.js +2 -1
  19. package/dist/chat/ui/composer/use-prompt-history.js +1 -1
  20. package/dist/chat/ui/composer/use-prompt-menus.js +216 -253
  21. package/dist/chat/ui/composer/use-take-back.js +2 -2
  22. package/dist/chat/ui/diff/ApprovalReview.js +1 -1
  23. package/dist/chat/ui/highlighter.d.ts +0 -6
  24. package/dist/chat/ui/highlighter.js +0 -12
  25. package/dist/chat/ui/icons.d.ts +0 -1
  26. package/dist/chat/ui/icons.js +0 -3
  27. package/dist/chat/ui/rows/MessageRows.js +2 -5
  28. package/dist/chat/ui/rows/row-rhythm.js +2 -5
  29. package/dist/chat/ui/theme-watch.js +3 -7
  30. package/dist/prompts/logic/usePromptSession.js +2 -1
  31. package/dist/providers/AddAccountForm.js +2 -1
  32. package/dist/providers/CliDetail.js +104 -130
  33. package/dist/providers/account-actions.js +6 -9
  34. package/dist/state/chats.d.ts +1 -0
  35. package/dist/state/chats.js +6 -11
  36. package/dist/state/provider-accounts.d.ts +1 -1
  37. package/dist/state/provider-accounts.js +2 -8
  38. package/dist/transport.d.ts +2 -0
  39. package/dist/transport.js +14 -0
  40. package/dist/usage/ChartLogo.d.ts +5 -0
  41. package/dist/usage/ChartLogo.js +48 -0
  42. package/dist/usage/LimitsList.js +6 -16
  43. package/dist/usage/UsageBreakdown.js +17 -40
  44. package/dist/usage/UsageLimits.js +183 -211
  45. package/dist/usage/UsagePage.js +3 -17
  46. package/dist/usage/UsageSummary.js +4 -9
  47. package/dist/usage/limits.js +16 -31
  48. package/dist/usage/summary.d.ts +1 -1
  49. package/dist/usage/summary.js +3 -5
  50. package/package.json +27 -27
  51. package/src/agents/AccountDot.tsx +1 -0
  52. package/src/agents/AgentIcon.tsx +2 -0
  53. package/src/agents/AgentModelLabel.tsx +4 -6
  54. package/src/agents/ModelOptionControl.tsx +8 -0
  55. package/src/agents/account-limits.ts +3 -0
  56. package/src/agents/accounts.ts +12 -0
  57. package/src/agents/model-name.ts +13 -1
  58. package/src/agents/model-options.ts +2 -0
  59. package/src/agents/model-requirement.ts +4 -0
  60. package/src/agents/option-words.ts +2 -0
  61. package/src/agents/provider-paths.ts +8 -3
  62. package/src/agents/status-look.ts +1 -0
  63. package/src/chat/account-choice.ts +25 -19
  64. package/src/chat/actions.ts +5 -0
  65. package/src/chat/attachments.ts +8 -0
  66. package/src/chat/bookmarks.ts +13 -1
  67. package/src/chat/chat-client.ts +63 -0
  68. package/src/chat/chat-references.ts +3 -0
  69. package/src/chat/composer-effort.ts +5 -0
  70. package/src/chat/composer-send.ts +4 -0
  71. package/src/chat/drafts.ts +21 -17
  72. package/src/chat/forks.ts +4 -0
  73. package/src/chat/guards.ts +2 -0
  74. package/src/chat/index.ts +7 -0
  75. package/src/chat/logic/ansi.ts +47 -0
  76. package/src/chat/logic/bookmarks.ts +7 -0
  77. package/src/chat/logic/command-looks.ts +29 -0
  78. package/src/chat/logic/context-usage.ts +3 -0
  79. package/src/chat/logic/continue-after-stop.ts +5 -0
  80. package/src/chat/logic/effort.ts +4 -0
  81. package/src/chat/logic/embedded-code.ts +63 -0
  82. package/src/chat/logic/fork.ts +22 -0
  83. package/src/chat/logic/generated-image.ts +7 -0
  84. package/src/chat/logic/goal.ts +7 -0
  85. package/src/chat/logic/handback.ts +2 -0
  86. package/src/chat/logic/limit.ts +8 -0
  87. package/src/chat/logic/picker-entries.ts +16 -4
  88. package/src/chat/logic/resume-compaction.ts +22 -5
  89. package/src/chat/logic/scrubber.ts +38 -5
  90. package/src/chat/logic/thought.ts +5 -0
  91. package/src/chat/logic/thread-cards.ts +6 -0
  92. package/src/chat/logic/timeline-copy.ts +4 -9
  93. package/src/chat/logic/timeline-target.ts +15 -0
  94. package/src/chat/logic/timeline.ts +113 -10
  95. package/src/chat/logic/tools.ts +30 -0
  96. package/src/chat/logic/visual-height.ts +5 -0
  97. package/src/chat/logic/visual-viewport.ts +1 -0
  98. package/src/chat/logic/welcome.ts +3 -0
  99. package/src/chat/logic/workflow.ts +26 -7
  100. package/src/chat/mentions.ts +56 -0
  101. package/src/chat/pending-prompts.ts +16 -0
  102. package/src/chat/persisted-json.ts +15 -0
  103. package/src/chat/preferences.ts +16 -0
  104. package/src/chat/prompt-hold.ts +2 -0
  105. package/src/chat/quote.ts +7 -0
  106. package/src/chat/reading-width.ts +1 -0
  107. package/src/chat/recent-messages.ts +5 -0
  108. package/src/chat/resume-compaction-dismissals.ts +1 -0
  109. package/src/chat/stash.ts +12 -0
  110. package/src/chat/subagent-conversation.ts +24 -4
  111. package/src/chat/subagent-list.ts +14 -0
  112. package/src/chat/subagent-support.ts +1 -0
  113. package/src/chat/subagent-view.ts +9 -0
  114. package/src/chat/thumbnails.ts +14 -0
  115. package/src/chat/timeline-flash.ts +2 -0
  116. package/src/chat/timeline-scroll.ts +23 -0
  117. package/src/chat/ui/AccountPill.tsx +4 -0
  118. package/src/chat/ui/AnsiOutput.tsx +1 -0
  119. package/src/chat/ui/BookmarkMarker.tsx +5 -0
  120. package/src/chat/ui/BookmarkSubmenu.tsx +1 -0
  121. package/src/chat/ui/ChatActivity.tsx +13 -0
  122. package/src/chat/ui/CodeBlock.tsx +8 -0
  123. package/src/chat/ui/Composer.tsx +15 -0
  124. package/src/chat/ui/ComposerInput.tsx +22 -0
  125. package/src/chat/ui/DiffPool.tsx +3 -0
  126. package/src/chat/ui/EditDiff.tsx +5 -0
  127. package/src/chat/ui/GoalDock.tsx +13 -1
  128. package/src/chat/ui/ImageView.tsx +11 -0
  129. package/src/chat/ui/LimitState.tsx +23 -6
  130. package/src/chat/ui/Markdown.tsx +41 -0
  131. package/src/chat/ui/MermaidBlock.tsx +16 -0
  132. package/src/chat/ui/MessageActions.tsx +8 -0
  133. package/src/chat/ui/ModelMarks.tsx +1 -0
  134. package/src/chat/ui/Pickers.tsx +26 -0
  135. package/src/chat/ui/PromptComposer.tsx +2 -0
  136. package/src/chat/ui/PromptEditor.tsx +6 -0
  137. package/src/chat/ui/QuoteButton.tsx +12 -0
  138. package/src/chat/ui/ReferenceChip.tsx +3 -0
  139. package/src/chat/ui/ResumeCompactionDock.tsx +1 -0
  140. package/src/chat/ui/RowTime.tsx +1 -0
  141. package/src/chat/ui/RunSettings.tsx +38 -0
  142. package/src/chat/ui/Scrubber.tsx +14 -0
  143. package/src/chat/ui/SessionLine.tsx +1 -0
  144. package/src/chat/ui/ShellCommand.tsx +32 -0
  145. package/src/chat/ui/SubagentControls.tsx +7 -0
  146. package/src/chat/ui/SubagentInfo.tsx +1 -0
  147. package/src/chat/ui/SubagentStopButton.tsx +6 -0
  148. package/src/chat/ui/SubagentTimeline.tsx +10 -0
  149. package/src/chat/ui/Timeline.tsx +119 -0
  150. package/src/chat/ui/TimelineMenu.tsx +5 -0
  151. package/src/chat/ui/UnifiedDiff.tsx +6 -0
  152. package/src/chat/ui/UploadThumb.tsx +2 -0
  153. package/src/chat/ui/VisualDialogs.tsx +3 -0
  154. package/src/chat/ui/VisualFrame.tsx +15 -0
  155. package/src/chat/ui/Welcome.tsx +1 -0
  156. package/src/chat/ui/chip-clipboard.ts +20 -0
  157. package/src/chat/ui/code-lines.ts +14 -0
  158. package/src/chat/ui/code-theme.ts +1 -0
  159. package/src/chat/ui/composer/ComposerControls.tsx +5 -0
  160. package/src/chat/ui/composer/ComposerMenu.tsx +12 -0
  161. package/src/chat/ui/composer/DraftParts.tsx +2 -0
  162. package/src/chat/ui/composer/QueuedMessages.tsx +4 -0
  163. package/src/chat/ui/composer/chip-paste.ts +16 -0
  164. package/src/chat/ui/composer/chips.ts +6 -0
  165. package/src/chat/ui/composer/editor.ts +9 -0
  166. package/src/chat/ui/composer/insert.ts +2 -0
  167. package/src/chat/ui/composer/key-handler.ts +39 -0
  168. package/src/chat/ui/composer/keys.ts +23 -0
  169. package/src/chat/ui/composer/placeholder.ts +6 -0
  170. package/src/chat/ui/composer/prompt-picks.ts +1 -0
  171. package/src/chat/ui/composer/surface.ts +5 -0
  172. package/src/chat/ui/composer/use-chat-controls.ts +13 -0
  173. package/src/chat/ui/composer/use-compaction-offer.ts +3 -0
  174. package/src/chat/ui/composer/use-composer-menus.ts +2 -0
  175. package/src/chat/ui/composer/use-composer-submit.ts +27 -0
  176. package/src/chat/ui/composer/use-continuable-turn.ts +3 -5
  177. package/src/chat/ui/composer/use-draft-files.ts +24 -0
  178. package/src/chat/ui/composer/use-effort-stepper.ts +8 -0
  179. package/src/chat/ui/composer/use-notice.ts +4 -1
  180. package/src/chat/ui/composer/use-pending-requests.ts +3 -0
  181. package/src/chat/ui/composer/use-prompt-history.ts +20 -11
  182. package/src/chat/ui/composer/use-prompt-hold.ts +3 -0
  183. package/src/chat/ui/composer/use-prompt-menus.ts +34 -14
  184. package/src/chat/ui/composer/use-take-back.ts +20 -2
  185. package/src/chat/ui/diff/ApprovalReview.tsx +18 -15
  186. package/src/chat/ui/diff/DiffFallbackBoundary.tsx +1 -0
  187. package/src/chat/ui/diff/DiffOmitted.tsx +1 -0
  188. package/src/chat/ui/diff/DiffStack.tsx +2 -0
  189. package/src/chat/ui/diff/DiffUnavailable.tsx +1 -0
  190. package/src/chat/ui/diff/EditorEditDiff.tsx +13 -0
  191. package/src/chat/ui/diff/EditorPatchDiff.tsx +4 -0
  192. package/src/chat/ui/diff/LegacyDiffPool.tsx +2 -0
  193. package/src/chat/ui/diff/LegacyEditDiff.tsx +2 -0
  194. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +8 -0
  195. package/src/chat/ui/diff/RawDiff.tsx +1 -0
  196. package/src/chat/ui/diff/ReviewDiff.tsx +1 -0
  197. package/src/chat/ui/diff/diff-source-budget.ts +8 -0
  198. package/src/chat/ui/diff/editor-edit-model.ts +4 -0
  199. package/src/chat/ui/diff/review-plan.ts +9 -0
  200. package/src/chat/ui/diff/review-settle.ts +1 -0
  201. package/src/chat/ui/diff/syntax-runtime.ts +52 -0
  202. package/src/chat/ui/diff/use-review-stack.ts +9 -0
  203. package/src/chat/ui/diff-syntax.ts +12 -0
  204. package/src/chat/ui/diff-theme.ts +2 -0
  205. package/src/chat/ui/file-links.ts +2 -0
  206. package/src/chat/ui/find-reveal.ts +1 -0
  207. package/src/chat/ui/full-diff.ts +1 -0
  208. package/src/chat/ui/highlighter.ts +29 -15
  209. package/src/chat/ui/icons.tsx +2 -4
  210. package/src/chat/ui/index.ts +4 -0
  211. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +25 -0
  212. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +1 -0
  213. package/src/chat/ui/intelligent-ui/UiReply.tsx +63 -0
  214. package/src/chat/ui/intelligent-ui/block-local.ts +5 -0
  215. package/src/chat/ui/intelligent-ui/chart-data.ts +15 -0
  216. package/src/chat/ui/intelligent-ui/node-text.ts +17 -0
  217. package/src/chat/ui/intelligent-ui/renderers/binding.ts +1 -0
  218. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +31 -0
  219. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +4 -0
  220. package/src/chat/ui/intelligent-ui/renderers/content.tsx +18 -0
  221. package/src/chat/ui/intelligent-ui/renderers/data.tsx +11 -0
  222. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +13 -0
  223. package/src/chat/ui/intelligent-ui/renderers/links.tsx +9 -0
  224. package/src/chat/ui/intelligent-ui/renderers/map-slots.ts +15 -0
  225. package/src/chat/ui/intelligent-ui/renderers/map-style.ts +15 -0
  226. package/src/chat/ui/intelligent-ui/renderers/map-view.tsx +48 -0
  227. package/src/chat/ui/intelligent-ui/renderers/map.tsx +20 -0
  228. package/src/chat/ui/intelligent-ui/renderers/place-point.ts +3 -0
  229. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +3 -0
  230. package/src/chat/ui/intelligent-ui/renderers/text.tsx +8 -0
  231. package/src/chat/ui/intelligent-ui/reply-parts.ts +5 -0
  232. package/src/chat/ui/intelligent-ui/table-data.ts +17 -0
  233. package/src/chat/ui/intelligent-ui/use-in-view.ts +4 -0
  234. package/src/chat/ui/intelligent-ui/use-later.ts +4 -0
  235. package/src/chat/ui/intelligent-ui/use-truncated.ts +5 -0
  236. package/src/chat/ui/intelligent-ui/use-ui-links.ts +29 -0
  237. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +60 -0
  238. package/src/chat/ui/markdown-blocks.ts +64 -0
  239. package/src/chat/ui/mermaid.ts +4 -0
  240. package/src/chat/ui/quote-selection.ts +10 -0
  241. package/src/chat/ui/rehype-chips.ts +5 -0
  242. package/src/chat/ui/rehype-fade.ts +2 -0
  243. package/src/chat/ui/remark-html-as-text.ts +5 -0
  244. package/src/chat/ui/reveal.ts +18 -0
  245. package/src/chat/ui/rows/ForksRow.tsx +5 -0
  246. package/src/chat/ui/rows/GeneratedImageRow.tsx +10 -0
  247. package/src/chat/ui/rows/MessageRows.tsx +29 -5
  248. package/src/chat/ui/rows/RowContainer.tsx +1 -0
  249. package/src/chat/ui/rows/Rows.tsx +2 -0
  250. package/src/chat/ui/rows/SubagentRow.tsx +13 -0
  251. package/src/chat/ui/rows/TaskRow.tsx +7 -0
  252. package/src/chat/ui/rows/VisualRow.tsx +4 -0
  253. package/src/chat/ui/rows/WorkRows.tsx +46 -0
  254. package/src/chat/ui/rows/WorkflowRow.tsx +10 -0
  255. package/src/chat/ui/rows/row-rhythm.ts +7 -5
  256. package/src/chat/ui/theme-watch.ts +8 -7
  257. package/src/chat/ui/thread-references.ts +7 -0
  258. package/src/chat/ui/use-diff-source-tokens.ts +8 -0
  259. package/src/chat/ui/useToggleSet.ts +3 -0
  260. package/src/chat/ui/visual-theme.ts +30 -0
  261. package/src/chat/visual-bridge.ts +23 -0
  262. package/src/chat/visual-viewport.ts +12 -0
  263. package/src/host-hooks.ts +6 -4
  264. package/src/host.ts +2 -0
  265. package/src/index.ts +3 -0
  266. package/src/lazy.tsx +9 -0
  267. package/src/mounted-registry.ts +4 -0
  268. package/src/port-transport.ts +32 -0
  269. package/src/prompts/index.ts +2 -0
  270. package/src/prompts/logic/focus.ts +5 -0
  271. package/src/prompts/logic/keys.ts +12 -0
  272. package/src/prompts/logic/prompts.ts +8 -0
  273. package/src/prompts/logic/subjects.ts +8 -0
  274. package/src/prompts/logic/useFocusAfterAnswer.ts +2 -0
  275. package/src/prompts/logic/usePromptSession.ts +13 -1
  276. package/src/prompts/ui/ApprovalBody.tsx +2 -0
  277. package/src/prompts/ui/PromptActions.tsx +2 -0
  278. package/src/prompts/ui/PromptCard.tsx +6 -0
  279. package/src/prompts/ui/PromptView.tsx +6 -0
  280. package/src/prompts/ui/QuestionBody.tsx +11 -0
  281. package/src/providers/AccountColors.tsx +1 -0
  282. package/src/providers/AccountDetail.tsx +7 -0
  283. package/src/providers/AccountVariables.tsx +2 -0
  284. package/src/providers/AddAccountForm.tsx +5 -1
  285. package/src/providers/CliDetail.tsx +6 -5
  286. package/src/providers/InstallPage.tsx +11 -0
  287. package/src/providers/ProviderList.tsx +3 -0
  288. package/src/providers/ProvidersPane.tsx +18 -0
  289. package/src/providers/account-actions.ts +10 -8
  290. package/src/providers/install-steps.ts +3 -0
  291. package/src/providers/pane-target.ts +5 -0
  292. package/src/providers/provider-abilities.ts +1 -0
  293. package/src/providers/provider-groups.ts +6 -0
  294. package/src/providers/variables.ts +12 -0
  295. package/src/scope.ts +2 -0
  296. package/src/state/chats.ts +61 -14
  297. package/src/state/index.ts +2 -0
  298. package/src/state/provider-accounts.ts +7 -9
  299. package/src/state/providers.ts +1 -0
  300. package/src/state/usage.ts +6 -0
  301. package/src/storage.ts +18 -0
  302. package/src/testing/prompts.ts +5 -0
  303. package/src/transport.ts +18 -0
  304. package/src/usage/ChartLogo.tsx +12 -0
  305. package/src/usage/LimitsList.tsx +7 -8
  306. package/src/usage/UsageBreakdown.tsx +11 -4
  307. package/src/usage/UsageChart.tsx +5 -0
  308. package/src/usage/UsageLimits.tsx +10 -7
  309. package/src/usage/UsageLimitsCard.tsx +2 -0
  310. package/src/usage/UsagePage.tsx +16 -19
  311. package/src/usage/UsageSummary.tsx +4 -5
  312. package/src/usage/UsageTiles.tsx +2 -0
  313. package/src/usage/format.ts +7 -0
  314. package/src/usage/limit-groups.ts +14 -0
  315. package/src/usage/limits.ts +11 -13
  316. package/src/usage/money.ts +1 -0
  317. package/src/usage/summary.ts +15 -7
@@ -21,9 +21,11 @@ export function workflowAgentState(agent: ChatWorkflowAgent, workflowRunning: bo
21
21
  if (agent.status !== 'running') {
22
22
  return agent.status;
23
23
  }
24
+
24
25
  if (!workflowRunning) {
25
26
  return 'stopped';
26
27
  }
28
+
27
29
  return agent.startedAt === null ? 'waiting' : 'running';
28
30
  }
29
31
 
@@ -35,15 +37,21 @@ export function workflowPhases(workflow: ChatWorkflow, workflowRunning: boolean)
35
37
  const known = new Map(workflow.phases.map((phase) => [phase.index, phase]));
36
38
  const loose: WorkflowAgentView[] = [];
37
39
  const byPhase = new Map<number, WorkflowAgentView[]>();
40
+
38
41
  for (const agent of workflow.agents) {
39
42
  const view = { agent, state: workflowAgentState(agent, workflowRunning) };
43
+
40
44
  if (agent.phaseIndex === null || !known.has(agent.phaseIndex)) {
41
45
  loose.push(view);
46
+
42
47
  continue;
43
48
  }
49
+
44
50
  byPhase.set(agent.phaseIndex, [...(byPhase.get(agent.phaseIndex) ?? []), view]);
45
51
  }
52
+
46
53
  const phases = workflow.phases.map((phase) => ({ key: `phase-${phase.index}`, title: phase.title, agents: byPhase.get(phase.index) ?? [] }));
54
+
47
55
  return loose.length > 0 ? [{ key: 'loose', title: null, agents: loose }, ...phases] : phases;
48
56
  }
49
57
 
@@ -70,16 +78,18 @@ export interface WorkflowAgentPreview {
70
78
 
71
79
  /* The CLI's previews start with a markdown heading as often as not, which says nothing on one line. */
72
80
  function firstLine(text: string | undefined): string | null {
73
- const line = text
74
- ?.split('\n')
75
- .map((part) => part.replace(/^#+\s*/, '').trim())
76
- .find((part) => part !== '');
77
- return line === undefined ? null : line;
81
+ return (
82
+ text
83
+ ?.split('\n')
84
+ .map((part) => part.replace(/^#+\s*/, '').trim())
85
+ .find((part) => part !== '') ?? null
86
+ );
78
87
  }
79
88
 
80
89
  /* The second line of an agent: what it does while at work, what it answered once it stopped, what it was asked before. */
81
90
  export function workflowAgentPreview(view: WorkflowAgentView): WorkflowAgentPreview {
82
91
  const { agent } = view;
92
+
83
93
  switch (view.state) {
84
94
  case 'running':
85
95
  return agent.lastTool === null
@@ -99,10 +109,12 @@ export type WorkflowCounts = Record<WorkflowAgentState, number> & { total: numbe
99
109
 
100
110
  export function workflowCounts(phases: WorkflowPhaseView[]): WorkflowCounts {
101
111
  const counts: WorkflowCounts = { total: 0, running: 0, waiting: 0, stopped: 0, done: 0, failed: 0 };
112
+
102
113
  for (const view of phases.flatMap((phase) => phase.agents)) {
103
114
  counts.total += 1;
104
115
  counts[view.state] += 1;
105
116
  }
117
+
106
118
  return counts;
107
119
  }
108
120
 
@@ -112,22 +124,28 @@ const DESCRIPTION = /\bdescription\s*:\s*(['"`])((?:\\.|(?!\1)[^\\])*)\1/;
112
124
  /* The literal from its opening brace to the one that closes it, skipping braces inside strings. */
113
125
  function metaLiteral(script: string): string | null {
114
126
  const start = META_START.exec(script);
127
+
115
128
  if (start === null) {
116
129
  return null;
117
130
  }
131
+
118
132
  const from = start.index + start[0].length - 1;
119
133
  let depth = 0;
120
134
  let quote: string | null = null;
135
+
121
136
  for (let i = from; i < script.length; i++) {
122
137
  const char = script[i]!;
138
+
123
139
  if (quote !== null) {
124
140
  if (char === '\\') {
125
141
  i++;
126
142
  } else if (char === quote) {
127
143
  quote = null;
128
144
  }
145
+
129
146
  continue;
130
147
  }
148
+
131
149
  if (char === "'" || char === '"' || char === '`') {
132
150
  quote = char;
133
151
  } else if (char === '{') {
@@ -136,6 +154,7 @@ function metaLiteral(script: string): string | null {
136
154
  return script.slice(from, i + 1);
137
155
  }
138
156
  }
157
+
139
158
  return null;
140
159
  }
141
160
 
@@ -143,6 +162,6 @@ function metaLiteral(script: string): string | null {
143
162
  export function workflowDescription(input: unknown): string | null {
144
163
  const script = typeof input === 'object' && input !== null && 'script' in input ? input.script : undefined;
145
164
  const meta = typeof script === 'string' ? metaLiteral(script) : null;
146
- const found = meta === null ? undefined : DESCRIPTION.exec(meta)?.[2]?.replace(/\\(.)/g, '$1').trim();
147
- return found ? found : null;
165
+
166
+ return (meta === null ? undefined : DESCRIPTION.exec(meta)?.[2]?.replace(/\\(.)/g, '$1').trim()) || null;
148
167
  }
@@ -12,12 +12,14 @@ export function carriesMentions(types: readonly string[]): boolean {
12
12
 
13
13
  export function droppedMentions(data: Pick<DataTransfer, 'getData'>): string[] {
14
14
  const text = data.getData(MENTION_DRAG_TYPE) || data.getData(LEGACY_MENTION_DRAG_TYPE);
15
+
15
16
  return text.trim() === '' ? [] : text.trim().split(/\s+/);
16
17
  }
17
18
 
18
19
  export function writeMentionDrag(data: Pick<DataTransfer, 'setData'>, paths: readonly string[], legacy = true): void {
19
20
  const text = paths.join(' ');
20
21
  data.setData(MENTION_DRAG_TYPE, text);
22
+
21
23
  if (legacy) {
22
24
  data.setData(LEGACY_MENTION_DRAG_TYPE, text);
23
25
  }
@@ -36,6 +38,7 @@ export interface ClipboardChips {
36
38
  /* The chips among `segments`, without doubles, for the copy of `text`. */
37
39
  export function clipboardChipsOf(text: string, segments: readonly ChipSegment[]): ClipboardChips {
38
40
  const chips: ClipboardChips = { text, mentions: [], skills: [], references: [] };
41
+
39
42
  for (const segment of segments) {
40
43
  if (segment.kind === 'mention' && !chips.mentions.includes(segment.path)) {
41
44
  chips.mentions.push(segment.path);
@@ -45,6 +48,7 @@ export function clipboardChipsOf(text: string, segments: readonly ChipSegment[])
45
48
  chips.references.push({ id: segment.id, name: segment.name });
46
49
  }
47
50
  }
51
+
48
52
  return chips;
49
53
  }
50
54
 
@@ -64,28 +68,36 @@ const isStrings = (value: unknown): value is string[] => Array.isArray(value) &&
64
68
  /* The chips of a paste, or null when it has none or its plain text is no longer the text they were copied with. */
65
69
  export function readClipboardChips(data: Pick<DataTransfer, 'getData'>): ClipboardChips | null {
66
70
  const raw = data.getData(CHIPS_CLIPBOARD_TYPE);
71
+
67
72
  if (raw === '') {
68
73
  return null;
69
74
  }
75
+
70
76
  let parsed: unknown;
77
+
71
78
  try {
72
79
  parsed = JSON.parse(raw);
73
80
  } catch {
74
81
  return null;
75
82
  }
83
+
76
84
  if (typeof parsed !== 'object' || parsed === null) {
77
85
  return null;
78
86
  }
87
+
79
88
  const { text, mentions, skills, references } = parsed as Record<string, unknown>;
89
+
80
90
  const tokens = Array.isArray(references)
81
91
  ? references.filter(
82
92
  (token): token is ReferenceToken =>
83
93
  typeof token === 'object' && token !== null && typeof token.id === 'string' && typeof token.name === 'string' && token.name !== ''
84
94
  )
85
95
  : [];
96
+
86
97
  if (typeof text !== 'string' || lines(text) !== lines(data.getData('text/plain')) || !isStrings(mentions) || !isStrings(skills)) {
87
98
  return null;
88
99
  }
100
+
89
101
  return { text: lines(text), mentions, skills, references: tokens.map(({ id, name }) => ({ id, name })) };
90
102
  }
91
103
 
@@ -129,16 +141,21 @@ const PATH_GOES_ON = /[.:]$/;
129
141
  function findQuery(text: string, caret: number, sigil: string): MentionQuery | null {
130
142
  const before = text.slice(0, caret);
131
143
  const at = before.lastIndexOf(sigil);
144
+
132
145
  if (at < 0 || !isBoundary(before[at - 1])) {
133
146
  return null;
134
147
  }
148
+
135
149
  const query = before.slice(at + 1);
150
+
136
151
  if (/\s/.test(query) || !isBoundary(text[caret])) {
137
152
  return null;
138
153
  }
154
+
139
155
  if (query !== '' && isTokenEnd(query.at(-1)) && !(sigil === '@' && PATH_GOES_ON.test(query))) {
140
156
  return null;
141
157
  }
158
+
142
159
  return { start: at, query };
143
160
  }
144
161
 
@@ -154,6 +171,7 @@ export function findMentionQuery(text: string, caret: number): MentionQuery | nu
154
171
  /* The `$word` the caret sits in. The name has to start with a letter, so `$20` and `$1e6` stay text. */
155
172
  export function findSkillQuery(text: string, caret: number): MentionQuery | null {
156
173
  const found = findQuery(text, caret, '$');
174
+
157
175
  return found === null || (found.query !== '' && !/^[A-Za-z]/.test(found.query)) ? null : found;
158
176
  }
159
177
 
@@ -163,6 +181,7 @@ export function findSkillQuery(text: string, caret: number): MentionQuery | null
163
181
  */
164
182
  export function findReferenceQuery(text: string, caret: number): MentionQuery | null {
165
183
  const found = findQuery(text, caret, '#');
184
+
166
185
  return found === null || !/^\p{L}/u.test(found.query) ? null : found;
167
186
  }
168
187
 
@@ -170,6 +189,7 @@ export function findReferenceQuery(text: string, caret: number): MentionQuery |
170
189
  export function insertToken(text: string, query: MentionQuery, sigil: string, value: string): { text: string; caret: number } {
171
190
  const end = query.start + 1 + query.query.length;
172
191
  const token = `${sigil}${value} `;
192
+
173
193
  return { text: `${text.slice(0, query.start)}${token}${text.slice(end)}`, caret: query.start + token.length };
174
194
  }
175
195
 
@@ -198,29 +218,36 @@ export function mentionPick(index: number, search: MentionSearch, query: string)
198
218
  if (search.query !== query) {
199
219
  return null;
200
220
  }
221
+
201
222
  return search.files[index] ?? search.files[0] ?? null;
202
223
  }
203
224
 
204
225
  function findToken(text: string, sigil: string, value: string, from: number): number {
205
226
  const token = `${sigil}${value}`;
206
227
  let index = text.indexOf(token, from);
228
+
207
229
  while (index >= 0) {
208
230
  if (isBoundary(text[index - 1]) && isTokenEnd(text[index + token.length])) {
209
231
  return index;
210
232
  }
233
+
211
234
  index = text.indexOf(token, index + 1);
212
235
  }
236
+
213
237
  return -1;
214
238
  }
215
239
 
216
240
  function present(text: string, sigil: string, chosen: string[]): string[] {
217
241
  const found: Array<{ index: number; value: string }> = [];
242
+
218
243
  for (const value of new Set(chosen)) {
219
244
  const index = findToken(text, sigil, value, 0);
245
+
220
246
  if (index >= 0) {
221
247
  found.push({ index, value });
222
248
  }
223
249
  }
250
+
224
251
  return found.sort((a, b) => a.index - b.index).map((entry) => entry.value);
225
252
  }
226
253
 
@@ -237,6 +264,7 @@ export function presentSkills(text: string, chosen: string[]): string[] {
237
264
  /* The ids among `ids` whose `#name` still sits in the text as a whole token, in text order and without doubles. */
238
265
  export function presentReferences(text: string, ids: readonly string[], tokens: readonly ReferenceToken[]): string[] {
239
266
  const wanted = new Set(ids);
267
+
240
268
  return chipRanges(text, [], [], [], tokens)
241
269
  .map((range) => range.value)
242
270
  .filter((id, index, found) => wanted.has(id) && found.indexOf(id) === index);
@@ -247,12 +275,15 @@ export function chipText(segment: ChipSegment): string {
247
275
  if (segment.kind === 'mention') {
248
276
  return `@${segment.path}`;
249
277
  }
278
+
250
279
  if (segment.kind === 'skill') {
251
280
  return `$${segment.name}`;
252
281
  }
282
+
253
283
  if (segment.kind === 'reference') {
254
284
  return `#${segment.name}`;
255
285
  }
286
+
256
287
  return segment.text;
257
288
  }
258
289
 
@@ -262,9 +293,11 @@ export function chipText(segment: ChipSegment): string {
262
293
  */
263
294
  export function pastedMentions(text: string): string[] {
264
295
  const tokens = text.trim().split(/\s+/);
296
+
265
297
  if (tokens.some((token) => !/^@[^\s@]+$/.test(token))) {
266
298
  return [];
267
299
  }
300
+
268
301
  return [...new Set(tokens.map((token) => token.slice(1)))];
269
302
  }
270
303
 
@@ -295,27 +328,36 @@ export function chipRanges(
295
328
  ...[...new Set(skills)].map((value) => ({ sigil: '$', kind: 'skill' as const, spelled: value, value })),
296
329
  ...references.filter((token) => token.name !== '').map((token) => ({ sigil: '#', kind: 'reference' as const, spelled: token.name, value: token.id }))
297
330
  ].sort((a, b) => b.spelled.length - a.spelled.length);
331
+
298
332
  const allowed = (from: number, to: number): boolean => !excluded.some((range) => from < range.to && to > range.from);
333
+
299
334
  const ranges: ChipRange[] = [];
300
335
  let cursor = 0;
336
+
301
337
  while (cursor < text.length) {
302
338
  let best: ChipRange | null = null;
339
+
303
340
  for (const token of tokens) {
304
341
  const length = token.spelled.length + 1;
305
342
  let index = findToken(text, token.sigil, token.spelled, cursor);
343
+
306
344
  while (index >= 0 && !allowed(index, index + length)) {
307
345
  index = findToken(text, token.sigil, token.spelled, index + 1);
308
346
  }
347
+
309
348
  if (index >= 0 && (best === null || index < best.from)) {
310
349
  best = { from: index, to: index + length, kind: token.kind, value: token.value };
311
350
  }
312
351
  }
352
+
313
353
  if (best === null) {
314
354
  break;
315
355
  }
356
+
316
357
  ranges.push(best);
317
358
  cursor = best.to;
318
359
  }
360
+
319
361
  return ranges;
320
362
  }
321
363
 
@@ -323,9 +365,11 @@ function segmentOf(text: string, range: ChipRange): ChipSegment {
323
365
  if (range.kind === 'mention') {
324
366
  return { kind: 'mention', path: range.value };
325
367
  }
368
+
326
369
  if (range.kind === 'skill') {
327
370
  return { kind: 'skill', name: range.value };
328
371
  }
372
+
329
373
  return { kind: 'reference', id: range.value, name: text.slice(range.from + 1, range.to) };
330
374
  }
331
375
 
@@ -349,29 +393,37 @@ export function namedReferenceRanges(
349
393
  excluded: readonly TextRange[] = []
350
394
  ): ChipRange[] {
351
395
  const byName = new Map<string, string[]>();
396
+
352
397
  for (const item of items) {
353
398
  if (item.name !== '') {
354
399
  byName.set(item.name, [...(byName.get(item.name) ?? []), item.id]);
355
400
  }
356
401
  }
402
+
357
403
  const names = [...byName.keys()].sort((a, b) => b.length - a.length);
358
404
  const preferred = new Set(referenced);
359
405
  const ranges: ChipRange[] = [];
360
406
  let index = text.indexOf('#');
407
+
361
408
  while (index >= 0) {
362
409
  const from = index;
410
+
363
411
  const name =
364
412
  from > 0 && !REFERENCE_OPENER.test(text[from - 1]!)
365
413
  ? undefined
366
414
  : names.find((candidate) => text.startsWith(candidate, from + 1) && isTokenEnd(text[from + 1 + candidate.length]));
415
+
367
416
  const to = name === undefined ? from + 1 : from + 1 + name.length;
368
417
  const ids = name === undefined ? [] : byName.get(name)!;
369
418
  const id = ids.find((candidate) => preferred.has(candidate)) ?? (ids.length === 1 ? ids[0] : undefined);
419
+
370
420
  if (id !== undefined && !excluded.some((range) => from < range.to && to > range.from)) {
371
421
  ranges.push({ from, to, kind: 'reference', value: id });
372
422
  }
423
+
373
424
  index = text.indexOf('#', to);
374
425
  }
426
+
375
427
  return ranges;
376
428
  }
377
429
 
@@ -383,15 +435,19 @@ export function tokenizeNamedReferences(text: string, items: readonly ReferenceT
383
435
  function segmentsOf(text: string, ranges: readonly ChipRange[]): ChipSegment[] {
384
436
  const segments: ChipSegment[] = [];
385
437
  let cursor = 0;
438
+
386
439
  for (const range of ranges) {
387
440
  if (range.from > cursor) {
388
441
  segments.push({ kind: 'text', text: text.slice(cursor, range.from) });
389
442
  }
443
+
390
444
  segments.push(segmentOf(text, range));
391
445
  cursor = range.to;
392
446
  }
447
+
393
448
  if (cursor < text.length) {
394
449
  segments.push({ kind: 'text', text: text.slice(cursor) });
395
450
  }
451
+
396
452
  return segments;
397
453
  }
@@ -40,9 +40,11 @@ export class ChatPromptFeed {
40
40
  dispose(): void {
41
41
  this.disposed = true;
42
42
  this.unsubscribe?.();
43
+
43
44
  for (const release of this.children.values()) {
44
45
  release();
45
46
  }
47
+
46
48
  this.children.clear();
47
49
  }
48
50
 
@@ -50,49 +52,63 @@ export class ChatPromptFeed {
50
52
  if (this.disposed) {
51
53
  return;
52
54
  }
55
+
53
56
  const store = useChats.getState();
54
57
  const subjects = new Map<string, ChatPromptSubject>();
55
58
  const seen = new Set<string>();
59
+
56
60
  const visit = (chatId: string, source?: ChatPromptSubject['source']): void => {
57
61
  if (seen.has(chatId)) {
58
62
  return;
59
63
  }
64
+
60
65
  seen.add(chatId);
61
66
  const key = this.scope.keyOf(chatId);
62
67
  const row = store.byKey[key];
63
68
  const info = store.statusByKey[key]?.info ?? row?.info;
69
+
64
70
  for (const item of waitingRequestsOf(row)) {
65
71
  if (info !== undefined && (chatId === this.chatId || stillWaiting(item, info))) {
66
72
  subjects.set(`${chatId}:${item.requestId}`, { kind: 'chat', nodeId: chatId, item, ...(source === undefined ? {} : { source }) });
67
73
  }
68
74
  }
75
+
69
76
  for (const item of Object.values(row?.structure ?? {})) {
70
77
  if (item.kind !== 'subagent' || item.childId === undefined || seen.has(item.childId)) {
71
78
  continue;
72
79
  }
80
+
73
81
  const childInfo = store.statusByKey[this.scope.keyOf(item.childId)]?.info;
82
+
74
83
  if (item.status !== 'running' && childInfo?.status !== 'needs-you') {
75
84
  continue;
76
85
  }
86
+
77
87
  if (!this.children.has(item.childId)) {
78
88
  this.children.set(item.childId, this.scope.chats.observe(item.childId));
79
89
  }
90
+
80
91
  visit(item.childId, { label: subagentTitle(item), provider: childInfo?.provider });
81
92
  }
82
93
  };
94
+
83
95
  visit(this.chatId);
96
+
84
97
  for (const [chatId, release] of this.children) {
85
98
  if (!seen.has(chatId)) {
86
99
  this.children.delete(chatId);
87
100
  release();
88
101
  }
89
102
  }
103
+
90
104
  const next = [...subjects.values()];
105
+
91
106
  if (
92
107
  this.current === null ||
93
108
  next.length !== this.current.length ||
94
109
  next.some((subject, index) => {
95
110
  const previous = this.current?.[index];
111
+
96
112
  return (
97
113
  subject.nodeId !== previous?.nodeId ||
98
114
  subject.item !== previous.item ||
@@ -14,22 +14,30 @@ export function persistedJson<T>(
14
14
  ): PersistedJson<T> {
15
15
  let memory = fallback;
16
16
  let unwritten = false;
17
+
17
18
  const currentKey = (): string => (typeof key === 'string' ? key : key());
19
+
18
20
  return {
19
21
  read: () => {
20
22
  hydrateChatStorage();
23
+
21
24
  if (unwritten) {
22
25
  return memory;
23
26
  }
27
+
24
28
  try {
25
29
  const storage = chatStorageAdapter();
30
+
26
31
  if (!storage) {
27
32
  return memory;
28
33
  }
34
+
29
35
  let raw = storage.getItem(currentKey());
36
+
30
37
  if (raw === null) {
31
38
  for (const legacy of legacyKeys()) {
32
39
  raw = storage.getItem(legacy);
40
+
33
41
  if (raw !== null) {
34
42
  // Keep the old key for clients still using it, and migrate without changing the record.
35
43
  try {
@@ -37,11 +45,14 @@ export function persistedJson<T>(
37
45
  } catch {
38
46
  // A full storage still lets the existing record be read.
39
47
  }
48
+
40
49
  break;
41
50
  }
42
51
  }
43
52
  }
53
+
44
54
  memory = parse(raw);
55
+
45
56
  return memory;
46
57
  } catch {
47
58
  return memory;
@@ -51,13 +62,17 @@ export function persistedJson<T>(
51
62
  hydrateChatStorage();
52
63
  memory = value;
53
64
  unwritten = true;
65
+
54
66
  try {
55
67
  const storage = chatStorageAdapter();
68
+
56
69
  if (!storage) {
57
70
  return false;
58
71
  }
72
+
59
73
  storage.setItem(currentKey(), JSON.stringify(value));
60
74
  unwritten = false;
75
+
61
76
  return true;
62
77
  } catch {
63
78
  return false;
@@ -43,9 +43,11 @@ function parseFavoriteModels(stored: unknown): Partial<Record<AgentKind, string[
43
43
  if (typeof stored !== 'object' || stored === null || Array.isArray(stored)) {
44
44
  return {};
45
45
  }
46
+
46
47
  return Object.fromEntries(
47
48
  Object.entries(stored).flatMap(([provider, slugs]) => {
48
49
  const kept = Array.isArray(slugs) ? slugs.filter((slug): slug is string => typeof slug === 'string' && slug !== '') : [];
50
+
49
51
  return kept.length === 0 ? [] : [[provider, kept]];
50
52
  })
51
53
  );
@@ -60,8 +62,10 @@ export function parseChatPreferences(raw: string | null): ChatPreferences {
60
62
  if (raw === null) {
61
63
  return DEFAULT_CHAT_PREFERENCES;
62
64
  }
65
+
63
66
  try {
64
67
  const stored = JSON.parse(raw) as Partial<ChatPreferences>;
68
+
65
69
  return {
66
70
  selectionByProvider: { ...stored.selectionByProvider },
67
71
  lastProvider: stored.lastProvider ?? null,
@@ -86,9 +90,11 @@ export function defaultProvider(preferences: ChatPreferences): AgentKind | null
86
90
  /* The model a chat of this provider starts with; null lets the host fall back to the CLI's own default. */
87
91
  export function selectionFor(preferences: ChatPreferences, provider: AgentKind | null | undefined): ModelSelection | null {
88
92
  const owner = provider ?? preferences.lastProvider;
93
+
89
94
  if (owner === null) {
90
95
  return null;
91
96
  }
97
+
92
98
  return preferences.selectionByProvider[owner] ?? null;
93
99
  }
94
100
 
@@ -112,10 +118,13 @@ export function withSelection(preferences: ChatPreferences, provider: AgentKind,
112
118
  */
113
119
  export function accountFor(preferences: ChatPreferences, scopeId: string, provider: AgentKind, accounts?: ProviderAccounts | null): string | null {
114
120
  const picked = preferences.accountByMachine[scopeId]?.[provider] ?? null;
121
+
115
122
  if (picked === null || accounts === undefined) {
116
123
  return picked;
117
124
  }
125
+
118
126
  const account = accounts?.accounts[picked];
127
+
119
128
  return account?.kind === provider && account.enabled !== false ? picked : null;
120
129
  }
121
130
 
@@ -124,6 +133,7 @@ export function withAccount(preferences: ChatPreferences, scopeId: string, provi
124
133
  const { [provider]: _previous, ...others } = preferences.accountByMachine[scopeId] ?? {};
125
134
  const picks = account === null || account === provider ? others : { ...others, [provider]: account };
126
135
  const { [scopeId]: _machine, ...machines } = preferences.accountByMachine;
136
+
127
137
  return { ...preferences, accountByMachine: Object.keys(picks).length === 0 ? machines : { ...machines, [scopeId]: picks } };
128
138
  }
129
139
 
@@ -131,8 +141,10 @@ export function withAccount(preferences: ChatPreferences, scopeId: string, provi
131
141
  export function chatPreferencesPayload(preferences: ChatPreferences, scopeId: string, accounts?: ProviderAccounts | null): ChatPreferencesPayload {
132
142
  const picks = Object.keys(preferences.accountByMachine[scopeId] ?? {}).flatMap((provider) => {
133
143
  const account = accountFor(preferences, scopeId, provider as AgentKind, accounts);
144
+
134
145
  return account === null ? [] : [[provider, account] as const];
135
146
  });
147
+
136
148
  return {
137
149
  runtimeMode: preferences.runtimeMode,
138
150
  terminalRuntimeMode: preferences.terminalRuntimeMode,
@@ -156,6 +168,7 @@ export function readChatPreferences(): ChatPreferences {
156
168
 
157
169
  /* Where the remembered default is kept, which an app with more than one window of the same origin listens for. */
158
170
  export let CHAT_PREFERENCES_KEY = storageKey();
171
+
159
172
  onChatStorageConfiguration(() => {
160
173
  CHAT_PREFERENCES_KEY = storageKey();
161
174
  });
@@ -192,6 +205,7 @@ export function rememberSendKey(sendKey: ChatSendKey): void {
192
205
  export function rememberChatSelection(provider: AgentKind, selection: ModelSelection): void {
193
206
  const current = useChatPreferences.getState();
194
207
  const previous = current.selectionByProvider[provider];
208
+
195
209
  if (
196
210
  current.lastProvider === provider &&
197
211
  previous?.model === selection.model &&
@@ -200,6 +214,7 @@ export function rememberChatSelection(provider: AgentKind, selection: ModelSelec
200
214
  ) {
201
215
  return;
202
216
  }
217
+
203
218
  write(withSelection(current, provider, selection));
204
219
  }
205
220
 
@@ -217,6 +232,7 @@ export function withFavoriteToggled(preferences: ChatPreferences, provider: Agen
217
232
  const current = preferences.favoriteModels[provider] ?? [];
218
233
  const next = current.includes(slug) ? current.filter((each) => each !== slug) : [...current, slug];
219
234
  const { [provider]: _previous, ...others } = preferences.favoriteModels;
235
+
220
236
  return { ...preferences, favoriteModels: next.length === 0 ? others : { ...others, [provider]: next } };
221
237
  }
222
238
 
@@ -15,6 +15,7 @@ export function holdsPrompt(input: HoldInput, now: number): boolean {
15
15
  if (input.composing) {
16
16
  return true;
17
17
  }
18
+
18
19
  return input.typedAt !== null && now - input.typedAt < PROMPT_HOLD_PAUSE_MS;
19
20
  }
20
21
 
@@ -23,5 +24,6 @@ export function holdLiftsIn(input: HoldInput, now: number): number | null {
23
24
  if (input.composing || input.typedAt === null) {
24
25
  return null;
25
26
  }
27
+
26
28
  return Math.max(0, input.typedAt + PROMPT_HOLD_PAUSE_MS - now);
27
29
  }