@akanjs/cli 3.0.0-beta.2 → 3.0.0-beta.21

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 (190) hide show
  1. package/.build-stamp +1 -1
  2. package/CodeAgent-q2m47rqp.js +9 -0
  3. package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
  4. package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
  5. package/CodeTui-pdqhc3sn.js +1457 -0
  6. package/README.md +3 -0
  7. package/TunnelShare-scf4e4pd.js +9 -0
  8. package/{agent.command-d2gfxfck.js → agent.command-ttn09rx3.js} +8 -9
  9. package/akanCode.js +102 -0
  10. package/application.command-7gweenv7.js +179 -0
  11. package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-38exmejd.js} +63 -64
  12. package/buildBatch.proc.js +26 -25
  13. package/cloud.command-bhztw7c2.js +95 -0
  14. package/cloud.script-aabyhgba.js +20 -0
  15. package/code.command-6s8c0k91.js +154 -0
  16. package/commandManifest.json +1 -1
  17. package/consoleToStderr-h5bryj94.js +7 -0
  18. package/context.command-phmgywt2.js +48 -0
  19. package/context.runner-yvyw5g2d.js +21 -0
  20. package/{dependencyScanner-shs82ynp.js → dependencyScanner-ye2e6pdz.js} +2 -2
  21. package/{devTui-40ggmd99.js → devTui-q7k7bakd.js} +122 -40
  22. package/fileSys-5ngz93ga.js +9 -0
  23. package/guideline.command-7jk47fgq.js +42 -0
  24. package/guidelines/agentRule/agentRule.instruction.md +209 -28
  25. package/guidelines/conventions/conventions.instruction.md +96 -29
  26. package/guidelines/fieldRule/fieldRule.instruction.md +1 -0
  27. package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
  28. package/guidelines/queryRule/queryRule.instruction.md +17 -7
  29. package/guidelines/runtimeRule/runtimeRule.instruction.md +77 -3
  30. package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
  31. package/guidelines/transportRule/transportRule.instruction.md +51 -0
  32. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
  33. package/incrementalBuilder.proc.js +31 -48
  34. package/{index-h7yw2z9n.js → index-0bq14vr1.js} +8 -14
  35. package/{index-259dsnz3.js → index-1db2cw0y.js} +5 -8
  36. package/index-22d57v4c.js +266 -0
  37. package/index-3hx1z96c.js +87 -0
  38. package/{index-kcxmv48c.js → index-3t7g9e4r.js} +45 -79
  39. package/{index-ne2qy0nk.js → index-4dn1p115.js} +12 -12
  40. package/{index-q0y2jq9j.js → index-5b6tavv3.js} +6 -3
  41. package/index-7cqjvraf.js +69 -0
  42. package/index-7md2g9vw.js +6 -0
  43. package/{index-zry5nz81.js → index-8kb5p54p.js} +51 -30
  44. package/{index-ktb5njgy.js → index-9629w7ns.js} +49 -99
  45. package/index-9dp031by.js +12 -0
  46. package/{index-a64esmtb.js → index-aadpbnt2.js} +172 -308
  47. package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
  48. package/index-c6eggbee.js +123 -0
  49. package/index-d0arnrm8.js +700 -0
  50. package/index-d2242ksy.js +99 -0
  51. package/{index-600zhnve.js → index-dg97qp24.js} +70 -200
  52. package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
  53. package/{index-w8kmx1b6.js → index-fgq75haa.js} +198 -290
  54. package/{index-2tv3fdx4.js → index-ga6hygxr.js} +944 -1333
  55. package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
  56. package/index-hr5n7t4x.js +465 -0
  57. package/{index-hwkygn9s.js → index-khpfpcsd.js} +302 -33
  58. package/{index-7737gxcp.js → index-m0ms3n1c.js} +107 -241
  59. package/{index-ws5e0j51.js → index-mgfx3mfs.js} +43 -26
  60. package/index-p6pbz9n5.js +25 -0
  61. package/index-pp54p5zk.js +53 -0
  62. package/{index-j9yhg404.js → index-q44jbw48.js} +4 -7
  63. package/{index-mak3cd28.js → index-rmcw8yc8.js} +137 -255
  64. package/{index-pnqbkf5p.js → index-rnt66vym.js} +6 -2
  65. package/index-smme4r47.js +49 -0
  66. package/index-tgjy8xf3.js +187 -0
  67. package/index-v5eayqqw.js +2729 -0
  68. package/index-wa5jbxj1.js +760 -0
  69. package/{index-4edzf1gr.js → index-wp43xrjv.js} +77 -150
  70. package/{index-fz5q3gd7.js → index-ya5qbmyg.js} +10 -17
  71. package/index-yjkte9aj.js +1119 -0
  72. package/{index-gyd220p2.js → index-yy4s1j72.js} +554 -577
  73. package/{index-v0qh106m.js → index-z50k041j.js} +580 -862
  74. package/index.js +23 -22
  75. package/{library.command-nbsky672.js → library.command-ccb1cp92.js} +4 -5
  76. package/{localRegistry.command-yeq57wjr.js → localRegistry.command-ajb9n87j.js} +23 -42
  77. package/{module.command-29vqzrh8.js → module.command-5k6h407d.js} +8 -11
  78. package/{package.command-axv0abmn.js → package.command-e6ntvpez.js} +6 -7
  79. package/package.json +14 -6
  80. package/{page.command-vs9s3zrh.js → page.command-v4f90wb8.js} +4 -5
  81. package/{primitive.command-cbxmqbtc.js → primitive.command-g6km57q5.js} +9 -22
  82. package/prompter-ptaa3nk0.js +8 -0
  83. package/{quality.command-gmbs7nws.js → quality.command-de3p36pz.js} +205 -377
  84. package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
  85. package/{repair.command-bqsjptsj.js → repair.command-d3x1javq.js} +11 -35
  86. package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
  87. package/{scalar.command-6k1vhe9s.js → scalar.command-7ezrac2b.js} +7 -10
  88. package/skills/akan-code/SKILL.md +130 -0
  89. package/skills/akan-debug/SKILL.md +42 -0
  90. package/skills/akan-deps/SKILL.md +50 -0
  91. package/skills/akan-err/SKILL.md +39 -0
  92. package/skills/akan-module/SKILL.md +45 -0
  93. package/skills/akan-scaffold/SKILL.md +76 -0
  94. package/skills/akan-scalar/SKILL.md +52 -0
  95. package/skills/akan-service/SKILL.md +34 -0
  96. package/skills/akan-store/SKILL.md +67 -0
  97. package/skills/akan-ui/SKILL.md +63 -0
  98. package/skills/akan-validate/SKILL.md +44 -0
  99. package/{subspace.command-fwjfcts7.js → subspace.command-ps71ynp6.js} +138 -67
  100. package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
  101. package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
  102. package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
  103. package/templates/appSample/lib/task/task.abstract.ts +12 -32
  104. package/templates/appSample/lib/task/task.signal.spec.ts +15 -18
  105. package/templates/appSample/lib/task/task.signal.test.ts +15 -1
  106. package/templates/appSample/lib/task/task.signal.ts +1 -1
  107. package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
  108. package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
  109. package/templates/client.ts +7 -30
  110. package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
  111. package/templates/crudPages/[__model__Id]/page.tsx +2 -1
  112. package/templates/crudPages/new/page.tsx +2 -1
  113. package/templates/crudPages/page.tsx +2 -1
  114. package/templates/crudSinglePage/page.tsx +2 -1
  115. package/templates/lib/__lib/lib.dictionary.ts +0 -2
  116. package/templates/lib/__lib/lib.document.ts +0 -2
  117. package/templates/lib/sig.ts +8 -30
  118. package/templates/localDev/docker-compose.yaml.template +3 -14
  119. package/templates/module/__model__.signal.ts +12 -4
  120. package/templates/pkgRoot/tsconfig.json.template +2 -7
  121. package/templates/server.ts +0 -2
  122. package/templates/service/__model__.abstract.md.template +8 -0
  123. package/templates/workspaceRoot/.gitignore.template +0 -1
  124. package/tunnel.command-hsrv2mny.js +129 -0
  125. package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
  126. package/typecheck.proc.js +1 -1
  127. package/workflow.command-z2p32nmf.js +79 -0
  128. package/{workspace.command-8s050m0h.js → workspace.command-tmdqkrh0.js} +77 -109
  129. package/abstractCompactor-mjh5ppsq.js +0 -101
  130. package/application.command-pj2m7584.js +0 -204
  131. package/applicationReleasePackager-4yasxwv9.js +0 -244
  132. package/capacitorApp-36rbr858.js +0 -12
  133. package/cloud.command-jshwdq22.js +0 -93
  134. package/context.command-ez8a0zyz.js +0 -80
  135. package/devTuiView-c08ccbat.js +0 -9
  136. package/getDirname-dr3jz1xg.js +0 -7
  137. package/guideline.command-xmqwx8zm.js +0 -359
  138. package/guidelines/agentRule/agentRule.generate.json +0 -31
  139. package/guidelines/componentRule/componentRule.generate.json +0 -31
  140. package/guidelines/conventions/conventions.generate.json +0 -25
  141. package/guidelines/cssRule/cssRule.generate.json +0 -31
  142. package/guidelines/docPageRule/docPageRule.generate.json +0 -31
  143. package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
  144. package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
  145. package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
  146. package/guidelines/enumConstant/enumConstant.generate.json +0 -31
  147. package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
  148. package/guidelines/fieldRule/fieldRule.generate.json +0 -31
  149. package/guidelines/framework/framework.generate.json +0 -32
  150. package/guidelines/mcpRule/mcpRule.generate.json +0 -35
  151. package/guidelines/modelConstant/modelConstant.generate.json +0 -32
  152. package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
  153. package/guidelines/modelDocument/modelDocument.generate.json +0 -31
  154. package/guidelines/modelService/modelService.generate.json +0 -31
  155. package/guidelines/modelSignal/modelSignal.generate.json +0 -31
  156. package/guidelines/modelStore/modelStore.generate.json +0 -31
  157. package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
  158. package/guidelines/modelUnit/modelUnit.generate.json +0 -31
  159. package/guidelines/modelUtil/modelUtil.generate.json +0 -31
  160. package/guidelines/modelView/modelView.generate.json +0 -31
  161. package/guidelines/modelZone/modelZone.generate.json +0 -31
  162. package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
  163. package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
  164. package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
  165. package/guidelines/queryRule/queryRule.generate.json +0 -31
  166. package/guidelines/recipeRule/recipeRule.generate.json +0 -31
  167. package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
  168. package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
  169. package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
  170. package/guidelines/scalarModule/scalarModule.generate.json +0 -31
  171. package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
  172. package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
  173. package/guidelines/ssrRule/ssrRule.generate.json +0 -31
  174. package/guidelines/transportRule/transportRule.generate.json +0 -31
  175. package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
  176. package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
  177. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
  178. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
  179. package/index-2hq52jq3.js +0 -465
  180. package/index-47pj3aee.js +0 -76
  181. package/index-6wwzs1bc.js +0 -7
  182. package/index-8pm30m73.js +0 -841
  183. package/index-kn1h7p1x.js +0 -560
  184. package/index-meh558jy.js +0 -65
  185. package/index-rwg2aa4f.js +0 -229
  186. package/index-wt7qrtd4.js +0 -1516
  187. package/templates/__scalar/__model__/__model__.abstract.md +0 -28
  188. package/templates/module/__model__.abstract.md +0 -28
  189. package/templates/service/__model__.abstract.md +0 -28
  190. package/workflow.command-nwpcdc8p.js +0 -107
@@ -1,37 +1,17 @@
1
1
  // @bun
2
2
  import {
3
- openBrowser,
4
- plainTextOf,
5
- HOST_SOURCE,
6
- DevLogBuffer
7
- } from "./index-v0qh106m.js";
3
+ stripAnsi
4
+ } from "./index-7md2g9vw.js";
5
+ import {
6
+ writeClipboard
7
+ } from "./index-p6pbz9n5.js";
8
+ import {
9
+ openBrowser
10
+ } from "./index-9dp031by.js";
8
11
 
9
12
  // pkgs/@akanjs/cli/application/devTui.tsx
10
13
  import { render } from "ink";
11
14
 
12
- // pkgs/@akanjs/cli/clipboard.ts
13
- var clipboardCommands = () => {
14
- if (process.platform === "darwin")
15
- return [["pbcopy"]];
16
- if (process.platform === "win32")
17
- return [["clip"]];
18
- return [["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"]];
19
- };
20
- var writeClipboard = async (text) => {
21
- for (const command of clipboardCommands()) {
22
- try {
23
- const proc = Bun.spawn(command, {
24
- stdin: new TextEncoder().encode(text),
25
- stdout: "ignore",
26
- stderr: "ignore"
27
- });
28
- if (await proc.exited === 0)
29
- return true;
30
- } catch {}
31
- }
32
- return false;
33
- };
34
-
35
15
  // pkgs/@akanjs/cli/ui/DevTuiApp.tsx
36
16
  import { Box, Text, useInput } from "ink";
37
17
  import { useEffect, useState } from "react";
@@ -176,6 +156,8 @@ var DevTuiApp = ({ actions }) => {
176
156
  actions.copyLines();
177
157
  else if (input === "Y")
178
158
  actions.copyPath();
159
+ else if (input === "s")
160
+ actions.copyShareUrl();
179
161
  else if (input === "o")
180
162
  actions.openSelected();
181
163
  else if (input === "r")
@@ -236,12 +218,95 @@ var DevTuiApp = ({ actions }) => {
236
218
  }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV(Text, {
237
219
  dimColor: true,
238
220
  wrap: "truncate",
239
- children: `Tab next \xB7 1-9 app \xB7 \u2191\u2193 scroll (shift page) \xB7 G follow${snapshot.following ? "" : " (paused)"} \xB7 / grep${snapshot.grep ? ` (${snapshot.grep})` : ""} \xB7 e errors${snapshot.errorsOnly ? " on" : ""} \xB7 c clear \xB7 y copy \xB7 Y path \xB7 o open \xB7 r restart \xB7 q quit`
221
+ children: `${snapshot.hasShare ? "s share \xB7 " : ""}Tab next \xB7 1-9 app \xB7 \u2191\u2193 scroll (shift page) \xB7 G follow${snapshot.following ? "" : " (paused)"} \xB7 / grep${snapshot.grep ? ` (${snapshot.grep})` : ""} \xB7 e errors${snapshot.errorsOnly ? " on" : ""} \xB7 c clear \xB7 y copy \xB7 Y path \xB7 o open \xB7 r restart \xB7 q quit`
240
222
  }, undefined, false, undefined, this)
241
223
  ]
242
224
  }, undefined, true, undefined, this);
243
225
  };
244
226
 
227
+ // pkgs/@akanjs/cli/application/devLogBuffer.ts
228
+ var plainTextOf = (lines, { withApp = false } = {}) => {
229
+ const width = withApp ? lines.reduce((max, line) => Math.max(max, line.app.length), 0) : 0;
230
+ return lines.map((line) => `${withApp ? `${line.app.padEnd(width)} \u2502 ` : ""}${stripAnsi(line.text)}`).join(`
231
+ `);
232
+ };
233
+ var HOST_SOURCE = "host";
234
+ var childPrefix = /^\[child:(\d+) ([a-z-]+)\] \[(?:stdout|stderr)\] /;
235
+ var sourceOf = (text) => {
236
+ const match = childPrefix.exec(text);
237
+ return match ? `#${match[1]} ${match[2]}` : HOST_SOURCE;
238
+ };
239
+
240
+ class DevLogBuffer {
241
+ static defaultLimit = 5000;
242
+ #limit;
243
+ #lines = [];
244
+ #partial = new Map;
245
+ #sources = new Map;
246
+ #seq = 0;
247
+ constructor({ limit = DevLogBuffer.defaultLimit } = {}) {
248
+ this.#limit = Math.max(1, limit);
249
+ }
250
+ get size() {
251
+ return this.#lines.length;
252
+ }
253
+ sourcesOf(app) {
254
+ return [...this.#sources.get(app) ?? []];
255
+ }
256
+ push(app, kind, chunk) {
257
+ const key = `${app}:${kind}`;
258
+ const parts = `${this.#partial.get(key) ?? ""}${chunk}`.split(`
259
+ `);
260
+ this.#partial.set(key, parts.pop() ?? "");
261
+ return this.#append(app, kind, parts);
262
+ }
263
+ flushPartials() {
264
+ const flushed = [];
265
+ for (const [key, remainder] of [...this.#partial]) {
266
+ this.#partial.set(key, "");
267
+ if (!remainder)
268
+ continue;
269
+ const split = key.lastIndexOf(":");
270
+ const app = key.slice(0, split);
271
+ const kind = key.slice(split + 1) === "stderr" ? "stderr" : "stdout";
272
+ flushed.push(...this.#append(app, kind, [remainder]));
273
+ }
274
+ return flushed;
275
+ }
276
+ select({ app = null, source = null, grep, errorsOnly = false } = {}) {
277
+ const needle = grep?.trim().toLowerCase();
278
+ return this.#lines.filter((line) => {
279
+ if (app !== null && line.app !== app)
280
+ return false;
281
+ if (source !== null && line.source !== source)
282
+ return false;
283
+ if (errorsOnly && line.kind !== "stderr")
284
+ return false;
285
+ if (needle && !stripAnsi(line.text).toLowerCase().includes(needle))
286
+ return false;
287
+ return true;
288
+ });
289
+ }
290
+ clear(app = null) {
291
+ const kept = app === null ? [] : this.#lines.filter((line) => line.app !== app);
292
+ this.#lines.length = 0;
293
+ this.#lines.push(...kept);
294
+ }
295
+ #append(app, kind, texts) {
296
+ const known = this.#sources.get(app) ?? new Set;
297
+ this.#sources.set(app, known);
298
+ const added = texts.map((text) => {
299
+ const source = sourceOf(text);
300
+ known.add(source);
301
+ return { seq: ++this.#seq, app, source, kind, text };
302
+ });
303
+ this.#lines.push(...added);
304
+ if (this.#lines.length > this.#limit)
305
+ this.#lines.splice(0, this.#lines.length - this.#limit);
306
+ return added;
307
+ }
308
+ }
309
+
245
310
  // pkgs/@akanjs/cli/application/devLogWindow.ts
246
311
  var windowOf = (lines, rows, anchor) => {
247
312
  const height = Math.max(1, Math.trunc(rows));
@@ -313,6 +378,7 @@ class DevTui {
313
378
  clear: this.#clear,
314
379
  copyLines: this.#copyLines,
315
380
  copyPath: this.#copyPath,
381
+ copyShareUrl: this.#copyShareUrl,
316
382
  openSelected: this.#openSelected,
317
383
  restartSelected: this.#restartSelected,
318
384
  quit: this.#quit
@@ -348,7 +414,7 @@ class DevTui {
348
414
  process.stdout.write(`${line.app} \u2502 ${line.text}
349
415
  `);
350
416
  for (const status of this.#statuses)
351
- process.stdout.write(`${status.name} ${status.url} \u2014 ${status.state}
417
+ process.stdout.write(`${status.name} ${status.url}${status.shareUrl ? ` \xB7 ${status.shareUrl}` : ""} \u2014 ${status.state}
352
418
  `);
353
419
  const log = this.#supervisor.sessionLog;
354
420
  for (const name of log.appNames)
@@ -403,12 +469,12 @@ class DevTui {
403
469
  const logRows = Math.max(1, bodyHeight - DevTui.paneChromeRows);
404
470
  const rows = this.#railRows();
405
471
  const view = windowOf(this.#filtered(this.#target), logRows, this.#anchor);
406
- const status = this.#statuses.find((candidate) => candidate.name === this.#target.app);
472
+ const status = this.#selectedStatus();
407
473
  const railLabelWidth = rows.reduce((max, row) => Math.max(max, row.label.length + row.depth * 2), 0);
408
474
  this.#cachedSnapshot = {
409
475
  rows,
410
476
  selected: this.#indexOfTarget(rows),
411
- title: status ? `${status.name}${this.#target.source ? ` \xB7 ${this.#target.source}` : ""} \xB7 ${status.url} \xB7 ${status.state}${status.detail ? ` (${status.detail})` : ""}` : "all apps",
477
+ title: status ? `${status.name}${this.#target.source ? ` \xB7 ${this.#target.source}` : ""}${status.shareUrl ? ` \xB7 \u25C8 ${status.shareUrl}` : ""} \xB7 ${status.url} \xB7 ${status.state}${status.detail ? ` (${status.detail})` : ""}` : this.#mergedTitle(),
412
478
  lines: view.lines,
413
479
  above: view.above,
414
480
  below: view.below,
@@ -419,6 +485,7 @@ class DevTui {
419
485
  errorsOnly: this.#errorsOnly,
420
486
  editingGrep: this.#editingGrep,
421
487
  notice: this.#notice,
488
+ hasShare: this.#statuses.some((candidate) => !!candidate.shareUrl),
422
489
  readyCount: this.#statuses.filter((candidate) => candidate.state === "ready").length,
423
490
  appCount: this.#statuses.length,
424
491
  logRows,
@@ -467,8 +534,7 @@ class DevTui {
467
534
  };
468
535
  #setTarget(target) {
469
536
  this.#target = { app: target.app ?? null, source: target.source ?? null };
470
- this.#anchor = null;
471
- this.#renderNow();
537
+ this.#follow();
472
538
  }
473
539
  #scroll = (delta) => {
474
540
  this.#anchor = scrollAnchor(this.#filtered(this.#target), this.#snapshot().logRows, this.#anchor, delta);
@@ -480,8 +546,7 @@ class DevTui {
480
546
  };
481
547
  #setGrep = (grep) => {
482
548
  this.#grep = grep;
483
- this.#anchor = null;
484
- this.#renderNow();
549
+ this.#follow();
485
550
  };
486
551
  #setEditingGrep = (editing) => {
487
552
  this.#editingGrep = editing;
@@ -489,13 +554,11 @@ class DevTui {
489
554
  };
490
555
  #toggleErrorsOnly = () => {
491
556
  this.#errorsOnly = !this.#errorsOnly;
492
- this.#anchor = null;
493
- this.#renderNow();
557
+ this.#follow();
494
558
  };
495
559
  #clear = () => {
496
560
  this.#buffer.clear(this.#target.app ?? null);
497
- this.#anchor = null;
498
- this.#renderNow();
561
+ this.#follow();
499
562
  };
500
563
  #copyLines = () => {
501
564
  const lines = this.#filtered(this.#target);
@@ -520,6 +583,25 @@ class DevTui {
520
583
  const app = this.#target.app;
521
584
  return (app ? [app] : log.appNames).map((name) => log.relativePathOf(name));
522
585
  }
586
+ #copyShareUrl = () => {
587
+ const urls = this.#shareUrls();
588
+ if (urls.length === 0) {
589
+ this.#setNotice("no public share \u2014 start with --share");
590
+ return;
591
+ }
592
+ writeClipboard(urls.join(`
593
+ `)).then((copied) => {
594
+ this.#setNotice(copied ? `copied ${urls.join(" \xB7 ")}` : urls.join(" \xB7 "));
595
+ });
596
+ };
597
+ #shareUrls() {
598
+ const app = this.#target.app;
599
+ return this.#statuses.filter((status) => app === null || status.name === app).map((status) => status.shareUrl).filter((url) => !!url);
600
+ }
601
+ #mergedTitle() {
602
+ const urls = this.#shareUrls();
603
+ return urls.length === 0 ? "all apps" : `all apps \xB7 \u25C8 ${urls.join(" \xB7 ")}`;
604
+ }
523
605
  #setNotice(text) {
524
606
  this.#notice = text;
525
607
  if (this.#noticeTimer)
@@ -0,0 +1,9 @@
1
+ // @bun
2
+ import {
3
+ getDirname2,
4
+ FileSys2
5
+ } from "./index-5b6tavv3.js";
6
+ export {
7
+ FileSys2 as FileSys,
8
+ getDirname2 as getDirname
9
+ };
@@ -0,0 +1,42 @@
1
+ // @bun
2
+ import {
3
+ script,
4
+ command
5
+ } from "./index-rmcw8yc8.js";
6
+ import"./index-5b6tavv3.js";
7
+ import"./index-ga6hygxr.js";
8
+ import {
9
+ Prompter2
10
+ } from "./index-0bq14vr1.js";
11
+
12
+ // pkgs/@akanjs/cli/guideline/guideline.script.ts
13
+ import { Logger } from "akanjs/common";
14
+
15
+ class GuidelineScript extends script("guideline") {
16
+ async guideline(action, name = null) {
17
+ if (action === "list") {
18
+ const guidelines = await Prompter2.listGuidelines();
19
+ Logger.rawLog(guidelines.join(`
20
+ `));
21
+ return;
22
+ }
23
+ if (action === "show") {
24
+ if (!name)
25
+ throw new Error("Guideline name is required. Example: akan guideline show framework");
26
+ Logger.rawLog(await Prompter2.getInstruction(name));
27
+ return;
28
+ }
29
+ throw new Error(`Unknown guideline action: ${action}. Use "list" or "show".`);
30
+ }
31
+ }
32
+
33
+ // pkgs/@akanjs/cli/guideline/guideline.command.ts
34
+ class GuidelineCommand extends command("guideline", [GuidelineScript], ({ public: target }) => ({
35
+ guideline: target({ desc: "List or show Akan AI guideline instructions" }).arg("action", String, { desc: "list or show" }).arg("name", String, { desc: "guideline name for show", nullable: true }).exec(async function(action, name) {
36
+ await this.guidelineScript.guideline(action, name);
37
+ })
38
+ })) {
39
+ }
40
+ export {
41
+ GuidelineCommand
42
+ };
@@ -45,10 +45,16 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
45
45
  clear and close. `chrome={false}` drops the header bar whole, for an `inline` chat inside a panel the app
46
46
  already titles, and `defaultDraft` opens the composer with text in it without sending it. A panel driven by a
47
47
  controlled `open` with **no** `onOpenChange` draws no close button at all rather than an inert one. Then the
48
- slots: `AgentLauncher`, `AgentBubble`, `AgentComposer`, `AgentApproval`,
49
- `AgentQuestion`, `AgentQueued`, `AgentMenu`, `AgentMarkdown` and `AgentCode` each replace one part in `_overrides.tsx`, and
48
+ slots: `AgentLauncher`, `AgentSteps`, `AgentBubble`, `AgentComposer`, `AgentApproval`,
49
+ `AgentQuestion`, `AgentToolCard`, `AgentQueued`, `AgentMenu`, `AgentMarkdown` and `AgentCode` each replace one part in `_overrides.tsx`, and
50
50
  `akanjs/ui` exports every default beside them (`DefaultBubble`, `DefaultComposer`, …) so a skin composes the
51
- one it is replacing. `AgentCode` is where a highlighter binds — the fence's language reaches it — and a
51
+ one it is replacing. **`AgentSteps` is one agent turn** — the messages between the user message that opened it
52
+ and the next one, with `isRunning` saying whether more are still coming — which is the grain a chat needs to
53
+ fold a turn's steps into a `details` and stand its answer outside them. It is the one boundary a per-message
54
+ slot cannot see, since neither message on either side of it knows it is at an edge. The default draws the turn's
55
+ messages flat into a Fragment, so a chat that binds nothing renders exactly what it did before the slot existed. The attachment row is exported on its own as `AgentAttachments`, because a replaced
56
+ `AgentBubble` that redraws it by hand forks the rule for which carrier is shown how — and then keeps whatever
57
+ that rule was on the day it was copied. `AgentCode` is where a highlighter binds — the fence's language reaches it — and a
52
58
  replacement for `AgentBubble` carries its own `memo`, since the transcript re-renders on every delta. Only then
53
59
  `AgentChat`, which replaces the panel whole: it also replaces the slash commands, the compaction notice and the
54
60
  approval gate, so reach for it when the *layout* differs, not when the look does.
@@ -70,24 +76,41 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
70
76
  - **The LLM is configured in `option.ts`, never through the environment.** `option.setLlm({ apiKey, model, host })`
71
77
  — or `setLlm((options) => …)` to read the key out of the app's own env object, which is where a secret belongs —
72
78
  fills whichever adaptor holds `LlmAdaptorRole`, reaching it as the `llmOption` use. The settings are the role's
73
- rather than one provider's, so they survive a swap. **DeepSeek is the built-in default** (`deepseek-v4-flash` at
74
- `https://api.deepseek.com`); with no `apiKey` the app still boots and the chat answers `llmUnavailable`. Swap
75
- providers the way middleware is applied: `option.applyAdaptor(LlmAdaptorRole, AnthropicLlm)`. An app may still
76
- write its own — an `adapt()` class in a `srvkit/` implementing `LlmAdaptor.chat(request, onDelta?)`, where
77
- ignoring `onDelta` still answers whole — but for vision it no longer has to.
78
- - **Three providers ship, and only two of them read a picture.** `DeepseekLlm` is the default and declares no
79
- `accepts`, which is a fact about its API rather than a gap. `OpenaiLlm` speaks the same chat-completions dialect
80
- — `OpenaiDialect`, shared between them so a protocol fix lands once — against `https://api.openai.com/v1` and
81
- declares `{ image: true }`, sending an image as a content part. `AnthropicLlm` is the Messages API and declares
82
- `{ image: true, document: true }`; it is its own file rather than a branch, because the system prompt is a field
83
- and not a message, tool calls and results are content blocks, the results ride in a *user* turn, roles must
84
- alternate — so two wire turns that map to one role are merged — and `max_tokens` is required. **Both new ones
85
- require `model`**: a default would age into a 404, and worse, it would decide the vision claim for the app.
79
+ rather than one provider's, so they survive a swap. **`OpenaiLlm` is the built-in default**, pointed at
80
+ `https://api.openai.com/v1`; with no `apiKey` or no `model` the app still boots and the chat answers
81
+ `llmUnavailable`. Swap providers the way middleware is applied:
82
+ `option.applyAdaptor(LlmAdaptorRole, AnthropicLlm)`. An app may still write its own — an `adapt()` class in a
83
+ `srvkit/` implementing `LlmAdaptor.chat(request, onDelta?)`, where ignoring `onDelta` still answers whole — and
84
+ whatever settings it needs beyond `LlmOption` travel through the same `setLlm`, read back with
85
+ `use<MyLlmOption>()` from an interface extending it.
86
+ - **Two adaptors ship, one per wire — a vendor is a host and a model name, not a protocol.** `OpenaiLlm` is the
87
+ chat-completions dialect (`OpenaiDialect`) pointed at whatever `host` names, so OpenAI, DeepSeek, Groq,
88
+ Together, OpenRouter, Ollama and a self-hosted vLLM are all one class and one `option.setLlm({ host, model })`.
89
+ `AnthropicLlm` is the Messages API and declares `{ image: true, document: true }`; it is its own file rather
90
+ than a branch, because the system prompt is a field and not a message, tool calls and results are content
91
+ blocks, the results ride in a *user* turn, roles must alternate — so two wire turns that map to one role are
92
+ merged — and `max_tokens` is required. **Both require `model`**: a default would age into a 404, and worse, it
93
+ would decide the vision claim for the app.
94
+ - **`OpenaiLlm` claims vision for its default host and for nothing else.** OpenAI's own endpoint takes image
95
+ parts, so that is what it answers with no `host` set. A `host` the app named is a gateway the class knows
96
+ nothing about, and it stays text-only until `option.setLlm({ accepts })` says otherwise — handing bytes to a
97
+ model that cannot decode them kills the whole turn on a 400, where text-only degrades them to a note the model
98
+ can repeat back.
99
+ - **A refusal is `agent.error.llmRequestFailed`, and it names the host** — unless the prompt did not fit. The
100
+ provider's own sentence rides in `reason` and the hostname in `provider`. `LlmOverflow.refusal(host, status,
101
+ reason)` builds it, and reads the one refusal a chat can undo — a prompt past the window, recognized from each
102
+ provider's own sentence — as `agent.error.contextOverflow` with the `limit` it named, which the relay flags so
103
+ the chat compacts and asks again. An adaptor an app wrote throws through the same function, so its refusals
104
+ translate and recover the way the shipped ones do without a key in the framework's dictionary it cannot add.
86
105
  - **`accepts` is answered per model, through `option.setLlm({ accepts })`.** An adaptor answers for an API and one
87
106
  API serves models that differ, so the override rides beside the `model` it is a fact about. It is not a table
88
107
  the framework keeps: a table is a claim about models that ship after it, and getting this wrong is the worst
89
108
  failure available — a provider handed bytes it cannot decode either refuses the turn or accepts it having seen
90
109
  nothing, and the model then answers confidently about a file it never read.
110
+ - **`contextWindow` is declared the same way: `option.setLlm({ contextWindow })`.** Every turn relays it with the
111
+ answer ceiling the adaptor actually sends (`LlmAdaptor.limits` — `AnthropicLlm` always sends one, `OpenaiLlm`
112
+ none), and the chat guards its window with it. It is not a table either; a model id is retired or renamed long
113
+ before a table notices.
91
114
  - **An adaptor answers `null` for "not configured" and *throws* for a refusal it can explain.** The two are
92
115
  different things to be told: collapsing both into `null`, the way the adapter convention otherwise reads, left a
93
116
  user reading `llmUnavailable` — "no model is configured" — about a conversation that had merely outgrown the
@@ -110,8 +133,9 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
110
133
  arrives as an image. `accepts.document` is one boolean over every non-image type, so an adaptor whose API has a
111
134
  block for only some of them — Anthropic's `document` is PDF and nothing else — names the rest in the text rather
112
135
  than dropping them, which is the wire's own rule applied at the one layer that knows which blocks exist. Persisting keeps each
113
- attachment's name and drops its content: web storage is a few megabytes, one screenshot fills a chunk of it, and
114
- a save that fails is silent — so keeping the bytes would quietly stop keeping the transcript.
136
+ attachment's name, type, address and `ref` and drops only its content: web storage is a few megabytes, one
137
+ screenshot fills a chunk of it, and a save that fails is silent — so keeping the bytes would quietly stop keeping
138
+ the transcript, while an id is the handle a restored conversation has left to find the file with.
115
139
  **The ceilings are the message's, not the file's**: 4 MB per file, 8 MB and five files per message, and the same
116
140
  file twice is refused by name. The bytes ride inside one turn's JSON, so what a provider refuses is the sum —
117
141
  and a request that cannot be sent is one the user has to empty the composer to escape, which is why the refusal
@@ -121,6 +145,26 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
121
145
  reader produced**, not on the file that was picked: a reader that uploads and answers a `url` has already paid
122
146
  what the ceiling exists for, so refusing its 6 MB photo would refuse it for a cost it does not incur. Only the
123
147
  built-in path checks the source size, and there the file itself is the carrier.
148
+ - **An app may also build attachments itself and hand them to `session.send([{ role: "user", text, attachments }])`**,
149
+ which is why `MessageAttachment` is exported from `akanjs/ui` — the composer's path is typed by the `AttachReader`
150
+ callback, that one is not, and an object assembled with no nameable type is the one that gets a field wrong.
151
+ Nothing downstream trusts the result: an attachment whose `mimeType` is not a string is noted like any other
152
+ unreadable one, and its chip renders its name with no preview, because the host wrote the object and the declared
153
+ type is a claim rather than a check. A field that arrives wrong costs that attachment, never the turn.
154
+ - **A `url` carrier is an address the *provider* fetches, and bytes beside it win.** A reader that uploads answers
155
+ `{ url }` when its storage is publicly reachable, and `{ data }` — or both — when it is not: an attachment
156
+ carrying both is read from its bytes and displayed from its address, so the chip draws a thumbnail and the model
157
+ still gets the picture. That precedence exists because the other order fails the one way nothing reports: the
158
+ default storage backend serves a path only the app can resolve, the adaptor would hand it over as written, and
159
+ the model answers about a picture it never saw. Send a provider-reachable URL **alone** — paired with bytes it
160
+ costs the provider hop whatever the bytes weigh, for a fetch the provider would have done itself.
161
+ - **An image type the provider has no block for is refused at the adaptor, not upstream.** `accepts.image` is one
162
+ boolean, so `AgentService.readable` passes every `image/*` through, and the built-in composer reader base64s
163
+ every `image/*` as well — an `AttachReader` answering `null` means "not mine" and falls through to it, so an app
164
+ cannot gate one either. Both shipped vision adaptors therefore match an exact set (`jpeg`, `png`, `gif`, `webp`)
165
+ and name the rest in the text. It matters because an unsupported type is not one unread attachment: it is a
166
+ block the API refuses, so the **whole turn** dies on a vendor 400 — and `image/heic`, the iPhone camera default,
167
+ is the likeliest one an app meets.
124
168
  - **`MessageAttachment.ref` is the host's own handle on the file — carried, never read.** A file id, a storage key,
125
169
  whatever turns the attachment back into something a tool can be handed. The framework only moves it: the model is
126
170
  never shown one, and a tool the app publishes (`useFileFieldTool` below) is what spends it. Without it a host that
@@ -137,10 +181,56 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
137
181
  there: a voice user who is never told about the card is a conversation that simply ends.
138
182
  The contract is a subscription rather than `listen(): Promise<string>` on purpose: a promise fits push-to-talk
139
183
  and nothing else, so hands-free could then only arrive as a breaking change. `useSpeech` in a util lib's
140
- `webkit/` is the engine — the browser's own recognition and synthesis on the web, the Capacitor plugins in a
141
- WebView, **which has neither on Android or iOS**, so a `speech.plugin.ts` declares the permission and
142
- the packages the native build needs. An engine answering `available()` false renders no microphone at all,
184
+ `webkit/` is the engine — the browser's own recognition and synthesis. An app's WebView **has neither on
185
+ Android or iOS** and `@akanjs/native` ships no speech plugin yet, so a `speech.plugin.ts` declares only the
186
+ permission and its usage texts. An engine answering `available()` false renders no microphone at all,
143
187
  the same rule as publishing no tool for a control the screen does not draw.
188
+ - **Data the user points at rides the message too, as `references` — and its value is a snapshot.** A file is
189
+ something they handed over; a reference is something they pointed at while asking, so it belongs in the same
190
+ place and for the same reason. Screen context does not do this job: `ContextBlock`s are rebuilt from the screen
191
+ every turn, so what was named three turns ago is simply not in the conversation any more. A `MessageReference`
192
+ carries the host's own `refName`, the id, a label, an optional dotted `path` into the document, and the `value`
193
+ those resolved to. **The value is never re-read on a later turn**, which is the point: re-reading rewrites what
194
+ the person was looking at when they spoke, and the usual next thing the agent does is edit the very field it was
195
+ pointed at — leaving a live reference showing the result with no record of what it was changed from.
196
+ `refName`/`refId`/`path` travel so a tool can read the current value when the answer needs it, and
197
+ `AgentService.referenced` folds the whole lot into the message text in one place, so every provider carries a
198
+ reference without either adaptor knowing the word. **The value is masked on the way in and nowhere else**: masking
199
+ needs the model class, which no wire carries, so the model a host names when it stages the reference is the whole
200
+ of the decision about what leaves the browser — name the class that actually holds the field, because a `Light`
201
+ one usually does not. **The ceiling is 20,000 characters per reference**, applied as it is staged and again on
202
+ the server; a reference is bulkier than a tool result and outlives one, riding every later turn and being the last
203
+ thing compaction folds. Persisting keeps the pointer and drops the value with a note saying so, which is a better
204
+ restored state than an attachment can offer — the pointer is still live, so the answer is to read it again.
205
+ Staging lives on the **session** (`stage` / `staged` / `unstage(key)` / `clearStaged()`), not on the composer where
206
+ staged files live, because a reference comes from whichever component drew the data rather than from the paperclip;
207
+ `unstage` is keyed on `refName/refId#path` because what orders a message's references is its text, not that list.
208
+ - **Two ways to point at data, and which one depends on who knows the value.** `<Agent.Chat reference={[…]} />`
209
+ takes one `ReferenceSource` per kind of document — `refName`, a `type`, the app's own `search`, and a `resolve` —
210
+ and the composer's `@` menu offers whole documents from them; the token is written the moment a row is picked and
211
+ the value is staged when `resolve` lands, so a slow fetch never freezes the menu and one that fails leaves a
212
+ pointer that reads as *go and read it*. For a field *inside* a document, the component drawing it calls
213
+ `useAgentReference()` — it already holds the value, so there is no round trip, and it is the only thing that
214
+ knows a rich-text field stored as `field(Any)` reads as a paragraph rather than as the editor document it is. The
215
+ hook no-ops with a warning outside a session, the same call `AgentValue.publishable` makes: a card carrying a
216
+ reference button must not cost a route its render.
217
+ - **The token in the draft is what carries a reference, and the chip only draws it.** Deleting
218
+ `@[label](mention:refName/id#path)` by hand drops the reference exactly as removing the chip does — removing the
219
+ chip removes the token — so the two can never disagree, and nothing re-derives a value from text somebody edited.
220
+ A token pasted out of an earlier message travels as a pointer with a note, which is the same shape a restored
221
+ conversation produces. Staged references are the **session's**, not the composer's, because `useAgentReference`
222
+ is called from components the composer cannot see; writing the token is the composer's half, and `refer` warns
223
+ rather than staging silently when no chat is mounted on that session — the case where a card sits outside the
224
+ `<Agent.Zone>` its chat is inside.
225
+ - **The composer draws a pointer as the name it points at, never as the token.** Where `reference` sources are
226
+ declared the input is a Lexical editor rather than a textarea, and each pointer is one atomic node — a backspace
227
+ takes the whole thing rather than a character of a label that would then name nothing. Nothing above it changes:
228
+ the node's text content *is* the `@[…](mention:…)` token, so the draft string the chat reads, the `@` query, the
229
+ chips, the wire and the server's framing all see what they always saw, and every offset the chat hands over is
230
+ an offset into that string. It is its own chunk behind the chat's, fetched only where a source was declared, and
231
+ `mentions={false}` keeps the plain textarea — for an app that overrides `AgentComposer`, or one that would
232
+ rather see the tokens it is sending. An override that draws its own textarea keeps working: it fills the
233
+ `inputRef` it is handed, and the chat reads the caret off that when no editor filled `handleRef`.
144
234
  - **`attach` and `voice` both carry functions, so a server layout cannot pass either.** A closure does not cross
145
235
  the RSC boundary — `non-scalar-props-restricted` says so on `page/**` — so an app that wants either mounts the
146
236
  chat from a small client component in the app's own `ui/` that calls the hook.
@@ -212,6 +302,14 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
212
302
  `data-akan-action` like a store setter does. A `remove*` name defaults to a confirm gate. Reach a store action
213
303
  from the body — `.exec((id) => st.do.removeX(id))` — which is how an agent gets CRUD; `st.do` on its own
214
304
  reaches nobody.
305
+ - **`.card()` is the other way the chain ends, for an answer that is the user's to give.** Where `.exec()` runs a
306
+ function, `.card(({ submit, cancel }, ...args) => <Form …/>)` parks the call in the chat and renders the app's own
307
+ component there; what `submit` is handed is what the model reads back, and `cancel` is the refusal it reads
308
+ instead. A name and a phone number, a date somebody has to look up, a signature — a model that fills those in
309
+ has answered its own question, and every other shape of "ask the user" is `askUser`. The declared arguments are
310
+ checked **before** the card is parked, so a bad one reaches the model as a refusal rather than throwing inside
311
+ the render; `confirm` is not read at all, because the card in front of the user is already the asking. The frame
312
+ draws its own way out even when the card does not, and the turn is never parked on something with no exit.
215
313
  - **The second argument is how the call behaves, never what it is.** `{ settle, confirm, guard }`:
216
314
  `settle: false` marks a read that returns what is already there and skips the wait; every other tool is waited
217
315
  out before its effect on the screen is reported, because a write may still be landing when `exec` resolves. A
@@ -447,18 +545,29 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
447
545
  - **A long conversation summarizes itself, because nothing else is keeping it inside the model's window.** The
448
546
  loop runs in the browser and the relay holds no session, so an uncompacted chat grows until the provider
449
547
  refuses the whole request — a refusal, never a shorter answer, which is why compaction runs *before* the turn
450
- that would have overflowed rather than as a recovery after it. Past `compact.at` estimated tokens (four
451
- characters to a token, over the JSON the turn posts; 24k by default, well under the smallest window a provider
452
- is likely to have, since the tools and the screen context ride on top of it and neither compacts) the history above the last `keep` messages
453
- becomes one message standing in for it, flagged `summary` on the wire — `<Agent.Chat compact={{ at, keep }} />`
454
- tunes it per provider and `{ at: 0 }` turns it off, and `/compact` does the same on demand keeping nothing.
548
+ that would have overflowed. **Two triggers, each before every assistant turn.** `compact.at` is a ceiling on
549
+ cost: past that many estimated transcript tokens (four characters to a token, over the JSON the turn posts —
550
+ except an inlined picture, counted at the ~1,600 tokens a provider bills for its pixels rather than its base64;
551
+ 24k by default) the history compacts, because the relay resends the whole transcript every turn and the app
552
+ pays for each one. The **window guard** compacts once the prompt nears the window the server reports —
553
+ `window − answer ceiling (8,192 when unreported) − compact.buffer (13,000, the margin Claude Code leaves)` —
554
+ measured by the provider's own count of the last turn (`done.usage`, kept on that assistant message) plus the
555
+ four-character estimate for what arrived since, so a Korean transcript the character rule reads as small is
556
+ still caught. The window comes from `option.setLlm({ contextWindow })` or from the first refusal that names it;
557
+ with neither, only the ceiling runs. **A refusal for length is answered once**: a relay flags it `overflow` on
558
+ the wire, and the session compacts and sends the same turn again, and a second refusal in the same send fails
559
+ with `agent.error.contextOverflow`. Either way the history above the last `keep` messages
560
+ becomes one message standing in for it, flagged `summary` on the wire — `<Agent.Chat compact={{ at, keep, buffer }} />`
561
+ tunes it, `{ at: Infinity }` leaves only the window guard, `{ at: 0 }` turns off the ceiling, the guard and the recovery alike, and `/compact` does
562
+ the same on demand keeping nothing. The header draws the estimate against whichever trigger is nearer.
455
563
  **The cut never lands on a `tool` message**: the kept half may not open with a result whose call was summarized
456
564
  away, a shape every provider dialect rejects. A user message is preferred where the tail holds one — everything
457
565
  above it is settled — but one assistant turn that ran ten tools has none, and that is the transcript that
458
566
  outgrows the window, so a cut there opens on the assistant message instead. The summarizing
459
567
  turn carries no tools and no screen context — it summarizes the conversation, it does not act on it — and it is
460
568
  fed a *bounded* digest rather than the transcript itself, since the transcript being summarized is the one that
461
- no longer fits. A summary that cannot be produced leaves the transcript alone, the turn goes out as it would
569
+ no longer fits. The previous summary is the exception: it rides the digest whole, under its own label and outside
570
+ the bound, because it is the only record of what was compacted before it. A summary that cannot be produced leaves the transcript alone, the turn goes out as it would
462
571
  have, and the next one asks again — a summarizer that was momentarily unreachable says nothing about whether
463
572
  this transcript can shrink. One that *landed* and shrank nothing does, so that one is not retried until another
464
573
  threshold's worth has been added. On the
@@ -610,4 +719,76 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
610
719
  is among them. The loop parks on the question exactly as it parks on an approval, a dismissal is the tool's
611
720
  error result rather than a silent empty answer, and the settled exchange renders as question-and-answer instead
612
721
  of a tool row. **Never re-implement it per screen** — a `st.tool("askAboutX")` that opens a modal is the same
613
- thing with a worse transcript — and a hook tool named `askUser` shadows it like any other built-in.
722
+ thing with a worse transcript — and a hook tool named `askUser` shadows it like any other built-in. The line
723
+ between it and a `.card()` tool is what the answer *is*: a sentence or a pick off a list is `askUser` and belongs
724
+ to the conversation, while a shape the model must not invent — fields, a form, a signature — is a card the screen
725
+ declares, because only the screen knows what filling it in looks like.
726
+
727
+ ## What The Page Draws
728
+ - **The page shows the agent's work, not just the transcript.** A chat panel is closed as often as it is open, and
729
+ a form that fills itself or a tab that switches on its own is a change the user watches happen with nothing
730
+ anywhere attributing it. So **the control a call was published from is ringed** where it stands, scrolled to
731
+ first when it is off screen, and **a pointer travels to it and presses it** — both on by default, and both
732
+ driven from the tool call itself rather than from a store action, which cannot tell the agent's write from the
733
+ user's.
734
+ - **It costs an app nothing, for the same reason `data-akan-action` does.** `onChange={st.do.setTitleOnTask}` by
735
+ reference is what annotates the control, and the annotation is what makes it findable — so the fields and verbs
736
+ that matter light up with no code. An inline arrow publishes no tool, carries no annotation, and therefore
737
+ draws nothing: one wrapper silently costs three things at once.
738
+ - **For a control the framework did not render, spread `agentAttrs`** from `akanjs/ui`:
739
+ `<button {...agentAttrs(run)} onClick={run}>`, where `run` is what `st.tool(…).exec(…)` returned. It reads the
740
+ name off the callable, so there is nothing to keep in sync and no `ref` to thread — and it earns the same E2E
741
+ selector and accessibility name an `akanjs/ui` control has. Never hand-write `data-akan-*`.
742
+ - **A control that shares its handler with its siblings takes a key**: `agentAttrs(switchTab, menu)`, in the same
743
+ vocabulary the call's argument uses. One tool serves a whole tab strip or a whole list, so without it every
744
+ namesake is interchangeable in the DOM and the page can say *what* the agent did but never *where* — which the
745
+ no-guessing rule then turns into drawing nothing. `Tab.Menu` passes its `menu` key; do the same for a row verb
746
+ whose id the call names.
747
+ - **Almost nothing is waited on.** The call starts the moment the effect is handed its event; a ring that lands a
748
+ frame later costs the turn nothing, and an animation that held a call would make the agent slower for a
749
+ decoration. A turn past four scrolls stops scrolling and keeps ringing — eight scrolls across a page is motion
750
+ sickness rather than attribution. **The one exception is `navigate`**, whose press has to land before the router
751
+ replaces the tree the link is in; it is capped at 600ms and the call goes either way.
752
+ - **What it refuses to draw is the point.** A name several rows answer to rings nothing rather than guessing a row,
753
+ a call an approval or a guard turned back is never drawn at all, a tool no control carries draws nothing, and a
754
+ backgrounded tab draws nothing. A ring on the wrong element is worse than no ring: it is the screen telling the
755
+ user something untrue about what just happened. **A control the screen is not actually showing is not pointed at
756
+ either, and the pointer goes rather than travelling to it**: `checkVisibility` answers about an element alone, so
757
+ one under a modal's backdrop, inside a drawer that has slid off, or faded to nothing all pass it while being
758
+ invisible, and a pointer sent there lands on a blank patch of overlay. The test is `elementFromPoint` at the
759
+ control's own centre, taken after the reveal has settled. **`navigate` is mostly in that last class** — the router is not
760
+ an element, so it draws nothing. A bar across the top of the page was tried for it and removed: it read as
761
+ chrome the page had grown rather than as the agent doing something, which is the opposite of attribution. What
762
+ it does draw is the **link already on screen that goes where it is going**, when exactly one does and the user
763
+ can already see it: that one is an element, and the pointer presses it before the route moves. Addresses are
764
+ compared as routes (`router.routeOf`), since the locale and base-path segments are on the `<a>` and never on the
765
+ tool argument. An off-screen link is left alone — scrolling to a link and then leaving the page it is on is two
766
+ motions for one act. **Link presence is a drawing condition, never a permission one**: the agent may navigate
767
+ anywhere the user could type, and a zone that must not leave one screen drops `navigate` through `builtins`.
768
+ - **Turn it off with `visual`** on `Agent.Chat` / `Agent.Zone` — `false` for all of it, `{ cursor: false }` or
769
+ `{ reveal: false }` for one effect. The pointer is chrome (`data-agent-ui`, `aria-hidden`,
770
+ `pointer-events: none`), so `readScreen` never reads it back and `highlight` can never aim at it.
771
+ - **`ToolActivity` is the channel underneath**, a `ToolRunner` host option and an `AgentSession.onActivity`, firing
772
+ `start` / `end` around the execution alone. It is deliberately not `progress`: that one fires only for a tool
773
+ that chose to report, and a tool that says nothing about itself is exactly the one whose effect arrives
774
+ unexplained. The `start` may answer a promise, and the call waits for it — the whole of what makes drawing a
775
+ click on a link possible.
776
+ - **`AgentSession.onTurn(running)` is the other half**, and the unit the pointer's life is measured in. A model's
777
+ calls arrive with its own writing between them, seconds each, so a pointer that lived for the length of a *call*
778
+ spent every turn vanishing and coming back. Only the conversation loop reports there — `compact` runs under the
779
+ same internal flag and drives nothing on screen.
780
+ - **The pointer presses on arrival, never on departure** — a press fired as it leaves is a click on whatever it
781
+ was still over. It glides only when the hop is far enough to be worth following (a third of a second to cross
782
+ fifty pixels reads as lag, not motion), and teleports otherwise. It appears at the first control it presses,
783
+ **drifts clear of what it pressed** and waits out the gap between calls there as a spinner — a person clicks and
784
+ takes the hand away, and a spinner left sitting on the button covers the change it just caused — and fades when
785
+ the turn ends; **a turn that
786
+ drove no control draws no pointer at all**, since parking one in a corner for a turn that only answered a
787
+ question claims something that did not happen. While a reveal scrolls, it holds still — as a person's does — and
788
+ carries a chevron pointing the way the view is travelling, because stillness over a sliding page otherwise reads
789
+ as a pointer that has come loose rather than as the one doing the scrolling. The ring answers *where* and the
790
+ pointer answers *who*, which is why they are one default rather than two.
791
+ - **A form patch fans out over the fields it named.** `fill<Model>Form` is published by the form rather than by any
792
+ one control, so it rings one control per field in the arguments — resolved through the same
793
+ `data-akan-state="<model>Form.<field>"` the setter annotates — capped at five. `writeOn<Model>(path, value)`
794
+ draws nothing, for the same reason it carries no annotation: there is no control to put one on.