@sayknow-cli/coding-agent 0.3.0 → 0.3.2

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 (233) hide show
  1. package/bin/skc.js +4 -0
  2. package/dist/types/cli/mcp-cli.d.ts +25 -0
  3. package/dist/types/cli/notify-cli.d.ts +2 -0
  4. package/dist/types/cli/plugin-cli.d.ts +2 -0
  5. package/dist/types/cli.d.ts +6 -0
  6. package/dist/types/commands/mcp.d.ts +70 -0
  7. package/dist/types/commands/plugin.d.ts +6 -0
  8. package/dist/types/commands/session.d.ts +6 -0
  9. package/dist/types/config/keybindings.d.ts +2 -2
  10. package/dist/types/config/model-profile-activation.d.ts +8 -1
  11. package/dist/types/config/settings-schema.d.ts +39 -2
  12. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  13. package/dist/types/defaults/skc-defaults.d.ts +2 -13
  14. package/dist/types/extensibility/shared-events.d.ts +1 -0
  15. package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
  16. package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
  17. package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
  18. package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
  19. package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
  20. package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
  21. package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
  22. package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
  23. package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
  24. package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
  25. package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
  26. package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
  27. package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
  28. package/dist/types/lsp/types.d.ts +2 -0
  29. package/dist/types/main.d.ts +2 -0
  30. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  31. package/dist/types/modes/components/model-selector.d.ts +8 -0
  32. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  33. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  34. package/dist/types/notifications/attachment-registry.d.ts +17 -0
  35. package/dist/types/notifications/chat-adapters.d.ts +9 -0
  36. package/dist/types/notifications/config.d.ts +9 -1
  37. package/dist/types/notifications/engine.d.ts +59 -0
  38. package/dist/types/notifications/html-format.d.ts +11 -0
  39. package/dist/types/notifications/index.d.ts +149 -1
  40. package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
  41. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  42. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  43. package/dist/types/notifications/managed-daemon.d.ts +48 -0
  44. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  45. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  46. package/dist/types/notifications/recent-activity.d.ts +35 -0
  47. package/dist/types/notifications/telegram-daemon.d.ts +133 -16
  48. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  49. package/dist/types/notifications/threaded-inbound.d.ts +19 -0
  50. package/dist/types/notifications/threaded-render.d.ts +6 -1
  51. package/dist/types/notifications/topic-registry.d.ts +12 -9
  52. package/dist/types/runtime-mcp/types.d.ts +7 -0
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +16 -4
  55. package/dist/types/session/blob-store.d.ts +25 -0
  56. package/dist/types/session/session-manager.d.ts +57 -0
  57. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  58. package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
  59. package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
  60. package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
  61. package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
  62. package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
  63. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  64. package/dist/types/system-prompt.d.ts +2 -0
  65. package/dist/types/task/executor.d.ts +9 -1
  66. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  67. package/dist/types/tools/fetch.d.ts +23 -0
  68. package/dist/types/tools/index.d.ts +4 -1
  69. package/dist/types/tools/telegram-send.d.ts +32 -0
  70. package/dist/types/utils/changelog.d.ts +1 -0
  71. package/dist/types/web/insane/bridge.d.ts +103 -0
  72. package/dist/types/web/insane/url-guard.d.ts +25 -0
  73. package/dist/types/web/scrapers/types.d.ts +5 -0
  74. package/dist/types/web/scrapers/utils.d.ts +7 -1
  75. package/dist/types/web/search/provider.d.ts +18 -1
  76. package/dist/types/web/search/providers/insane.d.ts +53 -0
  77. package/dist/types/web/search/providers/text-citations.d.ts +23 -0
  78. package/dist/types/web/search/types.d.ts +12 -4
  79. package/package.json +14 -10
  80. package/scripts/g004-tmux-smoke.ts +100 -0
  81. package/scripts/g005-daemon-smoke.ts +180 -0
  82. package/scripts/g011-daemon-path-smoke.ts +153 -0
  83. package/scripts/verify-insane-vendor.ts +132 -0
  84. package/src/cli/args.ts +1 -1
  85. package/src/cli/fast-help.ts +30 -2
  86. package/src/cli/mcp-cli.ts +272 -0
  87. package/src/cli/notify-cli.ts +152 -5
  88. package/src/cli/plugin-cli.ts +66 -3
  89. package/src/cli.ts +30 -12
  90. package/src/commands/mcp.ts +117 -0
  91. package/src/commands/plugin.ts +4 -0
  92. package/src/commands/session.ts +18 -0
  93. package/src/commands/team.ts +1 -1
  94. package/src/config/keybindings.ts +2 -2
  95. package/src/config/model-profile-activation.ts +55 -7
  96. package/src/config/settings-schema.ts +30 -1
  97. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  98. package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
  99. package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
  100. package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
  101. package/src/defaults/skc/skills/team/SKILL.md +5 -3
  102. package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
  103. package/src/defaults/skc-defaults.ts +2 -27
  104. package/src/export/html/index.ts +2 -2
  105. package/src/extensibility/extensions/runner.ts +1 -0
  106. package/src/extensibility/shared-events.ts +1 -0
  107. package/src/extensibility/skc-plugins/compiler.ts +351 -0
  108. package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
  109. package/src/extensibility/skc-plugins/index.ts +9 -0
  110. package/src/extensibility/skc-plugins/injection.ts +109 -0
  111. package/src/extensibility/skc-plugins/installer.ts +434 -0
  112. package/src/extensibility/skc-plugins/loader.ts +3 -1
  113. package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
  114. package/src/extensibility/skc-plugins/observability.ts +84 -0
  115. package/src/extensibility/skc-plugins/paths.ts +1 -1
  116. package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
  117. package/src/extensibility/skc-plugins/registry.ts +180 -0
  118. package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
  119. package/src/extensibility/skc-plugins/schema.ts +250 -20
  120. package/src/extensibility/skc-plugins/session-validation.ts +147 -0
  121. package/src/extensibility/skc-plugins/types.ts +199 -3
  122. package/src/extensibility/skc-plugins/validation.ts +80 -0
  123. package/src/extensibility/skills.ts +15 -0
  124. package/src/hooks/skill-state.ts +57 -0
  125. package/src/internal-urls/docs-index.generated.ts +17 -13
  126. package/src/lsp/config.ts +16 -3
  127. package/src/lsp/defaults.json +7 -0
  128. package/src/lsp/types.ts +2 -0
  129. package/src/main.ts +14 -3
  130. package/src/modes/bridge/bridge-mode.ts +11 -0
  131. package/src/modes/components/assistant-message.ts +49 -1
  132. package/src/modes/components/custom-editor.ts +2 -0
  133. package/src/modes/components/footer.ts +2 -3
  134. package/src/modes/components/hook-editor.ts +1 -1
  135. package/src/modes/components/hook-selector.ts +67 -43
  136. package/src/modes/components/model-selector.ts +56 -11
  137. package/src/modes/components/status-line/git-utils.ts +25 -0
  138. package/src/modes/components/status-line.ts +10 -11
  139. package/src/modes/components/welcome.ts +2 -3
  140. package/src/modes/controllers/event-controller.ts +15 -0
  141. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  142. package/src/modes/controllers/selector-controller.ts +53 -11
  143. package/src/modes/interactive-mode.ts +50 -3
  144. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  145. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  146. package/src/modes/theme/defaults/index.ts +2 -0
  147. package/src/modes/utils/context-usage.ts +2 -2
  148. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  149. package/src/notifications/attachment-registry.ts +23 -0
  150. package/src/notifications/chat-adapters.ts +147 -0
  151. package/src/notifications/config.ts +23 -2
  152. package/src/notifications/engine.ts +100 -0
  153. package/src/notifications/html-format.ts +38 -0
  154. package/src/notifications/index.ts +417 -45
  155. package/src/notifications/lifecycle-commands.ts +238 -0
  156. package/src/notifications/lifecycle-control-runtime.ts +405 -0
  157. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  158. package/src/notifications/managed-daemon.ts +163 -0
  159. package/src/notifications/operator-runtime.ts +171 -0
  160. package/src/notifications/rate-limit-pool.ts +19 -0
  161. package/src/notifications/recent-activity.ts +132 -0
  162. package/src/notifications/telegram-daemon.ts +984 -242
  163. package/src/notifications/telegram-reference.ts +25 -7
  164. package/src/notifications/threaded-inbound.ts +60 -4
  165. package/src/notifications/threaded-render.ts +20 -2
  166. package/src/notifications/topic-registry.ts +23 -9
  167. package/src/prompts/agents/executor.md +2 -2
  168. package/src/runtime-mcp/transports/stdio.ts +38 -4
  169. package/src/runtime-mcp/types.ts +7 -0
  170. package/src/sdk.ts +157 -10
  171. package/src/session/agent-session.ts +248 -125
  172. package/src/session/blob-store.ts +196 -8
  173. package/src/session/session-manager.ts +762 -12
  174. package/src/skc-runtime/launch-tmux.ts +85 -22
  175. package/src/skc-runtime/ledger-event-renderer.ts +1 -0
  176. package/src/skc-runtime/psmux-detect.ts +239 -0
  177. package/src/skc-runtime/ralplan-runtime.ts +2 -2
  178. package/src/skc-runtime/team-runtime.ts +56 -23
  179. package/src/skc-runtime/tmux-common.ts +88 -4
  180. package/src/skc-runtime/tmux-sessions.ts +111 -9
  181. package/src/skc-runtime/ultragoal-guard.ts +25 -8
  182. package/src/skc-runtime/ultragoal-runtime.ts +75 -15
  183. package/src/skc-runtime/workflow-manifest.generated.json +29 -0
  184. package/src/skc-runtime/workflow-manifest.ts +7 -2
  185. package/src/slash-commands/builtin-registry.ts +23 -3
  186. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  187. package/src/slash-commands/helpers/parse.ts +2 -1
  188. package/src/system-prompt.ts +9 -0
  189. package/src/task/executor.ts +31 -7
  190. package/src/task/index.ts +2 -0
  191. package/src/tools/ask.ts +5 -1
  192. package/src/tools/bash.ts +9 -0
  193. package/src/tools/composer-bash-policy.ts +96 -0
  194. package/src/tools/fetch.ts +94 -1
  195. package/src/tools/index.ts +6 -1
  196. package/src/tools/telegram-send.ts +137 -0
  197. package/src/utils/changelog.ts +8 -0
  198. package/src/web/insane/bridge.ts +350 -0
  199. package/src/web/insane/url-guard.ts +159 -0
  200. package/src/web/scrapers/types.ts +143 -45
  201. package/src/web/scrapers/utils.ts +70 -19
  202. package/src/web/search/provider.ts +77 -18
  203. package/src/web/search/providers/anthropic.ts +70 -3
  204. package/src/web/search/providers/codex.ts +1 -119
  205. package/src/web/search/providers/gemini.ts +99 -0
  206. package/src/web/search/providers/insane.ts +551 -0
  207. package/src/web/search/providers/openai-compatible.ts +66 -32
  208. package/src/web/search/providers/text-citations.ts +111 -0
  209. package/src/web/search/types.ts +13 -2
  210. package/vendor/insane-search/LICENSE +21 -0
  211. package/vendor/insane-search/MANIFEST.json +24 -0
  212. package/vendor/insane-search/engine/__init__.py +23 -0
  213. package/vendor/insane-search/engine/__main__.py +128 -0
  214. package/vendor/insane-search/engine/bias_check.py +183 -0
  215. package/vendor/insane-search/engine/executor.py +254 -0
  216. package/vendor/insane-search/engine/fetch_chain.py +725 -0
  217. package/vendor/insane-search/engine/learning.py +175 -0
  218. package/vendor/insane-search/engine/phase0.py +214 -0
  219. package/vendor/insane-search/engine/safety.py +91 -0
  220. package/vendor/insane-search/engine/templates/package.json +11 -0
  221. package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
  222. package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
  223. package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
  224. package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
  225. package/vendor/insane-search/engine/tests/test_u1.py +200 -0
  226. package/vendor/insane-search/engine/tests/test_u4.py +131 -0
  227. package/vendor/insane-search/engine/tests/test_u5.py +163 -0
  228. package/vendor/insane-search/engine/tests/test_u7.py +124 -0
  229. package/vendor/insane-search/engine/transport.py +211 -0
  230. package/vendor/insane-search/engine/url_transforms.py +98 -0
  231. package/vendor/insane-search/engine/validators.py +331 -0
  232. package/vendor/insane-search/engine/waf_detector.py +214 -0
  233. package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
@@ -5,6 +5,14 @@ export interface NotificationConfig {
5
5
  enabled: boolean;
6
6
  botToken?: string;
7
7
  chatId?: string;
8
+ discord: {
9
+ botToken?: string;
10
+ channelId?: string;
11
+ };
12
+ slack: {
13
+ botToken?: string;
14
+ channelId?: string;
15
+ };
8
16
  redact: boolean;
9
17
  verbosity: "lean" | "verbose";
10
18
  idleTimeoutMs: number;
@@ -16,15 +24,28 @@ export function getNotificationConfig(settings: Settings): NotificationConfig {
16
24
  enabled: settings.get("notifications.enabled"),
17
25
  botToken: settings.get("notifications.telegram.botToken"),
18
26
  chatId: settings.get("notifications.telegram.chatId"),
27
+ discord: {
28
+ botToken: settings.get("notifications.discord.botToken"),
29
+ channelId: settings.get("notifications.discord.channelId"),
30
+ },
31
+ slack: {
32
+ botToken: settings.get("notifications.slack.botToken"),
33
+ channelId: settings.get("notifications.slack.channelId"),
34
+ },
19
35
  redact: settings.get("notifications.redact"),
20
36
  verbosity: settings.get("notifications.verbosity") === "verbose" ? "verbose" : "lean",
21
37
  idleTimeoutMs: settings.get("notifications.daemon.idleTimeoutMs"),
22
38
  };
23
39
  }
24
40
 
25
- /** Is global config sufficient for auto-on (enabled + botToken + chatId all present)? */
41
+ /** Is global config sufficient for auto-on (enabled + at least one configured adapter)? */
26
42
  export function isGloballyConfigured(cfg: NotificationConfig): boolean {
27
- return cfg.enabled && Boolean(cfg.botToken) && Boolean(cfg.chatId);
43
+ return (
44
+ cfg.enabled &&
45
+ ((Boolean(cfg.botToken) && Boolean(cfg.chatId)) ||
46
+ (Boolean(cfg.discord.botToken) && Boolean(cfg.discord.channelId)) ||
47
+ (Boolean(cfg.slack.botToken) && Boolean(cfg.slack.channelId)))
48
+ );
28
49
  }
29
50
 
30
51
  /** Resolve whether the notifications extension should be registered at SDK startup. */
@@ -0,0 +1,100 @@
1
+ import { buildRedactedAction, type RedactableAction } from "./config";
2
+
3
+ export type NotificationEvent =
4
+ | ({ type: "action_needed" } & RedactableAction)
5
+ | { type: "action_resolved"; id: string; sessionId: string; resolvedBy?: string }
6
+ | { type: "frame"; sessionId: string; frame: Record<string, unknown> };
7
+
8
+ export interface NotificationReplyRoute {
9
+ sessionId: string;
10
+ actionId: string;
11
+ answer: number | string | { selected?: Array<number | string>; custom?: string };
12
+ }
13
+
14
+ export interface NotificationAdapterPayload {
15
+ adapter: string;
16
+ channelKey?: string;
17
+ body: unknown;
18
+ route?: Omit<NotificationReplyRoute, "answer">;
19
+ }
20
+
21
+ export interface NotificationPresentationAdapter {
22
+ readonly kind: "telegram" | "discord" | "slack";
23
+ render(event: NotificationEvent): NotificationAdapterPayload[];
24
+ mapInbound(input: unknown): NotificationReplyRoute | undefined;
25
+ }
26
+
27
+ export interface EngineSessionSink {
28
+ sendReply(route: NotificationReplyRoute): void;
29
+ }
30
+
31
+ export interface NotificationEngineOptions {
32
+ redact: boolean;
33
+ sessionTag: (sessionId: string) => string;
34
+ }
35
+
36
+ /**
37
+ * Shared presentation engine for managed notification clients.
38
+ *
39
+ * It owns fanout, redaction boundaries, pending-action routing, and reply
40
+ * delivery into session sinks. Transport adapters stay pure: render an internal
41
+ * event into a public-safe payload and map an inbound transport interaction
42
+ * back into a session/action answer.
43
+ */
44
+ export class NotificationPresentationEngine {
45
+ readonly adapters: readonly NotificationPresentationAdapter[];
46
+ private readonly sessions = new Map<string, EngineSessionSink>();
47
+ private readonly pending = new Map<string, { sessionId: string; actionId: string }>();
48
+
49
+ constructor(
50
+ adapters: readonly NotificationPresentationAdapter[],
51
+ private readonly opts: NotificationEngineOptions,
52
+ ) {
53
+ this.adapters = adapters;
54
+ }
55
+
56
+ connectSession(sessionId: string, sink: EngineSessionSink): void {
57
+ this.sessions.set(sessionId, sink);
58
+ }
59
+
60
+ dropSession(sessionId: string): void {
61
+ this.sessions.delete(sessionId);
62
+ for (const [key, route] of this.pending) {
63
+ if (route.sessionId === sessionId) this.pending.delete(key);
64
+ }
65
+ }
66
+
67
+ fanout(event: NotificationEvent): NotificationAdapterPayload[] {
68
+ const safeEvent = this.redactEvent(event);
69
+ if (safeEvent.type === "action_needed" && safeEvent.kind === "ask") {
70
+ this.pending.set(safeEvent.id, { sessionId: safeEvent.sessionId, actionId: safeEvent.id });
71
+ }
72
+ if (safeEvent.type === "action_resolved") {
73
+ this.pending.delete(safeEvent.id);
74
+ }
75
+ return this.adapters.flatMap(adapter => adapter.render(safeEvent));
76
+ }
77
+
78
+ routeInbound(adapterKind: NotificationPresentationAdapter["kind"], input: unknown): boolean {
79
+ const adapter = this.adapters.find(candidate => candidate.kind === adapterKind);
80
+ const route = adapter?.mapInbound(input);
81
+ if (!route) return false;
82
+ const pending = this.pending.get(route.actionId);
83
+ if (!pending || pending.sessionId !== route.sessionId) return false;
84
+ const sink = this.sessions.get(route.sessionId);
85
+ if (!sink) return false;
86
+ sink.sendReply(route);
87
+ return true;
88
+ }
89
+
90
+ private redactEvent(event: NotificationEvent): NotificationEvent {
91
+ if (event.type !== "action_needed") return event;
92
+ return {
93
+ ...buildRedactedAction(event, {
94
+ redact: this.opts.redact,
95
+ sessionTag: this.opts.sessionTag(event.sessionId),
96
+ }),
97
+ type: "action_needed",
98
+ };
99
+ }
100
+ }
@@ -349,16 +349,54 @@ export function buttonLabel(label: string, index: number): string {
349
349
  return `${index + 1}. ${stripped}`;
350
350
  }
351
351
 
352
+ /** Numbered, escaped option list for the Telegram message body. */
353
+ export function numberedOptionList(labels: string[]): string {
354
+ return labels.map((label, i) => `${i + 1}. ${escapeHtml(label.replace(/^\s*\d+[.)]\s+/, ""))}`).join("\n");
355
+ }
356
+
357
+ /** Compact numeric button label; full option text belongs in the message body. */
358
+ export function choiceButtonLabel(index: number): string {
359
+ return String(index + 1);
360
+ }
361
+
352
362
  export interface InlineButton {
353
363
  text: string;
354
364
  callback_data: string;
355
365
  }
356
366
 
367
+ const COMPACT_BUTTONS_PER_ROW = 5;
368
+
357
369
  /** A prefixed button label is "long" when it is wide or contains a newline. */
358
370
  function isLongLabel(label: string): boolean {
359
371
  return label.length > 18 || /[\r\n]/.test(label);
360
372
  }
361
373
 
374
+ /**
375
+ * Lay out option callbacks as compact numeric buttons. Telegram mobile clients
376
+ * ellipsize long inline-keyboard labels and tall keyboards can be obscured by
377
+ * the composer, so the full choice text is rendered in the message body while
378
+ * the keyboard keeps only stable one-based tap targets.
379
+ */
380
+ export function buildCompactChoiceGrid(
381
+ labels: string[],
382
+ callbackForIndex: (index: number) => string,
383
+ ): InlineButton[][] {
384
+ const rows: InlineButton[][] = [];
385
+ let run: InlineButton[] = [];
386
+ const flush = () => {
387
+ if (run.length) {
388
+ rows.push(run);
389
+ run = [];
390
+ }
391
+ };
392
+ labels.forEach((_label, i) => {
393
+ run.push({ text: choiceButtonLabel(i), callback_data: callbackForIndex(i) });
394
+ if (run.length === COMPACT_BUTTONS_PER_ROW) flush();
395
+ });
396
+ flush();
397
+ return rows;
398
+ }
399
+
362
400
  /**
363
401
  * Lay out option labels as a numbered button grid. Long buttons take a
364
402
  * full-width row; runs of short buttons are packed into rows of up to 3. The