@volter/teams 0.1.18 → 0.1.19

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/viewer/viewer.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  // postMessage from the page embedding this one, which sends a fresh one before the last expires; it is held in memory
4
4
  // only. The server verifies it and admits exactly its target and scope; what this page reads from it is only for
5
5
  // showing where it is.
6
- import { attachBrowserPane, openBrowserMachine } from './channel.mjs';
6
+ import { attachBrowserPane, openBrowserMachine, openBrowserSession, sendBrowserTurn } from './channel.mjs';
7
7
  import { StreamRpc } from './stream-rpc.mjs';
8
8
 
9
9
  const $ = (id) => document.getElementById(id);
@@ -40,6 +40,14 @@ async function freshToken() {
40
40
  async function main() {
41
41
  if (!token) { status('No access token. Open this from the workplace.'); return; }
42
42
  const claims = claimsOf(token);
43
+ // A session token (a Room's session-backed Conversation) names one team session, not a surface.
44
+ if (claims.session?.id) {
45
+ $('target').textContent = `session · ${claims.session.id}`;
46
+ $('scope').textContent = claims.scope === 'control' ? 'control' : 'view only';
47
+ await showSession(claims.session, claims.scope === 'control');
48
+ ask('viewer-ready');
49
+ return;
50
+ }
43
51
  const surface = claims.surface ?? {};
44
52
  const config = surface.config ?? {};
45
53
  const control = claims.scope === 'control';
@@ -53,6 +61,78 @@ async function main() {
53
61
  ask('viewer-ready');
54
62
  }
55
63
 
64
+ // One team session, live: supercode-ui's team transcript (and, under a control token, its composer) fed by the
65
+ // session's follow stream, with turns typed into the session through its channel as the member's own.
66
+ async function showSession(session, control) {
67
+ const stylesheet = document.createElement('link');
68
+ stylesheet.rel = 'stylesheet';
69
+ stylesheet.href = '/view/session-view.css';
70
+ document.head.append(stylesheet);
71
+ const { mountSessionView } = await import('/view/session-view.mjs');
72
+ const base = `${location.origin}/api/v1/teams/${encodeURIComponent(session.team)}/sessions/${encodeURIComponent(session.id)}`;
73
+ const element = $('session');
74
+ element.hidden = false;
75
+ const view = mountSessionView(element, {
76
+ onSend: async (text) => {
77
+ view.update({ sending: true });
78
+ try {
79
+ const channel = await openBrowserSession({ origin: location.origin, teamId: session.team, sessionId: session.id, token: await freshToken() });
80
+ try {
81
+ const answer = await sendBrowserTurn(channel, text);
82
+ status(answer.code === 0 ? '' : `Not delivered: ${answer.text ?? 'the session did not take the turn'}`);
83
+ } finally { channel.close(); }
84
+ } catch (error) { status(`Not delivered: ${error.message}`); } finally { view.update({ sending: false }); }
85
+ },
86
+ });
87
+ view.update({ canSend: control });
88
+ const headers = async () => ({ Authorization: `Bearer ${await freshToken()}` });
89
+ const apply = (body) => view.update({ descriptor: body.descriptor, messages: body.messages.map((message) => message.value), status: 'ready', error: null });
90
+ let reading = null;
91
+ const read = () => (reading ??= (async () => {
92
+ try {
93
+ const response = await fetch(`${base}/messages?tail=300`, { headers: await headers() });
94
+ if (!response.ok) throw new Error(`reading the session failed (${response.status})`);
95
+ apply(await response.json());
96
+ } finally { reading = null; }
97
+ })());
98
+ // The follow stream: a snapshot, then an event each time the session changed (read it again). It ends when the
99
+ // token does or the connection drops; it is reopened with a fresh token from where it left off.
100
+ let cursor = null;
101
+ for (;;) {
102
+ try {
103
+ const response = await fetch(`${base}/events?tail=300${cursor ? `&after=${encodeURIComponent(cursor)}` : ''}`, { headers: await headers() });
104
+ if (!response.ok) throw new Error(`following the session failed (${response.status})`);
105
+ view.update({ live: true });
106
+ status('');
107
+ const reader = response.body.getReader();
108
+ let buffer = '';
109
+ for (;;) {
110
+ const { value, done } = await reader.read();
111
+ if (done) break;
112
+ buffer += decoder.decode(value, { stream: true });
113
+ let end;
114
+ while ((end = buffer.indexOf('\n\n')) >= 0) {
115
+ const block = buffer.slice(0, end);
116
+ buffer = buffer.slice(end + 2);
117
+ const data = block.split('\n').filter((line) => line.startsWith('data:')).map((line) => line.slice(5).trimStart()).join('\n');
118
+ if (!data) continue;
119
+ const event = JSON.parse(data);
120
+ if (event.cursor) cursor = event.cursor;
121
+ // The server ends a stream it can no longer resume (a slow reader, a retired cursor): start from a snapshot.
122
+ if (event.terminal && event.type === 'resync_required') cursor = null;
123
+ if (event.type === 'snapshot') apply(event.data);
124
+ else if (event.type === 'session_changed') read().catch((error) => status(error.message));
125
+ else if (event.type === 'session_deleted') { view.update({ live: false, status: 'error', error: 'This session was deleted.' }); return; }
126
+ }
127
+ }
128
+ } catch (error) {
129
+ status(error.message);
130
+ }
131
+ view.update({ live: false });
132
+ await new Promise((resolve) => setTimeout(resolve, 2000));
133
+ }
134
+ }
135
+
56
136
  async function showPane(connect, key, control) {
57
137
  const [{ Terminal }, { FitAddon }] = await Promise.all([
58
138
  import('https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/+esm'),