@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 +179 -32
- package/dist/web-server.js +323 -44
- package/package.json +3 -3
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
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}${
|
|
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'}
|
|
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
|
-
`
|
|
419
|
-
`
|
|
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:
|
|
447
|
-
lines.push('- c:
|
|
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(`
|
|
547
|
-
lines.push(`
|
|
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(
|
|
593
|
+
lines.push('needs_reconnect=true');
|
|
550
594
|
lines.push(`warning=${selected.binding_alert.message}`);
|
|
551
595
|
} else {
|
|
552
|
-
lines.push('
|
|
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]
|
|
577
|
-
'[c]
|
|
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
|
|
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 || '(
|
|
1160
|
-
const confirmed = await confirmAction(`
|
|
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('
|
|
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(`
|
|
1212
|
+
setStatus(`Attached chat link ${selected.conversation_id} -> ${latestState.activeChatSession}`, 'ok');
|
|
1169
1213
|
} else {
|
|
1170
|
-
setStatus('
|
|
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(`
|
|
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('
|
|
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
|
|
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
|
|
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;
|
package/dist/web-server.js
CHANGED
|
@@ -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
|
-
<
|
|
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:
|
|
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
|
|
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
|
|
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="
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
692
|
-
|
|
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
|
|
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 || '') + '">
|
|
710
|
-
'<button class="
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
'
|
|
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
|
|
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.
|
|
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/
|
|
39
|
-
"@pingagent/
|
|
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",
|