ucode-agent 1.12.1 → 1.14.0
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/package.json +1 -1
- package/src/core/loop.js +59 -8
- package/src/tools/index.js +42 -42
- package/src/ui/screen.js +56 -21
- package/src/ui/theme.js +30 -7
package/package.json
CHANGED
package/src/core/loop.js
CHANGED
|
@@ -434,10 +434,24 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
434
434
|
'',
|
|
435
435
|
'## How to work',
|
|
436
436
|
'',
|
|
437
|
-
'
|
|
438
|
-
'
|
|
439
|
-
'
|
|
440
|
-
'
|
|
437
|
+
'FIRST, EVERY TIME: one short line saying what you are about to do, then the tool',
|
|
438
|
+
'calls. Never open a turn with a tool call and no words. "Right, the HTML',
|
|
439
|
+
'structure first." / "Now the state and the render loop." / "That is the layout',
|
|
440
|
+
'done - onto the animations." / "Let me see what is there." One sentence, your own',
|
|
441
|
+
'voice, before the actions - not after them, not instead of them, not a',
|
|
442
|
+
'restatement of the request. The user watches this scroll past, and without those',
|
|
443
|
+
'lines it is a list of file operations they cannot read intent from.',
|
|
444
|
+
'',
|
|
445
|
+
'FIRST, EVERY TIME: write one short line saying what you are about to do, then',
|
|
446
|
+
'make the tool calls. Never open a turn with a tool call and no words. Examples:',
|
|
447
|
+
'"Right, the HTML structure first." / "Now the state and the render loop." /',
|
|
448
|
+
'"That is the layout done - onto the animations." / "Let me see what is there."',
|
|
449
|
+
'One sentence, your own voice, before the actions - not after them, not instead',
|
|
450
|
+
'of them, and not a restatement of what was asked. The user is watching this',
|
|
451
|
+
'scroll past; without those lines it is a list of file operations and they cannot',
|
|
452
|
+
'tell what you are building. This matters as much as the code.',
|
|
453
|
+
'',
|
|
454
|
+
|
|
441
455
|
'',
|
|
442
456
|
'Before you guess at an API, ask: type_of gives the exact signature from the',
|
|
443
457
|
'TypeScript this project has installed, and find_symbol says where something is declared without',
|
|
@@ -493,7 +507,8 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
493
507
|
' writing files. Running the install yourself afterwards just waits for that one.',
|
|
494
508
|
'- When you finish, ucode type-checks what you changed and hands you the errors, so',
|
|
495
509
|
' there is no need to run tsc yourself.',
|
|
496
|
-
'-
|
|
510
|
+
'- Do not open or drive a browser. Checking the page in one is something the user',
|
|
511
|
+
' asks for with /look; your job is to leave the app in a state worth looking at.',
|
|
497
512
|
' reports - errors, layout that overflows a phone, the review points worth fixing -',
|
|
498
513
|
' in one pass, then look once more. A clean second look means it is done: report',
|
|
499
514
|
' back instead of polishing in circles. Never call an interface finished unlooked at.',
|
|
@@ -1367,9 +1382,13 @@ export class Agent {
|
|
|
1367
1382
|
reportFailure(call, err) {
|
|
1368
1383
|
if (!(err instanceof ToolFailure)) throw err;
|
|
1369
1384
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1385
|
+
// Bad arguments are the model talking to itself. "old_string and new_string
|
|
1386
|
+
// are identical" is a correction it will make on the next step, and it means
|
|
1387
|
+
// nothing to whoever is watching except that something went wrong. It goes
|
|
1388
|
+
// to the model, which can act on it, and not to the screen. A refusal the
|
|
1389
|
+
// user made, and anything that actually failed, still shows.
|
|
1390
|
+
if (err instanceof Declined) this.ui.toolFailed('declined');
|
|
1391
|
+
else if (err.kind !== 'bad_args') this.ui.toolFailed(`${err.kind}: ${err.failed}`);
|
|
1373
1392
|
this.push({
|
|
1374
1393
|
role: 'tool',
|
|
1375
1394
|
toolCallId: call.id,
|
|
@@ -1815,6 +1834,7 @@ export class Agent {
|
|
|
1815
1834
|
case '/copy': return this.cmdCopy();
|
|
1816
1835
|
case '/stats': return this.cmdStats();
|
|
1817
1836
|
case '/doctor': return this.cmdDoctor();
|
|
1837
|
+
case '/look': return this.cmdLook(arg);
|
|
1818
1838
|
case '/deploy': return this.cmdDeploy(arg);
|
|
1819
1839
|
case '/exit':
|
|
1820
1840
|
case '/quit': return 'exit';
|
|
@@ -1825,11 +1845,42 @@ export class Agent {
|
|
|
1825
1845
|
}
|
|
1826
1846
|
}
|
|
1827
1847
|
|
|
1848
|
+
/**
|
|
1849
|
+
* Look at the running app, because the user asked to.
|
|
1850
|
+
*
|
|
1851
|
+
* This used to happen on its own, which meant a browser being driven while
|
|
1852
|
+
* someone was reading, and a window taking the screen mid-thought. It is
|
|
1853
|
+
* the same check as before; the difference is who starts it.
|
|
1854
|
+
*/
|
|
1855
|
+
async cmdLook(url) {
|
|
1856
|
+
const { lookAtApp } = await import('../tools/browser.js');
|
|
1857
|
+
const server = runningServers().at(-1);
|
|
1858
|
+
const at = (url ?? '').trim() || server?.url;
|
|
1859
|
+
if (!at) {
|
|
1860
|
+
this.ui.write(theme.warn(' nothing is running to look at.'));
|
|
1861
|
+
this.ui.note('start the app first, or pass a URL: /look http://localhost:3000');
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
this.ui.toolCall(`Looking at ${at}`);
|
|
1865
|
+
try {
|
|
1866
|
+
const out = await lookAtApp({ url: at });
|
|
1867
|
+
this.ui.write(out.content);
|
|
1868
|
+
// The model gets it too, so the next thing it says is about what is
|
|
1869
|
+
// actually on the page rather than what it believes it built.
|
|
1870
|
+
this.push({ role: 'user', content: `I looked at ${at}. This is what is there:
|
|
1871
|
+
|
|
1872
|
+
${out.content}` });
|
|
1873
|
+
} catch (err) {
|
|
1874
|
+
this.ui.write(theme.error(` ${err.failed ?? err.message}`));
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1828
1878
|
cmdHelp() {
|
|
1829
1879
|
const rows = [
|
|
1830
1880
|
['/help', 'this list'],
|
|
1831
1881
|
['/stats', 'time, steps and tokens this session'],
|
|
1832
1882
|
['/doctor', 'check that everything ucode needs is working'],
|
|
1883
|
+
['/look [url]', 'open the running app and report what is on the page'],
|
|
1833
1884
|
['/deploy [folder]', 'put the app online and get its link'],
|
|
1834
1885
|
['/model', 'show the models and switch between them'],
|
|
1835
1886
|
['/resume', 'pick up an earlier conversation'],
|
package/src/tools/index.js
CHANGED
|
@@ -23,6 +23,37 @@ const str = (description) => ({ type: 'string', description });
|
|
|
23
23
|
const int = (description) => ({ type: 'integer', description });
|
|
24
24
|
const bool = (description) => ({ type: 'boolean', description });
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The browser check is deliberately not in `tools`.
|
|
28
|
+
*
|
|
29
|
+
* It used to run itself whenever a dev server came up, which meant a window
|
|
30
|
+
* opening mid-thought and a page being driven while the user was reading. It
|
|
31
|
+
* is now something asked for: /look runs it, and nothing else does.
|
|
32
|
+
*/
|
|
33
|
+
export const lookAtAppTool = {
|
|
34
|
+
name: 'look_at_app',
|
|
35
|
+
description:
|
|
36
|
+
'Open the running app in a real browser at a phone width (375px) and a desktop width ' +
|
|
37
|
+
'(1440px) and report what a person would run into: console errors, failed requests, ' +
|
|
38
|
+
'content that spills off the side of the screen, broken images, unlabeled buttons and ' +
|
|
39
|
+
'fields. The first look at an app also brings a designer-style review of the ' +
|
|
40
|
+
'screenshots; later looks re-run only the fast checks. Use it once the dev server is ' +
|
|
41
|
+
'ready, fix what it reports, then look once more to confirm. Screenshots are saved ' +
|
|
42
|
+
'under .ucode/screenshots.',
|
|
43
|
+
parameters: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
url: str('The local URL the dev server reported, e.g. http://localhost:3000'),
|
|
47
|
+
paths: {
|
|
48
|
+
type: 'array',
|
|
49
|
+
description: 'Pages to open, e.g. ["/", "/settings"]. Defaults to ["/"]. Up to 4.',
|
|
50
|
+
items: { type: 'string' },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
required: ['url'],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
26
57
|
export const tools = [
|
|
27
58
|
{
|
|
28
59
|
name: 'create_app',
|
|
@@ -416,29 +447,6 @@ export const tools = [
|
|
|
416
447
|
required: ['commands'],
|
|
417
448
|
},
|
|
418
449
|
},
|
|
419
|
-
{
|
|
420
|
-
name: 'look_at_app',
|
|
421
|
-
description:
|
|
422
|
-
'Open the running app in a real browser at a phone width (375px) and a desktop width ' +
|
|
423
|
-
'(1440px) and report what a person would run into: console errors, failed requests, ' +
|
|
424
|
-
'content that spills off the side of the screen, broken images, unlabeled buttons and ' +
|
|
425
|
-
'fields. The first look at an app also brings a designer-style review of the ' +
|
|
426
|
-
'screenshots; later looks re-run only the fast checks. Use it once the dev server is ' +
|
|
427
|
-
'ready, fix what it reports, then look once more to confirm. Screenshots are saved ' +
|
|
428
|
-
'under .ucode/screenshots.',
|
|
429
|
-
parameters: {
|
|
430
|
-
type: 'object',
|
|
431
|
-
properties: {
|
|
432
|
-
url: str('The local URL the dev server reported, e.g. http://localhost:3000'),
|
|
433
|
-
paths: {
|
|
434
|
-
type: 'array',
|
|
435
|
-
description: 'Pages to open, e.g. ["/", "/settings"]. Defaults to ["/"]. Up to 4.',
|
|
436
|
-
items: { type: 'string' },
|
|
437
|
-
},
|
|
438
|
-
},
|
|
439
|
-
required: ['url'],
|
|
440
|
-
},
|
|
441
|
-
},
|
|
442
450
|
{
|
|
443
451
|
name: 'web_search',
|
|
444
452
|
description:
|
|
@@ -578,28 +586,20 @@ export async function runTool(name, args = {}, opts = {}) {
|
|
|
578
586
|
export function describe(name, args = {}) {
|
|
579
587
|
switch (name) {
|
|
580
588
|
case 'read_file':
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
return names.length && joined.length <= 60 ? `Reading ${joined}` : `Reading ${names.length} files`;
|
|
586
|
-
}
|
|
589
|
+
case 'read_files':
|
|
590
|
+
// Which file is in the diff and in the result; the transcript only has to
|
|
591
|
+
// say what kind of work is going on, so a run of them folds into one line.
|
|
592
|
+
return 'Reading files';
|
|
587
593
|
case 'write_file':
|
|
588
|
-
return
|
|
589
|
-
case 'batch_write':
|
|
590
|
-
|
|
591
|
-
const first = args.files?.[0]?.path;
|
|
592
|
-
return n === 1 && first ? `Writing ${clip(first)}` : `Writing ${n} files`;
|
|
593
|
-
}
|
|
594
|
+
return 'Writing app';
|
|
595
|
+
case 'batch_write':
|
|
596
|
+
return 'Writing app';
|
|
594
597
|
case 'edit_file':
|
|
595
|
-
return
|
|
598
|
+
return 'Writing app';
|
|
596
599
|
case 'multi_edit':
|
|
597
|
-
return
|
|
598
|
-
case 'edit_files':
|
|
599
|
-
|
|
600
|
-
const first = args.files?.[0]?.path;
|
|
601
|
-
return n === 1 && first ? `Editing ${clip(first)}` : `Editing ${n} files`;
|
|
602
|
-
}
|
|
600
|
+
return 'Writing app';
|
|
601
|
+
case 'edit_files':
|
|
602
|
+
return 'Writing app';
|
|
603
603
|
case 'update_plan':
|
|
604
604
|
return 'Updating the plan';
|
|
605
605
|
case 'delegate':
|
package/src/ui/screen.js
CHANGED
|
@@ -39,7 +39,7 @@ import chalk from 'chalk';
|
|
|
39
39
|
import {
|
|
40
40
|
theme, blue, sky, deep, dim, edge, ADDED, REMOVED, BANNER, BANNER_WIDTH, SPINNER,
|
|
41
41
|
boxTop, boxBottom, boxRow, visLen, padVis, clip, wrapAnsi,
|
|
42
|
-
shortenPath, asLabel, ensureColour, planLine, bare, narration, narrationMark, groupKind, groupLabel, groupTarget, runLine } from './theme.js';
|
|
42
|
+
shortenPath, asLabel, ensureColour, planLine, bare, narration, narrationMark, groupKind, groupLabel, groupTarget, runLine, planRows } from './theme.js';
|
|
43
43
|
import { FRAME_MS, fitActivity, shimmer, spinnerGlyph, formatDuration, doneLine, stepPaint } from './activity.js';
|
|
44
44
|
import { renderer, render, polish } from './markdown.js';
|
|
45
45
|
import { VERSION } from '../core/version.js';
|
|
@@ -58,7 +58,7 @@ export function isLabel(text) {
|
|
|
58
58
|
export const COMMANDS = [
|
|
59
59
|
'/help', '/model', '/models', '/session', '/sessions', '/resume',
|
|
60
60
|
'/new', '/remember', '/skills', '/clear', '/search', '/copy', '/exit',
|
|
61
|
-
'/stats', '/doctor', '/deploy',
|
|
61
|
+
'/stats', '/doctor', '/deploy', '/look',
|
|
62
62
|
];
|
|
63
63
|
|
|
64
64
|
// ANSI ----------------------------------------------------------------------
|
|
@@ -297,40 +297,66 @@ export class Screen {
|
|
|
297
297
|
// U+25CF, not U+23FA: the latter carries emoji presentation, which Windows
|
|
298
298
|
// Terminal draws as a white circle on a blue tile.
|
|
299
299
|
const kind = groupKind(label);
|
|
300
|
-
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// rather than adding another almost-identical one beneath it.
|
|
308
|
-
if (run && run.kind === kind && gap <= 1) {
|
|
309
|
-
if (gap === 1) this.lines.pop(); // its single result line, now counted
|
|
300
|
+
// One line per kind of work for as long as the model is working on one
|
|
301
|
+
// thing. Reading, writing and reading again used to draw six lines that
|
|
302
|
+
// said three things; now the "Reading files" line it already has is the
|
|
303
|
+
// one that counts up, wherever it sits.
|
|
304
|
+
const run = (this.segment ??= new Map()).get(kind);
|
|
305
|
+
|
|
306
|
+
if (run && this.lines[run.at] !== undefined) {
|
|
310
307
|
run.count++;
|
|
311
308
|
run.label = label;
|
|
312
309
|
run.targets.push(groupTarget(label));
|
|
313
|
-
this.
|
|
310
|
+
this.run = run;
|
|
311
|
+
this.paintRun({ live: true });
|
|
314
312
|
} else {
|
|
315
313
|
this.push(`${narrationMark()} ${narration(asLabel(label))}`);
|
|
316
314
|
this.run = {
|
|
317
315
|
kind, count: 1, at: this.lines.length - 1, label,
|
|
318
316
|
targets: [groupTarget(label)], added: 0, removed: 0,
|
|
319
317
|
};
|
|
318
|
+
this.segment.set(kind, this.run);
|
|
319
|
+
this.paintRun({ live: true });
|
|
320
320
|
}
|
|
321
321
|
this.updateSpinner(label);
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
/**
|
|
325
|
-
|
|
324
|
+
/**
|
|
325
|
+
* The model speaking — or a plan, or a failure — ends the segment.
|
|
326
|
+
*
|
|
327
|
+
* Up to that point a kind of work keeps one line and counts up on it. After
|
|
328
|
+
* it, the next read is a new piece of work and deserves its own line, which
|
|
329
|
+
* is what makes the transcript read as a sequence of things done rather
|
|
330
|
+
* than a set of running totals.
|
|
331
|
+
*/
|
|
332
|
+
endRun() {
|
|
333
|
+
if (this.run) this.paintRun({ live: false });
|
|
334
|
+
this.run = null;
|
|
335
|
+
this.segment = new Map();
|
|
336
|
+
}
|
|
326
337
|
|
|
327
338
|
/** Redraw the run's single line from what it has accumulated. */
|
|
328
|
-
|
|
339
|
+
/**
|
|
340
|
+
* Redraw the run's single line from what it has accumulated.
|
|
341
|
+
*
|
|
342
|
+
* While its step is still running the text shimmers, which is the only
|
|
343
|
+
* thing on screen saying "this is happening now" once the per-step result
|
|
344
|
+
* lines are gone. It settles to plain dim the moment the step finishes, so
|
|
345
|
+
* the finished ones above stay quiet.
|
|
346
|
+
*/
|
|
347
|
+
paintRun({ live = this.run?.live } = {}) {
|
|
329
348
|
if (!this.run) return;
|
|
330
|
-
|
|
349
|
+
const text = asLabel(runLine(this.run));
|
|
350
|
+
this.run.live = live;
|
|
351
|
+
this.lines[this.run.at] = `${narrationMark()} ${live ? shimmer(text, this.tick * FRAME_MS) : narration(text)}`;
|
|
331
352
|
this.render();
|
|
332
353
|
}
|
|
333
354
|
|
|
355
|
+
/** Let the line in flight animate, one frame per tick. */
|
|
356
|
+
paintLiveRun() {
|
|
357
|
+
if (this.run?.live && this.lines[this.run.at] !== undefined) this.paintRun({ live: true });
|
|
358
|
+
}
|
|
359
|
+
|
|
334
360
|
/**
|
|
335
361
|
* A change, as its two numbers.
|
|
336
362
|
*
|
|
@@ -347,8 +373,10 @@ export class Screen {
|
|
|
347
373
|
|
|
348
374
|
/** The checklist, when the model updates it. One line, wrapped if it must. */
|
|
349
375
|
plan(items) {
|
|
350
|
-
const
|
|
351
|
-
if (
|
|
376
|
+
const rows = planRows(items);
|
|
377
|
+
if (!rows.length) return;
|
|
378
|
+
this.endRun(); // a plan is not another step of whatever came before
|
|
379
|
+
for (const row of rows) this.push(row);
|
|
352
380
|
}
|
|
353
381
|
|
|
354
382
|
/**
|
|
@@ -359,7 +387,10 @@ export class Screen {
|
|
|
359
387
|
* already names the step, and a change adds its numbers to that same line.
|
|
360
388
|
* Only a failure earns a line of its own.
|
|
361
389
|
*/
|
|
362
|
-
toolResult() {
|
|
390
|
+
toolResult() {
|
|
391
|
+
// The step is over: the line stops moving and joins the quiet ones above.
|
|
392
|
+
if (this.run) this.paintRun({ live: false });
|
|
393
|
+
}
|
|
363
394
|
|
|
364
395
|
toolFailed(summary) {
|
|
365
396
|
// A failure is never folded away.
|
|
@@ -508,7 +539,10 @@ export class Screen {
|
|
|
508
539
|
thinkingEnd() {
|
|
509
540
|
if (this.thoughtSince === undefined) return;
|
|
510
541
|
const seconds = Math.round((Date.now() - this.thoughtSince) / 1000);
|
|
511
|
-
|
|
542
|
+
// How long it thought is not what the reader is here for, and a line of it
|
|
543
|
+
// between every step broke every run of steps into singletons — which is
|
|
544
|
+
// why nothing folded. The time is still on the status row while it runs.
|
|
545
|
+
void seconds;
|
|
512
546
|
this.thoughtSince = undefined;
|
|
513
547
|
}
|
|
514
548
|
|
|
@@ -851,6 +885,7 @@ export class Screen {
|
|
|
851
885
|
if (this.spinTimer) return;
|
|
852
886
|
this.spinTimer = setInterval(() => {
|
|
853
887
|
this.tick++;
|
|
888
|
+
this.paintLiveRun();
|
|
854
889
|
this.paintStatus();
|
|
855
890
|
}, FRAME_MS);
|
|
856
891
|
this.spinTimer.unref?.();
|
package/src/ui/theme.js
CHANGED
|
@@ -263,18 +263,41 @@ export function asLabel(text) {
|
|
|
263
263
|
* The model's checklist, as one short line — done ticked, the current item
|
|
264
264
|
* marked, the rest dim — so progress is visible without taking over the screen.
|
|
265
265
|
*/
|
|
266
|
+
/**
|
|
267
|
+
* The plan, as a block rather than a sentence.
|
|
268
|
+
*
|
|
269
|
+
* Six steps joined with separators made one line far wider than any terminal,
|
|
270
|
+
* so it wrapped — and a wrapped checklist has its ticks in the middle of the
|
|
271
|
+
* text, which is unreadable. Down the page each step keeps its own row, its
|
|
272
|
+
* mark stays in the left column, and the eye can find the one in progress
|
|
273
|
+
* without reading any of the others.
|
|
274
|
+
*
|
|
275
|
+
* Returns the rows; the caller pushes them.
|
|
276
|
+
*/
|
|
277
|
+
export function planRows(items) {
|
|
278
|
+
const list = (Array.isArray(items) ? items : []).slice(0, 8);
|
|
279
|
+
if (!list.length) return [];
|
|
280
|
+
const done = list.filter((i) => i?.done).length;
|
|
281
|
+
const current = list.findIndex((i) => !i?.done);
|
|
282
|
+
|
|
283
|
+
const rows = [` ${sky(`plan ${done}/${list.length}`)}`];
|
|
284
|
+
list.forEach((item, i) => {
|
|
285
|
+
const text = clip(String(item?.text ?? '').trim(), 64);
|
|
286
|
+
if (item?.done) rows.push(` ${theme.ok('✓')} ${dim(text)}`);
|
|
287
|
+
else if (i === current) rows.push(` ${blue('▸')} ${chalk.white(text)}`);
|
|
288
|
+
else rows.push(` ${dim('○')} ${dim(text)}`);
|
|
289
|
+
});
|
|
290
|
+
return rows;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Kept for the plain interface, which has one line to work with. */
|
|
266
294
|
export function planLine(items) {
|
|
267
295
|
const list = (Array.isArray(items) ? items : []).slice(0, 6);
|
|
268
296
|
if (!list.length) return '';
|
|
269
297
|
const done = list.filter((i) => i?.done).length;
|
|
270
298
|
const current = list.findIndex((i) => !i?.done);
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
if (item?.done) return `${theme.ok('✓')} ${dim(text)}`;
|
|
274
|
-
if (i === current) return `${blue('▸')} ${chalk.white(text)}`;
|
|
275
|
-
return dim(`○ ${text}`);
|
|
276
|
-
});
|
|
277
|
-
return ` ${sky(`plan ${done}/${list.length}`)} ${parts.join(dim(' · '))}`;
|
|
299
|
+
const now = current === -1 ? 'done' : clip(String(list[current]?.text ?? '').trim(), 40);
|
|
300
|
+
return ` ${sky(`plan ${done}/${list.length}`)} ${chalk.white(now)}`;
|
|
278
301
|
}
|
|
279
302
|
|
|
280
303
|
/**
|