@adecore/agents-react 0.25.8 → 0.25.9
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/chat/index.d.ts +1 -0
- package/dist/chat/index.js +1 -0
- package/dist/chat/logic/command-looks.d.ts +19 -0
- package/dist/chat/logic/command-looks.js +123 -0
- package/dist/chat/logic/embedded-code.d.ts +25 -0
- package/dist/chat/logic/embedded-code.js +27 -5
- package/dist/chat/logic/timeline.d.ts +5 -3
- package/dist/chat/logic/timeline.js +64 -10
- package/dist/chat/ui/ShellCommand.d.ts +11 -1
- package/dist/chat/ui/ShellCommand.js +60 -20
- package/dist/chat/ui/SubagentTimeline.js +9 -1
- package/dist/chat/ui/Timeline.js +9 -2
- package/dist/chat/ui/rows/Rows.js +2 -2
- package/dist/chat/ui/rows/SubagentRow.js +1 -1
- package/dist/chat/ui/rows/WorkRows.d.ts +13 -4
- package/dist/chat/ui/rows/WorkRows.js +55 -11
- package/dist/chat/ui/rows/WorkflowRow.js +1 -1
- package/dist/host.d.ts +25 -0
- package/dist/index.d.ts +1 -1
- package/dist/locales/en/agent-chat.d.ts +4 -0
- package/dist/locales/en/agent-chat.js +5 -1
- package/dist/locales/nl/agent-chat.d.ts +4 -0
- package/dist/locales/nl/agent-chat.js +5 -1
- package/package.json +8 -8
- package/src/chat/index.ts +1 -0
- package/src/chat/logic/command-looks.ts +157 -0
- package/src/chat/logic/embedded-code.ts +57 -11
- package/src/chat/logic/timeline.ts +70 -13
- package/src/chat/ui/ShellCommand.tsx +83 -23
- package/src/chat/ui/SubagentTimeline.tsx +11 -2
- package/src/chat/ui/Timeline.tsx +9 -2
- package/src/chat/ui/rows/Rows.tsx +2 -2
- package/src/chat/ui/rows/SubagentRow.tsx +1 -1
- package/src/chat/ui/rows/WorkRows.tsx +136 -19
- package/src/chat/ui/rows/WorkflowRow.tsx +1 -1
- package/src/host.ts +43 -0
- package/src/index.ts +3 -0
- package/src/locales/en/agent-chat.ts +5 -1
- package/src/locales/nl/agent-chat.ts +5 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { Fragment, useEffect, useState, type Dispatch, type ReactNode, type SetStateAction } from 'react';
|
|
1
|
+
import { Fragment, useContext, useEffect, useState, type Dispatch, type ReactNode, type SetStateAction } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import i18next from 'i18next';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { ChevronRight, FileDiff, X } from 'lucide-react';
|
|
6
6
|
import type { ChatCheckpointDiff, ChatCheckpointFile, ChatFileChange, ChatToolItem, ChatTurnItem } from '@adecore/agent-contracts';
|
|
7
7
|
import type { BaseMoved } from '../../logic/timeline';
|
|
8
|
+
import { matchCommandLook, type CommandLookRun } from '../../logic/command-looks';
|
|
8
9
|
import {
|
|
9
10
|
exitCodeOf,
|
|
10
11
|
fileChanges,
|
|
@@ -21,10 +22,10 @@ import {
|
|
|
21
22
|
type FileChange
|
|
22
23
|
} from '../../logic/tools';
|
|
23
24
|
import { useOpenForFind } from '../find-reveal';
|
|
24
|
-
import { useChatHost } from '../../../host';
|
|
25
|
+
import { useChatHost, type CommandLook } from '../../../host';
|
|
25
26
|
import { lazyNamed } from '../../../lazy';
|
|
26
27
|
import { useChatActions } from '../../actions';
|
|
27
|
-
import { useChatScope } from '../../../scope';
|
|
28
|
+
import { ChatScopeContext, useChatScope } from '../../../scope';
|
|
28
29
|
import { useCurrentItem } from '../../../state/chats';
|
|
29
30
|
import { useTimelineFlash } from '../../timeline-flash';
|
|
30
31
|
import { formatClockDuration, formatElapsedShort } from '@adecore/ui/format';
|
|
@@ -32,7 +33,7 @@ import { Icon, LazyBoundary, MiddleTruncate, Spinner, useTickingText } from '@ad
|
|
|
32
33
|
import { ROW_GUTTER, toolIcon } from '../icons';
|
|
33
34
|
import { AnsiOutput } from '../AnsiOutput';
|
|
34
35
|
import { RowTime } from '../RowTime';
|
|
35
|
-
import { ShellCommand } from '../ShellCommand';
|
|
36
|
+
import { ShellCommand, type NamedWord } from '../ShellCommand';
|
|
36
37
|
import { DiffStack, ReviewStackView } from '../diff/DiffStack';
|
|
37
38
|
import { reviewEntries, type ReviewEntry } from '../diff/review-plan';
|
|
38
39
|
import { useReviewStack } from '../diff/use-review-stack';
|
|
@@ -51,6 +52,59 @@ function clip(text: string): string {
|
|
|
51
52
|
return text.slice(0, OUTPUT_LIMIT) + clipNotice(text);
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
const BRAND = 'font-semibold';
|
|
56
|
+
|
|
57
|
+
/* `text` with `brand` in semibold where it names it, as a sentence about a look does. */
|
|
58
|
+
function withBrand(text: string, brands: readonly string[]): ReactNode {
|
|
59
|
+
for (const brand of brands) {
|
|
60
|
+
const at = brand === '' ? -1 : text.indexOf(brand);
|
|
61
|
+
if (at !== -1) {
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
{text.slice(0, at)}
|
|
65
|
+
<span className={BRAND}>{brand}</span>
|
|
66
|
+
{text.slice(at + brand.length)}
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return text;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* The detail of a look's run, with the words its host names drawn as those names. */
|
|
75
|
+
function NamedLookDetail({ run, chatId, useNames }: { run: CommandLookRun; chatId: string; useNames: NonNullable<CommandLook['useNames']> }) {
|
|
76
|
+
const scope = useContext(ChatScopeContext);
|
|
77
|
+
const nameOf = useNames(scope?.id ?? '', chatId);
|
|
78
|
+
const names = run.words.flatMap((word): NamedWord[] => {
|
|
79
|
+
const name = nameOf(word.text, word.option);
|
|
80
|
+
return name === null ? [] : [{ start: word.start, end: word.end, word: word.text, name }];
|
|
81
|
+
});
|
|
82
|
+
return <ShellCommand command={run.detail} code={run.code} names={names} />;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function LookDetail({ run, chatId }: { run: CommandLookRun; chatId: string }) {
|
|
86
|
+
const { useNames } = run.look;
|
|
87
|
+
if (useNames === undefined) {
|
|
88
|
+
return <ShellCommand command={run.detail} code={run.code} />;
|
|
89
|
+
}
|
|
90
|
+
// Keyed by the look, so a row whose command turns into another look's starts its hooks again.
|
|
91
|
+
return <NamedLookDetail key={run.look.program} run={run} chatId={chatId} useNames={useNames} />;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const TAG = 'shrink-0 rounded bg-surface-hover px-1.5 text-text-faint';
|
|
95
|
+
|
|
96
|
+
/* What else the line ran, in quiet tags: the other programs and a heredoc, and how many more runs of a look. */
|
|
97
|
+
function LookTags({ run }: { run: CommandLookRun }) {
|
|
98
|
+
const { t } = useTranslation('agent-chat');
|
|
99
|
+
const also = run.heredoc ? [...run.others, t('work.heredoc')] : run.others;
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
{also.length > 0 && <span className={TAG}>{t('work.alsoRan', { programs: also.join(', ') })}</span>}
|
|
103
|
+
{run.more > 0 && <span className={TAG}>{t('work.moreRuns', { count: run.more })}</span>}
|
|
104
|
+
</>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
54
108
|
/*
|
|
55
109
|
* The line of a call, a subagent or a group. Without `onToggle` it has nothing to open: no chevron, and
|
|
56
110
|
* not a button. `time` shows at the right edge while the line is hovered or focused.
|
|
@@ -58,8 +112,10 @@ function clip(text: string): string {
|
|
|
58
112
|
export function ToggleLine({
|
|
59
113
|
icon,
|
|
60
114
|
label,
|
|
115
|
+
brand,
|
|
61
116
|
detail,
|
|
62
117
|
command,
|
|
118
|
+
look,
|
|
63
119
|
path,
|
|
64
120
|
open = false,
|
|
65
121
|
onToggle,
|
|
@@ -70,10 +126,14 @@ export function ToggleLine({
|
|
|
70
126
|
className
|
|
71
127
|
}: {
|
|
72
128
|
icon: ReactNode;
|
|
73
|
-
label:
|
|
129
|
+
label: ReactNode;
|
|
130
|
+
/* Drawn in semibold before the label. */
|
|
131
|
+
brand?: string;
|
|
74
132
|
detail?: string;
|
|
75
133
|
/* Draws `detail` as a shell command, in the colors of the code theme. */
|
|
76
134
|
command?: boolean;
|
|
135
|
+
/* A shell call read as a look's tool, whose detail and tags take the place of `detail`. */
|
|
136
|
+
look?: { run: CommandLookRun; chatId: string };
|
|
77
137
|
/* `detail` is the path of a file, cut in the middle so its name stays in sight. */
|
|
78
138
|
path?: boolean;
|
|
79
139
|
open?: boolean;
|
|
@@ -94,11 +154,26 @@ export function ToggleLine({
|
|
|
94
154
|
const content = (
|
|
95
155
|
<>
|
|
96
156
|
<span className={clsx(ROW_GUTTER, live && 'text-accent')}>{icon}</span>
|
|
97
|
-
<span className={clsx('shrink-0', live && 'chat-live-text')}>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
157
|
+
<span className={clsx('shrink-0', live && 'chat-live-text')}>
|
|
158
|
+
{brand !== undefined && <span className={BRAND}>{brand}</span>}
|
|
159
|
+
{brand !== undefined && label !== '' && ' '}
|
|
160
|
+
{label}
|
|
161
|
+
</span>
|
|
162
|
+
{look !== undefined ? (
|
|
163
|
+
<>
|
|
164
|
+
{look.run.detail !== '' && (
|
|
165
|
+
<span data-find-field="summary" className="min-w-0 truncate font-mono text-text-faint">
|
|
166
|
+
<LookDetail run={look.run} chatId={look.chatId} />
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
169
|
+
<LookTags run={look.run} />
|
|
170
|
+
</>
|
|
171
|
+
) : (
|
|
172
|
+
detail && (
|
|
173
|
+
<span data-find-field="summary" className="min-w-0 truncate font-mono text-text-faint">
|
|
174
|
+
{command ? <ShellCommand command={detail} /> : path ? <MiddleTruncate text={detail} /> : detail}
|
|
175
|
+
</span>
|
|
176
|
+
)
|
|
102
177
|
)}
|
|
103
178
|
<span className="grow" />
|
|
104
179
|
{trailing}
|
|
@@ -237,6 +312,7 @@ function ToolBody({ tool }: { tool: ChatToolItem }) {
|
|
|
237
312
|
*/
|
|
238
313
|
export function WorkRow({
|
|
239
314
|
tool,
|
|
315
|
+
chatId,
|
|
240
316
|
nested,
|
|
241
317
|
detail,
|
|
242
318
|
icon,
|
|
@@ -244,6 +320,8 @@ export function WorkRow({
|
|
|
244
320
|
failed
|
|
245
321
|
}: {
|
|
246
322
|
tool: ChatToolItem;
|
|
323
|
+
/* The chat the call is in, which names the words of a look's run. */
|
|
324
|
+
chatId?: string;
|
|
247
325
|
nested?: boolean;
|
|
248
326
|
detail?: string;
|
|
249
327
|
icon?: ReactNode;
|
|
@@ -257,13 +335,17 @@ export function WorkRow({
|
|
|
257
335
|
useOpenForFind(tool.id, 'output', setOpen);
|
|
258
336
|
const summary = detail ?? toolSummary(tool.name, tool.input);
|
|
259
337
|
const opens = hasToolBody(tool);
|
|
338
|
+
const shell = shellCommand(tool.name, tool.input);
|
|
339
|
+
const run = detail === undefined && icon === undefined && label === undefined && shell !== null ? matchCommandLook(shell, host.commandLooks) : null;
|
|
260
340
|
return (
|
|
261
341
|
<div data-find-item={tool.id} className={nested ? 'ml-6' : undefined}>
|
|
262
342
|
<ToggleLine
|
|
263
|
-
icon={icon ?? toolIcon(tool.name)}
|
|
264
|
-
label={label ?? tool.name}
|
|
343
|
+
icon={run === null ? (icon ?? toolIcon(tool.name)) : <run.look.Mark size={12} />}
|
|
344
|
+
label={run === null ? (label ?? tool.name) : run.match.label}
|
|
345
|
+
brand={run?.look.brand}
|
|
265
346
|
detail={summary}
|
|
266
|
-
|
|
347
|
+
look={run === null ? undefined : { run, chatId: chatId ?? '' }}
|
|
348
|
+
command={detail === undefined && summary === shell}
|
|
267
349
|
path={detail === undefined && toolSummaryIsPath(tool.name, tool.input)}
|
|
268
350
|
open={open && opens}
|
|
269
351
|
onToggle={opens ? () => setOpen((o) => !o) : undefined}
|
|
@@ -290,11 +372,16 @@ export function WorkLiveRow({ chatId, tool: derived }: { chatId: string; tool: C
|
|
|
290
372
|
const tail = liveOutput(tool);
|
|
291
373
|
const summary = toolSummary(tool.name, tool.input);
|
|
292
374
|
const opens = hasToolBody(tool);
|
|
375
|
+
const shell = shellCommand(tool.name, tool.input);
|
|
376
|
+
const { commandLooks } = useChatHost();
|
|
377
|
+
const run = shell === null ? null : matchCommandLook(shell, commandLooks);
|
|
293
378
|
return (
|
|
294
379
|
<div>
|
|
295
380
|
<ToggleLine
|
|
296
|
-
icon={toolIcon(tool.name)}
|
|
297
|
-
label={tool.name}
|
|
381
|
+
icon={run === null ? toolIcon(tool.name) : <run.look.Mark size={12} />}
|
|
382
|
+
label={run === null ? tool.name : run.match.label}
|
|
383
|
+
brand={run?.look.brand}
|
|
384
|
+
look={run === null ? undefined : { run, chatId }}
|
|
298
385
|
detail={summary || tool.progress?.description || ''}
|
|
299
386
|
command={summary !== '' && summary === shellCommand(tool.name, tool.input)}
|
|
300
387
|
path={summary !== '' && toolSummaryIsPath(tool.name, tool.input)}
|
|
@@ -313,12 +400,31 @@ export function WorkLiveRow({ chatId, tool: derived }: { chatId: string; tool: C
|
|
|
313
400
|
);
|
|
314
401
|
}
|
|
315
402
|
|
|
316
|
-
/* A run of tool calls as one line ("Read 4 files"); opening it lists every call. */
|
|
317
|
-
export function WorkGroupRow({
|
|
403
|
+
/* A run of tool calls as one line ("Read 4 files"); opening it lists every call. A run of one look wears its mark and its brand. */
|
|
404
|
+
export function WorkGroupRow({
|
|
405
|
+
tools,
|
|
406
|
+
summary,
|
|
407
|
+
look,
|
|
408
|
+
expanded,
|
|
409
|
+
onToggle
|
|
410
|
+
}: {
|
|
411
|
+
tools: ChatToolItem[];
|
|
412
|
+
summary: string;
|
|
413
|
+
look?: CommandLook;
|
|
414
|
+
expanded: boolean;
|
|
415
|
+
onToggle(): void;
|
|
416
|
+
}) {
|
|
318
417
|
const failed = tools.some((tool) => tool.state === 'error');
|
|
319
418
|
return (
|
|
320
419
|
<div>
|
|
321
|
-
<ToggleLine
|
|
420
|
+
<ToggleLine
|
|
421
|
+
icon={look === undefined ? toolIcon(tools[0]!.name) : <look.Mark size={12} />}
|
|
422
|
+
label={look?.brand === undefined ? summary : withBrand(summary, [look.brand])}
|
|
423
|
+
open={expanded}
|
|
424
|
+
onToggle={onToggle}
|
|
425
|
+
failed={failed}
|
|
426
|
+
time={tools[0]!.createdAt}
|
|
427
|
+
/>
|
|
322
428
|
</div>
|
|
323
429
|
);
|
|
324
430
|
}
|
|
@@ -340,6 +446,8 @@ export function TurnFoldRow({
|
|
|
340
446
|
expanded: boolean;
|
|
341
447
|
onToggle(): void;
|
|
342
448
|
}) {
|
|
449
|
+
const { commandLooks } = useChatHost();
|
|
450
|
+
const brands = commandLooks?.flatMap(({ brand }) => (brand === undefined ? [] : [brand])) ?? [];
|
|
343
451
|
return (
|
|
344
452
|
<button
|
|
345
453
|
className={clsx(
|
|
@@ -353,7 +461,16 @@ export function TurnFoldRow({
|
|
|
353
461
|
<Icon icon={ChevronRight} size={12} className={clsx('transition-transform', expanded && 'rotate-90')} />
|
|
354
462
|
</span>
|
|
355
463
|
<span className="shrink-0 group-hover:text-text">{label}</span>
|
|
356
|
-
{work.length > 0 &&
|
|
464
|
+
{work.length > 0 && (
|
|
465
|
+
<span className="min-w-0 truncate text-text-faint">
|
|
466
|
+
{work.map((part, index) => (
|
|
467
|
+
<Fragment key={index}>
|
|
468
|
+
{' · '}
|
|
469
|
+
{withBrand(part, brands)}
|
|
470
|
+
</Fragment>
|
|
471
|
+
))}
|
|
472
|
+
</span>
|
|
473
|
+
)}
|
|
357
474
|
<span className="grow" />
|
|
358
475
|
<RowTime at={turn.createdAt} />
|
|
359
476
|
</button>
|
|
@@ -99,7 +99,7 @@ export function WorkflowRow({
|
|
|
99
99
|
<div>
|
|
100
100
|
<div className="flex items-center gap-1">
|
|
101
101
|
<div className="min-w-0 grow">
|
|
102
|
-
{running ? <WorkLiveRow chatId={chatId ?? ''} tool={tool} /> : <WorkRow tool={tool} detail={workflow.name ?? undefined} />}
|
|
102
|
+
{running ? <WorkLiveRow chatId={chatId ?? ''} tool={tool} /> : <WorkRow chatId={chatId} tool={tool} detail={workflow.name ?? undefined} />}
|
|
103
103
|
</div>
|
|
104
104
|
{running && chatId && workflow.taskId && <IconButton icon={Square} size="xs" label={t('activity.stop')} onClick={stop} />}
|
|
105
105
|
</div>
|
package/src/host.ts
CHANGED
|
@@ -195,6 +195,47 @@ export interface ReplyAuthor {
|
|
|
195
195
|
mark?: ReactNode;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/* A name a host gives a word of a command, such as the title of what an id stands for. */
|
|
199
|
+
export interface CommandName {
|
|
200
|
+
name: string;
|
|
201
|
+
/* Drawn before the name, at the size of the detail. */
|
|
202
|
+
icon?: ReactNode;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* What one run of a program is about, as its look reads it from the words. */
|
|
206
|
+
export interface CommandLookMatch {
|
|
207
|
+
/* Drawn where the tool name is, after the brand; empty draws the brand alone. */
|
|
208
|
+
label: string;
|
|
209
|
+
/* The word the detail starts at, counted from the first word after the program; the detail is the source from there, as it was typed. */
|
|
210
|
+
detailFrom: number;
|
|
211
|
+
/* Calls with the same key read as one sentence in a group or a folded turn. */
|
|
212
|
+
group?: { key: string; sentence(count: number): string };
|
|
213
|
+
/* Options whose value the line shortens past 40 characters, such as a prompt. */
|
|
214
|
+
fold?: readonly string[];
|
|
215
|
+
/* Options whose value is code, by Shiki language id, such as `{ '--sql': 'sql' }`. */
|
|
216
|
+
languages?: Readonly<Record<string, string>>;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* A program the app recognizes inside a shell call and draws as a tool of its own. */
|
|
220
|
+
export interface CommandLook {
|
|
221
|
+
/* The program as it is run, matched on its last path segment. */
|
|
222
|
+
program: string;
|
|
223
|
+
/* Drawn in semibold before every label of this look, such as a product name. */
|
|
224
|
+
brand?: string;
|
|
225
|
+
/* The mark in the gutter, drawn in `currentColor` at the size of a tool icon. */
|
|
226
|
+
Mark: ComponentType<{ size: number }>;
|
|
227
|
+
/* What one run is about, from its words with the quotes taken out; null leaves it a shell call. */
|
|
228
|
+
describe(args: readonly string[]): CommandLookMatch | null;
|
|
229
|
+
/* Calls of this look under different group keys, as one sentence; left out, such a run reads as shell calls. */
|
|
230
|
+
sentence?(count: number): string;
|
|
231
|
+
/*
|
|
232
|
+
* Names for words in the detail, such as ids, as a hook the row calls once. A word it names is drawn as
|
|
233
|
+
* that name, with the word itself in a tooltip; null leaves the word as it was typed. `option` is the
|
|
234
|
+
* option the word is the value of, or null for a word that follows none.
|
|
235
|
+
*/
|
|
236
|
+
useNames?(scopeId: string, chatId: string): (word: string, option: string | null) => CommandName | null;
|
|
237
|
+
}
|
|
238
|
+
|
|
198
239
|
/*
|
|
199
240
|
* Where the tiles of a Map come from. The host owns the service and whatever admits a request to it, so
|
|
200
241
|
* a chat never knows an address, a key or a vendor: it asks for the style of the theme and lets the host
|
|
@@ -363,6 +404,8 @@ export interface ChatHost {
|
|
|
363
404
|
useReplyAuthor(scopeId: string, chatId: string): ReplyAuthor | null;
|
|
364
405
|
/* The pages agents publish, drawn in the thread above their reply; null draws none. */
|
|
365
406
|
visuals: VisualHost | null;
|
|
407
|
+
/* Programs drawn as tools of their own; left out, every shell call reads as itself. */
|
|
408
|
+
commandLooks?: readonly CommandLook[];
|
|
366
409
|
}
|
|
367
410
|
|
|
368
411
|
const NO_PROMPTS: readonly HostPrompt[] = [];
|
package/src/index.ts
CHANGED
|
@@ -198,7 +198,11 @@ export default {
|
|
|
198
198
|
edits_one: '{{count}} edit',
|
|
199
199
|
edits_other: '{{count}} edits',
|
|
200
200
|
workingFor: 'Working for',
|
|
201
|
-
exitCode: 'Exit code {{code}}'
|
|
201
|
+
exitCode: 'Exit code {{code}}',
|
|
202
|
+
alsoRan: '+ {{programs}}',
|
|
203
|
+
heredoc: 'heredoc',
|
|
204
|
+
moreRuns_one: '+{{count}}',
|
|
205
|
+
moreRuns_other: '+{{count}}'
|
|
202
206
|
},
|
|
203
207
|
timeline: {
|
|
204
208
|
welcome: {
|
|
@@ -198,7 +198,11 @@ export default {
|
|
|
198
198
|
edits_one: '{{count}} bewerking',
|
|
199
199
|
edits_other: '{{count}} bewerkingen',
|
|
200
200
|
workingFor: 'Werkt al',
|
|
201
|
-
exitCode: 'Afsluitcode {{code}}'
|
|
201
|
+
exitCode: 'Afsluitcode {{code}}',
|
|
202
|
+
alsoRan: '+ {{programs}}',
|
|
203
|
+
heredoc: 'heredoc',
|
|
204
|
+
moreRuns_one: '+{{count}}',
|
|
205
|
+
moreRuns_other: '+{{count}}'
|
|
202
206
|
},
|
|
203
207
|
timeline: {
|
|
204
208
|
welcome: {
|