ompchamber 3.1.1 → 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.
Files changed (151) hide show
  1. package/dist/client/index.html +1 -1
  2. package/dist/client/index.js +306 -298
  3. package/dist/client/static/js/index-bsxxt89j.css +1 -0
  4. package/dist/client/static/js/{index-qhzsybkn.js → index-qkt4z7x0.js} +658 -658
  5. package/package.json +4 -3
  6. package/public/sw.js +54 -5
  7. package/src/cli/lib/output.js +15 -8
  8. package/src/client/components/common/Modal.tsx +14 -4
  9. package/src/client/components/{layout/session-sidebar/SubagentList.tsx → common/subagent-list/index.tsx} +8 -10
  10. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +6 -30
  11. package/src/client/components/layout/session-sidebar/Toolbar.tsx +11 -5
  12. package/src/client/components/layout/session-sidebar/index.tsx +4 -2
  13. package/src/client/components/mobile/LayoutWrapper.tsx +6 -0
  14. package/src/client/components/mobile/SessionSidebar.tsx +4 -0
  15. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +27 -8
  16. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +67 -29
  17. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.test.ts +84 -0
  18. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +59 -12
  19. package/src/client/components/mobile/mobile-session-sidebar/Toolbar.tsx +21 -1
  20. package/src/client/components/settings/Modal.tsx +45 -23
  21. package/src/client/components/settings/SettingsSidebarList.tsx +1 -1
  22. package/src/client/components/settings/categories/AgentSettings.tsx +45 -28
  23. package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
  24. package/src/client/components/settings/categories/CommandSettings.tsx +45 -28
  25. package/src/client/components/settings/categories/McpSettings.tsx +48 -31
  26. package/src/client/components/settings/categories/ProjectSettings.tsx +34 -19
  27. package/src/client/components/settings/categories/ProviderSettings.tsx +77 -53
  28. package/src/client/components/settings/categories/SkillSettings.tsx +34 -17
  29. package/src/client/components/settings/categories/UsageSettings.tsx +53 -39
  30. package/src/client/components/settings/categories/appearance-settings/EditorFontSection.tsx +78 -0
  31. package/src/client/components/settings/categories/behavior-settings/Editor.tsx +7 -7
  32. package/src/client/components/settings/categories/mcp-settings/ImportModal.tsx +1 -1
  33. package/src/client/components/settings/categories/mcp-settings/SidebarList.tsx +1 -1
  34. package/src/client/components/settings/categories/project-settings/SidebarList.tsx +1 -1
  35. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +8 -3
  36. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +1 -1
  37. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +9 -4
  38. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +1 -1
  39. package/src/client/components/settings/categories/skill-catalog/AddSourceModal.tsx +1 -1
  40. package/src/client/components/settings/categories/skill-settings/SidebarList.tsx +1 -1
  41. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +1 -1
  42. package/src/client/components/settings/master-detail/index.tsx +68 -0
  43. package/src/client/components/workspace/SearchPanel.tsx +2 -1
  44. package/src/client/components/workspace/chat-timeline/SubagentView.tsx +7 -1
  45. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +19 -12
  46. package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +2 -1
  47. package/src/client/components/workspace/chat-timeline/index.tsx +1 -0
  48. package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +2 -1
  49. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +2 -1
  50. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +2 -1
  51. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +2 -1
  52. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +2 -1
  53. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +2 -1
  54. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +2 -1
  55. package/src/client/components/workspace/diff-panel/SplitView.tsx +31 -5
  56. package/src/client/components/workspace/diff-panel/Toolbar.tsx +77 -6
  57. package/src/client/components/workspace/diff-panel/UnifiedView.tsx +18 -5
  58. package/src/client/components/workspace/diff-panel/index.tsx +36 -7
  59. package/src/client/components/workspace/editor/Toolbar.tsx +11 -4
  60. package/src/client/components/workspace/editor/index.tsx +15 -7
  61. package/src/client/components/workspace/git-panel/commit-modal/CommitDiffViewer.tsx +2 -1
  62. package/src/client/components/workspace/terminal-panel/KeyBar.tsx +194 -0
  63. package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +24 -0
  64. package/src/client/components/workspace/terminal-panel/index.tsx +98 -3
  65. package/src/client/components/workspace/terminal-panel/mount.ts +47 -0
  66. package/src/client/data/theme/terminal.ts +25 -8
  67. package/src/client/hooks/chat/omp/session-list.tsx +31 -1
  68. package/src/client/hooks/chat/omp/session-sidebar-controller.ts +7 -1
  69. package/src/client/hooks/chat/subagent/index.ts +37 -11
  70. package/src/client/hooks/editor/font.ts +53 -0
  71. package/src/client/hooks/editor/use-file-editor.ts +1 -1
  72. package/src/client/hooks/settings/master-detail.ts +33 -0
  73. package/src/client/hooks/ui/clipboard.ts +17 -0
  74. package/src/client/hooks/ui/keyboard-inset.test.ts +46 -0
  75. package/src/client/hooks/ui/keyboard-inset.ts +113 -0
  76. package/src/client/hooks/ui/text-overflow.test.ts +152 -0
  77. package/src/client/hooks/ui/text-overflow.ts +78 -0
  78. package/src/client/hooks/ui/touch-device.ts +46 -0
  79. package/src/client/hooks/workspace/expanded-sessions.ts +62 -0
  80. package/src/client/main.tsx +1 -0
  81. package/src/server/index.ts +4 -3
  82. package/src/server/lib/assets/conditional.server.test.ts +74 -0
  83. package/src/server/lib/assets/conditional.server.ts +54 -0
  84. package/src/server/lib/assets/dev-assets.server.test.ts +44 -0
  85. package/src/server/lib/assets/dev-assets.server.ts +121 -0
  86. package/src/server/lib/assets/fonts.server.ts +9 -4
  87. package/src/server/lib/bundler/build-errors.server.ts +136 -0
  88. package/src/server/lib/bundler/build-errors.test.ts +103 -0
  89. package/src/server/lib/bundler/dependencies.test.ts +134 -0
  90. package/src/server/lib/db/client.ts +5 -1
  91. package/src/server/lib/fs/git-diff.ts +34 -10
  92. package/src/server/lib/fs/git-repos.ts +81 -0
  93. package/src/server/lib/lifecycle/flock.ts +199 -0
  94. package/src/server/lib/lifecycle/identity.ts +5 -1
  95. package/src/server/lib/lifecycle/listener.ts +29 -0
  96. package/src/server/lib/lifecycle/port-guard.ts +18 -3
  97. package/src/server/lib/lifecycle/proc/darwin.ts +52 -17
  98. package/src/server/lib/lifecycle/proc/linux.ts +29 -7
  99. package/src/server/lib/lifecycle/proc/ps.ts +7 -0
  100. package/src/server/lib/lifecycle/proc/types.ts +17 -0
  101. package/src/server/lib/lifecycle/proc/win32.ts +5 -0
  102. package/src/server/lib/observer-ws.ts +150 -0
  103. package/src/server/lib/omp/rpc/constants.ts +9 -0
  104. package/src/server/lib/omp/rpc/subagent-liveness.ts +2 -12
  105. package/src/server/lib/omp/session/file-scan.ts +7 -1
  106. package/src/server/lib/omp/session/files.ts +95 -96
  107. package/src/server/lib/omp/session/locator.ts +18 -8
  108. package/src/server/lib/omp/session/messages.ts +7 -7
  109. package/src/server/lib/omp/session/scan.ts +108 -0
  110. package/src/server/lib/omp/session/stats.ts +50 -50
  111. package/src/server/lib/omp/session/telemetry/scan.ts +89 -7
  112. package/src/server/lib/omp/session/telemetry-raw.ts +24 -8
  113. package/src/server/lib/omp/session/usage/aggregate.ts +170 -55
  114. package/src/server/lib/terminal/runtime.server.ts +3 -0
  115. package/src/server/lib/terminal/session.server.ts +30 -3
  116. package/src/server/plugins/shell.server.ts +52 -13
  117. package/src/server/plugins/ssr.ts +43 -7
  118. package/src/server/plugins/static.ts +51 -18
  119. package/src/server/routes/agent/command.test.ts +69 -0
  120. package/src/server/routes/agent/command.ts +14 -0
  121. package/src/server/routes/agent/ws.test.ts +159 -0
  122. package/src/server/routes/agent/ws.ts +22 -97
  123. package/src/server/routes/btw/ws.ts +22 -95
  124. package/src/server/routes/fs/git.ts +61 -70
  125. package/src/server/routes/omp/session-stats.ts +1 -1
  126. package/src/server/routes/telemetry/tokens.ts +6 -4
  127. package/src/server/routes/terminal/run.ts +4 -8
  128. package/src/server/routes/terminal/ws.ts +24 -2
  129. package/src/shared/lib/chat/omp/agent-events.test.ts +33 -1
  130. package/src/shared/lib/chat/omp/agent-events.ts +10 -0
  131. package/src/shared/lib/code/editor/typography.test.ts +93 -0
  132. package/src/shared/lib/code/editor/typography.ts +86 -0
  133. package/src/shared/lib/code/language.ts +146 -0
  134. package/src/shared/lib/code/syntax-highlight.test.ts +2 -1
  135. package/src/shared/lib/code/syntax-highlight.ts +0 -122
  136. package/src/shared/lib/fonts/NOTICE.md +44 -0
  137. package/src/shared/lib/fonts/nerd-symbols.css +55 -0
  138. package/src/shared/lib/fonts/nerd-symbols.test.ts +134 -0
  139. package/src/shared/lib/fonts/symbols-nerd-font-mono.woff2 +0 -0
  140. package/src/shared/lib/fs/diff-parser.test.ts +5 -5
  141. package/src/shared/lib/fs/diff-parser.ts +67 -42
  142. package/src/shared/lib/omp/subagent/parse.test.ts +88 -0
  143. package/src/shared/lib/omp/subagent/parse.ts +17 -0
  144. package/src/shared/lib/omp/subagent/transcript-client.test.ts +100 -0
  145. package/src/shared/lib/omp/subagent/transcript-client.ts +17 -1
  146. package/src/shared/lib/settings/diff.test.ts +1 -1
  147. package/src/shared/lib/workspace/terminal/keys.test.ts +189 -0
  148. package/src/shared/lib/workspace/terminal/keys.ts +225 -0
  149. package/src/shared/types/omp/subagent.ts +6 -0
  150. package/src/shared/types/settings/state.ts +7 -16
  151. package/dist/client/static/js/index-5yfre1kw.css +0 -1
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The editor's typography, in one place.
8
+ *
9
+ * The desktop editor and the phone's full-screen editor are the same surface on
10
+ * two layouts, so their text has to be the same text — the same face and the
11
+ * same line box. It was not: the stack was written out as a literal twice in
12
+ * the desktop editor and replaced by a bare `monospace` on the phone, which
13
+ * dropped Fira Code (and the ligatures the whole console is typeset in) and put
14
+ * the phone's line numbers in a different face from the code beside them — the
15
+ * gutter was already `font-mono`.
16
+ *
17
+ * `EDITOR_LINE_HEIGHT` is a ratio rather than a pixel value for the reason the
18
+ * desktop editor already used one: the zoom control changes the font size, and
19
+ * a fixed pixel line box keeps the rows at the size they were chosen for at
20
+ * 12px — cramped at 18px, loose at 10px. It has to be applied to the gutter as
21
+ * well as the editor, because with word wrap off the gutter rows carry no
22
+ * explicit height and take their spacing from their own line-height; a gutter
23
+ * pinned to a different value drifts out of register with the lines it counts.
24
+ *
25
+ * The two layouts keep their own padding and gutter width — a phone has less
26
+ * room, and the padding is chrome, not type. Everything that decides how a
27
+ * glyph is drawn lives here.
28
+ */
29
+
30
+ /** Fira Code first (bundled — see `fira-code-fonts.css`), then faces a system may already have. */
31
+ export const EDITOR_FONT_FAMILY = '"Fira Code", "JetBrains Mono", "SF Mono", Consolas, monospace';
32
+
33
+ /**
34
+ * The faces the editor font picker offers, in the order it lists them.
35
+ *
36
+ * Each entry is a CSS family name. The editor puts the chosen one FIRST in its
37
+ * stack and keeps the rest of `EDITOR_FONT_FAMILY` behind it, so a face the
38
+ * device does not have falls through to the bundled Fira Code rather than
39
+ * rendering a blank or a system serif — which is what makes this setting safe
40
+ * to offer without shipping every font in the list. Only Fira Code is bundled;
41
+ * the others are names a system may or may not have, and the picker says so.
42
+ *
43
+ * `id` is the stored value and is deliberately the CSS name itself: a mapping
44
+ * table would add a second thing to keep in sync, and the name is already the
45
+ * only thing the renderer needs.
46
+ */
47
+ export const EDITOR_FONT_CHOICES = [
48
+ { id: 'Fira Code', label: 'Fira Code', bundled: true },
49
+ { id: 'JetBrains Mono', label: 'JetBrains Mono', bundled: false },
50
+ { id: 'SF Mono', label: 'SF Mono', bundled: false },
51
+ { id: 'Menlo', label: 'Menlo', bundled: false },
52
+ { id: 'Consolas', label: 'Consolas', bundled: false },
53
+ { id: 'Monaco', label: 'Monaco', bundled: false },
54
+ { id: 'Courier New', label: 'Courier New', bundled: false },
55
+ ] as const;
56
+
57
+ /** The stored setting's default; also the first entry the picker lists. */
58
+ export const EDITOR_DEFAULT_FONT_FAMILY = 'Fira Code';
59
+
60
+ /**
61
+ * The editor's font stack with `family` preferred.
62
+ *
63
+ * The chosen face is quoted (the list carries multi-word names) and placed
64
+ * ahead of the shared stack, so an unknown or uninstalled name degrades to the
65
+ * bundled Fira Code instead of replacing it. An empty or absent choice yields
66
+ * the shared stack unchanged, which is what a settings row that predates this
67
+ * option holds.
68
+ */
69
+ export function editorFontStack(family?: string | null): string {
70
+ const chosen = (family ?? '').trim();
71
+ if (!chosen || chosen === EDITOR_DEFAULT_FONT_FAMILY) return EDITOR_FONT_FAMILY;
72
+ return `"${chosen}", ${EDITOR_FONT_FAMILY}`;
73
+ }
74
+
75
+ /** Line box as a multiple of the font size; apply to the editor AND its gutter. */
76
+ export const EDITOR_LINE_HEIGHT = 1.5;
77
+
78
+ /** Font size in px both editors open at. */
79
+ export const EDITOR_DEFAULT_FONT_SIZE = 12;
80
+
81
+ /**
82
+ * Zoom bounds, in px. Shared so the two layouts offer the same range — a phone
83
+ * clamped to a narrower one is a different editor, not a smaller one.
84
+ */
85
+ export const EDITOR_MIN_FONT_SIZE = 8;
86
+ export const EDITOR_MAX_FONT_SIZE = 24;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Language detection for the highlighter: which Shiki grammar a path maps to,
8
+ * and whether a block of text looks like code at all.
9
+ *
10
+ * Both are pure string predicates over the input, with no highlighter state, so
11
+ * they live beside the tokenizer rather than inside it — the tokenizer needs a
12
+ * booted Shiki instance, these do not, and a caller that only wants to pick a
13
+ * language (a tool card deciding whether to render a code surface) does not
14
+ * have to pull one in.
15
+ */
16
+
17
+ /** Map file extension to Shiki language id */
18
+ export function getLanguageFromPath(filePath?: string): string {
19
+ if (!filePath) return 'javascript';
20
+ const clean = filePath.split('?')[0].split('#')[0].replace(/:\d+(?:-\d+)?$/, '');
21
+ const ext = clean.split('.').pop()?.toLowerCase();
22
+ switch (ext) {
23
+ case 'ts': return 'typescript';
24
+ case 'tsx': return 'tsx';
25
+ case 'js':
26
+ case 'mjs':
27
+ case 'cjs': return 'javascript';
28
+ case 'jsx': return 'jsx';
29
+ case 'json': return 'json';
30
+ case 'md':
31
+ case 'markdown': return 'markdown';
32
+ case 'css': return 'css';
33
+ case 'sh':
34
+ case 'bash':
35
+ case 'zsh': return 'bash';
36
+ case 'diff':
37
+ case 'patch': return 'diff';
38
+ case 'yaml':
39
+ case 'yml': return 'yaml';
40
+ case 'html': return 'html';
41
+ case 'svg':
42
+ case 'xml': return 'xml';
43
+ case 'py': return 'python';
44
+ case 'go': return 'go';
45
+ case 'rs': return 'rust';
46
+ case 'sql': return 'sql';
47
+ case 'toml': return 'toml';
48
+ case 'java': return 'java';
49
+ case 'c':
50
+ case 'h': return 'c';
51
+ case 'cpp':
52
+ case 'cc':
53
+ case 'hpp': return 'cpp';
54
+ case 'cs': return 'csharp';
55
+ case 'rb': return 'ruby';
56
+ case 'php': return 'php';
57
+ case 'kt':
58
+ case 'kts': return 'kotlin';
59
+ case 'swift': return 'swift';
60
+ case 'scala': return 'scala';
61
+ case 'lua': return 'lua';
62
+ case 'pl': return 'perl';
63
+ case 'dockerfile': return 'dockerfile';
64
+ case 'ini':
65
+ case 'cfg':
66
+ case 'conf': return 'ini';
67
+ case 'properties': return 'properties';
68
+ case 'env': return 'bash';
69
+ case 'zig': return 'zig';
70
+ case 'ex':
71
+ case 'exs': return 'elixir';
72
+ case 'dart': return 'dart';
73
+ case 'ml':
74
+ case 'mli': return 'ocaml';
75
+ case 'hs': return 'haskell';
76
+ case 'nix': return 'nix';
77
+ case 'tf':
78
+ case 'tfvars':
79
+ case 'hcl': return 'hcl';
80
+ case 'graphql':
81
+ case 'gql': return 'graphql';
82
+ case 'sol': return 'solidity';
83
+ case 'r': return 'r';
84
+ case 'jl': return 'julia';
85
+ case 'erl': return 'erlang';
86
+ case 'clj':
87
+ case 'cljs':
88
+ case 'cljc':
89
+ case 'edn': return 'clojure';
90
+ case 'scm':
91
+ case 'ss': return 'scheme';
92
+ case 'rkt': return 'racket';
93
+ case 'proto': return 'protobuf';
94
+ case 'makefile':
95
+ case 'mk': return 'makefile';
96
+ case 'ps1':
97
+ case 'psm1': return 'powershell';
98
+ case 'json5': return 'json5';
99
+ case 'less': return 'less';
100
+ case 'scss': return 'scss';
101
+ case 'sass': return 'sass';
102
+ case 'styl': return 'stylus';
103
+ case 'vue': return 'vue';
104
+ case 'svelte': return 'svelte';
105
+ case 'astro': return 'astro';
106
+ // `.m` is genuinely ambiguous (Objective-C vs MATLAB); map to Objective-C,
107
+ // the common editor default.
108
+ case 'mm':
109
+ case 'm': return 'objective-c';
110
+ case 'groovy':
111
+ case 'gvy':
112
+ case 'gradle': return 'groovy';
113
+ default: return 'javascript';
114
+ }
115
+ }
116
+
117
+ /**
118
+ * The patterns `isCodeLike` probes, at module scope.
119
+ *
120
+ * A regex *literal* inside the function body constructs a fresh `RegExp` on
121
+ * every call, and this runs per render from the notice, task-result and Bash
122
+ * cards. Compiled once here instead.
123
+ */
124
+ const CODE_PATTERNS = [
125
+ /^(?:import|export)\s+.+from\s+['"][^'"]+['"]/m,
126
+ /^(?:const|let|var|function|class|interface|type|enum)\s+[A-Za-z0-9_$]+/m,
127
+ /(?:=>|\bconsole\.(?:log|error|warn)|return\s+|async\s+function)/,
128
+ /^\s*\d+:\s*(?:import|const|let|function|\/\/|\/\*|<|[A-Za-z])/m,
129
+ /^\s*\[[A-Za-z0-9_./-]+\.[a-zA-Z0-9]+\]\s*\n\d+:/m,
130
+ ] as const;
131
+
132
+ /** Check if a string looks like code or structured programming text */
133
+ export function isCodeLike(text: string): boolean {
134
+ if (!text || text.length < 5) return false;
135
+ const trimmed = text.trim();
136
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
137
+ try {
138
+ JSON.parse(trimmed);
139
+ return true;
140
+ } catch {
141
+ // not valid JSON
142
+ }
143
+ }
144
+
145
+ return CODE_PATTERNS.some((pattern) => pattern.test(trimmed));
146
+ }
@@ -7,7 +7,8 @@ import { beforeAll, describe, expect, test } from 'bun:test';
7
7
 
8
8
  import { bootSyntax, onLanguageReady, requestLanguage } from '@/shared/lib/code/highlighter';
9
9
  import { findMatches } from '@/shared/lib/code/editor/find';
10
- import { getLanguageFromPath, highlightCode, highlightLines } from '@/shared/lib/code/syntax-highlight';
10
+ import { getLanguageFromPath } from '@/shared/lib/code/language';
11
+ import { highlightCode, highlightLines } from '@/shared/lib/code/syntax-highlight';
11
12
  import { highlightCodeWindow } from '@/shared/lib/code/windowed-highlight';
12
13
 
13
14
  /** Shiki emits `<span class="shiki">…` with dual-theme CSS variables per token. */
@@ -7,128 +7,6 @@ import { SHIKI_THEMES } from '@/shared/lib/code/shiki-themes';
7
7
 
8
8
  export type { HighlightLinesOptions };
9
9
 
10
- /** Map file extension to Shiki language id */
11
- export function getLanguageFromPath(filePath?: string): string {
12
- if (!filePath) return 'javascript';
13
- const clean = filePath.split('?')[0].split('#')[0].replace(/:\d+(?:-\d+)?$/, '');
14
- const ext = clean.split('.').pop()?.toLowerCase();
15
- switch (ext) {
16
- case 'ts': return 'typescript';
17
- case 'tsx': return 'tsx';
18
- case 'js':
19
- case 'mjs':
20
- case 'cjs': return 'javascript';
21
- case 'jsx': return 'jsx';
22
- case 'json': return 'json';
23
- case 'md':
24
- case 'markdown': return 'markdown';
25
- case 'css': return 'css';
26
- case 'sh':
27
- case 'bash':
28
- case 'zsh': return 'bash';
29
- case 'diff':
30
- case 'patch': return 'diff';
31
- case 'yaml':
32
- case 'yml': return 'yaml';
33
- case 'html': return 'html';
34
- case 'svg':
35
- case 'xml': return 'xml';
36
- case 'py': return 'python';
37
- case 'go': return 'go';
38
- case 'rs': return 'rust';
39
- case 'sql': return 'sql';
40
- case 'toml': return 'toml';
41
- case 'java': return 'java';
42
- case 'c':
43
- case 'h': return 'c';
44
- case 'cpp':
45
- case 'cc':
46
- case 'hpp': return 'cpp';
47
- case 'cs': return 'csharp';
48
- case 'rb': return 'ruby';
49
- case 'php': return 'php';
50
- case 'kt':
51
- case 'kts': return 'kotlin';
52
- case 'swift': return 'swift';
53
- case 'scala': return 'scala';
54
- case 'lua': return 'lua';
55
- case 'pl': return 'perl';
56
- case 'dockerfile': return 'dockerfile';
57
- case 'ini':
58
- case 'cfg':
59
- case 'conf': return 'ini';
60
- case 'properties': return 'properties';
61
- case 'env': return 'bash';
62
- case 'zig': return 'zig';
63
- case 'ex':
64
- case 'exs': return 'elixir';
65
- case 'dart': return 'dart';
66
- case 'ml':
67
- case 'mli': return 'ocaml';
68
- case 'hs': return 'haskell';
69
- case 'nix': return 'nix';
70
- case 'tf':
71
- case 'tfvars':
72
- case 'hcl': return 'hcl';
73
- case 'graphql':
74
- case 'gql': return 'graphql';
75
- case 'sol': return 'solidity';
76
- case 'r': return 'r';
77
- case 'jl': return 'julia';
78
- case 'erl': return 'erlang';
79
- case 'clj':
80
- case 'cljs':
81
- case 'cljc':
82
- case 'edn': return 'clojure';
83
- case 'scm':
84
- case 'ss': return 'scheme';
85
- case 'rkt': return 'racket';
86
- case 'proto': return 'protobuf';
87
- case 'makefile':
88
- case 'mk': return 'makefile';
89
- case 'ps1':
90
- case 'psm1': return 'powershell';
91
- case 'json5': return 'json5';
92
- case 'less': return 'less';
93
- case 'scss': return 'scss';
94
- case 'sass': return 'sass';
95
- case 'styl': return 'stylus';
96
- case 'vue': return 'vue';
97
- case 'svelte': return 'svelte';
98
- case 'astro': return 'astro';
99
- // `.m` is genuinely ambiguous (Objective-C vs MATLAB); map to Objective-C,
100
- // the common editor default.
101
- case 'mm':
102
- case 'm': return 'objective-c';
103
- case 'groovy':
104
- case 'gvy':
105
- case 'gradle': return 'groovy';
106
- default: return 'javascript';
107
- }
108
- }
109
-
110
- /** Check if a string looks like code or structured programming text */
111
- export function isCodeLike(text: string): boolean {
112
- if (!text || text.length < 5) return false;
113
- const trimmed = text.trim();
114
- if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
115
- try {
116
- JSON.parse(trimmed);
117
- return true;
118
- } catch {
119
- // not valid JSON
120
- }
121
- }
122
-
123
- const codePatterns = [
124
- /^(?:import|export)\s+.+from\s+['"][^'"]+['"]/m,
125
- /^(?:const|let|var|function|class|interface|type|enum)\s+[A-Za-z0-9_$]+/m,
126
- /(?:=>|\bconsole\.(?:log|error|warn)|return\s+|async\s+function)/,
127
- /^\s*\d+:\s*(?:import|const|let|function|\/\/|\/\*|<|[A-Za-z])/m,
128
- /^\s*\[[A-Za-z0-9_./-]+\.[a-zA-Z0-9]+\]\s*\n\d+:/m,
129
- ];
130
- return codePatterns.some((pattern) => pattern.test(trimmed));
131
- }
132
10
 
133
11
  /**
134
12
  * Pick a language that can be tokenized *synchronously* right now. A grammar
@@ -0,0 +1,44 @@
1
+ # Bundled font: Symbols Nerd Font Mono
2
+
3
+ `symbols-nerd-font-mono.woff2` is a subset of **SymbolsNerdFontMono-Regular.ttf**
4
+ from [Nerd Fonts](https://github.com/ryanoasis/nerd-fonts) **v3.5.1**, built by
5
+ `scripts/nerd-symbols-font.mjs`. It is the fallback face for the terminal's
6
+ private-use glyphs (powerline separators, devicons, Font Awesome, Material
7
+ Design icons); see `nerd-symbols.css` for why it is needed and how it is scoped.
8
+
9
+ Subset coverage — the private-use areas plus the handful of Nerd-specific
10
+ codepoints outside them:
11
+
12
+ U+E000-F8FF BMP private use
13
+ U+F0000-FFFFD Supplementary private use area-A (Material Design Icons)
14
+ U+23FB-23FE, U+2630, U+2665, U+26A1, U+276C-2771, U+2B58
15
+
16
+ ## Licences
17
+
18
+ The font is a patched build combining several upstream icon sets. Each is
19
+ redistributed here under its own licence, unchanged in substance (the subset
20
+ keeps the glyph outlines verbatim; only `layout-features` and hinting are
21
+ dropped, which are not licence terms).
22
+
23
+ | Icon set | Upstream | Licence |
24
+ |--------------------------|-------------------------------------------------------|-------------|
25
+ | Codicons | https://github.com/microsoft/vscode-codicons | CC BY 4.0 |
26
+ | Devicons | https://github.com/devicons/devicon | MIT |
27
+ | extraglyphs | https://github.com/source-foundry/Hack | MIT |
28
+ | Font Awesome | https://github.com/FortAwesome/Font-Awesome | CC BY 4.0 |
29
+ | Font Awesome Extension | https://github.com/AndreLZGava/font-awesome-extension | MIT |
30
+ | Font Logos | https://github.com/lukas-w/font-logos | unlicensed |
31
+ | Material Design Icons | https://github.com/Templarian/MaterialDesign-Font | Apache 2.0 |
32
+ | Octicons | https://github.com/primer/octicons | MIT |
33
+ | Seti and original | https://github.com/jesseweed/seti-ui | MIT |
34
+ | Pomicons | https://github.com/gabrielelana/pomicons | OFL 1.1 RFN |
35
+ | Powerline Extra Symbols | https://github.com/ryanoasis/powerline-extra-symbols | MIT |
36
+ | Powerline Symbols | https://github.com/powerline/powerline | MIT |
37
+
38
+ The Nerd Fonts project itself (the patching, the font metadata and the
39
+ non-icon glyphs) is MIT, Copyright (c) 2014 Ryan L McIntyre. Its licence text
40
+ accompanies the upstream release at
41
+ https://github.com/ryanoasis/nerd-fonts/blob/master/LICENSE.
42
+
43
+ "Font Logos" is marked unlicensed upstream; it ships inside the Nerd Fonts
44
+ release that this subset is taken from, and is redistributed on that basis.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /*
7
+ * The bundled Nerd Font symbols face.
8
+ *
9
+ * A shell prompt is drawn with private-use symbols — powerline separators,
10
+ * devicons, Font Awesome, Material Design icons — that no coding webfont
11
+ * contains. `@fontsource/fira-code`'s latin subset maps 226 codepoints and not
12
+ * one of them is above U+FFFF, so the terminal's stack can only reach them
13
+ * through a Nerd Font. Until now the stack named installed families only
14
+ * (`XTERM_GLYPH_FALLBACKS`), which means the glyphs appeared on the machine
15
+ * that had one installed and were hollow tofu boxes on every other device — a
16
+ * phone, a second laptop, a colleague's browser.
17
+ *
18
+ * This face closes that gap without taking the choice away: it is declared LAST
19
+ * in the stack, after every installed Nerd Font family, so a machine that
20
+ * already has one renders from it and never fetches these bytes.
21
+ *
22
+ * `unicode-range` is what makes "never fetches" true rather than aspirational.
23
+ * A face whose range no laid-out glyph falls in is never downloaded, so a page
24
+ * that renders only Latin text (or a desktop where an installed family wins)
25
+ * issues no request at all. Verified: a latin-only document with this
26
+ * stylesheet loaded fetched only `fira-code-latin-400-normal.woff2` and left
27
+ * this face `unloaded`; a document laying out U+E0B0 with an installed family
28
+ * ahead of it fetched nothing.
29
+ *
30
+ * `font-display: block` rather than `swap`, unlike the text faces beside it: a
31
+ * swapped symbol paints a tofu box first and repaints, and in a terminal that
32
+ * box is exactly the artifact this face exists to remove. `mount.ts` awaits the
33
+ * face before opening xterm for the same reason — see `ensureNerdGlyphs`.
34
+ *
35
+ * Generated by `bun run scripts/nerd-symbols-font.mjs` from
36
+ * `SymbolsNerdFontMono-Regular.ttf` (Nerd Fonts v3.5.1). See NOTICE.md in this
37
+ * directory for the icon-set licences.
38
+ */
39
+
40
+ @font-face {
41
+ font-family: 'OMPChamber Nerd Symbols';
42
+ font-style: normal;
43
+ font-weight: 400;
44
+ font-display: block;
45
+ unicode-range:
46
+ U+E000-F8FF,
47
+ U+F0000-FFFFD,
48
+ U+23FB-23FE,
49
+ U+2630,
50
+ U+2665,
51
+ U+26A1,
52
+ U+276C-2771,
53
+ U+2B58;
54
+ src: url('./symbols-nerd-font-mono.woff2') format('woff2');
55
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The terminal's Nerd Font coverage is one contract split across three files,
8
+ * and every way it can break is silent at runtime:
9
+ *
10
+ * - The bundled face's family name is written twice — in the stack
11
+ * (`XTERM_FONT_FAMILY`) and in the `@font-face` (`nerd-symbols.css`). If they
12
+ * drift, the face loads and nothing uses it: prompts render as tofu boxes
13
+ * with no error anywhere.
14
+ * - The face must stay LAST in the stack. Moved ahead of the installed families
15
+ * it would win on every machine, which both changes the glyph shapes a user
16
+ * with a Nerd Font already sees AND makes every desktop download 1.1 MB it
17
+ * never needed. That is a bandwidth regression with no visible symptom on the
18
+ * developer's own laptop.
19
+ * - The font file and its `unicode-range` must agree. A range that names
20
+ * codepoints the subset dropped leaves those glyphs as tofu even though the
21
+ * face is loaded and "working".
22
+ *
23
+ * These assertions are cheap and each one pins a failure this change was made
24
+ * to prevent, so they are permanent rather than throwaway.
25
+ */
26
+
27
+ import { describe, expect, test } from 'bun:test';
28
+ import { readFileSync } from 'node:fs';
29
+ import { join } from 'node:path';
30
+
31
+ import {
32
+ NERD_SYMBOLS_FAMILY,
33
+ XTERM_FONT_FAMILY,
34
+ XTERM_GLYPH_FALLBACKS,
35
+ } from '@/client/data/theme/terminal';
36
+
37
+ const FONT_DIR = import.meta.dir;
38
+ const CSS = readFileSync(join(FONT_DIR, 'nerd-symbols.css'), 'utf8');
39
+ const WOFF2 = join(FONT_DIR, 'symbols-nerd-font-mono.woff2');
40
+
41
+ /** Parse `unicode-range: a, b, c;` into codepoint intervals. */
42
+ function declaredRanges(css: string): Array<[number, number]> {
43
+ const block = css.match(/unicode-range:\s*([^;]+);/);
44
+ if (!block) throw new Error('nerd-symbols.css declares no unicode-range');
45
+ return block[1]
46
+ .split(',')
47
+ .map((entry) => entry.trim())
48
+ .filter(Boolean)
49
+ .map((entry) => {
50
+ const [start, end] = entry.split('-');
51
+ const lo = Number.parseInt(start.replace(/^U\+/i, ''), 16);
52
+ const hi = end ? Number.parseInt(end.replace(/^U\+/i, ''), 16) : lo;
53
+ return [lo, hi] as [number, number];
54
+ });
55
+ }
56
+
57
+ describe('bundled Nerd Font symbols face', () => {
58
+ test('the stylesheet declares the family the terminal stack names', () => {
59
+ const declared = CSS.match(/font-family:\s*'([^']+)'/)?.[1];
60
+ expect(declared).toBe(NERD_SYMBOLS_FAMILY);
61
+ });
62
+
63
+ test('the face is LAST in the stack, after every installed family', () => {
64
+ // A local Nerd Font must win, so the bundled face is only reached on a
65
+ // machine that has none — which is also what keeps the desktop from
66
+ // downloading it (its unicode-range is PUA-only, so it is never fetched
67
+ // when another family supplies the glyph).
68
+ expect(XTERM_FONT_FAMILY.indexOf(NERD_SYMBOLS_FAMILY)).toBeGreaterThan(
69
+ XTERM_FONT_FAMILY.indexOf('PowerlineSymbols'),
70
+ );
71
+ // Only the generic fallbacks may follow it.
72
+ const after = XTERM_FONT_FAMILY.slice(XTERM_FONT_FAMILY.indexOf(NERD_SYMBOLS_FAMILY));
73
+ expect(after).not.toMatch(/Nerd Font Mono'/);
74
+ });
75
+
76
+ test('every installed family is still offered before the bundled one', () => {
77
+ for (const family of ['FiraCode Nerd Font Mono', 'JetBrainsMono Nerd Font', 'Symbols Nerd Font Mono']) {
78
+ expect(XTERM_GLYPH_FALLBACKS).toContain(family);
79
+ expect(XTERM_FONT_FAMILY.indexOf(family)).toBeLessThan(
80
+ XTERM_FONT_FAMILY.indexOf(NERD_SYMBOLS_FAMILY),
81
+ );
82
+ }
83
+ });
84
+
85
+ test('the subset actually covers the ranges the stylesheet claims', () => {
86
+ // Reading the cmap needs fonttools, which is not a project dependency. The
87
+ // absence is a legitimate skip; a *failure* of the probe is not, so the two
88
+ // are told apart rather than both being swallowed as "no fonttools".
89
+ const available = Bun.spawnSync([
90
+ 'python3',
91
+ '-c',
92
+ 'import fontTools, brotli',
93
+ ]);
94
+ if (available.exitCode !== 0) {
95
+ console.warn('skipping font coverage check: python3 fonttools/brotli unavailable');
96
+ return;
97
+ }
98
+
99
+ const probe = Bun.spawnSync([
100
+ 'python3',
101
+ '-c',
102
+ `
103
+ import json
104
+ from fontTools.ttLib import TTFont
105
+
106
+ cmap = set(TTFont(${JSON.stringify(WOFF2)}).getBestCmap())
107
+ ranges = ${JSON.stringify(declaredRanges(CSS))}
108
+ uncovered = [r for r in ranges if not any(cp in cmap for cp in range(r[0], r[1] + 1))]
109
+ # The prompt glyphs this face exists for.
110
+ required = [0xE0B0, 0xE0B4, 0xE0B6, 0xE606, 0xE7A8, 0xE718, 0xF015, 0xF03E, 0xF121B, 0xF0001]
111
+ print(json.dumps({ "codepoints": len(cmap), "uncovered": uncovered, "missing": [c for c in required if c not in cmap] }))
112
+ `,
113
+ ]);
114
+
115
+ // fonttools is known to be importable here, so a non-zero exit is a real
116
+ // failure (a corrupt subset, a bad path) and must not read as a skip.
117
+ expect(probe.stderr.toString()).toBe('');
118
+
119
+ const result = JSON.parse(probe.stdout.toString()) as {
120
+ codepoints: number;
121
+ uncovered: Array<[number, number]>;
122
+ missing: number[];
123
+ };
124
+ expect(result.uncovered).toEqual([]);
125
+ expect(result.missing).toEqual([]);
126
+ expect(result.codepoints).toBeGreaterThan(10_000);
127
+ });
128
+
129
+ test('the face file is present and non-trivial', async () => {
130
+ const file = Bun.file(WOFF2);
131
+ expect(await file.exists()).toBe(true);
132
+ expect(file.size).toBeGreaterThan(100_000);
133
+ });
134
+ });
@@ -61,7 +61,7 @@ describe('parseUnifiedDiff', () => {
61
61
  });
62
62
 
63
63
  test('folds a whitespace-only rewrite into context when ignoring whitespace', () => {
64
- const { lines, splitRows, additions, deletions } = parseUnifiedDiff(WHITESPACE_ONLY, true);
64
+ const { lines, splitRows, additions, deletions } = parseUnifiedDiff(WHITESPACE_ONLY, { ignoreWhitespace: true });
65
65
 
66
66
  // The counts the toolbar renders must match what is on screen.
67
67
  expect(additions).toBe(0);
@@ -81,7 +81,7 @@ describe('parseUnifiedDiff', () => {
81
81
  });
82
82
 
83
83
  test('keeps a real change when ignoring whitespace', () => {
84
- const { lines, additions, deletions } = parseUnifiedDiff(REAL_CHANGE, true);
84
+ const { lines, additions, deletions } = parseUnifiedDiff(REAL_CHANGE, { ignoreWhitespace: true });
85
85
 
86
86
  expect(additions).toBe(1);
87
87
  expect(deletions).toBe(1);
@@ -90,7 +90,7 @@ describe('parseUnifiedDiff', () => {
90
90
  });
91
91
 
92
92
  test('folds only the whitespace pair of a mixed hunk', () => {
93
- const { lines, additions, deletions } = parseUnifiedDiff(MIXED, true);
93
+ const { lines, additions, deletions } = parseUnifiedDiff(MIXED, { ignoreWhitespace: true });
94
94
 
95
95
  // Line 1 differs by indentation only; line 2 is a genuine edit.
96
96
  expect(additions).toBe(1);
@@ -110,7 +110,7 @@ describe('parseUnifiedDiff', () => {
110
110
  });
111
111
 
112
112
  test('returns empty results for an empty diff', () => {
113
- expect(parseUnifiedDiff('', true)).toEqual({ lines: [], splitRows: [], additions: 0, deletions: 0 });
114
- expect(parseUnifiedDiff(' \n', false)).toEqual({ lines: [], splitRows: [], additions: 0, deletions: 0 });
113
+ expect(parseUnifiedDiff('', { ignoreWhitespace: true })).toEqual({ lines: [], splitRows: [], additions: 0, deletions: 0 });
114
+ expect(parseUnifiedDiff(' \n', { ignoreWhitespace: false })).toEqual({ lines: [], splitRows: [], additions: 0, deletions: 0 });
115
115
  });
116
116
  });