pi-roundtable-web 0.7.18 → 0.8.0
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/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/dist/{chunk-mr32cg74.js → chunk-2cm6wak5.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +3 -3
- package/src/api-types.ts +13 -3
- package/src/console-api.ts +54 -19
- package/src/conversations.ts +49 -5
- package/src/web-plugin.ts +4 -0
package/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>Console</title>
|
|
7
7
|
|
|
8
|
-
<link rel="stylesheet" crossorigin href="./chunk-e8z9q554.css"><script type="module" crossorigin src="./chunk-
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./chunk-e8z9q554.css"><script type="module" crossorigin src="./chunk-2cm6wak5.js"></script></head>
|
|
9
9
|
<body>
|
|
10
10
|
<div id="root"></div>
|
|
11
11
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-roundtable-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "An owner-only web console for pi-roundtable: conversations, transcripts, and memory notes, with live updates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,14 +40,14 @@
|
|
|
40
40
|
"jose": "6.2.12"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"pi-roundtable": ">=0.
|
|
43
|
+
"pi-roundtable": ">=0.8.0 <0.9.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@biomejs/biome": "2.5.15",
|
|
47
47
|
"@types/bun": "1.4.2",
|
|
48
48
|
"@types/react": "19.3.0",
|
|
49
49
|
"@types/react-dom": "19.3.0",
|
|
50
|
-
"pi-roundtable": "0.
|
|
50
|
+
"pi-roundtable": "0.8.0",
|
|
51
51
|
"react": "19.3.0",
|
|
52
52
|
"react-dom": "19.3.0",
|
|
53
53
|
"react-markdown": "10.1.0",
|
package/src/api-types.ts
CHANGED
|
@@ -80,8 +80,16 @@ export interface DashboardView {
|
|
|
80
80
|
party: PartyView[];
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Who a stored conversation belongs to; `plugin` is one a plugin runs through `context.turns`,
|
|
85
|
+
* such as a web chat, as the host's registry records it.
|
|
86
|
+
*/
|
|
87
|
+
export type ConversationKind =
|
|
88
|
+
| "agent"
|
|
89
|
+
| "group"
|
|
90
|
+
| "owner"
|
|
91
|
+
| "outside"
|
|
92
|
+
| "plugin";
|
|
85
93
|
|
|
86
94
|
export interface ConversationView {
|
|
87
95
|
/** The channel key: `discord:<id>`, `agentgroup:<channel>.<agent>`, or `mcp:<session>`. */
|
|
@@ -97,9 +105,11 @@ export interface ConversationView {
|
|
|
97
105
|
liveBytes: number;
|
|
98
106
|
archives: number;
|
|
99
107
|
lastActive?: string;
|
|
100
|
-
/** Outside-agent conversations only: up to 80 characters of the first message. */
|
|
108
|
+
/** Outside-agent and plugin conversations only: up to 80 characters of the first message. */
|
|
101
109
|
firstMessage?: string;
|
|
102
110
|
startedAt?: string;
|
|
111
|
+
/** Plugin conversations only: the name the registry gives it. */
|
|
112
|
+
title?: string;
|
|
103
113
|
/** Turns running or waiting in the channel. */
|
|
104
114
|
busy: number;
|
|
105
115
|
}
|
package/src/console-api.ts
CHANGED
|
@@ -2,12 +2,13 @@ import { join } from "node:path";
|
|
|
2
2
|
import type {
|
|
3
3
|
AgentTeam,
|
|
4
4
|
ChannelKey,
|
|
5
|
+
ConversationRegistry,
|
|
5
6
|
Logger,
|
|
6
7
|
MemoryKind,
|
|
7
8
|
QueuePort,
|
|
8
9
|
SpeakerMemory,
|
|
9
10
|
} from "pi-roundtable";
|
|
10
|
-
import { MemoryError } from "pi-roundtable";
|
|
11
|
+
import { MemoryError, parseChannelKey } from "pi-roundtable";
|
|
11
12
|
import { thinkingLabel } from "pi-roundtable/kit";
|
|
12
13
|
import type {
|
|
13
14
|
ApiError,
|
|
@@ -28,10 +29,12 @@ import {
|
|
|
28
29
|
conversationFiles,
|
|
29
30
|
parseKey,
|
|
30
31
|
readTranscript,
|
|
32
|
+
registeredConversations,
|
|
33
|
+
registeredDir,
|
|
31
34
|
type StoredConversation,
|
|
35
|
+
type StoredKind,
|
|
32
36
|
storedConversations,
|
|
33
37
|
} from "./conversations.ts";
|
|
34
|
-
|
|
35
38
|
import {
|
|
36
39
|
type ConsoleFeatures,
|
|
37
40
|
type ConsolePresentation,
|
|
@@ -61,6 +64,8 @@ export interface ConsolePorts {
|
|
|
61
64
|
>;
|
|
62
65
|
/** Conversations the console must not list or read. */
|
|
63
66
|
exclude?: (key: ChannelKey) => boolean;
|
|
67
|
+
/** The host's conversation registry; without it only the conversations found by name are listed. */
|
|
68
|
+
registry?: Pick<ConversationRegistry, "list" | "get">;
|
|
64
69
|
/** Text a relayed message begins with, which is not the owner's words. */
|
|
65
70
|
relayNotes: readonly string[];
|
|
66
71
|
/** Called after a note is written, so every open page refreshes. */
|
|
@@ -272,29 +277,56 @@ export class ConsoleApi {
|
|
|
272
277
|
// ── Conversations ──────────────────────────────────────────────────────
|
|
273
278
|
|
|
274
279
|
async conversations(): Promise<ConversationsView> {
|
|
275
|
-
const { sessionsDir, relayNotes,
|
|
276
|
-
const
|
|
277
|
-
excluded: (key) =>
|
|
278
|
-
(exclude?.(key) ?? false) ||
|
|
279
|
-
(this.#ports.features?.party?.contains(key) ?? false),
|
|
280
|
+
const { sessionsDir, relayNotes, registry } = this.#ports;
|
|
281
|
+
const filter = {
|
|
282
|
+
excluded: (key: ChannelKey) => this.#hidden(key),
|
|
280
283
|
relayNotes,
|
|
281
|
-
}
|
|
284
|
+
};
|
|
285
|
+
// The registry first; the directory scan reads the conversations from before it, by name.
|
|
286
|
+
const recorded = registry
|
|
287
|
+
? registeredConversations(sessionsDir, await registry.list(), filter)
|
|
288
|
+
: [];
|
|
289
|
+
const stored = [
|
|
290
|
+
...recorded,
|
|
291
|
+
...storedConversations(sessionsDir, filter),
|
|
292
|
+
].sort((a, b) => (b.lastActive ?? "").localeCompare(a.lastActive ?? ""));
|
|
282
293
|
return {
|
|
283
294
|
conversations: await Promise.all(stored.map((c) => this.#view(c))),
|
|
284
295
|
};
|
|
285
296
|
}
|
|
286
297
|
|
|
298
|
+
#hidden(key: ChannelKey): boolean {
|
|
299
|
+
return (
|
|
300
|
+
(this.#ports.exclude?.(key) ?? false) ||
|
|
301
|
+
(this.#ports.features?.party?.contains(key) ?? false)
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The session directory and identity of a conversation the console may read, or undefined. */
|
|
306
|
+
async #located(
|
|
307
|
+
key: string,
|
|
308
|
+
): Promise<
|
|
309
|
+
{ dir: string; kind: StoredKind; id: string; member?: string } | undefined
|
|
310
|
+
> {
|
|
311
|
+
const parsed = parseKey(key);
|
|
312
|
+
if (parsed) return parsed;
|
|
313
|
+
const record = await this.#ports.registry?.get(key as ChannelKey);
|
|
314
|
+
return record
|
|
315
|
+
? {
|
|
316
|
+
dir: registeredDir(record.key),
|
|
317
|
+
kind: "registered",
|
|
318
|
+
id: parseChannelKey(record.key).id,
|
|
319
|
+
}
|
|
320
|
+
: undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
287
323
|
async transcript(
|
|
288
324
|
key: string,
|
|
289
325
|
archive: string | undefined,
|
|
290
326
|
): Promise<TranscriptView> {
|
|
291
|
-
const { sessionsDir, relayNotes
|
|
292
|
-
const parsed =
|
|
293
|
-
if (
|
|
294
|
-
!parsed ||
|
|
295
|
-
exclude?.(key as ChannelKey) ||
|
|
296
|
-
this.#ports.features?.party?.contains(key as ChannelKey)
|
|
297
|
-
)
|
|
327
|
+
const { sessionsDir, relayNotes } = this.#ports;
|
|
328
|
+
const parsed = await this.#located(key);
|
|
329
|
+
if (!parsed || this.#hidden(key as ChannelKey))
|
|
298
330
|
throw new HttpError(404, "There is no such conversation.");
|
|
299
331
|
const dir = join(sessionsDir, parsed.dir);
|
|
300
332
|
const files = conversationFiles(dir);
|
|
@@ -324,17 +356,20 @@ export class ConsoleApi {
|
|
|
324
356
|
const kind: ConversationKind =
|
|
325
357
|
stored.kind === "mcp"
|
|
326
358
|
? "outside"
|
|
327
|
-
: stored.kind === "
|
|
328
|
-
? "
|
|
329
|
-
:
|
|
359
|
+
: stored.kind === "registered"
|
|
360
|
+
? "plugin"
|
|
361
|
+
: stored.kind === "group"
|
|
362
|
+
? "group"
|
|
363
|
+
: (team?.owns(stored.key) ?? "owner");
|
|
330
364
|
return {
|
|
331
365
|
key: stored.key,
|
|
332
366
|
kind,
|
|
333
367
|
id: stored.id,
|
|
334
368
|
...(stored.member ? { member: stored.member } : {}),
|
|
335
|
-
...(stored.kind === "mcp"
|
|
369
|
+
...(stored.kind === "mcp" || stored.kind === "registered"
|
|
336
370
|
? {}
|
|
337
371
|
: { channel: await this.#name(stored.id) }),
|
|
372
|
+
...(stored.title === undefined ? {} : { title: stored.title }),
|
|
338
373
|
liveBytes: stored.liveBytes,
|
|
339
374
|
archives: stored.archives,
|
|
340
375
|
...(stored.lastActive ? { lastActive: stored.lastActive } : {}),
|
package/src/conversations.ts
CHANGED
|
@@ -9,8 +9,12 @@ import {
|
|
|
9
9
|
statSync,
|
|
10
10
|
} from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
type ChannelKey,
|
|
14
|
+
type ConversationRecord,
|
|
15
|
+
parseChannelKey,
|
|
16
|
+
} from "pi-roundtable";
|
|
17
|
+
import { channelSegment, textOf } from "pi-roundtable/kit";
|
|
14
18
|
import type { TranscriptEntry } from "./api-types.ts";
|
|
15
19
|
|
|
16
20
|
const FIRST_MESSAGE_CHARS = 80;
|
|
@@ -41,8 +45,11 @@ export interface ConversationFiles {
|
|
|
41
45
|
lastActive?: string;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The kinds of session directory the console reads: the three it recognises by name, and any
|
|
50
|
+
* other conversation the host's registry records.
|
|
51
|
+
*/
|
|
52
|
+
export type StoredKind = "discord" | "group" | "mcp" | "registered";
|
|
46
53
|
|
|
47
54
|
/** An owner, agent, group-member, or outside-agent conversation found on disk. */
|
|
48
55
|
export interface StoredConversation extends ConversationFiles {
|
|
@@ -52,9 +59,11 @@ export interface StoredConversation extends ConversationFiles {
|
|
|
52
59
|
id: string;
|
|
53
60
|
/** Group conversations only: the agent whose conversation inside the group this is. */
|
|
54
61
|
member?: string;
|
|
55
|
-
/** Outside-agent conversations only. */
|
|
62
|
+
/** Outside-agent and registered conversations only. */
|
|
56
63
|
firstMessage?: string;
|
|
57
64
|
startedAt?: string;
|
|
65
|
+
/** Registered conversations only: the name the registry gives it. */
|
|
66
|
+
title?: string;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
/** The session directory a channel key names, or undefined when the key is not one the console reads. */
|
|
@@ -253,6 +262,41 @@ export function storedConversations(
|
|
|
253
262
|
);
|
|
254
263
|
}
|
|
255
264
|
|
|
265
|
+
/** The session directory of a conversation the registry records, by its key. */
|
|
266
|
+
export function registeredDir(key: ChannelKey): string {
|
|
267
|
+
return channelSegment(key);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The conversations the host's registry records and the directory scan does not recognise, with
|
|
272
|
+
* their files; one whose first turn left no session file has nothing to read and is left out.
|
|
273
|
+
*/
|
|
274
|
+
export function registeredConversations(
|
|
275
|
+
sessionsDir: string,
|
|
276
|
+
records: readonly ConversationRecord[],
|
|
277
|
+
options: {
|
|
278
|
+
excluded: (key: ChannelKey) => boolean;
|
|
279
|
+
relayNotes: readonly string[];
|
|
280
|
+
},
|
|
281
|
+
): StoredConversation[] {
|
|
282
|
+
const found: StoredConversation[] = [];
|
|
283
|
+
for (const record of records) {
|
|
284
|
+
if (parseKey(record.key) || options.excluded(record.key)) continue;
|
|
285
|
+
const dir = join(sessionsDir, registeredDir(record.key));
|
|
286
|
+
const files = conversationFiles(dir);
|
|
287
|
+
if (!files) continue;
|
|
288
|
+
found.push({
|
|
289
|
+
key: record.key,
|
|
290
|
+
kind: "registered",
|
|
291
|
+
id: parseChannelKey(record.key).id,
|
|
292
|
+
...files,
|
|
293
|
+
...opening(dir, options.relayNotes),
|
|
294
|
+
...(record.title === undefined ? {} : { title: record.title }),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return found;
|
|
298
|
+
}
|
|
299
|
+
|
|
256
300
|
/** The archive folders of a conversation, newest first. */
|
|
257
301
|
export function archiveNames(dir: string): string[] {
|
|
258
302
|
const archiveDir = join(dir, "archive");
|
package/src/web-plugin.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AGENTS,
|
|
3
|
+
CONVERSATIONS,
|
|
3
4
|
definePlugin,
|
|
4
5
|
MEMORY,
|
|
5
6
|
type RoundtablePlugin,
|
|
@@ -45,6 +46,8 @@ async function build(settings: ResolvedOptions, context: Context) {
|
|
|
45
46
|
}
|
|
46
47
|
const team = services.find(AGENTS)?.team;
|
|
47
48
|
const connection = services.find(DISCORD)?.connection;
|
|
49
|
+
// Absent on a host without the registry: the console then lists conversations found by name only.
|
|
50
|
+
const registry = services.find(CONVERSATIONS);
|
|
48
51
|
const listeners: (() => void)[] = [];
|
|
49
52
|
const changed = () => {
|
|
50
53
|
for (const listener of listeners) listener();
|
|
@@ -65,6 +68,7 @@ async function build(settings: ResolvedOptions, context: Context) {
|
|
|
65
68
|
? { memory: services.get(MEMORY).forSpeaker(settings.ownerId) }
|
|
66
69
|
: {}),
|
|
67
70
|
...(settings.exclude ? { exclude: settings.exclude } : {}),
|
|
71
|
+
...(registry ? { registry } : {}),
|
|
68
72
|
relayNotes: settings.relayNotes,
|
|
69
73
|
changed,
|
|
70
74
|
logger,
|