@llblab/pi-kit 0.1.13 → 0.2.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 (70) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +5 -3
  4. package/node_modules/@llblab/pi-actors/BACKLOG.md +1 -1
  5. package/node_modules/@llblab/pi-actors/CHANGELOG.md +15 -0
  6. package/node_modules/@llblab/pi-actors/README.md +5 -3
  7. package/node_modules/@llblab/pi-actors/dist/index.js +4 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +2 -2
  9. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +42 -19
  10. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.d.ts +2 -1
  11. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +7 -2
  12. package/node_modules/@llblab/pi-actors/dist/lib/limits.d.ts +9 -0
  13. package/node_modules/@llblab/pi-actors/dist/lib/limits.js +9 -0
  14. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +10 -11
  15. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +81 -56
  16. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +31 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +180 -0
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +115 -0
  19. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +623 -0
  20. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +3 -0
  21. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +341 -13
  22. package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.d.ts +1 -1
  23. package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.js +5 -3
  24. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.d.ts +16 -0
  25. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.js +143 -0
  26. package/node_modules/@llblab/pi-actors/dist/lib/temp.js +1 -1
  27. package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.js +3 -1
  28. package/node_modules/@llblab/pi-actors/dist/scripts/async-runner.mjs +5 -19
  29. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +2 -2
  30. package/node_modules/@llblab/pi-actors/dist/skills/actors/references/runs.md +1 -1
  31. package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +1 -1
  32. package/node_modules/@llblab/pi-actors/docs/README.md +1 -0
  33. package/node_modules/@llblab/pi-actors/docs/async-runs.md +8 -4
  34. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +207 -0
  35. package/node_modules/@llblab/pi-actors/index.ts +4 -1
  36. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +42 -21
  37. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +8 -3
  38. package/node_modules/@llblab/pi-actors/lib/limits.ts +9 -0
  39. package/node_modules/@llblab/pi-actors/lib/observability.ts +97 -78
  40. package/node_modules/@llblab/pi-actors/lib/pi.ts +210 -0
  41. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +800 -0
  42. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +370 -18
  43. package/node_modules/@llblab/pi-actors/lib/runs-trace.ts +6 -4
  44. package/node_modules/@llblab/pi-actors/lib/session-evidence.ts +153 -0
  45. package/node_modules/@llblab/pi-actors/lib/temp.ts +1 -1
  46. package/node_modules/@llblab/pi-actors/lib/tools-inspect.ts +4 -1
  47. package/node_modules/@llblab/pi-actors/package.json +3 -3
  48. package/node_modules/@llblab/pi-actors/scripts/async-runner.mjs +5 -19
  49. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +2 -2
  50. package/node_modules/@llblab/pi-actors/skills/actors/references/runs.md +1 -1
  51. package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +1 -1
  52. package/node_modules/@llblab/pi-telegram/AGENTS.md +4 -4
  53. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +11 -0
  54. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  55. package/node_modules/@llblab/pi-telegram/docs/activity.md +4 -4
  56. package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
  57. package/node_modules/@llblab/pi-telegram/docs/outbound.md +2 -2
  58. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -4
  59. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +4 -5
  60. package/node_modules/@llblab/pi-telegram/docs/voice.md +12 -2
  61. package/node_modules/@llblab/pi-telegram/index.ts +0 -1
  62. package/node_modules/@llblab/pi-telegram/lib/activity.ts +3 -5
  63. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +13 -17
  64. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +1 -1
  65. package/node_modules/@llblab/pi-telegram/lib/config.ts +25 -25
  66. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +10 -73
  67. package/node_modules/@llblab/pi-telegram/lib/status.ts +11 -1
  68. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  69. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +5 -3
  70. package/package.json +3 -3
@@ -105,7 +105,6 @@ export interface TelegramConfig {
105
105
  assistant?: {
106
106
  draftPreviews?: boolean;
107
107
  rendering?: TelegramAssistantRenderingMode;
108
- proactivePush?: boolean;
109
108
  activity?: TelegramActivityVerbosity;
110
109
  timeInjection?: TelegramTimeMode;
111
110
  /** @deprecated use activity */
@@ -454,10 +453,32 @@ function omitTelegramRootProfileFields(config: TelegramConfig): TelegramConfig {
454
453
  return sharedConfig;
455
454
  }
456
455
 
456
+ function omitRetiredProactivePush(config: TelegramConfig): {
457
+ config: TelegramConfig;
458
+ changed: boolean;
459
+ } {
460
+ const assistant = config.assistant as
461
+ | (NonNullable<TelegramConfig["assistant"]> & { proactivePush?: unknown })
462
+ | undefined;
463
+ if (!assistant || !Object.hasOwn(assistant, "proactivePush")) {
464
+ return { config, changed: false };
465
+ }
466
+ const { proactivePush: _proactivePush, ...remainingAssistant } = assistant;
467
+ const next = { ...config };
468
+ if (Object.keys(remainingAssistant).length > 0) {
469
+ next.assistant = remainingAssistant;
470
+ } else {
471
+ delete next.assistant;
472
+ }
473
+ return { config: next, changed: true };
474
+ }
475
+
457
476
  export function normalizeTelegramDefaultProfileConfig(config: TelegramConfig): {
458
477
  config: TelegramConfig;
459
478
  changed: boolean;
460
479
  } {
480
+ const retiredProactivePush = omitRetiredProactivePush(config);
481
+ config = retiredProactivePush.config;
461
482
  const hasLegacyRootProfile = [
462
483
  "botToken",
463
484
  "botUsername",
@@ -465,7 +486,9 @@ export function normalizeTelegramDefaultProfileConfig(config: TelegramConfig): {
465
486
  "allowedUserId",
466
487
  "lastUpdateId",
467
488
  ].some((field) => Object.hasOwn(config, field));
468
- if (!hasLegacyRootProfile) return { config, changed: false };
489
+ if (!hasLegacyRootProfile) {
490
+ return { config, changed: retiredProactivePush.changed };
491
+ }
469
492
  const canonicalProfile = config.profiles?.[TELEGRAM_DEFAULT_PROFILE_NAME];
470
493
  const legacyToken = config.botToken?.trim();
471
494
  if (Object.hasOwn(config, "botToken") && !legacyToken) {
@@ -712,27 +735,6 @@ export function createTelegramConfigStore(
712
735
  };
713
736
  }
714
737
 
715
- export function createTelegramProactivePushChecker(
716
- configStore: Pick<TelegramConfigStore, "get">,
717
- ): () => boolean {
718
- return () => configStore.get().assistant?.proactivePush ?? true;
719
- }
720
-
721
- export function createTelegramProactivePushSetter(
722
- configStore: TelegramMutableConfigStore,
723
- ): (enabled: boolean) => Promise<void> {
724
- return async (enabled) => {
725
- await loadLatestTelegramConfig(configStore);
726
- const current = configStore.get();
727
- const config: TelegramConfig = {
728
- ...current,
729
- assistant: { ...current.assistant, proactivePush: enabled },
730
- };
731
- configStore.set(config);
732
- await configStore.persist(config);
733
- };
734
- }
735
-
736
738
  export function createTelegramDraftPreviewsChecker(
737
739
  configStore: Pick<TelegramConfigStore, "get">,
738
740
  ): () => boolean {
@@ -1019,8 +1021,6 @@ export function createTelegramConfigControls(
1019
1021
  configStore: TelegramMutableConfigStore,
1020
1022
  ) {
1021
1023
  return {
1022
- isProactivePushEnabled: createTelegramProactivePushChecker(configStore),
1023
- setProactivePushEnabled: createTelegramProactivePushSetter(configStore),
1024
1024
  areDraftPreviewsEnabled: createTelegramDraftPreviewsChecker(configStore),
1025
1025
  setDraftPreviewsEnabled: createTelegramDraftPreviewsSetter(configStore),
1026
1026
  getAssistantRenderingMode:
@@ -21,7 +21,6 @@ import type { TelegramVoiceReplyMode } from "./voice.ts";
21
21
  export type TelegramSettingsMenuReplyMarkup = TelegramInlineKeyboardMarkup;
22
22
 
23
23
  export interface TelegramSettingsStateDeps {
24
- isProactivePushEnabled: () => boolean;
25
24
  areDraftPreviewsEnabled: () => boolean;
26
25
  getAssistantRenderingMode: () => TelegramAssistantRenderingMode;
27
26
  getActivityVerbosity: () => TelegramActivityVerbosity;
@@ -32,7 +31,6 @@ export interface TelegramSettingsStateDeps {
32
31
  }
33
32
 
34
33
  export interface TelegramSettingsMutationDeps extends TelegramSettingsStateDeps {
35
- setProactivePushEnabled: (enabled: boolean) => Promise<void>;
36
34
  setDraftPreviewsEnabled: (enabled: boolean) => Promise<void>;
37
35
  setAssistantRenderingMode: (
38
36
  mode: TelegramAssistantRenderingMode,
@@ -139,7 +137,6 @@ export interface TelegramSettingsMenuRuntimeDeps<
139
137
  export const SETTINGS_MENU_TITLE = "<b>⚙️ Settings:</b>";
140
138
  export const AUTOMATIC_THREAD_CLEANUP_SETTINGS_TITLE =
141
139
  "<b>🧹 Thread cleanup:</b>";
142
- export const PROACTIVE_PUSH_SETTINGS_TITLE = "<b>📌 Proactive push:</b>";
143
140
  export const DRAFT_PREVIEWS_SETTINGS_TITLE = "<b>📝 Draft previews:</b>";
144
141
  export const ASSISTANT_RENDERING_SETTINGS_TITLE =
145
142
  "<b>🧾 Assistant rendering:</b>";
@@ -181,19 +178,6 @@ export function buildAutomaticThreadCleanupSettingsText(
181
178
  ].join("\n");
182
179
  }
183
180
 
184
- export function buildProactivePushSettingsText(
185
- proactivePushEnabled: boolean,
186
- ): string {
187
- return [
188
- `${PROACTIVE_PUSH_SETTINGS_TITLE} <code>${proactivePushEnabled ? "on" : "off"}</code>`,
189
- "",
190
- "Control whether public assistant output from local/autonomous work is projected to Telegram.",
191
- "",
192
- "<code>-</code> <code>on</code> (default): send each completed public block, including visible checkpoints and the final answer, while connected.",
193
- "<code>-</code> <code>off</code>: keep local/autonomous assistant blocks in Pi; Telegram-originated replies still use their normal delivery path.",
194
- ].join("\n");
195
- }
196
-
197
181
  export function buildDraftPreviewsSettingsText(enabled: boolean): string {
198
182
  return [
199
183
  `${DRAFT_PREVIEWS_SETTINGS_TITLE} <code>${enabled ? "on" : "off"}</code>`,
@@ -265,7 +249,6 @@ export function buildTimeInjectionModeSettingsText(
265
249
  }
266
250
 
267
251
  export function buildTelegramSettingsMenuReplyMarkup(
268
- proactivePushEnabled: boolean,
269
252
  draftPreviewsEnabled: boolean,
270
253
  assistantRenderingModeOrVoiceReplyMode:
271
254
  TelegramAssistantRenderingMode | TelegramVoiceReplyMode,
@@ -325,10 +308,6 @@ export function buildTelegramSettingsMenuReplyMarkup(
325
308
  text: `🔬 Activity: ${activityVerbosity}`,
326
309
  callback_data: "settings:open:activity-verbosity",
327
310
  },
328
- {
329
- text: `📌 Proactive push: ${proactivePushEnabled ? "on" : "off"}`,
330
- callback_data: "settings:open:proactive",
331
- },
332
311
  {
333
312
  text: `🕒 Time injection: ${getTelegramSettingsStateValueLabel(timeInjectionMode)}`,
334
313
  callback_data: "settings:open:time-injection",
@@ -362,7 +341,6 @@ export async function openTelegramSettingsMenu<
362
341
  state,
363
342
  buildTelegramSettingsMenuText(),
364
343
  buildTelegramSettingsMenuReplyMarkup(
365
- deps.isProactivePushEnabled(),
366
344
  deps.areDraftPreviewsEnabled(),
367
345
  deps.getAssistantRenderingMode(),
368
346
  deps.getVoiceReplyMode(),
@@ -399,26 +377,6 @@ export function buildAutomaticThreadCleanupSettingsReplyMarkup(
399
377
  };
400
378
  }
401
379
 
402
- export function buildProactivePushSettingsReplyMarkup(
403
- proactivePushEnabled: boolean,
404
- ): TelegramSettingsMenuReplyMarkup {
405
- return {
406
- inline_keyboard: [
407
- [{ text: "⬆️ Back", callback_data: "settings:list" }],
408
- [
409
- {
410
- text: proactivePushEnabled ? "🟢 On" : "⚫️ On",
411
- callback_data: "settings:set:proactive:on",
412
- },
413
- {
414
- text: proactivePushEnabled ? "⚫️ Off" : "🟡 Off",
415
- callback_data: "settings:set:proactive:off",
416
- },
417
- ],
418
- ],
419
- };
420
- }
421
-
422
380
  export function buildDraftPreviewsSettingsReplyMarkup(
423
381
  enabled: boolean,
424
382
  ): TelegramSettingsMenuReplyMarkup {
@@ -521,7 +479,6 @@ export async function updateTelegramSettingsMenuMessage(
521
479
  await deps.updateSettingsMessage(
522
480
  buildTelegramSettingsMenuText(),
523
481
  buildTelegramSettingsMenuReplyMarkup(
524
- deps.isProactivePushEnabled(),
525
482
  deps.areDraftPreviewsEnabled(),
526
483
  deps.getAssistantRenderingMode(),
527
484
  deps.getVoiceReplyMode(),
@@ -544,16 +501,6 @@ export async function updateAutomaticThreadCleanupSettingsMessage(
544
501
  );
545
502
  }
546
503
 
547
- export async function updateProactivePushSettingsMessage(
548
- deps: TelegramSettingsMenuCallbackDeps,
549
- ): Promise<void> {
550
- const proactivePushEnabled = deps.isProactivePushEnabled();
551
- await deps.updateSettingsMessage(
552
- buildProactivePushSettingsText(proactivePushEnabled),
553
- buildProactivePushSettingsReplyMarkup(proactivePushEnabled),
554
- );
555
- }
556
-
557
504
  export async function updateDraftPreviewsSettingsMessage(
558
505
  deps: TelegramSettingsMenuCallbackDeps,
559
506
  ): Promise<void> {
@@ -621,9 +568,16 @@ export async function handleTelegramSettingsMenuCallbackAction(
621
568
  await deps.answerCallbackQuery(callbackQueryId);
622
569
  return true;
623
570
  }
624
- if (data === "settings:open:proactive") {
625
- await updateProactivePushSettingsMessage(deps);
626
- await deps.answerCallbackQuery(callbackQueryId);
571
+ if (
572
+ data === "settings:open:proactive" ||
573
+ data === "settings:set:proactive:on" ||
574
+ data === "settings:set:proactive:off"
575
+ ) {
576
+ await updateTelegramSettingsMenuMessage(deps, deps.sectionRegistry);
577
+ await deps.answerCallbackQuery(
578
+ callbackQueryId,
579
+ "Public assistant output is always delivered while Telegram is connected.",
580
+ );
627
581
  return true;
628
582
  }
629
583
  if (
@@ -751,19 +705,6 @@ export async function handleTelegramSettingsMenuCallbackAction(
751
705
  );
752
706
  return true;
753
707
  }
754
- if (
755
- data === "settings:set:proactive:on" ||
756
- data === "settings:set:proactive:off"
757
- ) {
758
- const enabled = data.endsWith(":on");
759
- await deps.setProactivePushEnabled(enabled);
760
- await updateProactivePushSettingsMessage(deps);
761
- await deps.answerCallbackQuery(
762
- callbackQueryId,
763
- `Proactive push ${enabled ? "enabled" : "disabled"}`,
764
- );
765
- return true;
766
- }
767
708
  await deps.answerCallbackQuery(callbackQueryId);
768
709
  return true;
769
710
  }
@@ -781,7 +722,6 @@ export function createTelegramSettingsMenuRuntime<
781
722
  return openTelegramSettingsMenu(
782
723
  {
783
724
  getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
784
- isProactivePushEnabled: deps.isProactivePushEnabled,
785
725
  areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
786
726
  getAssistantRenderingMode: deps.getAssistantRenderingMode,
787
727
  getActivityVerbosity: deps.getActivityVerbosity,
@@ -805,7 +745,6 @@ export function createTelegramSettingsMenuRuntime<
805
745
  await deps.reloadConfig?.();
806
746
  return updateTelegramSettingsMenuMessage(
807
747
  {
808
- isProactivePushEnabled: deps.isProactivePushEnabled,
809
748
  areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
810
749
  getAssistantRenderingMode: deps.getAssistantRenderingMode,
811
750
  getActivityVerbosity: deps.getActivityVerbosity,
@@ -849,7 +788,6 @@ export function createTelegramSettingsMenuRuntime<
849
788
  deps.storeModelMenuState(state);
850
789
  }
851
790
  return handleTelegramSettingsMenuCallbackAction(query.id, query.data, {
852
- isProactivePushEnabled: deps.isProactivePushEnabled,
853
791
  areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
854
792
  getAssistantRenderingMode: deps.getAssistantRenderingMode,
855
793
  getActivityVerbosity: deps.getActivityVerbosity,
@@ -857,7 +795,6 @@ export function createTelegramSettingsMenuRuntime<
857
795
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
858
796
  getTimeInjectionMode: deps.getTimeInjectionMode,
859
797
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
860
- setProactivePushEnabled: deps.setProactivePushEnabled,
861
798
  setDraftPreviewsEnabled: deps.setDraftPreviewsEnabled,
862
799
  setAssistantRenderingMode: deps.setAssistantRenderingMode,
863
800
  setActivityVerbosity: deps.setActivityVerbosity,
@@ -14,6 +14,7 @@ export interface TelegramUsageStats {
14
14
  totalCacheRead: number;
15
15
  totalCacheWrite: number;
16
16
  totalCost: number;
17
+ latestCacheHitRate?: number;
17
18
  }
18
19
 
19
20
  interface TelegramUsageMessage {
@@ -1473,6 +1474,9 @@ function collectUsageStats(ctx: TelegramStatusContext): TelegramUsageStats {
1473
1474
  stats.totalCacheRead += usage.cacheRead;
1474
1475
  stats.totalCacheWrite += usage.cacheWrite;
1475
1476
  stats.totalCost += usage.cost.total;
1477
+ const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
1478
+ stats.latestCacheHitRate =
1479
+ promptTokens > 0 ? (usage.cacheRead / promptTokens) * 100 : undefined;
1476
1480
  }
1477
1481
  return stats;
1478
1482
  }
@@ -1494,6 +1498,12 @@ function buildUsageSummary(stats: TelegramUsageStats): string | undefined {
1494
1498
  tokenParts.push(`R${formatTokens(stats.totalCacheRead)}`);
1495
1499
  if (stats.totalCacheWrite)
1496
1500
  tokenParts.push(`W${formatTokens(stats.totalCacheWrite)}`);
1501
+ if (
1502
+ (stats.totalCacheRead > 0 || stats.totalCacheWrite > 0) &&
1503
+ stats.latestCacheHitRate !== undefined
1504
+ ) {
1505
+ tokenParts.push(`CH${stats.latestCacheHitRate.toFixed(1)}%`);
1506
+ }
1497
1507
  return tokenParts.length > 0 ? tokenParts.join(" ") : undefined;
1498
1508
  }
1499
1509
 
@@ -1549,7 +1559,7 @@ export function buildStatusHtml(
1549
1559
  const usageSummary = buildUsageSummary(stats);
1550
1560
  const costSummary = buildCostSummary(stats, usesSubscription);
1551
1561
  if (usageSummary) {
1552
- lines.push(buildStatusRow("Usage", usageSummary));
1562
+ lines.push(buildStatusRow("Tokens", usageSummary));
1553
1563
  }
1554
1564
  if (costSummary) {
1555
1565
  lines.push(buildStatusRow("Cost", costSummary));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -136,11 +136,11 @@ Use the transport's canonical prompt-button syntax. For pi-telegram, one top-lev
136
136
 
137
137
  Model the control surface as an ordered ragged sequence of independently sized rows, not as a rectangular matrix to fill. Rectangular grids are one specialization for genuinely spatial or coordinate-bearing state; most interfaces should vary row width according to hierarchy, grouping, label pressure, and action priority.
138
138
 
139
- - Default to one full-width button per row for non-spatial controls. Put controls in one compact row only when they are genuine peers that answer the same local question or form one coherent toolbar/navigation group **and** their rendered labels comfortably fit a narrow phone-width chat.
139
+ - Default to one full-width button per row for non-spatial controls. In pi-telegram matrix syntax, emit these vertical controls as top-level cells without wrapping them in a nested row array. A nested array explicitly opts into horizontal grouping and is justified only when the controls are genuine peers and every label is unmistakably compact—roughly no more than 15 visible characters including its emoji and space. This is a judgment heuristic, not a request to mechanically count characters: if fit is uncertain, wording is multi-phrase, or truncation is plausible, keep top-level vertical buttons.
140
140
  - Use a singleton full-width row for a structurally independent, pinned, primary, summary, or high-consequence action, and whenever label length makes horizontal grouping cramped or ambiguous.
141
141
  - Vary row widths intentionally—for example `1 → 2 → 4 → 1 → 2`—and never pad a row with empty, duplicate, or no-op controls merely to produce uniform dimensions.
142
142
  - Preserve reading order across rows: orientation and structural navigation first, primary content or choices next, secondary controls afterward, and destructive actions visibly separated when present.
143
- - Treat two columns as an earned compact mode, not the default: a pair normally fits when each label is no more than one emoji plus roughly two average-length words. If either label has more words, unusually long words, qualifiers, or likely wrapping, place each button on its own row. Use at most two columns for readable text labels; move additional peer choices into more semantic rows rather than compressing textual buttons across a phone-width line. Three through five columns are for short symbols, glyphs, coordinates, or compact codes whose position carries meaning. Six through eight may be used only for single-glyph or similarly minimal position-bearing labels whose grouping materially improves the interaction; a row of emoji-only controls may therefore legitimately use up to eight columns. Eight is the phone-width UX maximum: never generate a row of nine or more controls even though the parser has no artificial width cap. Never shorten necessary wording merely to increase row density; regroup or use full-width rows when labels need explanation, wrap ambiguously, or lose meaning without prose.
143
+ - Treat two columns as an earned compact exception, never the default: a pair normally fits only when each label is unmistakably short—approximately one emoji plus one or two short words and roughly 15 visible characters or fewer. Do not count mechanically to justify density. If either label has more words, unusually long words, qualifiers, or any plausible wrapping or ellipsis risk, place each button on its own top-level row. Use at most two columns for readable text labels; move additional peer choices into more semantic rows rather than compressing textual buttons across a phone-width line. Three through five columns are for short symbols, glyphs, coordinates, or compact codes whose position carries meaning. Six through eight may be used only for single-glyph or similarly minimal position-bearing labels whose grouping materially improves the interaction; a row of emoji-only controls may therefore legitimately use up to eight columns. Eight is the phone-width UX maximum: never generate a row of nine or more controls even though the parser has no artificial width cap. Never shorten necessary wording merely to increase row density; regroup or use full-width rows when labels need explanation, wrap ambiguously, or lose meaning without prose.
144
144
 
145
145
  Treat vertical extent independently from horizontal density. A genuinely spatial surface may retain many rows—such as an `8×16` field—when vertical continuity, coordinates, and one-glance topology matter; do not paginate merely to make its height match its width. For non-spatial collections, however, a tall button wall should yield to semantic grouping, progressive disclosure, or pagination. Keep compact state and instructions above a tall surface, preserve stable coordinates across regeneration, and avoid repeating prose between rows.
146
146
 
@@ -187,7 +187,7 @@ Button prompts must:
187
187
  - Request fresh inspection when state may have changed.
188
188
  - Avoid embedding volatile output that should be rediscovered.
189
189
 
190
- Labels stay short, distinct, and scannable. Prefer an explicit `label` over exposing a long prompt as button text. Emoji are explicitly allowed and encouraged when one consistent semantic marker improves scanning or expressiveness; keep their meaning consistent across sibling controls, avoid decorative noise, and do not rely on emoji or color alone. If buttons are unavailable, render the same control surface as a numbered choice list.
190
+ Labels stay short, distinct, and scannable. Prefer an explicit `label` over exposing a long prompt as button text. Every generated human-readable action label must start with the most semantically appropriate emoji, followed by one ASCII space and concise action text; selecting that emoji is a required design step, not optional polish. When label and prompt are intentionally identical, use the same emoji-prefixed value rather than dropping the marker through compact syntax. Keep emoji meaning consistent across sibling controls, avoid decorative noise, and do not rely on emoji or color alone. An emoji-free text label remains a transport-compatible fallback only when no honest semantic emoji can be identified after considering the action, domain, and state; convenience, label pressure, or uncertainty between several reasonable candidates is not sufficient. Genuine coordinates, established symbolic controls, and intentionally emoji-only spatial cells keep their domain grammar. If buttons are unavailable, render the same control surface as a numbered choice list.
191
191
 
192
192
  ## Capability Adapters
193
193
 
@@ -243,6 +243,8 @@ Before sending a surface, verify:
243
243
  - Complete versus filtered or adapted output is labeled honestly.
244
244
  - No secret appears in visible text or button payloads.
245
245
  - Every button carries a valid self-contained next intent and measurably shortens likely feedback.
246
+ - Every human-readable action label uses `emoji + space + text`; an emoji-free fallback has an explicit semantic reason rather than mere convenience.
247
+ - Non-spatial action buttons are top-level vertical cells by default; every nested row has an obvious compact-peer justification with no plausible truncation risk.
246
248
  - The surface preserves free-form feedback when choices are not exhaustive.
247
249
  - High-impact operations route through confirmation.
248
250
  - Back/Up and Refresh appear only when useful.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.1.13",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -40,10 +40,10 @@
40
40
  "LICENSE"
41
41
  ],
42
42
  "dependencies": {
43
- "@llblab/pi-actors": "0.50.0",
43
+ "@llblab/pi-actors": "0.52.0",
44
44
  "@llblab/pi-codex-usage": "0.9.4",
45
45
  "@llblab/pi-grow-loop": "0.7.3",
46
- "@llblab/pi-telegram": "0.40.0"
46
+ "@llblab/pi-telegram": "0.41.0"
47
47
  },
48
48
  "bundledDependencies": [
49
49
  "@llblab/pi-actors",