glad-web 1.0.37 → 1.0.39

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
@@ -486,15 +486,47 @@
486
486
  }
487
487
 
488
488
  function inlineMarkdown(text) {
489
+ const protectedSegments = [];
490
+ const protect = value => `\uE000${protectedSegments.push(value) - 1}\uE001`;
491
+ const restore = value => value.replace(/\uE000(\d+)\uE001/g, (_match, index) => protectedSegments[Number(index)] || '');
492
+ const formatText = value => {
493
+ const codeSegments = [];
494
+ const protectCode = code => `\uE002${codeSegments.push(code) - 1}\uE003`;
495
+ const restoreCode = formatted => formatted.replace(/\uE002(\d+)\uE003/g, (_match, index) => codeSegments[Number(index)] || '');
496
+ let formatted = value.replace(/`([^`]+)`/g, (_match, code) => protectCode(`<code>${code}</code>`));
497
+ formatted = formatted.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
498
+ formatted = formatted.replace(/(^|[^\p{L}\p{N}_])__([^_\n]+)__(?![\p{L}\p{N}_])/gu, '$1<strong>$2</strong>');
499
+ formatted = formatted.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
500
+ formatted = formatted.replace(/(^|[^\p{L}\p{N}_])_([^_\n]+)_(?![\p{L}\p{N}_])/gu, '$1<em>$2</em>');
501
+ return restoreCode(formatted);
502
+ };
503
+
489
504
  let html = escapeHtml(text || '');
490
- html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
491
- html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
492
- html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
493
- html = html.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
494
- html = html.replace(/_([^_\n]+)_/g, '<em>$1</em>');
495
- html = html.replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/g, '<img src="$2" alt="$1">');
496
- html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
497
- return html;
505
+ html = html.replace(/`([^`]+)`|!\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)|\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g,
506
+ (_match, code, alt, imageUrl, label, linkUrl) => {
507
+ if (code !== undefined) return protect(`<code>${code}</code>`);
508
+ if (imageUrl !== undefined) return protect(`<img src="${imageUrl}" alt="${alt}">`);
509
+ return protect(`<a href="${linkUrl}" target="_blank" rel="noopener noreferrer">${formatText(label)}</a>`);
510
+ });
511
+ return restore(formatText(html));
512
+ }
513
+
514
+ function parseMarkdownFenceOpener(line) {
515
+ const match = String(line || '').match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
516
+ if (!match) return null;
517
+ const marker = match[1];
518
+ const info = match[2].trim();
519
+ if (marker[0] === '`' && info.includes('`')) return null;
520
+ return {
521
+ character: marker[0],
522
+ length: marker.length,
523
+ language: info.split(/\s+/, 1)[0] || ''
524
+ };
525
+ }
526
+
527
+ function isMarkdownFenceCloser(line, opener) {
528
+ const match = String(line || '').match(/^\s{0,3}(`+|~+)\s*$/);
529
+ return Boolean(match && match[1][0] === opener.character && match[1].length >= opener.length);
498
530
  }
499
531
 
500
532
  function splitMarkdownBlocks(markdown) {
@@ -506,17 +538,16 @@
506
538
  i++;
507
539
  continue;
508
540
  }
509
- const fence = lines[i].match(/^```(\w+)?\s*$/);
541
+ const fence = parseMarkdownFenceOpener(lines[i]);
510
542
  if (fence) {
511
- const language = fence[1] || '';
512
543
  const content = [];
513
544
  i++;
514
- while (i < lines.length && !/^```\s*$/.test(lines[i])) {
545
+ while (i < lines.length && !isMarkdownFenceCloser(lines[i], fence)) {
515
546
  content.push(lines[i]);
516
547
  i++;
517
548
  }
518
549
  if (i < lines.length) i++;
519
- blocks.push({ type: 'code', language, content: content.join('\n') });
550
+ blocks.push({ type: 'code', language: fence.language, content: content.join('\n') });
520
551
  continue;
521
552
  }
522
553
  if (/^\s*[-*_]{3,}\s*$/.test(lines[i])) {
@@ -541,13 +572,15 @@
541
572
  continue;
542
573
  }
543
574
  if (/^\s*([-*+])\s+/.test(lines[i]) || /^\s*\d+\.\s+/.test(lines[i])) {
544
- const ordered = /^\s*\d+\.\s+/.test(lines[i]);
575
+ const orderedMatch = lines[i].match(/^\s*(\d+)\.\s+/);
576
+ const ordered = Boolean(orderedMatch);
577
+ const start = ordered ? Number(orderedMatch[1]) : null;
545
578
  const items = [];
546
579
  while (i < lines.length && (ordered ? /^\s*\d+\.\s+/.test(lines[i]) : /^\s*[-*+]\s+/.test(lines[i]))) {
547
580
  items.push(lines[i].replace(ordered ? /^\s*\d+\.\s+/ : /^\s*[-*+]\s+/, ''));
548
581
  i++;
549
582
  }
550
- blocks.push({ type: ordered ? 'ol' : 'ul', items });
583
+ blocks.push({ type: ordered ? 'ol' : 'ul', items, ...(ordered ? { start } : {}) });
551
584
  continue;
552
585
  }
553
586
  if (/^\s*>\s?/.test(lines[i])) {
@@ -561,7 +594,12 @@
561
594
  }
562
595
  const paragraph = [];
563
596
  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;
597
+ 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;
598
+ paragraph.push(lines[i]);
599
+ i++;
600
+ }
601
+ // Always make progress even if a future block detector and parser disagree.
602
+ if (!paragraph.length) {
565
603
  paragraph.push(lines[i]);
566
604
  i++;
567
605
  }
@@ -580,7 +618,7 @@
580
618
  if (block.type === 'hr') return '<hr>';
581
619
  if (block.type === 'header') return `<h${block.level}>${inlineMarkdown(block.text)}</h${block.level}>`;
582
620
  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>`;
621
+ if (block.type === 'ol') return `<ol${block.start !== 1 ? ` start="${block.start}"` : ''}>${block.items.map(item => `<li>${inlineMarkdown(item)}</li>`).join('')}</ol>`;
584
622
  if (block.type === 'quote') return `<blockquote>${renderMarkdown(block.text)}</blockquote>`;
585
623
  if (block.type === 'table') return renderMarkdownTable(block.rows);
586
624
  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.39",
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"