ompchamber 0.7.0 → 1.0.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 (165) hide show
  1. package/dist/client/index.html +1 -1
  2. package/dist/client/static/css/index.e880517fcd.css +2 -0
  3. package/dist/client/static/js/async/{3y.4c2596f64c.js → 3y.71f2ccf3b6.js} +2 -2
  4. package/dist/client/static/js/async/{xw.ae67ba06fd.js → 5b.0d6ae50fe8.js} +10 -10
  5. package/dist/client/static/js/async/5bh.8b2363424f.js +1 -0
  6. package/dist/client/static/js/async/{vy.bbe114c4e0.js → bi.09688ba75c.js} +1 -2
  7. package/dist/client/static/js/async/c.797d933882.js +1 -0
  8. package/dist/client/static/js/async/{dc.6dc4b0a04b.js → dc.856fda6560.js} +2 -2
  9. package/dist/client/static/js/async/g.a06ca3b3e9.js +2 -0
  10. package/dist/client/static/js/async/g.a06ca3b3e9.js.LICENSE.txt +11 -0
  11. package/dist/client/static/js/async/{m.4b29d34f93.js → m.c038c71b9f.js} +2 -2
  12. package/dist/client/static/js/async/ma.2c261b04dd.js +2 -0
  13. package/dist/client/static/js/async/ms.98960af4eb.js +67 -0
  14. package/dist/client/static/js/async/nam.b5663e919f.js +10 -0
  15. package/dist/client/static/js/async/nam.b5663e919f.js.LICENSE.txt +11 -0
  16. package/dist/client/static/js/async/ot.84c71b308f.js +2 -0
  17. package/dist/client/static/js/async/ot.84c71b308f.js.LICENSE.txt +11 -0
  18. package/dist/client/static/js/async/{q.542c184fec.js → q.23af2a358f.js} +3 -3
  19. package/dist/client/static/js/async/{s.413c2a4683.js → s.222ee974f6.js} +2 -2
  20. package/dist/client/static/js/async/t.4dca0e2ba8.js +1 -0
  21. package/dist/client/static/js/async/uf.46ad6209fa.js +2 -0
  22. package/dist/client/static/js/async/xn.6360379f71.js +2 -0
  23. package/dist/client/static/js/async/xnl.feb0021e50.js +2 -0
  24. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +12 -0
  25. package/dist/client/static/js/index.0de15c5e17.js +33 -0
  26. package/dist/client/static/js/{uy.2e8929ed0c.js → jp.50817896f3.js} +6 -6
  27. package/package.json +2 -1
  28. package/src/client/components/common/MarkdownRenderer.tsx +69 -27
  29. package/src/client/components/common/code-editor/history.test.ts +118 -0
  30. package/src/client/components/common/code-editor/history.ts +101 -0
  31. package/src/client/components/common/code-editor/index.tsx +56 -66
  32. package/src/client/components/common/code-surface/index.tsx +84 -11
  33. package/src/client/components/common/code-surface/measure.ts +66 -17
  34. package/src/client/components/common/diagram-viewer/index.tsx +258 -0
  35. package/src/client/components/common/diagram-viewer/read-block.ts +53 -0
  36. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +2 -1
  37. package/src/client/components/layout/session-sidebar/SessionItem.tsx +11 -4
  38. package/src/client/components/layout/session-sidebar/SessionList.tsx +1 -1
  39. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +1 -1
  40. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +1 -1
  41. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +2 -2
  42. package/src/client/components/settings/LazyModal.tsx +1 -1
  43. package/src/client/components/settings/Modal.tsx +2 -1
  44. package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +11 -4
  45. package/src/client/components/workspace/chat-timeline/QueueList.tsx +18 -14
  46. package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +8 -1
  47. package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +11 -52
  48. package/src/client/components/workspace/chat-timeline/chat-input/selection.ts +57 -0
  49. package/src/client/components/workspace/chat-timeline/index.tsx +124 -108
  50. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/FrameControl.tsx +172 -0
  51. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/QuestionBlock.tsx +155 -0
  52. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/drafts.ts +113 -0
  53. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.tsx +92 -0
  54. package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Footer.tsx +3 -3
  55. package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Header.tsx +3 -3
  56. package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Lazy.tsx +8 -8
  57. package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/SelectBody.tsx +3 -3
  58. package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/index.tsx +16 -10
  59. package/src/client/components/workspace/chat-timeline/tool-renderers/index.tsx +2 -2
  60. package/src/client/components/workspace/file-explorer/index.tsx +18 -5
  61. package/src/client/components/workspace/git-panel/BranchToolbar.tsx +2 -2
  62. package/src/client/components/workspace/git-panel/index.tsx +5 -1
  63. package/src/client/components/workspace/terminal-panel/Header.tsx +51 -19
  64. package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +86 -282
  65. package/src/client/components/workspace/terminal-panel/index.tsx +76 -50
  66. package/src/client/components/workspace/terminal-panel/mount.ts +275 -0
  67. package/src/client/data/theme/terminal.ts +27 -29
  68. package/src/client/hooks/chat/omp/session-list.tsx +23 -2
  69. package/src/client/hooks/chat/omp/session-sidebar-controller.ts +1 -1
  70. package/src/client/hooks/chat/timeline/actions.ts +23 -34
  71. package/src/client/hooks/chat/timeline/ask-frames.ts +102 -0
  72. package/src/client/hooks/chat/timeline/extension-dialog.ts +12 -9
  73. package/src/client/hooks/chat/timeline/index.ts +18 -28
  74. package/src/client/hooks/chat/timeline/prompt-text.ts +35 -0
  75. package/src/client/hooks/chat/timeline/queue.ts +110 -46
  76. package/src/client/hooks/chat/timeline/send.ts +13 -27
  77. package/src/client/hooks/chat/timeline/session-load.ts +48 -106
  78. package/src/client/hooks/chat/timeline/session-pagination.ts +144 -0
  79. package/src/client/hooks/editor/use-code-window.ts +118 -0
  80. package/src/client/hooks/ui/input-required-alert.ts +43 -0
  81. package/src/client/hooks/ui/notification-sound.ts +65 -0
  82. package/src/client/hooks/ui/theme.ts +22 -6
  83. package/src/client/hooks/workspace/terminal/index.ts +217 -0
  84. package/src/client/hooks/workspace/terminal/socket.ts +227 -0
  85. package/src/client/tailwind.css +38 -12
  86. package/src/server/db.server.ts +19 -0
  87. package/src/server/lib/fs/git-sync.ts +191 -0
  88. package/src/server/lib/omp/rpc/manager.ts +72 -6
  89. package/src/server/lib/omp/rpc/session-commands.ts +6 -0
  90. package/src/server/lib/omp/rpc/session-registry.ts +18 -0
  91. package/src/server/lib/omp/session/sidebar-data.server.ts +10 -4
  92. package/src/server/lib/queue/delivery.server.ts +114 -0
  93. package/src/server/lib/queue/schema-migration.server.ts +50 -0
  94. package/src/server/lib/queue/store.server.ts +185 -0
  95. package/src/server/lib/terminal/foreground.ts +73 -0
  96. package/src/server/lib/terminal/history.test.ts +118 -0
  97. package/src/server/lib/terminal/history.ts +205 -0
  98. package/src/server/lib/terminal/runtime.server.ts +338 -0
  99. package/src/server/lib/terminal/session.server.ts +215 -0
  100. package/src/server/lib/terminal/shell.test.ts +97 -0
  101. package/src/server/lib/terminal/shell.ts +103 -0
  102. package/src/server/routes/fs/git.ts +22 -12
  103. package/src/server/routes/index.ts +2 -1
  104. package/src/server/routes/sessions/index.ts +11 -3
  105. package/src/server/routes/sessions/queue.ts +121 -0
  106. package/src/server/routes/sessions/session.ts +1 -132
  107. package/src/server/routes/settings/commands.ts +1 -1
  108. package/src/server/routes/terminal/index.ts +2 -2
  109. package/src/server/routes/terminal/run.ts +8 -100
  110. package/src/server/routes/terminal/sessions.ts +24 -0
  111. package/src/server/routes/terminal/ws.ts +149 -0
  112. package/src/shared/lib/chat/ask-questions.test.ts +169 -0
  113. package/src/shared/lib/chat/ask-questions.ts +264 -0
  114. package/src/shared/lib/chat/omp/agent-events.test.ts +37 -1
  115. package/src/shared/lib/chat/omp/agent-events.ts +30 -0
  116. package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +9 -1
  117. package/src/shared/lib/chat/timeline/omp-callbacks.ts +45 -8
  118. package/src/shared/lib/code/lazy-window.test.ts +161 -0
  119. package/src/shared/lib/code/lazy-window.ts +156 -0
  120. package/src/shared/lib/code/syntax-highlight.test.ts +50 -1
  121. package/src/shared/lib/code/syntax-highlight.ts +14 -1
  122. package/src/shared/lib/code/windowed-highlight.ts +63 -0
  123. package/src/shared/lib/markdown/mermaid.ts +8 -6
  124. package/src/shared/lib/omp/session/stream-state.server.ts +2 -3
  125. package/src/shared/lib/workspace/session-state/store.ts +1 -4
  126. package/src/shared/lib/workspace/terminal/protocol.ts +222 -0
  127. package/src/shared/types/omp/agent.ts +14 -2
  128. package/src/shared/types/terminal.ts +17 -15
  129. package/src/shared/types/workspace.ts +6 -2
  130. package/dist/client/static/css/index.51f68210a5.css +0 -2
  131. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  132. package/dist/client/static/js/async/c.cd45b0ac45.js +0 -1
  133. package/dist/client/static/js/async/fw.6a51998a8e.js +0 -6
  134. package/dist/client/static/js/async/g.86e2256a70.js +0 -2
  135. package/dist/client/static/js/async/kc.bbdb35b771.js +0 -1
  136. package/dist/client/static/js/async/mw.ca1512563c.js +0 -10
  137. package/dist/client/static/js/async/rf.d626afc367.js +0 -2
  138. package/dist/client/static/js/async/t.cda4891526.js +0 -1
  139. package/dist/client/static/js/async/uf.24ca3d49c3.js +0 -2
  140. package/dist/client/static/js/async/uf.24ca3d49c3.js.LICENSE.txt +0 -6
  141. package/dist/client/static/js/async/v0.394ed37c6b.js +0 -2
  142. package/dist/client/static/js/async/v0.394ed37c6b.js.LICENSE.txt +0 -6
  143. package/dist/client/static/js/async/vy.bbe114c4e0.js.LICENSE.txt +0 -6
  144. package/dist/client/static/js/async/xn.afecebbbd9.js +0 -2
  145. package/dist/client/static/js/index.eccad0b70a.js +0 -33
  146. package/dist/client/static/js/index.eccad0b70a.js.LICENSE.txt +0 -4
  147. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Ask.tsx +0 -205
  148. package/src/client/components/workspace/terminal-panel/InputBar.tsx +0 -82
  149. package/src/client/components/workspace/terminal-panel/LogEntry.tsx +0 -70
  150. package/src/client/hooks/workspace/terminal-output.ts +0 -66
  151. package/src/client/hooks/workspace/terminal.ts +0 -283
  152. package/src/server/lib/fs/terminal-cwd.ts +0 -49
  153. package/src/server/routes/terminal/stream.ts +0 -100
  154. /package/dist/client/static/js/async/{3y.4c2596f64c.js.LICENSE.txt → 3y.71f2ccf3b6.js.LICENSE.txt} +0 -0
  155. /package/dist/client/static/js/async/{g.86e2256a70.js.LICENSE.txt → 5b.0d6ae50fe8.js.LICENSE.txt} +0 -0
  156. /package/dist/client/static/js/async/{dc.6dc4b0a04b.js.LICENSE.txt → dc.856fda6560.js.LICENSE.txt} +0 -0
  157. /package/dist/client/static/js/async/{fw.6a51998a8e.js.LICENSE.txt → m.c038c71b9f.js.LICENSE.txt} +0 -0
  158. /package/dist/client/static/js/async/{m.4b29d34f93.js.LICENSE.txt → ma.2c261b04dd.js.LICENSE.txt} +0 -0
  159. /package/dist/client/static/js/async/{xn.afecebbbd9.js.LICENSE.txt → ms.98960af4eb.js.LICENSE.txt} +0 -0
  160. /package/dist/client/static/js/async/{mw.ca1512563c.js.LICENSE.txt → q.23af2a358f.js.LICENSE.txt} +0 -0
  161. /package/dist/client/static/js/async/{q.542c184fec.js.LICENSE.txt → s.222ee974f6.js.LICENSE.txt} +0 -0
  162. /package/dist/client/static/js/async/{rf.d626afc367.js.LICENSE.txt → uf.46ad6209fa.js.LICENSE.txt} +0 -0
  163. /package/dist/client/static/js/async/{s.413c2a4683.js.LICENSE.txt → xn.6360379f71.js.LICENSE.txt} +0 -0
  164. /package/dist/client/static/js/{async/xw.ae67ba06fd.js.LICENSE.txt → index.0de15c5e17.js.LICENSE.txt} +0 -0
  165. /package/dist/client/static/js/{uy.2e8929ed0c.js.LICENSE.txt → jp.50817896f3.js.LICENSE.txt} +0 -0
@@ -1,315 +1,119 @@
1
1
  import { useEffect, useRef, useState, useImperativeHandle, useCallback } from 'preact/hooks';
2
2
  import { forwardRef } from 'preact/compat';
3
3
  import { ArrowDown } from 'lucide-preact';
4
- import type { Terminal } from '@xterm/xterm';
5
- import type { FitAddon } from '@xterm/addon-fit';
6
4
  import { useTheme } from '@/client/hooks/ui/theme';
7
- import { useTerminalOutputSync } from '@/client/hooks/workspace/terminal-output';
8
- import {
9
- getXtermTheme,
10
- XTERM_FONT_FAMILY,
11
- safePatchFitAddon,
12
- safePatchRenderService,
13
- getTerminalSessionOutput,
14
- type XtermCore,
15
- type XtermModule,
16
- } from '@/client/data/theme/terminal';
5
+ import { getXtermTheme } from '@/client/data/theme/terminal';
6
+ import { mountXterm, type XtermMount } from '@/client/components/workspace/terminal-panel/mount';
17
7
 
18
8
  export interface RealtimeXtermHandle {
19
- write: (data: string) => void;
20
- writeln: (line: string) => void;
21
- clear: () => void;
9
+ /** Raw PTY output. Bytes, not text: xterm's decoder reassembles them. */
10
+ write: (bytes: Uint8Array) => void;
11
+ /**
12
+ * Write the retained scrollback at the size it was drawn for, then fit back
13
+ * to the panel. Replaying history at another width leaves wrapped fragments
14
+ * the shell's own redraw never clears.
15
+ */
16
+ writeReplay: (bytes: Uint8Array, cols: number, rows: number) => void;
17
+ /** Drop the buffer and reset the cursor — used when the shell is replaced. */
18
+ reset: () => void;
22
19
  focus: () => void;
23
- scrollToBottom: () => void;
24
- scrollToTop: () => void;
25
- scrollLines: (amount: number) => void;
26
20
  }
27
21
 
28
22
  interface RealtimeXtermViewProps {
29
- onCommandSubmit?: (cmd: string, options?: { fromXterm?: boolean }) => void;
30
- cwd: string;
31
- }
32
-
33
- function handleKeyNavigation(term: Terminal, event: globalThis.KeyboardEvent): boolean {
34
- if (event.type !== 'keydown') return true;
35
- if (event.shiftKey) {
36
- if (event.key === 'PageUp') { term.scrollPages(-1); return false; }
37
- if (event.key === 'PageDown') { term.scrollPages(1); return false; }
38
- if (event.key === 'Home') { term.scrollToTop(); return false; }
39
- if (event.key === 'End') { term.scrollToBottom(); return false; }
40
- if (event.key === 'ArrowUp') { term.scrollLines(-1); return false; }
41
- if (event.key === 'ArrowDown') { term.scrollLines(1); return false; }
42
- }
43
- return true;
23
+ /** Keystrokes and paste, raw. */
24
+ onInput: (data: string) => void;
25
+ /** Non-UTF-8 payloads xterm reports separately (some mouse reports). */
26
+ onBinaryInput: (data: string) => void;
27
+ /** The grid changed; the PTY must be resized to match. */
28
+ onGridChange: (cols: number, rows: number) => void;
29
+ /** The terminal is measurable and knows its size; attach with these. */
30
+ onReady: (cols: number, rows: number) => void;
44
31
  }
45
32
 
33
+ /**
34
+ * xterm view bound to a real PTY.
35
+ *
36
+ * Every keystroke goes to the shell as bytes — line editing, history, tab
37
+ * completion and Ctrl+C are the shell's job, not this component's. Output is
38
+ * written as bytes so a character split across two PTY reads is reassembled by
39
+ * xterm's own streaming decoder rather than by a decode step here.
40
+ */
46
41
  export const RealtimeXtermView = forwardRef<RealtimeXtermHandle, RealtimeXtermViewProps>(
47
- ({ onCommandSubmit, cwd: _cwd }, ref) => {
42
+ ({ onInput, onBinaryInput, onGridChange, onReady }, ref) => {
48
43
  const { isDark } = useTheme();
49
44
  const containerRef = useRef<HTMLDivElement>(null);
50
- const terminalRef = useRef<Terminal | null>(null);
51
- const fitAddonRef = useRef<FitAddon | null>(null);
52
- const inputBufferRef = useRef<string>('');
53
- const pendingWritesRef = useRef<string[]>([]);
45
+ const mountRef = useRef<XtermMount | null>(null);
54
46
  const [isScrolledUp, setIsScrolledUp] = useState(false);
55
- const onCommandSubmitRef = useRef(onCommandSubmit);
56
- const { recordOutput, clearOutput } = useTerminalOutputSync(terminalRef);
57
47
 
58
- useEffect(() => {
59
- if (terminalRef.current) {
60
- terminalRef.current.options.theme = getXtermTheme(isDark);
61
- terminalRef.current.refresh(0, terminalRef.current.rows - 1);
62
- }
63
- }, [isDark]);
48
+ const inputRef = useRef(onInput);
49
+ inputRef.current = onInput;
50
+ const binaryInputRef = useRef(onBinaryInput);
51
+ binaryInputRef.current = onBinaryInput;
52
+ const gridChangeRef = useRef(onGridChange);
53
+ gridChangeRef.current = onGridChange;
54
+ const readyRef = useRef(onReady);
55
+ readyRef.current = onReady;
56
+ const themeRef = useRef(isDark);
57
+ themeRef.current = isDark;
64
58
 
65
59
  useEffect(() => {
66
- onCommandSubmitRef.current = onCommandSubmit;
67
- }, [onCommandSubmit]);
68
-
69
- const scrollToBottom = useCallback(() => {
70
- if (terminalRef.current) {
71
- terminalRef.current.scrollToBottom();
72
- setIsScrolledUp(false);
73
- }
74
- }, []);
75
-
76
- const scrollToTop = useCallback(() => {
77
- terminalRef.current?.scrollToTop();
78
- }, []);
79
-
80
- const scrollLines = useCallback((amount: number) => {
81
- terminalRef.current?.scrollLines(amount);
82
- }, []);
60
+ const term = mountRef.current?.term;
61
+ if (!term) return;
62
+ term.options.theme = getXtermTheme(isDark);
63
+ term.refresh(0, term.rows - 1);
64
+ }, [isDark]);
83
65
 
84
66
  useImperativeHandle(ref, () => ({
85
- write: (data: string) => {
86
- recordOutput(data);
87
- if (terminalRef.current) {
88
- const term = terminalRef.current;
89
- const wasAtBottom = term.buffer.active.viewportY === term.buffer.active.baseY;
90
- term.write(data, () => {
91
- if (wasAtBottom) term.scrollToBottom();
92
- });
93
- } else {
94
- pendingWritesRef.current.push(data);
95
- }
67
+ write(bytes: Uint8Array) {
68
+ mountRef.current?.term.write(bytes);
96
69
  },
97
- writeln: (line: string) => {
98
- recordOutput(line + '\r\n');
99
- if (terminalRef.current) {
100
- const term = terminalRef.current;
101
- const wasAtBottom = term.buffer.active.viewportY === term.buffer.active.baseY;
102
- term.writeln(line, () => {
103
- if (wasAtBottom) term.scrollToBottom();
104
- });
105
- } else {
106
- pendingWritesRef.current.push(line + '\r\n');
107
- }
70
+ writeReplay(bytes: Uint8Array, cols: number, rows: number) {
71
+ mountRef.current?.writeReplay(bytes, cols, rows);
108
72
  },
109
- clear: () => {
110
- clearOutput();
111
- if (terminalRef.current) {
112
- terminalRef.current.clear();
113
- terminalRef.current.write('\r\x1b[33m$\x1b[0m ');
114
- recordOutput('\r\x1b[33m$\x1b[0m ');
115
- setIsScrolledUp(false);
116
- } else {
117
- pendingWritesRef.current = [];
118
- }
73
+ reset() {
74
+ mountRef.current?.term.reset();
119
75
  },
120
- focus: () => {
121
- terminalRef.current?.focus();
76
+ focus() {
77
+ mountRef.current?.term.focus();
122
78
  },
123
- scrollToBottom,
124
- scrollToTop,
125
- scrollLines,
126
- }));
127
-
128
- useEffect(() => {
129
- let isMounted = true;
130
- let termInstance: Terminal | null = null;
131
- let fitAddonInstance: FitAddon | null = null;
132
- let resizeObserver: ResizeObserver | null = null;
133
- let dataDisposable: { dispose: () => void } | null = null;
134
- let scrollDisposable: { dispose: () => void } | null = null;
135
- let fitTimeout: ReturnType<typeof setTimeout> | null = null;
136
- let rafId: number | null = null;
137
- let cleanupTouch: (() => void) | null = null;
138
-
139
- const safeFit = () => {
140
- if (!isMounted || !termInstance || !fitAddonInstance || !containerRef.current) return;
141
- const el = containerRef.current;
142
- if (el.clientWidth <= 0 || el.clientHeight <= 0) return;
143
- try {
144
- const core = (termInstance as unknown as XtermCore)._core;
145
- const renderService = core?._renderService;
146
- if (!renderService) return;
147
- const dims = renderService.dimensions;
148
- if (!dims?.css?.cell?.width || !dims?.css?.cell?.height) return;
149
- fitAddonInstance.fit();
150
- } catch {}
151
- };
152
-
153
- async function initXterm() {
154
- if (!containerRef.current || !isMounted) return;
155
-
156
- const xtermModule = (await import('@xterm/xterm')) as unknown as XtermModule<typeof Terminal>;
157
- const fitModule = (await import('@xterm/addon-fit')) as unknown as XtermModule<typeof FitAddon>;
158
-
159
- const TerminalClass =
160
- xtermModule.Terminal ||
161
- xtermModule.default?.Terminal ||
162
- xtermModule.default;
79
+ }), []);
163
80
 
164
- const FitAddonClass =
165
- fitModule.FitAddon ||
166
- fitModule.default?.FitAddon ||
167
- fitModule.default;
168
-
169
- if (!TerminalClass || !FitAddonClass || !containerRef.current || !isMounted) return;
170
-
171
- safePatchFitAddon(FitAddonClass);
172
-
173
- const term = new TerminalClass({
174
- cursorBlink: true,
175
- cursorStyle: 'block',
176
- fontSize: 12,
177
- lineHeight: 1.25,
178
- fontFamily: XTERM_FONT_FAMILY,
179
- theme: getXtermTheme(isDark),
180
- convertEol: true,
181
- scrollback: 10000,
182
- scrollSensitivity: 1.5,
183
- fastScrollSensitivity: 5,
184
- smoothScrollDuration: 0,
185
- allowProposedApi: true,
186
- }) as Terminal;
187
-
188
- term.open(containerRef.current);
189
- safePatchRenderService(term);
190
-
191
- const fitAddon = new FitAddonClass() as FitAddon;
192
- term.loadAddon(fitAddon);
193
-
194
- termInstance = term;
195
- fitAddonInstance = fitAddon;
196
- terminalRef.current = term;
197
- fitAddonRef.current = fitAddon;
198
-
199
- rafId = requestAnimationFrame(() => {
200
- if (isMounted) safeFit();
201
- });
202
-
203
- term.attachCustomKeyEventHandler((event: globalThis.KeyboardEvent) => handleKeyNavigation(term, event));
204
-
205
- scrollDisposable = term.onScroll(() => {
206
- const buffer = term.buffer.active;
207
- setIsScrolledUp(buffer.viewportY < buffer.baseY);
208
- });
209
-
210
- const savedOutput = getTerminalSessionOutput();
211
- if (savedOutput) {
212
- term.write(savedOutput);
213
- } else {
214
- const welcome = '\x1b[1;33m[OMPChamber Realtime Terminal]\x1b[0m\r\n\x1b[90mStream connected. Live xterm canvas active.\x1b[0m\r\n\r\n\x1b[33m$\x1b[0m ';
215
- term.write(welcome);
216
- recordOutput(welcome);
217
- }
218
-
219
- if (pendingWritesRef.current.length > 0) {
220
- for (const chunk of pendingWritesRef.current) {
221
- term.write(chunk);
222
- }
223
- pendingWritesRef.current = [];
224
- term.scrollToBottom();
225
- }
226
-
227
- dataDisposable = term.onData((data: string) => {
228
- if (data === '\r') {
229
- const cmd = inputBufferRef.current.trim();
230
- term.write('\r\n');
231
- inputBufferRef.current = '';
232
- if (cmd && onCommandSubmitRef.current) {
233
- recordOutput(cmd + '\r\n');
234
- onCommandSubmitRef.current(cmd, { fromXterm: true });
235
- } else {
236
- term.write('\x1b[33m$\x1b[0m ');
237
- recordOutput('\r\n\x1b[33m$\x1b[0m ');
238
- }
239
- } else if (data === '\x7f' || data === '\b') {
240
- if (inputBufferRef.current.length > 0) {
241
- inputBufferRef.current = inputBufferRef.current.slice(0, -1);
242
- term.write('\b \b');
243
- }
244
- } else if (data === '\x03') {
245
- inputBufferRef.current = '';
246
- term.write('^C\r\n\x1b[33m$\x1b[0m ');
247
- recordOutput('^C\r\n\x1b[33m$\x1b[0m ');
248
- } else if (data === '\x0c') {
249
- clearOutput();
250
- term.clear();
251
- term.write('\x1b[33m$\x1b[0m ');
252
- recordOutput('\x1b[33m$\x1b[0m ');
253
- } else if (data >= ' ') {
254
- inputBufferRef.current += data;
255
- term.write(data);
256
- }
257
- });
258
-
259
- let touchStartY = 0;
260
- const containerEl = containerRef.current;
261
- const handleTouchStart = (e: globalThis.TouchEvent) => {
262
- if (e.touches.length === 1) touchStartY = e.touches[0].clientY;
263
- };
264
- const handleTouchMove = (e: globalThis.TouchEvent) => {
265
- if (e.touches.length === 1) {
266
- const deltaY = touchStartY - e.touches[0].clientY;
267
- if (Math.abs(deltaY) >= 16) {
268
- term.scrollLines(Math.trunc(deltaY / 16));
269
- touchStartY = e.touches[0].clientY;
270
- }
271
- }
272
- };
273
-
274
- if (containerEl) {
275
- containerEl.addEventListener('touchstart', handleTouchStart, { passive: true });
276
- containerEl.addEventListener('touchmove', handleTouchMove, { passive: true });
277
- cleanupTouch = () => {
278
- containerEl.removeEventListener('touchstart', handleTouchStart);
279
- containerEl.removeEventListener('touchmove', handleTouchMove);
280
- };
281
- }
282
-
283
- resizeObserver = new ResizeObserver(() => {
284
- if (fitTimeout) clearTimeout(fitTimeout);
285
- fitTimeout = setTimeout(() => {
286
- safeFit();
287
- }, 35);
288
- });
81
+ const scrollToBottom = useCallback(() => {
82
+ mountRef.current?.term.scrollToBottom();
83
+ setIsScrolledUp(false);
84
+ }, []);
289
85
 
290
- if (containerEl) {
291
- resizeObserver.observe(containerEl);
86
+ // Mount once. Sizing flows out through callbacks; the server owns the PTY,
87
+ // so nothing here restarts a shell on its own.
88
+ useEffect(() => {
89
+ const container = containerRef.current;
90
+ if (!container) return;
91
+ let cancelled = false;
92
+
93
+ void mountXterm(
94
+ container,
95
+ themeRef.current,
96
+ {
97
+ onInput: (data) => inputRef.current(data),
98
+ onBinaryInput: (data) => binaryInputRef.current(data),
99
+ onGridChange: (cols, rows) => gridChangeRef.current(cols, rows),
100
+ onReady: (cols, rows) => readyRef.current(cols, rows),
101
+ onScrolledUpChange: setIsScrolledUp,
102
+ },
103
+ () => cancelled,
104
+ ).then((mount) => {
105
+ if (!mount) return;
106
+ if (cancelled) {
107
+ mount.dispose();
108
+ return;
292
109
  }
293
- }
294
-
295
- initXterm();
110
+ mountRef.current = mount;
111
+ });
296
112
 
297
113
  return () => {
298
- isMounted = false;
299
- if (rafId !== null) cancelAnimationFrame(rafId);
300
- if (fitTimeout !== null) clearTimeout(fitTimeout);
301
- cleanupTouch?.();
302
- dataDisposable?.dispose();
303
- scrollDisposable?.dispose();
304
- resizeObserver?.disconnect();
305
- try {
306
- fitAddonInstance?.dispose();
307
- } catch {}
308
- try {
309
- termInstance?.dispose();
310
- } catch {}
311
- terminalRef.current = null;
312
- fitAddonRef.current = null;
114
+ cancelled = true;
115
+ mountRef.current?.dispose();
116
+ mountRef.current = null;
313
117
  };
314
118
  }, []);
315
119
 
@@ -1,9 +1,9 @@
1
1
  import { useCallback, useRef } from 'preact/hooks';
2
2
  import { useTerminal } from '@/client/hooks/workspace/terminal';
3
3
  import { useSessionState } from '@/client/hooks/workspace/session-state';
4
+ import { useTheme } from '@/client/hooks/ui/theme';
4
5
  import { TerminalHeader } from '@/client/components/workspace/terminal-panel/Header';
5
6
  import { TerminalQuickActions } from '@/client/components/workspace/terminal-panel/QuickActions';
6
- import { TerminalInputBar } from '@/client/components/workspace/terminal-panel/InputBar';
7
7
  import { RealtimeXtermView, type RealtimeXtermHandle } from '@/client/components/workspace/terminal-panel/RealtimeXtermView';
8
8
 
9
9
  interface TerminalPanelProps {
@@ -13,52 +13,80 @@ interface TerminalPanelProps {
13
13
  showHeader?: boolean;
14
14
  }
15
15
 
16
+ const encoder = new TextEncoder();
17
+
16
18
  export function TerminalPanel({ className = '', enabled = true, rootPath, showHeader = true }: TerminalPanelProps) {
17
19
  const xtermRef = useRef<RealtimeXtermHandle>(null);
20
+ const { isDark } = useTheme();
18
21
  const [activeRepo, setActiveRepo] = useSessionState<string>('terminal.activeRepo', '.');
22
+ const gridRef = useRef<{ cols: number; rows: number } | null>(null);
19
23
 
20
- const handleStreamChunk = useCallback((text: string) => {
21
- xtermRef.current?.write(text);
22
- }, []);
23
-
24
- const handleCommandStart = useCallback((cmd: string, options?: { fromXterm?: boolean }) => {
25
- if (!options?.fromXterm) {
26
- xtermRef.current?.write(`\r\x1b[K\x1b[33m$\x1b[0m \x1b[1m${cmd}\x1b[0m\r\n`);
27
- }
24
+ const handleOutput = useCallback((bytes: Uint8Array) => {
25
+ xtermRef.current?.write(bytes);
28
26
  }, []);
29
27
 
30
- const handleCommandEnd = useCallback((exitCode: number, _cwd: string) => {
31
- if (exitCode !== 0 && exitCode !== 130) {
32
- xtermRef.current?.write(`\x1b[31m[exit ${exitCode}]\x1b[0m\r\n`);
33
- }
34
- xtermRef.current?.write(`\x1b[33m$\x1b[0m `);
28
+ const handleReplay = useCallback((bytes: Uint8Array, frame: { replayCols: number; replayRows: number }) => {
29
+ xtermRef.current?.writeReplay(bytes, frame.replayCols, frame.replayRows);
35
30
  }, []);
36
31
 
37
- const handleXtermClear = useCallback(() => {
38
- xtermRef.current?.clear();
32
+ const handleRestart = useCallback(() => {
33
+ xtermRef.current?.reset();
39
34
  }, []);
40
35
 
41
36
  const {
42
- terminalInput,
43
- setTerminalInput,
44
- isRunning,
37
+ status,
45
38
  cwd,
39
+ shell,
40
+ exitCode,
41
+ error,
46
42
  systemInfo,
47
- executeCommand,
48
- cancelRunningCommand,
49
- handleKeyDown,
43
+ attach,
44
+ resize,
45
+ input,
46
+ restart,
50
47
  } = useTerminal({
51
- onStreamChunk: handleStreamChunk,
52
- onCommandStart: handleCommandStart,
53
- onCommandEnd: handleCommandEnd,
54
- onClear: handleXtermClear,
55
48
  root: enabled ? rootPath : undefined,
56
49
  repo: activeRepo,
50
+ theme: isDark ? 'dark' : 'light',
51
+ onOutput: handleOutput,
52
+ onReplay: handleReplay,
53
+ onRestart: handleRestart,
57
54
  });
58
55
 
59
- const handleCommandSubmit = useCallback((cmd: string, options?: { fromXterm?: boolean }) => {
60
- executeCommand(cmd, options);
61
- }, [executeCommand]);
56
+ // The view reports its grid before the first attach and on every fit, so the
57
+ // PTY is created at the panel's real size instead of a default 80x24.
58
+ const handleReady = useCallback((cols: number, rows: number) => {
59
+ gridRef.current = { cols, rows };
60
+ attach(cols, rows);
61
+ }, [attach]);
62
+
63
+ const handleGridChange = useCallback((cols: number, rows: number) => {
64
+ const previous = gridRef.current;
65
+ gridRef.current = { cols, rows };
66
+ if (!previous) return;
67
+ resize(cols, rows);
68
+ }, [resize]);
69
+
70
+ // Keystrokes arrive as text; `onBinary` payloads are latin-1 bytes and must
71
+ // not be UTF-8 encoded on the way out.
72
+ const handleInput = useCallback((data: string) => {
73
+ input(encoder.encode(data));
74
+ }, [input]);
75
+
76
+ const handleBinaryInput = useCallback((data: string) => {
77
+ const bytes = new Uint8Array(data.length);
78
+ for (let i = 0; i < data.length; i += 1) bytes[i] = data.charCodeAt(i) & 0xff;
79
+ input(bytes);
80
+ }, [input]);
81
+
82
+ const handleQuickAction = useCallback((command: string) => {
83
+ input(encoder.encode(`${command}\r`));
84
+ xtermRef.current?.focus();
85
+ }, [input]);
86
+
87
+ const handleRestartClick = useCallback(() => {
88
+ restart();
89
+ }, [restart]);
62
90
 
63
91
  if (!enabled) {
64
92
  return (
@@ -72,39 +100,37 @@ export function TerminalPanel({ className = '', enabled = true, rootPath, showHe
72
100
  <div className={`flex flex-col h-full w-full bg-paper text-ink overflow-hidden ${className}`}>
73
101
  {showHeader && (
74
102
  <TerminalHeader
75
- isRunning={isRunning}
103
+ status={status}
104
+ exitCode={exitCode}
76
105
  rootPath={rootPath}
77
106
  activeRepo={activeRepo}
78
107
  onSelectRepo={setActiveRepo}
79
- bunVersion={systemInfo.bunVersion}
80
- nodeVersion={systemInfo.nodeVersion}
108
+ cwd={cwd}
109
+ shell={shell}
110
+ gitBranch={systemInfo.gitBranch}
111
+ onRestart={handleRestartClick}
81
112
  />
82
113
  )}
83
114
 
84
- {/* Preset Command Shortcuts */}
85
- <TerminalQuickActions
86
- onSelectCommand={executeCommand}
87
- disabled={isRunning}
88
- />
115
+ {/* Preset command shortcuts: typed into the live shell, not a new process */}
116
+ <TerminalQuickActions onSelectCommand={handleQuickAction} disabled={status !== 'running'} />
117
+
118
+ {error && (
119
+ <div className="px-3 py-1.5 border-b border-error/20 bg-error/5 text-[11px] font-mono text-error flex-shrink-0">
120
+ {error}
121
+ </div>
122
+ )}
89
123
 
90
- {/* Real-time Xterm Terminal Canvas */}
124
+ {/* Real-time Xterm Terminal Canvas, driven by the PTY socket */}
91
125
  <div className="flex-1 w-full min-h-0 bg-canvas overflow-hidden">
92
126
  <RealtimeXtermView
93
127
  ref={xtermRef}
94
- cwd={cwd}
95
- onCommandSubmit={handleCommandSubmit}
128
+ onInput={handleInput}
129
+ onBinaryInput={handleBinaryInput}
130
+ onReady={handleReady}
131
+ onGridChange={handleGridChange}
96
132
  />
97
133
  </div>
98
-
99
- {/* Input Bar with History and Cancel controls */}
100
- <TerminalInputBar
101
- value={terminalInput}
102
- onChange={setTerminalInput}
103
- onSubmit={() => executeCommand()}
104
- onKeyDown={handleKeyDown}
105
- onCancel={cancelRunningCommand}
106
- isRunning={isRunning}
107
- />
108
134
  </div>
109
135
  );
110
136
  }