glad-web 1.0.13 → 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.
package/README.md CHANGED
@@ -122,7 +122,7 @@ glad tools detect
122
122
 
123
123
  ## Supported Tools
124
124
 
125
- Glad currently auto-detects the 20 terminal AI tools defined in the code registry. The names below are the registry `displayName` values used by Glad:
125
+ Glad currently auto-detects the 21 terminal AI tools defined in the code registry. The names below are the registry `displayName` values used by Glad:
126
126
 
127
127
  | Tool | Detected command |
128
128
  | --- | --- |
@@ -132,6 +132,7 @@ Glad currently auto-detects the 20 terminal AI tools defined in the code registr
132
132
  | Copilot | `copilot` |
133
133
  | Cody | `cody chat` |
134
134
  | Gemini | `gemini` |
135
+ | Antigravity | `agy` |
135
136
  | Continue | `cn` |
136
137
  | Cursor | `cursor-agent` |
137
138
  | ChatGPT | `chatgpt` |
package/README.zh-CN.md CHANGED
@@ -122,7 +122,7 @@ glad tools detect
122
122
 
123
123
  ## 支持的工具
124
124
 
125
- Glad 当前会自动检测代码注册表中定义的 20 个终端 AI 工具。下面的名称严格使用 Glad 注册表里的 `displayName`:
125
+ Glad 当前会自动检测代码注册表中定义的 21 个终端 AI 工具。下面的名称严格使用 Glad 注册表里的 `displayName`:
126
126
 
127
127
  | 工具 | 检测命令 |
128
128
  | --- | --- |
@@ -132,6 +132,7 @@ Glad 当前会自动检测代码注册表中定义的 20 个终端 AI 工具。
132
132
  | Copilot | `copilot` |
133
133
  | Cody | `cody chat` |
134
134
  | Gemini | `gemini` |
135
+ | Antigravity | `agy` |
135
136
  | Continue | `cn` |
136
137
  | Cursor | `cursor-agent` |
137
138
  | ChatGPT | `chatgpt` |
@@ -68,6 +68,15 @@ const AI_TOOLS = {
68
68
  website: 'https://developers.google.com/gemini-code-assist',
69
69
  checkInstalled: async () => await commandExists('gemini')
70
70
  },
71
+ 'antigravity': {
72
+ key: 'antigravity',
73
+ command: 'agy',
74
+ args: [],
75
+ displayName: 'Antigravity',
76
+ description: 'Google Antigravity CLI for local agentic coding workflows',
77
+ website: 'https://antigravity.google/docs/cli-overview',
78
+ checkInstalled: async () => await commandExists('agy')
79
+ },
71
80
  'continue': {
72
81
  key: 'continue',
73
82
  command: 'cn',
@@ -344,16 +344,23 @@ 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
  }
350
352
  sessionManager.logWsConnected(sessionId, req);
351
353
 
352
- // Send catchup buffer
353
- const history = session.buffer.getAfter(0);
354
- if (history.length > 0) {
355
- sessionManager.logWsCatchup(sessionId, history);
356
- ws.send(JSON.stringify({ type: 'output', data: history.map(m => m.data).join('') }));
354
+ // Send catchup output. TUI tools may skip the raw circular buffer, so fall
355
+ // back to the rendered/text history snapshot instead of reconnecting blank.
356
+ const catchup = sessionManager.getCatchupOutput(sessionId);
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) {
362
+ sessionManager.logWsCatchupOutput(sessionId, catchup);
363
+ ws.send(JSON.stringify({ type: 'output', data: catchup.data }));
357
364
  }
358
365
 
359
366
  ws.on('message', (message) => {
@@ -364,7 +371,12 @@ async function webCommand(options) {
364
371
  }
365
372
  if (payload.type === 'resize' && session.resizeOwner === ws) {
366
373
  sessionManager.logWsResize(sessionId, payload.cols, payload.rows);
367
- 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
+ }
368
380
  }
369
381
  } catch (e) {
370
382
  logger.error('WS Message Error: ' + e.message);
@@ -23,7 +23,7 @@ class PTYManager {
23
23
  this.onExitCallback = null;
24
24
  this.localResizeListener = null;
25
25
 
26
- this.tuiTools = ['opencode', 'kilo'];
26
+ this.tuiTools = ['antigravity', 'opencode', 'kilo'];
27
27
  this.isTUIMode = this.tuiTools.includes(tool.key);
28
28
 
29
29
  this.recentOutputs = [];
@@ -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;
@@ -192,6 +202,33 @@ class SessionManager extends EventEmitter {
192
202
  };
193
203
  }
194
204
 
205
+ getCatchupOutput(id) {
206
+ const session = this.get(id);
207
+ if (!session) return null;
208
+
209
+ const bufferHistory = session.buffer.getAfter(0);
210
+ if (bufferHistory.length > 0) {
211
+ return {
212
+ source: 'buffer',
213
+ items: bufferHistory.length,
214
+ data: bufferHistory.map(message => message.data).join('')
215
+ };
216
+ }
217
+
218
+ const historySource = session.renderedHistory || session.textHistory;
219
+ if (!historySource || typeof historySource.toJSON !== 'function') {
220
+ return { source: 'none', items: 0, data: '' };
221
+ }
222
+
223
+ const snapshot = historySource.toJSON();
224
+ const text = String(snapshot.text || '');
225
+ return {
226
+ source: session.renderedHistory ? 'rendered-history' : 'text-history',
227
+ items: snapshot.lines || 0,
228
+ data: text ? text + (text.endsWith('\n') ? '' : '\r\n') : ''
229
+ };
230
+ }
231
+
195
232
  getDiagnostics(id) {
196
233
  const session = this.get(id);
197
234
  return session ? this.getSessionDiagnostics(session) : null;
@@ -234,6 +271,17 @@ class SessionManager extends EventEmitter {
234
271
  }, { compact: true });
235
272
  }
236
273
 
274
+ logWsCatchupOutput(id, catchup) {
275
+ const session = this.get(id);
276
+ if (!session) return;
277
+ this.logSessionDiagnostics('ws-catchup', session, {
278
+ catchupSource: catchup.source,
279
+ catchupItems: catchup.items,
280
+ catchupBytes: Buffer.byteLength(String(catchup.data || ''), 'utf8'),
281
+ catchupPreview: previewText(catchup.data)
282
+ }, { compact: true });
283
+ }
284
+
237
285
  logWsResize(id, cols, rows) {
238
286
  const session = this.get(id);
239
287
  if (!session) return;
@@ -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,12 +174,13 @@
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
- <button class="command-key" data-command="/stat" title="Send /stat">/stat</button>
183
+ <button class="command-key" data-command="/stat" data-role="usage-shortcut" title="Send /stat">/stat</button>
183
184
  </div>
184
185
  </div>
185
186
  </div>
@@ -831,19 +832,53 @@
831
832
  }
832
833
 
833
834
  function updateToolShortcuts(toolKey) {
834
- const actionBtn = document.querySelector('[data-role="tool-action-shortcut"]');
835
- if (!actionBtn) return;
836
-
837
- if (toolKey === 'codex') {
838
- actionBtn.dataset.key = 'codex-perm';
839
- actionBtn.textContent = '/perm';
840
- actionBtn.title = 'Send /perm';
841
- return;
842
- }
843
-
844
- actionBtn.dataset.key = 'ctrl-y';
845
- actionBtn.textContent = 'Ctrl+Y';
846
- 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
+ }));
847
882
  }
848
883
 
849
884
  function joinSession(id, sessionName, toolKey = null) {
@@ -892,6 +927,7 @@
892
927
  currentSocket.onmessage = (e) => {
893
928
  const msg = JSON.parse(e.data);
894
929
  if (msg.type === 'output') term.write(msg.data);
930
+ if (msg.type === 'reset') term.reset();
895
931
  if (msg.type === 'exit') showLobby();
896
932
  };
897
933
  }
@@ -1144,7 +1180,7 @@
1144
1180
  setTimeout(() => { sendWS('\r'); }, 1000);
1145
1181
  return;
1146
1182
  }
1147
- 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' };
1148
1184
  if (sequences[key]) sendWS(sequences[key]);
1149
1185
  });
1150
1186
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glad-web",
3
- "version": "1.0.13",
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": {