agentgui 1.0.975 → 1.0.976

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.975",
3
+ "version": "1.0.976",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -131,7 +131,8 @@ function scheduleStreamRender() {
131
131
  requestAnimationFrame(() => {
132
132
  streamRenderScheduled = false;
133
133
  render();
134
- scrollChatToBottom();
134
+ // Kit AgentChat's IntersectionObserver sentinel handles streaming auto-scroll;
135
+ // calling scrollChatToBottom here forces a synchronous scrollHeight layout reflow.
135
136
  });
136
137
  }
137
138
 
@@ -262,6 +263,7 @@ function navTo(tab, { writeHash: doWriteHash = true, push = true } = {}) {
262
263
  // Leaving chat clears any pending new-chat confirmation so it doesn't linger
263
264
  // as a stale banner when the user returns.
264
265
  if (prev === 'chat' && tab !== 'chat') state.confirmingNewChat = false;
266
+ if (prev !== tab) state.confirmingClearData = false;
265
267
  state.tab = tab;
266
268
  // Live history SSE feeds both the History tab (event log) and the Live
267
269
  // dashboard (per-session activity tally + stream-health signal); open it on
@@ -367,7 +369,11 @@ let _liveTick = null;
367
369
  function startLiveTick() {
368
370
  if (_liveTick) return;
369
371
  _liveTick = setInterval(() => {
370
- if (state.tab === 'live' && Array.isArray(state.active) && state.active.length) scheduleRender();
372
+ if (state.tab === 'live' && Array.isArray(state.active) && state.active.length) {
373
+ scheduleRender();
374
+ } else if (state.tab !== 'live' && !(Array.isArray(state.active) && state.active.length)) {
375
+ clearInterval(_liveTick); _liveTick = null;
376
+ }
371
377
  }, 1000);
372
378
  }
373
379
  async function stopActiveChat(sid) {
@@ -512,9 +518,9 @@ function view() {
512
518
  const liveActive = state.tab === 'history' && state.live.connected && (Date.now() - state.live.lastEventTs < 30000);
513
519
  const dotLabel = state.tab === 'history'
514
520
  ? (state.live.error
515
- ? state.live.error + (state.live.reconnects ? ' · ' + state.live.reconnects + ' reconnects' : '')
516
- : (liveActive ? 'live · ' + state.live.eventCount : (state.live.connected ? 'live' : 'connecting…')))
517
- : (ok ? (state.health.ws === 'reconnecting' ? 'connecting' : 'connected') : 'offline');
521
+ ? 'stream: ' + state.live.error + (state.live.reconnects ? ' · ' + state.live.reconnects + ' reconnects' : '')
522
+ : (liveActive ? 'stream: live · ' + state.live.eventCount : (state.live.connected ? 'stream: live' : 'stream: connecting…')))
523
+ : (ok ? (state.health.ws === 'reconnecting' ? 'connecting' : 'connected') : 'offline');
518
524
  const dotLive = state.tab === 'history' ? (liveActive || state.live.connected) : ok;
519
525
  // The status dot is drawn entirely by CSS (.status-dot::before) - a small
520
526
  // colored disc, real product design, not a text glyph. State drives its colour
@@ -1050,7 +1056,7 @@ function fileDialog() {
1050
1056
  return PromptDialog({
1051
1057
  title: 'Rename ' + d.file.name, value: d.file.name, placeholder: 'new name',
1052
1058
  error: d.error || null, busy: !!d.busy,
1053
- confirmLabel: d.busy ? 'renaming...' : 'rename', cancelLabel: 'cancel',
1059
+ confirmLabel: d.busy ? 'renaming' : 'rename', cancelLabel: 'cancel',
1054
1060
  onCancel: closeFileDialog,
1055
1061
  onConfirm: (v) => {
1056
1062
  // Every confirm press produces visible feedback - never a silent no-op.
@@ -1067,7 +1073,7 @@ function fileDialog() {
1067
1073
  ? 'Delete this folder and everything inside it? This cannot be undone.'
1068
1074
  : 'Delete this file? This cannot be undone.',
1069
1075
  error: d.error || null, busy: !!d.busy,
1070
- confirmLabel: d.busy ? 'deleting...' : 'delete', cancelLabel: 'cancel', destructive: true,
1076
+ confirmLabel: d.busy ? 'deleting' : 'delete', cancelLabel: 'cancel', destructive: true,
1071
1077
  onCancel: closeFileDialog,
1072
1078
  onConfirm: () => runFileMutation(() => B.deleteEntry(state.backend, d.file.path, isDir), 'deleted ' + d.file.name),
1073
1079
  });
@@ -1081,7 +1087,7 @@ function fileDialog() {
1081
1087
  ? 'Folders are deleted with everything inside them. '
1082
1088
  : '') + 'This cannot be undone.',
1083
1089
  error: d.error || null, busy: !!d.busy,
1084
- confirmLabel: d.busy ? 'deleting...' : 'delete ' + n, cancelLabel: 'cancel', destructive: true,
1090
+ confirmLabel: d.busy ? 'deleting' : 'delete ' + n, cancelLabel: 'cancel', destructive: true,
1085
1091
  onCancel: closeFileDialog,
1086
1092
  onConfirm: runBulkDelete,
1087
1093
  });
@@ -1092,7 +1098,7 @@ function fileDialog() {
1092
1098
  title: 'Move ' + n + ' selected ' + (n === 1 ? 'entry' : 'entries'),
1093
1099
  value: state.files.path || '', placeholder: 'destination folder path',
1094
1100
  error: d.error || null, busy: !!d.busy,
1095
- confirmLabel: d.busy ? 'moving...' : 'move ' + n, cancelLabel: 'cancel',
1101
+ confirmLabel: d.busy ? 'moving' : 'move ' + n, cancelLabel: 'cancel',
1096
1102
  onCancel: closeFileDialog,
1097
1103
  onConfirm: (v) => {
1098
1104
  if (!v) { d.error = 'enter a destination folder'; render(); return; }
@@ -1105,7 +1111,7 @@ function fileDialog() {
1105
1111
  return PromptDialog({
1106
1112
  title: 'New folder', value: '', placeholder: 'folder name',
1107
1113
  error: d.error || null, busy: !!d.busy,
1108
- confirmLabel: d.busy ? 'creating...' : 'create', cancelLabel: 'cancel',
1114
+ confirmLabel: d.busy ? 'creating' : 'create', cancelLabel: 'cancel',
1109
1115
  onCancel: closeFileDialog,
1110
1116
  onConfirm: (v) => {
1111
1117
  if (!v) { d.error = 'enter a folder name'; render(); return; }
@@ -2495,7 +2501,7 @@ function historyMain() {
2495
2501
  const meta = SessionMeta({
2496
2502
  items: [
2497
2503
  sess && sess.cwd ? { label: 'cwd', value: sess.cwd, title: sess.cwd } : null,
2498
- sessionDuration() ? { label: 'duration', value: sessionDuration() } : null,
2504
+ (() => { const dur = sessionDuration(); return dur ? { label: 'duration', value: dur } : null; })(),
2499
2505
  { label: 'session id', value: state.selectedSid.slice(0, 8) + '…', title: state.selectedSid, onCopy: () => copyText(state.selectedSid, 'session id copied') },
2500
2506
  // Spelled counter vocabulary in the detail strip (events/turns/tools/
2501
2507
  // errors); the abbreviated 'ev/tools/err' triple stays compact-row-only.
@@ -2571,7 +2577,7 @@ function historyMain() {
2571
2577
  }
2572
2578
  return {
2573
2579
  key,
2574
- code: String(absIdx + 1).padStart(4, '0'),
2580
+ code: String(total - shown.length + i + 1).padStart(4, '0'),
2575
2581
  rail,
2576
2582
  expanded, // disclosure state -> kit Row sets aria-expanded
2577
2583
  highlight: q || undefined,
@@ -2811,7 +2817,9 @@ function historySide() {
2811
2817
  function isValidUrl(s) {
2812
2818
  if (!s) return true; // blank = same-origin is valid
2813
2819
  try {
2814
- const u = new URL(s.startsWith('http') ? s : 'http://' + s);
2820
+ // Only add http:// prefix for schemeless inputs (no ://); inputs with an
2821
+ // explicit non-http scheme (ftp://, ws://) must fail the protocol check.
2822
+ const u = new URL(s.includes('://') ? s : 'http://' + s);
2815
2823
  return u.protocol === 'http:' || u.protocol === 'https:'; // reject ftp:/ws:/etc
2816
2824
  } catch { return false; }
2817
2825
  }
@@ -2821,12 +2829,15 @@ function isValidUrl(s) {
2821
2829
  // becomes the same string we validated. Blank stays blank (same-origin).
2822
2830
  function normalizeBackend(s) {
2823
2831
  if (!s) return '';
2824
- try { return new URL(s.startsWith('http') ? s : 'http://' + s).origin; }
2832
+ try { return new URL(s.includes('://') ? s : 'http://' + s).origin; }
2825
2833
  catch { return s; }
2826
2834
  }
2827
2835
 
2828
2836
  async function saveBackend() {
2829
- if (!isValidUrl(state.backendDraft) || state.backendDraft === state.backend) return;
2837
+ if (!isValidUrl(state.backendDraft)) return;
2838
+ // Re-submitting the current URL (e.g. after a failed health check) re-runs
2839
+ // the health probe and shows connecting… so the user gets visible feedback.
2840
+ if (state.backendDraft === state.backend) { state.backendStatus = 'connecting'; render(); await recheckHealth(); return; }
2830
2841
  // Switching backend orphans the local chat transcript (it belongs to the old
2831
2842
  // server's sessions). Confirm once if there's a transcript to lose - and the
2832
2843
  // confirmation binds to the EXACT value confirmed: editing the URL after
@@ -2856,7 +2867,7 @@ function healthSummary() {
2856
2867
  // (connected/offline/connecting) so the same state reads the same word
2857
2868
  // everywhere, instead of the raw health.status ('ok'/'down').
2858
2869
  const connWord = hh.status === 'ok' ? 'connected'
2859
- : hh.status === 'unknown' ? 'connecting'
2870
+ : hh.status === 'unknown' ? 'connecting'
2860
2871
  : 'offline';
2861
2872
  bits.push([connWord, 'Backend connection status']);
2862
2873
  if (hh.version) bits.push(['v' + hh.version, 'Server version']);
@@ -2918,7 +2929,7 @@ function settingsMain() {
2918
2929
  key: 'savebtn',
2919
2930
  type: 'submit',
2920
2931
  primary: true,
2921
- disabled: !isValid || state.backendDraft === state.backend || state.backendStatus === 'connecting',
2932
+ disabled: !isValid || state.backendStatus === 'connecting',
2922
2933
  onClick: (e) => { e.preventDefault(); saveBackend(); },
2923
2934
  children: state.backendStatus === 'connecting' ? 'connecting…' : 'save + reconnect',
2924
2935
  title: isValid ? 'Save backend URL and reconnect' : 'Fix URL format first',
@@ -3066,7 +3077,7 @@ function agentsPanel() {
3066
3077
  // Rail tone keeps its GUI-wide meaning: green=ok/selected,
3067
3078
  // flame=error/unavailable. Selection is shown via `active`, not by
3068
3079
  // borrowing purple (purple is reserved for subagents).
3069
- rail: (a.id === state.selectedAgent || avail) ? 'green' : 'flame',
3080
+ rail: !avail ? 'flame' : (a.id === state.selectedAgent ? 'green' : undefined),
3070
3081
  active: a.id === state.selectedAgent,
3071
3082
  // Non-installable agents are genuinely inert: mark them disabled (no
3072
3083
  // click, no button role) instead of looking clickable but doing nothing.
@@ -377,6 +377,7 @@ export async function* streamChat(base, { model, messages, signal, agentId, resu
377
377
  } else if (ev.type === 'streaming_progress') {
378
378
  const block = ev.block;
379
379
  if (block?.type === 'text' && block.text) push({ type: 'text', text: block.text });
380
+ else if (block?.type === 'thinking' && block.thinking) push({ type: 'thinking', text: block.thinking });
380
381
  else if (block?.type === 'tool_use') push({ type: 'tool', block });
381
382
  else if (block?.type === 'tool_result') push({ type: 'tool_result', block });
382
383
  else if (block?.type === 'result') push({ type: 'result', block });
@@ -498,12 +498,12 @@
498
498
  .ds-247420 ::selection { background: color-mix(in oklab, var(--accent) 24%, var(--bg-2)); color: var(--fg); }
499
499
 
500
500
  /* Every root has a CQ container so fluid type can resolve to a meaningful inline-size. */
501
- .ds-247420 .app, .ds-247420[class*="kit-"], .ds-247420 .ds-stage {
501
+ .ds-247420 .app, .ds-247420 .ds-stage {
502
502
  container-type: inline-size;
503
503
  overflow-x: clip;
504
504
  min-width: 0;
505
505
  }
506
- .ds-247420 .app *, .ds-247420[class*="kit-"] * { min-width: 0; }
506
+ .ds-247420 .app * { min-width: 0; }
507
507
 
508
508
  /* ============================================================
509
509
  Typography