castle-web-cli 0.4.0 → 0.4.2
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/api.d.ts +53 -5
- package/dist/api.js +42 -15
- package/dist/config.d.ts +2 -0
- package/dist/config.js +25 -11
- package/dist/get-deck.d.ts +3 -0
- package/dist/get-deck.js +64 -0
- package/dist/ide-client.d.ts +1 -0
- package/dist/ide-client.js +537 -0
- package/dist/ide.d.ts +16 -0
- package/dist/ide.js +546 -0
- package/dist/index.js +84 -57
- package/dist/init.d.ts +3 -1
- package/dist/init.js +170 -24
- package/dist/localPaths.d.ts +6 -0
- package/dist/localPaths.js +33 -0
- package/dist/login.js +1 -1
- package/dist/preview.d.ts +4 -1
- package/dist/preview.js +63 -41
- package/dist/save-deck.d.ts +2 -0
- package/dist/{push.js → save-deck.js} +66 -5
- package/dist/serve.d.ts +2 -0
- package/dist/serve.js +293 -22
- package/kits/basic-2d/.prettierrc +8 -0
- package/kits/basic-2d/CLAUDE.md +131 -0
- package/kits/basic-2d/behaviors/Camera.jsx +43 -0
- package/kits/basic-2d/behaviors/Collider.jsx +71 -0
- package/kits/basic-2d/behaviors/Drawing.jsx +139 -0
- package/kits/basic-2d/behaviors/Layout.jsx +16 -0
- package/kits/basic-2d/drawings/floor.drawing +70 -0
- package/kits/basic-2d/editors/App.jsx +152 -0
- package/kits/basic-2d/editors/CodeEditor.jsx +112 -0
- package/kits/basic-2d/editors/DrawingEditor.jsx +222 -0
- package/kits/basic-2d/editors/FileBrowser.jsx +143 -0
- package/kits/basic-2d/editors/PlayOnly.jsx +21 -0
- package/kits/basic-2d/editors/SceneEditor.jsx +1012 -0
- package/kits/basic-2d/editors/behaviorRegistry.js +24 -0
- package/kits/basic-2d/editors/editorHistory.js +52 -0
- package/kits/basic-2d/engine/ScenePlayer.jsx +83 -0
- package/kits/basic-2d/engine/SceneUI.jsx +67 -0
- package/kits/basic-2d/engine/TouchControls.jsx +136 -0
- package/kits/basic-2d/engine/autoInspector.jsx +51 -0
- package/kits/basic-2d/engine/files.js +62 -0
- package/kits/basic-2d/engine/scene.js +420 -0
- package/kits/basic-2d/engine/ui.jsx +344 -0
- package/kits/basic-2d/engine/ui.module.css +928 -0
- package/kits/basic-2d/eslint.config.js +50 -0
- package/kits/basic-2d/index.html +11 -0
- package/kits/basic-2d/main.jsx +10 -0
- package/kits/basic-2d/package-lock.json +2706 -0
- package/kits/basic-2d/package.json +41 -0
- package/kits/basic-2d/scenes/main.scene +108 -0
- package/kits/basic-2d/vite.config.js +1 -0
- package/kits/basic-2d-frozen/.prettierrc +8 -0
- package/kits/basic-2d-frozen/CLAUDE.md +131 -0
- package/kits/basic-2d-frozen/behaviors/Camera.jsx +43 -0
- package/kits/basic-2d-frozen/behaviors/Collider.jsx +71 -0
- package/kits/basic-2d-frozen/behaviors/Drawing.jsx +139 -0
- package/kits/basic-2d-frozen/behaviors/Layout.jsx +16 -0
- package/kits/basic-2d-frozen/drawings/floor.drawing +70 -0
- package/kits/basic-2d-frozen/editors/App.jsx +152 -0
- package/kits/basic-2d-frozen/editors/CodeEditor.jsx +112 -0
- package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +222 -0
- package/kits/basic-2d-frozen/editors/FileBrowser.jsx +143 -0
- package/kits/basic-2d-frozen/editors/PlayOnly.jsx +21 -0
- package/kits/basic-2d-frozen/editors/SceneEditor.jsx +1012 -0
- package/kits/basic-2d-frozen/editors/behaviorRegistry.js +24 -0
- package/kits/basic-2d-frozen/editors/editorHistory.js +52 -0
- package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +83 -0
- package/kits/basic-2d-frozen/engine/SceneUI.jsx +67 -0
- package/kits/basic-2d-frozen/engine/TouchControls.jsx +136 -0
- package/kits/basic-2d-frozen/engine/autoInspector.jsx +51 -0
- package/kits/basic-2d-frozen/engine/files.js +62 -0
- package/kits/basic-2d-frozen/engine/scene.js +420 -0
- package/kits/basic-2d-frozen/engine/ui.jsx +344 -0
- package/kits/basic-2d-frozen/engine/ui.module.css +928 -0
- package/kits/basic-2d-frozen/eslint.config.js +50 -0
- package/kits/basic-2d-frozen/index.html +11 -0
- package/kits/basic-2d-frozen/main.jsx +10 -0
- package/kits/basic-2d-frozen/package-lock.json +2706 -0
- package/kits/basic-2d-frozen/package.json +41 -0
- package/kits/basic-2d-frozen/scenes/main.scene +108 -0
- package/kits/basic-2d-frozen/vite.config.js +1 -0
- package/kits/rpg-2d/.prettierrc +8 -0
- package/kits/rpg-2d/behaviors/Camera.tsx +52 -0
- package/kits/rpg-2d/behaviors/Collider.tsx +98 -0
- package/kits/rpg-2d/behaviors/Dialog.tsx +184 -0
- package/kits/rpg-2d/behaviors/Drawing.tsx +161 -0
- package/kits/rpg-2d/behaviors/Friend.tsx +45 -0
- package/kits/rpg-2d/behaviors/Layout.tsx +29 -0
- package/kits/rpg-2d/behaviors/PlayerController.tsx +255 -0
- package/kits/rpg-2d/behaviors/Portal.tsx +60 -0
- package/kits/rpg-2d/behaviors/QuestLog.tsx +90 -0
- package/kits/rpg-2d/behaviors/SaveMenu.tsx +123 -0
- package/kits/rpg-2d/behaviors/Tilemap.tsx +90 -0
- package/kits/rpg-2d/drawings/bld-home.drawing +8136 -0
- package/kits/rpg-2d/drawings/env-crate.drawing +509 -0
- package/kits/rpg-2d/drawings/env-fence.drawing +536 -0
- package/kits/rpg-2d/drawings/env-flower-bed.drawing +607 -0
- package/kits/rpg-2d/drawings/env-fountain.drawing +2622 -0
- package/kits/rpg-2d/drawings/env-hedge.drawing +601 -0
- package/kits/rpg-2d/drawings/env-house-blue.drawing +1 -0
- package/kits/rpg-2d/drawings/env-house-green.drawing +1 -0
- package/kits/rpg-2d/drawings/env-tree-oak.drawing +1540 -0
- package/kits/rpg-2d/drawings/env-tree-pine.drawing +1315 -0
- package/kits/rpg-2d/drawings/floor.drawing +70 -0
- package/kits/rpg-2d/drawings/fx-sparkle.drawing +926 -0
- package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +1099 -0
- package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +4177 -0
- package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +1099 -0
- package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +4177 -0
- package/kits/rpg-2d/drawings/player-idle-down.drawing +1070 -0
- package/kits/rpg-2d/drawings/player-idle-left.drawing +1070 -0
- package/kits/rpg-2d/drawings/player-idle-right.drawing +1070 -0
- package/kits/rpg-2d/drawings/player-idle-up.drawing +1070 -0
- package/kits/rpg-2d/drawings/player-walk-down.drawing +4148 -0
- package/kits/rpg-2d/drawings/player-walk-left.drawing +4148 -0
- package/kits/rpg-2d/drawings/player-walk-right.drawing +4148 -0
- package/kits/rpg-2d/drawings/player-walk-up.drawing +4148 -0
- package/kits/rpg-2d/editors/App.tsx +163 -0
- package/kits/rpg-2d/editors/CodeEditor.tsx +120 -0
- package/kits/rpg-2d/editors/DrawingEditor.tsx +278 -0
- package/kits/rpg-2d/editors/FileBrowser.tsx +191 -0
- package/kits/rpg-2d/editors/PlayOnly.tsx +26 -0
- package/kits/rpg-2d/editors/SceneEditor.tsx +1093 -0
- package/kits/rpg-2d/editors/behaviorRegistry.ts +33 -0
- package/kits/rpg-2d/editors/editorHistory.ts +75 -0
- package/kits/rpg-2d/editors/editorProps.ts +10 -0
- package/kits/rpg-2d/engine/ScenePlayer.tsx +130 -0
- package/kits/rpg-2d/engine/SceneUI.tsx +74 -0
- package/kits/rpg-2d/engine/TouchControls.tsx +157 -0
- package/kits/rpg-2d/engine/autoInspector.tsx +111 -0
- package/kits/rpg-2d/engine/drawing.ts +81 -0
- package/kits/rpg-2d/engine/files.ts +215 -0
- package/kits/rpg-2d/engine/scene.ts +484 -0
- package/kits/rpg-2d/engine/ui.module.css +928 -0
- package/kits/rpg-2d/engine/ui.tsx +483 -0
- package/kits/rpg-2d/eslint.config.js +46 -0
- package/kits/rpg-2d/index.html +11 -0
- package/kits/rpg-2d/main.tsx +14 -0
- package/kits/rpg-2d/package-lock.json +3149 -0
- package/kits/rpg-2d/package.json +46 -0
- package/kits/rpg-2d/scenes/main.scene +203 -0
- package/kits/rpg-2d/tsconfig.json +17 -0
- package/kits/rpg-2d/vite-env.d.ts +7 -0
- package/kits/rpg-2d/vite.config.js +1 -0
- package/package.json +27 -5
- package/AGENTS.md +0 -24
- package/dist/push.d.ts +0 -1
- package/src/api.ts +0 -160
- package/src/bundle.ts +0 -28
- package/src/config.ts +0 -36
- package/src/index.ts +0 -110
- package/src/init.ts +0 -71
- package/src/login.ts +0 -24
- package/src/preview.ts +0 -93
- package/src/push.ts +0 -118
- package/src/serve.ts +0 -128
- package/tsconfig.json +0 -13
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
// Browser-side terminal for the `castle-web serve --ide` panel. Plain DOM (no
|
|
2
|
+
// framework) so it can be served straight from dist with no bundler. xterm.js
|
|
3
|
+
// and addon-fit are loaded as UMD <script> tags before this file, so they
|
|
4
|
+
// arrive as the `Terminal` and `FitAddon` globals.
|
|
5
|
+
//
|
|
6
|
+
// Workarounds ported from castle-cli's `ide` branch and lemo threads' xterm:
|
|
7
|
+
// backoff reconnect + wake handlers, screen replay with a multi-stage repaint
|
|
8
|
+
// repair, FitAddon resize with send-dedup, wheel scroll-chain containment, and
|
|
9
|
+
// the text-presentation fix for symbols browsers like to emoji-render.
|
|
10
|
+
(function () {
|
|
11
|
+
const host = document.getElementById('term-host');
|
|
12
|
+
if (!host)
|
|
13
|
+
return;
|
|
14
|
+
const toggleButton = document.getElementById('term-toggle');
|
|
15
|
+
const reconnectDelaysMs = [250, 500, 1000, 2000, 4000, 8000, 12000, 15000];
|
|
16
|
+
// After a replay, xterm's grid can render at a stale size until a few
|
|
17
|
+
// repaints land; threads found this cadence reliable.
|
|
18
|
+
const replayRepairDelaysMs = [50, 160];
|
|
19
|
+
// Force text presentation for symbols browsers render as wide colour emoji;
|
|
20
|
+
// without it bullets/arrows in CLI output break the terminal grid.
|
|
21
|
+
const textPresentationPattern = /[•‣⁃∙■-◿☀-➿⬀-⯿]/g;
|
|
22
|
+
const variationSelectorText = '︎';
|
|
23
|
+
function forceTextSymbols(value) {
|
|
24
|
+
return value.replace(textPresentationPattern, (c) => `${c}${variationSelectorText}`);
|
|
25
|
+
}
|
|
26
|
+
// Terminal themes. The dark palette is Tokyo Night (from the cli `ide`
|
|
27
|
+
// branch); the choice persists in localStorage and is set from the cog.
|
|
28
|
+
const lightTheme = {
|
|
29
|
+
background: '#ffffff',
|
|
30
|
+
foreground: '#1f2328',
|
|
31
|
+
cursor: '#1f2328',
|
|
32
|
+
cursorAccent: '#ffffff',
|
|
33
|
+
selectionBackground: '#cfe0ff',
|
|
34
|
+
selectionForeground: '#1f2328',
|
|
35
|
+
black: '#24292e',
|
|
36
|
+
red: '#cf222e',
|
|
37
|
+
green: '#116329',
|
|
38
|
+
yellow: '#7d4e00',
|
|
39
|
+
blue: '#0969da',
|
|
40
|
+
magenta: '#8250df',
|
|
41
|
+
cyan: '#1b7c83',
|
|
42
|
+
white: '#6e7781',
|
|
43
|
+
brightBlack: '#57606a',
|
|
44
|
+
brightRed: '#a40e26',
|
|
45
|
+
brightGreen: '#1a7f37',
|
|
46
|
+
brightYellow: '#9a6700',
|
|
47
|
+
brightBlue: '#218bff',
|
|
48
|
+
brightMagenta: '#a475f9',
|
|
49
|
+
brightCyan: '#3192aa',
|
|
50
|
+
brightWhite: '#8c959f',
|
|
51
|
+
};
|
|
52
|
+
const darkTheme = {
|
|
53
|
+
background: '#1a1b26',
|
|
54
|
+
foreground: '#c0caf5',
|
|
55
|
+
cursor: '#c0caf5',
|
|
56
|
+
cursorAccent: '#1a1b26',
|
|
57
|
+
selectionBackground: '#283457',
|
|
58
|
+
selectionForeground: '#c0caf5',
|
|
59
|
+
black: '#15161e',
|
|
60
|
+
red: '#f7768e',
|
|
61
|
+
green: '#9ece6a',
|
|
62
|
+
yellow: '#e0af68',
|
|
63
|
+
blue: '#7aa2f7',
|
|
64
|
+
magenta: '#bb9af7',
|
|
65
|
+
cyan: '#7dcfff',
|
|
66
|
+
white: '#a9b1d6',
|
|
67
|
+
brightBlack: '#414868',
|
|
68
|
+
brightRed: '#f7768e',
|
|
69
|
+
brightGreen: '#9ece6a',
|
|
70
|
+
brightYellow: '#e0af68',
|
|
71
|
+
brightBlue: '#7aa2f7',
|
|
72
|
+
brightMagenta: '#bb9af7',
|
|
73
|
+
brightCyan: '#7dcfff',
|
|
74
|
+
brightWhite: '#c0caf5',
|
|
75
|
+
};
|
|
76
|
+
const themeStorageKey = 'castle-terminal-theme';
|
|
77
|
+
function storedThemeName() {
|
|
78
|
+
try {
|
|
79
|
+
return localStorage.getItem(themeStorageKey) === 'dark' ? 'dark' : 'light';
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return 'light';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let themeName = storedThemeName();
|
|
86
|
+
const term = new Terminal({
|
|
87
|
+
convertEol: false,
|
|
88
|
+
cursorBlink: true,
|
|
89
|
+
macOptionIsMeta: true,
|
|
90
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
|
91
|
+
fontSize: 12,
|
|
92
|
+
fontWeight: 400,
|
|
93
|
+
fontWeightBold: 700,
|
|
94
|
+
letterSpacing: 0,
|
|
95
|
+
lineHeight: 1.25,
|
|
96
|
+
scrollback: 4000,
|
|
97
|
+
theme: themeName === 'dark' ? darkTheme : lightTheme,
|
|
98
|
+
});
|
|
99
|
+
const fit = new FitAddon.FitAddon();
|
|
100
|
+
term.loadAddon(fit);
|
|
101
|
+
term.open(host);
|
|
102
|
+
fit.fit();
|
|
103
|
+
term.focus();
|
|
104
|
+
// The PTY is lazy: the WebSocket isn't opened (and the server doesn't spawn
|
|
105
|
+
// a shell) until the user first reveals the terminal. If they never click
|
|
106
|
+
// the toggle, no PTY ever runs.
|
|
107
|
+
let activated = false;
|
|
108
|
+
let socket = null;
|
|
109
|
+
let socketToken = 0;
|
|
110
|
+
let reconnectAttempt = 0;
|
|
111
|
+
let reconnectTimer = null;
|
|
112
|
+
let reconnectEnabled = true;
|
|
113
|
+
let needsWakeReconnect = document.visibilityState === 'hidden';
|
|
114
|
+
let lastSentCols = 0;
|
|
115
|
+
let lastSentRows = 0;
|
|
116
|
+
const intentionallyClosed = new WeakSet();
|
|
117
|
+
const replayRepairFrames = [];
|
|
118
|
+
const replayRepairTimers = [];
|
|
119
|
+
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
120
|
+
const wsUrl = `${wsProtocol}//${window.location.host}/__castle/pty`;
|
|
121
|
+
//
|
|
122
|
+
// Toggle: hide the panel without unmounting / resizing xterm, so the text
|
|
123
|
+
// layout survives a hide/show cycle untouched (CSS keeps #term at 500px).
|
|
124
|
+
//
|
|
125
|
+
const openStorageKey = 'castle-terminal-open';
|
|
126
|
+
function storedOpen() {
|
|
127
|
+
try {
|
|
128
|
+
return localStorage.getItem(openStorageKey) === '1';
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function persistOpen(open) {
|
|
135
|
+
try {
|
|
136
|
+
localStorage.setItem(openStorageKey, open ? '1' : '0');
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
/* storage disabled -- state just won't persist */
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// First reveal spawns the PTY; later reveals just re-focus a live one.
|
|
143
|
+
function activateTerminal() {
|
|
144
|
+
if (activated)
|
|
145
|
+
return;
|
|
146
|
+
activated = true;
|
|
147
|
+
connect();
|
|
148
|
+
}
|
|
149
|
+
if (toggleButton) {
|
|
150
|
+
toggleButton.addEventListener('click', () => {
|
|
151
|
+
const open = document.body.classList.toggle('term-open');
|
|
152
|
+
persistOpen(open);
|
|
153
|
+
// The header strip is a flex item that appears/disappears with
|
|
154
|
+
// `term-open`, so the xterm host's leftover height changes -- re-fit so
|
|
155
|
+
// the grid matches the new content height (the ResizeObserver also
|
|
156
|
+
// catches this, but fit here makes the relayout immediate).
|
|
157
|
+
fitAndSendResize();
|
|
158
|
+
if (!open) {
|
|
159
|
+
document.body.classList.remove('term-settings-open');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
activateTerminal();
|
|
163
|
+
term.focus();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
//
|
|
167
|
+
// Settings cog: a small popover with a light/dark theme switch for the
|
|
168
|
+
// terminal. The choice persists in localStorage.
|
|
169
|
+
//
|
|
170
|
+
const settingsButton = document.getElementById('term-settings');
|
|
171
|
+
const settingsPanel = document.getElementById('term-settings-panel');
|
|
172
|
+
const segButtons = Array.from(document.querySelectorAll('#term-theme-seg button'));
|
|
173
|
+
function applyTheme(name) {
|
|
174
|
+
themeName = name;
|
|
175
|
+
term.options.theme = name === 'dark' ? darkTheme : lightTheme;
|
|
176
|
+
document.body.classList.toggle('term-dark', name === 'dark');
|
|
177
|
+
for (const button of segButtons) {
|
|
178
|
+
button.classList.toggle('active', button.dataset.theme === name);
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
localStorage.setItem(themeStorageKey, name);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
/* storage disabled -- theme just won't persist */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
applyTheme(themeName);
|
|
188
|
+
if (settingsButton) {
|
|
189
|
+
settingsButton.addEventListener('click', (event) => {
|
|
190
|
+
event.stopPropagation();
|
|
191
|
+
document.body.classList.toggle('term-settings-open');
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
for (const button of segButtons) {
|
|
195
|
+
button.addEventListener('click', () => {
|
|
196
|
+
applyTheme(button.dataset.theme === 'dark' ? 'dark' : 'light');
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// A click anywhere outside the popover (or cog) closes it.
|
|
200
|
+
document.addEventListener('click', (event) => {
|
|
201
|
+
if (!document.body.classList.contains('term-settings-open'))
|
|
202
|
+
return;
|
|
203
|
+
const target = event.target;
|
|
204
|
+
if (settingsPanel && settingsPanel.contains(target))
|
|
205
|
+
return;
|
|
206
|
+
if (settingsButton && settingsButton.contains(target))
|
|
207
|
+
return;
|
|
208
|
+
document.body.classList.remove('term-settings-open');
|
|
209
|
+
});
|
|
210
|
+
//
|
|
211
|
+
// Ctrl+T moves keyboard focus between the terminal and the deck iframe.
|
|
212
|
+
// While the iframe holds focus the parent sees no keydowns, so the deck
|
|
213
|
+
// page forwards Ctrl+T via postMessage (see DECK_FOCUS_SCRIPT in ide.ts).
|
|
214
|
+
// The focused side persists in localStorage and is restored on load.
|
|
215
|
+
//
|
|
216
|
+
const deckFrame = document.getElementById('deck-frame');
|
|
217
|
+
const focusStorageKey = 'castle-terminal-focus';
|
|
218
|
+
function storedFocus() {
|
|
219
|
+
try {
|
|
220
|
+
return localStorage.getItem(focusStorageKey) === 'terminal' ? 'terminal' : 'deck';
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return 'deck';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
let focusTarget = storedFocus();
|
|
227
|
+
function setFocus(target) {
|
|
228
|
+
focusTarget = target;
|
|
229
|
+
try {
|
|
230
|
+
localStorage.setItem(focusStorageKey, target);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
/* storage disabled -- focus side just won't persist */
|
|
234
|
+
}
|
|
235
|
+
if (target === 'terminal') {
|
|
236
|
+
// Focusing the terminal implies revealing it.
|
|
237
|
+
if (!document.body.classList.contains('term-open')) {
|
|
238
|
+
document.body.classList.add('term-open');
|
|
239
|
+
persistOpen(true);
|
|
240
|
+
activateTerminal();
|
|
241
|
+
}
|
|
242
|
+
term.focus();
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
deckFrame?.contentWindow?.focus();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function toggleFocus() {
|
|
249
|
+
setFocus(focusTarget === 'terminal' ? 'deck' : 'terminal');
|
|
250
|
+
}
|
|
251
|
+
// Ctrl+T while the parent (terminal or shell chrome) holds focus. Capture
|
|
252
|
+
// phase + stopImmediatePropagation so xterm never receives the keystroke.
|
|
253
|
+
document.addEventListener('keydown', (event) => {
|
|
254
|
+
if (event.ctrlKey &&
|
|
255
|
+
!event.metaKey &&
|
|
256
|
+
!event.altKey &&
|
|
257
|
+
!event.shiftKey &&
|
|
258
|
+
(event.key === 't' || event.key === 'T')) {
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
event.stopImmediatePropagation();
|
|
261
|
+
toggleFocus();
|
|
262
|
+
}
|
|
263
|
+
}, true);
|
|
264
|
+
// Ctrl+T forwarded from inside the deck iframe.
|
|
265
|
+
window.addEventListener('message', (event) => {
|
|
266
|
+
if (!event.data || event.data.type !== 'castle-ide-toggle-focus')
|
|
267
|
+
return;
|
|
268
|
+
if (deckFrame && event.source !== deckFrame.contentWindow)
|
|
269
|
+
return;
|
|
270
|
+
toggleFocus();
|
|
271
|
+
});
|
|
272
|
+
// Keep focusTarget honest when focus moves by click rather than Ctrl+T.
|
|
273
|
+
if (term.textarea) {
|
|
274
|
+
term.textarea.addEventListener('focus', () => {
|
|
275
|
+
focusTarget = 'terminal';
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
window.addEventListener('blur', () => {
|
|
279
|
+
if (deckFrame && document.activeElement === deckFrame)
|
|
280
|
+
focusTarget = 'deck';
|
|
281
|
+
});
|
|
282
|
+
function clearReconnectTimer() {
|
|
283
|
+
if (reconnectTimer === null)
|
|
284
|
+
return;
|
|
285
|
+
window.clearTimeout(reconnectTimer);
|
|
286
|
+
reconnectTimer = null;
|
|
287
|
+
}
|
|
288
|
+
// xterm sometimes leaves stale rows after a resize/replay; force a redraw.
|
|
289
|
+
function refreshRows() {
|
|
290
|
+
if (term.rows > 0)
|
|
291
|
+
term.refresh(0, term.rows - 1);
|
|
292
|
+
}
|
|
293
|
+
function sendResize() {
|
|
294
|
+
if (!socket || socket.readyState !== WebSocket.OPEN)
|
|
295
|
+
return;
|
|
296
|
+
if (term.cols === lastSentCols && term.rows === lastSentRows)
|
|
297
|
+
return;
|
|
298
|
+
lastSentCols = term.cols;
|
|
299
|
+
lastSentRows = term.rows;
|
|
300
|
+
socket.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
|
301
|
+
}
|
|
302
|
+
function fitNow() {
|
|
303
|
+
fit.fit();
|
|
304
|
+
sendResize();
|
|
305
|
+
}
|
|
306
|
+
function fitAndSendResize() {
|
|
307
|
+
window.requestAnimationFrame(() => {
|
|
308
|
+
fitNow();
|
|
309
|
+
refreshRows();
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
//
|
|
313
|
+
// Replay repaint repair (ported from threads): a replayed screen can render
|
|
314
|
+
// at a stale size; repaint across a microtask, two animation frames, and a
|
|
315
|
+
// couple of timers so the grid settles whatever the layout timing.
|
|
316
|
+
//
|
|
317
|
+
function clearReplayRepairWork() {
|
|
318
|
+
while (replayRepairFrames.length > 0) {
|
|
319
|
+
const frame = replayRepairFrames.pop();
|
|
320
|
+
if (frame !== undefined)
|
|
321
|
+
window.cancelAnimationFrame(frame);
|
|
322
|
+
}
|
|
323
|
+
while (replayRepairTimers.length > 0) {
|
|
324
|
+
const timer = replayRepairTimers.pop();
|
|
325
|
+
if (timer !== undefined)
|
|
326
|
+
window.clearTimeout(timer);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function repairReplayLayout() {
|
|
330
|
+
fitNow();
|
|
331
|
+
refreshRows();
|
|
332
|
+
}
|
|
333
|
+
function scheduleReplayRepair() {
|
|
334
|
+
clearReplayRepairWork();
|
|
335
|
+
window.queueMicrotask(repairReplayLayout);
|
|
336
|
+
replayRepairFrames.push(window.requestAnimationFrame(() => {
|
|
337
|
+
repairReplayLayout();
|
|
338
|
+
replayRepairFrames.push(window.requestAnimationFrame(repairReplayLayout));
|
|
339
|
+
}));
|
|
340
|
+
for (const delay of replayRepairDelaysMs) {
|
|
341
|
+
replayRepairTimers.push(window.setTimeout(repairReplayLayout, delay));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function handleMessage(msg) {
|
|
345
|
+
if (msg.type === 'replay') {
|
|
346
|
+
// Replay must land on a blank terminal, then repaint once layout settles.
|
|
347
|
+
term.reset();
|
|
348
|
+
term.write(forceTextSymbols(String(msg.data ?? '')), scheduleReplayRepair);
|
|
349
|
+
}
|
|
350
|
+
else if (msg.type === 'output') {
|
|
351
|
+
term.write(forceTextSymbols(String(msg.data ?? '')));
|
|
352
|
+
}
|
|
353
|
+
else if (msg.type === 'exit') {
|
|
354
|
+
reconnectEnabled = false;
|
|
355
|
+
clearReconnectTimer();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function scheduleReconnect() {
|
|
359
|
+
if (!reconnectEnabled)
|
|
360
|
+
return;
|
|
361
|
+
clearReconnectTimer();
|
|
362
|
+
if (document.visibilityState === 'hidden' || navigator.onLine === false)
|
|
363
|
+
return;
|
|
364
|
+
if (reconnectAttempt >= reconnectDelaysMs.length)
|
|
365
|
+
return;
|
|
366
|
+
const delay = reconnectDelaysMs[reconnectAttempt];
|
|
367
|
+
reconnectAttempt += 1;
|
|
368
|
+
reconnectTimer = window.setTimeout(connect, delay);
|
|
369
|
+
}
|
|
370
|
+
function connect() {
|
|
371
|
+
if (!reconnectEnabled)
|
|
372
|
+
return;
|
|
373
|
+
clearReconnectTimer();
|
|
374
|
+
if (document.visibilityState === 'hidden' || navigator.onLine === false)
|
|
375
|
+
return;
|
|
376
|
+
if (socket) {
|
|
377
|
+
intentionallyClosed.add(socket);
|
|
378
|
+
try {
|
|
379
|
+
socket.close(1000, 'reconnect');
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
/* ignore */
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const ws = new WebSocket(wsUrl);
|
|
386
|
+
const token = ++socketToken;
|
|
387
|
+
socket = ws;
|
|
388
|
+
lastSentCols = 0;
|
|
389
|
+
lastSentRows = 0;
|
|
390
|
+
ws.addEventListener('open', () => {
|
|
391
|
+
if (socketToken !== token)
|
|
392
|
+
return;
|
|
393
|
+
reconnectAttempt = 0;
|
|
394
|
+
fitAndSendResize();
|
|
395
|
+
});
|
|
396
|
+
ws.addEventListener('message', (event) => {
|
|
397
|
+
if (socketToken !== token)
|
|
398
|
+
return;
|
|
399
|
+
try {
|
|
400
|
+
handleMessage(JSON.parse(String(event.data)));
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
/* ignore malformed frame */
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
ws.addEventListener('close', () => {
|
|
407
|
+
if (socketToken !== token)
|
|
408
|
+
return;
|
|
409
|
+
socket = null;
|
|
410
|
+
if (!reconnectEnabled || intentionallyClosed.has(ws))
|
|
411
|
+
return;
|
|
412
|
+
scheduleReconnect();
|
|
413
|
+
});
|
|
414
|
+
ws.addEventListener('error', () => {
|
|
415
|
+
/* the close handler drives reconnect */
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
//
|
|
419
|
+
// Wake handling: a backgrounded / offline tab drops its socket; reconnect as
|
|
420
|
+
// soon as it becomes visible, focused, or back online (ported from `ide`).
|
|
421
|
+
//
|
|
422
|
+
function wake(force) {
|
|
423
|
+
if (!activated || !reconnectEnabled || document.visibilityState === 'hidden')
|
|
424
|
+
return;
|
|
425
|
+
reconnectAttempt = 0;
|
|
426
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
427
|
+
fitAndSendResize();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (socket && socket.readyState === WebSocket.CONNECTING)
|
|
431
|
+
return;
|
|
432
|
+
if (force || !socket || socket.readyState >= WebSocket.CLOSING)
|
|
433
|
+
connect();
|
|
434
|
+
}
|
|
435
|
+
document.addEventListener('visibilitychange', () => {
|
|
436
|
+
if (document.visibilityState === 'hidden') {
|
|
437
|
+
needsWakeReconnect = true;
|
|
438
|
+
clearReconnectTimer();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const force = needsWakeReconnect;
|
|
442
|
+
needsWakeReconnect = false;
|
|
443
|
+
wake(force);
|
|
444
|
+
});
|
|
445
|
+
window.addEventListener('pagehide', () => {
|
|
446
|
+
needsWakeReconnect = true;
|
|
447
|
+
if (socket) {
|
|
448
|
+
intentionallyClosed.add(socket);
|
|
449
|
+
try {
|
|
450
|
+
socket.close(1000, 'page hidden');
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
/* ignore */
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
clearReconnectTimer();
|
|
457
|
+
});
|
|
458
|
+
window.addEventListener('pageshow', (event) => {
|
|
459
|
+
const force = needsWakeReconnect || event.persisted;
|
|
460
|
+
needsWakeReconnect = false;
|
|
461
|
+
wake(force);
|
|
462
|
+
});
|
|
463
|
+
window.addEventListener('focus', () => {
|
|
464
|
+
const force = needsWakeReconnect;
|
|
465
|
+
needsWakeReconnect = false;
|
|
466
|
+
wake(force);
|
|
467
|
+
});
|
|
468
|
+
window.addEventListener('offline', () => {
|
|
469
|
+
needsWakeReconnect = true;
|
|
470
|
+
clearReconnectTimer();
|
|
471
|
+
});
|
|
472
|
+
window.addEventListener('online', () => {
|
|
473
|
+
const force = needsWakeReconnect;
|
|
474
|
+
needsWakeReconnect = false;
|
|
475
|
+
wake(force);
|
|
476
|
+
});
|
|
477
|
+
// Send Ctrl+Enter as CSI 13;5u so claude/codex CLIs receive newline+modifier.
|
|
478
|
+
term.attachCustomKeyEventHandler((ev) => {
|
|
479
|
+
if (ev.type !== 'keydown')
|
|
480
|
+
return true;
|
|
481
|
+
if (ev.key === 'Enter' && ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
|
482
|
+
ev.preventDefault();
|
|
483
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
484
|
+
socket.send(JSON.stringify({ type: 'input', data: '\x1b[13;5u' }));
|
|
485
|
+
}
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
return true;
|
|
489
|
+
});
|
|
490
|
+
term.onData((data) => {
|
|
491
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
492
|
+
socket.send(JSON.stringify({ type: 'input', data }));
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
// Keep wheel scrolling inside the terminal -- don't chain to the host page
|
|
496
|
+
// when the viewport is at its top/bottom (or has nothing to scroll).
|
|
497
|
+
host.addEventListener('wheel', (event) => {
|
|
498
|
+
const viewport = host.querySelector('.xterm-viewport');
|
|
499
|
+
if (!viewport)
|
|
500
|
+
return;
|
|
501
|
+
const maxScrollTop = viewport.scrollHeight - viewport.clientHeight;
|
|
502
|
+
if (maxScrollTop <= 0) {
|
|
503
|
+
event.preventDefault();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const pastTop = viewport.scrollTop <= 0 && event.deltaY < 0;
|
|
507
|
+
const pastBottom = viewport.scrollTop >= maxScrollTop - 1 && event.deltaY > 0;
|
|
508
|
+
if (pastTop || pastBottom)
|
|
509
|
+
event.preventDefault();
|
|
510
|
+
}, { passive: false });
|
|
511
|
+
const observer = new ResizeObserver(() => fitAndSendResize());
|
|
512
|
+
observer.observe(host);
|
|
513
|
+
// Restore the persisted open state: if the terminal was open last visit,
|
|
514
|
+
// reveal it and reconnect (re-attaching, or spawning, the pty).
|
|
515
|
+
if (storedOpen()) {
|
|
516
|
+
document.body.classList.add('term-open');
|
|
517
|
+
activateTerminal();
|
|
518
|
+
}
|
|
519
|
+
// Restore the persisted focus side. xterm needs a layout pass after
|
|
520
|
+
// term.open() before focus reliably lands on its helper textarea -- a
|
|
521
|
+
// synchronous focus here runs before xterm finishes mounting and silently
|
|
522
|
+
// fails. Defer the terminal-focus restore behind a fit + two animation
|
|
523
|
+
// frames so the grid has settled first (matching threads' post-layout
|
|
524
|
+
// focus sequencing).
|
|
525
|
+
if (storedFocus() === 'terminal' && document.body.classList.contains('term-open')) {
|
|
526
|
+
window.requestAnimationFrame(() => {
|
|
527
|
+
fitNow();
|
|
528
|
+
window.requestAnimationFrame(() => {
|
|
529
|
+
term.focus();
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
else if (deckFrame) {
|
|
534
|
+
deckFrame.contentWindow?.focus();
|
|
535
|
+
}
|
|
536
|
+
})();
|
|
537
|
+
export {};
|
package/dist/ide.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import { Duplex } from 'stream';
|
|
3
|
+
export declare const IDE_ASSET_PREFIX = "/__castle/ide/";
|
|
4
|
+
export declare const PTY_WS_PATH = "/__castle/pty";
|
|
5
|
+
export declare const DECK_FOCUS_SCRIPT: string;
|
|
6
|
+
export interface IdeServer {
|
|
7
|
+
/** Serve the IDE page + its static assets. Returns true if it handled the request. */
|
|
8
|
+
handleHttpRequest(req: http.IncomingMessage, res: http.ServerResponse, reqPath: string): boolean;
|
|
9
|
+
/** Attach the PTY WebSocket if the upgrade targets the PTY path. */
|
|
10
|
+
handleUpgrade(req: http.IncomingMessage, socket: Duplex, head: Buffer): boolean;
|
|
11
|
+
shutdown(): void;
|
|
12
|
+
}
|
|
13
|
+
export declare function createIdeServer(opts: {
|
|
14
|
+
deckDir: string;
|
|
15
|
+
deckLabel: string;
|
|
16
|
+
}): IdeServer;
|