@knightcodeai/cli 0.19.0 → 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.
Files changed (57) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/bundle/chunks/anthropic-messages-5ZM7Z2XQ.js +6 -0
  3. package/dist/bundle/chunks/{azure-openai-responses-ZZQJJY7B.js → azure-openai-responses-OUS4VJNA.js} +1 -1
  4. package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
  5. package/dist/bundle/chunks/chunk-5ZES34QN.js +2 -0
  6. package/dist/bundle/chunks/{chunk-T47GKPON.js → chunk-6GRVIFTN.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-IIS4JDFO.js → chunk-6KWPS3LA.js} +23 -22
  8. package/dist/bundle/chunks/{chunk-R7X2RAGR.js → chunk-6X7IXVQH.js} +4 -4
  9. package/dist/bundle/chunks/{chunk-RZDCZFG7.js → chunk-B7A4GIBK.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-JSLSFYBX.js → chunk-BWGDAXFH.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-3QKQRXCZ.js → chunk-DFRYMT3C.js} +3 -3
  12. package/dist/bundle/chunks/{chunk-BTRAS32C.js → chunk-IIWY7U5S.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-OQHO44L6.js → chunk-J63X3FRT.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-KRDGPCZM.js → chunk-JMJS643J.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-C62RVFZT.js → chunk-L6QGGIZG.js} +2 -2
  16. package/dist/bundle/chunks/{chunk-AA6MLWWY.js → chunk-MZ5UVOTP.js} +1 -1
  17. package/dist/bundle/chunks/chunk-N5M3XNPN.js +2 -0
  18. package/dist/bundle/chunks/{chunk-GPHSXYPT.js → chunk-RKNFSQUG.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-2QXPCKUQ.js → chunk-VQBEVSZH.js} +1 -1
  20. package/dist/bundle/chunks/{cli-SIPOI5P3.js → cli-72YA573P.js} +1 -1
  21. package/dist/bundle/chunks/cloudflare-workers-ai-system-one-AWHNPEAT.js +2 -0
  22. package/dist/bundle/chunks/{easter-egg-3d-BVWLABZN.js → easter-egg-3d-XXPCDCZV.js} +1 -1
  23. package/dist/bundle/chunks/{execute-6UIHV2DR.js → execute-KWIUMPDE.js} +1 -1
  24. package/dist/bundle/chunks/{google-generative-ai-RFCCCXK6.js → google-generative-ai-HPLZ2ABU.js} +1 -1
  25. package/dist/bundle/chunks/{google-vertex-WN3JAZCD.js → google-vertex-E3UTTTIG.js} +1 -1
  26. package/dist/bundle/chunks/{mistral-conversations-GIGX6BW6.js → mistral-conversations-KT4C4XTZ.js} +1 -1
  27. package/dist/bundle/chunks/{openai-codex-responses-3Y7R752C.js → openai-codex-responses-BCLSSOJ6.js} +1 -1
  28. package/dist/bundle/chunks/openai-completions-GTNAEXX6.js +7 -0
  29. package/dist/bundle/chunks/{openai-decisions-MLI3SMFN.js → openai-decisions-CSQTW3S4.js} +1 -1
  30. package/dist/bundle/chunks/{openai-responses-BNRA5YTD.js → openai-responses-ILFXLMJW.js} +1 -1
  31. package/dist/bundle/chunks/{openrouter-images-JWVDFQJ4.js → openrouter-images-J66HXAMB.js} +1 -1
  32. package/dist/bundle/chunks/{runtime-AROIXIAV.js → runtime-OW5JKWBE.js} +1 -1
  33. package/dist/bundle/chunks/typesafe-system-one-QA5VXBEZ.js +2 -0
  34. package/dist/bundle/chunks/{virtual-modules-IYKQM2DL.js → virtual-modules-5DLTLPP7.js} +1 -1
  35. package/dist/bundle/cli-runtime.js +1 -1
  36. package/dist/core/export-html/template.css +9 -0
  37. package/dist/core/export-html/template.js +165 -45
  38. package/docs/app-server.md +66 -8
  39. package/docs/app-server.schema.json +6200 -1048
  40. package/docs/containerization.md +2 -2
  41. package/docs/declarative-views.md +118 -0
  42. package/docs/desktop.md +26 -0
  43. package/docs/docs.json +4 -0
  44. package/docs/extensions.md +4 -2
  45. package/docs/models-configuration.md +9 -0
  46. package/docs/models.md +8 -4
  47. package/examples/extensions/README.md +1 -0
  48. package/examples/extensions/desktop-view.ts +72 -0
  49. package/examples/extensions/gondolin/index.ts +8 -10
  50. package/examples/extensions/gondolin/package-lock.json +20 -30
  51. package/examples/extensions/gondolin/package.json +1 -1
  52. package/package.json +1 -1
  53. package/dist/bundle/chunks/anthropic-messages-34POCEPV.js +0 -6
  54. package/dist/bundle/chunks/chunk-2QXUEOVY.js +0 -2
  55. package/dist/bundle/chunks/cloudflare-workers-ai-system-one-QHP5M5HC.js +0 -2
  56. package/dist/bundle/chunks/openai-completions-HBXENZUJ.js +0 -7
  57. 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
- function sortChildren(node) {
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.unshift(current);
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 tree = buildTree();
160
- function mapNodes(node) {
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.forEach(mapNodes);
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
- function markActive(node) {
190
- let has = activePathIds.has(node.entry.id);
191
- for (const child of node.children) {
192
- if (markActive(child)) has = true;
193
- }
194
- containsActive.set(node, has);
195
- return has;
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.input + globalStats.cost.output + globalStats.cost.cacheRead + globalStats.cost.cacheWrite;
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
- // Render system prompt (user's base prompt, applies to all providers)
1442
- if (systemPrompt) {
1443
- const lines = systemPrompt.split('\n');
1444
- const previewLines = 10;
1445
- if (lines.length > previewLines) {
1446
- const preview = lines.slice(0, previewLines).join('\n');
1447
- const remaining = lines.length - previewLines;
1448
- html += `<div class="system-prompt expandable" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
1449
- <div class="system-prompt-header">System Prompt</div>
1450
- <div class="system-prompt-preview">${escapeHtml(preview)}</div>
1451
- <div class="system-prompt-expand-hint">... (${remaining} more lines, click to expand)</div>
1452
- <div class="system-prompt-full">${escapeHtml(systemPrompt)}</div>
1453
- </div>`;
1454
- } else {
1455
- html += `<div class="system-prompt">
1456
- <div class="system-prompt-header">System Prompt</div>
1457
- <div class="system-prompt-full" style="display: block">${escapeHtml(systemPrompt)}</div>
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
- if (tools && tools.length > 0) {
1463
- html += `<div class="tools-list">
1464
- <div class="tools-header">Available Tools</div>
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
  // ============================================================
@@ -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}`. A fork is named after its source with ` (fork)` appended. `thread/resume` also returns warnings; render them from the
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. Buffer queue/usage events
209
- while awaiting the response, install the snapshots (an empty queue clears stale
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`. Widgets, status, header, footer
311
- and custom components draw only in the terminal and do nothing here. Threads a
312
- terminal-mode connection loads have no dialogs yet.
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
 
@@ -544,6 +576,24 @@ and nothing is written. Built-in commands that only work in the terminal
544
576
  {"id":17,"method":"command/list","params":{"cwd":"/path/to/project"}}
545
577
  ```
546
578
 
579
+ ### `ui/event`
580
+
581
+ Params: `{threadId, widgetKey, id, value?}`. Result: `{}`. A press of the button
582
+ `id`, or a change of the field `id`, in the widget `widgetKey`'s view: `value` is
583
+ a string for a text or select field, a boolean for a toggle, and absent for a
584
+ button. Every handler of the thread gets `{widgetKey, id, value?}`; a handler
585
+ that fails becomes a `warning`. The extension answers by setting the widget
586
+ again: a field shows the view's value, not what was typed. A press of a widget or
587
+ control that is gone answers `{}` and reaches nobody.
588
+
589
+ Errors: `threadNotFound` when the thread is not loaded, and `invalidParams` when
590
+ the value does not fit the control (a toggle's string, a select value outside its
591
+ options, a button with a value).
592
+
593
+ ```json
594
+ {"id":18,"method":"ui/event","params":{"threadId":"THREAD_ID","widgetKey":"checks","id":"verbose","value":true}}
595
+ ```
596
+
547
597
  ### `thread/delete`
548
598
 
549
599
  Params: `{threadId}`. Result: `{}`. Permanently removes an archived transcript;
@@ -741,6 +791,14 @@ Limits count UTF-8 bytes:
741
791
  - 1 MiB per history/list response, including envelope reserve.
742
792
  - 64 KiB per inline turn/item; larger values use references.
743
793
  - 48 KiB per payload chunk before base64 encoding.
794
+ - 64 KiB per view in a tool result or an extension message, as serialized JSON;
795
+ a larger or invalid view is dropped and the raw output or text shows.
796
+ - 16 KiB per widget (its lines or view), 16 widgets and 50 statuses per thread;
797
+ status and widget keys of 1 to 128 characters; status text, title and working
798
+ message cut to 200 code points. A refused widget or status gives one
799
+ `warning` per key per load (a load warning when it happened while the thread
800
+ loaded).
801
+ - Views nest stacks and lists at most 8 levels deep.
744
802
 
745
803
  The in-repository `AppServerClient` supports typed `request`, `onNotification`,
746
804
  `onRequest`, `readPayload`, `resolveTurn`, and `resolveItem` calls. Its