@ouraihub/aether-ui 0.1.0 → 0.1.2

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 (90) hide show
  1. package/package.json +3 -3
  2. package/src/chat/ApprovalCard.svelte +8 -2
  3. package/src/chat/ChatComposer.svelte +319 -0
  4. package/src/chat/ChatMessage.svelte +262 -0
  5. package/src/chat/ChatPanel.css +386 -0
  6. package/src/chat/ChatPanel.svelte +276 -1880
  7. package/src/chat/ChatPanelApprovals.svelte +28 -0
  8. package/src/chat/ChatPanelComposer.svelte +336 -0
  9. package/src/chat/ChatPanelDialogs.svelte +242 -0
  10. package/src/chat/ChatPanelHeader.svelte +108 -0
  11. package/src/chat/ChatPanelMessages.svelte +166 -0
  12. package/src/chat/ChatPanelMode.svelte +190 -0
  13. package/src/chat/ChatPanelState.svelte.ts +1 -0
  14. package/src/chat/ChatPanelToolbar.svelte +436 -0
  15. package/src/chat/ChatSurface.svelte +168 -0
  16. package/src/chat/ContextUsageStatus.svelte +97 -0
  17. package/src/chat/MessageItem.svelte +111 -214
  18. package/src/chat/ModelEffortPicker.svelte +326 -0
  19. package/src/chat/PickerEffort.svelte +71 -0
  20. package/src/chat/ReasoningControl.svelte +184 -0
  21. package/src/chat/ReasoningSlider.svelte +174 -0
  22. package/src/chat/SkillPicker.svelte +14 -6
  23. package/src/chat/UsageControl.svelte +223 -0
  24. package/src/chat/UsageDetails.svelte +44 -90
  25. package/src/chat/chat-attachments.svelte.ts +78 -0
  26. package/src/chat/chat-panel-history.svelte.ts +430 -0
  27. package/src/chat/chat-panel-models.svelte.ts +97 -0
  28. package/src/chat/chat-panel-props.ts +38 -0
  29. package/src/chat/context-status.ts +101 -0
  30. package/src/chat/icons/LICENSE-REMIX +186 -0
  31. package/src/chat/icons/LICENSE-SIMPLE-ICONS +30 -0
  32. package/src/chat/icons/PathIcon.svelte +8 -0
  33. package/src/chat/icons/effort-icons.ts +65 -0
  34. package/src/chat/icons/status-icons.ts +105 -0
  35. package/src/chat/index.ts +25 -1
  36. package/src/chat/model-effort.ts +177 -0
  37. package/src/chat/run-session-history.ts +56 -0
  38. package/src/chat/run-session-types.ts +69 -0
  39. package/src/chat/run-session.svelte.ts +73 -135
  40. package/src/chat/test-support/chat-fixture.ts +194 -0
  41. package/src/chat/test-support/conversation-fixtures.ts +32 -0
  42. package/src/chat/test-support/run-session-fixture.ts +112 -0
  43. package/src/dev/Demo.svelte +3 -1
  44. package/src/i18n/en-chat.d.ts +1 -0
  45. package/src/i18n/en-chat.js +201 -0
  46. package/src/i18n/en.ts +58 -180
  47. package/src/i18n/index.ts +7 -2
  48. package/src/i18n/zh-CN-chat.d.ts +1 -0
  49. package/src/i18n/zh-CN-chat.js +190 -0
  50. package/src/i18n/zh-CN.ts +8 -153
  51. package/src/logs/LogPanel.svelte +9 -3
  52. package/src/logs/LogSettings.svelte +3 -1
  53. package/src/mcp/McpClientEditor.svelte +204 -0
  54. package/src/mcp/McpClientSettings.svelte +57 -130
  55. package/src/mcp/McpServerSettings.svelte +47 -11
  56. package/src/mcp/input.ts +2 -1
  57. package/src/ports/contract.ts +11 -2
  58. package/src/ports/fake-optional.ts +81 -0
  59. package/src/ports/fake.ts +90 -86
  60. package/src/ports/search.ts +11 -2
  61. package/src/ports/test-support/fake-fixture.ts +63 -0
  62. package/src/providers/EndpointKeysDialog.svelte +27 -13
  63. package/src/providers/EndpointModels.svelte +311 -0
  64. package/src/providers/EndpointPanel.svelte +33 -258
  65. package/src/providers/ModelPickerDialog.svelte +59 -16
  66. package/src/providers/ModelPricing.svelte +125 -0
  67. package/src/providers/ModelSelect.svelte +22 -8
  68. package/src/providers/ModelSettingsDialog.svelte +61 -126
  69. package/src/providers/ProviderDialogs.svelte +120 -0
  70. package/src/providers/ProviderList.svelte +159 -0
  71. package/src/providers/ProvidersSettings.svelte +67 -207
  72. package/src/providers/RequestConfigDialog.svelte +40 -12
  73. package/src/providers/SecretInput.svelte +11 -3
  74. package/src/providers/draft.ts +39 -13
  75. package/src/providers/edit-session.ts +31 -7
  76. package/src/providers/reasoning.ts +18 -4
  77. package/src/providers/revision.svelte.ts +3 -1
  78. package/src/providers/test-support/providers-fixture.ts +120 -0
  79. package/src/search/SearchAdvancedTools.svelte +182 -0
  80. package/src/search/SearchProviderCard.svelte +195 -0
  81. package/src/search/SearchSettings.svelte +62 -265
  82. package/src/search/settings-flow.ts +8 -3
  83. package/src/skills/SkillAdd.svelte +51 -18
  84. package/src/skills/SkillAuthoring.svelte +69 -20
  85. package/src/skills/SkillEditor.svelte +13 -3
  86. package/src/skills/SkillPreviewDialog.svelte +19 -5
  87. package/src/skills/SkillsSettings.svelte +11 -4
  88. package/src/skills/test-support/skills-fixture.ts +139 -0
  89. package/src/tauri/index.ts +23 -7
  90. package/src/web/index.ts +90 -25
@@ -0,0 +1,186 @@
1
+ Remix Icon License v1.0
2
+ Version 1.0 – January 2026
3
+ Copyright (c) 2017–2026 Remix Design
4
+
5
+ This License governs the use of the Remix Icon library, including all icons,
6
+ SVG files, font files, and related assets (collectively, the "Icons").
7
+
8
+ 1. Grant of License
9
+
10
+ Subject to the terms and conditions of this License, Remix Design grants you
11
+ a worldwide, royalty-free, non-exclusive license to use, copy, modify,
12
+ merge, and distribute the Icons, as permitted below.
13
+
14
+ 2. Permitted Uses
15
+
16
+ You are permitted to:
17
+
18
+ 2.1 Commercial and Non-Commercial Use
19
+ Use the Icons in personal, commercial, and client projects, including
20
+ but not limited to:
21
+ - Websites, web applications, and mobile applications
22
+ - Software products and SaaS platforms
23
+ - User interface designs, design systems, and UI kits
24
+ - Presentations, documentation, and marketing materials
25
+ - Physical or digital products where the Icons are decorative or
26
+ functional elements
27
+
28
+ 2.2 Modification and Derivative Works
29
+ - Modify, adapt, or customize the Icons
30
+ - Change colors, sizes, stroke styles, or other visual attributes
31
+ to fit your project needs
32
+
33
+ 2.3 Distribution as Part of a Larger Work
34
+ Include the Icons (modified or unmodified) as part of a larger product,
35
+ provided that:
36
+ - The Icons are functional or decorative components of the product, and
37
+ - The Icons are not marketed or sold as the primary value of the product
38
+
39
+ 2.4 Attribution (Optional)
40
+ Attribution to Remix Icon is appreciated but not required.
41
+ Suggested attribution:
42
+ "Icons by Remix Icon (https://remixicon.com)"
43
+
44
+ 3. Restrictions
45
+
46
+ 3.1 Prohibited: Standalone Icon Sales
47
+ You may NOT:
48
+ - Sell, sublicense, or distribute the Icons as a standalone product
49
+ - Package the Icons as an independent icon pack, icon library, or
50
+ icon font for sale
51
+ - Offer the Icons as a downloadable asset collection where the Icons
52
+ themselves constitute the primary product value
53
+
54
+ Clarification:
55
+ A product is considered a prohibited standalone icon sale if:
56
+ - The Icons are the primary advertised feature, or
57
+ - Removing the Icons would eliminate the core value of the product
58
+
59
+ Examples:
60
+ - Prohibited: "Premium Icon Pack – 2,000+ Icons for $29"
61
+ - Prohibited: "Extended Remix Icon Library (Paid)"
62
+ - Permitted: Website or app templates that include Icons as UI elements
63
+ - Permitted: Design systems or UI kits where Icons are a minor component
64
+
65
+ 3.2 Prohibited: Competing Icon Libraries
66
+ You may NOT use the Icons to create, distribute, or sell a competing
67
+ icon library or icon set, even if the Icons are modified with only
68
+ minor or superficial changes (such as color, size, stroke width,
69
+ corner radius, or layout).
70
+
71
+ 3.3 Prohibited: Logo, Trademark, and Brand Identity Use
72
+ You may NOT use the Icons, whether modified or unmodified, as:
73
+ - A logo, trademark, or brand identifier
74
+ - The primary visual identity of a company, product, or service
75
+
76
+ This includes, but is not limited to:
77
+ - Creating a logo by applying simple modifications such as color changes,
78
+ rotation, combination, or layout
79
+ - Using an Icon as a brand mark, app icon, or identity symbol where the
80
+ Icon itself represents the brand
81
+
82
+ Rationale:
83
+ Remix Icon is designed for functional and informational use. Using the
84
+ Icons as brand identifiers may impair their recognizability and neutrality
85
+ in other functional contexts.
86
+
87
+ 4. Brand Icons and Trademarks
88
+
89
+ Some Icons represent third-party brands or services.
90
+
91
+ 4.1 Permitted Use of Brand Icons
92
+ You may use brand icons solely to represent or link to the corresponding
93
+ brand, product, or service.
94
+ Examples:
95
+ - Using a GitHub icon to link to a GitHub repository
96
+ - Displaying social media icons in a website footer
97
+
98
+ 4.2 Prohibited Use of Brand Icons
99
+ You may NOT:
100
+ - Use brand icons as your own logo or trademark
101
+ - Modify brand icons to create a new brand identity
102
+ - Use brand icons in a way that implies endorsement or affiliation
103
+ by the brand owner
104
+ - Use brand icons for purposes unrelated to the represented brand
105
+
106
+ 4.3 Trademark Responsibility
107
+ All brand icons are subject to the trademark rights of their respective
108
+ owners. This License does not grant any trademark rights.
109
+ Users are responsible for ensuring that their use of brand icons complies
110
+ with applicable trademark laws.
111
+
112
+ 5. Copyright and License Notice
113
+
114
+ When distributing the complete Icon library or substantial portions thereof:
115
+
116
+ - You must retain existing copyright notices in source files
117
+ - You should include a copy of this License or a link to it
118
+
119
+ For individual use of Icons within projects (e.g., compiled apps, websites,
120
+ presentations), no copyright notice is required.
121
+
122
+ 6. No Trademark Rights
123
+
124
+ This License does not grant permission to use:
125
+
126
+ - The "Remix Icon" name, logo, or trademarks
127
+ - Any trademarks represented by brand icons
128
+
129
+ You may refer to Remix Icon by name solely for attribution purposes.
130
+
131
+ 7. Disclaimer of Warranty
132
+
133
+ THE ICONS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
134
+ IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY,
135
+ FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
136
+
137
+ 8. Limitation of Liability
138
+
139
+ IN NO EVENT SHALL REMIX DESIGN BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
140
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING
141
+ FROM, OUT OF, OR IN CONNECTION WITH THE ICONS OR THEIR USE.
142
+
143
+ 9. License Compatibility
144
+
145
+ You may integrate the Icons into projects under permissive licenses
146
+ (such as MIT, BSD, or Apache 2.0), provided that:
147
+
148
+ - The Icons themselves remain governed by this License
149
+ - The restrictions in Section 3 continue to apply
150
+
151
+ This License may be incompatible with licenses that require unrestricted
152
+ redistribution of assets as standalone products or mandatory application
153
+ of strong copyleft or share-alike terms.
154
+
155
+ 10. Termination
156
+
157
+ Your rights under this License terminate automatically if you breach its
158
+ terms.
159
+
160
+ Upon termination:
161
+
162
+ - You must cease any distribution that violates this License
163
+ - You may continue to use the Icons in projects already distributed
164
+ prior to termination
165
+ - End users who received such projects are not affected
166
+
167
+ 11. License Versioning
168
+
169
+ Remix Design may publish revised versions of this License.
170
+
171
+ You may choose to use the Icons under:
172
+
173
+ - The version of this License under which you received them, or
174
+ - Any later version published by Remix Design
175
+
176
+ Once chosen, that version may be used indefinitely.
177
+
178
+ How to Apply This License
179
+
180
+ Include the following notice in your documentation when appropriate:
181
+
182
+ "This project uses icons from Remix Icon (https://remixicon.com),
183
+ licensed under the Remix Icon License v1.0."
184
+
185
+ When distributing the complete Icon library or substantial portions of it,
186
+ include a copy of this License.
@@ -0,0 +1,30 @@
1
+ # CC0 1.0 Universal
2
+
3
+ ## Statement of Purpose
4
+
5
+ The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an “owner”) of an original work of authorship and/or a database (each, a “Work”).
6
+
7
+ Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works (“Commons”) that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others.
8
+
9
+ For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the “Affirmer”), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights.
10
+
11
+ 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights (“Copyright and Related Rights”). Copyright and Related Rights include, but are not limited to, the following:
12
+ 1. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work;
13
+ 2. moral rights retained by the original author(s) and/or performer(s);
14
+ 3. publicity and privacy rights pertaining to a person’s image or likeness depicted in a Work;
15
+ 4. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(i), below;
16
+ 5. rights protecting the extraction, dissemination, use and reuse of data in a Work;
17
+ 6. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and
18
+ 7. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof.
19
+
20
+ 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the “Waiver”). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer’s heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer’s express Statement of Purpose.
21
+
22
+ 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer’s express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer’s Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the “License”). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer’s express Statement of Purpose.
23
+
24
+ 4. Limitations and Disclaimers.
25
+ 1. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document.
26
+ 2. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.
27
+ 3. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person’s Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work.
28
+ 4. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
29
+
30
+ For more information, please see <https://creativecommons.org/publicdomain/zero/1.0>.
@@ -0,0 +1,8 @@
1
+ <script lang="ts">
2
+ // A 24×24 single-path mark colored by `currentColor`, for marks absent from the icon library.
3
+ import type { SVGAttributes } from 'svelte/elements'
4
+
5
+ let { d, ...rest }: { d: string } & SVGAttributes<SVGSVGElement> = $props()
6
+ </script>
7
+
8
+ <svg viewBox="0 0 24 24" aria-hidden="true" {...rest}><path fill="currentColor" {d} /></svg>
@@ -0,0 +1,65 @@
1
+ // Effort marks as 24×24 path data, colored by `currentColor`; one mark per reasoning level.
2
+ // Remix Icon 4.9.0 (LICENSE-REMIX): RiZzz, RiLeaf, RiFlashlight, RiContrastDrop2, RiSparkling2,
3
+ // RiSparkling and RiFire, Line variants. Path data unmodified.
4
+ import type { ReasoningEffort } from '../../ports'
5
+
6
+ const paths: Partial<Record<ReasoningEffort, string>> = {
7
+ none: [
8
+ 'M11 11V13L5.67257 19H11V21H3V19L8.32557 13H3V11H11ZM21 3V5L15.6726 11H21V13H13V11L18',
9
+ '.3256 5H13V3H21Z',
10
+ ].join(''),
11
+ minimal: [
12
+ 'M20.998 3V5C20.998 14.6274 15.6255 19 8.99805 19L5.24077 18.9999C5.0786 19.912 4.998',
13
+ '05 20.907 4.99805 22H2.99805C2.99805 20.6373 3.11376 19.3997 3.34381 18.2682C3.1133 ',
14
+ '16.9741 2.99805 15.2176 2.99805 13C2.99805 7.47715 7.4752 3 12.998 3C14.998 3 16.998',
15
+ ' 4 20.998 3ZM12.998 5C8.57977 5 4.99805 8.58172 4.99805 13C4.99805 13.3624 5.00125 1',
16
+ '3.7111 5.00759 14.0459C6.26198 12.0684 8.09902 10.5048 10.5019 9.13176L11.4942 10.86',
17
+ '82C8.6393 12.4996 6.74554 14.3535 5.77329 16.9998L8.99805 17C15.0132 17 18.8692 13.0',
18
+ '269 18.9949 5.38766C17.6229 5.52113 16.3481 5.436 14.7754 5.20009C13.6243 5.02742 13',
19
+ '.3988 5 12.998 5Z',
20
+ ].join(''),
21
+ low: ['M13 9H21L11 24V15H4L13 0V9ZM11 11V7.22063L7.53238 13H13V17.3944L17.263 11H11Z'].join(''),
22
+ medium: [
23
+ 'M12 3.09735L7.05025 8.04709C4.31658 10.7808 4.31658 15.2129 7.05025 17.9466C9.78392 ',
24
+ '20.6803 14.2161 20.6803 16.9497 17.9466C19.6834 15.2129 19.6834 10.7808 16.9497 8.04',
25
+ '71L12 3.09735ZM12 0.268921L18.364 6.63288C21.8787 10.1476 21.8787 15.8461 18.364 19.',
26
+ '3608C14.8492 22.8755 9.15076 22.8755 5.63604 19.3608C2.12132 15.8461 2.12132 10.1476',
27
+ ' 5.63604 6.63288L12 0.268921ZM7 12.9968H17C17 15.7583 14.7614 17.9968 12 17.9968C9.2',
28
+ '3858 17.9968 7 15.7583 7 12.9968Z',
29
+ ].join(''),
30
+ high: [
31
+ 'M17.0007 1.20825 18.3195 3.68108 20.7923 4.99992 18.3195 6.31876 17.0007 8.79159 15.',
32
+ '6818 6.31876 13.209 4.99992 15.6818 3.68108 17.0007 1.20825ZM10.6673 9.33325 15.6673',
33
+ ' 11.9999 10.6673 14.6666 8.00065 19.6666 5.33398 14.6666.333984 11.9999 5.33398 9.33',
34
+ '325 8.00065 4.33325 10.6673 9.33325ZM11.4173 11.9999 9.18905 10.8115 8.00065 8.58325',
35
+ ' 6.81224 10.8115 4.58398 11.9999 6.81224 13.1883 8.00065 15.4166 9.18905 13.1883 11.',
36
+ '4173 11.9999ZM19.6673 16.3333 18.0007 13.2083 16.334 16.3333 13.209 17.9999 16.334 1',
37
+ '9.6666 18.0007 22.7916 19.6673 19.6666 22.7923 17.9999 19.6673 16.3333Z',
38
+ ].join(''),
39
+ xhigh: [
40
+ 'M14 4.4375C15.3462 4.4375 16.4375 3.34619 16.4375 2H17.5625C17.5625 3.34619 18.6538 ',
41
+ '4.4375 20 4.4375V5.5625C18.6538 5.5625 17.5625 6.65381 17.5625 8H16.4375C16.4375 6.6',
42
+ '5381 15.3462 5.5625 14 5.5625V4.4375ZM1 11C4.31371 11 7 8.31371 7 5H9C9 8.31371 11.6',
43
+ '863 11 15 11V13C11.6863 13 9 15.6863 9 19H7C7 15.6863 4.31371 13 1 13V11ZM4.87601 12',
44
+ 'C6.18717 12.7276 7.27243 13.8128 8 15.124 8.72757 13.8128 9.81283 12.7276 11.124 12 ',
45
+ '9.81283 11.2724 8.72757 10.1872 8 8.87601 7.27243 10.1872 6.18717 11.2724 4.87601 12',
46
+ 'ZM17.25 14C17.25 15.7949 15.7949 17.25 14 17.25V18.75C15.7949 18.75 17.25 20.2051 17',
47
+ '.25 22H18.75C18.75 20.2051 20.2051 18.75 22 18.75V17.25C20.2051 17.25 18.75 15.7949 ',
48
+ '18.75 14H17.25Z',
49
+ ].join(''),
50
+ max: [
51
+ 'M12 23C16.1421 23 19.5 19.6421 19.5 15.5C19.5 14.6345 19.2697 13.8032 19 13.0296C17.',
52
+ '3333 14.6765 16.0667 15.5 15.2 15.5C19.1954 8.5 17 5.5 11 1.5C11.5 6.49951 8.20403 8',
53
+ '.77375 6.86179 10.0366C5.40786 11.4045 4.5 13.3462 4.5 15.5C4.5 19.6421 7.85786 23 1',
54
+ '2 23ZM12.7094 5.23498C15.9511 7.98528 15.9666 10.1223 13.463 14.5086C12.702 15.8419 ',
55
+ '13.6648 17.5 15.2 17.5C15.8884 17.5 16.5841 17.2992 17.3189 16.9051C16.6979 19.262 1',
56
+ '4.5519 21 12 21C8.96243 21 6.5 18.5376 6.5 15.5C6.5 13.9608 7.13279 12.5276 8.23225 ',
57
+ '11.4932C8.35826 11.3747 8.99749 10.8081 9.02477 10.7836C9.44862 10.4021 9.7978 10.06',
58
+ '63 10.1429 9.69677C11.3733 8.37932 12.2571 6.91631 12.7094 5.23498Z',
59
+ ].join(''),
60
+ }
61
+
62
+ /** The mark for `effort`; `auto` and `default` share the medium drop. */
63
+ export function effortPath(effort: ReasoningEffort): string {
64
+ return paths[effort] ?? (paths.medium as string)
65
+ }
@@ -0,0 +1,105 @@
1
+ // Status marks as 24×24 path data, colored by `currentColor`.
2
+ // Apple and Linux: Simple Icons (CC0, LICENSE-SIMPLE-ICONS). Windows: original geometry.
3
+ // Wand: Remix Icon 4.9.0 RiMagicLine (LICENSE-REMIX). Brand marks stay their owners' marks.
4
+
5
+ export const windowsPath = [
6
+ 'M2 5.2 10.2 4v7.2H2zm9.5-1.4L22 2.3v8.9H11.5zM2 12.6h8.2v7.2L2 18.7zm9.5 0H22v8.9L11',
7
+ '.5 20z',
8
+ ].join('')
9
+
10
+ export const applePath = [
11
+ 'M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.',
12
+ '675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.',
13
+ '935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.6',
14
+ '36-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.5',
15
+ '59-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.',
16
+ '012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3',
17
+ '.714 1.338.104 2.715-.688 3.559-1.701',
18
+ ].join('')
19
+
20
+ export const linuxPath = [
21
+ 'M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.',
22
+ '953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489a.424.4',
23
+ '24 0 00-.11.135c-.26.268-.45.6-.663.839-.199.199-.485.267-.797.4-.313.136-.658.269-.',
24
+ '864.68-.09.189-.136.394-.132.602 0 .199.027.4.055.536.058.399.116.728.04.97-.249.68-',
25
+ '.28 1.145-.106 1.484.174.334.535.47.94.601.81.2 1.91.135 2.774.6.926.466 1.866.67 2.',
26
+ '616.47.526-.116.97-.464 1.208-.946.587-.003 1.23-.269 2.26-.334.699-.058 1.574.267 2',
27
+ '.577.2.025.134.063.198.114.333l.003.003c.391.778 1.113 1.132 1.884 1.071.771-.06 1.5',
28
+ '92-.536 2.257-1.306.631-.765 1.683-1.084 2.378-1.503.348-.199.629-.469.649-.853.023-',
29
+ '.4-.2-.811-.714-1.376v-.097l-.003-.003c-.17-.2-.25-.535-.338-.926-.085-.401-.182-.78',
30
+ '6-.492-1.046h-.003c-.059-.054-.123-.067-.188-.135a.357.357 0 00-.19-.064c.431-1.278.',
31
+ '264-2.55-.173-3.694-.533-1.41-1.465-2.638-2.175-3.483-.796-1.005-1.576-1.957-1.56-3.',
32
+ '368.026-2.152.236-6.133-3.544-6.139zm.529 3.405h.013c.213 0 .396.062.584.198.19.135.',
33
+ '33.332.438.533.105.259.158.459.166.724 0-.02.006-.04.006-.06v.105a.086.086 0 01-.004',
34
+ '-.021l-.004-.024a1.807 1.807 0 01-.15.706.953.953 0 01-.213.335.71.71 0 00-.088-.042',
35
+ 'c-.104-.045-.198-.064-.284-.133a1.312 1.312 0 00-.22-.066c.05-.06.146-.133.183-.198.',
36
+ '053-.128.082-.264.088-.402v-.02a1.21 1.21 0 00-.061-.4c-.045-.134-.101-.2-.183-.333-',
37
+ '.084-.066-.167-.132-.267-.132h-.016c-.093 0-.176.03-.262.132a.8.8 0 00-.205.334 1.18',
38
+ ' 1.18 0 00-.09.4v.019c.002.089.008.179.02.267-.193-.067-.438-.135-.607-.202a1.635 1.',
39
+ '635 0 01-.018-.2v-.02a1.772 1.772 0 01.15-.768c.082-.22.232-.406.43-.533a.985.985 0 ',
40
+ '01.594-.2zm-2.962.059h.036c.142 0 .27.048.399.135.146.129.264.288.344.465.09.199.14.',
41
+ '4.153.667v.004c.007.134.006.2-.002.266v.08c-.03.007-.056.018-.083.024-.152.055-.274.',
42
+ '135-.393.2.012-.09.013-.18.003-.267v-.015c-.012-.133-.04-.2-.082-.333a.613.613 0 00-',
43
+ '.166-.267.248.248 0 00-.183-.064h-.021c-.071.006-.13.04-.186.132a.552.552 0 00-.12.2',
44
+ '7.944.944 0 00-.023.33v.015c.012.135.037.2.08.334.046.134.098.2.166.268.01.009.02.01',
45
+ '8.034.024-.07.057-.117.07-.176.136a.304.304 0 01-.131.068 2.62 2.62 0 01-.275-.402 1',
46
+ '.772 1.772 0 01-.155-.667 1.759 1.759 0 01.08-.668 1.43 1.43 0 01.283-.535c.128-.133',
47
+ '.26-.2.418-.2zm1.37 1.706c.332 0 .733.065 1.216.399.293.2.523.269 1.052.468h.003c.25',
48
+ '5.136.405.266.478.399v-.131a.571.571 0 01.016.47c-.123.31-.516.643-1.063.842v.002c-.',
49
+ '268.135-.501.333-.775.465-.276.135-.588.292-1.012.267a1.139 1.139 0 01-.448-.067 3.5',
50
+ '66 3.566 0 01-.322-.198c-.195-.135-.363-.332-.612-.465v-.005h-.005c-.4-.246-.616-.51',
51
+ '2-.686-.71-.07-.268-.005-.47.193-.6.224-.135.38-.271.483-.336.104-.074.143-.102.176-',
52
+ '.131h.002v-.003c.169-.202.436-.47.839-.601.139-.036.294-.065.466-.065zm2.8 2.142c.35',
53
+ '8 1.417 1.196 3.475 1.735 4.473.286.534.855 1.659 1.102 3.024.156-.005.33.018.513.06',
54
+ '4.646-1.671-.546-3.467-1.089-3.966-.22-.2-.232-.335-.123-.335.59.534 1.365 1.572 1.6',
55
+ '46 2.757.13.535.16 1.104.021 1.67.067.028.135.06.205.067 1.032.534 1.413.938 1.23 1.',
56
+ '537v-.043c-.06-.003-.12 0-.18 0h-.016c.151-.467-.182-.825-1.065-1.224-.915-.4-1.646-',
57
+ '.336-1.77.465-.008.043-.013.066-.018.135-.068.023-.139.053-.209.064-.43.268-.662.669',
58
+ '-.793 1.187-.13.533-.17 1.156-.205 1.869v.003c-.02.334-.17.838-.319 1.35-1.5 1.072-3',
59
+ '.58 1.538-5.348.334a2.645 2.645 0 00-.402-.533 1.45 1.45 0 00-.275-.333c.182 0 .338-',
60
+ '.03.465-.067a.615.615 0 00.314-.334c.108-.267 0-.697-.345-1.163-.345-.467-.931-.995-',
61
+ '1.788-1.521-.63-.4-.986-.87-1.15-1.396-.165-.534-.143-1.085-.015-1.645.245-1.07.873-',
62
+ '2.11 1.274-2.763.107-.065.037.135-.408.974-.396.751-1.14 2.497-.122 3.854a8.123 8.12',
63
+ '3 0 01.647-2.876c.564-1.278 1.743-3.504 1.836-5.268.048.036.217.135.289.202.218.133.',
64
+ '38.333.59.465.21.201.477.335.876.335.039.003.075.006.11.006.412 0 .73-.134.997-.268.',
65
+ '29-.134.52-.334.74-.4h.005c.467-.135.835-.402 1.044-.7zm2.185 8.958c.037.6.343 1.245',
66
+ '.882 1.377.588.134 1.434-.333 1.791-.765l.211-.01c.315-.007.577.01.847.268l.003.003c',
67
+ '.208.199.305.53.391.876.085.4.154.78.409 1.066.486.527.645.906.636 1.14l.003-.007v.0',
68
+ '18l-.003-.012c-.015.262-.185.396-.498.595-.63.401-1.746.712-2.457 1.57-.618.737-1.37',
69
+ ' 1.14-2.036 1.191-.664.053-1.237-.2-1.574-.898l-.005-.003c-.21-.4-.12-1.025.056-1.69',
70
+ '.176-.668.428-1.344.463-1.897.037-.714.076-1.335.195-1.814.12-.465.308-.797.641-.984',
71
+ 'l.045-.022zm-10.814.049h.01c.053 0 .105.005.157.014.376.055.706.333 1.023.752l.91 1.',
72
+ '664.003.003c.243.533.754 1.064 1.189 1.637.434.598.77 1.131.729 1.57v.006c-.057.744-',
73
+ '.48 1.148-1.125 1.294-.645.135-1.52.002-2.395-.464-.968-.536-2.118-.469-2.857-.602-.',
74
+ '369-.066-.61-.2-.723-.4-.11-.2-.113-.602.123-1.23v-.004l.002-.003c.117-.334.03-.752-',
75
+ '.027-1.118-.055-.401-.083-.71.043-.94.16-.334.396-.4.69-.533.294-.135.64-.202.915-.4',
76
+ '7h.002v-.002c.256-.268.445-.601.668-.838.19-.201.38-.336.663-.336zm7.159-9.074c-.435',
77
+ '.201-.945.535-1.488.535-.542 0-.97-.267-1.28-.466-.154-.134-.28-.268-.373-.335-.164-',
78
+ '.134-.144-.333-.074-.333.109.016.129.134.199.2.096.066.215.2.36.333.292.2.68.467 1.1',
79
+ '67.467.485 0 1.053-.267 1.398-.466.195-.135.445-.334.648-.467.156-.136.149-.267.279-',
80
+ '.267.128.016.034.134-.147.332a8.097 8.097 0 01-.69.468zm-1.082-1.583V5.64c-.006-.02.',
81
+ '013-.042.029-.05.074-.043.18-.027.26.004.063 0 .16.067.15.135-.006.049-.085.066-.135',
82
+ '.066-.055 0-.092-.043-.141-.068-.052-.018-.146-.008-.163-.065zm-.551 0c-.02.058-.113',
83
+ '.049-.166.066-.047.025-.086.068-.14.068-.05 0-.13-.02-.136-.068-.01-.066.088-.133.15',
84
+ '-.133.08-.031.184-.047.259-.005.019.009.036.03.03.05v.02h.003z',
85
+ ].join('')
86
+
87
+ export const wandPath = [
88
+ 'M15.1986 9.94447C14.7649 9.5337 14.4859 8.98613 14.4085 8.39384L14.0056 5.31138L11.2',
89
+ '75 6.79724C10.7503 7.08274 10.1433 7.17888 9.55608 7.06948L6.49998 6.50015L7.06931 9',
90
+ '.55625C7.17871 10.1435 7.08257 10.7505 6.79707 11.2751L5.31121 14.0057L8.39367 14.40',
91
+ '86C8.98596 14.4861 9.53353 14.7651 9.94431 15.1987L12.0821 17.4557L13.4178 14.6486C1',
92
+ '3.6745 14.1092 14.109 13.6747 14.6484 13.418L17.4555 12.0823L15.1986 9.94447ZM15.223',
93
+ '8 15.5079L13.0111 20.1581C12.8687 20.4573 12.5107 20.5844 12.2115 20.442C12.1448 20.',
94
+ '4103 12.0845 20.3665 12.0337 20.3129L8.49229 16.5741C8.39749 16.474 8.27113 16.4096 ',
95
+ '8.13445 16.3918L3.02816 15.7243C2.69958 15.6814 2.46804 15.3802 2.51099 15.0516C2.52',
96
+ '056 14.9784 2.54359 14.9075 2.5789 14.8426L5.04031 10.3192C5.1062 10.1981 5.12839 10',
97
+ '.058 5.10314 9.92253L4.16 4.85991C4.09931 4.53414 4.3142 4.22086 4.63997 4.16017C4.7',
98
+ '126 4.14664 4.78711 4.14664 4.85974 4.16017L9.92237 5.10331C10.0579 5.12855 10.198 5',
99
+ '.10637 10.319 5.04048L14.8424 2.57907C15.1335 2.42068 15.4979 2.52825 15.6562 2.8193',
100
+ '1C15.6916 2.88421 15.7146 2.95507 15.7241 3.02833L16.3916 8.13462C16.4095 8.2713 16.',
101
+ '4739 8.39766 16.5739 8.49245L20.3127 12.0338C20.5533 12.2617 20.5636 12.6415 20.3357',
102
+ ' 12.8821C20.2849 12.9357 20.2246 12.9795 20.1579 13.0112L15.5078 15.224C15.3833 15.2',
103
+ '832 15.283 15.3835 15.2238 15.5079ZM16.0206 17.435L17.4348 16.0208L21.6775 20.2634L2',
104
+ '0.2633 21.6776L16.0206 17.435Z',
105
+ ].join('')
package/src/chat/index.ts CHANGED
@@ -1,2 +1,26 @@
1
- // ChatPanel (docs/16 §4.2); it renders ToolCallItem, ApprovalCard and SkillPicker itself.
2
1
  export { default as ChatPanel } from './ChatPanel.svelte'
2
+ export { default as ChatSurface } from './ChatSurface.svelte'
3
+ export { default as ChatComposer } from './ChatComposer.svelte'
4
+ export { default as ChatMessage } from './ChatMessage.svelte'
5
+ export { default as ReasoningControl } from './ReasoningControl.svelte'
6
+ export { default as UsageControl } from './UsageControl.svelte'
7
+ export { default as ModelEffortPicker } from './ModelEffortPicker.svelte'
8
+ export { default as ContextUsageStatus } from './ContextUsageStatus.svelte'
9
+ export { default as ReasoningSlider } from './ReasoningSlider.svelte'
10
+ export {
11
+ groupModels,
12
+ modelKey,
13
+ nextValue,
14
+ parseQuery,
15
+ pickerModels,
16
+ type PickerModel,
17
+ type PickerValue,
18
+ } from './model-effort'
19
+ export {
20
+ compact,
21
+ detectPlatform,
22
+ pressure,
23
+ spending,
24
+ type Platform,
25
+ type SpendingTurn,
26
+ } from './context-status'
@@ -0,0 +1,177 @@
1
+ // Pure rules for the composer model/effort picker (docs/31 §3–§4.2): search, ranking,
2
+ // carry-over of an explicit effort and the secret-free row projection. No I/O.
3
+ import type { KitI18n } from '../i18n'
4
+ import type { ModelOption, Provider, ReasoningEffort } from '../ports'
5
+ import { REASONING_EFFORTS } from '../providers/reasoning'
6
+
7
+ /** One selectable model as the picker shows it; `key` stays an opaque host identity. */
8
+ export interface PickerModel {
9
+ key: string
10
+ providerId: string
11
+ provider: string
12
+ name: string
13
+ /** Route, price and context facts; empty parts are omitted, never guessed. */
14
+ detail: string
15
+ /** The route has an enabled, readable key. Keyless models stay selectable. */
16
+ hasKey: boolean
17
+ /** Declared levels in display order, without `default`. */
18
+ efforts: ReasoningEffort[]
19
+ contextWindow: number | null
20
+ }
21
+
22
+ /** `effort: 'default'` sends no reasoning control; it is not `none`. */
23
+ export interface PickerValue {
24
+ key: string
25
+ effort: ReasoningEffort
26
+ }
27
+
28
+ export const modelKey = (endpointId: string, modelId: string) => `${endpointId}\n${modelId}`
29
+
30
+ /** Splits a trailing level (`opus high`); `off` is accepted for `none`. */
31
+ export function parseQuery(query: string): { text: string; effort: ReasoningEffort | null } {
32
+ const trimmed = query.trim()
33
+ const match = /^(.*\S)\s+(\S+)$/.exec(trimmed)
34
+ if (match?.[1] && match[2]) {
35
+ const word = match[2].toLowerCase()
36
+ const effort = word === 'off' ? 'none' : REASONING_EFFORTS.find((item) => item === word)
37
+ if (effort) return { text: match[1].trim(), effort }
38
+ }
39
+ return { text: trimmed, effort: null }
40
+ }
41
+
42
+ /** Lower is better; `null` hides the model (no match, or the typed level is not declared). */
43
+ export function score(model: PickerModel, query: string): number | null {
44
+ const { text, effort } = parseQuery(query)
45
+ if (effort && !model.efforts.includes(effort)) return null
46
+ const haystack = `${model.name} ${model.provider} ${model.detail}`.toLowerCase()
47
+ const needle = text.toLowerCase()
48
+ const index = haystack.indexOf(needle)
49
+ if (index >= 0) return index
50
+ const parts = needle.split(/\s+/).filter(Boolean)
51
+ if (parts.every((part) => haystack.includes(part))) return 500
52
+ let rank = 1000
53
+ let from = 0
54
+ for (const char of needle.replace(/\s+/g, '')) {
55
+ const found = haystack.indexOf(char, from)
56
+ if (found < 0) return null
57
+ rank += found
58
+ from = found + 1
59
+ }
60
+ return rank
61
+ }
62
+
63
+ /** Typed level first, then the current explicit level if declared, else `default`. */
64
+ export function nextValue(
65
+ model: PickerModel,
66
+ current: PickerValue | null,
67
+ query: string,
68
+ ): PickerValue {
69
+ const typed = parseQuery(query).effort
70
+ if (typed && model.efforts.includes(typed)) return { key: model.key, effort: typed }
71
+ const kept = current?.effort
72
+ const effort = kept && kept !== 'default' && model.efforts.includes(kept) ? kept : 'default'
73
+ return { key: model.key, effort }
74
+ }
75
+
76
+ export interface PickerGroup {
77
+ id: string
78
+ provider: string
79
+ models: PickerModel[]
80
+ }
81
+
82
+ /** Groups in first-seen order; models inside a group are ranked when searching. */
83
+ export function groupModels(models: readonly PickerModel[], query: string): PickerGroup[] {
84
+ const searching = query.trim() !== ''
85
+ const groups: PickerGroup[] = []
86
+ for (const model of models) {
87
+ if (searching && score(model, query) === null) continue
88
+ const group = groups.find((item) => item.id === model.providerId)
89
+ if (group) group.models.push(model)
90
+ else groups.push({ id: model.providerId, provider: model.provider, models: [model] })
91
+ }
92
+ if (searching)
93
+ for (const group of groups)
94
+ group.models.sort((a, b) => (score(a, query) ?? Infinity) - (score(b, query) ?? Infinity))
95
+ return groups
96
+ }
97
+
98
+ /** Header facts: key presence, the first model's route and the model count. */
99
+ export function groupSummary(group: PickerGroup, i18n: KitI18n): string {
100
+ const route = group.models[0]?.detail.split(' · ')[0]?.trim() ?? ''
101
+ const keyed = group.models.some((model) => model.hasKey)
102
+ const head = [keyed ? i18n.t('chat.picker.keyed') : '', route].filter(Boolean).join(' · ')
103
+ return `${head} ${group.models.length}`.trim()
104
+ }
105
+
106
+ /** Filled bars (0–4) of the trigger; `none` is not a reasoning rank. */
107
+ export function effortBars(model: PickerModel, effort: ReasoningEffort): number {
108
+ if (effort === 'default' || effort === 'none') return 0
109
+ const ranked = model.efforts.filter((item) => item !== 'none')
110
+ const rank = ranked.indexOf(effort)
111
+ if (rank < 0) return 0
112
+ return Math.round(((rank + 1) / ranked.length) * 4)
113
+ }
114
+
115
+ /** Color family of a level: fast (info), deep (warning) or regular (primary). */
116
+ export function effortTone(effort: ReasoningEffort): 'fast' | 'deep' | 'regular' {
117
+ if (effort === 'none' || effort === 'minimal' || effort === 'low') return 'fast'
118
+ if (effort === 'xhigh' || effort === 'max') return 'deep'
119
+ return 'regular'
120
+ }
121
+
122
+ function contextSize(tokens: number): string {
123
+ if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`
124
+ if (tokens >= 1000) return `${Math.floor(tokens / 1000)}K`
125
+ return String(tokens)
126
+ }
127
+
128
+ /**
129
+ * Joins options with provider metadata for the second row. Without `providers` the picker still
130
+ * works; route names fall back to the option label, prices and windows are left out.
131
+ */
132
+ export function pickerModels(
133
+ options: readonly ModelOption[],
134
+ providers: readonly Provider[] | null,
135
+ i18n: KitI18n,
136
+ ): PickerModel[] {
137
+ return options.map((option) => {
138
+ const segments = option.label.split(' · ')
139
+ let provider: Provider | undefined
140
+ let endpoint: Provider['endpoints'][number] | undefined
141
+ for (const item of providers ?? []) {
142
+ endpoint = item.endpoints.find((candidate) => candidate.id === option.endpointId)
143
+ if (endpoint) {
144
+ provider = item
145
+ break
146
+ }
147
+ }
148
+ const model = endpoint?.models.find((candidate) => candidate.id === option.modelId)
149
+ const name = option.name?.trim() || option.modelId
150
+ const route = endpoint?.slug ?? segments[1] ?? ''
151
+ const parts = [route]
152
+ if (name !== option.modelId) parts.push(option.modelId)
153
+ const tier = model?.pricing?.tiers[0]
154
+ if (model?.pricing && tier)
155
+ parts.push(
156
+ i18n.t('chat.picker.price', {
157
+ currency: model.pricing.currency,
158
+ input: tier.input,
159
+ output: tier.output,
160
+ }),
161
+ )
162
+ const contextWindow = model?.contextWindow ?? null
163
+ if (contextWindow)
164
+ parts.push(i18n.t('chat.picker.context', { size: contextSize(contextWindow) }))
165
+ const declared = option.reasoning?.efforts ?? []
166
+ return {
167
+ key: modelKey(option.endpointId, option.modelId),
168
+ providerId: provider?.id ?? segments[0] ?? option.endpointId,
169
+ provider: provider?.name ?? segments[0] ?? option.endpointId,
170
+ name,
171
+ detail: parts.filter(Boolean).join(' · '),
172
+ hasKey: endpoint?.keys.some((key) => key.enabled && key.readable) ?? false,
173
+ efforts: REASONING_EFFORTS.filter((effort) => declared.includes(effort)),
174
+ contextWindow,
175
+ }
176
+ })
177
+ }
@@ -0,0 +1,56 @@
1
+ import type { Cost, TokenUsage, Turn } from '../ports'
2
+ import type { ChatTurn } from './run-session-types'
3
+
4
+ export function fromHistory(turn: Turn): ChatTurn {
5
+ return {
6
+ id: turn.id,
7
+ persisted: true,
8
+ parentId: turn.parentId,
9
+ questionId: turn.questionId,
10
+ supersedesId: turn.supersedesId,
11
+ kind: turn.kind,
12
+ endpointId: turn.endpointId,
13
+ modelId: turn.modelId,
14
+ input: turn.input,
15
+ images: turn.images,
16
+ generated: turn.generated,
17
+ text: turn.text,
18
+ reasoningText: turn.reasoningText ?? '',
19
+ reasoningEffort: turn.reasoningEffort ?? 'default',
20
+ usage: turn.usage ?? null,
21
+ cost: turn.cost ?? null,
22
+ contextUsage: turn.contextUsage ?? null,
23
+ status: turn.status,
24
+ error: turn.errorCode,
25
+ tools: [],
26
+ exhausted: false,
27
+ search: { status: 'idle', query: null, provider: null, hits: [], error: null },
28
+ }
29
+ }
30
+
31
+ export function cumulativeUsage(turns: readonly ChatTurn[]): TokenUsage {
32
+ const total: TokenUsage = {
33
+ inputTokens: null,
34
+ outputTokens: null,
35
+ cacheReadTokens: null,
36
+ cacheWriteTokens: null,
37
+ reasoningTokens: null,
38
+ }
39
+ for (const turn of turns)
40
+ for (const key of Object.keys(total) as (keyof TokenUsage)[]) {
41
+ const value = turn.usage?.[key]
42
+ if (value != null) total[key] = (total[key] ?? 0) + value
43
+ }
44
+ return total
45
+ }
46
+
47
+ export function cumulativeCost(turns: readonly ChatTurn[]): Cost[] {
48
+ const totals: Cost[] = []
49
+ for (const { cost } of turns) {
50
+ if (!cost) continue
51
+ const found = totals.find((item) => item.currency === cost.currency)
52
+ if (found) found.amount += cost.amount
53
+ else totals.push({ ...cost })
54
+ }
55
+ return totals
56
+ }