@knightcodeai/cli 0.19.0 → 0.21.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/CHANGELOG.md +106 -0
- package/dist/bundle/chunks/anthropic-messages-EY2CGNFR.js +6 -0
- package/dist/bundle/chunks/azure-openai-responses-KGI5F27P.js +2 -0
- package/dist/bundle/chunks/bedrock-converse-stream.js +8 -5
- package/dist/bundle/chunks/chunk-424HQSAU.js +10 -0
- package/dist/bundle/chunks/{chunk-3QKQRXCZ.js → chunk-5O4LFJS7.js} +4 -4
- package/dist/bundle/chunks/chunk-5ZES34QN.js +2 -0
- package/dist/bundle/chunks/chunk-BLHRVYHG.js +9 -0
- package/dist/bundle/chunks/{chunk-RZDCZFG7.js → chunk-EJS73Z3L.js} +1 -1
- package/dist/bundle/chunks/chunk-F22TYE3W.js +7 -0
- package/dist/bundle/chunks/{chunk-C62RVFZT.js → chunk-FSAHEI7P.js} +6 -6
- package/dist/bundle/chunks/chunk-K6AEXKBA.js +30 -0
- package/dist/bundle/chunks/chunk-O2HKRIDR.js +16 -0
- package/dist/bundle/chunks/{chunk-KRDGPCZM.js → chunk-OLW2IS5G.js} +2 -2
- package/dist/bundle/chunks/chunk-QEVQ7EIS.js +2 -0
- package/dist/bundle/chunks/chunk-RRNUW65U.js +4 -0
- package/dist/bundle/chunks/{chunk-AA6MLWWY.js → chunk-TOEFTX4C.js} +1 -1
- package/dist/bundle/chunks/{chunk-IIS4JDFO.js → chunk-TOGJI36I.js} +147 -134
- package/dist/bundle/chunks/chunk-VLP4ZJF6.js +2 -0
- package/dist/bundle/chunks/{chunk-2QXPCKUQ.js → chunk-VQBEVSZH.js} +1 -1
- package/dist/bundle/chunks/chunk-XDEOVQPJ.js +108 -0
- package/dist/bundle/chunks/chunk-YDO6TXKD.js +2 -0
- package/dist/bundle/chunks/{cli-SIPOI5P3.js → cli-CIRIEG66.js} +1 -1
- package/dist/bundle/chunks/cloudflare-workers-ai-system-one-H3D4RYQQ.js +2 -0
- package/dist/bundle/chunks/{easter-egg-3d-BVWLABZN.js → easter-egg-3d-YLN6ACWS.js} +1 -1
- package/dist/bundle/chunks/{execute-6UIHV2DR.js → execute-P6HIB6ET.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-RFCCCXK6.js → google-generative-ai-C3FLXY47.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-WN3JAZCD.js → google-vertex-AXIPR47C.js} +1 -1
- package/dist/bundle/chunks/knightcode-messages-LTLKDRGW.js +8 -0
- package/dist/bundle/chunks/{llama-cpp-classify-FEWC5ZIH.js → llama-cpp-classify-J2EJVDIZ.js} +1 -1
- package/dist/bundle/chunks/mistral-conversations-N2UBIKKR.js +5 -0
- package/dist/bundle/chunks/{openai-codex-responses-3Y7R752C.js → openai-codex-responses-REOHLPAA.js} +1 -1
- package/dist/bundle/chunks/openai-completions-XONAGXPC.js +7 -0
- package/dist/bundle/chunks/{openai-decisions-MLI3SMFN.js → openai-decisions-ACB7F6SS.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-BNRA5YTD.js → openai-responses-OHJS7C73.js} +1 -1
- package/dist/bundle/chunks/{openrouter-images-JWVDFQJ4.js → openrouter-images-SMNBEOPV.js} +1 -1
- package/dist/bundle/chunks/{runtime-AROIXIAV.js → runtime-WNZGA67H.js} +1 -1
- package/dist/bundle/chunks/typesafe-system-one-KB2F2YGV.js +2 -0
- package/dist/bundle/chunks/{virtual-modules-IYKQM2DL.js → virtual-modules-EU3BUTGN.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 +168 -14
- package/docs/app-server.schema.json +7505 -1860
- package/docs/cli.md +3 -3
- package/docs/containerization.md +2 -2
- package/docs/custom-provider.md +2 -0
- package/docs/declarative-views.md +118 -0
- package/docs/desktop.md +109 -4
- package/docs/docs.json +4 -0
- package/docs/environment-variables.md +1 -0
- package/docs/extensions.md +7 -3
- package/docs/keybindings.md +1 -0
- package/docs/mcp.md +16 -2
- package/docs/message-types.md +1 -1
- package/docs/models-configuration.md +9 -0
- package/docs/models.md +8 -4
- package/docs/packages.md +2 -0
- package/docs/plan-mode.md +4 -0
- package/docs/settings.md +3 -3
- package/docs/slash-commands.md +2 -0
- package/docs/usage.md +23 -0
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/custom-provider-anthropic/index.ts +27 -31
- 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/examples/extensions/todo.ts +3 -0
- package/examples/mcp-app/server.mjs +177 -0
- package/package.json +1 -1
- package/dist/bundle/chunks/anthropic-messages-34POCEPV.js +0 -6
- package/dist/bundle/chunks/azure-openai-responses-ZZQJJY7B.js +0 -2
- package/dist/bundle/chunks/chunk-2QXUEOVY.js +0 -2
- package/dist/bundle/chunks/chunk-4CRI7BMY.js +0 -2
- package/dist/bundle/chunks/chunk-BTRAS32C.js +0 -7
- package/dist/bundle/chunks/chunk-FA32X2UJ.js +0 -7
- package/dist/bundle/chunks/chunk-GPHSXYPT.js +0 -2
- package/dist/bundle/chunks/chunk-JSLSFYBX.js +0 -108
- package/dist/bundle/chunks/chunk-OQHO44L6.js +0 -16
- package/dist/bundle/chunks/chunk-R7X2RAGR.js +0 -30
- package/dist/bundle/chunks/chunk-T47GKPON.js +0 -11
- package/dist/bundle/chunks/chunk-UU4A4NV5.js +0 -2
- package/dist/bundle/chunks/cloudflare-workers-ai-system-one-QHP5M5HC.js +0 -2
- package/dist/bundle/chunks/knightcode-messages-57CV36UV.js +0 -8
- package/dist/bundle/chunks/mistral-conversations-GIGX6BW6.js +0 -5
- package/dist/bundle/chunks/openai-completions-HBXENZUJ.js +0 -7
- package/dist/bundle/chunks/typesafe-system-one-5LELZY2F.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
|
// ============================================================
|
package/docs/app-server.md
CHANGED
|
@@ -63,7 +63,8 @@ Start a thread with its project directory:
|
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Wait for each response. `thread/start` and `thread/fork` return
|
|
66
|
-
`{thread, warnings}
|
|
66
|
+
`{thread, warnings, ui}` (`ui` is the thread's extension UI, see
|
|
67
|
+
[Dialogs and extension UI](#dialogs-and-extension-ui)). A fork is named after its source with ` (fork)` appended. `thread/resume` also returns warnings; render them from the
|
|
67
68
|
response, because they can occur before subscription. A warning has
|
|
68
69
|
`{message, source, path?, line?, column?}`, where `source` is `settings`,
|
|
69
70
|
`extension`, `trust`, or `server`. Malformed settings, refused extension loads,
|
|
@@ -205,9 +206,9 @@ Desktop OS notifications are for live outcomes, not initial history.
|
|
|
205
206
|
`thread/resume` returns history and subscribes in the same synchronous send step.
|
|
206
207
|
Its separate `runningTurn` is a current snapshot, even when an old history cursor
|
|
207
208
|
is supplied. Its required `queue` and `usage` are also live send-time snapshots,
|
|
208
|
-
not values from handler entry or the history cursor
|
|
209
|
-
while awaiting the response, install the snapshots (an
|
|
210
|
-
state), then replay the buffered events. Invalid cursors subscribe nobody.
|
|
209
|
+
not values from handler entry or the history cursor; so is `ui`. Buffer
|
|
210
|
+
queue/usage/`ui/*` events while awaiting the response, install the snapshots (an
|
|
211
|
+
empty queue clears stale state), then replay the buffered events. Invalid cursors subscribe nobody.
|
|
211
212
|
Install `runningTurn` before applying buffered deltas. `thread/replaced` moves
|
|
212
213
|
subscriptions to the new thread; captured old extension contexts become stale.
|
|
213
214
|
`thread/unsubscribe` returns `{status: "unsubscribed" | "notSubscribed"}`.
|
|
@@ -285,7 +286,7 @@ The most recent initiating subscriber is asked first. Disconnect or unsubscribe
|
|
|
285
286
|
cancels that attempt and migrates it to another subscriber; no subscribers means
|
|
286
287
|
cancel. `serverRequest/cancelled` withdraws an outstanding request.
|
|
287
288
|
|
|
288
|
-
## Dialogs
|
|
289
|
+
## Dialogs and extension UI
|
|
289
290
|
|
|
290
291
|
What KnightCode asks the user mid-turn reaches a desktop connection as a server
|
|
291
292
|
request, asked the way approvals are; the thread reports `waitingOn: ["dialog"]`
|
|
@@ -307,9 +308,40 @@ risky call and plan mode's review. `kind` says which: a select carries
|
|
|
307
308
|
`{"confirmed": true}` for a confirm, or `{}` to cancel.
|
|
308
309
|
|
|
309
310
|
Notices such as "Downloaded model" arrive as the notification `ui/notify` with
|
|
310
|
-
a `level` of `info`, `warning` or `error`.
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
a `level` of `info`, `warning` or `error`.
|
|
312
|
+
|
|
313
|
+
What an extension shows beside the transcript is the thread's extension UI:
|
|
314
|
+
|
|
315
|
+
- The `ui` of the `thread/start`, `thread/resume` and `thread/fork` results is
|
|
316
|
+
all of it as it stands when the response is written:
|
|
317
|
+
`{statuses: [{key, text}], title?, workingMessage?, workingVisible, widgets}`.
|
|
318
|
+
Statuses are sorted by key. Each widget is `{key, placement, lines}` or
|
|
319
|
+
`{key, placement, view}`, `placement` being `aboveEditor` or `belowEditor`;
|
|
320
|
+
those above come first, each group in the order they were last set.
|
|
321
|
+
- `ui/state` carries every field but the widgets, whole, whenever one changes.
|
|
322
|
+
An absent `title` or `workingMessage` is a cleared one.
|
|
323
|
+
`workingVisible: false` asks the client to hide its waiting indicator, and
|
|
324
|
+
`workingMessage` replaces its text.
|
|
325
|
+
- `ui/widget` carries one widget set or replaced (it moves to the end of its
|
|
326
|
+
placement), or `{threadId, key}` alone when it is removed.
|
|
327
|
+
|
|
328
|
+
Install `ui` from the result, then apply the notifications, buffered across a
|
|
329
|
+
resume as `queue` is. A `/reload` clears everything the old extensions showed,
|
|
330
|
+
with one `ui/state` and a removal per widget, before the new ones start. The
|
|
331
|
+
server strips ANSI escapes and keeps text on one line, at most 200 code points;
|
|
332
|
+
widget lines keep their spacing. A [view](declarative-views.md) is data, never
|
|
333
|
+
HTML; the server checked it before sending.
|
|
334
|
+
|
|
335
|
+
A button or field in a widget's view answers with the request `ui/event`
|
|
336
|
+
(below), which reaches every handler the thread's extensions registered with
|
|
337
|
+
`ctx.ui.onViewEvent`. Views on `toolCall` and `extensionMessage` items (their
|
|
338
|
+
optional `view`) are for display only.
|
|
339
|
+
|
|
340
|
+
Widget factories, `custom()` components, header and footer components, raw
|
|
341
|
+
terminal input and the working indicator draw only in the terminal: the server
|
|
342
|
+
writes one line to its log per method per thread load and draws nothing. Editor
|
|
343
|
+
access (`setEditorText`, `getEditorText`, `pasteToEditor`) does nothing.
|
|
344
|
+
Threads a terminal-mode connection loads have no dialogs or extension UI yet.
|
|
313
345
|
|
|
314
346
|
## Workspaces, diffs and git
|
|
315
347
|
|
|
@@ -340,8 +372,12 @@ file as `{path, additions, deletions}`: the net line totals between the working
|
|
|
340
372
|
tree before and after the turn. A read-back turn carries the same list as
|
|
341
373
|
`Turn.files`. This
|
|
342
374
|
means "changed while the turn ran", not "changed by the turn": an edit another
|
|
343
|
-
local thread or an editor made meanwhile is in the list too. A worktree
|
|
344
|
-
is
|
|
375
|
+
local thread or an editor made meanwhile is in the list too. A worktree
|
|
376
|
+
thread's list is its own unless another thread runs in the same worktree (a
|
|
377
|
+
start whose `cwd` is inside it), whose edits it can include. Untracked files
|
|
378
|
+
count up to 500; past that, those under a folder holding 200 or more untracked
|
|
379
|
+
files are left out, and if more than 500 still remain before or after the
|
|
380
|
+
turn, only tracked files count.
|
|
345
381
|
|
|
346
382
|
Review comments are drafts the client keeps. They travel with the next
|
|
347
383
|
`turn/start` or `turn/queue/add` as `comments`; the server quotes the commented
|
|
@@ -470,6 +506,36 @@ Connections, sign-in and status belong to the MCP extension in each thread
|
|
|
470
506
|
{"id":28,"method":"mcp/add","params":{"scope":"global","name":"docs","config":{"url":"https://example.com/mcp"}}}
|
|
471
507
|
```
|
|
472
508
|
|
|
509
|
+
### `mcp/app/readResource` and `mcp/app/callTool`
|
|
510
|
+
|
|
511
|
+
MCP Apps ([MCP](mcp.md#mcp-apps)): a client that draws apps sends `desktop` as
|
|
512
|
+
its mode; the thread's MCP servers then hear the client supports
|
|
513
|
+
`io.modelcontextprotocol/ui`. A call to a tool with an app carries
|
|
514
|
+
`mcpApp: {server, resourceUri, result?, resultOmitted?}` on its `toolCall`
|
|
515
|
+
item, live and read back: `result` is the server's whole `CallToolResult`
|
|
516
|
+
(with `_meta` and `structuredContent`), or `resultOmitted: true` past 256 KiB
|
|
517
|
+
serialized, when the app gets the item's text output instead.
|
|
518
|
+
|
|
519
|
+
`mcp/app/readResource {threadId, server, uri}` returns the server's
|
|
520
|
+
`ReadResourceResult` `{contents, _meta?}`, the app's HTML first.
|
|
521
|
+
`mcp/app/callTool {threadId, server, name, arguments?, confirmed?}` returns its
|
|
522
|
+
`CallToolResult` `{content, structuredContent?, isError?, _meta?}`. Both reach
|
|
523
|
+
only the named server, through the thread's MCP extension, and only while it is
|
|
524
|
+
connected (`mcpUnavailable` otherwise); a call reaches only a tool whose
|
|
525
|
+
`_meta.ui.visibility` includes `"app"` and that the configuration does not hide
|
|
526
|
+
(`invalidParams` otherwise). Outside `fullAccess`, a tool not annotated
|
|
527
|
+
`readOnlyHint` answers `approvalRequired` (`data: {server, tool}`) until sent
|
|
528
|
+
again with `confirmed: true` after the user agreed. Arguments over 1 MiB and a
|
|
529
|
+
resource over 4 MiB, serialized, answer `invalidParams`; a tool's result over
|
|
530
|
+
4 MiB is left out of an answer whose text says the tool ran, since it did. A call
|
|
531
|
+
during a turn runs
|
|
532
|
+
at once; the server gives the MCP extension 60 seconds to answer a resource read
|
|
533
|
+
(`internal` after that), and a tool call as long as the server's own `timeout`.
|
|
534
|
+
|
|
535
|
+
```json
|
|
536
|
+
{"id":29,"method":"mcp/app/callTool","params":{"threadId":"THREAD_ID","server":"counter","name":"increment","arguments":{"by":1},"confirmed":true}}
|
|
537
|
+
```
|
|
538
|
+
|
|
473
539
|
### `resource/list` and `resource/set`
|
|
474
540
|
|
|
475
541
|
Extensions, skills, prompt templates and themes as `knightcode config` lists them.
|
|
@@ -544,6 +610,24 @@ and nothing is written. Built-in commands that only work in the terminal
|
|
|
544
610
|
{"id":17,"method":"command/list","params":{"cwd":"/path/to/project"}}
|
|
545
611
|
```
|
|
546
612
|
|
|
613
|
+
### `ui/event`
|
|
614
|
+
|
|
615
|
+
Params: `{threadId, widgetKey, id, value?}`. Result: `{}`. A press of the button
|
|
616
|
+
`id`, or a change of the field `id`, in the widget `widgetKey`'s view: `value` is
|
|
617
|
+
a string for a text or select field, a boolean for a toggle, and absent for a
|
|
618
|
+
button. Every handler of the thread gets `{widgetKey, id, value?}`; a handler
|
|
619
|
+
that fails becomes a `warning`. The extension answers by setting the widget
|
|
620
|
+
again: a field shows the view's value, not what was typed. A press of a widget or
|
|
621
|
+
control that is gone answers `{}` and reaches nobody.
|
|
622
|
+
|
|
623
|
+
Errors: `threadNotFound` when the thread is not loaded, and `invalidParams` when
|
|
624
|
+
the value does not fit the control (a toggle's string, a select value outside its
|
|
625
|
+
options, a button with a value).
|
|
626
|
+
|
|
627
|
+
```json
|
|
628
|
+
{"id":18,"method":"ui/event","params":{"threadId":"THREAD_ID","widgetKey":"checks","id":"verbose","value":true}}
|
|
629
|
+
```
|
|
630
|
+
|
|
547
631
|
### `thread/delete`
|
|
548
632
|
|
|
549
633
|
Params: `{threadId}`. Result: `{}`. Permanently removes an archived transcript;
|
|
@@ -578,7 +662,7 @@ Diagnostics observe the shared server, not only this connection.
|
|
|
578
662
|
### `thread/diff/read`
|
|
579
663
|
|
|
580
664
|
Params: `{threadId, scope?, against?, commit?, context?, ignoreWhitespace?}`.
|
|
581
|
-
Result: `{scope, root, files, filesOmitted?, untrackedSkipped?, turnId?, stale?, against?}`.
|
|
665
|
+
Result: `{scope, root, files, filesOmitted?, untrackedSkipped?, turnId?, stale?, against?, fromTree?, toTree?}`.
|
|
582
666
|
`files` are `{path, oldPath?, status, binary, additions, deletions, tooLarge?, hunks}`,
|
|
583
667
|
their paths relative to the repository's `root` (not the thread's cwd, which can
|
|
584
668
|
be a subdirectory of it); each hunk keeps its lines with their `" "`, `"+"`, `"-"` or
|
|
@@ -590,7 +674,9 @@ counted (`filesOmitted`). A hunk id hashes the file's header (its blob ids) and
|
|
|
590
674
|
the hunk at the read's `context`: an action must pass the same `context` (a
|
|
591
675
|
`context: 0` id matches no action), and after an action changes a file, read it
|
|
592
676
|
again, as its other hunks' ids change too. A hunk id from an `ignoreWhitespace` read matches no
|
|
593
|
-
action. Reads take no lock and never write the user's index.
|
|
677
|
+
action. Reads take no lock and never write the user's index. Unless
|
|
678
|
+
`ignoreWhitespace` is set, the result also names the two trees it compared,
|
|
679
|
+
`fromTree` and `toTree`, for `thread/diff/file`.
|
|
594
680
|
Errors: `threadNotFound`, `notGitRepo`; `invalidParams` for a missing `commit`
|
|
595
681
|
or no branch to compare with.
|
|
596
682
|
|
|
@@ -598,6 +684,60 @@ or no branch to compare with.
|
|
|
598
684
|
{"id":20,"method":"thread/diff/read","params":{"threadId":"THREAD_ID","scope":"uncommitted","context":3}}
|
|
599
685
|
```
|
|
600
686
|
|
|
687
|
+
### `thread/diff/file`
|
|
688
|
+
|
|
689
|
+
Params: `{threadId, fromTree, toTree, path, oldPath?}`. Result:
|
|
690
|
+
`{old?, new?, binary, tooLarge}`. Both sides of one file from a
|
|
691
|
+
`thread/diff/read` result, by the trees it named, so a client can show the
|
|
692
|
+
unchanged lines between its hunks. `old` is the file in `fromTree` at `oldPath`
|
|
693
|
+
(else `path`), `new` the file in `toTree` at `path`; a side the file is absent
|
|
694
|
+
from is left out. Neither side comes when the file is binary (a NUL in its first
|
|
695
|
+
8 KiB) or either side is larger than 2 MiB. The trees stay readable after the
|
|
696
|
+
working tree moves on, so the sides always match the read's hunks.
|
|
697
|
+
Errors: `threadNotFound`, `notGitRepo`; `invalidParams` when a tree is not a
|
|
698
|
+
full object id of a tree in the repository, or the file is in neither tree.
|
|
699
|
+
|
|
700
|
+
```json
|
|
701
|
+
{"id":24,"method":"thread/diff/file","params":{"threadId":"THREAD_ID","fromTree":"FROM_TREE","toTree":"TO_TREE","path":"src/a.ts"}}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### `file/read`
|
|
705
|
+
|
|
706
|
+
Params: `{threadId, path, maxBytes?}`. Result: `{path, size, binary, tooLarge,
|
|
707
|
+
text?, mtimeMs}`. A file's text, `path` resolved against the thread's cwd like a
|
|
708
|
+
tool's path. `maxBytes` is 1 byte to 8 MiB (default 2 MiB). `text` comes only
|
|
709
|
+
when the file is neither binary (a NUL in its first 8 KiB, or bytes that are not
|
|
710
|
+
UTF-8, which decoded and saved would be replaced) nor larger than
|
|
711
|
+
`maxBytes`; `size` is its length in bytes; `mtimeMs` its modification time when
|
|
712
|
+
read, for `file/write`.
|
|
713
|
+
Errors: `threadNotFound`; `invalidParams` when there is no such file or the path
|
|
714
|
+
is not a file.
|
|
715
|
+
|
|
716
|
+
```json
|
|
717
|
+
{"id":25,"method":"file/read","params":{"threadId":"THREAD_ID","path":"src/a.ts"}}
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### `file/list`
|
|
721
|
+
|
|
722
|
+
Params: `{threadId, path?}`. Result: `{root, entries, truncated}`. One folder's
|
|
723
|
+
entries for a file tree: `path` resolved like `file/read`'s, the thread's cwd
|
|
724
|
+
when absent; `root` is the cwd and each entry `{path, type}` is relative to it,
|
|
725
|
+
`/`-separated, `type` `file` or `directory` (a link to a folder is a folder).
|
|
726
|
+
Hidden entries are included. Past 5,000 entries the rest are left out and
|
|
727
|
+
`truncated` is true.
|
|
728
|
+
Errors: `threadNotFound`; `invalidParams` when the path is not a folder or
|
|
729
|
+
cannot be read.
|
|
730
|
+
|
|
731
|
+
### `file/write`
|
|
732
|
+
|
|
733
|
+
Params: `{threadId, path, text, expectedMtimeMs?}`. Result: `{mtimeMs}`. Writes
|
|
734
|
+
`text` (UTF-8, at most 8 MiB) to the file in place, so it keeps its mode and
|
|
735
|
+
links; a missing file is created. With `expectedMtimeMs` (the `mtimeMs` of the
|
|
736
|
+
read the text came from), a file modified since is refused with `fileChanged`
|
|
737
|
+
and left as it is.
|
|
738
|
+
Errors: `threadNotFound`; `fileChanged`; `invalidParams` when the path is a
|
|
739
|
+
folder or cannot be written.
|
|
740
|
+
|
|
601
741
|
### `thread/diff/revert`
|
|
602
742
|
|
|
603
743
|
Params: `{threadId, scope?, against?, commit?, path?, hunkId?, context?, reapply?, turnId?}`.
|
|
@@ -619,7 +759,7 @@ path outside the diff; `gitConflict` with `diffChanged` or `patchFailed`.
|
|
|
619
759
|
|
|
620
760
|
### `thread/handoff`
|
|
621
761
|
|
|
622
|
-
Params: `{threadId, to: "local" | "worktree", branch?, environment?}`. Result:
|
|
762
|
+
Params: `{threadId, to: "local" | "worktree", branch?, environment?, dryRun?}`. Result:
|
|
623
763
|
`{thread, branch?, warnings}`. To the main checkout: the worktree's `HEAD`
|
|
624
764
|
becomes a branch checked out there (the worktree's own branch, else `branch`,
|
|
625
765
|
else the previous handoff's branch fast-forwarded, else
|
|
@@ -630,6 +770,10 @@ checkout gets a worktree. The changes are saved under `<agentDir>/handoff/`
|
|
|
630
770
|
before anything moves and leave the source only after they landed. The thread
|
|
631
771
|
is reloaded where it now runs: subscribers receive `thread/replaced` with the
|
|
632
772
|
same id, and queued inputs follow it.
|
|
773
|
+
With `dryRun`, the server runs every check the handoff would and answers with
|
|
774
|
+
the same refusal, or with the unchanged thread and the branch the main checkout
|
|
775
|
+
would get; it writes nothing, creates no worktree, and never holds up a real
|
|
776
|
+
handoff.
|
|
633
777
|
Errors: `turnInProgress`; `invalidParams` for a thread with no message yet or
|
|
634
778
|
already on that side; `gitConflict` with `localChanges`,
|
|
635
779
|
`operationInProgress`, `branchExists`, `recoveryRequired` (with `data.patch`)
|
|
@@ -684,7 +828,9 @@ it (names unique, at most 50). An untrusted project's scripts never run. See
|
|
|
684
828
|
|
|
685
829
|
Each acts on the thread's own checkout: its worktree, or the user's checkout.
|
|
686
830
|
`git/status` → branch, upstream, ahead/behind, staged/unstaged/untracked/
|
|
687
|
-
conflicted counts and an operation in progress. `git/
|
|
831
|
+
conflicted counts and an operation in progress. `git/origin` → the checkout's
|
|
832
|
+
`root`, the `url` of its origin remote (else its first remote) and the `branch`
|
|
833
|
+
checked out, each absent when there is none. `git/stage` and `git/unstage`
|
|
688
834
|
take `{threadId, path?, hunkId?, context?}`: no path is everything; paths are
|
|
689
835
|
literal. `git/commit {threadId, message?, includeUnstaged?}` stages everything
|
|
690
836
|
first unless `includeUnstaged: false`; an empty message is written by the
|
|
@@ -741,6 +887,14 @@ Limits count UTF-8 bytes:
|
|
|
741
887
|
- 1 MiB per history/list response, including envelope reserve.
|
|
742
888
|
- 64 KiB per inline turn/item; larger values use references.
|
|
743
889
|
- 48 KiB per payload chunk before base64 encoding.
|
|
890
|
+
- 64 KiB per view in a tool result or an extension message, as serialized JSON;
|
|
891
|
+
a larger or invalid view is dropped and the raw output or text shows.
|
|
892
|
+
- 16 KiB per widget (its lines or view), 16 widgets and 50 statuses per thread;
|
|
893
|
+
status and widget keys of 1 to 128 characters; status text, title and working
|
|
894
|
+
message cut to 200 code points. A refused widget or status gives one
|
|
895
|
+
`warning` per key per load (a load warning when it happened while the thread
|
|
896
|
+
loaded).
|
|
897
|
+
- Views nest stacks and lists at most 8 levels deep.
|
|
744
898
|
|
|
745
899
|
The in-repository `AppServerClient` supports typed `request`, `onNotification`,
|
|
746
900
|
`onRequest`, `readPayload`, `resolveTurn`, and `resolveItem` calls. Its
|