@bismawy/pi-arnative 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.8] - 2026-09-28
4
+
5
+ ### Fixed
6
+ - Custom `ctrl+alt+<letter>` shortcuts (`t` next tab, `n` new session, `r` reload) never fired on Windows: Windows Terminal ≥ 1.24 aliases every `Ctrl+Alt` combo to AltGr ([microsoft/terminal#20052](https://github.com/microsoft/terminal/pull/20052)), so the legacy `ESC` + control-byte encoding is never emitted. All of them now also accept `alt+<letter>` on Windows **and WSL** (same input translation), matching pi's own convention (`alt+v`, `alt+p`). Linux keeps `ctrl+alt`, macOS keeps `ctrl+option` (plain Option composes characters there).
7
+ - Shortcut ids, platform variants and cheatsheet labels now come from one place (`lib/shortcuts.ts`) instead of being spelled out per extension; the label is derived from the key id that is actually registered, so a cheatsheet can no longer advertise a key the platform never binds.
8
+ - README: platform notes for Windows/WSL, macOS, Linux desktops and multiplexers, including `PI_TUI_ESC_TIMEOUT` for the intermittent “Escape + stray letter” glitch (`ESC`-prefixed shortcuts flushed by pi's ~10 ms lone-ESC timer).
9
+ - Reload state for the chat clock now lives on a `globalThis` symbol instead of module locals: `/reload` re-imports the module while the prototype patches stay on the class, so the surviving render closure and the new `session_start` handler read the same map and theme. The old shape captured `ctx` in the module, which pi rejects after reload/session replacement, and that throw surfaced as an uncaughtException during a render pass.
10
+
11
+ ---
12
+
3
13
  ## [0.2.7] - 2026-09-28
4
14
 
5
15
  ### Fixed
package/README.md CHANGED
@@ -38,22 +38,33 @@ pi --extension ./extensions/footer.ts
38
38
  | Key / Action | Result |
39
39
  | --- | --- |
40
40
  | `/usage` | Show session usage & token metrics |
41
- | `ctrl+alt+t` | Cycle header tabs (Directory → Model → Context → Skills → Extensions → Themes → Shortcut) |
42
- | `ctrl+alt+n` | Start a new session (`/new`) — see [Keybindings](#keybindings) |
43
- | `ctrl+alt+r` | Quick reload runtime (`/reload`) |
41
+ | `ctrl+alt+t` (`alt+t` on Windows/WSL) | Cycle header tabs (Directory → Model → Context → Skills → Extensions → Themes → Shortcut) |
42
+ | `ctrl+alt+n` (`alt+n` on Windows/WSL) | Start a new session (`/new`) — see [Keybindings](#keybindings) |
43
+ | `ctrl+alt+r` (`alt+r` on Windows/WSL) | Quick reload runtime (`/reload`) |
44
44
  | Click tab | Open selected header tab |
45
45
  | Click tool / `ctrl+e` | Toggle tool output box |
46
46
 
47
- Same cheatsheet in the header's **Shortcut** tab; on narrow terminals the rightmost tabs drop off first.
47
+ Same cheatsheet in the header's **Shortcut** tab — it shows the variant your platform actually uses (on macOS `alt` reads `option`); on narrow terminals the rightmost tabs drop off first.
48
+
49
+ ### Platform notes
50
+
51
+ - **Windows / WSL:** Windows Terminal ≥ 1.24 aliases every `Ctrl+Alt` combo to AltGr ([microsoft/terminal#20052](https://github.com/microsoft/terminal/pull/20052)), so the legacy `ESC` + control-byte encoding never leaves the terminal. Both variants are registered there; `alt+…` is what pi itself uses for the same reason (`alt+v` paste image, `alt+p` cycle model). Setting `"altGrAliasing": false` in your Windows Terminal profile brings `ctrl+alt+…` back.
52
+ - **macOS:** only `ctrl+option+…` is registered — plain Option composes characters (`Option+r` = ®) unless “Use Option as Meta key” is on. Works in iTerm2 and Terminal.app as-is.
53
+ - **Linux desktops:** `ctrl+alt+t` is “launch terminal” on Ubuntu/Fedora GNOME, which eats the key before pi sees it. Rebind it there if you want tab cycling.
54
+ - **Multiplexers (Herdr/tmux) + slow PTYs:** every `ctrl+alt+…` / `alt+…` shortcut is sent as `ESC` followed by a byte. pi waits only ~10 ms for that second byte before treating a lone `ESC` as the Escape key — and an Escape mid-turn aborts it, leaving a stray `r`/`t`/`n` in the editor. That is why the shortcut can look “glitchy” and why it is intermittent. Raise the window with `PI_TUI_ESC_TIMEOUT=100` in the environment that launches pi (same knob pi auto-applies over SSH, at the cost of a ~100 ms delay before a real Escape registers):
55
+
56
+ ```powershell
57
+ $env:PI_TUI_ESC_TIMEOUT = 100; pi
58
+ ```
48
59
 
49
60
  ## Keybindings
50
61
 
51
62
  `ctrl+alt+n` is a plain user keybinding, not a package shortcut — pi-arnative never registers it.
52
- Add it once to `~/.pi/agent/keybindings.json`:
63
+ Add it once to `~/.pi/agent/keybindings.json` (the list form keeps Linux and Windows working):
53
64
 
54
65
  ```json
55
66
  {
56
- "app.session.new": "ctrl+alt+n"
67
+ "app.session.new": ["ctrl+alt+n", "alt+n"]
57
68
  }
58
69
  ```
59
70
 
@@ -9,16 +9,17 @@
9
9
  */
10
10
  import type { AssistantMessage } from "@earendil-works/pi-ai";
11
11
  import { CustomEditor, FooterComponent, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
- import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
12
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
13
13
  import { execFile } from "node:child_process";
14
14
  import { stripAnsi } from "../lib/ansi.ts";
15
15
  import { assert, isMain } from "../lib/check.ts";
16
16
  import { capitalize, modelDisplayParts } from "../lib/format.ts";
17
+ import { IS_WINDOWS_LIKE, SHORTCUT_RELOAD } from "../lib/shortcuts.ts";
17
18
  import { formatTokens } from "../lib/usage-store.ts";
18
19
 
20
+ // ctrl+alt+r (alt+r on Windows/WSL — see lib/shortcuts.ts).
19
21
  // The TUI drops key-release events before handleInput, so press+release cannot
20
22
  // fire this twice even when the terminal reports event types (Kitty flag 2).
21
- const RELOAD_SHORTCUT = Key.ctrlAlt("r");
22
23
 
23
24
  // Intercept the built-in FooterComponent to force a pure 2-line footer
24
25
  const PATCHED_KEY = Symbol.for("pi-arnative.footer2LinesPatched");
@@ -464,7 +465,7 @@ export default function (pi: ExtensionAPI) {
464
465
  }
465
466
 
466
467
  handleInput(data: string) {
467
- if (matchesKey(data, RELOAD_SHORTCUT)) {
468
+ if (SHORTCUT_RELOAD.matches(data)) {
468
469
  this.setText("");
469
470
  if (this.onSubmit) {
470
471
  this.onSubmit("/reload");
@@ -642,9 +643,10 @@ if (isMain(import.meta.url)) {
642
643
 
643
644
  // Guard: the reload shortcut must match every encoding a terminal can deliver,
644
645
  // so a typo in the key id fails here instead of silently doing nothing.
645
- assert(matchesKey("\x1b\x12", RELOAD_SHORTCUT), "shortcut matches legacy ctrl+alt+r (ESC + 0x12)");
646
- assert(matchesKey("\x1b[114;7u", RELOAD_SHORTCUT), "shortcut matches Kitty CSI-u ctrl+alt+r");
647
- assert(!matchesKey("\x12", RELOAD_SHORTCUT), "plain ctrl+r does not trigger reload");
648
- assert(!matchesKey("r", RELOAD_SHORTCUT), "bare letter r does not trigger reload");
646
+ assert(SHORTCUT_RELOAD.matches("\x1b\x12"), "shortcut matches legacy ctrl+alt+r (ESC + 0x12)");
647
+ assert(SHORTCUT_RELOAD.matches("\x1b[114;7u"), "shortcut matches Kitty CSI-u ctrl+alt+r");
648
+ assert(SHORTCUT_RELOAD.matches("\x1br") === IS_WINDOWS_LIKE, "ESC + r triggers reload only where alt+r is bound");
649
+ assert(!SHORTCUT_RELOAD.matches("\x12"), "plain ctrl+r does not trigger reload");
650
+ assert(!SHORTCUT_RELOAD.matches("r"), "bare letter r does not trigger reload");
649
651
  console.log("footer.ts self-check OK");
650
652
  }
@@ -24,7 +24,6 @@ import {
24
24
  import {
25
25
  truncateToWidth,
26
26
  visibleWidth,
27
- Key,
28
27
  type Component,
29
28
  type TUI,
30
29
  type TuiMouseEvent,
@@ -34,11 +33,12 @@ import { renderBoxLines } from "../lib/box.ts";
34
33
  import { stripAnsi, themeOf } from "../lib/ansi.ts";
35
34
  import { assert, isMain } from "../lib/check.ts";
36
35
  import { capitalize, modelDisplayParts } from "../lib/format.ts";
36
+ import { SHORTCUT_NEW_SESSION, SHORTCUT_NEXT_TAB, SHORTCUT_RELOAD } from "../lib/shortcuts.ts";
37
37
  import { formatTokens, getModelAllTimeUsage } from "../lib/usage-store.ts";
38
38
 
39
39
  export type TabKey = "Model" | "Directory" | "Context" | "Skills" | "Extensions" | "Themes" | "Shortcut";
40
40
 
41
- // Tab order for Ctrl+Alt+T and for the menu line.
41
+ // Tab order for the next-tab shortcut and for the menu line.
42
42
  export const TAB_ORDER: TabKey[] = ["Directory", "Model", "Context", "Skills", "Extensions", "Themes", "Shortcut"];
43
43
 
44
44
  export const SECTION_ICONS: Record<TabKey, string> = {
@@ -176,9 +176,9 @@ export const SHORTCUTS: string[] = [
176
176
  "[/] Commands",
177
177
  "[!] Bash",
178
178
  "[Ctrl+o] More/expand",
179
- "[Ctrl+alt+t] Next tab",
180
- "[Ctrl+alt+n] New",
181
- "[Ctrl+alt+r] Reload",
179
+ `[${SHORTCUT_NEXT_TAB.label}] Next tab`,
180
+ `[${SHORTCUT_NEW_SESSION.label}] New`,
181
+ `[${SHORTCUT_RELOAD.label}] Reload`,
182
182
  ];
183
183
 
184
184
  export function wrapCommaItems(items: string[], maxWidth: number): string[] {
@@ -633,17 +633,17 @@ if (UserMessageComponent?.prototype && !(globalThis as Record<symbol, boolean>)[
633
633
  }
634
634
 
635
635
  export default function (pi: ExtensionAPI) {
636
- // ctrl+alt+t: next tab.
637
- pi.registerShortcut(Key.ctrlAlt("t"), {
638
- description: "Next header tab",
639
- handler: async () => {
640
- const hdr = activeHeaderInstance;
641
- if (!hdr) return;
642
- const order = TAB_ORDER;
643
- hdr.activeTab = order[(order.indexOf(hdr.activeTab) + 1) % order.length]!;
644
- hdr.tui?.requestRender?.();
645
- },
646
- });
636
+ // Next header tab — every platform variant of the shortcut (lib/shortcuts.ts).
637
+ const nextTab = async () => {
638
+ const hdr = activeHeaderInstance;
639
+ if (!hdr) return;
640
+ const order = TAB_ORDER;
641
+ hdr.activeTab = order[(order.indexOf(hdr.activeTab) + 1) % order.length]!;
642
+ hdr.tui?.requestRender?.();
643
+ };
644
+ for (const key of SHORTCUT_NEXT_TAB.keys) {
645
+ pi.registerShortcut(key, { description: "Next header tab", handler: nextTab });
646
+ }
647
647
 
648
648
  pi.on("session_start", async (_event, ctx) => {
649
649
  activeThemeProxy = themeOf<Themeish>(ctx) ?? activeThemeProxy;
@@ -860,7 +860,10 @@ if (isMain(import.meta.url)) {
860
860
  );
861
861
  const scContent = stripAnsi(linesShortcut.slice(6).join(" "));
862
862
  assert(scContent.includes("[Esc] Interrupt"), "Shortcut tab: legend moved into the content");
863
- assert(scContent.includes("[Ctrl+alt+r] Reload"), "Shortcut tab: reload shortcut present in the content");
863
+ assert(
864
+ scContent.includes(`[${SHORTCUT_RELOAD.label}] Reload`),
865
+ `Shortcut tab: reload shortcut present in the content (${SHORTCUT_RELOAD.label})`,
866
+ );
864
867
  assert(stripAnsi(linesShortcut[2]!).includes("Arnative v"), "line 2 keeps the Arnative brand while the Shortcut tab is active");
865
868
  hdr.activeTab = "Model";
866
869
  hdr.render(140);
@@ -15,9 +15,19 @@ import { sliceByColumn, visibleWidth } from "@earendil-works/pi-tui";
15
15
  import { ansiBgOpen, stripAnsi, themeOf } from "../lib/ansi.ts";
16
16
  import { assert, isMain } from "../lib/check.ts";
17
17
 
18
- const USER_TIMESTAMPS_MAP = new Map<string, number>();
19
- let getBranch: (() => readonly unknown[]) | null = null;
20
- let prefilled = false;
18
+ // Shared across reloads: /reload re-imports this module (moduleCache: false), but the
19
+ // prototype patches below survive on the class, so the patched closure and the newly
20
+ // registered handlers must read the SAME map and theme. Module-local state would leave
21
+ // the closure on stale data — and a captured ctx even on an invalidated one (pi throws
22
+ // "stale after session replacement or reload" inside the render pass -> uncaughtException).
23
+ type ThemeProxy = { fg(color: string, text: string): string; bg?(color: string, text: string): string } | null;
24
+ const TS_STATE_KEY = Symbol.for("pi-arnative.chatTimestampState");
25
+ const tsState = ((globalThis as Record<symbol, unknown>)[TS_STATE_KEY] ??= {
26
+ patched: false,
27
+ map: new Map<string, number>(),
28
+ theme: null as ThemeProxy,
29
+ }) as { patched: boolean; map: Map<string, number>; theme: ThemeProxy };
30
+ const USER_TIMESTAMPS_MAP = tsState.map;
21
31
 
22
32
  function userTextOf(message: { content?: unknown }): string {
23
33
  const c = message.content;
@@ -44,8 +54,6 @@ function prefillUserTimestamps(entries: readonly unknown[]): void {
44
54
  if (text) USER_TIMESTAMPS_MAP.set(text, m.timestamp || Date.parse(entry.timestamp ?? "") || Date.now());
45
55
  }
46
56
  }
47
- let activeThemeProxy: { fg(color: string, text: string): string; bg?(color: string, text: string): string } | null = null;
48
-
49
57
  function formatClock(timestamp?: number): string {
50
58
  const d = timestamp && timestamp > 0 ? new Date(timestamp) : new Date();
51
59
  const h = String(d.getHours()).padStart(2, "0");
@@ -98,9 +106,8 @@ export function placeTimeAtRight(
98
106
  return `${leftPart}${bg}${pad}${timeBadge} ${reset}`;
99
107
  }
100
108
 
101
- const CHAT_TIMESTAMP_PATCHED = Symbol.for("pi-arnative.chatTimestampPatched");
102
- if (!(globalThis as Record<symbol, boolean>)[CHAT_TIMESTAMP_PATCHED]) {
103
- (globalThis as Record<symbol, boolean>)[CHAT_TIMESTAMP_PATCHED] = true;
109
+ if (!tsState.patched) {
110
+ tsState.patched = true;
104
111
 
105
112
  if (UserMessageComponent?.prototype?.render) {
106
113
  const origRender = UserMessageComponent.prototype.render;
@@ -110,22 +117,15 @@ if (!(globalThis as Record<symbol, boolean>)[CHAT_TIMESTAMP_PATCHED]) {
110
117
 
111
118
  // Fix the bubble bg first (reset Markdown lines make padding dark),
112
119
  // then paste the clock (which deliberately drops the bg in the badge area).
113
- const bgOpen = ansiBgOpen(activeThemeProxy, "userMessageBg");
120
+ const bgOpen = ansiBgOpen(tsState.theme, "userMessageBg");
114
121
  for (let i = 0; i < lines.length; i++) lines[i] = repairBubbleBg(lines[i], bgOpen);
115
122
 
116
- // Restore/reload safe: old user messages arrive without message_start events,
117
- // so fill the map once from the session history before the first lookup.
118
- if (!prefilled && getBranch) {
119
- prefillUserTimestamps(getBranch());
120
- prefilled = true;
121
- }
122
-
123
123
  const textKey = (this as { text?: string }).text?.trim() ?? "";
124
124
  const ts = USER_TIMESTAMPS_MAP.get(textKey);
125
125
  // No known timestamp -> no clock (a "now" clock would be a lie on old messages).
126
126
  if (!ts) return lines;
127
127
  const timeStr = formatClock(ts);
128
- const badge = formatTimeBadge(timeStr, activeThemeProxy);
128
+ const badge = formatTimeBadge(timeStr, tsState.theme);
129
129
  const timeW = visibleWidth(timeStr);
130
130
 
131
131
  // First text line sits at index 1 (below the box top padding)
@@ -165,7 +165,7 @@ if (!(globalThis as Record<symbol, boolean>)[CHAT_TIMESTAMP_PATCHED]) {
165
165
  if (!hasText || lines.length === 0) return lines;
166
166
 
167
167
  const timeStr = formatClock(lastMsg?.timestamp);
168
- const badge = formatTimeBadge(timeStr, activeThemeProxy);
168
+ const badge = formatTimeBadge(timeStr, tsState.theme);
169
169
  const timeW = visibleWidth(timeStr);
170
170
 
171
171
  // First text line that is neither an OSC zone nor a thinking header
@@ -212,11 +212,15 @@ export default function (pi: ExtensionAPI) {
212
212
  });
213
213
 
214
214
  // The active theme is re-read per session (ctx.ui.theme); render reads it lazily.
215
+ // History for old user messages is read HERE, never at render time: the ctx is only
216
+ // valid inside a handler. A ctx captured in the module throws after session
217
+ // replacement/reload (pi's "stale after session replacement or reload" guard) and
218
+ // that throw would land inside a render pass -> uncaughtException. The session
219
+ // manager already holds the restored history when session_start fires (all reasons:
220
+ // startup/resume/fork/new/reload).
215
221
  pi.on("session_start", async (_event, ctx) => {
216
- activeThemeProxy = themeOf<typeof activeThemeProxy>(ctx) ?? activeThemeProxy;
217
- // History getter for the restore prefill (render-time, once).
218
- getBranch = () => ctx.sessionManager.getBranch();
219
- prefilled = false;
222
+ tsState.theme = themeOf<ThemeProxy>(ctx) ?? tsState.theme;
223
+ prefillUserTimestamps(ctx.sessionManager.getBranch());
220
224
  });
221
225
  }
222
226
 
@@ -264,5 +268,23 @@ if (isMain(import.meta.url)) {
264
268
  ]);
265
269
  assert(USER_TIMESTAMPS_MAP.get("old question") === 1000, "prefill stores the message timestamp");
266
270
  assert((USER_TIMESTAMPS_MAP.get("old reply") ?? 0) > 0, "prefill falls back to the entry timestamp");
271
+
272
+ // Reload invariant: /reload re-imports the module while the prototype patch below keeps
273
+ // running, so the fresh handlers MUST write into the same map/theme the patched render reads.
274
+ const reloaded = (await import(`${import.meta.url}?reload=1`)) as typeof import("./timestamps.ts");
275
+ const handlers: Record<string, (event: unknown, ctx: unknown) => unknown> = {};
276
+ const theme = { fg: (_c: string, t: string) => t };
277
+ reloaded.default({ on: (ev: string, h: never) => void (handlers[ev] = h) } as never);
278
+ await handlers.session_start(
279
+ {},
280
+ {
281
+ sessionManager: {
282
+ getBranch: () => [{ timestamp: "", message: { role: "user", content: "after reload", timestamp: 4242 } }],
283
+ },
284
+ ui: { theme },
285
+ },
286
+ );
287
+ assert(USER_TIMESTAMPS_MAP.get("after reload") === 4242, "reloaded module prefills the shared map read by the patched render");
288
+ assert(tsState.theme === theme, "reloaded module shares the theme read by the patched render");
267
289
  console.log("timestamps.ts self-check OK");
268
290
  }
@@ -0,0 +1,67 @@
1
+ // Platform-correct key ids for the package's single-letter shortcuts (t / n / r).
2
+ //
3
+ // Windows Terminal >= 1.24 aliases every Ctrl+Alt combo to AltGr (microsoft/terminal
4
+ // PR #20052), so the legacy "ESC + control byte" pair (ctrl+alt+r = \x1b\x12) that
5
+ // matchesKey() looks for is never emitted — the keystroke dies inside the terminal.
6
+ // A WSL pane inherits that same input translation, so it counts as Windows-like too,
7
+ // mirroring pi's own useWindowsKeybindings() (not exported from the package root).
8
+ // pi falls back to alt+<letter> for this class of problem (app.clipboard.pasteImage ->
9
+ // alt+v, app.model.cycleBackward -> alt+p) and every Windows terminal prefixes
10
+ // alt+<letter> with ESC (libuv win/tty.c), so do the same here. macOS keeps ctrl+alt
11
+ // only: plain Option there composes characters (Option+r = ®) unless "Use Option as
12
+ // Meta key" is enabled, while ctrl+alt transmits in every macOS terminal.
13
+ import { Key, matchesKey } from "@earendil-works/pi-tui";
14
+ import { assert, isMain } from "./check.ts";
15
+
16
+ /** Windows Terminal's AltGr aliasing hits native Windows and WSL panes alike. */
17
+ export const IS_WINDOWS_LIKE =
18
+ process.platform === "win32" ||
19
+ (process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP));
20
+
21
+ type Shortcut = {
22
+ /** Key ids for pi.registerShortcut; the last one is the platform-native key. */
23
+ keys: string[];
24
+ /** Cheatsheet label: `Alt+t` on Windows-like, `Ctrl+alt+t` elsewhere, `Ctrl+option+t` on macOS. */
25
+ label: string;
26
+ /** Matcher for editors that own their input loop (footer.ts). */
27
+ matches: (data: string) => boolean;
28
+ };
29
+
30
+ // Built once, at module scope: `matches` runs on the per-keystroke input path.
31
+ function build(letter: string): Shortcut {
32
+ const keys = IS_WINDOWS_LIKE ? [Key.ctrlAlt(letter), Key.alt(letter)] : [Key.ctrlAlt(letter)];
33
+ const native = keys[keys.length - 1]!;
34
+ const named = process.platform === "darwin" ? native.replace(/alt/g, "option") : native;
35
+ return {
36
+ keys,
37
+ label: named.charAt(0).toUpperCase() + named.slice(1),
38
+ matches: (data) => keys.some((key) => matchesKey(data, key)),
39
+ };
40
+ }
41
+
42
+ export const SHORTCUT_NEXT_TAB = build("t");
43
+ export const SHORTCUT_NEW_SESSION = build("n");
44
+ export const SHORTCUT_RELOAD = build("r");
45
+
46
+ if (isMain(import.meta.url)) {
47
+ assert(SHORTCUT_RELOAD.keys[0] === Key.ctrlAlt("r"), "ctrl+alt+r is registered on every platform");
48
+ assert(
49
+ SHORTCUT_RELOAD.keys.includes(Key.alt("r")) === IS_WINDOWS_LIKE,
50
+ "alt+r is registered only where Ctrl+Alt is aliased away",
51
+ );
52
+ assert(
53
+ SHORTCUT_RELOAD.label.toLowerCase().replace(/option/, "alt") === SHORTCUT_RELOAD.keys[SHORTCUT_RELOAD.keys.length - 1],
54
+ `label derives from the native key id (${SHORTCUT_RELOAD.label})`,
55
+ );
56
+ assert(SHORTCUT_RELOAD.matches("\x1b\x12"), "legacy ESC + 0x12 matches ctrl+alt+r");
57
+ assert(SHORTCUT_RELOAD.matches("\x1b[114;7u"), "Kitty CSI-u matches ctrl+alt+r");
58
+ assert(SHORTCUT_RELOAD.matches("\x1br") === IS_WINDOWS_LIKE, "ESC + r matches only where alt+r is registered");
59
+ assert(
60
+ SHORTCUT_RELOAD.matches("\x1b[114;3u") === IS_WINDOWS_LIKE,
61
+ "Kitty CSI-u alt+r fires exactly where alt+r is registered",
62
+ );
63
+ assert(!SHORTCUT_RELOAD.matches("\x12"), "bare control char does not match ctrl+alt+r");
64
+ assert(!SHORTCUT_RELOAD.matches("r"), "bare letter does not match ctrl+alt+r");
65
+ assert(!SHORTCUT_NEXT_TAB.matches("\x1br"), "alt+r does not match the next-tab shortcut");
66
+ console.log("shortcuts.ts self-check OK");
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bismawy/pi-arnative",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Arnative look for Pi: 11 warm-neutral themes, rounded tool boxes, branded header with tabs, 2-line footer with git/model/status, transcript clock, and a /usage dashboard.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "scripts": {
42
42
  "dev": "pi --extension ./extensions/tools.ts --extension ./extensions/section-headers.ts --extension ./extensions/footer.ts --extension ./extensions/timestamps.ts --extension ./extensions/ui-render-tweaks.ts --extension ./extensions/usage.ts",
43
- "test": "node lib/box.ts && node lib/ansi.ts && node themes/gen-themes.mjs --check && node extensions/tools.ts && node extensions/footer.ts && node extensions/timestamps.ts && node extensions/section-headers.ts && node extensions/ui-render-tweaks.ts && node extensions/usage.ts",
43
+ "test": "node lib/box.ts && node lib/ansi.ts && node lib/shortcuts.ts && node themes/gen-themes.mjs --check && node extensions/tools.ts && node extensions/footer.ts && node extensions/timestamps.ts && node extensions/section-headers.ts && node extensions/ui-render-tweaks.ts && node extensions/usage.ts",
44
44
  "themes": "node themes/gen-themes.mjs"
45
45
  }
46
46
  }