ompchamber 3.2.0 → 3.3.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/dist/client/index.html +1 -1
- package/dist/client/index.js +239 -230
- package/dist/client/static/js/index-bsxxt89j.css +1 -0
- package/dist/client/static/js/{index-1q6165r3.js → index-qkt4z7x0.js} +640 -640
- package/package.json +4 -3
- package/public/sw.js +54 -5
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +27 -8
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +1 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.test.ts +84 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +14 -5
- package/src/client/components/settings/Modal.tsx +10 -17
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/appearance-settings/EditorFontSection.tsx +78 -0
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +17 -11
- package/src/client/components/workspace/editor/index.tsx +15 -7
- package/src/client/components/workspace/terminal-panel/KeyBar.tsx +194 -0
- package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +24 -0
- package/src/client/components/workspace/terminal-panel/index.tsx +98 -3
- package/src/client/components/workspace/terminal-panel/mount.ts +47 -0
- package/src/client/data/theme/terminal.ts +25 -8
- package/src/client/hooks/editor/font.ts +53 -0
- package/src/client/hooks/ui/clipboard.ts +17 -0
- package/src/client/hooks/ui/keyboard-inset.test.ts +46 -0
- package/src/client/hooks/ui/keyboard-inset.ts +113 -0
- package/src/client/hooks/ui/text-overflow.test.ts +152 -0
- package/src/client/hooks/ui/text-overflow.ts +78 -0
- package/src/client/hooks/ui/touch-device.ts +46 -0
- package/src/client/main.tsx +1 -0
- package/src/server/index.ts +4 -3
- package/src/server/lib/assets/conditional.server.test.ts +74 -0
- package/src/server/lib/assets/conditional.server.ts +54 -0
- package/src/server/lib/assets/dev-assets.server.test.ts +44 -0
- package/src/server/lib/assets/dev-assets.server.ts +121 -0
- package/src/server/lib/assets/fonts.server.ts +9 -4
- package/src/server/lib/bundler/build-errors.server.ts +136 -0
- package/src/server/lib/bundler/build-errors.test.ts +103 -0
- package/src/server/lib/bundler/dependencies.test.ts +134 -0
- package/src/server/lib/lifecycle/listener.ts +29 -0
- package/src/server/plugins/shell.server.ts +52 -13
- package/src/server/plugins/ssr.ts +43 -7
- package/src/server/plugins/static.ts +51 -18
- package/src/shared/lib/code/editor/typography.test.ts +93 -0
- package/src/shared/lib/code/editor/typography.ts +86 -0
- package/src/shared/lib/fonts/NOTICE.md +44 -0
- package/src/shared/lib/fonts/nerd-symbols.css +55 -0
- package/src/shared/lib/fonts/nerd-symbols.test.ts +134 -0
- package/src/shared/lib/fonts/symbols-nerd-font-mono.woff2 +0 -0
- package/src/shared/lib/settings/diff.test.ts +1 -1
- package/src/shared/lib/workspace/terminal/keys.test.ts +189 -0
- package/src/shared/lib/workspace/terminal/keys.ts +225 -0
- package/src/shared/types/settings/state.ts +7 -16
- package/dist/client/static/js/index-01dv383b.css +0 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The bytes the terminal key bar sends.
|
|
8
|
+
*
|
|
9
|
+
* Every expectation here is copied from xterm's own `evaluateKeyboardEvent`
|
|
10
|
+
* (`@xterm/xterm/src/common/input/Keyboard.ts`), because the shell cannot tell a
|
|
11
|
+
* bar button from a physical key: a sequence that merely looks plausible works
|
|
12
|
+
* in bash and breaks in vim. Three of those rules are the ones this file exists
|
|
13
|
+
* to pin — a modifier is a CSI PARAMETER and not an ESC prefix, application
|
|
14
|
+
* cursor mode swaps in the SS3 spelling, and Ctrl on a literal key is a latch
|
|
15
|
+
* for typed text rather than a byte to fold.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, expect, test } from 'bun:test';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
NO_MODIFIERS,
|
|
22
|
+
TERMINAL_KEYS,
|
|
23
|
+
TERMINAL_SYMBOLS,
|
|
24
|
+
applyModifiers,
|
|
25
|
+
ctrlCharacter,
|
|
26
|
+
resolveKeySequence,
|
|
27
|
+
type TerminalModifiers,
|
|
28
|
+
} from '@/shared/lib/workspace/terminal/keys';
|
|
29
|
+
|
|
30
|
+
const CTRL: TerminalModifiers = { ctrl: true, alt: false };
|
|
31
|
+
const ALT: TerminalModifiers = { ctrl: false, alt: true };
|
|
32
|
+
|
|
33
|
+
describe('resolveKeySequence — cursor keys', () => {
|
|
34
|
+
test('unmodified arrows use CSI', () => {
|
|
35
|
+
expect(resolveKeySequence('up')).toBe('\x1b[A');
|
|
36
|
+
expect(resolveKeySequence('down')).toBe('\x1b[B');
|
|
37
|
+
expect(resolveKeySequence('right')).toBe('\x1b[C');
|
|
38
|
+
expect(resolveKeySequence('left')).toBe('\x1b[D');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('application cursor mode swaps in the SS3 spelling', () => {
|
|
42
|
+
const options = { applicationCursorKeys: true };
|
|
43
|
+
expect(resolveKeySequence('up', options)).toBe('\x1bOA');
|
|
44
|
+
expect(resolveKeySequence('left', options)).toBe('\x1bOD');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('Home and End always use the terminfo spelling, DECCKM or not', () => {
|
|
48
|
+
// `TERM=xterm-256color` declares khome/kend as SS3, so the CSI form is a
|
|
49
|
+
// sequence the shell does not bind: zsh rings the bell and ignores it.
|
|
50
|
+
expect(resolveKeySequence('home')).toBe('\x1bOH');
|
|
51
|
+
expect(resolveKeySequence('end')).toBe('\x1bOF');
|
|
52
|
+
expect(resolveKeySequence('home', { applicationCursorKeys: true })).toBe('\x1bOH');
|
|
53
|
+
expect(resolveKeySequence('end', { applicationCursorKeys: true })).toBe('\x1bOF');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('a modifier beats application cursor mode, as xterm does', () => {
|
|
57
|
+
const options = { applicationCursorKeys: true, modifiers: CTRL };
|
|
58
|
+
expect(resolveKeySequence('up', options)).toBe('\x1b[1;5A');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('a modifier is a CSI parameter, never an ESC prefix', () => {
|
|
62
|
+
expect(resolveKeySequence('up', { modifiers: CTRL })).toBe('\x1b[1;5A');
|
|
63
|
+
expect(resolveKeySequence('up', { modifiers: ALT })).toBe('\x1b[1;3A');
|
|
64
|
+
expect(resolveKeySequence('up', { modifiers: { ctrl: true, alt: true } })).toBe('\x1b[1;7A');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('resolveKeySequence — keys with a fixed spelling', () => {
|
|
69
|
+
test('an unmodified page/delete key drops its leading 1', () => {
|
|
70
|
+
expect(resolveKeySequence('delete')).toBe('\x1b[3~');
|
|
71
|
+
expect(resolveKeySequence('pageUp')).toBe('\x1b[5~');
|
|
72
|
+
expect(resolveKeySequence('pageDown')).toBe('\x1b[6~');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('page keys carry their prefix once modified', () => {
|
|
76
|
+
expect(resolveKeySequence('pageUp', { modifiers: CTRL })).toBe('\x1b[5;5~');
|
|
77
|
+
expect(resolveKeySequence('delete', { modifiers: ALT })).toBe('\x1b[3;3~');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('a page key has no SS3 form, so DECCKM does not touch it', () => {
|
|
81
|
+
expect(resolveKeySequence('pageUp', { applicationCursorKeys: true })).toBe('\x1b[5~');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('escape, tab and shift-tab are literal', () => {
|
|
85
|
+
expect(resolveKeySequence('escape')).toBe('\x1b');
|
|
86
|
+
expect(resolveKeySequence('tab')).toBe('\t');
|
|
87
|
+
expect(resolveKeySequence('shiftTab')).toBe('\x1b[Z');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('resolveKeySequence — modifiers on a literal key', () => {
|
|
92
|
+
test('Ctrl does NOT fold a literal key into a control byte', () => {
|
|
93
|
+
// The `^C` button is the only Ctrl+C. A tapped `|` stays a pipe: folding it
|
|
94
|
+
// would send FS (0x1c), which is not a pipe and not a signal.
|
|
95
|
+
expect(resolveKeySequence('pipe', { modifiers: CTRL })).toBe('|');
|
|
96
|
+
expect(resolveKeySequence('underscore', { modifiers: CTRL })).toBe('_');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('Alt on a literal key is the ESC prefix', () => {
|
|
100
|
+
expect(resolveKeySequence('pipe', { modifiers: ALT })).toBe('\x1b|');
|
|
101
|
+
expect(resolveKeySequence('escape', { modifiers: ALT })).toBe('\x1b\x1b');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('the dedicated control keys are already the folded byte', () => {
|
|
105
|
+
expect(resolveKeySequence('interrupt')).toBe('\x03');
|
|
106
|
+
expect(resolveKeySequence('eof')).toBe('\x04');
|
|
107
|
+
expect(resolveKeySequence('clear')).toBe('\x0c');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('resolveKeySequence — defaults and coverage', () => {
|
|
112
|
+
test('no options means no modifier and no application mode', () => {
|
|
113
|
+
// `pageUp` is the plain CSI default; Home/End carry a terminfo spelling.
|
|
114
|
+
expect(resolveKeySequence('pageUp')).toBe('\x1b[5~');
|
|
115
|
+
expect(NO_MODIFIERS).toEqual({ ctrl: false, alt: false });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('every control key resolves to bytes', () => {
|
|
119
|
+
for (const def of TERMINAL_KEYS) {
|
|
120
|
+
expect(resolveKeySequence(def.id).length, def.id).toBeGreaterThan(0);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('every symbol resolves to its own literal', () => {
|
|
125
|
+
for (const def of TERMINAL_SYMBOLS) {
|
|
126
|
+
expect(resolveKeySequence(def.id), def.id).toBe(def.sequence);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('the symbol layer covers the characters a phone buries', () => {
|
|
131
|
+
const labels = new Set(TERMINAL_SYMBOLS.map((def) => def.label));
|
|
132
|
+
for (const required of ['|', '~', '\\', '$', '*', '?', '&', '<', '>', '`', '{', '}', '[', ']', '"', "'"]) {
|
|
133
|
+
expect(labels.has(required), required).toBe(true);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('a symbol under a latched Alt is an ESC prefix, like any literal', () => {
|
|
138
|
+
expect(resolveKeySequence('dollar', { modifiers: ALT })).toBe('\x1b$');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('an unknown id sends nothing rather than a guess', () => {
|
|
142
|
+
expect(resolveKeySequence('nope' as never)).toBe('');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('ctrlCharacter', () => {
|
|
147
|
+
test('letters fold case-insensitively onto C0', () => {
|
|
148
|
+
expect(ctrlCharacter('c')).toBe('\x03');
|
|
149
|
+
expect(ctrlCharacter('C')).toBe('\x03');
|
|
150
|
+
expect(ctrlCharacter('a')).toBe('\x01');
|
|
151
|
+
expect(ctrlCharacter('z')).toBe('\x1a');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('the symbol set matches the terminal, not the ASCII table', () => {
|
|
155
|
+
expect(ctrlCharacter('@')).toBe('\x00');
|
|
156
|
+
expect(ctrlCharacter(' ')).toBe('\x00');
|
|
157
|
+
expect(ctrlCharacter('[')).toBe('\x1b');
|
|
158
|
+
expect(ctrlCharacter('_')).toBe('\x1f');
|
|
159
|
+
expect(ctrlCharacter('?')).toBe('\x7f');
|
|
160
|
+
expect(ctrlCharacter('!')).toBeNull();
|
|
161
|
+
expect(ctrlCharacter('ab')).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('applyModifiers', () => {
|
|
166
|
+
test('a latched Ctrl folds a typed letter', () => {
|
|
167
|
+
expect(applyModifiers('c', CTRL)).toBe('\x03');
|
|
168
|
+
expect(applyModifiers('r', CTRL)).toBe('\x12');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('a latched Alt prefixes ESC', () => {
|
|
172
|
+
expect(applyModifiers('b', ALT)).toBe('\x1bb');
|
|
173
|
+
expect(applyModifiers('f', { ctrl: true, alt: true })).toBe('\x1b\x06');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('no modifier is the identity', () => {
|
|
177
|
+
expect(applyModifiers('c', NO_MODIFIERS)).toBe('c');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('a paste is left intact — the latch does not corrupt it', () => {
|
|
181
|
+
const pasted = 'bun run build\n';
|
|
182
|
+
expect(applyModifiers(pasted, CTRL)).toBe(pasted);
|
|
183
|
+
expect(applyModifiers(pasted, ALT)).toBe(pasted);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('a character with no control code passes through', () => {
|
|
187
|
+
expect(applyModifiers('!', CTRL)).toBe('!');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The keys a touch keyboard cannot produce, and the bytes they send.
|
|
8
|
+
*
|
|
9
|
+
* A phone's soft keyboard has no Ctrl, no Alt, no arrows, no Esc and no `|`, so
|
|
10
|
+
* a shell on a phone is unusable without a strip of buttons that emit the raw
|
|
11
|
+
* bytes those keys would. Every sequence here is what xterm's own
|
|
12
|
+
* `evaluateKeyboardEvent` sends for the same physical key
|
|
13
|
+
* (`@xterm/xterm/src/common/input/Keyboard.ts`) — the shell on the far end
|
|
14
|
+
* cannot tell the two apart, so a bar key that invented its own encoding would
|
|
15
|
+
* work in bash and break in vim.
|
|
16
|
+
*
|
|
17
|
+
* Three rules come straight out of that file:
|
|
18
|
+
* - A CSI key carries its modifiers as a PARAMETER (`ESC [ 1 ; 5 A` is
|
|
19
|
+
* Ctrl+Up), never as an ESC prefix; only a literal key gets the prefix.
|
|
20
|
+
* - Application cursor mode (DECCKM, which vim and less set) swaps the arrows
|
|
21
|
+
* and Home/End to their SS3 spelling (`ESC O A`), and a modifier wins over
|
|
22
|
+
* it — xterm ignores the mode once a modifier is held.
|
|
23
|
+
* - Ctrl has no literal spelling: it folds a typed character into a control
|
|
24
|
+
* character (Ctrl+C is `0x03`), so a literal button keeps its own byte.
|
|
25
|
+
*
|
|
26
|
+
* There is deliberately no Cmd key. macOS reports it as `metaKey` and xterm
|
|
27
|
+
* discards it — `if (ev.metaKey) break;` in the cursor-key branches — because
|
|
28
|
+
* Cmd is the browser's modifier, not the terminal's: in a terminal Cmd+C and
|
|
29
|
+
* Cmd+V mean copy and paste, which is why those are actions in `KeyBar.tsx`
|
|
30
|
+
* rather than bytes here.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export type TerminalModifier = 'ctrl' | 'alt';
|
|
34
|
+
|
|
35
|
+
export interface TerminalModifiers {
|
|
36
|
+
ctrl: boolean;
|
|
37
|
+
alt: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Frozen, so clearing a latch is an identity change and re-renders nothing. */
|
|
41
|
+
export const NO_MODIFIERS: TerminalModifiers = Object.freeze({ ctrl: false, alt: false });
|
|
42
|
+
|
|
43
|
+
/** A CSI key: `ESC [ <prefix> <final>`, or `ESC O <final>` under DECCKM. */
|
|
44
|
+
interface CsiSpec {
|
|
45
|
+
/** Leading parameter, dropped from the unmodified form when it is `1`. */
|
|
46
|
+
prefix: string;
|
|
47
|
+
final: string;
|
|
48
|
+
/** SS3 spelling, used only while the shell has DECCKM set. */
|
|
49
|
+
application?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Spelling `terminfo` declares for the UNMODIFIED key, when it differs from
|
|
52
|
+
* the CSI default. This is not cosmetic: a line editor binds the sequences its
|
|
53
|
+
* terminfo entry names, so a key whose entry disagrees with what we send is
|
|
54
|
+
* simply not recognised — zsh answers Home with a bell and does nothing.
|
|
55
|
+
*/
|
|
56
|
+
terminfo?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TerminalKeyDef<Id extends string = string> {
|
|
60
|
+
id: Id;
|
|
61
|
+
/** Printed on the button — a glyph where one exists. */
|
|
62
|
+
label: string;
|
|
63
|
+
/** Spoken name: the button's `title` and `aria-label`. */
|
|
64
|
+
name: string;
|
|
65
|
+
/** Literal bytes, for keys with no CSI form. */
|
|
66
|
+
sequence?: string;
|
|
67
|
+
csi?: CsiSpec;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A key that is always a literal byte — a symbol, with no CSI form. */
|
|
71
|
+
export interface TerminalLiteralKeyDef<Id extends string = string> extends TerminalKeyDef<Id> {
|
|
72
|
+
sequence: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Control and cursor keys — the row that is always in reach.
|
|
77
|
+
*
|
|
78
|
+
* Ordered by how often a terminal needs it, because the bar wraps: the head
|
|
79
|
+
* stays on the first row and the tail lands on the last.
|
|
80
|
+
*/
|
|
81
|
+
export const TERMINAL_KEYS: readonly TerminalKeyDef[] = [
|
|
82
|
+
{ id: 'escape', label: 'Esc', name: 'Escape', sequence: '\x1b' },
|
|
83
|
+
{ id: 'tab', label: 'Tab', name: 'Tab', sequence: '\t' },
|
|
84
|
+
{ id: 'interrupt', label: '^C', name: 'Ctrl+C — interrupt', sequence: '\x03' },
|
|
85
|
+
{ id: 'left', label: '←', name: 'Left arrow', csi: { prefix: '1', final: 'D', application: 'OD' } },
|
|
86
|
+
{ id: 'up', label: '↑', name: 'Up arrow', csi: { prefix: '1', final: 'A', application: 'OA' } },
|
|
87
|
+
{ id: 'down', label: '↓', name: 'Down arrow', csi: { prefix: '1', final: 'B', application: 'OB' } },
|
|
88
|
+
{ id: 'right', label: '→', name: 'Right arrow', csi: { prefix: '1', final: 'C', application: 'OC' } },
|
|
89
|
+
// Home/End carry `terminfo` because xterm's CSI default is not what the
|
|
90
|
+
// shell's terminfo entry names. `TERM=xterm-256color` declares khome/kend as
|
|
91
|
+
// SS3 (`\EOH`/`\EOF`) — `infocmp xterm-256color` prints exactly that — while
|
|
92
|
+
// xterm.js sends `ESC[H`/`ESC[F` whenever DECCKM is off, which is the state a
|
|
93
|
+
// shell prompt is in. Verified against the chamber's own zsh: the CSI form
|
|
94
|
+
// makes ZLE ring the bell and leave the cursor where it was, the SS3 form
|
|
95
|
+
// moves it.
|
|
96
|
+
{ id: 'home', label: 'Home', name: 'Home', csi: { prefix: '1', final: 'H', application: 'OH', terminfo: '\x1bOH' } },
|
|
97
|
+
{ id: 'end', label: 'End', name: 'End', csi: { prefix: '1', final: 'F', application: 'OF', terminfo: '\x1bOF' } },
|
|
98
|
+
{ id: 'pageUp', label: 'PgUp', name: 'Page up', csi: { prefix: '5', final: '~' } },
|
|
99
|
+
{ id: 'pageDown', label: 'PgDn', name: 'Page down', csi: { prefix: '6', final: '~' } },
|
|
100
|
+
{ id: 'delete', label: 'Del', name: 'Delete', csi: { prefix: '3', final: '~' } },
|
|
101
|
+
{ id: 'shiftTab', label: '⇧Tab', name: 'Shift+Tab', sequence: '\x1b[Z' },
|
|
102
|
+
{ id: 'eof', label: '^D', name: 'Ctrl+D — end of input', sequence: '\x04' },
|
|
103
|
+
{ id: 'clear', label: '^L', name: 'Ctrl+L — clear screen', sequence: '\x0c' },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Symbols a phone buries behind its "123" / "#+=" layer, plus the ones no layer
|
|
108
|
+
* has at all (`~`, `|`, `\`, backtick). Reaching `$` on an iOS keyboard is a
|
|
109
|
+
* mode switch per character, which is what makes a shell on a phone unusable —
|
|
110
|
+
* each of these is one tap.
|
|
111
|
+
*/
|
|
112
|
+
export const TERMINAL_SYMBOLS: readonly TerminalLiteralKeyDef[] = [
|
|
113
|
+
{ id: 'pipe', label: '|', name: 'Pipe', sequence: '|' },
|
|
114
|
+
{ id: 'tilde', label: '~', name: 'Tilde', sequence: '~' },
|
|
115
|
+
{ id: 'dollar', label: '$', name: 'Dollar', sequence: '$' },
|
|
116
|
+
{ id: 'asterisk', label: '*', name: 'Asterisk', sequence: '*' },
|
|
117
|
+
{ id: 'question', label: '?', name: 'Question mark', sequence: '?' },
|
|
118
|
+
{ id: 'underscore', label: '_', name: 'Underscore', sequence: '_' },
|
|
119
|
+
{ id: 'backslash', label: '\\', name: 'Backslash', sequence: '\\' },
|
|
120
|
+
{ id: 'slash', label: '/', name: 'Slash', sequence: '/' },
|
|
121
|
+
{ id: 'minus', label: '-', name: 'Minus', sequence: '-' },
|
|
122
|
+
{ id: 'equals', label: '=', name: 'Equals', sequence: '=' },
|
|
123
|
+
{ id: 'plus', label: '+', name: 'Plus', sequence: '+' },
|
|
124
|
+
{ id: 'ampersand', label: '&', name: 'Ampersand', sequence: '&' },
|
|
125
|
+
{ id: 'semicolon', label: ';', name: 'Semicolon', sequence: ';' },
|
|
126
|
+
{ id: 'colon', label: ':', name: 'Colon', sequence: ':' },
|
|
127
|
+
{ id: 'comma', label: ',', name: 'Comma', sequence: ',' },
|
|
128
|
+
{ id: 'dot', label: '.', name: 'Period', sequence: '.' },
|
|
129
|
+
{ id: 'at', label: '@', name: 'At sign', sequence: '@' },
|
|
130
|
+
{ id: 'percent', label: '%', name: 'Percent', sequence: '%' },
|
|
131
|
+
{ id: 'caret', label: '^', name: 'Caret', sequence: '^' },
|
|
132
|
+
{ id: 'hash', label: '#', name: 'Hash', sequence: '#' },
|
|
133
|
+
{ id: 'bang', label: '!', name: 'Exclamation mark', sequence: '!' },
|
|
134
|
+
{ id: 'parenOpen', label: '(', name: 'Open parenthesis', sequence: '(' },
|
|
135
|
+
{ id: 'parenClose', label: ')', name: 'Close parenthesis', sequence: ')' },
|
|
136
|
+
{ id: 'bracketOpen', label: '[', name: 'Open bracket', sequence: '[' },
|
|
137
|
+
{ id: 'bracketClose', label: ']', name: 'Close bracket', sequence: ']' },
|
|
138
|
+
{ id: 'braceOpen', label: '{', name: 'Open brace', sequence: '{' },
|
|
139
|
+
{ id: 'braceClose', label: '}', name: 'Close brace', sequence: '}' },
|
|
140
|
+
{ id: 'lessThan', label: '<', name: 'Less than', sequence: '<' },
|
|
141
|
+
{ id: 'greaterThan', label: '>', name: 'Greater than', sequence: '>' },
|
|
142
|
+
{ id: 'quote', label: "'", name: 'Single quote', sequence: "'" },
|
|
143
|
+
{ id: 'doubleQuote', label: '"', name: 'Double quote', sequence: '"' },
|
|
144
|
+
{ id: 'backtick', label: '`', name: 'Backtick', sequence: '`' },
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
export type TerminalKeyId = (typeof TERMINAL_KEYS)[number]['id'] | (typeof TERMINAL_SYMBOLS)[number]['id'];
|
|
148
|
+
|
|
149
|
+
/** Every key the bar can send, indexed for lookup. An unknown id resolves to nothing. */
|
|
150
|
+
const KEY_BY_ID: Partial<Record<TerminalKeyId, TerminalKeyDef>> = {};
|
|
151
|
+
for (const def of [...TERMINAL_KEYS, ...TERMINAL_SYMBOLS]) KEY_BY_ID[def.id as TerminalKeyId] = def;
|
|
152
|
+
|
|
153
|
+
export interface ResolveKeyOptions {
|
|
154
|
+
/** The shell's DECCKM state — xterm reports it as `term.modes`. */
|
|
155
|
+
applicationCursorKeys?: boolean;
|
|
156
|
+
modifiers?: TerminalModifiers;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The bytes one bar button sends. */
|
|
160
|
+
export function resolveKeySequence(id: TerminalKeyId, options: ResolveKeyOptions = {}): string {
|
|
161
|
+
const def = KEY_BY_ID[id];
|
|
162
|
+
if (!def) return '';
|
|
163
|
+
const modifiers = options.modifiers ?? NO_MODIFIERS;
|
|
164
|
+
if (def.csi) return encodeCsi(def.csi, modifiers, options.applicationCursorKeys ?? false);
|
|
165
|
+
// A literal key keeps its own byte: Ctrl is a latch for typed text and a CSI
|
|
166
|
+
// parameter for a cursor key, so a tapped `|` is a pipe, not FS.
|
|
167
|
+
const sequence = def.sequence ?? '';
|
|
168
|
+
return modifiers.alt ? `\x1b${sequence}` : sequence;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function encodeCsi(spec: CsiSpec, modifiers: TerminalModifiers, applicationCursorKeys: boolean): string {
|
|
172
|
+
const parameter = modifierParameter(modifiers);
|
|
173
|
+
if (parameter !== null) return `\x1b[${spec.prefix};${parameter}${spec.final}`;
|
|
174
|
+
// Unmodified: `terminfo` first, then DECCKM, then xterm's CSI default. A line
|
|
175
|
+
// editor binds the sequences its terminfo entry names, so the entry is the
|
|
176
|
+
// authority — the DECCKM check below it is only for the keys whose entry and
|
|
177
|
+
// xterm's default already agree (the arrows).
|
|
178
|
+
if (spec.terminfo) return spec.terminfo;
|
|
179
|
+
if (applicationCursorKeys && spec.application) return `\x1b${spec.application}`;
|
|
180
|
+
return `\x1b[${spec.prefix === '1' ? '' : spec.prefix}${spec.final}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** xterm's modifier parameter: 1 + shift(1) + alt(2) + ctrl(4). No modifier, no parameter. */
|
|
184
|
+
function modifierParameter(modifiers: TerminalModifiers): number | null {
|
|
185
|
+
const bits = (modifiers.alt ? 2 : 0) | (modifiers.ctrl ? 4 : 0);
|
|
186
|
+
return bits === 0 ? null : bits + 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Fold a latched modifier into text typed on the soft keyboard — the path a
|
|
191
|
+
* phone has no other way to reach. Ctrl+C typed as `c` must arrive as `0x03`,
|
|
192
|
+
* and Alt as the ESC prefix (the meta behaviour every terminal has).
|
|
193
|
+
*
|
|
194
|
+
* Only a single character folds: a paste arrives through the same event, and
|
|
195
|
+
* prefixing an ESC to a whole pasted line (or folding its first character into
|
|
196
|
+
* a control code) would corrupt it.
|
|
197
|
+
*/
|
|
198
|
+
export function applyModifiers(data: string, modifiers: TerminalModifiers): string {
|
|
199
|
+
if (data.length !== 1) return data;
|
|
200
|
+
let out = data;
|
|
201
|
+
if (modifiers.ctrl) out = ctrlCharacter(out) ?? out;
|
|
202
|
+
if (modifiers.alt) out = `\x1b${out}`;
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const CTRL_SYMBOLS: Record<string, string> = {
|
|
207
|
+
'@': '\x00',
|
|
208
|
+
' ': '\x00',
|
|
209
|
+
'\x7f': '\x08',
|
|
210
|
+
'[': '\x1b',
|
|
211
|
+
'\\': '\x1c',
|
|
212
|
+
']': '\x1d',
|
|
213
|
+
'^': '\x1e',
|
|
214
|
+
'_': '\x1f',
|
|
215
|
+
'?': '\x7f',
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/** The control character a letter or symbol folds into, or null when it has none. */
|
|
219
|
+
export function ctrlCharacter(character: string): string | null {
|
|
220
|
+
if (character.length !== 1) return null;
|
|
221
|
+
const code = character.charCodeAt(0);
|
|
222
|
+
if (code >= 0x61 && code <= 0x7a) return String.fromCharCode(code - 0x60);
|
|
223
|
+
if (code >= 0x41 && code <= 0x5a) return String.fromCharCode(code - 0x40);
|
|
224
|
+
return CTRL_SYMBOLS[character] ?? null;
|
|
225
|
+
}
|
|
@@ -33,17 +33,18 @@ export interface SettingsCategoryItem {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export interface SettingsState {
|
|
36
|
-
binaryPath: string;
|
|
37
|
-
showUpdateNotifications: boolean;
|
|
38
|
-
agentControlTool: boolean;
|
|
39
|
-
ompChamberWebTool: boolean;
|
|
40
36
|
/** Palette id from the theme catalog (`shared/lib/theme/catalog.ts`). Typed
|
|
41
37
|
* as the catalog's own id union, so a theme dropped from it breaks the build
|
|
42
38
|
* here rather than rendering unstyled at runtime. */
|
|
43
39
|
theme: ThemeId;
|
|
44
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Font family the code editors are set in (`EDITOR_FONT_FAMILIES` in
|
|
42
|
+
* `shared/lib/code/editor/typography.ts`). The value is a CSS family name the
|
|
43
|
+
* editor puts FIRST in its stack, so a face the device does not have falls
|
|
44
|
+
* through to the bundled Fira Code and then to the generic — a setting that
|
|
45
|
+
* cannot render a broken editor, only a different one.
|
|
46
|
+
*/
|
|
45
47
|
editorFont: string;
|
|
46
|
-
streamResponses: boolean;
|
|
47
48
|
streamTransport: StreamTransport;
|
|
48
49
|
/** Generate a session title from the first run. omp suppresses its
|
|
49
50
|
* own auto-titling under `--mode rpc-ui` (PI_NO_TITLE), so without this a
|
|
@@ -51,18 +52,8 @@ export interface SettingsState {
|
|
|
51
52
|
* once when the first user message settles, retried once at that run's end;
|
|
52
53
|
* a later run never re-titles it — and a user-set name is never overwritten. */
|
|
53
54
|
autoSessionTitle: boolean;
|
|
54
|
-
expandedThinking: boolean;
|
|
55
|
-
detailedToolCalls: boolean;
|
|
56
|
-
notificationsEnabled: boolean;
|
|
57
|
-
buildFailureAlert: boolean;
|
|
58
55
|
soundAlerts: boolean;
|
|
59
56
|
chatCompletionSound: boolean;
|
|
60
|
-
defaultWorkspacePath: string;
|
|
61
|
-
tunnelEnabled: boolean;
|
|
62
|
-
tunnelSubdomain: string;
|
|
63
|
-
activeProvider: string;
|
|
64
|
-
autoApproveSafeCmds: boolean;
|
|
65
|
-
autoPatchErrors: boolean;
|
|
66
57
|
followUpBehavior: 'queue' | 'steering';
|
|
67
58
|
keybindingSend: 'Enter' | 'Shift + Enter' | 'Ctrl / Cmd + Enter';
|
|
68
59
|
keybindingNewLine: 'Enter' | 'Shift + Enter' | 'Ctrl / Cmd + Enter';
|