@volter/teams 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/viewer/session-view-entry.mjs +40 -6
- package/viewer/session-view.mjs +23 -22
- package/viewer/viewer.mjs +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/teams",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Volter Teams, the neutral team plane: machine identity and daemon core, the local door and server link, admission from machine grants, product doors, and the Teams server's neutral routes (company RFC 0008).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,12 +10,31 @@ import { projectSubagentTranscript } from '@volter/supercode-ui/controller';
|
|
|
10
10
|
|
|
11
11
|
// A person's picture: the brand's person placeholder. An empty seed is its one standard anonymous face.
|
|
12
12
|
const personImage = (seed = '') => `https://brand.volter.ai/runner/svg?seed=${encodeURIComponent(seed)}&size=36&radius=18`;
|
|
13
|
+
// An agent's picture (this session's own, a peer session's): the brand's agent mark, a rounded square, never a face.
|
|
14
|
+
const agentImage = (seed) => `https://brand.volter.ai/agent/svg?seed=${encodeURIComponent(seed)}&size=36&radius=8`;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A message another session sent this one, as its harness delivers it: an envelope naming the sender (a
|
|
18
|
+
* `cross-session-message` tag, the innermost one when the harness wraps it in its own) around the body. Returns the
|
|
19
|
+
* sender and the body, or null for an ordinary turn.
|
|
20
|
+
*/
|
|
21
|
+
function peerMessage(text) {
|
|
22
|
+
const close = text.search(/<\\?\/cross-session-message>/);
|
|
23
|
+
if (close < 0) return null;
|
|
24
|
+
const opens = [...text.slice(0, close).matchAll(/<\\?cross-session-message\b([^>]*)>/g)];
|
|
25
|
+
if (!opens.length) return null;
|
|
26
|
+
const inner = opens[opens.length - 1];
|
|
27
|
+
const attributes = (tag) => Object.fromEntries([...tag.matchAll(/([\w-]+)="([^"]*)"/g)].map((m) => [m[1], m[2]]));
|
|
28
|
+
const own = attributes(inner[1]), outer = attributes(opens[0][1]);
|
|
29
|
+
const body = text.slice(inner.index + inner[0].length, close).trim();
|
|
30
|
+
return { name: own['from-name'] || outer['from-name'] || own.from || outer.from || 'Another session', address: own.from || outer.from || '', body };
|
|
31
|
+
}
|
|
13
32
|
|
|
14
33
|
/**
|
|
15
34
|
* Who wrote each user turn, from the message's own annotations (source_name / source_principal_id, recorded when a
|
|
16
35
|
* Room member typed it): their name and face, or, for anyone unnamed, the standard anonymous face. Never an agent mark.
|
|
17
36
|
*/
|
|
18
|
-
function withPeople(transcript, messages) {
|
|
37
|
+
function withPeople(transcript, messages, { agentName = null, sessionId = '' } = {}) {
|
|
19
38
|
const byTime = new Map();
|
|
20
39
|
for (const message of messages) {
|
|
21
40
|
if (message?.role !== 'user') continue;
|
|
@@ -23,7 +42,13 @@ function withPeople(transcript, messages) {
|
|
|
23
42
|
if (Number.isFinite(at)) byTime.set(at, message.metadata);
|
|
24
43
|
}
|
|
25
44
|
return transcript.map((entry) => {
|
|
45
|
+
// This session's own turns: named as the Room knows its agent, with the agent mark.
|
|
46
|
+
if (entry.role === 'assistant') return { ...entry, actor: { ...(agentName ? { name: agentName } : {}), image: agentImage(sessionId) } };
|
|
26
47
|
if (entry.role !== 'user') return entry;
|
|
48
|
+
// A harness's delivery notice about a message is a compact system line, not a person's turn.
|
|
49
|
+
if (/^\s*\[Cross-session [a-z ]*notice\]/i.test(entry.text)) return { ...entry, role: 'notice', text: entry.text.trim().split('\n')[0] };
|
|
50
|
+
const peer = peerMessage(entry.text);
|
|
51
|
+
if (peer) return { ...entry, text: peer.body, actor: { name: peer.name, image: agentImage(peer.address.startsWith('sc:') ? peer.address : peer.name) }, context: [{ id: `${entry.id}:envelope`, kind: 'envelope', label: 'Delivered by supercode', detail: entry.text }] };
|
|
27
52
|
const metadata = byTime.get(entry.ts) ?? {};
|
|
28
53
|
const name = metadata.source_name || null;
|
|
29
54
|
return { ...entry, actor: { name: name ?? 'Someone in the session', image: personImage(name ? (metadata.source_principal_id || name) : '') } };
|
|
@@ -55,20 +80,29 @@ function normalizedSession(descriptor, messages) {
|
|
|
55
80
|
* and machines); `control` (the token's scope) shows the composer; `onSend(text)` is called with a turn the
|
|
56
81
|
* member typed, and resolves once it was delivered or refused.
|
|
57
82
|
*/
|
|
58
|
-
export function mountSessionView(element, { onSend, control = false }) {
|
|
83
|
+
export function mountSessionView(element, { onSend, control = false, agentName = null }) {
|
|
59
84
|
// One badge, following the token's scope: fixed when mounted (the team view reads its labels once).
|
|
60
85
|
const labels = { readOnly: control ? 'Control' : 'Read-only' };
|
|
61
|
-
let current = { descriptor: null, messages: [], members: [], machines: [], live: false, status: 'loading', error: null, canSend: control, sending: false };
|
|
86
|
+
let current = { descriptor: null, messages: [], members: [], machines: [], live: false, status: 'loading', error: null, canSend: control, sending: false, pending: null };
|
|
62
87
|
const adapter = {
|
|
63
|
-
|
|
88
|
+
// A sent line shows at once as the user's pending turn: a session mid-turn records it only when it takes it in.
|
|
89
|
+
onIntent: async (intent) => {
|
|
90
|
+
const text = intent?.action === 'send' ? intent.text?.trim() : '';
|
|
91
|
+
if (!text) return;
|
|
92
|
+
current = { ...current, pending: { text, status: 'sending' } }; draw();
|
|
93
|
+
const delivered = await onSend(text);
|
|
94
|
+
if (!delivered) { current = { ...current, pending: { text, status: 'failed' } }; draw(); }
|
|
95
|
+
},
|
|
64
96
|
copyText: (text) => globalThis.navigator?.clipboard?.writeText(text),
|
|
65
97
|
};
|
|
66
98
|
const draw = () => {
|
|
67
99
|
const { descriptor, messages, members, machines, live, status, error, canSend, sending } = current;
|
|
68
|
-
|
|
100
|
+
// The pending line goes once the session has recorded it (a user message holding that text).
|
|
101
|
+
if (current.pending && messages.some((message) => message?.role === 'user' && typeof message.content === 'string' && message.content.includes(current.pending.text))) current = { ...current, pending: null };
|
|
102
|
+
const transcript = descriptor ? withPeople(projectSubagentTranscript(normalizedSession(descriptor, messages), { prefix: descriptor.id, maxEntries: 500 }), messages, { agentName, sessionId: descriptor.id }) : [];
|
|
69
103
|
const composerState = normalizeUiState({ ...EMPTY_UI_STATE, startup: 'ready', harness: descriptor?.harness ?? '', canSend: canSend && !sending, busy: sending, transcript });
|
|
70
104
|
render(h('div', { class: 'session-view' },
|
|
71
|
-
h(TeamTranscript, { session: descriptor, transcript, members, machines, live, status, error, adapter, labels }),
|
|
105
|
+
h(TeamTranscript, { session: descriptor, transcript, members, machines, live, status, error, adapter, labels, pending: current.pending }),
|
|
72
106
|
canSend ? h('div', { class: 'session-composer scui-root' }, h(Composer, { state: composerState, adapter })) : null,
|
|
73
107
|
), element);
|
|
74
108
|
};
|