@knightcodeai/cli 0.18.4 → 0.20.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/CHANGELOG.md +90 -0
- package/dist/bundle/chunks/anthropic-messages-5ZM7Z2XQ.js +6 -0
- package/dist/bundle/chunks/azure-openai-responses-OUS4VJNA.js +2 -0
- package/dist/bundle/chunks/bedrock-converse-stream.js +3 -3
- package/dist/bundle/chunks/chunk-5ZES34QN.js +2 -0
- package/dist/bundle/chunks/{chunk-3VVG5HD5.js → chunk-6GRVIFTN.js} +2 -2
- package/dist/bundle/chunks/{chunk-FG6M5B3F.js → chunk-6KWPS3LA.js} +159 -124
- package/dist/bundle/chunks/chunk-6X7IXVQH.js +30 -0
- package/dist/bundle/chunks/{chunk-T6M3FJYR.js → chunk-B7A4GIBK.js} +1 -1
- package/dist/bundle/chunks/{chunk-3B66IKOZ.js → chunk-BWGDAXFH.js} +1 -1
- package/dist/bundle/chunks/{chunk-MXIWLMNT.js → chunk-CCP7VAJU.js} +1 -1
- package/dist/bundle/chunks/{chunk-L2XRQUGK.js → chunk-DFRYMT3C.js} +4 -4
- package/dist/bundle/chunks/{chunk-NGWSYXD5.js → chunk-IIWY7U5S.js} +3 -3
- package/dist/bundle/chunks/{chunk-SLWTIEJK.js → chunk-J63X3FRT.js} +1 -1
- package/dist/bundle/chunks/{chunk-HRZGVTRM.js → chunk-JMJS643J.js} +1 -1
- package/dist/bundle/chunks/{chunk-GCXF7ZNR.js → chunk-L6QGGIZG.js} +4 -4
- package/dist/bundle/chunks/chunk-MZ5UVOTP.js +2 -0
- package/dist/bundle/chunks/chunk-N5M3XNPN.js +2 -0
- package/dist/bundle/chunks/{chunk-XILUO467.js → chunk-RKNFSQUG.js} +1 -1
- package/dist/bundle/chunks/chunk-UU4A4NV5.js +2 -0
- package/dist/bundle/chunks/{chunk-2QXPCKUQ.js → chunk-VQBEVSZH.js} +1 -1
- package/dist/bundle/chunks/{cli-AMSWLH7A.js → cli-72YA573P.js} +1 -1
- package/dist/bundle/chunks/cloudflare-workers-ai-system-one-AWHNPEAT.js +2 -0
- package/dist/bundle/chunks/{easter-egg-3d-VH6SGOZS.js → easter-egg-3d-XXPCDCZV.js} +1 -1
- package/dist/bundle/chunks/{execute-4UQWU4SD.js → execute-KWIUMPDE.js} +1 -1
- package/dist/bundle/chunks/github-copilot.js +1 -1
- package/dist/bundle/chunks/google-generative-ai-HPLZ2ABU.js +2 -0
- package/dist/bundle/chunks/google-vertex-E3UTTTIG.js +2 -0
- package/dist/bundle/chunks/knightcode-messages-57CV36UV.js +8 -0
- package/dist/bundle/chunks/{llama-cpp-classify-HRPRKOZA.js → llama-cpp-classify-FEWC5ZIH.js} +2 -2
- package/dist/bundle/chunks/{mistral-conversations-NUSSE6NX.js → mistral-conversations-KT4C4XTZ.js} +1 -1
- package/dist/bundle/chunks/openai-codex-responses-BCLSSOJ6.js +10 -0
- package/dist/bundle/chunks/openai-completions-GTNAEXX6.js +7 -0
- package/dist/bundle/chunks/{openai-decisions-ENPSYANO.js → openai-decisions-CSQTW3S4.js} +1 -1
- package/dist/bundle/chunks/openai-responses-ILFXLMJW.js +4 -0
- package/dist/bundle/chunks/openrouter-images-J66HXAMB.js +2 -0
- package/dist/bundle/chunks/{runtime-IU2Z5WWF.js → runtime-OW5JKWBE.js} +1 -1
- package/dist/bundle/chunks/typesafe-system-one-QA5VXBEZ.js +2 -0
- package/dist/bundle/chunks/{virtual-modules-AK4BYDR6.js → virtual-modules-5DLTLPP7.js} +1 -1
- package/dist/bundle/cli-runtime.js +1 -1
- package/dist/core/export-html/template.css +9 -0
- package/dist/core/export-html/template.js +165 -45
- package/docs/app-server.md +350 -12
- package/docs/app-server.schema.json +11493 -2563
- package/docs/containerization.md +2 -2
- package/docs/declarative-views.md +118 -0
- package/docs/desktop.md +45 -4
- package/docs/docs.json +4 -0
- package/docs/extensions.md +4 -2
- package/docs/models-configuration.md +9 -0
- package/docs/models.md +8 -4
- package/docs/settings.md +53 -0
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/desktop-view.ts +72 -0
- package/examples/extensions/gondolin/index.ts +8 -10
- package/examples/extensions/gondolin/package-lock.json +20 -30
- package/examples/extensions/gondolin/package.json +1 -1
- package/package.json +1 -1
- package/dist/bundle/chunks/anthropic-messages-VD2GN7C4.js +0 -6
- package/dist/bundle/chunks/azure-openai-responses-6IJ5JZ3Y.js +0 -2
- package/dist/bundle/chunks/chunk-3PSTVJEH.js +0 -2
- package/dist/bundle/chunks/chunk-4DOSUUX3.js +0 -30
- package/dist/bundle/chunks/chunk-CRLO4X52.js +0 -2
- package/dist/bundle/chunks/chunk-HHWP56PR.js +0 -2
- package/dist/bundle/chunks/cloudflare-workers-ai-system-one-JBMTTHVI.js +0 -2
- package/dist/bundle/chunks/google-generative-ai-QBWYWMCA.js +0 -2
- package/dist/bundle/chunks/google-vertex-4KVZWEAI.js +0 -2
- package/dist/bundle/chunks/knightcode-messages-3GNOTMSI.js +0 -8
- package/dist/bundle/chunks/openai-codex-responses-66UQNGWB.js +0 -10
- package/dist/bundle/chunks/openai-completions-QTYKORY2.js +0 -7
- package/dist/bundle/chunks/openai-responses-JSX72BKV.js +0 -3
- package/dist/bundle/chunks/openrouter-images-ZI7LLZ4Y.js +0 -2
- package/dist/bundle/chunks/typesafe-system-one-ETHWOYKC.js +0 -2
|
@@ -98,14 +98,12 @@
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
// Sort children by timestamp
|
|
102
|
-
|
|
101
|
+
// Sort children by timestamp. Iterative: long sessions form chains deeper than the JS call stack.
|
|
102
|
+
for (const node of nodeMap.values()) {
|
|
103
103
|
node.children.sort((a, b) =>
|
|
104
104
|
new Date(a.entry.timestamp).getTime() - new Date(b.entry.timestamp).getTime()
|
|
105
105
|
);
|
|
106
|
-
node.children.forEach(sortChildren);
|
|
107
106
|
}
|
|
108
|
-
roots.forEach(sortChildren);
|
|
109
107
|
|
|
110
108
|
return roots;
|
|
111
109
|
}
|
|
@@ -134,14 +132,14 @@
|
|
|
134
132
|
const path = [];
|
|
135
133
|
let current = byId.get(targetId);
|
|
136
134
|
while (current) {
|
|
137
|
-
path.
|
|
135
|
+
path.push(current);
|
|
138
136
|
// Stop if no parent or self-referencing (root)
|
|
139
137
|
if (!current.parentId || current.parentId === current.id) {
|
|
140
138
|
break;
|
|
141
139
|
}
|
|
142
140
|
current = byId.get(current.parentId);
|
|
143
141
|
}
|
|
144
|
-
return path;
|
|
142
|
+
return path.reverse();
|
|
145
143
|
}
|
|
146
144
|
|
|
147
145
|
// Tree node lookup for finding leaves
|
|
@@ -156,12 +154,12 @@
|
|
|
156
154
|
// Build tree node map lazily
|
|
157
155
|
if (!treeNodeMap) {
|
|
158
156
|
treeNodeMap = new Map();
|
|
159
|
-
const
|
|
160
|
-
|
|
157
|
+
const stack = [...buildTree()];
|
|
158
|
+
while (stack.length > 0) {
|
|
159
|
+
const node = stack.pop();
|
|
161
160
|
treeNodeMap.set(node.entry.id, node);
|
|
162
|
-
node.children.
|
|
161
|
+
for (const child of node.children) stack.push(child);
|
|
163
162
|
}
|
|
164
|
-
tree.forEach(mapNodes);
|
|
165
163
|
}
|
|
166
164
|
|
|
167
165
|
const node = treeNodeMap.get(nodeId);
|
|
@@ -185,16 +183,22 @@
|
|
|
185
183
|
const multipleRoots = roots.length > 1;
|
|
186
184
|
|
|
187
185
|
// Mark which subtrees contain the active leaf
|
|
186
|
+
// Iterative post-order: long sessions form chains deeper than the JS call stack.
|
|
188
187
|
const containsActive = new Map();
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
188
|
+
const postOrder = [];
|
|
189
|
+
const pending = [...roots];
|
|
190
|
+
while (pending.length > 0) {
|
|
191
|
+
const node = pending.pop();
|
|
192
|
+
postOrder.push(node);
|
|
193
|
+
for (const child of node.children) pending.push(child);
|
|
194
|
+
}
|
|
195
|
+
for (let i = postOrder.length - 1; i >= 0; i--) {
|
|
196
|
+
const node = postOrder[i];
|
|
197
|
+
containsActive.set(
|
|
198
|
+
node,
|
|
199
|
+
activePathIds.has(node.entry.id) || node.children.some(child => containsActive.get(child))
|
|
200
|
+
);
|
|
196
201
|
}
|
|
197
|
-
roots.forEach(markActive);
|
|
198
202
|
|
|
199
203
|
// Stack: [node, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild]
|
|
200
204
|
const stack = [];
|
|
@@ -313,6 +317,76 @@
|
|
|
313
317
|
return '';
|
|
314
318
|
}
|
|
315
319
|
|
|
320
|
+
// ============================================================
|
|
321
|
+
// SYSTEM MESSAGES (pure data)
|
|
322
|
+
// ============================================================
|
|
323
|
+
|
|
324
|
+
function systemContentText(content) {
|
|
325
|
+
if (typeof content === 'string') return content;
|
|
326
|
+
if (Array.isArray(content)) {
|
|
327
|
+
return content.filter(c => c.type === 'text' && c.text).map(c => c.text).join('\n');
|
|
328
|
+
}
|
|
329
|
+
return '';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Full prompt text of a system message, mirroring getSystemMessageText() in @knightcode/ai. */
|
|
333
|
+
function getSystemMessageText(msg) {
|
|
334
|
+
const parts = [systemContentText(msg.content)];
|
|
335
|
+
for (const value of Object.values(msg.sections || {})) {
|
|
336
|
+
if (value !== null) parts.push(value);
|
|
337
|
+
}
|
|
338
|
+
return parts.filter(part => part.length > 0).join('\n\n');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Text of a later system message, mirroring renderSystemMessageUpdate() in @knightcode/ai. */
|
|
342
|
+
function renderSystemMessageUpdate(msg) {
|
|
343
|
+
const parts = [];
|
|
344
|
+
const text = systemContentText(msg.content);
|
|
345
|
+
if (text.length > 0) parts.push(text);
|
|
346
|
+
for (const [name, value] of Object.entries(msg.sections || {})) {
|
|
347
|
+
parts.push(value === null
|
|
348
|
+
? `Removed system prompt section "${name}".`
|
|
349
|
+
: `Updated system prompt section "${name}":\n\n${value}`);
|
|
350
|
+
}
|
|
351
|
+
return parts.join('\n\n');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function carriesSystemState(entry) {
|
|
355
|
+
return (entry.type === 'message' && entry.message.role === 'system') ||
|
|
356
|
+
(entry.type === 'compaction' && entry.systemMessage);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Whether a system entry starts the prompt on its branch. Later system entries
|
|
361
|
+
* patch the prompt, so they render as updates instead of a full prompt.
|
|
362
|
+
*/
|
|
363
|
+
function isLeadingSystemEntry(entry) {
|
|
364
|
+
let current = entry.parentId && entry.parentId !== entry.id ? byId.get(entry.parentId) : undefined;
|
|
365
|
+
while (current) {
|
|
366
|
+
if (carriesSystemState(current)) return false;
|
|
367
|
+
if (!current.parentId || current.parentId === current.id) break;
|
|
368
|
+
current = byId.get(current.parentId);
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Short description of what a later system message changes, e.g. "project_context, -rules, tools +bash". */
|
|
374
|
+
function describeSystemUpdate(msg) {
|
|
375
|
+
const parts = [];
|
|
376
|
+
if (systemContentText(msg.content).length > 0) parts.push('instructions');
|
|
377
|
+
for (const [name, value] of Object.entries(msg.sections || {})) {
|
|
378
|
+
parts.push(value === null ? `-${name}` : name);
|
|
379
|
+
}
|
|
380
|
+
const toolChanges = [
|
|
381
|
+
...(msg.toolsAdded || []).map(t => `+${t.name}`),
|
|
382
|
+
...(msg.toolsRemoved || []).map(t => `-${t.name}`),
|
|
383
|
+
];
|
|
384
|
+
if (toolChanges.length > 0) parts.push(`tools ${toolChanges.join(' ')}`);
|
|
385
|
+
return parts.join(', ');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const hasSystemEntries = entries.some(entry => entry.type === 'message' && entry.message.role === 'system');
|
|
389
|
+
|
|
316
390
|
/**
|
|
317
391
|
* Parse a skill block from message text.
|
|
318
392
|
* Returns null if the text doesn't contain a skill block.
|
|
@@ -337,6 +411,13 @@
|
|
|
337
411
|
case 'message': {
|
|
338
412
|
const msg = entry.message;
|
|
339
413
|
parts.push(msg.role);
|
|
414
|
+
if (msg.role === 'system') {
|
|
415
|
+
parts.push(isLeadingSystemEntry(entry) ? 'system prompt' : 'system update');
|
|
416
|
+
parts.push(getSystemMessageText(msg));
|
|
417
|
+
for (const name of Object.keys(msg.sections || {})) parts.push(name);
|
|
418
|
+
for (const tool of [...(msg.toolsAdded || []), ...(msg.toolsRemoved || [])]) parts.push(tool.name);
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
340
421
|
if (msg.content) parts.push(extractContent(msg.content));
|
|
341
422
|
if (msg.role === 'bashExecution' && msg.command) parts.push(msg.command);
|
|
342
423
|
break;
|
|
@@ -675,6 +756,13 @@
|
|
|
675
756
|
const cmd = truncate(normalize(msg.command || ''));
|
|
676
757
|
return labelHtml + `<span class="tree-role-tool">[bash]:</span> ${escapeHtml(cmd)}`;
|
|
677
758
|
}
|
|
759
|
+
if (msg.role === 'system') {
|
|
760
|
+
if (isLeadingSystemEntry(entry)) {
|
|
761
|
+
return labelHtml + `<span class="tree-muted">[system prompt]</span>`;
|
|
762
|
+
}
|
|
763
|
+
const changes = describeSystemUpdate(msg);
|
|
764
|
+
return labelHtml + `<span class="tree-muted">[system update${changes ? ': ' + escapeHtml(truncate(changes)) : ''}]</span>`;
|
|
765
|
+
}
|
|
678
766
|
return labelHtml + `<span class="tree-muted">[${escapeHtml(msg.role)}]</span>`;
|
|
679
767
|
}
|
|
680
768
|
case 'compaction':
|
|
@@ -1327,6 +1415,24 @@
|
|
|
1327
1415
|
}
|
|
1328
1416
|
|
|
1329
1417
|
if (msg.role === 'toolResult') return '';
|
|
1418
|
+
|
|
1419
|
+
if (msg.role === 'system') {
|
|
1420
|
+
let html = '';
|
|
1421
|
+
if (isLeadingSystemEntry(entry)) {
|
|
1422
|
+
const text = getSystemMessageText(msg);
|
|
1423
|
+
if (text) html += renderSystemPromptBlock('System Prompt', text);
|
|
1424
|
+
html += renderToolsList('Available Tools', msg.toolsAdded);
|
|
1425
|
+
} else {
|
|
1426
|
+
const text = renderSystemMessageUpdate(msg);
|
|
1427
|
+
const changes = describeSystemUpdate(msg);
|
|
1428
|
+
if (text) html += renderSystemPromptBlock(`System Prompt Update${changes ? ': ' + changes : ''}`, text);
|
|
1429
|
+
html += renderToolsList('Tools Added', msg.toolsAdded);
|
|
1430
|
+
if (msg.toolsRemoved && msg.toolsRemoved.length > 0) {
|
|
1431
|
+
html += `<div class="tools-list"><div class="tools-header">Tools Removed</div><div class="tools-content">${msg.toolsRemoved.map(t => `<div class="tool-item"><span class="tool-item-name">${escapeHtml(t.name)}</span></div>`).join('')}</div></div>`;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return html ? `<div class="system-entry" id="${entryDomId}">${tsHtml}${html}</div>` : '';
|
|
1435
|
+
}
|
|
1330
1436
|
}
|
|
1331
1437
|
|
|
1332
1438
|
if (entry.type === 'model_change') {
|
|
@@ -1360,7 +1466,7 @@
|
|
|
1360
1466
|
let userMessages = 0, assistantMessages = 0, toolResults = 0;
|
|
1361
1467
|
let customMessages = 0, compactions = 0, branchSummaries = 0, toolCalls = 0;
|
|
1362
1468
|
const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
1363
|
-
const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
1469
|
+
const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
1364
1470
|
const models = new Set();
|
|
1365
1471
|
|
|
1366
1472
|
for (const entry of entryList) {
|
|
@@ -1376,10 +1482,18 @@
|
|
|
1376
1482
|
tokens.cacheRead += msg.usage.cacheRead || 0;
|
|
1377
1483
|
tokens.cacheWrite += msg.usage.cacheWrite || 0;
|
|
1378
1484
|
if (msg.usage.cost) {
|
|
1485
|
+
const componentTotal =
|
|
1486
|
+
(msg.usage.cost.input || 0) +
|
|
1487
|
+
(msg.usage.cost.output || 0) +
|
|
1488
|
+
(msg.usage.cost.cacheRead || 0) +
|
|
1489
|
+
(msg.usage.cost.cacheWrite || 0);
|
|
1379
1490
|
cost.input += msg.usage.cost.input || 0;
|
|
1380
1491
|
cost.output += msg.usage.cost.output || 0;
|
|
1381
1492
|
cost.cacheRead += msg.usage.cost.cacheRead || 0;
|
|
1382
1493
|
cost.cacheWrite += msg.usage.cost.cacheWrite || 0;
|
|
1494
|
+
// Provider-reported totals can intentionally differ from catalog-estimated components.
|
|
1495
|
+
// Fall back to the component sum for sessions written before cost.total was recorded.
|
|
1496
|
+
cost.total += typeof msg.usage.cost.total === 'number' ? msg.usage.cost.total : componentTotal;
|
|
1383
1497
|
}
|
|
1384
1498
|
}
|
|
1385
1499
|
toolCalls += msg.content.filter(c => c.type === 'toolCall').length;
|
|
@@ -1400,7 +1514,7 @@
|
|
|
1400
1514
|
const globalStats = computeStats(entries);
|
|
1401
1515
|
|
|
1402
1516
|
function renderHeader() {
|
|
1403
|
-
const totalCost = globalStats.cost.
|
|
1517
|
+
const totalCost = globalStats.cost.total;
|
|
1404
1518
|
|
|
1405
1519
|
const tokenParts = [];
|
|
1406
1520
|
if (globalStats.tokens.input) tokenParts.push(`↑${formatTokens(globalStats.tokens.input)}`);
|
|
@@ -1438,30 +1552,39 @@
|
|
|
1438
1552
|
</div>
|
|
1439
1553
|
</div>`;
|
|
1440
1554
|
|
|
1441
|
-
//
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
</div
|
|
1459
|
-
|
|
1555
|
+
// Sessions with system entries render the prompt and tools in the transcript, where they
|
|
1556
|
+
// occur. Older sessions have none, so fall back to the exporting agent's current state.
|
|
1557
|
+
if (!hasSystemEntries) {
|
|
1558
|
+
if (systemPrompt) html += renderSystemPromptBlock('System Prompt', systemPrompt);
|
|
1559
|
+
html += renderToolsList('Available Tools', tools);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
return html;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function renderSystemPromptBlock(title, text) {
|
|
1566
|
+
const lines = text.split('\n');
|
|
1567
|
+
const previewLines = 10;
|
|
1568
|
+
if (lines.length > previewLines) {
|
|
1569
|
+
const preview = lines.slice(0, previewLines).join('\n');
|
|
1570
|
+
const remaining = lines.length - previewLines;
|
|
1571
|
+
return `<div class="system-prompt expandable" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
|
|
1572
|
+
<div class="system-prompt-header">${escapeHtml(title)}</div>
|
|
1573
|
+
<div class="system-prompt-preview">${escapeHtml(preview)}</div>
|
|
1574
|
+
<div class="system-prompt-expand-hint">... (${remaining} more lines, click to expand)</div>
|
|
1575
|
+
<div class="system-prompt-full">${escapeHtml(text)}</div>
|
|
1576
|
+
</div>`;
|
|
1460
1577
|
}
|
|
1578
|
+
return `<div class="system-prompt">
|
|
1579
|
+
<div class="system-prompt-header">${escapeHtml(title)}</div>
|
|
1580
|
+
<div class="system-prompt-full" style="display: block">${escapeHtml(text)}</div>
|
|
1581
|
+
</div>`;
|
|
1582
|
+
}
|
|
1461
1583
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1584
|
+
function renderToolsList(title, tools) {
|
|
1585
|
+
if (!tools || tools.length === 0) return '';
|
|
1586
|
+
return `<div class="tools-list">
|
|
1587
|
+
<div class="tools-header">${escapeHtml(title)}</div>
|
|
1465
1588
|
<div class="tools-content">
|
|
1466
1589
|
${tools.map(t => {
|
|
1467
1590
|
const hasParams = t.parameters && typeof t.parameters === 'object' && t.parameters.properties && Object.keys(t.parameters.properties).length > 0;
|
|
@@ -1486,9 +1609,6 @@
|
|
|
1486
1609
|
}).join('')}
|
|
1487
1610
|
</div>
|
|
1488
1611
|
</div>`;
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
return html;
|
|
1492
1612
|
}
|
|
1493
1613
|
|
|
1494
1614
|
// ============================================================
|