@pingagent/sdk 0.1.11 → 0.1.12

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
@@ -5,6 +5,8 @@ import * as path from 'node:path';
5
5
  import * as os from 'node:os';
6
6
  import * as readline from 'node:readline';
7
7
  import { spawnSync } from 'node:child_process';
8
+ import { createRequire } from 'node:module';
9
+ import { fileURLToPath } from 'node:url';
8
10
  import {
9
11
  PingAgentClient,
10
12
  generateIdentity,
@@ -40,6 +42,8 @@ import {
40
42
  } from '../dist/index.js';
41
43
  import { ERROR_HINTS, SCHEMA_TEXT } from '@pingagent/schemas';
42
44
 
45
+ const require = createRequire(import.meta.url);
46
+ const THIS_FILE = fileURLToPath(import.meta.url);
43
47
  const DEFAULT_SERVER = 'https://pingagent.chat';
44
48
  const UPGRADE_URL = 'https://pingagent.chat';
45
49
  const DEFAULT_IDENTITY_PATH = path.join(os.homedir(), '.pingagent', 'identity.json');
@@ -140,29 +144,69 @@ function writeTrustPolicyDoc(identityPath, doc) {
140
144
 
141
145
  function findOpenClawInstallScript() {
142
146
  const explicit = process.env.PINGAGENT_OPENCLAW_INSTALL_BIN;
143
- if (explicit) return { cmd: process.execPath, args: [resolvePath(explicit)] };
144
- const repoScript = path.resolve(process.cwd(), 'packages', 'openclaw-install', 'install.mjs');
145
- if (fs.existsSync(repoScript)) return { cmd: process.execPath, args: [repoScript] };
146
- return null;
147
+ if (explicit) return { cmd: process.execPath, args: [resolvePath(explicit)], source: 'env' };
148
+
149
+ const localCandidates = [
150
+ path.resolve(process.cwd(), 'packages', 'openclaw-install', 'install.mjs'),
151
+ path.resolve(path.dirname(THIS_FILE), '..', '..', 'openclaw-install', 'install.mjs'),
152
+ ];
153
+ for (const candidate of localCandidates) {
154
+ if (fs.existsSync(candidate)) return { cmd: process.execPath, args: [candidate], source: 'repo' };
155
+ }
156
+
157
+ try {
158
+ const pkgJsonPath = require.resolve('@pingagent/openclaw-install/package.json');
159
+ const installedScript = path.join(path.dirname(pkgJsonPath), 'install.mjs');
160
+ if (fs.existsSync(installedScript)) {
161
+ return { cmd: process.execPath, args: [installedScript], source: 'installed-package' };
162
+ }
163
+ } catch {
164
+ // fall through to npx fallback
165
+ }
166
+
167
+ return { cmd: 'npx', args: ['-y', '@pingagent/openclaw-install'], source: 'npx' };
147
168
  }
148
169
 
149
170
  function runOpenClawInstall(args) {
150
171
  const resolved = findOpenClawInstallScript();
151
- if (!resolved) {
152
- return { ok: false, stdout: '', stderr: 'OpenClaw installer script not found locally. Set PINGAGENT_OPENCLAW_INSTALL_BIN.' };
153
- }
154
172
  const result = spawnSync(resolved.cmd, [...resolved.args, ...args], {
155
173
  encoding: 'utf-8',
156
174
  env: process.env,
157
175
  });
176
+ const errorMessage = result.error
177
+ ? (result.error.code === 'ENOENT'
178
+ ? `Failed to launch ${resolved.cmd}. Install npx/Node tooling or set PINGAGENT_OPENCLAW_INSTALL_BIN.`
179
+ : String(result.error.message || result.error))
180
+ : '';
158
181
  return {
159
- ok: result.status === 0,
182
+ ok: result.status === 0 && !result.error,
160
183
  stdout: String(result.stdout ?? ''),
161
- stderr: String(result.stderr ?? ''),
184
+ stderr: errorMessage || String(result.stderr ?? ''),
162
185
  status: result.status ?? 1,
186
+ source: resolved.source,
163
187
  };
164
188
  }
165
189
 
190
+ function getHostPanelSurfaceUrl() {
191
+ const portRaw = String(process.env.PINGAGENT_WEB_PORT || '3846').trim();
192
+ const port = Number.parseInt(portRaw, 10);
193
+ return `http://127.0.0.1:${Number.isFinite(port) ? port : 3846}/host-panel`;
194
+ }
195
+
196
+ function getSurfaceRecommendationLines(primaryCommandPrefix = 'npx @pingagent/sdk', secondaryCommandPrefix = 'pingagent') {
197
+ return [
198
+ 'With GUI: Host Panel',
199
+ ` Start locally: ${primaryCommandPrefix} web`,
200
+ ...(secondaryCommandPrefix && secondaryCommandPrefix !== primaryCommandPrefix ? [` Or via local bin: ${secondaryCommandPrefix} web`] : []),
201
+ ` URL when running: ${getHostPanelSurfaceUrl()}`,
202
+ 'Headless / low-token: TUI',
203
+ ` ${primaryCommandPrefix} host tui`,
204
+ ...(secondaryCommandPrefix && secondaryCommandPrefix !== primaryCommandPrefix ? [` ${secondaryCommandPrefix} host tui`] : []),
205
+ ` ${primaryCommandPrefix} host tui --once`,
206
+ 'MCP: agent/runtime control surface, not the default human operator UI',
207
+ ];
208
+ }
209
+
166
210
  function clearScreen() {
167
211
  process.stdout.write('\x1Bc');
168
212
  }
@@ -194,11 +238,11 @@ function truncateLine(value, max = 100) {
194
238
 
195
239
  function formatSessionRow(session, selected) {
196
240
  const marker = selected ? '>' : ' ';
197
- const rebind = session.binding_alert ? ' !rebind' : '';
241
+ const reconnect = session.binding_alert ? ' !reconnect' : '';
198
242
  const trust = session.trust_state || 'unknown';
199
243
  const unread = session.unread_count ?? 0;
200
244
  const who = truncateLine(session.remote_did || session.conversation_id || 'unknown', 40);
201
- return `${marker} ${who} [${trust}] unread=${unread}${rebind}`;
245
+ return `${marker} ${who} [${trust}] unread=${unread}${reconnect}`;
202
246
  }
203
247
 
204
248
  function formatMessageRow(message) {
@@ -410,13 +454,13 @@ function renderHostTuiScreen(hostState, uiState) {
410
454
  'PingAgent Host TUI',
411
455
  `DID: ${hostState.identity.did}`,
412
456
  `status=${formatStatusLine(uiState?.statusLevel || 'info', uiState?.statusMessage || '(ready)')}${statusTs}${statusCountdown}`,
413
- `runtime_mode=${hostState.runtimeMode} receive_mode=${hostState.ingressRuntime?.receive_mode || 'webhook'} active_chat_session=${hostState.activeChatSession || '(none)'}`,
457
+ `runtime_mode=${hostState.runtimeMode} receive_mode=${hostState.ingressRuntime?.receive_mode || 'webhook'} current_openclaw_chat=${hostState.activeChatSession || '(none)'}`,
414
458
  `ingress=${ingressLabel}${degraded ? ' action=[f] fix-now' : ''}`,
415
459
  uiState?.publicLinkUrl ? `public_link=${uiState.publicLinkUrl}` : null,
416
460
  `sessions=${sessions.length} unread_total=${hostState.unreadTotal ?? 0} alert_sessions=${hostState.alertSessions ?? 0} view=${view}`,
417
461
  `policy=${hostState.policyPath}`,
418
- `session_map=${hostState.sessionMapPath}`,
419
- `binding_alerts=${hostState.sessionBindingAlertsPath}`,
462
+ `chat_link_map=${hostState.sessionMapPath}`,
463
+ `chat_link_alerts=${hostState.sessionBindingAlertsPath}`,
420
464
  hostState.ingressRuntime?.hooks_last_error ? `hooks_error=${truncateLine(hostState.ingressRuntime.hooks_last_error, 120)}` : null,
421
465
  '',
422
466
  ].filter(Boolean);
@@ -443,8 +487,8 @@ function renderHostTuiScreen(hostState, uiState) {
443
487
  lines.push('- A: apply first open trust recommendation for selected session');
444
488
  lines.push('- D: dismiss current open recommendation');
445
489
  lines.push('- R: reopen dismissed/superseded recommendation');
446
- lines.push('- b: bind selected conversation to current chat session');
447
- lines.push('- c: clear selected binding');
490
+ lines.push('- b: attach selected session to the current OpenClaw chat');
491
+ lines.push('- c: detach the selected chat link');
448
492
  lines.push('- q: quit');
449
493
  } else if (view === 'history') {
450
494
  lines.push('Conversation History');
@@ -543,13 +587,13 @@ function renderHostTuiScreen(hostState, uiState) {
543
587
  lines.push(`remote=${selected.remote_did || '(unknown)'}`);
544
588
  lines.push(`trust=${selected.trust_state} unread=${selected.unread_count}`);
545
589
  lines.push(`last_preview=${selected.last_message_preview || '(none)'}`);
546
- lines.push(`binding=${selected.binding?.session_key || '(unbound)'}`);
547
- lines.push(`current_chat=${hostState.activeChatSession || '(none)'}`);
590
+ lines.push(`chat_link=${selected.binding?.session_key || '(none)'}`);
591
+ lines.push(`current_openclaw_chat=${hostState.activeChatSession || '(none)'}`);
548
592
  if (selected.binding_alert) {
549
- lines.push(`needs_rebind=true`);
593
+ lines.push('needs_reconnect=true');
550
594
  lines.push(`warning=${selected.binding_alert.message}`);
551
595
  } else {
552
- lines.push('needs_rebind=false');
596
+ lines.push('needs_reconnect=false');
553
597
  }
554
598
  if (openRecommendation) {
555
599
  lines.push(`trust_action=${getTrustRecommendationActionLabel(openRecommendation)}`);
@@ -573,8 +617,8 @@ function renderHostTuiScreen(hostState, uiState) {
573
617
  '[S] summary',
574
618
  '[o] history',
575
619
  '[t] tasks',
576
- '[b] bind-current',
577
- '[c] clear-binding',
620
+ '[b] attach-chat',
621
+ '[c] detach-chat',
578
622
  ].filter(Boolean).join(' ');
579
623
  lines.push(`actions=${actionBar}`);
580
624
  lines.push('');
@@ -611,7 +655,7 @@ function renderHostTuiScreen(hostState, uiState) {
611
655
  }
612
656
 
613
657
  lines.push('');
614
- lines.push('Keys: ↑/↓ or j/k select Enter/l open Esc/h back g/G jump r refresh a approve 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 bind c clear ? help q quit');
658
+ lines.push('Keys: ↑/↓ or j/k select Enter/l open Esc/h back g/G jump r refresh a approve 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');
615
659
  return lines.join('\n');
616
660
  }
617
661
 
@@ -1156,18 +1200,18 @@ async function runHostTui(identityPath, opts) {
1156
1200
  const selected = (latestState.sessions || []).find((session) => session.session_key === uiState.selectedSessionKey);
1157
1201
  if (!selected?.conversation_id) return;
1158
1202
  const current = latestState.activeChatSession || '(none)';
1159
- const previous = selected.binding?.session_key || '(unbound)';
1160
- const confirmed = await confirmAction(`Rebind conversation ${selected.conversation_id}\nRemote DID: ${selected.remote_did || '(unknown)'}\nCurrent chat: ${current}\nPrevious binding: ${previous}\nProceed?`);
1203
+ const previous = selected.binding?.session_key || '(none)';
1204
+ const confirmed = await confirmAction(`Attach chat link for conversation ${selected.conversation_id}\nRemote DID: ${selected.remote_did || '(unknown)'}\nCurrent OpenClaw chat: ${current}\nPrevious chat link: ${previous}\nProceed?`);
1161
1205
  if (confirmed) {
1162
1206
  if (!latestState.activeChatSession) {
1163
- setStatus('Rebind failed: no active chat session.', 'err');
1207
+ setStatus('Attach failed: no active OpenClaw chat.', 'err');
1164
1208
  latestState = redraw();
1165
1209
  return;
1166
1210
  }
1167
1211
  setSessionBinding(selected.conversation_id, latestState.activeChatSession);
1168
- setStatus(`Rebound ${selected.conversation_id} -> ${latestState.activeChatSession}`, 'ok');
1212
+ setStatus(`Attached chat link ${selected.conversation_id} -> ${latestState.activeChatSession}`, 'ok');
1169
1213
  } else {
1170
- setStatus('Rebind cancelled.', 'warn');
1214
+ setStatus('Attach chat link cancelled.', 'warn');
1171
1215
  }
1172
1216
  latestState = redraw();
1173
1217
  return;
@@ -1176,7 +1220,7 @@ async function runHostTui(identityPath, opts) {
1176
1220
  const selected = (latestState.sessions || []).find((session) => session.session_key === uiState.selectedSessionKey);
1177
1221
  if (!selected?.conversation_id) return;
1178
1222
  removeSessionBinding(selected.conversation_id);
1179
- setStatus(`Cleared binding for ${selected.conversation_id}`, 'ok');
1223
+ setStatus(`Detached chat link for ${selected.conversation_id}`, 'ok');
1180
1224
  latestState = redraw();
1181
1225
  return;
1182
1226
  }
@@ -3121,13 +3165,116 @@ program
3121
3165
  else console.log(`Connected. conversation=${convo.data.conversation_id} did=${targetDid}`);
3122
3166
  });
3123
3167
 
3168
+ async function runHostBootstrap(opts) {
3169
+ const validTemplates = new Set(['launchd', 'systemd', 'docker', 'pm2', 'supervisord']);
3170
+ const template = opts.template ? String(opts.template).trim() : '';
3171
+ if (template && !validTemplates.has(template)) {
3172
+ console.error(`Unsupported template: ${template}. Valid: ${Array.from(validTemplates).join(', ')}`);
3173
+ process.exit(1);
3174
+ }
3175
+
3176
+ const steps = [];
3177
+ const runStep = (label, args) => {
3178
+ const result = runOpenClawInstall(args);
3179
+ steps.push({ label, args, result });
3180
+ return { label, args, result };
3181
+ };
3182
+
3183
+ const installStep = runStep('install', []);
3184
+ const hooksStep = installStep.result.ok ? runStep('hooks repair', ['fix-hooks']) : null;
3185
+ const verifyStep = installStep.result.ok && hooksStep?.result.ok
3186
+ ? runStep('runtime verify', ['verify-runtime', '--fix-hooks'])
3187
+ : null;
3188
+
3189
+ let runnerStep = null;
3190
+ if (verifyStep?.result.ok && opts.write) {
3191
+ const runnerArgs = ['init-runner', '--ingress', '--panel'];
3192
+ if (template) runnerArgs.push('--template', template);
3193
+ const supportsWrite = !template || template === 'launchd' || template === 'systemd';
3194
+ if (supportsWrite) runnerArgs.push('--write');
3195
+ runnerStep = runStep(supportsWrite ? 'runner setup' : 'runner template', runnerArgs);
3196
+ }
3197
+
3198
+ if (runnerStep?.result.stdout && opts.write) {
3199
+ console.log(runnerStep.result.stdout.trim());
3200
+ if (runnerStep.result.stderr.trim()) console.error(runnerStep.result.stderr.trim());
3201
+ console.log('');
3202
+ }
3203
+
3204
+ const failed = steps.find((step) => !step.result.ok) ?? null;
3205
+ const formatStepStatus = (step, fallback = 'skipped') => (step ? (step.result.ok ? 'ok' : 'failed') : fallback);
3206
+ const runnerStatus = !opts.write
3207
+ ? 'not_written'
3208
+ : !runnerStep
3209
+ ? 'skipped'
3210
+ : !runnerStep.result.ok
3211
+ ? 'failed'
3212
+ : (template && template !== 'launchd' && template !== 'systemd')
3213
+ ? 'template_printed_not_started'
3214
+ : 'written_not_started';
3215
+ const installerSource = installStep.result.source || 'unknown';
3216
+
3217
+ console.log('PingAgent Host Bootstrap');
3218
+ console.log('========================');
3219
+ console.log(`install=${formatStepStatus(installStep)}`);
3220
+ console.log(`hooks_repair=${formatStepStatus(hooksStep)}`);
3221
+ console.log(`runtime_verify=${formatStepStatus(verifyStep)}`);
3222
+ console.log(`installer_source=${installerSource}`);
3223
+ console.log(`runner=${runnerStatus}`);
3224
+ console.log(`host_panel_url=${getHostPanelSurfaceUrl()}`);
3225
+ console.log('host_panel_started_by_bootstrap=false');
3226
+ console.log('host_panel_start=npx @pingagent/sdk web');
3227
+ console.log('host_panel_start_local=pingagent web');
3228
+ console.log('tui=npx @pingagent/sdk host tui');
3229
+ console.log('tui_local=pingagent host tui');
3230
+ console.log('');
3231
+ console.log('Control surfaces:');
3232
+ for (const line of getSurfaceRecommendationLines('npx @pingagent/sdk', 'pingagent')) console.log(line);
3233
+ console.log('');
3234
+ if (!opts.write) {
3235
+ console.log('Next steps:');
3236
+ console.log(' Bootstrap validates and repairs config, but it does not start long-lived daemons.');
3237
+ console.log(' Start the Host Panel now with: npx @pingagent/sdk web (or pingagent web)');
3238
+ console.log(' Use the headless surface now with: npx @pingagent/sdk host tui (or pingagent host tui)');
3239
+ console.log(` Re-run with: npx @pingagent/sdk host bootstrap --write${template ? ` --template ${template}` : ''}`);
3240
+ console.log(' Manual path: npx @pingagent/openclaw-install init-runner --ingress --panel');
3241
+ } else if (runnerStep?.result.ok) {
3242
+ console.log('Next steps:');
3243
+ console.log(' Runner files/templates were generated, but bootstrap did not start those services.');
3244
+ if (!template || template === 'launchd' || template === 'systemd') {
3245
+ console.log(' Follow the printed launchctl/systemctl instructions to start them.');
3246
+ } else {
3247
+ console.log(' Start the generated runner with your chosen process manager.');
3248
+ }
3249
+ }
3250
+
3251
+ if (failed) {
3252
+ const stdout = failed.result.stdout.trim();
3253
+ const stderr = failed.result.stderr.trim();
3254
+ console.error('');
3255
+ console.error(`Bootstrap failed during ${failed.label}.`);
3256
+ if (stdout) console.error(stdout);
3257
+ if (stderr) console.error(stderr);
3258
+ process.exit(1);
3259
+ }
3260
+ }
3261
+
3124
3262
  const host = program
3125
3263
  .command('host')
3126
- .description('Headless runtime inspection and control for PingAgent host state');
3264
+ .description('OpenClaw host activation and operator control surfaces for GUI, headless, and low-token workflows');
3265
+
3266
+ host
3267
+ .command('bootstrap')
3268
+ .description('Run the idempotent OpenClaw activation flow and print the recommended Host Panel / TUI surfaces')
3269
+ .option('--write', 'Write launchd/systemd runner files when supported')
3270
+ .option('--template <name>', 'Runner template: launchd, systemd, docker, pm2, or supervisord')
3271
+ .action(async (opts) => {
3272
+ await runHostBootstrap(opts);
3273
+ });
3127
3274
 
3128
3275
  host
3129
3276
  .command('tui')
3130
- .description('Start a terminal UI for runtime, sessions, bindings, and rebind actions')
3277
+ .description('Start the headless / low-token terminal UI for runtime, sessions, chat links, and repair actions')
3131
3278
  .option('--once', 'Print one snapshot and exit')
3132
3279
  .option('--refresh-ms <ms>', 'Refresh interval in interactive mode', '2000')
3133
3280
  .option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
@@ -3142,7 +3289,7 @@ host
3142
3289
 
3143
3290
  program
3144
3291
  .command('web')
3145
- .description('Start local web UI and host panel for debugging, runtime inspection, trust policy, and audit. By default scans ~/.pingagent for profiles; use --identity-dir to lock to one profile.')
3292
+ .description('Start the Host Panel, the primary GUI surface for runtime inspection, trust policy, and repair. Use pingagent host tui for headless or low-token operation.')
3146
3293
  .option('--port <port>', 'Port for the web server', '3846')
3147
3294
  .action(async (opts) => {
3148
3295
  const serverUrl = process.env.PINGAGENT_SERVER_URL || DEFAULT_SERVER;
@@ -153,6 +153,20 @@ function getHostPanelHtml() {
153
153
  }
154
154
  .empty { color: #94a3b8; font-size: 13px; }
155
155
  .link-row { display: flex; gap: 10px; margin-top: 20px; }
156
+ .toolbar-row { display: flex; justify-content: space-between; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; }
157
+ .toolbar-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
158
+ .mode-toggle { display: inline-flex; gap: 6px; padding: 4px; border-radius: 999px; border: 1px solid #334155; background: #020617; }
159
+ .mode-toggle button {
160
+ border: 0;
161
+ background: transparent;
162
+ color: #94a3b8;
163
+ border-radius: 999px;
164
+ padding: 6px 10px;
165
+ cursor: pointer;
166
+ }
167
+ .mode-toggle button.active { background: #0f766e; color: #ecfeff; }
168
+ .summary-pills { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
169
+ .summary-pills .pill { font-size: 11px; }
156
170
  @media (max-width: 1000px) {
157
171
  .layout { grid-template-columns: 1fr; }
158
172
  .sidebar { border-right: none; border-bottom: 1px solid #1e293b; }
@@ -196,7 +210,17 @@ function getHostPanelHtml() {
196
210
  <div class="grid stats" id="statsGrid"></div>
197
211
  <div class="grid runtime-layout" style="margin-top:16px">
198
212
  <div class="card">
199
- <h2>Recent Sessions</h2>
213
+ <div class="toolbar-row">
214
+ <h2 style="margin:0">Recent Sessions</h2>
215
+ <div class="toolbar-actions">
216
+ <button class="secondary-btn" id="toggleUnreadBtn" style="width:auto">Unread only: off</button>
217
+ <button class="secondary-btn" id="nextUnreadBtn" style="width:auto">Next unread</button>
218
+ <div class="mode-toggle" aria-label="Runtime detail mode">
219
+ <button id="detailModeBasicBtn" class="active" type="button">Basic</button>
220
+ <button id="detailModeAdvancedBtn" type="button">Advanced</button>
221
+ </div>
222
+ </div>
223
+ </div>
200
224
  <div class="sessions" id="sessionList"></div>
201
225
  </div>
202
226
  <div class="grid">
@@ -325,14 +349,28 @@ function getHostPanelHtml() {
325
349
  </div>
326
350
 
327
351
  <script>
352
+ const initialQuery = (function () {
353
+ const params = new URLSearchParams(window.location.search);
354
+ const profile = params.get('profile');
355
+ const sessionKey = params.get('session_key');
356
+ const view = params.get('view');
357
+ return {
358
+ profile: profile && profile.trim() ? profile.trim() : null,
359
+ sessionKey: sessionKey && sessionKey.trim() ? sessionKey.trim() : null,
360
+ view: view === 'policy' ? 'policy' : 'runtime',
361
+ };
362
+ })();
363
+
328
364
  const state = {
329
- selectedProfile: sessionStorage.getItem('pingagent_host_panel_profile') || null,
330
- currentTab: 'runtime',
365
+ selectedProfile: initialQuery.profile || sessionStorage.getItem('pingagent_host_panel_profile') || null,
366
+ currentTab: initialQuery.view,
331
367
  profiles: [],
332
368
  overview: null,
333
369
  session: null,
334
370
  policy: null,
335
- selectedSessionKey: null,
371
+ selectedSessionKey: initialQuery.sessionKey || null,
372
+ detailMode: sessionStorage.getItem('pingagent_host_panel_detail_mode') || 'basic',
373
+ showUnreadOnly: false,
336
374
  };
337
375
 
338
376
  function esc(value) {
@@ -356,6 +394,45 @@ function getHostPanelHtml() {
356
394
  try { return new Date(value).toLocaleString(); } catch { return String(value); }
357
395
  }
358
396
 
397
+ function syncUrlState() {
398
+ const url = new URL(window.location.href);
399
+ if (state.selectedProfile) url.searchParams.set('profile', state.selectedProfile);
400
+ else url.searchParams.delete('profile');
401
+ if (state.selectedSessionKey) url.searchParams.set('session_key', state.selectedSessionKey);
402
+ else url.searchParams.delete('session_key');
403
+ url.searchParams.set('view', state.currentTab === 'policy' ? 'policy' : 'runtime');
404
+ history.replaceState(null, '', url.pathname + (url.search ? url.search : ''));
405
+ }
406
+
407
+ function setDetailMode(mode) {
408
+ state.detailMode = mode === 'advanced' ? 'advanced' : 'basic';
409
+ sessionStorage.setItem('pingagent_host_panel_detail_mode', state.detailMode);
410
+ document.getElementById('detailModeBasicBtn').classList.toggle('active', state.detailMode === 'basic');
411
+ document.getElementById('detailModeAdvancedBtn').classList.toggle('active', state.detailMode === 'advanced');
412
+ if (state.overview) renderOverview();
413
+ if (state.session) renderSession();
414
+ }
415
+
416
+ function buildSessionLink(sessionKey) {
417
+ const url = new URL(window.location.href);
418
+ if (state.selectedProfile) url.searchParams.set('profile', state.selectedProfile);
419
+ else url.searchParams.delete('profile');
420
+ if (sessionKey) url.searchParams.set('session_key', sessionKey);
421
+ else url.searchParams.delete('session_key');
422
+ url.searchParams.set('view', 'runtime');
423
+ return url.toString();
424
+ }
425
+
426
+ async function copyText(text, fallbackLabel) {
427
+ try {
428
+ await navigator.clipboard.writeText(text);
429
+ window.alert((fallbackLabel || 'Copied') + ':
430
+ ' + text);
431
+ } catch {
432
+ window.prompt(fallbackLabel || 'Copy', text);
433
+ }
434
+ }
435
+
359
436
  async function api(path, opts) {
360
437
  let url = path;
361
438
  if (state.selectedProfile) {
@@ -405,6 +482,7 @@ function getHostPanelHtml() {
405
482
  document.getElementById('navPolicy').classList.toggle('active', tab === 'policy');
406
483
  document.getElementById('runtimePanel').classList.toggle('active', tab === 'runtime');
407
484
  document.getElementById('policyPanel').classList.toggle('active', tab === 'policy');
485
+ syncUrlState();
408
486
  }
409
487
 
410
488
  function renderHeader() {
@@ -494,9 +572,40 @@ function getHostPanelHtml() {
494
572
  '</div>';
495
573
  }
496
574
 
575
+ function getOverviewSessions() {
576
+ return state.overview && Array.isArray(state.overview.sessions) ? state.overview.sessions : [];
577
+ }
578
+
579
+ function getVisibleSessions() {
580
+ const sessions = getOverviewSessions();
581
+ return state.showUnreadOnly
582
+ ? sessions.filter(function (session) { return Number(session.unread_count || 0) > 0; })
583
+ : sessions;
584
+ }
585
+
586
+ function syncSelectedSessionFromOverview() {
587
+ const allSessions = getOverviewSessions();
588
+ const visibleSessions = getVisibleSessions();
589
+ if (!allSessions.length) {
590
+ state.selectedSessionKey = null;
591
+ state.session = null;
592
+ return;
593
+ }
594
+ const hasSelected = allSessions.some(function (session) { return session.session_key === state.selectedSessionKey; });
595
+ if (!hasSelected) {
596
+ state.selectedSessionKey = visibleSessions.length ? visibleSessions[0].session_key : allSessions[0].session_key;
597
+ return;
598
+ }
599
+ if (state.showUnreadOnly) {
600
+ const stillVisible = visibleSessions.some(function (session) { return session.session_key === state.selectedSessionKey; });
601
+ if (!stillVisible) state.selectedSessionKey = visibleSessions.length ? visibleSessions[0].session_key : null;
602
+ }
603
+ }
604
+
497
605
  function renderOverview() {
498
606
  const overview = state.overview;
499
607
  if (!overview) return;
608
+ syncSelectedSessionFromOverview();
500
609
  const ingressState = ingressStatusModel(overview);
501
610
  document.getElementById('activationCard').innerHTML =
502
611
  '<div class="status-strip">' +
@@ -536,24 +645,27 @@ function getHostPanelHtml() {
536
645
  return '<div class="card"><div class="label">' + esc(item.label) + '</div><div class="value">' + esc(item.value) + '</div><div class="muted small">' + esc(item.sub) + '</div></div>';
537
646
  }).join('');
538
647
 
539
- const sessions = Array.isArray(overview.sessions) ? overview.sessions : [];
648
+ const toggleUnreadBtn = document.getElementById('toggleUnreadBtn');
649
+ if (toggleUnreadBtn) toggleUnreadBtn.textContent = 'Unread only: ' + (state.showUnreadOnly ? 'on' : 'off');
650
+ const sessions = getVisibleSessions();
540
651
  if (!sessions.length) {
541
- document.getElementById('sessionList').innerHTML = '<div class="empty">No sessions yet.</div>';
652
+ document.getElementById('sessionList').innerHTML = '<div class="empty">' + (state.showUnreadOnly ? 'No unread sessions.' : 'No sessions yet.') + '</div>';
542
653
  } else {
543
- if (!state.selectedSessionKey) state.selectedSessionKey = sessions[0].session_key;
544
654
  document.getElementById('sessionList').innerHTML = sessions.map(function (session) {
545
655
  const active = session.session_key === state.selectedSessionKey ? ' active' : '';
546
656
  const badges = [
547
657
  '<span class="badge ' + esc(session.trust_state) + '">' + esc(session.trust_state) + '</span>',
548
658
  session.binding_alert
549
- ? '<button type="button" class="badge alert rebind-badge-btn" data-session="' + esc(session.session_key) + '" data-conversation="' + esc(session.conversation_id || '') + '" data-bound-session="' + esc(session.mapped_work_session || '') + '" data-remote-did="' + esc(session.remote_did || '') + '" title="' + esc(session.binding_alert.message || 'Rebind this conversation to the current chat session') + '">Needs rebind</button>'
659
+ ? '<button type="button" class="badge alert rebind-badge-btn" data-session="' + esc(session.session_key) + '" data-conversation="' + esc(session.conversation_id || '') + '" data-bound-session="' + esc(session.mapped_work_session || '') + '" data-remote-did="' + esc(session.remote_did || '') + '" title="OpenClaw chat link needs attention">Needs reconnect</button>'
550
660
  : '',
551
661
  ].filter(Boolean).join('');
552
662
  return '<div class="session-row' + active + '" data-session="' + esc(session.session_key) + '">' +
553
- '<div class="top"><strong>' + esc(session.remote_did || session.conversation_id || 'unknown') + '</strong><div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">' + badges + '</div></div>' +
554
- '<div class="muted small" style="margin-top:6px">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
663
+ '<div class="top"><strong>' + esc(session.remote_did || session.session_key || 'unknown') + '</strong><div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">' + badges + '</div></div>' +
555
664
  '<div class="muted small">unread=' + esc(session.unread_count) + ' \xB7 last=' + esc(fmtTs(session.last_remote_activity_at || session.updated_at)) + '</div>' +
556
- '<div class="muted small">work_session=' + esc(session.mapped_work_session || '(unbound)') + (session.is_active_work_session ? ' \xB7 active_chat=true' : '') + '</div>' +
665
+ (state.detailMode === 'advanced'
666
+ ? '<div class="muted small" style="margin-top:6px">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
667
+ '<div class="muted small">work_session=' + esc(session.mapped_work_session || '(unbound)') + (session.is_active_work_session ? ' \xB7 active_chat=true' : '') + '</div>'
668
+ : '') +
557
669
  '<div class="muted small" style="margin-top:6px">' + esc(session.last_message_preview || '(no preview)') + '</div>' +
558
670
  '</div>';
559
671
  }).join('');
@@ -678,25 +790,47 @@ conversation=' + result.conversation_id));
678
790
  const bindingAlert = detail.bindingAlert || null;
679
791
  const activeWorkSession = detail.activeWorkSession || null;
680
792
  const summary = detail.sessionSummary || null;
793
+ const isAdvanced = state.detailMode === 'advanced';
794
+ const sessionLink = buildSessionLink(session.session_key);
795
+ const summaryPills = [];
796
+ if (contact && contact.action) summaryPills.push('<span class="pill">contact=' + esc(contact.action) + '</span>');
797
+ if (task && task.action) summaryPills.push('<span class="pill">task=' + esc(task.action) + '</span>');
798
+ if (session.trust_state) summaryPills.push('<span class="pill">trust=' + esc(session.trust_state) + '</span>');
799
+ if (Number(session.unread_count || 0) > 0) summaryPills.push('<span class="pill">unread=' + esc(session.unread_count) + '</span>');
800
+ if (binding && binding.session_key) summaryPills.push('<span class="pill">chat link attached</span>');
801
+ else if (activeWorkSession) summaryPills.push('<span class="pill">current OpenClaw chat available</span>');
802
+ const policyBlock = isAdvanced
803
+ ? '<pre style="margin-top:8px">[Contact]\\naction=' + esc(contact.action) + '\\nsource=' + esc(contact.source) + (contact.matched_rule ? '\\nmatched_rule=' + esc(contact.matched_rule) : '') + '\\n' + esc(contact.explanation) + '\\n\\n[Task]\\naction=' + esc(task.action) + '\\nsource=' + esc(task.source) + (task.matched_rule ? '\\nmatched_rule=' + esc(task.matched_rule) : '') + '\\n' + esc(task.explanation) + '</pre>'
804
+ : '<div class="summary-pills" style="margin-top:8px">' + summaryPills.join('') + '</div>' +
805
+ '<div class="muted small" style="margin-top:10px">' + esc(contact.explanation || '(no contact explanation)') + '</div>' +
806
+ '<div class="muted small" style="margin-top:6px">' + esc(task.explanation || '(no task explanation)') + '</div>';
681
807
 
682
808
  el.innerHTML = '' +
683
809
  '<div class="two-col">' +
684
810
  '<div>' +
685
811
  '<div class="label">Session</div>' +
686
812
  '<div style="margin-top:8px"><strong>' + esc(session.remote_did || '(unknown)') + '</strong></div>' +
687
- '<div class="muted small">session=' + esc(session.session_key) + '</div>' +
688
- '<div class="muted small">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
689
813
  '<div class="muted small">trust=' + esc(session.trust_state) + ' \xB7 unread=' + esc(session.unread_count) + '</div>' +
690
814
  '<div class="muted small">last activity=' + esc(fmtTs(session.last_remote_activity_at || session.updated_at)) + '</div>' +
691
- '<div class="muted small" style="margin-top:8px">active_chat_session=' + esc(activeWorkSession || '(none)') + '</div>' +
692
- '<div class="muted small">binding=' + esc(binding ? binding.session_key : '(unbound)') + '</div>' +
815
+ (isAdvanced
816
+ ? '<div style="margin-top:8px">' +
817
+ '<div class="muted small">session=' + esc(session.session_key) + '</div>' +
818
+ '<div class="muted small">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
819
+ '<div class="muted small">active_chat_session=' + esc(activeWorkSession || '(none)') + '</div>' +
820
+ '<div class="muted small">binding=' + esc(binding ? binding.session_key : '(unbound)') + '</div>' +
821
+ '</div>'
822
+ : '') +
693
823
  (openRecommendation
694
824
  ? '<div class="muted small" style="margin-top:8px">trust_action=' + esc(recommendationActionLabel(openRecommendation)) + '</div>'
695
825
  : (reopenRecommendation ? '<div class="muted small" style="margin-top:8px">trust_action=' + esc(recommendationActionLabel(reopenRecommendation)) + '</div>' : '')) +
826
+ (summaryPills.length ? '<div class="summary-pills">' + summaryPills.join('') + '</div>' : '') +
696
827
  (bindingAlert
697
- ? '<div style="margin-top:10px;padding:10px 12px;border:1px solid #ef4444;border-radius:10px;background:rgba(127,29,29,0.25);color:#fecaca"><strong>Needs rebind</strong><div class="small" style="margin-top:6px">' + esc(bindingAlert.message || 'Bound work session is missing. Rebind this PingAgent conversation to the current chat session.') + '</div></div>'
828
+ ? '<div style="margin-top:10px;padding:10px 12px;border:1px solid #ef4444;border-radius:10px;background:rgba(127,29,29,0.25);color:#fecaca"><strong>Needs reconnect</strong><div class="small" style="margin-top:6px">' + esc(isAdvanced ? (bindingAlert.message || 'Bound work session is missing. Rebind this PingAgent conversation to the current chat session.') : 'OpenClaw chat link is stale. Attach this PingAgent session to the current OpenClaw chat.') + '</div></div>'
698
829
  : '') +
699
830
  '<div class="row-actions">' +
831
+ (session.trust_state === 'pending'
832
+ ? '<button class="action-btn approve-session-btn" data-session="' + esc(session.session_key) + '">Approve Contact</button>'
833
+ : '') +
700
834
  (openRecommendation
701
835
  ? '<button class="action-btn apply-session-recommendation-btn" data-session="' + esc(session.session_key) + '">' + esc(recommendationActionLabel(openRecommendation)) + '</button>'
702
836
  : '') +
@@ -706,13 +840,24 @@ conversation=' + result.conversation_id));
706
840
  (!openRecommendation && reopenRecommendation
707
841
  ? '<button class="secondary-btn reopen-session-recommendation-btn" data-session="' + esc(session.session_key) + '">Reopen</button>'
708
842
  : '') +
709
- '<button class="action-btn bind-current-btn" data-conversation="' + esc(session.conversation_id || '') + '">Bind Current Chat</button>' +
710
- '<button class="danger-btn clear-binding-btn" data-conversation="' + esc(session.conversation_id || '') + '">Clear Binding</button>' +
843
+ '<button class="action-btn bind-current-btn" data-conversation="' + esc(session.conversation_id || '') + '">Attach to Current Chat</button>' +
844
+ '<button class="secondary-btn mark-read-btn" data-session="' + esc(session.session_key) + '">Mark read</button>' +
845
+ '<button class="secondary-btn copy-session-link-btn" data-session="' + esc(session.session_key) + '">Copy Session Link</button>' +
846
+ '<button class="danger-btn clear-binding-btn" data-conversation="' + esc(session.conversation_id || '') + '">Detach Chat Link</button>' +
847
+ '</div>' +
848
+ '<div class="form-grid" style="margin-top:16px">' +
849
+ '<label class="label">Reply in this session</label>' +
850
+ '<textarea id="sessionReplyInput" placeholder="Send a text reply in this session"></textarea>' +
851
+ '<div class="row-actions"><button class="action-btn" id="sendSessionReplyBtn">Send Reply</button></div>' +
711
852
  '</div>' +
712
853
  '</div>' +
713
854
  '<div>' +
714
855
  '<div class="label">Policy Decisions</div>' +
715
- '<pre style="margin-top:8px">[Contact]\\naction=' + esc(contact.action) + '\\nsource=' + esc(contact.source) + (contact.matched_rule ? '\\nmatched_rule=' + esc(contact.matched_rule) : '') + '\\n' + esc(contact.explanation) + '\\n\\n[Task]\\naction=' + esc(task.action) + '\\nsource=' + esc(task.source) + (task.matched_rule ? '\\nmatched_rule=' + esc(task.matched_rule) : '') + '\\n' + esc(task.explanation) + '</pre>' +
856
+ policyBlock +
857
+ (isAdvanced && recommendations.length
858
+ ? '<pre style="margin-top:12px">recommendations_debug=' + esc(JSON.stringify(recommendations.map(function (item) { return { id: item.id, status: item.status, policy: item.policy, action: item.action, current_action: item.current_action, match: item.match, confidence: item.confidence }; }), null, 2)) + '</pre>'
859
+ : '') +
860
+ (isAdvanced ? '<div class="muted small" style="margin-top:10px">permalink=' + esc(sessionLink) + '</div>' : '') +
716
861
  '</div>' +
717
862
  '</div>' +
718
863
  '<div class="grid two-col" style="margin-top:16px">' +
@@ -730,9 +875,7 @@ conversation=' + result.conversation_id));
730
875
  '<textarea id="sessionSummaryOpenQuestions" placeholder="Open questions">' + esc(summary && summary.open_questions ? summary.open_questions : '') + '</textarea>' +
731
876
  '<textarea id="sessionSummaryNextAction" placeholder="Next action">' + esc(summary && summary.next_action ? summary.next_action : '') + '</textarea>' +
732
877
  '<textarea id="sessionSummaryHandoff" placeholder="Handoff-ready summary">' + esc(summary && summary.handoff_ready_text ? summary.handoff_ready_text : '') + '</textarea>' +
733
- '<div class="row-actions">' +
734
- '<button class="action-btn" id="saveSessionSummaryBtn">Save Summary</button>' +
735
- '</div>' +
878
+ '<div class="row-actions"><button class="action-btn" id="saveSessionSummaryBtn">Save Summary</button></div>' +
736
879
  '</div>' +
737
880
  '</div>' +
738
881
  '</div>' +
@@ -759,8 +902,8 @@ conversation=' + result.conversation_id));
759
902
  ? '<button class="secondary-btn reopen-session-recommendation-btn" data-session="' + esc(session.session_key) + '">Reopen</button>'
760
903
  : '';
761
904
  return '<div class="recommendation-row"><div class="top"><strong>' + esc(item.policy) + '</strong><span class="badge">' + esc(item.status + ' \xB7 ' + item.action) + '</span></div>' +
762
- '<div class="muted small">current=' + esc(item.current_action) + ' \xB7 confidence=' + esc(item.confidence) + '</div>' +
763
- '<div class="muted small">match=' + esc(item.match) + '</div>' +
905
+ (isAdvanced ? '<div class="muted small">current=' + esc(item.current_action) + ' \xB7 confidence=' + esc(item.confidence) + '</div>' : '') +
906
+ (isAdvanced ? '<div class="muted small">match=' + esc(item.match) + '</div>' : '') +
764
907
  '<div style="margin-top:8px">' + esc(item.reason) + '</div>' +
765
908
  '<div class="row-actions">' + actionButton + dismissButton + reopenButton + '</div>' +
766
909
  '</div>';
@@ -770,10 +913,10 @@ conversation=' + result.conversation_id));
770
913
  '<div class="grid two-col" style="margin-top:16px">' +
771
914
  '<div><div class="label">Recent Messages</div><div class="message-list" style="margin-top:8px">' +
772
915
  (messages.length ? messages.map(function (msg) {
773
- const summary = msg.schema === 'pingagent.text@1' && msg.payload && msg.payload.text
916
+ const messageSummary = msg.schema === 'pingagent.text@1' && msg.payload && msg.payload.text
774
917
  ? msg.payload.text
775
918
  : JSON.stringify(msg.payload || {});
776
- return '<div class="message-row"><div class="muted small">' + esc(fmtTs(msg.ts_ms)) + ' \xB7 ' + esc(msg.direction) + ' \xB7 ' + esc(msg.schema) + '</div><div style="margin-top:8px">' + esc(summary) + '</div></div>';
919
+ return '<div class="message-row"><div class="muted small">' + esc(fmtTs(msg.ts_ms)) + ' \xB7 ' + esc(msg.direction) + ' \xB7 ' + esc(msg.schema) + '</div><div style="margin-top:8px">' + esc(messageSummary) + '</div></div>';
777
920
  }).join('') : '<div class="empty">No local message history yet.</div>') +
778
921
  '</div></div>' +
779
922
  '<div><div class="label">Policy Audit</div><div class="audit-list" style="margin-top:8px">' +
@@ -786,6 +929,24 @@ conversation=' + result.conversation_id));
786
929
  '</div></div>' +
787
930
  '</div>';
788
931
 
932
+ el.querySelectorAll('.approve-session-btn').forEach(function (btn) {
933
+ btn.addEventListener('click', async function () {
934
+ const sessionKey = btn.getAttribute('data-session');
935
+ const result = await api('/api/runtime/session/approve', {
936
+ method: 'POST',
937
+ headers: { 'Content-Type': 'application/json' },
938
+ body: JSON.stringify({ session_key: sessionKey }),
939
+ });
940
+ await refreshAll();
941
+ const promoted = result && result.dm_conversation_id
942
+ ? getOverviewSessions().find(function (item) { return item.conversation_id === result.dm_conversation_id; })
943
+ : null;
944
+ state.selectedSessionKey = promoted ? promoted.session_key : sessionKey;
945
+ renderOverview();
946
+ if (state.selectedSessionKey) await loadSession(state.selectedSessionKey);
947
+ setTab('runtime');
948
+ });
949
+ });
789
950
  el.querySelectorAll('.bind-current-btn').forEach(function (btn) {
790
951
  btn.addEventListener('click', async function () {
791
952
  const conversationId = btn.getAttribute('data-conversation');
@@ -793,6 +954,22 @@ conversation=' + result.conversation_id));
793
954
  await promptBindCurrentChat(conversationId);
794
955
  });
795
956
  });
957
+ el.querySelectorAll('.mark-read-btn').forEach(function (btn) {
958
+ btn.addEventListener('click', async function () {
959
+ await api('/api/runtime/session/mark-read', {
960
+ method: 'POST',
961
+ headers: { 'Content-Type': 'application/json' },
962
+ body: JSON.stringify({ session_key: btn.getAttribute('data-session') }),
963
+ });
964
+ await refreshAll();
965
+ setTab('runtime');
966
+ });
967
+ });
968
+ el.querySelectorAll('.copy-session-link-btn').forEach(function (btn) {
969
+ btn.addEventListener('click', async function () {
970
+ await copyText(buildSessionLink(btn.getAttribute('data-session')), 'Session link');
971
+ });
972
+ });
796
973
  el.querySelectorAll('.clear-binding-btn').forEach(function (btn) {
797
974
  btn.addEventListener('click', async function () {
798
975
  await api('/api/runtime/session-bindings/clear', {
@@ -837,23 +1014,42 @@ conversation=' + result.conversation_id));
837
1014
  setTab('runtime');
838
1015
  });
839
1016
  });
1017
+ const sendSessionReplyBtn = document.getElementById('sendSessionReplyBtn');
1018
+ if (sendSessionReplyBtn) {
1019
+ sendSessionReplyBtn.addEventListener('click', async function () {
1020
+ const input = document.getElementById('sessionReplyInput');
1021
+ const message = input.value.trim();
1022
+ if (!message) {
1023
+ window.alert('Reply text is required.');
1024
+ return;
1025
+ }
1026
+ await api('/api/runtime/session/reply', {
1027
+ method: 'POST',
1028
+ headers: { 'Content-Type': 'application/json' },
1029
+ body: JSON.stringify({ session_key: session.session_key, message: message }),
1030
+ });
1031
+ input.value = '';
1032
+ await refreshAll();
1033
+ setTab('runtime');
1034
+ });
1035
+ }
840
1036
  const saveSessionSummaryBtn = document.getElementById('saveSessionSummaryBtn');
841
1037
  if (saveSessionSummaryBtn) {
842
1038
  saveSessionSummaryBtn.addEventListener('click', async function () {
843
1039
  await api('/api/runtime/session-summary', {
844
1040
  method: 'POST',
845
1041
  headers: { 'Content-Type': 'application/json' },
846
- body: JSON.stringify({
847
- session_key: session.session_key,
848
- objective: document.getElementById('sessionSummaryObjective').value.trim(),
849
- context: document.getElementById('sessionSummaryContext').value.trim(),
850
- constraints: document.getElementById('sessionSummaryConstraints').value.trim(),
851
- decisions: document.getElementById('sessionSummaryDecisions').value.trim(),
852
- open_questions: document.getElementById('sessionSummaryOpenQuestions').value.trim(),
853
- next_action: document.getElementById('sessionSummaryNextAction').value.trim(),
854
- handoff_ready_text: document.getElementById('sessionSummaryHandoff').value.trim(),
855
- }),
856
- });
1042
+ body: JSON.stringify({
1043
+ session_key: session.session_key,
1044
+ objective: document.getElementById('sessionSummaryObjective').value.trim(),
1045
+ context: document.getElementById('sessionSummaryContext').value.trim(),
1046
+ constraints: document.getElementById('sessionSummaryConstraints').value.trim(),
1047
+ decisions: document.getElementById('sessionSummaryDecisions').value.trim(),
1048
+ open_questions: document.getElementById('sessionSummaryOpenQuestions').value.trim(),
1049
+ next_action: document.getElementById('sessionSummaryNextAction').value.trim(),
1050
+ handoff_ready_text: document.getElementById('sessionSummaryHandoff').value.trim(),
1051
+ }),
1052
+ });
857
1053
  await refreshAll();
858
1054
  setTab('runtime');
859
1055
  });
@@ -867,7 +1063,7 @@ conversation=' + result.conversation_id));
867
1063
  const previous = previousBinding || (state.session && state.session.binding ? state.session.binding.session_key : null) || '(unbound)';
868
1064
  const targetRemoteDid = remoteDid || (state.session && state.session.session ? state.session.session.remote_did : null) || '(unknown)';
869
1065
  const confirmed = window.confirm(
870
- 'Rebind this PingAgent conversation to the current chat session?' +
1066
+ 'Attach this PingAgent session to the current OpenClaw chat?' +
871
1067
  '
872
1068
 
873
1069
  Conversation: ' + conversationId +
@@ -875,9 +1071,9 @@ Conversation: ' + conversationId +
875
1071
  Remote DID: ' + targetRemoteDid +
876
1072
  '
877
1073
 
878
- Current chat: ' + (current || '(none)') +
1074
+ Current OpenClaw chat: ' + (current || '(none)') +
879
1075
  '
880
- Previous binding: ' + previous
1076
+ Previous chat link: ' + previous
881
1077
  );
882
1078
  if (!confirmed) return;
883
1079
  await api('/api/runtime/session-bindings/bind-current', {
@@ -1061,14 +1257,15 @@ Previous binding: ' + previous
1061
1257
 
1062
1258
  async function loadOverview() {
1063
1259
  state.overview = await api('/api/runtime/overview');
1260
+ syncSelectedSessionFromOverview();
1064
1261
  renderHeader();
1065
1262
  renderOverview();
1066
- const sessions = state.overview && Array.isArray(state.overview.sessions) ? state.overview.sessions : [];
1067
- if (!state.selectedSessionKey && sessions.length) {
1068
- state.selectedSessionKey = sessions[0].session_key;
1069
- }
1070
1263
  if (state.selectedSessionKey) {
1071
1264
  await loadSession(state.selectedSessionKey);
1265
+ } else {
1266
+ state.session = null;
1267
+ renderSession();
1268
+ syncUrlState();
1072
1269
  }
1073
1270
  }
1074
1271
 
@@ -1076,6 +1273,7 @@ Previous binding: ' + previous
1076
1273
  if (!sessionKey) return;
1077
1274
  state.selectedSessionKey = sessionKey;
1078
1275
  state.session = await api('/api/runtime/session?session_key=' + encodeURIComponent(sessionKey));
1276
+ syncUrlState();
1079
1277
  renderSession();
1080
1278
  }
1081
1279
 
@@ -1096,6 +1294,30 @@ Previous binding: ' + previous
1096
1294
 
1097
1295
  document.getElementById('navRuntime').addEventListener('click', function () { setTab('runtime'); });
1098
1296
  document.getElementById('navPolicy').addEventListener('click', function () { setTab('policy'); });
1297
+ document.getElementById('toggleUnreadBtn').addEventListener('click', async function () {
1298
+ state.showUnreadOnly = !state.showUnreadOnly;
1299
+ syncSelectedSessionFromOverview();
1300
+ renderOverview();
1301
+ if (state.selectedSessionKey) await loadSession(state.selectedSessionKey);
1302
+ else {
1303
+ syncUrlState();
1304
+ renderSession();
1305
+ }
1306
+ });
1307
+ document.getElementById('nextUnreadBtn').addEventListener('click', async function () {
1308
+ const unreadSessions = getOverviewSessions().filter(function (session) { return Number(session.unread_count || 0) > 0; });
1309
+ if (!unreadSessions.length) {
1310
+ window.alert('No unread sessions.');
1311
+ return;
1312
+ }
1313
+ const currentIndex = unreadSessions.findIndex(function (session) { return session.session_key === state.selectedSessionKey; });
1314
+ const next = unreadSessions[(currentIndex + 1 + unreadSessions.length) % unreadSessions.length];
1315
+ state.selectedSessionKey = next.session_key;
1316
+ renderOverview();
1317
+ await loadSession(state.selectedSessionKey);
1318
+ });
1319
+ document.getElementById('detailModeBasicBtn').addEventListener('click', function () { setDetailMode('basic'); });
1320
+ document.getElementById('detailModeAdvancedBtn').addEventListener('click', function () { setDetailMode('advanced'); });
1099
1321
  document.getElementById('rulePolicy').addEventListener('change', updateRuleActionOptions);
1100
1322
  document.getElementById('saveDefaultsBtn').addEventListener('click', async function () {
1101
1323
  await api('/api/runtime/policy/defaults', {
@@ -1178,9 +1400,12 @@ Previous binding: ' + previous
1178
1400
  });
1179
1401
 
1180
1402
  async function init() {
1403
+ setTab(state.currentTab);
1404
+ setDetailMode(state.detailMode);
1181
1405
  await loadProfiles();
1182
1406
  updateRuleActionOptions();
1183
1407
  await refreshAll();
1408
+ syncUrlState();
1184
1409
  }
1185
1410
 
1186
1411
  init().catch(function (error) {
@@ -1679,6 +1904,18 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
1679
1904
  }))
1680
1905
  };
1681
1906
  }
1907
+ function resolveSessionForInput(sessionManager, input) {
1908
+ if (!sessionManager) return null;
1909
+ const sessionKey = String(input.session_key ?? "").trim();
1910
+ const conversationId = String(input.conversation_id ?? "").trim();
1911
+ const remoteDid = String(input.remote_did ?? "").trim();
1912
+ let session = sessionKey ? sessionManager.get(sessionKey) : null;
1913
+ if (!session && conversationId) session = sessionManager.getByConversationId(conversationId);
1914
+ if (!session && remoteDid) {
1915
+ session = sessionManager.listRecentSessions(100).find((item) => item.remote_did === remoteDid) ?? null;
1916
+ }
1917
+ return session ?? sessionManager.getActiveSession() ?? sessionManager.listRecentSessions(1)[0] ?? null;
1918
+ }
1682
1919
  async function handleApi(pathname, req, ctx) {
1683
1920
  const client = ctx.client;
1684
1921
  const contactManager = ctx.contactManager;
@@ -1780,6 +2017,48 @@ async function handleApi(pathname, req, ctx) {
1780
2017
  };
1781
2018
  }
1782
2019
  if (parts[1] === "session") {
2020
+ const sessionManager = ctx.client.getSessionManager();
2021
+ if (!sessionManager) throw new Error("Session actions require a writable local store");
2022
+ if (parts[2] === "reply" && req.method === "POST") {
2023
+ const body = await readBody(req);
2024
+ const session = resolveSessionForInput(sessionManager, body);
2025
+ if (!session?.conversation_id) throw new Error("No session selected");
2026
+ const text = String(body?.message ?? "").trim();
2027
+ if (!text) throw new Error("Missing message");
2028
+ const sendRes = await client.sendMessage(session.conversation_id, SCHEMA_TEXT, { text });
2029
+ if (!sendRes.ok) throw new Error(sendRes.error?.message ?? "Failed to send");
2030
+ sessionManager.focusSession(session.session_key);
2031
+ return {
2032
+ ok: true,
2033
+ session: sessionManager.get(session.session_key),
2034
+ message_id: sendRes.data?.message_id ?? null
2035
+ };
2036
+ }
2037
+ if (parts[2] === "approve" && req.method === "POST") {
2038
+ const body = await readBody(req);
2039
+ const session = resolveSessionForInput(sessionManager, body);
2040
+ if (!session?.conversation_id) throw new Error("No session selected");
2041
+ const approveRes = await client.approveContact(session.conversation_id);
2042
+ if (!approveRes.ok) throw new Error(approveRes.error?.message ?? "Failed to approve contact");
2043
+ sessionManager.focusSession(session.session_key);
2044
+ return {
2045
+ ok: true,
2046
+ session: sessionManager.get(session.session_key),
2047
+ trusted: approveRes.data?.trusted ?? true,
2048
+ dm_conversation_id: approveRes.data?.dm_conversation_id ?? session.conversation_id
2049
+ };
2050
+ }
2051
+ if (parts[2] === "mark-read" && req.method === "POST") {
2052
+ const body = await readBody(req);
2053
+ const session = resolveSessionForInput(sessionManager, body);
2054
+ if (!session?.session_key) throw new Error("No session selected");
2055
+ const updated = sessionManager.markRead(session.session_key);
2056
+ if (!updated) throw new Error("Failed to mark session as read");
2057
+ return {
2058
+ ok: true,
2059
+ session: updated
2060
+ };
2061
+ }
1783
2062
  const url = new URL(req.url || "", "http://x");
1784
2063
  const sessionKey = url.searchParams.get("session_key");
1785
2064
  return buildSessionOverviewPayload(ctx, sessionKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pingagent/sdk",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -35,8 +35,8 @@
35
35
  "uuid": "^11.0.0",
36
36
  "ws": "^8.0.0",
37
37
  "@pingagent/protocol": "0.1.1",
38
- "@pingagent/schemas": "0.1.2",
39
- "@pingagent/a2a": "0.1.1"
38
+ "@pingagent/a2a": "0.1.1",
39
+ "@pingagent/schemas": "0.1.2"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/better-sqlite3": "^7.6.0",