glad-web 1.0.37 → 1.0.38

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.
@@ -183,6 +183,12 @@ function toolDetails(raw) {
183
183
  };
184
184
  }
185
185
 
186
+ function codexReconnectProgress(message) {
187
+ const match = String(message || '').match(/\bReconnecting(?:\.\.\.)?\s*(\d+)\s*\/\s*(\d+)\b/i);
188
+ if (!match) return null;
189
+ return { attempt: Number(match[1]), maximum: Number(match[2]) };
190
+ }
191
+
186
192
  class CodexStructuredSession extends EventEmitter {
187
193
  constructor({ id, tool, workingDir, name, logger, options = {} }) {
188
194
  super();
@@ -203,6 +209,7 @@ class CodexStructuredSession extends EventEmitter {
203
209
  this.threadId = options.resume || null;
204
210
  this.currentTurnId = null;
205
211
  this.currentTurnStartedAt = null;
212
+ this.reconnectAbortTurnId = null;
206
213
  this.threadTurns = new Map();
207
214
  this.turnContexts = new Map();
208
215
  this.providerItemContexts = new Map();
@@ -575,7 +582,15 @@ class CodexStructuredSession extends EventEmitter {
575
582
  return;
576
583
  }
577
584
  if (method === 'error') {
578
- this.append({ kind: 'event', level: 'error', text: params.error?.message || 'Codex reported an error.' });
585
+ const message = params.error?.message || 'Codex reported an error.';
586
+ this.append({ kind: 'event', level: 'error', text: message });
587
+ const reconnect = codexReconnectProgress(message);
588
+ const turnId = String(params.turnId || this.currentTurnId || 'active');
589
+ if (params.willRetry && reconnect?.attempt === 4 && reconnect.maximum === 5
590
+ && this.reconnectAbortTurnId !== turnId && this.abort('Aborted after Codex reconnect attempt 4/5.')) {
591
+ this.reconnectAbortTurnId = turnId;
592
+ this.emitEvent({ type: 'runtime-disconnected', activeTurn: true, turnId });
593
+ }
579
594
  if (!params.willRetry) { this.compacting = false; this.setStatus('idle'); }
580
595
  return;
581
596
  }
package/lib/web/claude.js CHANGED
@@ -497,6 +497,24 @@
497
497
  return html;
498
498
  }
499
499
 
500
+ function parseMarkdownFenceOpener(line) {
501
+ const match = String(line || '').match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
502
+ if (!match) return null;
503
+ const marker = match[1];
504
+ const info = match[2].trim();
505
+ if (marker[0] === '`' && info.includes('`')) return null;
506
+ return {
507
+ character: marker[0],
508
+ length: marker.length,
509
+ language: info.split(/\s+/, 1)[0] || ''
510
+ };
511
+ }
512
+
513
+ function isMarkdownFenceCloser(line, opener) {
514
+ const match = String(line || '').match(/^\s{0,3}(`+|~+)\s*$/);
515
+ return Boolean(match && match[1][0] === opener.character && match[1].length >= opener.length);
516
+ }
517
+
500
518
  function splitMarkdownBlocks(markdown) {
501
519
  const lines = String(markdown || '').replace(/\r\n/g, '\n').split('\n');
502
520
  const blocks = [];
@@ -506,17 +524,16 @@
506
524
  i++;
507
525
  continue;
508
526
  }
509
- const fence = lines[i].match(/^```(\w+)?\s*$/);
527
+ const fence = parseMarkdownFenceOpener(lines[i]);
510
528
  if (fence) {
511
- const language = fence[1] || '';
512
529
  const content = [];
513
530
  i++;
514
- while (i < lines.length && !/^```\s*$/.test(lines[i])) {
531
+ while (i < lines.length && !isMarkdownFenceCloser(lines[i], fence)) {
515
532
  content.push(lines[i]);
516
533
  i++;
517
534
  }
518
535
  if (i < lines.length) i++;
519
- blocks.push({ type: 'code', language, content: content.join('\n') });
536
+ blocks.push({ type: 'code', language: fence.language, content: content.join('\n') });
520
537
  continue;
521
538
  }
522
539
  if (/^\s*[-*_]{3,}\s*$/.test(lines[i])) {
@@ -541,13 +558,15 @@
541
558
  continue;
542
559
  }
543
560
  if (/^\s*([-*+])\s+/.test(lines[i]) || /^\s*\d+\.\s+/.test(lines[i])) {
544
- const ordered = /^\s*\d+\.\s+/.test(lines[i]);
561
+ const orderedMatch = lines[i].match(/^\s*(\d+)\.\s+/);
562
+ const ordered = Boolean(orderedMatch);
563
+ const start = ordered ? Number(orderedMatch[1]) : null;
545
564
  const items = [];
546
565
  while (i < lines.length && (ordered ? /^\s*\d+\.\s+/.test(lines[i]) : /^\s*[-*+]\s+/.test(lines[i]))) {
547
566
  items.push(lines[i].replace(ordered ? /^\s*\d+\.\s+/ : /^\s*[-*+]\s+/, ''));
548
567
  i++;
549
568
  }
550
- blocks.push({ type: ordered ? 'ol' : 'ul', items });
569
+ blocks.push({ type: ordered ? 'ol' : 'ul', items, ...(ordered ? { start } : {}) });
551
570
  continue;
552
571
  }
553
572
  if (/^\s*>\s?/.test(lines[i])) {
@@ -561,7 +580,12 @@
561
580
  }
562
581
  const paragraph = [];
563
582
  while (i < lines.length && lines[i].trim()) {
564
- if (/^```/.test(lines[i]) || /^\s{0,3}#{1,4}\s+/.test(lines[i]) || /^\s*([-*+])\s+/.test(lines[i]) || /^\s*\d+\.\s+/.test(lines[i])) break;
583
+ if (parseMarkdownFenceOpener(lines[i]) || /^\s{0,3}#{1,4}\s+/.test(lines[i]) || /^\s*([-*+])\s+/.test(lines[i]) || /^\s*\d+\.\s+/.test(lines[i])) break;
584
+ paragraph.push(lines[i]);
585
+ i++;
586
+ }
587
+ // Always make progress even if a future block detector and parser disagree.
588
+ if (!paragraph.length) {
565
589
  paragraph.push(lines[i]);
566
590
  i++;
567
591
  }
@@ -580,7 +604,7 @@
580
604
  if (block.type === 'hr') return '<hr>';
581
605
  if (block.type === 'header') return `<h${block.level}>${inlineMarkdown(block.text)}</h${block.level}>`;
582
606
  if (block.type === 'ul') return `<ul>${block.items.map(item => `<li>${inlineMarkdown(item)}</li>`).join('')}</ul>`;
583
- if (block.type === 'ol') return `<ol>${block.items.map(item => `<li>${inlineMarkdown(item)}</li>`).join('')}</ol>`;
607
+ if (block.type === 'ol') return `<ol${block.start !== 1 ? ` start="${block.start}"` : ''}>${block.items.map(item => `<li>${inlineMarkdown(item)}</li>`).join('')}</ol>`;
584
608
  if (block.type === 'quote') return `<blockquote>${renderMarkdown(block.text)}</blockquote>`;
585
609
  if (block.type === 'table') return renderMarkdownTable(block.rows);
586
610
  return `<p>${inlineMarkdown(block.text).replace(/\n/g, '<br>')}</p>`;
package/lib/web/core.js CHANGED
@@ -229,18 +229,18 @@
229
229
  <h3>${escapeHtml(s.name)}${s.hasUnreadCompletion ? '<span class="completion-dot" title="Completed"></span>' : ''}${timerBadge} <button class="icon-btn" onclick="renameSession('${s.id}', decodePathValue('${encodedName}'), event)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></button></h3>
230
230
  <p>${escapeHtml(s.tool)}</p>
231
231
  <p>${new Date(s.startTime).toLocaleTimeString()}</p>
232
- <div class="session-dir-row">
233
- <button class="copy-dir-btn" title="Copy directory" onclick="copySessionDirectory(decodePathValue('${encodedDir}'), event)">
234
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
235
- </button>
236
- <p title="${escapeHtml(workingDirectory)}">${escapeHtml(workingDirectory)}</p>
237
- </div>
238
232
  </div>
239
233
  <div class="session-actions">
240
234
  ${renderServerChanSessionAction(s)}
241
235
  <button class="btn-join" onclick="joinSession('${s.id}', decodePathValue('${encodedName}'), '${escapeHtml(s.toolKey || '')}')">Connect</button>
242
236
  <button class="icon-btn btn-delete" onclick="deleteSession('${s.id}', event)"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg></button>
243
237
  </div>
238
+ <div class="session-dir-row">
239
+ <button class="copy-dir-btn" title="Copy directory" onclick="copySessionDirectory(decodePathValue('${encodedDir}'), event)">
240
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
241
+ </button>
242
+ <p title="${escapeHtml(workingDirectory)}">${escapeHtml(workingDirectory)}</p>
243
+ </div>
244
244
  </div>`;
245
245
  });
246
246
  list.innerHTML = html;
@@ -11,18 +11,18 @@
11
11
  .header-action-btn.icon-only { width: 36px; padding: 0; }
12
12
  .header-action-btn:active { background: #0062cc; transform: scale(.97); }
13
13
  .btn-retry { background: #333; color: #fff; border: none; padding: 8px 16px; border-radius: 20px; margin-top: 10px; cursor: pointer; }
14
- .session-card { background: var(--card-bg); border-radius: 12px; padding: 16px; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; transition: transform 0.1s; position: relative; }
14
+ .session-card { background: var(--card-bg); border-radius: 12px; padding: 12px 16px 8px; margin-bottom: 12px; display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 10px; row-gap: 0; align-items: center; transition: transform 0.1s; position: relative; }
15
15
  .session-card:active { transform: scale(0.98); }
16
16
  .completion-dot { width: 9px; height: 9px; border-radius: 50%; background: #ff3b30; flex-shrink: 0; }
17
17
  .session-info { flex: 1; min-width: 0; }
18
18
  .session-info h3 { margin: 0 0 4px 0; font-size: 17px; display: flex; align-items: center; gap: 8px; }
19
19
  .session-info p { margin: 0; font-size: 13px; color: var(--text-dim); }
20
20
  .timer-count-badge { display: inline-flex; align-items: center; gap: 3px; color: #fff; background: rgba(0,122,255,0.22); border: 1px solid rgba(0,122,255,0.34); border-radius: 999px; padding: 2px 7px; font-size: 11px; font-weight: 800; flex-shrink: 0; }
21
- .session-dir-row { display: flex; align-items: center; gap: 6px; margin-top: 2px; }
21
+ .session-dir-row { grid-column: 1 / -1; display: flex; align-items: center; gap: 6px; width: 100%; min-width: 0; }
22
22
  .session-dir-row p { flex: 1; min-width: 0; font-family: monospace; overflow-wrap: anywhere; }
23
23
  .copy-dir-btn { color: var(--text-dim); background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; width: 28px; height: 28px; padding: 0; display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; }
24
24
  .copy-dir-btn:active { color: #fff; background: rgba(255,255,255,0.12); }
25
- .session-actions { display: flex; gap: 12px; align-items: center; margin-left: 10px; }
25
+ .session-actions { display: flex; gap: 12px; align-items: center; }
26
26
  .serverchan-toggle { width: 34px; height: 34px; padding: 0; border: 1px solid rgba(255,255,255,0.1); border-radius: 50%; background: rgba(255,255,255,0.05); color: var(--text-dim); cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
27
27
  .serverchan-toggle.active { color: #34c759; background: rgba(52,199,89,0.13); }
28
28
  .serverchan-toggle:active { color: #fff; background: rgba(255,255,255,0.12); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glad-web",
3
- "version": "1.0.37",
3
+ "version": "1.0.38",
4
4
  "description": "Glad transforms terminal-based AI coding tools into a polished, mobile-friendly local Web interface.",
5
5
  "bin": {
6
6
  "glad": "bin/cli.js"