codsh-bundle 0.3.0 → 0.4.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.
package/lib/index.js CHANGED
@@ -88,6 +88,7 @@ const SGR$1 = {
88
88
  /** A theme that emits no sequences, used off a TTY and under `NO_COLOR`. */
89
89
  const PLAIN = {
90
90
  colored: false,
91
+ setLight: () => {},
91
92
  dim: (text) => text,
92
93
  bold: (text) => text,
93
94
  error: (text) => text,
@@ -122,9 +123,13 @@ function createTheme(isTty, env) {
122
123
  if (!isTty || env.NO_COLOR !== void 0) return PLAIN;
123
124
  const palette = env.TERM?.includes("256color") === true || env.COLORTERM !== void 0;
124
125
  const wrap = (code) => (text) => `${code}${text}${SGR$1.reset}`;
126
+ let gray = "\x1B[38;5;245m";
125
127
  return {
126
128
  colored: true,
127
- dim: wrap(palette ? "\x1B[38;5;245m" : SGR$1.dim),
129
+ setLight(light) {
130
+ gray = light ? "\x1B[38;5;242m" : "\x1B[38;5;245m";
131
+ },
132
+ dim: (text) => `${palette ? gray : SGR$1.dim}${text}${SGR$1.reset}`,
128
133
  bold: wrap(SGR$1.bold),
129
134
  error: wrap(SGR$1.red),
130
135
  success: wrap(SGR$1.green),
@@ -141,6 +146,25 @@ function createTheme(isTty, env) {
141
146
  };
142
147
  }
143
148
  /**
149
+ * Whether an OSC 10/11 color answer names a light color.
150
+ *
151
+ * Channels arrive as `rgb:RR/GG/BB` with one to four hex digits each; each is
152
+ * normalized by its own width before the relative-luminance weighting.
153
+ * @param payload - the reply payload, e.g. `rgb:ffff/ffff/ffff`.
154
+ * @returns true for light, false for dark, undefined when unparseable.
155
+ */
156
+ function backgroundIsLight(payload) {
157
+ const match = /^rgba?:([0-9a-f]{1,4})\/([0-9a-f]{1,4})\/([0-9a-f]{1,4})/i.exec(payload.trim());
158
+ if (match === null) return void 0;
159
+ const channel = (hex) => Number.parseInt(hex, 16) / (16 ** hex.length - 1);
160
+ const [red, green, blue] = [
161
+ channel(match[1] ?? "0"),
162
+ channel(match[2] ?? "0"),
163
+ channel(match[3] ?? "0")
164
+ ];
165
+ return .2126 * red + .7152 * green + .0722 * blue > .5;
166
+ }
167
+ /**
144
168
  * Display columns a string occupies once printed, ignoring styling sequences.
145
169
  *
146
170
  * Measured by `string-width` — the width authority cli-table3, ink, and every
@@ -653,14 +677,48 @@ const PASTE_END = "\x1B[201~";
653
677
  const WHEEL_LINES = 1;
654
678
  /** An SGR mouse report: `ESC [ < button ; column ; row (M|m)`. */
655
679
  const MOUSE = /^\u001B\[<(\d+);(\d+);(\d+)([Mm])/;
680
+ /**
681
+ * A kitty-keyboard-protocol report: `ESC [ code (:alternates) ; mods (:event) u`.
682
+ *
683
+ * The viewport pushes the protocol's disambiguate flag on entry (the same
684
+ * flag Claude Code pushes), so terminals that speak it report Esc and every
685
+ * modified key unambiguously — which is what makes Shift+Enter a key at all.
686
+ * Terminals that don't speak it ignore the push and keep sending the legacy
687
+ * sequences below.
688
+ */
689
+ const KITTY = /^\u001B\[(\d+)(?::\d+)*(?:;(\d+)(?::(\d+))?)?u/;
690
+ /** Kitty modifier bits, after the encoded +1 offset is removed. */
691
+ const KITTY_SHIFT = 1;
692
+ /** The Alt bit. */
693
+ const KITTY_ALT = 2;
694
+ /** The Control bit. */
695
+ const KITTY_CTRL = 4;
656
696
  /** Wheel-up button code in the SGR encoding; wheel-down is one higher. */
657
697
  const WHEEL_UP = 64;
658
698
  /** Modifier bits in an SGR button code: Shift, Meta, and Control. */
659
699
  const MOUSE_MODIFIERS = 28;
660
700
  /** The motion bit, set on reports sent while a button is held. */
661
701
  const MOUSE_MOTION = 32;
702
+ /**
703
+ * An OSC reply from the terminal: `ESC ] code ; payload (BEL | ESC \)`.
704
+ *
705
+ * The viewport asks for the background color (OSC 11) on entry; the reply
706
+ * arrives on stdin and must be consumed here — leaked past the decoder it
707
+ * would be typed into the input box as text.
708
+ */
709
+ const OSC_REPLY = /^\u001B\](\d+);([^\u0007\u001B]*)(?:\u0007|\u001B\\)/;
710
+ /** An OSC reply still arriving, its possible ST half included. */
711
+ const OSC_PARTIAL = /^\u001B\](?:\d*(?:;[^\u0007\u001B]*)?)?\u001B?$/;
662
712
  /** Sequences that resolve to one key, longest first so a prefix never wins. */
663
713
  const SEQUENCES = [
714
+ ["\x1B[I", {
715
+ kind: "focus",
716
+ focused: true
717
+ }],
718
+ ["\x1B[O", {
719
+ kind: "focus",
720
+ focused: false
721
+ }],
664
722
  ["\x1B[5~", {
665
723
  kind: "page",
666
724
  direction: -1
@@ -816,7 +874,23 @@ var KeyDecoder = class {
816
874
  }
817
875
  return [];
818
876
  }
819
- if (/^\u001B(\[(<\d*(;\d*){0,2})?)?$/.test(this.held)) return void 0;
877
+ const kitty = KITTY.exec(this.held);
878
+ if (kitty !== null) {
879
+ this.held = this.held.slice(kitty[0].length);
880
+ return this.kittyKey(Number(kitty[1]), Number(kitty[2] ?? "1"), Number(kitty[3] ?? "1"));
881
+ }
882
+ const osc = OSC_REPLY.exec(this.held);
883
+ if (osc !== null) {
884
+ this.held = this.held.slice(osc[0].length);
885
+ const code = Number(osc[1]);
886
+ if (code === 10 || code === 11) return [{
887
+ kind: "osc-reply",
888
+ code,
889
+ payload: osc[2] ?? ""
890
+ }];
891
+ return [];
892
+ }
893
+ if (/^\u001B(\[[\d;:<]*)?$/.test(this.held) || OSC_PARTIAL.test(this.held)) return void 0;
820
894
  for (const [sequence, key] of SEQUENCES) {
821
895
  if (this.held.startsWith(sequence)) {
822
896
  this.held = this.held.slice(sequence.length);
@@ -846,6 +920,38 @@ var KeyDecoder = class {
846
920
  }];
847
921
  }
848
922
  /**
923
+ * Map one kitty-protocol report onto the same keys the legacy bytes make.
924
+ * @param code - the key's Unicode code point.
925
+ * @param mods - the encoded modifiers, offset by one.
926
+ * @param event - press (1), repeat (2), or release (3).
927
+ * @returns the keys produced; unknown chords are swallowed, never typed.
928
+ */
929
+ kittyKey(code, mods, event) {
930
+ if (event === 3) return [];
931
+ const bits = Math.max(0, mods - 1);
932
+ const shift = (bits & KITTY_SHIFT) !== 0;
933
+ const alt = (bits & KITTY_ALT) !== 0;
934
+ const ctrl = (bits & KITTY_CTRL) !== 0;
935
+ if (code === 27) return [{ kind: "escape" }];
936
+ if (code === 13) return [shift || alt ? { kind: "newline" } : { kind: "enter" }];
937
+ if (code === 9) return [shift ? { kind: "shift-tab" } : { kind: "tab" }];
938
+ if (code === 127 || code === 8) return [alt || ctrl ? { kind: "kill-word" } : { kind: "backspace" }];
939
+ if (ctrl && code >= 97 && code <= 122) {
940
+ const control = CONTROLS[String.fromCharCode(code - 96)];
941
+ return control === void 0 ? [] : [control];
942
+ }
943
+ if (alt) {
944
+ if (code === 98) return [{ kind: "word-left" }];
945
+ if (code === 102) return [{ kind: "word-right" }];
946
+ return [];
947
+ }
948
+ if (!ctrl && code >= 32) return [{
949
+ kind: "text",
950
+ text: String.fromCodePoint(code)
951
+ }];
952
+ return [];
953
+ }
954
+ /**
849
955
  * Collect bracketed-paste content up to its end marker.
850
956
  * @returns the paste key once complete, otherwise undefined.
851
957
  */
@@ -958,6 +1064,24 @@ const LEAVE_ALT = "\x1B[?1049l";
958
1064
  const ENABLE_MOUSE = "\x1B[?1002h\x1B[?1006h";
959
1065
  /** Stop reporting them. */
960
1066
  const DISABLE_MOUSE = "\x1B[?1006l\x1B[?1002l";
1067
+ /**
1068
+ * Push the kitty keyboard protocol's disambiguate flag — what Claude Code
1069
+ * pushes — so Shift+Enter, Esc, and control chords report unambiguously on
1070
+ * terminals that speak it. Others ignore the push and keep the legacy bytes.
1071
+ */
1072
+ const ENABLE_KITTY_KEYS = "\x1B[>1u";
1073
+ /** Pop it, restoring whatever the shell had. */
1074
+ const DISABLE_KITTY_KEYS = "\x1B[<u";
1075
+ /** Report focus in/out (mode 1004), which the bell policy reads. */
1076
+ const ENABLE_FOCUS = "\x1B[?1004h";
1077
+ /** Stop reporting focus. */
1078
+ const DISABLE_FOCUS = "\x1B[?1004l";
1079
+ /**
1080
+ * Ask the terminal for its background color (OSC 11), the way opencode and
1081
+ * Codex do. The reply decides the light-background palette; a terminal that
1082
+ * never answers leaves the dark default standing.
1083
+ */
1084
+ const QUERY_BACKGROUND = "\x1B]11;?\x07";
961
1085
  /** Ask the terminal to paint a frame atomically, so no half-frame is shown. */
962
1086
  const SYNC_BEGIN = "\x1B[?2026h";
963
1087
  /** End the atomic frame. */
@@ -1037,7 +1161,7 @@ var Screen = class {
1037
1161
  enter() {
1038
1162
  if (this.active) return;
1039
1163
  this.active = true;
1040
- this.host.write(`${ENTER_ALT}${ENABLE_MOUSE}${HIDE_CURSOR}`);
1164
+ this.host.write(`${ENTER_ALT}${ENABLE_MOUSE}${ENABLE_KITTY_KEYS}${ENABLE_FOCUS}${QUERY_BACKGROUND}${HIDE_CURSOR}`);
1041
1165
  this.painted = [];
1042
1166
  this.render();
1043
1167
  }
@@ -1050,7 +1174,7 @@ var Screen = class {
1050
1174
  leave() {
1051
1175
  if (!this.active) return;
1052
1176
  this.active = false;
1053
- this.host.write(`${DISABLE_MOUSE}${SHOW_CURSOR}${LEAVE_ALT}`);
1177
+ this.host.write(`${DISABLE_FOCUS}${DISABLE_KITTY_KEYS}${DISABLE_MOUSE}${SHOW_CURSOR}${LEAVE_ALT}`);
1054
1178
  this.painted = [];
1055
1179
  }
1056
1180
  /**
@@ -1442,6 +1566,11 @@ var TerminalConsole = class {
1442
1566
  keyHandler;
1443
1567
  /** Keys decoded before any handler registered — type-ahead is never dropped. */
1444
1568
  earlyKeys = [];
1569
+ /** Whether the terminal window has focus; undefined until it reports. */
1570
+ focused;
1571
+ /** The terminal's OSC 11 background answer, kept for late listeners. */
1572
+ background;
1573
+ backgroundHandler;
1445
1574
  escapeTimer;
1446
1575
  ended = false;
1447
1576
  /** The viewport this surface owns on a terminal; absent off one. */
@@ -1600,6 +1729,17 @@ var TerminalConsole = class {
1600
1729
  }
1601
1730
  /** Hand one key to the handler, or hold it until one registers. */
1602
1731
  deliver(key) {
1732
+ if (key.kind === "focus") {
1733
+ this.focused = key.focused;
1734
+ return;
1735
+ }
1736
+ if (key.kind === "osc-reply") {
1737
+ if (key.code === 11) {
1738
+ this.background = key.payload;
1739
+ this.backgroundHandler?.(key.payload);
1740
+ }
1741
+ return;
1742
+ }
1603
1743
  if (this.keyHandler === void 0) {
1604
1744
  this.earlyKeys.push(key);
1605
1745
  return;
@@ -1771,9 +1911,21 @@ var TerminalConsole = class {
1771
1911
  /** Ring the terminal bell; a pipe gets nothing to beep with. */
1772
1912
  bell() {
1773
1913
  if (!this.isTty) return;
1914
+ if (this.focused === true) return;
1774
1915
  this.output.write("\x07");
1775
1916
  }
1776
1917
  /**
1918
+ * Register for the terminal's background-color answer (OSC 11).
1919
+ *
1920
+ * The answer often lands before anyone is ready to hear it — the query goes
1921
+ * out with the first frame — so a buffered reply is delivered immediately.
1922
+ * @param handler - receives the raw payload, e.g. `rgb:1e1e/1e1e/2e2e`.
1923
+ */
1924
+ onBackground(handler) {
1925
+ this.backgroundHandler = handler;
1926
+ if (this.background !== void 0) handler(this.background);
1927
+ }
1928
+ /**
1777
1929
  * Set the terminal window title.
1778
1930
  * @param title - the title text; control bytes are the terminal's to reject.
1779
1931
  */
@@ -4131,6 +4283,10 @@ async function run(ctx, config, io) {
4131
4283
  if (sessions === void 0) return;
4132
4284
  const cwd = process.cwd();
4133
4285
  const theme = createTheme(io.console.isTty, process.env);
4286
+ io.console.onBackground((payload) => {
4287
+ const light = backgroundIsLight(payload);
4288
+ if (light !== void 0) theme.setLight(light);
4289
+ });
4134
4290
  const preset = await installPackagedPreset();
4135
4291
  if (preset.installed) io.console.write(theme.dim(`installed preset into ${preset.path}`));
4136
4292
  const composed = await compose(ctx, config, cwd);
@@ -42,6 +42,11 @@ export declare class TerminalConsole {
42
42
  private keyHandler;
43
43
  /** Keys decoded before any handler registered — type-ahead is never dropped. */
44
44
  private readonly earlyKeys;
45
+ /** Whether the terminal window has focus; undefined until it reports. */
46
+ private focused;
47
+ /** The terminal's OSC 11 background answer, kept for late listeners. */
48
+ private background;
49
+ private backgroundHandler;
45
50
  private escapeTimer;
46
51
  private ended;
47
52
  /** The viewport this surface owns on a terminal; absent off one. */
@@ -212,6 +217,14 @@ export declare class TerminalConsole {
212
217
  clearRegion(): void;
213
218
  /** Ring the terminal bell; a pipe gets nothing to beep with. */
214
219
  bell(): void;
220
+ /**
221
+ * Register for the terminal's background-color answer (OSC 11).
222
+ *
223
+ * The answer often lands before anyone is ready to hear it — the query goes
224
+ * out with the first frame — so a buffered reply is delivered immediately.
225
+ * @param handler - receives the raw payload, e.g. `rgb:1e1e/1e1e/2e2e`.
226
+ */
227
+ onBackground(handler: (payload: string) => void): void;
215
228
  /**
216
229
  * Set the terminal window title.
217
230
  * @param title - the title text; control bytes are the terminal's to reject.
@@ -81,6 +81,13 @@ export type Key = {
81
81
  kind: 'mouse-up';
82
82
  row: number;
83
83
  column: number;
84
+ } | {
85
+ kind: 'focus';
86
+ focused: boolean;
87
+ } | {
88
+ kind: 'osc-reply';
89
+ code: number;
90
+ payload: string;
84
91
  };
85
92
  /** Decodes terminal bytes into keys, holding partial sequences between reads. */
86
93
  export declare class KeyDecoder {
@@ -111,6 +118,14 @@ export declare class KeyDecoder {
111
118
  * @returns the keys produced, or undefined when more bytes are needed.
112
119
  */
113
120
  private take;
121
+ /**
122
+ * Map one kitty-protocol report onto the same keys the legacy bytes make.
123
+ * @param code - the key's Unicode code point.
124
+ * @param mods - the encoded modifiers, offset by one.
125
+ * @param event - press (1), repeat (2), or release (3).
126
+ * @returns the keys produced; unknown chords are swallowed, never typed.
127
+ */
128
+ private kittyKey;
114
129
  /**
115
130
  * Collect bracketed-paste content up to its end marker.
116
131
  * @returns the paste key once complete, otherwise undefined.
@@ -21,6 +21,14 @@ export interface Theme {
21
21
  path(text: string): string;
22
22
  /** The user's own echoed input. */
23
23
  user(text: string): string;
24
+ /**
25
+ * Adopt the light- or dark-background palette.
26
+ *
27
+ * Base ANSI colors are the terminal theme's to map, but the secondary-text
28
+ * gray is an absolute palette entry, and the shade that recedes on a dark
29
+ * background washes out on a light one.
30
+ */
31
+ setLight(light: boolean): void;
24
32
  /** Roles used inside a fenced code block. */
25
33
  readonly syntax: SyntaxTheme;
26
34
  }
@@ -47,6 +55,15 @@ export interface SyntaxTheme {
47
55
  * @returns the styling functions for this surface.
48
56
  */
49
57
  export declare function createTheme(isTty: boolean, env: Record<string, string | undefined>): Theme;
58
+ /**
59
+ * Whether an OSC 10/11 color answer names a light color.
60
+ *
61
+ * Channels arrive as `rgb:RR/GG/BB` with one to four hex digits each; each is
62
+ * normalized by its own width before the relative-luminance weighting.
63
+ * @param payload - the reply payload, e.g. `rgb:ffff/ffff/ffff`.
64
+ * @returns true for light, false for dark, undefined when unparseable.
65
+ */
66
+ export declare function backgroundIsLight(payload: string): boolean | undefined;
50
67
  /**
51
68
  * Display columns a string occupies once printed, ignoring styling sequences.
52
69
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codsh-bundle",
3
3
  "description": "The codsh runtime: the interactive TTY surface and code-cli agent preset, installed into a dsh profile. Users install codsh-cli (the launcher) — this package is what it registers.",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "main": "lib/index.js",