glad-web 1.0.14 → 1.0.15

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.
@@ -344,6 +344,8 @@ async function webCommand(options) {
344
344
 
345
345
  ws.sessionId = sessionId;
346
346
  const session = sessionManager.get(sessionId);
347
+ const isReconnect = session.hasConnectedWebClient;
348
+ session.hasConnectedWebClient = true;
347
349
  if (!session.resizeOwner) {
348
350
  session.resizeOwner = ws;
349
351
  }
@@ -352,7 +354,11 @@ async function webCommand(options) {
352
354
  // Send catchup output. TUI tools may skip the raw circular buffer, so fall
353
355
  // back to the rendered/text history snapshot instead of reconnecting blank.
354
356
  const catchup = sessionManager.getCatchupOutput(sessionId);
355
- if (catchup && catchup.data) {
357
+ ws.needsTuiRedraw = ['antigravity', 'claude-code', 'codex', 'gemini'].includes(session.tool.key)
358
+ && (isReconnect || (catchup && catchup.source === 'rendered-history'));
359
+ if (ws.needsTuiRedraw) {
360
+ ws.send(JSON.stringify({ type: 'reset' }));
361
+ } else if (catchup && catchup.data) {
356
362
  sessionManager.logWsCatchupOutput(sessionId, catchup);
357
363
  ws.send(JSON.stringify({ type: 'output', data: catchup.data }));
358
364
  }
@@ -365,7 +371,12 @@ async function webCommand(options) {
365
371
  }
366
372
  if (payload.type === 'resize' && session.resizeOwner === ws) {
367
373
  sessionManager.logWsResize(sessionId, payload.cols, payload.rows);
368
- sessionManager.resize(sessionId, payload.cols, payload.rows);
374
+ if (ws.needsTuiRedraw) {
375
+ ws.needsTuiRedraw = false;
376
+ sessionManager.redraw(sessionId, payload.cols, payload.rows);
377
+ } else {
378
+ sessionManager.resize(sessionId, payload.cols, payload.rows);
379
+ }
369
380
  }
370
381
  } catch (e) {
371
382
  logger.error('WS Message Error: ' + e.message);
@@ -197,6 +197,20 @@ class PTYManager {
197
197
  }
198
198
  }
199
199
 
200
+ redraw(cols, rows) {
201
+ if (!this.ptyProcess) return false;
202
+
203
+ const targetCols = Math.max(2, Number.parseInt(cols, 10) || 80);
204
+ const targetRows = Math.max(1, Number.parseInt(rows, 10) || 24);
205
+ const pulseCols = targetCols > 2 ? targetCols - 1 : targetCols + 1;
206
+
207
+ this.ptyProcess.resize(pulseCols, targetRows);
208
+ setTimeout(() => {
209
+ if (this.ptyProcess) this.ptyProcess.resize(targetCols, targetRows);
210
+ }, 30);
211
+ return true;
212
+ }
213
+
200
214
  setupLocalResizeListener() {
201
215
  if (!process.stdout.isTTY) return;
202
216
  this.localResizeListener = () => {
@@ -97,6 +97,7 @@ class SessionManager extends EventEmitter {
97
97
  inputSeq: 0,
98
98
  completionReadInputSeq: 0,
99
99
  resizeOwner: null,
100
+ hasConnectedWebClient: false,
100
101
  hasUnreadCompletion: false,
101
102
  write: data => this.write(id, data),
102
103
  isRunning: () => this.has(id) && ptyManager.isRunning(),
@@ -137,6 +138,15 @@ class SessionManager extends EventEmitter {
137
138
  return true;
138
139
  }
139
140
 
141
+ redraw(id, cols, rows) {
142
+ const session = this.get(id);
143
+ if (!session) return false;
144
+ if (session.renderedHistory) {
145
+ session.renderedHistory.resize(cols, rows);
146
+ }
147
+ return session.ptyManager.redraw(cols, rows);
148
+ }
149
+
140
150
  rename(id, name) {
141
151
  const session = this.get(id);
142
152
  if (!session || !name) return null;
@@ -52,7 +52,7 @@
52
52
  #shortcut-rail::-webkit-scrollbar { display: none; }
53
53
  .shortcut-group { flex: 0 0 calc(100vw - 28px); display: grid; gap: 8px; scroll-snap-align: start; }
54
54
  .simple-shortcuts { grid-template-columns: repeat(6, minmax(0, 1fr)); }
55
- .complex-shortcuts { grid-template-columns: repeat(5, minmax(0, 1fr)); }
55
+ .complex-shortcuts { grid-template-columns: repeat(6, minmax(0, 1fr)); }
56
56
  .key-btn, .command-key { flex: 0 0 auto; min-width: 0; height: 40px; border: 1px solid rgba(255,255,255,0.11); border-radius: 12px; background: rgba(255,255,255,0.08); color: #d1d5db; font-size: 12px; font-weight: 700; letter-spacing: 0; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; white-space: nowrap; }
57
57
  .key-btn { font-size: 11px; }
58
58
  .shortcut-group .key-btn, .shortcut-group .command-key { width: 100%; }
@@ -174,9 +174,10 @@
174
174
  <div class="key-btn" data-key="esc">Esc</div>
175
175
  <div class="key-btn" data-key="tab">Tab</div>
176
176
  </div>
177
- <div class="shortcut-group complex-shortcuts">
177
+ <div class="shortcut-group complex-shortcuts" data-role="tool-shortcuts">
178
+ <div class="key-btn special-key" data-key="left">←</div>
179
+ <div class="key-btn special-key" data-key="right">→</div>
178
180
  <div class="key-btn" data-key="ctrl-c">Ctrl+C</div>
179
- <div class="key-btn" data-key="ctrl-y" data-role="tool-action-shortcut" title="Send Ctrl+Y">Ctrl+Y</div>
180
181
  <button class="command-key" data-command="/model" title="Send /model">/model</button>
181
182
  <button class="command-key" data-command="/resume" title="Send /resume">/resume</button>
182
183
  <button class="command-key" data-command="/stat" data-role="usage-shortcut" title="Send /stat">/stat</button>
@@ -831,28 +832,53 @@
831
832
  }
832
833
 
833
834
  function updateToolShortcuts(toolKey) {
834
- const actionBtn = document.querySelector('[data-role="tool-action-shortcut"]');
835
- const usageBtn = document.querySelector('[data-role="usage-shortcut"]');
836
-
837
- if (usageBtn) {
838
- const command = toolKey === 'antigravity' ? '/usage' : '/stat';
839
- usageBtn.dataset.command = command;
840
- usageBtn.textContent = command;
841
- usageBtn.title = `Send ${command}`;
842
- }
843
-
844
- if (!actionBtn) return;
845
-
846
- if (toolKey === 'codex') {
847
- actionBtn.dataset.key = 'codex-perm';
848
- actionBtn.textContent = '/perm';
849
- actionBtn.title = 'Send /perm';
850
- return;
851
- }
852
-
853
- actionBtn.dataset.key = 'ctrl-y';
854
- actionBtn.textContent = 'Ctrl+Y';
855
- actionBtn.title = 'Send Ctrl+Y';
835
+ const usesUsageCommand = ['antigravity', 'claude-code'].includes(toolKey);
836
+ const usageCommand = usesUsageCommand ? '/usage' : '/stat';
837
+ const defaultShortcuts = [
838
+ { key: 'left', label: '←', special: true },
839
+ { key: 'right', label: '', special: true },
840
+ { key: 'ctrl-c', label: 'Ctrl+C' },
841
+ { command: '/model' },
842
+ { command: '/resume' },
843
+ { command: usageCommand }
844
+ ];
845
+ const toolShortcuts = {
846
+ codex: [
847
+ { key: 'ctrl-c', label: 'Ctrl+C' },
848
+ { key: 'codex-perm', label: '/perm', title: 'Send /perm' },
849
+ { command: '/model' },
850
+ { command: '/resume' },
851
+ { command: '/stat' }
852
+ ],
853
+ gemini: [
854
+ { key: 'ctrl-c', label: 'Ctrl+C' },
855
+ { key: 'ctrl-y', label: 'Ctrl+Y', title: 'Send Ctrl+Y' },
856
+ { command: '/model' },
857
+ { command: '/resume' },
858
+ { command: '/stat' }
859
+ ]
860
+ };
861
+ const shortcuts = toolShortcuts[toolKey] || defaultShortcuts;
862
+ const shortcutGroup = document.querySelector('[data-role="tool-shortcuts"]');
863
+ if (!shortcutGroup) return;
864
+
865
+ shortcutGroup.style.gridTemplateColumns = `repeat(${shortcuts.length}, minmax(0, 1fr))`;
866
+ shortcutGroup.replaceChildren(...shortcuts.map(shortcut => {
867
+ const button = document.createElement(shortcut.command ? 'button' : 'div');
868
+ button.className = shortcut.command
869
+ ? 'command-key'
870
+ : `key-btn${shortcut.special ? ' special-key' : ''}`;
871
+ if (shortcut.command) {
872
+ button.dataset.command = shortcut.command;
873
+ button.textContent = shortcut.command;
874
+ button.title = `Send ${shortcut.command}`;
875
+ } else {
876
+ button.dataset.key = shortcut.key;
877
+ button.textContent = shortcut.label;
878
+ button.title = shortcut.title || '';
879
+ }
880
+ return button;
881
+ }));
856
882
  }
857
883
 
858
884
  function joinSession(id, sessionName, toolKey = null) {
@@ -901,6 +927,7 @@
901
927
  currentSocket.onmessage = (e) => {
902
928
  const msg = JSON.parse(e.data);
903
929
  if (msg.type === 'output') term.write(msg.data);
930
+ if (msg.type === 'reset') term.reset();
904
931
  if (msg.type === 'exit') showLobby();
905
932
  };
906
933
  }
@@ -1153,7 +1180,7 @@
1153
1180
  setTimeout(() => { sendWS('\r'); }, 1000);
1154
1181
  return;
1155
1182
  }
1156
- const sequences = { up: '\x1b[A', down: '\x1b[B', enter: '\r', esc: '\x1b', tab: '\t', 'ctrl-c': '\x03', 'ctrl-y': '\x19' };
1183
+ const sequences = { up: '\x1b[A', down: '\x1b[B', left: '\x1b[D', right: '\x1b[C', enter: '\r', esc: '\x1b', tab: '\t', 'ctrl-c': '\x03', 'ctrl-y': '\x19' };
1157
1184
  if (sequences[key]) sendWS(sequences[key]);
1158
1185
  });
1159
1186
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glad-web",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "Glad transforms terminal-based AI coding tools into a polished, mobile-friendly local Web interface.",
5
5
  "main": "index.js",
6
6
  "bin": {