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.
- package/dist/client/index.html +1 -1
- package/dist/client/static/css/index.e880517fcd.css +2 -0
- package/dist/client/static/js/async/{3y.4c2596f64c.js → 3y.71f2ccf3b6.js} +2 -2
- package/dist/client/static/js/async/{xw.ae67ba06fd.js → 5b.0d6ae50fe8.js} +10 -10
- package/dist/client/static/js/async/5bh.8b2363424f.js +1 -0
- package/dist/client/static/js/async/{vy.bbe114c4e0.js → bi.09688ba75c.js} +1 -2
- package/dist/client/static/js/async/c.797d933882.js +1 -0
- package/dist/client/static/js/async/{dc.6dc4b0a04b.js → dc.856fda6560.js} +2 -2
- package/dist/client/static/js/async/g.a06ca3b3e9.js +2 -0
- package/dist/client/static/js/async/g.a06ca3b3e9.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/{m.4b29d34f93.js → m.c038c71b9f.js} +2 -2
- package/dist/client/static/js/async/ma.2c261b04dd.js +2 -0
- package/dist/client/static/js/async/ms.98960af4eb.js +67 -0
- package/dist/client/static/js/async/nam.b5663e919f.js +10 -0
- package/dist/client/static/js/async/nam.b5663e919f.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/ot.84c71b308f.js +2 -0
- package/dist/client/static/js/async/ot.84c71b308f.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/{q.542c184fec.js → q.23af2a358f.js} +3 -3
- package/dist/client/static/js/async/{s.413c2a4683.js → s.222ee974f6.js} +2 -2
- package/dist/client/static/js/async/t.4dca0e2ba8.js +1 -0
- package/dist/client/static/js/async/uf.46ad6209fa.js +2 -0
- package/dist/client/static/js/async/xn.6360379f71.js +2 -0
- package/dist/client/static/js/async/xnl.feb0021e50.js +2 -0
- package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +12 -0
- package/dist/client/static/js/index.0de15c5e17.js +33 -0
- package/dist/client/static/js/{uy.2e8929ed0c.js → jp.50817896f3.js} +6 -6
- package/package.json +2 -1
- package/src/client/components/common/MarkdownRenderer.tsx +69 -27
- package/src/client/components/common/code-editor/history.test.ts +118 -0
- package/src/client/components/common/code-editor/history.ts +101 -0
- package/src/client/components/common/code-editor/index.tsx +56 -66
- package/src/client/components/common/code-surface/index.tsx +84 -11
- package/src/client/components/common/code-surface/measure.ts +66 -17
- package/src/client/components/common/diagram-viewer/index.tsx +258 -0
- package/src/client/components/common/diagram-viewer/read-block.ts +53 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +2 -1
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +11 -4
- package/src/client/components/layout/session-sidebar/SessionList.tsx +1 -1
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +1 -1
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +1 -1
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +2 -2
- package/src/client/components/settings/LazyModal.tsx +1 -1
- package/src/client/components/settings/Modal.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +11 -4
- package/src/client/components/workspace/chat-timeline/QueueList.tsx +18 -14
- package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +8 -1
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +11 -52
- package/src/client/components/workspace/chat-timeline/chat-input/selection.ts +57 -0
- package/src/client/components/workspace/chat-timeline/index.tsx +124 -108
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/FrameControl.tsx +172 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/QuestionBlock.tsx +155 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/drafts.ts +113 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.tsx +92 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Footer.tsx +3 -3
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Header.tsx +3 -3
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/Lazy.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/SelectBody.tsx +3 -3
- package/src/client/components/workspace/chat-timeline/tool-renderers/{ask-dialog → extension-dialog}/index.tsx +16 -10
- package/src/client/components/workspace/chat-timeline/tool-renderers/index.tsx +2 -2
- package/src/client/components/workspace/file-explorer/index.tsx +18 -5
- package/src/client/components/workspace/git-panel/BranchToolbar.tsx +2 -2
- package/src/client/components/workspace/git-panel/index.tsx +5 -1
- package/src/client/components/workspace/terminal-panel/Header.tsx +51 -19
- package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +86 -282
- package/src/client/components/workspace/terminal-panel/index.tsx +76 -50
- package/src/client/components/workspace/terminal-panel/mount.ts +275 -0
- package/src/client/data/theme/terminal.ts +27 -29
- package/src/client/hooks/chat/omp/session-list.tsx +23 -2
- package/src/client/hooks/chat/omp/session-sidebar-controller.ts +1 -1
- package/src/client/hooks/chat/timeline/actions.ts +23 -34
- package/src/client/hooks/chat/timeline/ask-frames.ts +102 -0
- package/src/client/hooks/chat/timeline/extension-dialog.ts +12 -9
- package/src/client/hooks/chat/timeline/index.ts +18 -28
- package/src/client/hooks/chat/timeline/prompt-text.ts +35 -0
- package/src/client/hooks/chat/timeline/queue.ts +110 -46
- package/src/client/hooks/chat/timeline/send.ts +13 -27
- package/src/client/hooks/chat/timeline/session-load.ts +48 -106
- package/src/client/hooks/chat/timeline/session-pagination.ts +144 -0
- package/src/client/hooks/editor/use-code-window.ts +118 -0
- package/src/client/hooks/ui/input-required-alert.ts +43 -0
- package/src/client/hooks/ui/notification-sound.ts +65 -0
- package/src/client/hooks/ui/theme.ts +22 -6
- package/src/client/hooks/workspace/terminal/index.ts +217 -0
- package/src/client/hooks/workspace/terminal/socket.ts +227 -0
- package/src/client/tailwind.css +38 -12
- package/src/server/db.server.ts +19 -0
- package/src/server/lib/fs/git-sync.ts +191 -0
- package/src/server/lib/omp/rpc/manager.ts +72 -6
- package/src/server/lib/omp/rpc/session-commands.ts +6 -0
- package/src/server/lib/omp/rpc/session-registry.ts +18 -0
- package/src/server/lib/omp/session/sidebar-data.server.ts +10 -4
- package/src/server/lib/queue/delivery.server.ts +114 -0
- package/src/server/lib/queue/schema-migration.server.ts +50 -0
- package/src/server/lib/queue/store.server.ts +185 -0
- package/src/server/lib/terminal/foreground.ts +73 -0
- package/src/server/lib/terminal/history.test.ts +118 -0
- package/src/server/lib/terminal/history.ts +205 -0
- package/src/server/lib/terminal/runtime.server.ts +338 -0
- package/src/server/lib/terminal/session.server.ts +215 -0
- package/src/server/lib/terminal/shell.test.ts +97 -0
- package/src/server/lib/terminal/shell.ts +103 -0
- package/src/server/routes/fs/git.ts +22 -12
- package/src/server/routes/index.ts +2 -1
- package/src/server/routes/sessions/index.ts +11 -3
- package/src/server/routes/sessions/queue.ts +121 -0
- package/src/server/routes/sessions/session.ts +1 -132
- package/src/server/routes/settings/commands.ts +1 -1
- package/src/server/routes/terminal/index.ts +2 -2
- package/src/server/routes/terminal/run.ts +8 -100
- package/src/server/routes/terminal/sessions.ts +24 -0
- package/src/server/routes/terminal/ws.ts +149 -0
- package/src/shared/lib/chat/ask-questions.test.ts +169 -0
- package/src/shared/lib/chat/ask-questions.ts +264 -0
- package/src/shared/lib/chat/omp/agent-events.test.ts +37 -1
- package/src/shared/lib/chat/omp/agent-events.ts +30 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +9 -1
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +45 -8
- package/src/shared/lib/code/lazy-window.test.ts +161 -0
- package/src/shared/lib/code/lazy-window.ts +156 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +50 -1
- package/src/shared/lib/code/syntax-highlight.ts +14 -1
- package/src/shared/lib/code/windowed-highlight.ts +63 -0
- package/src/shared/lib/markdown/mermaid.ts +8 -6
- package/src/shared/lib/omp/session/stream-state.server.ts +2 -3
- package/src/shared/lib/workspace/session-state/store.ts +1 -4
- package/src/shared/lib/workspace/terminal/protocol.ts +222 -0
- package/src/shared/types/omp/agent.ts +14 -2
- package/src/shared/types/terminal.ts +17 -15
- package/src/shared/types/workspace.ts +6 -2
- package/dist/client/static/css/index.51f68210a5.css +0 -2
- package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
- package/dist/client/static/js/async/c.cd45b0ac45.js +0 -1
- package/dist/client/static/js/async/fw.6a51998a8e.js +0 -6
- package/dist/client/static/js/async/g.86e2256a70.js +0 -2
- package/dist/client/static/js/async/kc.bbdb35b771.js +0 -1
- package/dist/client/static/js/async/mw.ca1512563c.js +0 -10
- package/dist/client/static/js/async/rf.d626afc367.js +0 -2
- package/dist/client/static/js/async/t.cda4891526.js +0 -1
- package/dist/client/static/js/async/uf.24ca3d49c3.js +0 -2
- package/dist/client/static/js/async/uf.24ca3d49c3.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/v0.394ed37c6b.js +0 -2
- package/dist/client/static/js/async/v0.394ed37c6b.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/vy.bbe114c4e0.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/xn.afecebbbd9.js +0 -2
- package/dist/client/static/js/index.eccad0b70a.js +0 -33
- package/dist/client/static/js/index.eccad0b70a.js.LICENSE.txt +0 -4
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Ask.tsx +0 -205
- package/src/client/components/workspace/terminal-panel/InputBar.tsx +0 -82
- package/src/client/components/workspace/terminal-panel/LogEntry.tsx +0 -70
- package/src/client/hooks/workspace/terminal-output.ts +0 -66
- package/src/client/hooks/workspace/terminal.ts +0 -283
- package/src/server/lib/fs/terminal-cwd.ts +0 -49
- package/src/server/routes/terminal/stream.ts +0 -100
- /package/dist/client/static/js/async/{3y.4c2596f64c.js.LICENSE.txt → 3y.71f2ccf3b6.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{g.86e2256a70.js.LICENSE.txt → 5b.0d6ae50fe8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{dc.6dc4b0a04b.js.LICENSE.txt → dc.856fda6560.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{fw.6a51998a8e.js.LICENSE.txt → m.c038c71b9f.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{m.4b29d34f93.js.LICENSE.txt → ma.2c261b04dd.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{xn.afecebbbd9.js.LICENSE.txt → ms.98960af4eb.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{mw.ca1512563c.js.LICENSE.txt → q.23af2a358f.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{q.542c184fec.js.LICENSE.txt → s.222ee974f6.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{rf.d626afc367.js.LICENSE.txt → uf.46ad6209fa.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{s.413c2a4683.js.LICENSE.txt → xn.6360379f71.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{async/xw.ae67ba06fd.js.LICENSE.txt → index.0de15c5e17.js.LICENSE.txt} +0 -0
- /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 {
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
({
|
|
42
|
+
({ onInput, onBinaryInput, onGridChange, onReady }, ref) => {
|
|
48
43
|
const { isDark } = useTheme();
|
|
49
44
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
50
|
-
const
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
86
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
76
|
+
focus() {
|
|
77
|
+
mountRef.current?.term.focus();
|
|
122
78
|
},
|
|
123
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
21
|
-
xtermRef.current?.write(
|
|
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
|
|
31
|
-
|
|
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
|
|
38
|
-
xtermRef.current?.
|
|
32
|
+
const handleRestart = useCallback(() => {
|
|
33
|
+
xtermRef.current?.reset();
|
|
39
34
|
}, []);
|
|
40
35
|
|
|
41
36
|
const {
|
|
42
|
-
|
|
43
|
-
setTerminalInput,
|
|
44
|
-
isRunning,
|
|
37
|
+
status,
|
|
45
38
|
cwd,
|
|
39
|
+
shell,
|
|
40
|
+
exitCode,
|
|
41
|
+
error,
|
|
46
42
|
systemInfo,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
103
|
+
status={status}
|
|
104
|
+
exitCode={exitCode}
|
|
76
105
|
rootPath={rootPath}
|
|
77
106
|
activeRepo={activeRepo}
|
|
78
107
|
onSelectRepo={setActiveRepo}
|
|
79
|
-
|
|
80
|
-
|
|
108
|
+
cwd={cwd}
|
|
109
|
+
shell={shell}
|
|
110
|
+
gitBranch={systemInfo.gitBranch}
|
|
111
|
+
onRestart={handleRestartClick}
|
|
81
112
|
/>
|
|
82
113
|
)}
|
|
83
114
|
|
|
84
|
-
{/* Preset
|
|
85
|
-
<TerminalQuickActions
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
}
|