@adecore/agents-react 0.21.2 → 0.22.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/dist/agents/model-requirement.d.ts +6 -0
- package/dist/agents/model-requirement.js +26 -0
- package/dist/chat/chat-client.d.ts +2 -1
- package/dist/chat/chat-client.js +2 -0
- package/dist/chat/drafts.d.ts +2 -1
- package/dist/chat/drafts.js +15 -5
- package/dist/chat/logic/picker-entries.d.ts +10 -0
- package/dist/chat/logic/picker-entries.js +19 -0
- package/dist/chat/logic/timeline.d.ts +5 -0
- package/dist/chat/logic/timeline.js +10 -1
- package/dist/chat/prompt-hold.d.ts +8 -0
- package/dist/chat/prompt-hold.js +16 -0
- package/dist/chat/quote.d.ts +4 -0
- package/dist/chat/quote.js +19 -0
- package/dist/chat/stash.d.ts +8 -2
- package/dist/chat/stash.js +21 -5
- package/dist/chat/subagent-list.d.ts +7 -1
- package/dist/chat/subagent-list.js +13 -1
- package/dist/chat/timeline-scroll.d.ts +6 -2
- package/dist/chat/timeline-scroll.js +12 -7
- package/dist/chat/ui/ChatActivity.js +22 -8
- package/dist/chat/ui/Composer.js +103 -19
- package/dist/chat/ui/ComposerInput.d.ts +3 -1
- package/dist/chat/ui/ComposerInput.js +13 -4
- package/dist/chat/ui/Pickers.js +26 -18
- package/dist/chat/ui/PromptComposer.d.ts +8 -1
- package/dist/chat/ui/PromptComposer.js +18 -6
- package/dist/chat/ui/QuoteButton.js +2 -2
- package/dist/chat/ui/SubagentControls.d.ts +4 -2
- package/dist/chat/ui/SubagentControls.js +12 -8
- package/dist/chat/ui/SubagentInfo.d.ts +1 -1
- package/dist/chat/ui/SubagentInfo.js +9 -15
- package/dist/chat/ui/SubagentStopButton.js +2 -1
- package/dist/chat/ui/Timeline.js +24 -3
- package/dist/chat/ui/composer/menu-aria.d.ts +10 -0
- package/dist/chat/ui/composer/menu-aria.js +18 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +3 -3
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +1 -1
- package/dist/chat/ui/intelligent-ui/chart-data.d.ts +22 -2
- package/dist/chat/ui/intelligent-ui/chart-data.js +83 -19
- package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/chart.js +99 -35
- package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +8 -2
- package/dist/chat/ui/intelligent-ui/renderers/content.js +5 -3
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +9 -4
- package/dist/chat/ui/intelligent-ui/renderers/links.js +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/text.js +5 -2
- package/dist/chat/ui/intelligent-ui/table-data.js +4 -4
- package/dist/chat/ui/intelligent-ui/use-truncated.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/use-truncated.js +28 -0
- package/dist/chat/ui/quote-selection.d.ts +5 -2
- package/dist/chat/ui/quote-selection.js +3 -2
- package/dist/chat/ui/rows/MessageRows.js +24 -5
- package/dist/chat/ui/rows/Rows.js +1 -1
- package/dist/chat/ui/rows/WorkRows.d.ts +3 -1
- package/dist/chat/ui/rows/WorkRows.js +4 -4
- package/dist/host.d.ts +1 -0
- package/dist/host.js +1 -0
- package/dist/locales/en/agent-chat.json +39 -10
- package/dist/locales/nl/agent-chat.json +39 -10
- package/dist/state/chats.d.ts +2 -1
- package/dist/state/chats.js +19 -1
- package/package.json +29 -4
- package/src/agents/model-requirement.ts +36 -0
- package/src/chat/chat-client.ts +5 -0
- package/src/chat/drafts.ts +27 -7
- package/src/chat/logic/picker-entries.ts +25 -0
- package/src/chat/logic/timeline.ts +17 -2
- package/src/chat/prompt-hold.ts +27 -0
- package/src/chat/quote.ts +20 -0
- package/src/chat/stash.ts +31 -5
- package/src/chat/subagent-list.ts +19 -5
- package/src/chat/timeline-scroll.ts +24 -10
- package/src/chat/ui/ChatActivity.tsx +35 -9
- package/src/chat/ui/Composer.tsx +197 -52
- package/src/chat/ui/ComposerInput.tsx +18 -3
- package/src/chat/ui/Pickers.tsx +51 -30
- package/src/chat/ui/PromptComposer.tsx +37 -8
- package/src/chat/ui/QuoteButton.tsx +3 -2
- package/src/chat/ui/SubagentControls.tsx +22 -19
- package/src/chat/ui/SubagentInfo.tsx +24 -42
- package/src/chat/ui/SubagentStopButton.tsx +2 -1
- package/src/chat/ui/Timeline.tsx +26 -4
- package/src/chat/ui/composer/menu-aria.ts +29 -0
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +5 -5
- package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +6 -1
- package/src/chat/ui/intelligent-ui/chart-data.ts +106 -20
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +237 -72
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +9 -4
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +31 -15
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +16 -5
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +3 -3
- package/src/chat/ui/intelligent-ui/renderers/text.tsx +18 -8
- package/src/chat/ui/intelligent-ui/table-data.ts +4 -4
- package/src/chat/ui/intelligent-ui/use-truncated.ts +33 -0
- package/src/chat/ui/quote-selection.ts +6 -4
- package/src/chat/ui/rows/MessageRows.tsx +44 -5
- package/src/chat/ui/rows/Rows.tsx +3 -1
- package/src/chat/ui/rows/WorkRows.tsx +15 -1
- package/src/host.ts +3 -0
- package/src/locales/en/agent-chat.json +39 -10
- package/src/locales/nl/agent-chat.json +39 -10
- package/src/state/chats.ts +31 -2
|
@@ -91,7 +91,8 @@
|
|
|
91
91
|
"user": {
|
|
92
92
|
"heading": "Jij",
|
|
93
93
|
"showAll": "Alles tonen",
|
|
94
|
-
"showLess": "Minder tonen"
|
|
94
|
+
"showLess": "Minder tonen",
|
|
95
|
+
"quoteGone": "Het antwoord waar dit citaat vandaan kwam, staat niet meer in deze chat."
|
|
95
96
|
},
|
|
96
97
|
"report": {
|
|
97
98
|
"heading": "Rapport"
|
|
@@ -107,7 +108,8 @@
|
|
|
107
108
|
"note": {
|
|
108
109
|
"hide": "Verbergen",
|
|
109
110
|
"show": "Tonen",
|
|
110
|
-
"openFork": "Fork openen"
|
|
111
|
+
"openFork": "Fork openen",
|
|
112
|
+
"openWaiting": "Openen om te antwoorden"
|
|
111
113
|
},
|
|
112
114
|
"compaction": {
|
|
113
115
|
"plain": "Context gecomprimeerd",
|
|
@@ -165,6 +167,9 @@
|
|
|
165
167
|
},
|
|
166
168
|
"changedFiles_one": "{{count}} gewijzigd bestand",
|
|
167
169
|
"changedFiles_other": "{{count}} gewijzigde bestanden",
|
|
170
|
+
"folderChanges_one": "{{count}} wijziging in de map tijdens deze beurt",
|
|
171
|
+
"folderChanges_other": "{{count}} wijzigingen in de map tijdens deze beurt",
|
|
172
|
+
"baseMoved": "De branch is tijdens deze beurt verplaatst ({{from}} naar {{to}}), dus dit omvat ook wat daarmee binnenkwam.",
|
|
168
173
|
"andMore": "en meer, afgekapt",
|
|
169
174
|
"edits_one": "{{count}} bewerking",
|
|
170
175
|
"edits_other": "{{count}} bewerkingen",
|
|
@@ -249,6 +254,17 @@
|
|
|
249
254
|
},
|
|
250
255
|
"composer": {
|
|
251
256
|
"slotFailed": "Dit onderdeel kon niet worden getekend",
|
|
257
|
+
"inputLabel": "Bericht",
|
|
258
|
+
"menu": {
|
|
259
|
+
"commands": "Commando's",
|
|
260
|
+
"skills": "Skills",
|
|
261
|
+
"mentions": "Vermeldingen"
|
|
262
|
+
},
|
|
263
|
+
"held": {
|
|
264
|
+
"question": "Er wacht een vraag. Die opent zodra je even stopt met typen.",
|
|
265
|
+
"approval": "Er wacht een goedkeuring. Die opent zodra je even stopt met typen.",
|
|
266
|
+
"show": "Nu tonen"
|
|
267
|
+
},
|
|
252
268
|
"commands": {
|
|
253
269
|
"model": "Wissel van model",
|
|
254
270
|
"compact": "Comprimeer de context",
|
|
@@ -294,7 +310,8 @@
|
|
|
294
310
|
},
|
|
295
311
|
"quote": {
|
|
296
312
|
"add": "Citeren",
|
|
297
|
-
"remove": "Citaat verwijderen"
|
|
313
|
+
"remove": "Citaat verwijderen",
|
|
314
|
+
"goToSource": "Naar het antwoord"
|
|
298
315
|
},
|
|
299
316
|
"removeAttachment": "{{name}} verwijderen",
|
|
300
317
|
"attach": "Bestanden toevoegen",
|
|
@@ -319,6 +336,9 @@
|
|
|
319
336
|
"monitors_one": "{{count}} monitor",
|
|
320
337
|
"monitors_other": "{{count}} monitors",
|
|
321
338
|
"pausedUntil": "gepauzeerd tot {{time}}",
|
|
339
|
+
"progress": "{{done}} van {{total}} sub-agents klaar",
|
|
340
|
+
"needsYou_one": "{{count}} vraagt aandacht",
|
|
341
|
+
"needsYou_other": "{{count}} vragen aandacht",
|
|
322
342
|
"took": {
|
|
323
343
|
"done": "klaar in {{duration}}",
|
|
324
344
|
"failed": "mislukt na {{duration}}",
|
|
@@ -332,8 +352,10 @@
|
|
|
332
352
|
"subagents": {
|
|
333
353
|
"stop": {
|
|
334
354
|
"task": "Deze taak stoppen",
|
|
355
|
+
"native": "Deze sub-agent stoppen",
|
|
335
356
|
"mark": "Markeren als gestopt. De CLI kan een losse sub-agent niet stoppen, dus die werkt mogelijk door tot het proces van de chat eindigt."
|
|
336
357
|
},
|
|
358
|
+
"stale": "Deze sub-agent hoort bij een proces dat al gestopt is.",
|
|
337
359
|
"title": "Sub-agents",
|
|
338
360
|
"backToMain": "Terug naar de main agent",
|
|
339
361
|
"loading": "Gesprek laden…",
|
|
@@ -344,9 +366,7 @@
|
|
|
344
366
|
"stopFailed": "De sub-agent kon niet worden gestopt",
|
|
345
367
|
"info": {
|
|
346
368
|
"label": "Over deze sub-agent",
|
|
347
|
-
"tokens": "{{tokens}} tokens"
|
|
348
|
-
"showTask": "Opdracht tonen",
|
|
349
|
-
"hideTask": "Opdracht verbergen"
|
|
369
|
+
"tokens": "{{tokens}} tokens"
|
|
350
370
|
}
|
|
351
371
|
},
|
|
352
372
|
"modes": {
|
|
@@ -382,7 +402,10 @@
|
|
|
382
402
|
"noMatch": "Geen modellen gevonden",
|
|
383
403
|
"legacyGroup_one": "Legacy-modellen ({{count}})",
|
|
384
404
|
"legacyGroup_other": "Legacy-modellen ({{count}})",
|
|
385
|
-
"legacy": "Legacy"
|
|
405
|
+
"legacy": "Legacy",
|
|
406
|
+
"requires": "Vereist {{cli}} {{version}} of nieuwer op {{machine}}",
|
|
407
|
+
"requiresHere": "Vereist {{cli}} {{version}} of nieuwer",
|
|
408
|
+
"providerSettings": "Providerinstellingen"
|
|
386
409
|
},
|
|
387
410
|
"stash": {
|
|
388
411
|
"title": "Stashed prompts",
|
|
@@ -504,7 +527,13 @@
|
|
|
504
527
|
"chart": {
|
|
505
528
|
"values": "Waarden",
|
|
506
529
|
"summary": "{{count}} waarden, hoogste {{label}} met {{value}}",
|
|
507
|
-
"summarySeries": "{{count}} categorieën, hoogste {{series}} bij {{label}} met {{value}}"
|
|
530
|
+
"summarySeries": "{{count}} categorieën, hoogste {{series}} bij {{label}} met {{value}}",
|
|
531
|
+
"rowsLeftOut": "De eerste {{shown}} van {{total}} rijen zijn getekend.",
|
|
532
|
+
"seriesLeftOut_one": "{{series}} is niet getekend.",
|
|
533
|
+
"seriesLeftOut_other": "{{series}} zijn niet getekend.",
|
|
534
|
+
"sideBySide": "Negatieve waarden kunnen niet gestapeld worden, dus de reeksen staan naast elkaar.",
|
|
535
|
+
"showValues": "Alle waarden tonen",
|
|
536
|
+
"hideValues": "Waarden verbergen"
|
|
508
537
|
},
|
|
509
538
|
"image": {
|
|
510
539
|
"label": "Afbeelding",
|
|
@@ -528,8 +557,8 @@
|
|
|
528
557
|
"answered": "Beantwoord met “{{label}}”",
|
|
529
558
|
"chose": "Gekozen: “{{label}}”",
|
|
530
559
|
"choseOlder": "Gekozen: “{{label}}”, bij het antwoord van {{time}}",
|
|
531
|
-
"
|
|
532
|
-
"
|
|
560
|
+
"notes_one": "{{count}} opmerking",
|
|
561
|
+
"notes_other": "{{count}} opmerkingen",
|
|
533
562
|
"shownAsText": "Als tekst getoond",
|
|
534
563
|
"unknownComponent": "Deze versie kan {{component}} niet tekenen",
|
|
535
564
|
"renderFailed": "Dit deel kon niet getekend worden",
|
package/dist/state/chats.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatApprovalItem, ChatBookmark, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem, ChatVisual } from '@adecore/agent-contracts';
|
|
1
|
+
import type { AgentKind, ChatApprovalItem, ChatBookmark, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem, ChatVisual } from '@adecore/agent-contracts';
|
|
2
2
|
export interface ChatState {
|
|
3
3
|
info: ChatInfo;
|
|
4
4
|
items: Record<string, ChatItem>;
|
|
@@ -43,5 +43,6 @@ export declare function applyEvent(state: ChatState, event: ChatEvent): ChatStat
|
|
|
43
43
|
export declare const useChats: import("zustand").UseBoundStore<import("zustand").StoreApi<ChatsStore>>;
|
|
44
44
|
export declare function chatSink(keyOf: (chatId: string) => string): ChatSink;
|
|
45
45
|
export declare function useChatRow<T>(chatId: string, select: (row: ChatState | undefined) => T): T;
|
|
46
|
+
export declare function useReportedModels(provider: AgentKind): ReadonlySet<string>;
|
|
46
47
|
export declare function useCurrentItem<T extends ChatItem>(chatId: string, derived: T): T;
|
|
47
48
|
export {};
|
package/dist/state/chats.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { useContext, useMemo } from 'react';
|
|
1
2
|
import { create } from 'zustand';
|
|
2
|
-
import { useChatScope } from '../scope.js';
|
|
3
|
+
import { ChatScopeContext, useChatScope } from '../scope.js';
|
|
3
4
|
function isWaiting(item) {
|
|
4
5
|
return (item?.kind === 'approval' && item.decision === 'pending') || (item?.kind === 'question' && item.state === 'pending');
|
|
5
6
|
}
|
|
@@ -209,6 +210,23 @@ export function useChatRow(chatId, select) {
|
|
|
209
210
|
const { keyOf } = useChatScope();
|
|
210
211
|
return useChats((s) => select(s.byKey[keyOf(chatId)]));
|
|
211
212
|
}
|
|
213
|
+
/*
|
|
214
|
+
* The models the CLI of this provider reported running in the chats this client holds of the host.
|
|
215
|
+
* A model it ran is proof the CLI offers it, whatever the version says. Outside a scope there is none.
|
|
216
|
+
*/
|
|
217
|
+
export function useReportedModels(provider) {
|
|
218
|
+
const scope = useContext(ChatScopeContext);
|
|
219
|
+
const joined = useChats((s) => {
|
|
220
|
+
const names = new Set();
|
|
221
|
+
for (const [key, state] of Object.entries(s.statusByKey)) {
|
|
222
|
+
if (scope?.owns(key) === true && state.info.provider === provider && state.info.model !== null) {
|
|
223
|
+
names.add(state.info.model);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return [...names].sort().join('\n');
|
|
227
|
+
});
|
|
228
|
+
return useMemo(() => new Set(joined === '' ? [] : joined.split('\n')), [joined]);
|
|
229
|
+
}
|
|
212
230
|
/* The item as the thread holds it now, for a row derived from the structure, which a delta leaves alone. */
|
|
213
231
|
export function useCurrentItem(chatId, derived) {
|
|
214
232
|
return useChatRow(chatId, (row) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"types": "./dist/agents/accounts.d.ts",
|
|
35
35
|
"default": "./dist/agents/accounts.js"
|
|
36
36
|
},
|
|
37
|
+
"./agents/model-requirement": {
|
|
38
|
+
"source": "./src/agents/model-requirement.ts",
|
|
39
|
+
"types": "./dist/agents/model-requirement.d.ts",
|
|
40
|
+
"default": "./dist/agents/model-requirement.js"
|
|
41
|
+
},
|
|
37
42
|
"./agents/model-name": {
|
|
38
43
|
"source": "./src/agents/model-name.ts",
|
|
39
44
|
"types": "./dist/agents/model-name.d.ts",
|
|
@@ -104,6 +109,11 @@
|
|
|
104
109
|
"types": "./dist/chat/guards.d.ts",
|
|
105
110
|
"default": "./dist/chat/guards.js"
|
|
106
111
|
},
|
|
112
|
+
"./chat/logic/picker-entries": {
|
|
113
|
+
"source": "./src/chat/logic/picker-entries.ts",
|
|
114
|
+
"types": "./dist/chat/logic/picker-entries.d.ts",
|
|
115
|
+
"default": "./dist/chat/logic/picker-entries.js"
|
|
116
|
+
},
|
|
107
117
|
"./chat/logic/ansi": {
|
|
108
118
|
"source": "./src/chat/logic/ansi.ts",
|
|
109
119
|
"types": "./dist/chat/logic/ansi.d.ts",
|
|
@@ -219,6 +229,11 @@
|
|
|
219
229
|
"types": "./dist/chat/preferences.d.ts",
|
|
220
230
|
"default": "./dist/chat/preferences.js"
|
|
221
231
|
},
|
|
232
|
+
"./chat/prompt-hold": {
|
|
233
|
+
"source": "./src/chat/prompt-hold.ts",
|
|
234
|
+
"types": "./dist/chat/prompt-hold.d.ts",
|
|
235
|
+
"default": "./dist/chat/prompt-hold.js"
|
|
236
|
+
},
|
|
222
237
|
"./chat/quote": {
|
|
223
238
|
"source": "./src/chat/quote.ts",
|
|
224
239
|
"types": "./dist/chat/quote.d.ts",
|
|
@@ -494,6 +509,11 @@
|
|
|
494
509
|
"types": "./dist/chat/ui/composer/keys.d.ts",
|
|
495
510
|
"default": "./dist/chat/ui/composer/keys.js"
|
|
496
511
|
},
|
|
512
|
+
"./chat/ui/composer/menu-aria": {
|
|
513
|
+
"source": "./src/chat/ui/composer/menu-aria.ts",
|
|
514
|
+
"types": "./dist/chat/ui/composer/menu-aria.d.ts",
|
|
515
|
+
"default": "./dist/chat/ui/composer/menu-aria.js"
|
|
516
|
+
},
|
|
497
517
|
"./chat/ui/diff-theme": {
|
|
498
518
|
"source": "./src/chat/ui/diff-theme.ts",
|
|
499
519
|
"types": "./dist/chat/ui/diff-theme.d.ts",
|
|
@@ -619,6 +639,11 @@
|
|
|
619
639
|
"types": "./dist/chat/ui/intelligent-ui/use-later.d.ts",
|
|
620
640
|
"default": "./dist/chat/ui/intelligent-ui/use-later.js"
|
|
621
641
|
},
|
|
642
|
+
"./chat/ui/intelligent-ui/use-truncated": {
|
|
643
|
+
"source": "./src/chat/ui/intelligent-ui/use-truncated.ts",
|
|
644
|
+
"types": "./dist/chat/ui/intelligent-ui/use-truncated.d.ts",
|
|
645
|
+
"default": "./dist/chat/ui/intelligent-ui/use-truncated.js"
|
|
646
|
+
},
|
|
622
647
|
"./chat/ui/intelligent-ui/use-ui-links": {
|
|
623
648
|
"source": "./src/chat/ui/intelligent-ui/use-ui-links.ts",
|
|
624
649
|
"types": "./dist/chat/ui/intelligent-ui/use-ui-links.d.ts",
|
|
@@ -1011,8 +1036,8 @@
|
|
|
1011
1036
|
"test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
|
|
1012
1037
|
},
|
|
1013
1038
|
"dependencies": {
|
|
1014
|
-
"@adecore/agent-contracts": "0.
|
|
1015
|
-
"@adecore/intelligent-ui": "0.
|
|
1039
|
+
"@adecore/agent-contracts": "0.22.1",
|
|
1040
|
+
"@adecore/intelligent-ui": "0.22.1",
|
|
1016
1041
|
"@codemirror/commands": "^6.11.1",
|
|
1017
1042
|
"@codemirror/language": "^6.12.4",
|
|
1018
1043
|
"@codemirror/state": "^6.7.6",
|
|
@@ -1033,7 +1058,7 @@
|
|
|
1033
1058
|
"zustand": "^5.0.15"
|
|
1034
1059
|
},
|
|
1035
1060
|
"peerDependencies": {
|
|
1036
|
-
"@adecore/ui": "^0.
|
|
1061
|
+
"@adecore/ui": "^0.22.1",
|
|
1037
1062
|
"i18next": "^26.4.2",
|
|
1038
1063
|
"react": "^19.3.0",
|
|
1039
1064
|
"react-dom": "^19.3.0",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ModelInfo, ProviderInfo } from '@adecore/agent-contracts';
|
|
2
|
+
|
|
3
|
+
export interface ModelRequirement {
|
|
4
|
+
/* The CLI's name, as the provider list gives it. */
|
|
5
|
+
cli: string;
|
|
6
|
+
minVersion: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function partsOf(version: string, strict: boolean): number[] | null {
|
|
10
|
+
const found = (strict ? /^(\d+)\.(\d+)(?:\.(\d+))?$/ : /(\d+)\.(\d+)\.(\d+)/).exec(version);
|
|
11
|
+
return found === null ? null : [Number(found[1]), Number(found[2]), Number(found[3] ?? 0)];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Whether the model's reported name is this model: its slug, the slug with a suffix such as `[1m]`. */
|
|
15
|
+
function isReported(slug: string, reported: ReadonlySet<string>): boolean {
|
|
16
|
+
return [...reported].some((name) => name === slug || name.startsWith(`${slug}[`));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* What a model asks of the CLI that is too old for it, or null when nothing blocks it: no minimum is
|
|
21
|
+
* known, the CLI's version is not a readable x.y.z (so an unknown version invents no requirement), the
|
|
22
|
+
* CLI is new enough, or a chat of this provider already reported running the model.
|
|
23
|
+
*/
|
|
24
|
+
export function modelRequirementOf(model: ModelInfo, provider: ProviderInfo, reported: ReadonlySet<string>): ModelRequirement | null {
|
|
25
|
+
const minimum = model.minCliVersion === undefined ? null : partsOf(model.minCliVersion, true);
|
|
26
|
+
const installed = provider.version === null ? null : partsOf(provider.version, false);
|
|
27
|
+
if (minimum === null || installed === null || isReported(model.slug, reported)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
for (let i = 0; i < 3; i++) {
|
|
31
|
+
if (installed[i] !== minimum[i]) {
|
|
32
|
+
return (installed[i] ?? 0) > (minimum[i] ?? 0) ? null : { cli: provider.name, minVersion: model.minCliVersion ?? '' };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
package/src/chat/chat-client.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ChatAttachResult,
|
|
5
5
|
ChatBookmark,
|
|
6
6
|
ChatPreferencesPayload,
|
|
7
|
+
ChatQuoteSource,
|
|
7
8
|
ChatSkill,
|
|
8
9
|
ChatVisual,
|
|
9
10
|
ModelSelection,
|
|
@@ -35,6 +36,8 @@ export interface ChatSendExtras {
|
|
|
35
36
|
skills?: string[];
|
|
36
37
|
/* Chats of the project picked with `@`; only their ids travel, the agent reads them itself. */
|
|
37
38
|
chats?: string[];
|
|
39
|
+
/* The reply a quote at the head of the text was taken from. */
|
|
40
|
+
quoteSource?: ChatQuoteSource;
|
|
38
41
|
attachments?: ChatAttachmentUpload[];
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -170,9 +173,11 @@ export class ChatClient {
|
|
|
170
173
|
return this.transport.request('chat.send', {
|
|
171
174
|
chatId,
|
|
172
175
|
text,
|
|
176
|
+
delivery: extras.delivery,
|
|
173
177
|
mentions: extras.mentions,
|
|
174
178
|
skills: extras.skills,
|
|
175
179
|
chats: extras.chats,
|
|
180
|
+
quoteSource: extras.quoteSource,
|
|
176
181
|
attachments: extras.attachments
|
|
177
182
|
});
|
|
178
183
|
}
|
package/src/chat/drafts.ts
CHANGED
|
@@ -14,12 +14,22 @@ export interface ChatDraft {
|
|
|
14
14
|
attachments: ChatAttachmentUpload[];
|
|
15
15
|
/* A piece of an answer that goes above the text as a blockquote; empty for none. */
|
|
16
16
|
quote: string;
|
|
17
|
+
/* The reply the quote was taken from; null when the quote names none. */
|
|
18
|
+
quoteSource: string | null;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
export const EMPTY_DRAFT: ChatDraft = { text: '', mentions: [], skills: [], chats: [], attachments: [], quote: '' };
|
|
21
|
+
export const EMPTY_DRAFT: ChatDraft = { text: '', mentions: [], skills: [], chats: [], attachments: [], quote: '', quoteSource: null };
|
|
20
22
|
|
|
21
23
|
// Older records only had text; the rest is filled in on read.
|
|
22
|
-
type DraftRecord = {
|
|
24
|
+
type DraftRecord = {
|
|
25
|
+
text: string;
|
|
26
|
+
mentions?: string[];
|
|
27
|
+
skills?: string[];
|
|
28
|
+
chats?: string[];
|
|
29
|
+
attachments?: ChatAttachmentUpload[];
|
|
30
|
+
quote?: string;
|
|
31
|
+
quoteSource?: string;
|
|
32
|
+
};
|
|
23
33
|
|
|
24
34
|
const storage = persistedJson<Record<string, DraftRecord>>(
|
|
25
35
|
storageKey,
|
|
@@ -63,7 +73,8 @@ export function readDraft(chatId: string): ChatDraft {
|
|
|
63
73
|
skills: record.skills ?? [],
|
|
64
74
|
chats: record.chats ?? [],
|
|
65
75
|
attachments: record.attachments ?? [],
|
|
66
|
-
quote: record.quote ?? ''
|
|
76
|
+
quote: record.quote ?? '',
|
|
77
|
+
quoteSource: record.quoteSource ?? null
|
|
67
78
|
}
|
|
68
79
|
: EMPTY_DRAFT;
|
|
69
80
|
}
|
|
@@ -82,13 +93,21 @@ export function writeDraft(chatId: string, draft: ChatDraft): void {
|
|
|
82
93
|
skills: draft.skills,
|
|
83
94
|
chats: draft.chats,
|
|
84
95
|
attachments: draft.attachments,
|
|
85
|
-
quote: draft.quote
|
|
96
|
+
quote: draft.quote,
|
|
97
|
+
...(draft.quoteSource === null ? {} : { quoteSource: draft.quoteSource })
|
|
86
98
|
};
|
|
87
99
|
if (storage.write(drafts)) {
|
|
88
100
|
return;
|
|
89
101
|
}
|
|
90
102
|
// A file can outgrow the storage quota; the text is the part worth keeping then.
|
|
91
|
-
drafts[chatId] = {
|
|
103
|
+
drafts[chatId] = {
|
|
104
|
+
text: draft.text,
|
|
105
|
+
mentions: draft.mentions,
|
|
106
|
+
skills: draft.skills,
|
|
107
|
+
chats: draft.chats,
|
|
108
|
+
quote: draft.quote,
|
|
109
|
+
...(draft.quoteSource === null ? {} : { quoteSource: draft.quoteSource })
|
|
110
|
+
};
|
|
92
111
|
storage.write(drafts);
|
|
93
112
|
}
|
|
94
113
|
|
|
@@ -108,7 +127,7 @@ function union(first: readonly string[], second: readonly string[]): string[] {
|
|
|
108
127
|
*/
|
|
109
128
|
export function takeBackIntoDraft(
|
|
110
129
|
current: ChatDraft,
|
|
111
|
-
taken: Omit<ChatDraft, 'quote'>
|
|
130
|
+
taken: Omit<ChatDraft, 'quote' | 'quoteSource'>
|
|
112
131
|
): { draft: ChatDraft; rejected: Array<{ name: string; reason: string }> } {
|
|
113
132
|
const checked = checkAttachmentLimits(
|
|
114
133
|
current.attachments.length,
|
|
@@ -122,7 +141,8 @@ export function takeBackIntoDraft(
|
|
|
122
141
|
skills: union(taken.skills, current.skills),
|
|
123
142
|
chats: union(taken.chats, current.chats),
|
|
124
143
|
attachments: [...checked.accepted.map((entry) => entry.upload), ...current.attachments],
|
|
125
|
-
quote: current.quote
|
|
144
|
+
quote: current.quote,
|
|
145
|
+
quoteSource: current.quoteSource
|
|
126
146
|
},
|
|
127
147
|
rejected: checked.rejected
|
|
128
148
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ModelInfo, ProviderInfo } from '@adecore/agent-contracts';
|
|
2
|
+
|
|
3
|
+
/* A model row, or the one row that folds the legacy models open. Both take a turn in the arrow keys. */
|
|
4
|
+
export type PickerEntry = { kind: 'model'; provider: ProviderInfo; model: ModelInfo } | { kind: 'legacy'; count: number };
|
|
5
|
+
|
|
6
|
+
function matches(model: ModelInfo, query: string): boolean {
|
|
7
|
+
return model.name.toLowerCase().includes(query) || model.slug.toLowerCase().includes(query);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* The rows of the model list. A model an old CLI cannot run is a row like any other, so a search for
|
|
12
|
+
* only that model still finds the row that explains why it cannot be picked.
|
|
13
|
+
*/
|
|
14
|
+
export function pickerEntriesOf(providers: readonly ProviderInfo[], query: string, legacyOpen: boolean): PickerEntry[] {
|
|
15
|
+
const rows = providers.flatMap((entry) => entry.models.map((model) => ({ kind: 'model' as const, provider: entry, model })));
|
|
16
|
+
if (query !== '') {
|
|
17
|
+
return rows.filter((row) => matches(row.model, query));
|
|
18
|
+
}
|
|
19
|
+
const live = rows.filter((row) => !row.model.legacy);
|
|
20
|
+
const legacy = rows.filter((row) => row.model.legacy);
|
|
21
|
+
if (legacy.length === 0) {
|
|
22
|
+
return live;
|
|
23
|
+
}
|
|
24
|
+
return legacyOpen ? [...live, { kind: 'legacy', count: legacy.length }, ...legacy] : [...live, { kind: 'legacy', count: legacy.length }];
|
|
25
|
+
}
|
|
@@ -52,7 +52,7 @@ export type TimelineRow =
|
|
|
52
52
|
| { kind: 'question'; id: string; item: ChatQuestionItem }
|
|
53
53
|
| { kind: 'note'; id: string; level: 'info' | 'warning' | 'error'; text: string; from?: string }
|
|
54
54
|
| { kind: 'compaction'; id: string; preTokens: number | null }
|
|
55
|
-
| { kind: 'changed-files'; id: string; turnId: string; tools: ChatToolItem[]; diff: ChatCheckpointDiff | null; checkpoint: boolean }
|
|
55
|
+
| { kind: 'changed-files'; id: string; turnId: string; tools: ChatToolItem[]; diff: ChatCheckpointDiff | null; checkpoint: boolean; baseMoved?: BaseMoved }
|
|
56
56
|
| { kind: 'turn-fold'; id: string; turn: ChatTurnItem; label: string; work: string[]; hiddenCount: number; expanded: boolean }
|
|
57
57
|
| { kind: 'forks'; id: string; turnId: string }
|
|
58
58
|
| { kind: 'working'; id: string; startedAt: number }
|
|
@@ -322,6 +322,12 @@ function rowsForItems(items: ChatItem[], options: TimelineOptions, children: Map
|
|
|
322
322
|
return rows;
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
/* The branch the turn started on was not the one it ended on, so its diff also holds what came in with the move. */
|
|
326
|
+
export interface BaseMoved {
|
|
327
|
+
from: string;
|
|
328
|
+
to: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
325
331
|
/* The card of a settled turn: the checkpoint diff when the host took one, else what the tool calls carry. */
|
|
326
332
|
function changedFilesRow(turn: ChatTurnItem, items: ChatItem[]): TimelineRow | null {
|
|
327
333
|
const edits = items.filter(
|
|
@@ -332,7 +338,16 @@ function changedFilesRow(turn: ChatTurnItem, items: ChatItem[]): TimelineRow | n
|
|
|
332
338
|
if (empty) {
|
|
333
339
|
return null;
|
|
334
340
|
}
|
|
335
|
-
|
|
341
|
+
const moved = turn.checkpointHead !== undefined && turn.checkpointHeadAfter !== undefined && turn.checkpointHead !== turn.checkpointHeadAfter;
|
|
342
|
+
return {
|
|
343
|
+
kind: 'changed-files',
|
|
344
|
+
id: `files-${turn.id}`,
|
|
345
|
+
turnId: turn.id,
|
|
346
|
+
tools: edits,
|
|
347
|
+
diff,
|
|
348
|
+
checkpoint: turn.checkpoint !== undefined,
|
|
349
|
+
...(moved ? { baseMoved: { from: turn.checkpointHead!, to: turn.checkpointHeadAfter! } } : {})
|
|
350
|
+
};
|
|
336
351
|
}
|
|
337
352
|
|
|
338
353
|
function lastAssistantRow(rows: TimelineRow[]): TimelineRow | null {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* How long a person must stop typing before a waiting prompt card takes the composer's place. */
|
|
2
|
+
export const PROMPT_HOLD_PAUSE_MS = 2000;
|
|
3
|
+
|
|
4
|
+
export interface HoldInput {
|
|
5
|
+
/* When the last change to the draft was made; null when none was since the hold last lifted. */
|
|
6
|
+
typedAt: number | null;
|
|
7
|
+
/* An IME composition is under way, which can run for any length without a change. */
|
|
8
|
+
composing: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type HoldRelease = 'pause' | 'send' | 'blur' | 'show';
|
|
12
|
+
|
|
13
|
+
/* A card waits while the person is composing, or typed less than the pause ago. */
|
|
14
|
+
export function holdsPrompt(input: HoldInput, now: number): boolean {
|
|
15
|
+
if (input.composing) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return input.typedAt !== null && now - input.typedAt < PROMPT_HOLD_PAUSE_MS;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* The milliseconds until the hold lifts by itself; null when it holds until something else lifts it, or does not hold. */
|
|
22
|
+
export function holdLiftsIn(input: HoldInput, now: number): number | null {
|
|
23
|
+
if (input.composing || input.typedAt === null) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return Math.max(0, input.typedAt + PROMPT_HOLD_PAUSE_MS - now);
|
|
27
|
+
}
|
package/src/chat/quote.ts
CHANGED
|
@@ -24,3 +24,23 @@ export function withQuote(quote: string, message: string): string {
|
|
|
24
24
|
.join('\n');
|
|
25
25
|
return message === '' ? block : `${block}\n\n${message}`;
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* The inverse of `withQuote`: the quote a message opens with and what follows it. Null when the text
|
|
30
|
+
* does not open with a blockquote that ends on a blank line or the end of the text.
|
|
31
|
+
*/
|
|
32
|
+
export function splitQuote(text: string): { quote: string; rest: string } | null {
|
|
33
|
+
const lines = text.split('\n');
|
|
34
|
+
let end = 0;
|
|
35
|
+
while (end < lines.length && (lines[end] === '>' || lines[end]!.startsWith('> '))) {
|
|
36
|
+
end += 1;
|
|
37
|
+
}
|
|
38
|
+
if (end === 0 || (end < lines.length && lines[end] !== '')) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const quote = lines
|
|
42
|
+
.slice(0, end)
|
|
43
|
+
.map((line) => line.slice(2))
|
|
44
|
+
.join('\n');
|
|
45
|
+
return { quote, rest: lines.slice(end + 1).join('\n') };
|
|
46
|
+
}
|
package/src/chat/stash.ts
CHANGED
|
@@ -27,9 +27,17 @@ export interface StashedPrompt {
|
|
|
27
27
|
mentions: string[];
|
|
28
28
|
skills: string[];
|
|
29
29
|
attachments: StashedAttachment[];
|
|
30
|
+
/* A quote that knew its reply, kept apart from the text so it can lead back to it in the chat it came from. */
|
|
31
|
+
quote?: StashedQuote;
|
|
30
32
|
createdAt: number;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export interface StashedQuote {
|
|
36
|
+
text: string;
|
|
37
|
+
chatId: string;
|
|
38
|
+
itemId: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
function strings(value: unknown): string[] {
|
|
34
42
|
return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : [];
|
|
35
43
|
}
|
|
@@ -45,6 +53,18 @@ function attachments(value: unknown): StashedAttachment[] {
|
|
|
45
53
|
}));
|
|
46
54
|
}
|
|
47
55
|
|
|
56
|
+
function stashedQuote(value: unknown): StashedQuote | undefined {
|
|
57
|
+
if (!isRecord(value) || typeof value.text !== 'string' || typeof value.chatId !== 'string' || typeof value.itemId !== 'string') {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return { text: value.text, chatId: value.chatId, itemId: value.itemId };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function stashedQuoteField(value: unknown): { quote?: StashedQuote } {
|
|
64
|
+
const quote = stashedQuote(value);
|
|
65
|
+
return quote === undefined ? {} : { quote };
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
/* Only the fields we know today, so anything an older or a broken write left behind falls away here. */
|
|
49
69
|
export function parseStash(raw: string | null): StashedPrompt[] {
|
|
50
70
|
if (raw === null) {
|
|
@@ -64,6 +84,7 @@ export function parseStash(raw: string | null): StashedPrompt[] {
|
|
|
64
84
|
mentions: strings(entry.mentions),
|
|
65
85
|
skills: strings(entry.skills),
|
|
66
86
|
attachments: attachments(entry.attachments),
|
|
87
|
+
...stashedQuoteField(entry.quote),
|
|
67
88
|
createdAt: typeof entry.createdAt === 'number' ? entry.createdAt : 0
|
|
68
89
|
}))
|
|
69
90
|
.slice(0, STASH_LIMIT);
|
|
@@ -77,14 +98,19 @@ export function withStashed(prompts: StashedPrompt[], entry: StashedPrompt): Sta
|
|
|
77
98
|
return [entry, ...prompts].slice(0, STASH_LIMIT);
|
|
78
99
|
}
|
|
79
100
|
|
|
80
|
-
/*
|
|
81
|
-
|
|
101
|
+
/*
|
|
102
|
+
* What a draft looks like on the shelf; null when there is nothing worth putting there. A quote that
|
|
103
|
+
* knows its reply stays apart, since it leads back to that reply in `chatId`; any other is folded into the text.
|
|
104
|
+
*/
|
|
105
|
+
export function stashedFrom(draft: ChatDraft, id: string, now: number, chatId: string): StashedPrompt | null {
|
|
82
106
|
if (isEmptyDraft(draft)) {
|
|
83
107
|
return null;
|
|
84
108
|
}
|
|
109
|
+
const kept = draft.quote !== '' && draft.quoteSource !== null;
|
|
85
110
|
return {
|
|
86
111
|
id,
|
|
87
|
-
text: withQuote(draft.quote, draft.text),
|
|
112
|
+
text: kept ? draft.text : withQuote(draft.quote, draft.text),
|
|
113
|
+
...(kept ? { quote: { text: draft.quote, chatId, itemId: draft.quoteSource! } } : {}),
|
|
88
114
|
mentions: [...draft.mentions],
|
|
89
115
|
skills: [...draft.skills],
|
|
90
116
|
attachments: draft.attachments.map((attachment) => ({ name: attachment.name, mime: attachment.mime, size: uploadBytes(attachment) })),
|
|
@@ -106,8 +132,8 @@ function write(prompts: StashedPrompt[]): void {
|
|
|
106
132
|
}
|
|
107
133
|
|
|
108
134
|
/* Puts a draft on the shelf; false when the composer held nothing to put there. */
|
|
109
|
-
export function stashDraft(draft: ChatDraft): boolean {
|
|
110
|
-
const entry = stashedFrom(draft, `stash-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, Date.now());
|
|
135
|
+
export function stashDraft(draft: ChatDraft, chatId: string): boolean {
|
|
136
|
+
const entry = stashedFrom(draft, `stash-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, Date.now(), chatId);
|
|
111
137
|
if (!entry) {
|
|
112
138
|
return false;
|
|
113
139
|
}
|
|
@@ -48,6 +48,16 @@ export function taskRowStateOf(item: ChatSubagentItem, task: SubagentTask | null
|
|
|
48
48
|
return word;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/* How far delegated work is: what settled as done, what still runs or waits, and how many of those wait on a person. */
|
|
52
|
+
export function progressOf(states: readonly TaskRowState[]): { done: number; total: number; waiting: number; active: number } {
|
|
53
|
+
return {
|
|
54
|
+
done: states.filter((state) => state === 'done').length,
|
|
55
|
+
total: states.length,
|
|
56
|
+
waiting: states.filter((state) => state === 'waiting').length,
|
|
57
|
+
active: states.filter((state) => state === 'running' || state === 'paused' || state === 'waiting').length
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
/*
|
|
52
62
|
* What the flyout over the composer lists: every sub-agent still at work, and the ones that settled
|
|
53
63
|
* since the last message, so a finished batch stays until the next message is sent. That includes an
|
|
@@ -106,11 +116,12 @@ export function entryTimeOf(item: ChatSubagentItem, task: SubagentTask | null, n
|
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
/*
|
|
109
|
-
* What the Stop of an active entry does: a task stops the node working on it, and a subagent
|
|
110
|
-
*
|
|
111
|
-
* turn may still wait on it, so stopping the turn
|
|
119
|
+
* What the Stop of an active entry does: a task stops the node working on it, and a subagent whose CLI
|
|
120
|
+
* named its task id (`native`) is ended on its own, turn or not. Any other subagent of the CLI's own is
|
|
121
|
+
* only marked stopped; while the chat's turn runs that turn may still wait on it, so stopping the turn
|
|
122
|
+
* (the composer's Stop) is the only offer then.
|
|
112
123
|
*/
|
|
113
|
-
export type SubagentStop = 'task' | 'mark';
|
|
124
|
+
export type SubagentStop = 'task' | 'native' | 'mark';
|
|
114
125
|
|
|
115
126
|
export function stopOf(item: ChatSubagentItem, turnRunning: boolean): SubagentStop | null {
|
|
116
127
|
if (item.status !== 'running') {
|
|
@@ -119,11 +130,14 @@ export function stopOf(item: ChatSubagentItem, turnRunning: boolean): SubagentSt
|
|
|
119
130
|
if (item.origin === 'ruimte') {
|
|
120
131
|
return item.childId === undefined ? null : 'task';
|
|
121
132
|
}
|
|
133
|
+
if (item.native?.agentId) {
|
|
134
|
+
return 'native';
|
|
135
|
+
}
|
|
122
136
|
return turnRunning ? null : 'mark';
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
export function stopLabel(stop: SubagentStop): string {
|
|
126
|
-
return
|
|
140
|
+
return i18next.t(`agent-chat:subagents.stop.${stop}`);
|
|
127
141
|
}
|
|
128
142
|
|
|
129
143
|
/*
|