@pingagent/sdk 0.1.14 → 0.1.16

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/bin/pingagent.js CHANGED
@@ -25,12 +25,24 @@ import {
25
25
  TaskHandoffManager,
26
26
  TrustPolicyAuditManager,
27
27
  CollaborationEventManager,
28
+ CollaborationProjectionOutboxManager,
29
+ NotificationIntentManager,
30
+ OperatorSeenStateManager,
28
31
  TrustRecommendationManager,
29
32
  getTrustRecommendationActionLabel,
30
33
  formatCapabilityCardSummary,
31
34
  defaultTrustPolicyDoc,
32
35
  normalizeTrustPolicyDoc,
33
36
  upsertTrustPolicyRecommendation,
37
+ summarizeSinceLastSeen,
38
+ buildDeliveryTimeline,
39
+ buildProjectionPreview,
40
+ listPendingDecisionViews,
41
+ summarizeHumanDelivery,
42
+ listRecentBindingsForSession,
43
+ deriveTransportHealth,
44
+ readTransportPreference,
45
+ switchTransportPreference,
34
46
  getActiveSessionFilePath,
35
47
  getSessionMapFilePath,
36
48
  getSessionBindingAlertsFilePath,
@@ -243,7 +255,16 @@ function formatSessionRow(session, selected) {
243
255
  const trust = session.trust_state || 'unknown';
244
256
  const unread = session.unread_count ?? 0;
245
257
  const who = truncateLine(session.remote_did || session.conversation_id || 'unknown', 40);
246
- return `${marker} ${who} [${trust}] unread=${unread}${reconnect}`;
258
+ const seen = session.since_last_seen || {};
259
+ const sinceCount = (seen.new_external_messages ?? 0)
260
+ + (seen.new_conclusions ?? 0)
261
+ + (seen.new_handoffs ?? 0)
262
+ + (seen.new_decisions ?? 0)
263
+ + (seen.new_failures ?? 0)
264
+ + (seen.new_repairs ?? 0)
265
+ + (seen.new_projection_failures ?? 0);
266
+ const sinceBadge = sinceCount > 0 ? ` new=${sinceCount}` : '';
267
+ return `${marker} ${who} [${trust}] unread=${unread}${sinceBadge}${reconnect}`;
247
268
  }
248
269
 
249
270
  function formatMessageRow(message) {
@@ -346,6 +367,21 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
346
367
  const alertByConversation = new Map(alerts.map((row) => [row.conversation_id, row]));
347
368
  const activeChatSession = readCurrentActiveSessionKey();
348
369
  const ingressRuntime = readIngressRuntimeStatus();
370
+ const transportPreference = readTransportPreference();
371
+ const transportHealth = deriveTransportHealth({
372
+ runtime_status: ingressRuntime
373
+ ? {
374
+ ...ingressRuntime,
375
+ preferred_transport_mode: transportPreference?.preferred_mode ?? ingressRuntime.preferred_transport_mode ?? 'bridge',
376
+ }
377
+ : {
378
+ receive_mode: 'webhook',
379
+ transport_mode: transportPreference?.preferred_mode ?? 'bridge',
380
+ preferred_transport_mode: transportPreference?.preferred_mode ?? 'bridge',
381
+ },
382
+ recent_events: collaborationEventManager.listRecent(30),
383
+ projection_outbox_failed: new CollaborationProjectionOutboxManager(store).listByStatus('failed', 20),
384
+ });
349
385
  const desiredSelectedSessionKey =
350
386
  (selectedSessionKey && sessions.some((session) => session.session_key === selectedSessionKey) ? selectedSessionKey : null) ??
351
387
  sessionManager.getActiveSession()?.session_key ??
@@ -376,9 +412,9 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
376
412
  ? collaborationEventManager.listBySession(selectedSession.session_key, 12)
377
413
  : [];
378
414
  const selectedPendingCollaborationEvents = selectedSession
379
- ? collaborationEventManager.listPendingBySession(selectedSession.session_key, 12)
415
+ ? listPendingDecisionViews(store, 100).filter((event) => event.session_key === selectedSession.session_key).slice(0, 12)
380
416
  : [];
381
- const pendingCollaborationEventsGlobal = collaborationEventManager.listPending(50);
417
+ const pendingCollaborationEventsGlobal = listPendingDecisionViews(store, 50);
382
418
  const selectedMessages = selectedSession?.conversation_id
383
419
  ? historyManager.listRecent(selectedSession.conversation_id, 12)
384
420
  : [];
@@ -402,18 +438,47 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
402
438
  : [];
403
439
  const unreadTotal = sessionsWithMeta.reduce((sum, session) => sum + (session.unread_count ?? 0), 0);
404
440
  const alertSessions = sessionsWithMeta.filter((session) => !!session.binding_alert).length;
441
+ const sinceLastSeenGlobal = summarizeSinceLastSeen(store, {
442
+ operator_id: 'tui',
443
+ scope_type: 'global',
444
+ });
445
+ const sessionsWithSeen = sessionsWithMeta.map((session) => ({
446
+ ...session,
447
+ since_last_seen: summarizeSinceLastSeen(store, {
448
+ operator_id: 'tui',
449
+ scope_type: 'session',
450
+ scope_key: session.session_key,
451
+ }),
452
+ }));
453
+ const selectedDeliveryTimeline = selectedSession
454
+ ? buildDeliveryTimeline(store, selectedSession.session_key, 30)
455
+ : [];
456
+ const selectedProjectionPreview = selectedSession
457
+ ? buildProjectionPreview(store, selectedSession.session_key, policy.doc.collaboration_projection?.preset || 'balanced', 5)
458
+ : null;
459
+ const humanDelivery = summarizeHumanDelivery(store, 20);
460
+ const selectedRecentBindings = selectedSession
461
+ ? listRecentBindingsForSession(store, selectedSession.session_key, selectedSession.conversation_id, 12)
462
+ : [];
463
+ const selectedRecentNotificationIntents = selectedSession
464
+ ? new NotificationIntentManager(store).listBySession(selectedSession.session_key, 12)
465
+ : [];
405
466
  return {
406
467
  identity,
407
468
  runtimeMode,
408
469
  activeChatSession,
409
470
  ingressRuntime,
471
+ transportPreference,
472
+ transportHealth,
410
473
  activeChatSessionFile: getActiveSessionFilePath(),
411
474
  sessionMapPath: getSessionMapFilePath(),
412
475
  sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
413
476
  policyPath: policy.path,
414
477
  policyDoc: policy.doc,
415
478
  projectionPreset: policy.doc.collaboration_projection?.preset || 'balanced',
416
- sessions: sessionsWithMeta,
479
+ humanDelivery,
480
+ sinceLastSeenGlobal,
481
+ sessions: sessionsWithSeen,
417
482
  tasks: taskManager.listRecent(30).map((task) => ({
418
483
  ...task,
419
484
  handoff: taskHandoffManager.get(task.task_id),
@@ -426,6 +491,10 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
426
491
  selectedAuditEvents,
427
492
  selectedCollaborationEvents,
428
493
  selectedPendingCollaborationEvents,
494
+ selectedDeliveryTimeline,
495
+ selectedProjectionPreview,
496
+ selectedRecentBindings,
497
+ selectedRecentNotificationIntents,
429
498
  pendingCollaborationEventsGlobal,
430
499
  selectedMessages,
431
500
  selectedHistoryPage,
@@ -450,6 +519,10 @@ function renderHostTuiScreen(hostState, uiState) {
450
519
  const pendingCollaborationEvents = hostState.selectedPendingCollaborationEvents || [];
451
520
  const pendingCollaborationEventsGlobal = hostState.pendingCollaborationEventsGlobal || [];
452
521
  const messages = hostState.selectedMessages || [];
522
+ const deliveryTimeline = hostState.selectedDeliveryTimeline || [];
523
+ const projectionPreview = hostState.selectedProjectionPreview || null;
524
+ const recentBindings = hostState.selectedRecentBindings || [];
525
+ const recentNotificationIntents = hostState.selectedRecentNotificationIntents || [];
453
526
  const recommendations = hostState.selectedRecommendations || [];
454
527
  const openRecommendation = recommendations.find((item) => item.status === 'open') || null;
455
528
  const reopenRecommendation = recommendations.find((item) => item.status === 'dismissed' || item.status === 'superseded') || null;
@@ -469,14 +542,33 @@ function renderHostTuiScreen(hostState, uiState) {
469
542
  || hostState.ingressRuntime.receive_mode === 'polling_degraded'
470
543
  || !!hostState.ingressRuntime.hooks_last_error;
471
544
  const ingressLabel = degraded ? 'Degraded' : 'Ready';
545
+ const transportHealth = hostState.transportHealth || { state: 'Ready', transport_mode: 'bridge', preferred_transport_mode: 'bridge', retry_queue_length: 0, consecutive_failures: 0 };
546
+ const humanDelivery = hostState.humanDelivery || {
547
+ mode: 'projection_outbox',
548
+ active_bindings: 0,
549
+ pending_intents: 0,
550
+ unresolved_intents: 0,
551
+ failed_intents: 0,
552
+ acknowledged_intents: 0,
553
+ channel_capabilities: [],
554
+ supported_channels: [],
555
+ unsupported_channels: [],
556
+ last_canary_ok: null,
557
+ last_canary_at: null,
558
+ };
559
+ const sinceLastSeenGlobal = hostState.sinceLastSeenGlobal || {};
472
560
  const lines = [
473
561
  'PingAgent Host TUI',
474
562
  `DID: ${hostState.identity.did}`,
475
563
  `status=${formatStatusLine(uiState?.statusLevel || 'info', uiState?.statusMessage || '(ready)')}${statusTs}${statusCountdown}`,
476
564
  `runtime_mode=${hostState.runtimeMode} receive_mode=${hostState.ingressRuntime?.receive_mode || 'webhook'} current_openclaw_chat=${hostState.activeChatSession || '(none)'}`,
477
565
  `ingress=${ingressLabel}${degraded ? ' action=[f] fix-now' : ''}`,
566
+ `transport=${transportHealth.transport_mode} preferred=${transportHealth.preferred_transport_mode} state=${transportHealth.state} retry_queue=${transportHealth.retry_queue_length} failures=${transportHealth.consecutive_failures}`,
567
+ `human_delivery mode=${humanDelivery.mode || 'projection_outbox'} active_bindings=${humanDelivery.active_bindings ?? 0} pending=${humanDelivery.pending_intents ?? 0} unresolved=${humanDelivery.unresolved_intents ?? 0} failed=${humanDelivery.failed_intents ?? 0} acked=${humanDelivery.acknowledged_intents ?? 0}`,
568
+ `human_delivery_channels supported=${(humanDelivery.supported_channels || []).join(',') || '(none)'} unsupported=${(humanDelivery.unsupported_channels || []).join(',') || '(none)'} canary_ok=${typeof humanDelivery.last_canary_ok === 'boolean' ? String(humanDelivery.last_canary_ok) : '(unknown)'} at=${humanDelivery.last_canary_at || '(none)'}`,
478
569
  uiState?.publicLinkUrl ? `public_link=${uiState.publicLinkUrl}` : null,
479
570
  `sessions=${sessions.length} unread_total=${hostState.unreadTotal ?? 0} alert_sessions=${hostState.alertSessions ?? 0} view=${view} projection=${hostState.projectionPreset || 'balanced'}`,
571
+ `since_last_seen external=${sinceLastSeenGlobal.new_external_messages ?? 0} conclusions=${sinceLastSeenGlobal.new_conclusions ?? 0} decisions=${sinceLastSeenGlobal.new_decisions ?? 0} failures=${sinceLastSeenGlobal.new_failures ?? 0}`,
480
572
  `policy=${hostState.policyPath}`,
481
573
  `chat_link_map=${hostState.sessionMapPath}`,
482
574
  `chat_link_alerts=${hostState.sessionBindingAlertsPath}`,
@@ -505,12 +597,14 @@ function renderHostTuiScreen(hostState, uiState) {
505
597
  lines.push('- y: dump task detail to stdout (task-detail view, choose json/plain)');
506
598
  lines.push('- u: approve the selected collaboration decision');
507
599
  lines.push('- U: reject the selected collaboration decision');
600
+ lines.push('- b: switch preferred transport to bridge');
601
+ lines.push('- c: switch preferred transport to channel');
508
602
  lines.push('- f: repair OpenClaw hooks config');
509
603
  lines.push('- A: apply first open trust recommendation for selected session');
510
604
  lines.push('- D: dismiss current open recommendation');
511
605
  lines.push('- R: reopen dismissed/superseded recommendation');
512
- lines.push('- b: attach selected session to the current OpenClaw chat');
513
- lines.push('- c: detach the selected chat link');
606
+ lines.push('- B: attach selected session to the current OpenClaw chat');
607
+ lines.push('- C: detach the selected chat link');
514
608
  lines.push('- q: quit');
515
609
  } else if (view === 'history') {
516
610
  lines.push('Conversation History');
@@ -576,7 +670,7 @@ function renderHostTuiScreen(hostState, uiState) {
576
670
  lines.push(...(pendingCollaborationEventsGlobal.length
577
671
  ? pendingCollaborationEventsGlobal.map((event, idx) => {
578
672
  const marker = idx === selectedDecisionIndex ? '>' : ' ';
579
- return `${marker} ${event.id} [${event.severity}] ${truncateLine(event.summary || '(none)', 88)} session=${event.session_key || '(none)'}`;
673
+ return `${marker} ${event.id} [${event.severity}${event.overdue ? ':overdue' : ''}] ${truncateLine(event.summary || '(none)', 88)} session=${event.session_key || '(none)'}`;
580
674
  })
581
675
  : ['- none']));
582
676
  } else if (view === 'tasks') {
@@ -642,7 +736,12 @@ function renderHostTuiScreen(hostState, uiState) {
642
736
  if (pendingCollaborationEvents.length > 0) {
643
737
  lines.push(`pending_collaboration_decisions=${pendingCollaborationEvents.length}`);
644
738
  lines.push(`next_decision=${truncateLine(pendingCollaborationEvents[0].summary || '(none)', 100)}`);
739
+ if (pendingCollaborationEvents[0].overdue) {
740
+ lines.push(`next_decision_overdue=${Math.ceil((pendingCollaborationEvents[0].overdue_by_ms || 0) / 60000)}m`);
741
+ }
645
742
  }
743
+ const sessionSeen = selected.since_last_seen || {};
744
+ lines.push(`since_last_seen external=${sessionSeen.new_external_messages ?? 0} conclusions=${sessionSeen.new_conclusions ?? 0} decisions=${sessionSeen.new_decisions ?? 0} failures=${sessionSeen.new_failures ?? 0}`);
646
745
  const actionBar = [
647
746
  selected.trust_state === 'pending' ? '[a] approve' : null,
648
747
  '[A] apply-rec',
@@ -656,8 +755,8 @@ function renderHostTuiScreen(hostState, uiState) {
656
755
  pendingCollaborationEvents.length ? '[U] reject-decision' : null,
657
756
  '[o] history',
658
757
  '[t] tasks',
659
- '[b] attach-chat',
660
- '[c] detach-chat',
758
+ '[B] attach-chat',
759
+ '[C] detach-chat',
661
760
  ].filter(Boolean).join(' ');
662
761
  lines.push(`actions=${actionBar}`);
663
762
  lines.push('');
@@ -688,6 +787,35 @@ function renderHostTuiScreen(hostState, uiState) {
688
787
  lines.push(...(collaborationEvents.length
689
788
  ? collaborationEvents.map((event) => `- ${event.event_type} [${event.severity}${event.approval_required ? `:${event.approval_status}` : ''}] ${truncateLine(event.summary || '', 90)}`)
690
789
  : ['- none']));
790
+ if (projectionPreview) {
791
+ lines.push('');
792
+ lines.push(`Projection Preview [preset=${projectionPreview.preset}]`);
793
+ lines.push(...(projectionPreview.recent.length
794
+ ? projectionPreview.recent.map((entry) => `- ${entry.event_type} => ${entry.disposition} (${truncateLine(entry.reason, 70)})`)
795
+ : ['- none']));
796
+ }
797
+ if (view === 'detail') {
798
+ lines.push('');
799
+ lines.push('Delivery Timeline');
800
+ lines.push(...(deliveryTimeline.length
801
+ ? deliveryTimeline.slice(0, 10).map((entry) => `- ${formatTs(entry.ts_ms, false)} ${entry.kind} ${truncateLine(entry.summary || '', 72)}`)
802
+ : ['- none']));
803
+ lines.push('');
804
+ lines.push('Human Reply Targets');
805
+ lines.push(...(recentBindings.length
806
+ ? recentBindings.slice(0, 6).map((binding) => `- ${binding.channel} -> ${truncateLine(binding.to || '(none)', 48)} [${binding.status}] owner=${truncateLine(binding.owner_ref || '(none)', 32)}`)
807
+ : ['- none']));
808
+ lines.push('');
809
+ lines.push('Notification Intents');
810
+ lines.push(...(recentNotificationIntents.length
811
+ ? recentNotificationIntents.slice(0, 6).map((intent) => `- ${intent.intent_type} [${intent.status}] ${truncateLine(intent.summary || '', 72)}${intent.acknowledged_at ? ` ack=${formatTs(intent.acknowledged_at, false)}` : ''}`)
812
+ : ['- none']));
813
+ lines.push('');
814
+ lines.push('Channel Capability Canary');
815
+ lines.push(...((humanDelivery.channel_capabilities || []).length
816
+ ? humanDelivery.channel_capabilities.slice(0, 8).map((capability) => `- ${capability.channel} configured=${capability.configured ? 'true' : 'false'} explicit_send=${capability.supports_explicit_send ? 'true' : 'false'} dry_run=${capability.supports_dry_run ? 'true' : 'false'} canary=${typeof capability.last_canary_ok === 'boolean' ? String(capability.last_canary_ok) : '(unknown)'}`)
817
+ : ['- none']));
818
+ }
691
819
  lines.push('');
692
820
  lines.push('Audit');
693
821
  lines.push(...(auditEvents.length
@@ -699,7 +827,7 @@ function renderHostTuiScreen(hostState, uiState) {
699
827
  }
700
828
 
701
829
  lines.push('');
702
- lines.push('Keys: ↑/↓ or j/k select Enter/l open Esc/h back g/G jump r refresh a approve u/U decide A apply-rec D dismiss-rec R reopen-rec d demo m read p reply o history s search t tasks x cancel-task y dump f fix-hooks b attach-chat c detach-chat ? help q quit');
830
+ lines.push('Keys: ↑/↓ or j/k select Enter/l open Esc/h back g/G jump r refresh a approve u/U decide A apply-rec D dismiss-rec R reopen-rec d demo m read p reply o history s search t tasks x cancel-task y dump f fix-hooks b transport->bridge c transport->channel B attach-chat C detach-chat ? help q quit');
703
831
  return lines.join('\n');
704
832
  }
705
833
 
@@ -720,6 +848,10 @@ async function runHostTui(identityPath, opts) {
720
848
  historySearchQuery: '',
721
849
  publicLinkUrl: '',
722
850
  };
851
+ const seenState = {
852
+ globalView: null,
853
+ sessionKey: null,
854
+ };
723
855
 
724
856
  const render = () => {
725
857
  if (uiState.statusExpiresAt && Date.now() >= uiState.statusExpiresAt) {
@@ -762,7 +894,49 @@ async function runHostTui(identityPath, opts) {
762
894
  return rendered.hostState;
763
895
  };
764
896
 
897
+ const markSeen = (scopeType, scopeKey = null) => {
898
+ const store = openStore(identityPath);
899
+ try {
900
+ return new OperatorSeenStateManager(store).markSeen({
901
+ operator_id: 'tui',
902
+ scope_type: scopeType,
903
+ scope_key: scopeType === 'session' ? (scopeKey || null) : null,
904
+ last_seen_ts: Date.now(),
905
+ });
906
+ } finally {
907
+ store.close();
908
+ }
909
+ };
910
+
911
+ const refreshSeenMarkers = (hostState, options = {}) => {
912
+ let changed = false;
913
+ const forceGlobal = !!options.forceGlobal;
914
+ const forceSession = !!options.forceSession;
915
+ if (uiState.view === 'overview' || uiState.view === 'decisions') {
916
+ if (forceGlobal || seenState.globalView !== uiState.view) {
917
+ markSeen('global');
918
+ seenState.globalView = uiState.view;
919
+ changed = true;
920
+ }
921
+ if (uiState.view === 'overview') seenState.sessionKey = null;
922
+ } else if (['detail', 'history', 'tasks', 'task-detail'].includes(uiState.view)) {
923
+ const sessionKey = hostState.selectedSession?.session_key || null;
924
+ if (sessionKey && (forceSession || seenState.sessionKey !== sessionKey)) {
925
+ markSeen('session', sessionKey);
926
+ seenState.sessionKey = sessionKey;
927
+ changed = true;
928
+ }
929
+ }
930
+ return changed;
931
+ };
932
+
933
+ const rerenderAfterSeenUpdate = (hostState, options = {}) => {
934
+ if (!refreshSeenMarkers(hostState, options)) return hostState;
935
+ return redraw();
936
+ };
937
+
765
938
  let latestState = redraw();
939
+ latestState = rerenderAfterSeenUpdate(latestState, { forceGlobal: true });
766
940
  readline.emitKeypressEvents(process.stdin);
767
941
  if (process.stdin.setRawMode) process.stdin.setRawMode(true);
768
942
 
@@ -779,6 +953,50 @@ async function runHostTui(identityPath, opts) {
779
953
  uiState.statusAt = Date.now();
780
954
  };
781
955
 
956
+ const switchTransport = async (mode) => {
957
+ const label = mode === 'channel' ? 'channel' : 'bridge';
958
+ const confirmed = await confirmAction(`Switch preferred transport to ${label} and request a managed restart?`);
959
+ if (!confirmed) {
960
+ setStatus(`Transport switch to ${label} cancelled.`, 'warn');
961
+ latestState = redraw();
962
+ return;
963
+ }
964
+ try {
965
+ const result = switchTransportPreference(mode, {
966
+ updated_by: 'tui',
967
+ });
968
+ const store = openStore(identityPath);
969
+ try {
970
+ new CollaborationEventManager(store).record({
971
+ event_type: 'transport_switched',
972
+ severity: result.restarted ? 'notice' : 'warning',
973
+ summary: result.restarted
974
+ ? `Preferred transport switched to ${label}. A managed restart was attempted.`
975
+ : `Preferred transport switched to ${label}. Restart is still required.`,
976
+ detail: {
977
+ preferred_mode: result.preferred_mode,
978
+ restart_required: result.restart_required,
979
+ restart_method: result.restart_method ?? null,
980
+ restart_error: result.restart_error ?? null,
981
+ preference_path: result.preference_path,
982
+ },
983
+ });
984
+ } finally {
985
+ store.close();
986
+ }
987
+ setStatus(
988
+ result.restarted
989
+ ? `Preferred transport is now ${label}; managed restart attempted via ${result.restart_method || 'unknown method'}.`
990
+ : `Preferred transport saved as ${label}; restart still required.`,
991
+ result.restarted ? 'ok' : 'warn',
992
+ result.restarted ? 7000 : 9000,
993
+ );
994
+ } catch (error) {
995
+ setStatus(`Transport switch failed: ${error?.message || 'unknown error'}`, 'err', 9000);
996
+ }
997
+ latestState = redraw();
998
+ };
999
+
782
1000
  const applySessionRecommendation = (selected) => {
783
1001
  if (!selected?.remote_did) return { ok: false, message: 'No remote DID for selected session.' };
784
1002
  const store = openStore(identityPath);
@@ -985,6 +1203,10 @@ async function runHostTui(identityPath, opts) {
985
1203
  uiState.historySearchQuery = '';
986
1204
  }
987
1205
  latestState = redraw();
1206
+ latestState = rerenderAfterSeenUpdate(latestState, {
1207
+ forceGlobal: uiState.view === 'overview' || uiState.view === 'decisions',
1208
+ forceSession: ['detail', 'history', 'tasks', 'task-detail'].includes(uiState.view),
1209
+ });
988
1210
  };
989
1211
 
990
1212
  const jumpSelection = (target) => {
@@ -1007,6 +1229,10 @@ async function runHostTui(identityPath, opts) {
1007
1229
  uiState.historySearchQuery = '';
1008
1230
  }
1009
1231
  latestState = redraw();
1232
+ latestState = rerenderAfterSeenUpdate(latestState, {
1233
+ forceGlobal: uiState.view === 'overview' || uiState.view === 'decisions',
1234
+ forceSession: ['detail', 'history', 'tasks', 'task-detail'].includes(uiState.view),
1235
+ });
1010
1236
  };
1011
1237
 
1012
1238
  const stopInterval = () => {
@@ -1060,6 +1286,7 @@ async function runHostTui(identityPath, opts) {
1060
1286
  uiState.view = 'detail';
1061
1287
  }
1062
1288
  latestState = redraw();
1289
+ latestState = rerenderAfterSeenUpdate(latestState, { forceSession: uiState.view !== 'decisions' && uiState.view !== 'overview' });
1063
1290
  return;
1064
1291
  }
1065
1292
  if (key?.name === 'escape' || key?.name === 'h') {
@@ -1068,6 +1295,7 @@ async function runHostTui(identityPath, opts) {
1068
1295
  else if (uiState.view === 'decisions') uiState.view = 'overview';
1069
1296
  else uiState.view = 'overview';
1070
1297
  latestState = redraw();
1298
+ latestState = rerenderAfterSeenUpdate(latestState, { forceGlobal: uiState.view === 'overview' || uiState.view === 'decisions' });
1071
1299
  return;
1072
1300
  }
1073
1301
  if (_str === '?') {
@@ -1104,6 +1332,7 @@ async function runHostTui(identityPath, opts) {
1104
1332
  uiState.selectedHistoryPageIndex = 0;
1105
1333
  setStatus('Opened task list view.', 'info');
1106
1334
  latestState = redraw();
1335
+ latestState = rerenderAfterSeenUpdate(latestState, { forceSession: true });
1107
1336
  return;
1108
1337
  }
1109
1338
  if (key?.name === 'i') {
@@ -1111,6 +1340,7 @@ async function runHostTui(identityPath, opts) {
1111
1340
  uiState.selectedDecisionIndex = 0;
1112
1341
  setStatus('Opened decision inbox.', 'info');
1113
1342
  latestState = redraw();
1343
+ latestState = rerenderAfterSeenUpdate(latestState, { forceGlobal: true });
1114
1344
  return;
1115
1345
  }
1116
1346
  if (key?.name === 'r') {
@@ -1293,6 +1523,7 @@ async function runHostTui(identityPath, opts) {
1293
1523
  uiState.historySearchQuery = '';
1294
1524
  setStatus('Opened local history view.', 'info');
1295
1525
  latestState = redraw();
1526
+ latestState = rerenderAfterSeenUpdate(latestState, { forceSession: true });
1296
1527
  return;
1297
1528
  }
1298
1529
  if ((_str === '/' || key?.name === 's') && uiState.view === 'history') {
@@ -1305,6 +1536,14 @@ async function runHostTui(identityPath, opts) {
1305
1536
  return;
1306
1537
  }
1307
1538
  if (key?.name === 'b') {
1539
+ await switchTransport('bridge');
1540
+ return;
1541
+ }
1542
+ if (key?.name === 'c') {
1543
+ await switchTransport('channel');
1544
+ return;
1545
+ }
1546
+ if (_str === 'B') {
1308
1547
  const selected = (latestState.sessions || []).find((session) => session.session_key === uiState.selectedSessionKey);
1309
1548
  if (!selected?.conversation_id) return;
1310
1549
  const current = latestState.activeChatSession || '(none)';
@@ -1324,7 +1563,7 @@ async function runHostTui(identityPath, opts) {
1324
1563
  latestState = redraw();
1325
1564
  return;
1326
1565
  }
1327
- if (key?.name === 'c') {
1566
+ if (_str === 'C') {
1328
1567
  const selected = (latestState.sessions || []).find((session) => session.session_key === uiState.selectedSessionKey);
1329
1568
  if (!selected?.conversation_id) return;
1330
1569
  removeSessionBinding(selected.conversation_id);