@hachej/boring-ui-kit 0.4.3 → 0.4.4
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/README.md +48 -0
- package/dist/chat-outbox.d.ts +18 -0
- package/dist/chat-outbox.js +12 -0
- package/dist/mark-store.d.ts +38 -0
- package/dist/mark-store.js +84 -0
- package/dist/markdown-editor.d.ts +6 -0
- package/dist/markdown-editor.js +17 -2
- package/dist/markdown-highlight-layer.d.ts +26 -0
- package/dist/markdown-highlight-layer.js +107 -0
- package/dist/markdown-quote.d.ts +23 -0
- package/dist/markdown-quote.js +39 -0
- package/dist/native-chat.d.ts +21 -0
- package/dist/native-chat.js +111 -7
- package/dist/remote-chat.js +16 -14
- package/dist/shared-chat/error.d.ts +10 -0
- package/dist/shared-chat/error.js +13 -0
- package/dist/shared-chat/fake.d.ts +39 -0
- package/dist/shared-chat/fake.js +90 -0
- package/dist/shared-chat/index.d.ts +51 -0
- package/dist/shared-chat/index.js +255 -0
- package/dist/shared-chat/socket.d.ts +20 -0
- package/dist/shared-chat/socket.js +114 -0
- package/dist/shared-chat/source.d.ts +117 -0
- package/dist/shared-chat/source.js +6 -0
- package/package.json +15 -3
- package/r/agent-target.json +36 -0
- package/r/app-shell.json +26 -11
- package/r/board-viewer.json +5 -5
- package/r/button.json +1 -1
- package/r/diagram-viewer.json +1 -1
- package/r/dock.json +1 -1
- package/r/documents.json +10 -10
- package/r/feedback-preview.json +1 -1
- package/r/feedback.json +5 -5
- package/r/file-tree.json +5 -5
- package/r/highlights.json +29 -0
- package/r/html-viewer.json +3 -3
- package/r/maquette-viewer.json +2 -2
- package/r/markdown-editor.json +3 -3
- package/r/mockup-viewer.json +2 -2
- package/r/pi-ambient.json +7 -7
- package/r/pi-app.json +15 -14
- package/r/pi-chat.json +27 -19
- package/r/pi-workspace.json +4 -4
- package/r/provider-setup.json +2 -2
- package/r/record-viewer.json +40 -0
- package/r/registry.json +330 -86
- package/r/requests-board.json +41 -0
- package/r/shared-chat.json +79 -0
- package/r/task-list-viewer.json +2 -2
- package/r/theme.json +15 -0
- package/r/version-history.json +3 -3
- package/r/viewers.json +15 -9
package/dist/native-chat.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { awaitWithContext, withCancel } from '@earendil-works/chord/context';
|
|
1
2
|
import { randomUUID } from '@hachej/boring-files/platform';
|
|
2
3
|
function draft(text, attachments, version) {
|
|
3
4
|
if (typeof text !== 'string' || attachments.some(item => typeof item.id !== 'string' || !item.id || typeof item.name !== 'string'
|
|
@@ -23,6 +24,8 @@ export function createNativeChatController(options) {
|
|
|
23
24
|
const listeners = new Set();
|
|
24
25
|
let watch, opening, disposing;
|
|
25
26
|
const publish = (change) => {
|
|
27
|
+
if (snapshot.disposed)
|
|
28
|
+
return;
|
|
26
29
|
snapshot = Object.freeze({ ...snapshot, ...change, ...(change.send ? { send: Object.freeze(change.send) } : {}),
|
|
27
30
|
...(change.connection ? { connection: Object.freeze(change.connection) } : {}),
|
|
28
31
|
...(change.history ? { history: Object.freeze(change.history) } : {}), ...(change.outbox ? { outbox: Object.freeze(change.outbox) } : {}) });
|
|
@@ -57,7 +60,27 @@ export function createNativeChatController(options) {
|
|
|
57
60
|
historyScan = undefined;
|
|
58
61
|
return emptyHistory();
|
|
59
62
|
};
|
|
63
|
+
const restoring = new Map();
|
|
64
|
+
const hasText = (messages) => messages?.some(message => message.role === 'assistant' && Array.isArray(message.content) && message.content.some(part => part?.type === 'text' && typeof part.text === 'string' && part.text.length > 0)) ?? false;
|
|
65
|
+
const observeText = (view) => {
|
|
66
|
+
const live = view.docs['pi.live'], run = live?.['run'];
|
|
67
|
+
if (!run || typeof run !== 'object' || Array.isArray(run))
|
|
68
|
+
return;
|
|
69
|
+
const inputs = run['inputs'], taskId = run['taskId'], generation = live?.['generation'];
|
|
70
|
+
const message = generation && typeof generation === 'object' && !Array.isArray(generation) ? generation['message'] : undefined;
|
|
71
|
+
const streamed = message && typeof message === 'object' && !Array.isArray(message) && hasText([{ role: 'assistant', content: message['content'] }]);
|
|
72
|
+
if (!Array.isArray(inputs))
|
|
73
|
+
return;
|
|
74
|
+
for (const item of restoring.values())
|
|
75
|
+
if (inputs.includes(item.submissionId)) {
|
|
76
|
+
if (typeof taskId === 'number')
|
|
77
|
+
item.runTaskId = taskId;
|
|
78
|
+
if (streamed || view.entries.some(entry => entry.byTaskId === taskId && hasText(entry.model)))
|
|
79
|
+
item.textObserved = true;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
60
82
|
const acceptView = (view) => {
|
|
83
|
+
observeText(view);
|
|
61
84
|
const previous = snapshot.view?.entries;
|
|
62
85
|
const appendOnly = !previous || previous.every((entry, index) => view.entries[index]?.id === entry.id);
|
|
63
86
|
publish({ view, ...(!appendOnly ? { history: invalidateHistory() } : {}) });
|
|
@@ -68,9 +91,10 @@ export function createNativeChatController(options) {
|
|
|
68
91
|
const editable = () => { active(); return snapshot.draft; };
|
|
69
92
|
// The composer is emptied when a message is sent, so a confirmation never has to clear it (text typed meanwhile is a new draft).
|
|
70
93
|
const markAdmitted = (attempt, submissionId, record) => {
|
|
71
|
-
if (!stillPending(attempt))
|
|
72
|
-
return;
|
|
94
|
+
if (snapshot.disposed || !stillPending(attempt))
|
|
95
|
+
return false;
|
|
73
96
|
publish({ send: { kind: 'admitted', attempt, submissionId, ...(record ? { record } : {}) } });
|
|
97
|
+
return true;
|
|
74
98
|
};
|
|
75
99
|
/** Messages taken from the composer that wait for the current send (`outbox` in the snapshot). */
|
|
76
100
|
const waiting = [];
|
|
@@ -87,11 +111,16 @@ export function createNativeChatController(options) {
|
|
|
87
111
|
item.resolve(undefined);
|
|
88
112
|
if (snapshot.disposed)
|
|
89
113
|
return;
|
|
90
|
-
|
|
114
|
+
putBack([...drafts, ...left.map(item => item.draft)], { ...change, ...(left.length || snapshot.outbox.length ? { outbox: Object.freeze([]) } : {}) });
|
|
115
|
+
};
|
|
116
|
+
function putBack(selected, change = {}) {
|
|
117
|
+
if (snapshot.disposed)
|
|
118
|
+
return;
|
|
119
|
+
const current = snapshot.draft, back = selected.map(item => restoreOf.get(item) ?? item);
|
|
91
120
|
const text = [...back.map(item => item.text), current.text].filter(value => value.trim()).join('\n');
|
|
92
121
|
const attachments = [...back.flatMap(item => item.attachments), ...current.attachments].filter((item, index, all) => all.findIndex(other => other.id === item.id) === index);
|
|
93
|
-
publish({ ...change, ...(
|
|
94
|
-
}
|
|
122
|
+
publish({ ...change, ...(back.length ? { draft: draft(text, attachments, current.version + 1) } : {}) });
|
|
123
|
+
}
|
|
95
124
|
function definitelyNotAdmitted(error) {
|
|
96
125
|
try {
|
|
97
126
|
return options.definitelyNotAdmitted?.(error) === true;
|
|
@@ -100,6 +129,53 @@ export function createNativeChatController(options) {
|
|
|
100
129
|
return false;
|
|
101
130
|
}
|
|
102
131
|
}
|
|
132
|
+
function monitor(attempt, submission) {
|
|
133
|
+
if (!options.restoreOnError || snapshot.disposed || restoring.has(attempt.requestId))
|
|
134
|
+
return;
|
|
135
|
+
const waiter = withCancel(context);
|
|
136
|
+
const item = { submissionId: submission.id, cancel: waiter.cancel, textObserved: false };
|
|
137
|
+
restoring.set(attempt.requestId, item);
|
|
138
|
+
if (snapshot.view)
|
|
139
|
+
observeText(snapshot.view);
|
|
140
|
+
void (async () => {
|
|
141
|
+
try {
|
|
142
|
+
const record = await awaitWithContext(submission.wait(waiter.context), waiter.context);
|
|
143
|
+
if (snapshot.disposed || record.id !== item.submissionId || record.conversationId !== conversation.id || record.requestId !== attempt.requestId || record.type !== 'input'
|
|
144
|
+
|| record.status !== 'unanswered' || !['model_error', 'no_model', 'faulted'].includes(record.reason) || record.entry === undefined || item.textObserved)
|
|
145
|
+
return;
|
|
146
|
+
// The native transcript, not watch timing or connection loss, decides whether the failed input received any text.
|
|
147
|
+
const minEntryId = record.entry, detail = record.detail;
|
|
148
|
+
const newest = await awaitWithContext(conversation.entries({}, 1, undefined, waiter.context), waiter.context);
|
|
149
|
+
const maxEntryId = newest.items[0]?.id;
|
|
150
|
+
if (maxEntryId === undefined || maxEntryId < minEntryId)
|
|
151
|
+
return;
|
|
152
|
+
let cursor, ended = false;
|
|
153
|
+
do {
|
|
154
|
+
const page = await awaitWithContext(conversation.entries({ minEntryId, maxEntryId, order: 'ascending' }, 40, cursor, waiter.context), waiter.context);
|
|
155
|
+
if (snapshot.disposed || item.textObserved)
|
|
156
|
+
return;
|
|
157
|
+
for (const entry of page.items) {
|
|
158
|
+
if (hasText(entry.model))
|
|
159
|
+
return;
|
|
160
|
+
// One run may consume several inputs and retain interrupted attempts. Neither is the final error boundary.
|
|
161
|
+
if (entry.model?.some(message => message.role === 'assistant' && message.stopReason === 'error'
|
|
162
|
+
&& (item.runTaskId !== undefined ? entry.byTaskId === item.runTaskId : typeof detail === 'string' && message.errorMessage === detail))) {
|
|
163
|
+
ended = true;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
cursor = page.next;
|
|
168
|
+
} while (!ended && cursor !== undefined);
|
|
169
|
+
if (!snapshot.disposed && !item.textObserved)
|
|
170
|
+
putBack([attempt.draft]);
|
|
171
|
+
}
|
|
172
|
+
catch { /* Missing settlement/history is unknown, never proof of an empty failed answer. */ }
|
|
173
|
+
finally {
|
|
174
|
+
restoring.delete(attempt.requestId);
|
|
175
|
+
waiter.cancel();
|
|
176
|
+
}
|
|
177
|
+
})();
|
|
178
|
+
}
|
|
103
179
|
async function submitAttempt(attempt) {
|
|
104
180
|
publish({ send: { kind: 'submitting', attempt }, ...(!stopping && snapshot.stop !== 'idle' ? { stop: 'idle' } : {}) });
|
|
105
181
|
if (snapshot.disposed)
|
|
@@ -109,7 +185,8 @@ export function createNativeChatController(options) {
|
|
|
109
185
|
? [{ type: 'text', text: attempt.draft.text }, ...attempt.draft.attachments.map(item => ({ ...item.content }))]
|
|
110
186
|
: attempt.draft.text;
|
|
111
187
|
const submission = await conversation.submit({ type: 'input', requestId: attempt.requestId, content, whenBusy: attempt.whenBusy }, context);
|
|
112
|
-
markAdmitted(attempt, submission.id)
|
|
188
|
+
if (markAdmitted(attempt, submission.id))
|
|
189
|
+
monitor(attempt, submission);
|
|
113
190
|
return submission;
|
|
114
191
|
}
|
|
115
192
|
catch (error) {
|
|
@@ -151,6 +228,15 @@ export function createNativeChatController(options) {
|
|
|
151
228
|
const controller = {
|
|
152
229
|
conversation,
|
|
153
230
|
getSnapshot: () => snapshot,
|
|
231
|
+
/** Reads the exact native submission with this controller's Context; observing never aborts or owns its execution. */
|
|
232
|
+
submissionStatus: async (submission) => {
|
|
233
|
+
active();
|
|
234
|
+
const record = await submission.status(context);
|
|
235
|
+
active();
|
|
236
|
+
if (record.id !== submission.id || record.conversationId !== conversation.id)
|
|
237
|
+
throw new Error('Submission belongs to a different conversation');
|
|
238
|
+
return record;
|
|
239
|
+
},
|
|
154
240
|
subscribe: (listener) => { if (snapshot.disposed)
|
|
155
241
|
return () => { }; listeners.add(listener); return () => { listeners.delete(listener); }; },
|
|
156
242
|
setText: (text) => { const current = editable(); publish({ draft: draft(text, current.attachments, current.version + 1) }); },
|
|
@@ -316,7 +402,22 @@ export function createNativeChatController(options) {
|
|
|
316
402
|
if (record) {
|
|
317
403
|
if (record.conversationId !== conversation.id || record.requestId !== attempt.requestId || record.type !== 'input')
|
|
318
404
|
throw new Error('Native submission lookup returned a different binding');
|
|
319
|
-
markAdmitted(attempt, record.id, record);
|
|
405
|
+
const admitted = markAdmitted(attempt, record.id, record);
|
|
406
|
+
if (admitted && options.restoreOnError) {
|
|
407
|
+
// Reconciliation observes the original request only; no duplicate submission is made to obtain a handle.
|
|
408
|
+
monitor(attempt, { id: record.id, wait: async (waiter) => {
|
|
409
|
+
for (;;) {
|
|
410
|
+
if (waiter.abortSignal?.aborted)
|
|
411
|
+
throw waiter.abortSignal.reason;
|
|
412
|
+
const current = await awaitWithContext(conversation.commit(tx => tx.submissionByRequest(conversation.id, attempt.requestId), waiter), waiter);
|
|
413
|
+
if (!current || current.id !== record.id)
|
|
414
|
+
throw new Error('Submission binding is unavailable');
|
|
415
|
+
if (current.status === 'done' || current.status === 'unanswered')
|
|
416
|
+
return current;
|
|
417
|
+
await awaitWithContext(new Promise(resolve => setTimeout(resolve, 250)), waiter);
|
|
418
|
+
}
|
|
419
|
+
} });
|
|
420
|
+
}
|
|
320
421
|
}
|
|
321
422
|
return record;
|
|
322
423
|
},
|
|
@@ -367,6 +468,9 @@ export function createNativeChatController(options) {
|
|
|
367
468
|
return disposing;
|
|
368
469
|
disposing = Promise.resolve().then(async () => { await opening; const owned = watch; watch = undefined; return owned?.stop(); });
|
|
369
470
|
unlink?.();
|
|
471
|
+
for (const item of restoring.values())
|
|
472
|
+
item.cancel();
|
|
473
|
+
restoring.clear();
|
|
370
474
|
publish({ disposed: true, connection: { kind: 'closed' }, history: invalidateHistory() });
|
|
371
475
|
listeners.clear();
|
|
372
476
|
return disposing;
|
package/dist/remote-chat.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { awaitWithContext, BACKGROUND_CONTEXT } from '@earendil-works/chord/context';
|
|
1
2
|
import { applyImmutable } from '@earendil-works/chord/delta';
|
|
3
|
+
import { createChatOutbox } from './chat-outbox.js';
|
|
2
4
|
/** A watch request the host answered with an error status. */
|
|
3
5
|
class WatchRefused extends Error {
|
|
4
6
|
status;
|
|
@@ -40,17 +42,19 @@ export async function createRemoteChat(options) {
|
|
|
40
42
|
// The transport in use: `websocket` until its first socket fails to open, then `http` for good.
|
|
41
43
|
let mode = options.transport ?? 'http', current, seq = 0;
|
|
42
44
|
/** Calls sent over a socket and not answered yet, by call ID: sent again unchanged on the next socket. */
|
|
43
|
-
const outbox =
|
|
45
|
+
const outbox = createChatOutbox();
|
|
44
46
|
function exchange(op, init, extra) {
|
|
45
47
|
const http = () => new Request(url(op, extra), init);
|
|
46
48
|
if (mode !== 'websocket')
|
|
47
49
|
return send(http());
|
|
48
50
|
const id = `c${++seq}`;
|
|
49
51
|
const text = JSON.stringify({ kind: 'call', id, op, params: extra, ...(typeof init.body === 'string' ? { body: JSON.parse(init.body) } : {}) });
|
|
50
|
-
|
|
52
|
+
const pending = outbox.enqueue(id, { text, http });
|
|
53
|
+
try {
|
|
51
54
|
current?.send(text);
|
|
52
55
|
}
|
|
53
|
-
catch { /* sent again on the next socket */ }
|
|
56
|
+
catch { /* sent again on the next socket */ }
|
|
57
|
+
return pending;
|
|
54
58
|
}
|
|
55
59
|
/** Leave the socket for HTTP: calls still waiting for a socket go over HTTP, unchanged (same request ID, so the host deduplicates). */
|
|
56
60
|
function fallBack() {
|
|
@@ -58,7 +62,7 @@ export async function createRemoteChat(options) {
|
|
|
58
62
|
const pending = [...outbox.values()];
|
|
59
63
|
outbox.clear();
|
|
60
64
|
for (const call of pending)
|
|
61
|
-
send(call.http()).then(call.resolve, call.reject);
|
|
65
|
+
send(call.input.http()).then(call.resolve, call.reject);
|
|
62
66
|
}
|
|
63
67
|
async function call(op, init = {}, extra = {}) {
|
|
64
68
|
const response = await exchange(op, init, extra);
|
|
@@ -135,11 +139,7 @@ export async function createRemoteChat(options) {
|
|
|
135
139
|
return;
|
|
136
140
|
}
|
|
137
141
|
if (frame.kind === 'reply') {
|
|
138
|
-
|
|
139
|
-
if (pending) {
|
|
140
|
-
outbox.delete(frame.id);
|
|
141
|
-
pending.resolve(new Response(JSON.stringify(frame.body), { status: frame.status, headers: { 'content-type': 'application/json' } }));
|
|
142
|
-
}
|
|
142
|
+
outbox.resolve(frame.id, new Response(JSON.stringify(frame.body), { status: frame.status, headers: { 'content-type': 'application/json' } }));
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
145
|
if (frame.kind === 'event') {
|
|
@@ -158,7 +158,7 @@ export async function createRemoteChat(options) {
|
|
|
158
158
|
current = socket;
|
|
159
159
|
// Unanswered calls go again, unchanged: a submit keeps its request ID, so the host runs it once.
|
|
160
160
|
for (const pending of outbox.values())
|
|
161
|
-
socket.send(pending.text);
|
|
161
|
+
socket.send(pending.input.text);
|
|
162
162
|
ready({ first: frame.view, next, cancel });
|
|
163
163
|
return;
|
|
164
164
|
}
|
|
@@ -442,11 +442,13 @@ export async function createRemoteChat(options) {
|
|
|
442
442
|
throw new Error('Submission is unknown to the remote host'); return record; };
|
|
443
443
|
return {
|
|
444
444
|
id: submissionId, status,
|
|
445
|
-
wait: async () => { for (;;) {
|
|
446
|
-
|
|
445
|
+
wait: async (context) => { for (;;) {
|
|
446
|
+
if (context.abortSignal?.aborted)
|
|
447
|
+
throw context.abortSignal.reason;
|
|
448
|
+
const record = await awaitWithContext(status(), context);
|
|
447
449
|
if (record.status === 'done' || record.status === 'unanswered')
|
|
448
450
|
return record;
|
|
449
|
-
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
451
|
+
await awaitWithContext(new Promise(resolve => setTimeout(resolve, pollMs)), context);
|
|
450
452
|
} },
|
|
451
453
|
abort: () => withdraw(submissionId, requestId),
|
|
452
454
|
};
|
|
@@ -460,7 +462,7 @@ export async function createRemoteChat(options) {
|
|
|
460
462
|
...(query.order === undefined ? {} : { order: query.order }),
|
|
461
463
|
...(cursor === undefined ? {} : { cursor: JSON.stringify(cursor) }) })).page,
|
|
462
464
|
};
|
|
463
|
-
const context =
|
|
465
|
+
const context = BACKGROUND_CONTEXT;
|
|
464
466
|
const close = async () => { const first = unclaimed; unclaimed = undefined; await first?.stop(); };
|
|
465
467
|
return { conversation, context, close, link, withdraw: id => withdraw(id), definitelyNotAdmitted: error => error instanceof SubmissionRefused,
|
|
466
468
|
answer: (callId, answer) => post('answer', { callId, answer }), configure: change => post('configure', change) };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** A channel failure's message and retryability come from the host, or from an unconfirmed transport failure. */
|
|
2
|
+
export declare class ChannelRequestError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly retryable: boolean;
|
|
5
|
+
readonly refused: boolean;
|
|
6
|
+
constructor(code: string, message: string, retryable: boolean, options?: {
|
|
7
|
+
readonly refused?: boolean;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=error.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** A channel failure's message and retryability come from the host, or from an unconfirmed transport failure. */
|
|
2
|
+
export class ChannelRequestError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
retryable;
|
|
5
|
+
refused;
|
|
6
|
+
constructor(code, message, retryable, options) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.retryable = retryable;
|
|
10
|
+
this.name = 'ChannelRequestError';
|
|
11
|
+
this.refused = options?.refused ?? false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ChannelEvent, ChannelMessage, ChannelSource, Conversation, Member, SharedChatState } from './source.js';
|
|
2
|
+
export interface FakeChannelSeed {
|
|
3
|
+
readonly me: Member;
|
|
4
|
+
readonly members: Member[];
|
|
5
|
+
readonly conversations: Conversation[];
|
|
6
|
+
readonly messages: ChannelMessage[];
|
|
7
|
+
}
|
|
8
|
+
/** Fictional in-memory host. The controls exercise exactly the source events a real host delivers. */
|
|
9
|
+
export declare function createFakeChannelSource(seed: FakeChannelSeed): {
|
|
10
|
+
me: Member;
|
|
11
|
+
conversations: () => Promise<Conversation[]>;
|
|
12
|
+
members: (id: string) => Promise<Member[]>;
|
|
13
|
+
history: (id: string, opts: {
|
|
14
|
+
before?: string;
|
|
15
|
+
limit: number;
|
|
16
|
+
}) => Promise<{
|
|
17
|
+
before?: string;
|
|
18
|
+
messages: ChannelMessage[];
|
|
19
|
+
}>;
|
|
20
|
+
send: (input: Parameters<ChannelSource["send"]>[0]) => Promise<ChannelMessage>;
|
|
21
|
+
action: (input: Parameters<ChannelSource["action"]>[0]) => Promise<void>;
|
|
22
|
+
subscribe: (id: string, event: (event: ChannelEvent) => void, status?: (status: SharedChatState["status"]) => void) => () => void;
|
|
23
|
+
emit: (event: ChannelEvent) => void;
|
|
24
|
+
sends: {
|
|
25
|
+
conversationId: string;
|
|
26
|
+
text: string;
|
|
27
|
+
mentions: string[];
|
|
28
|
+
replyTo?: string;
|
|
29
|
+
clientId: string;
|
|
30
|
+
attachments?: File[];
|
|
31
|
+
}[];
|
|
32
|
+
actions: {
|
|
33
|
+
conversationId: string;
|
|
34
|
+
requestId: string;
|
|
35
|
+
actionId: string;
|
|
36
|
+
}[];
|
|
37
|
+
setStatus: (status: SharedChatState["status"]) => void;
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=fake.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ChannelRequestError } from './error.js';
|
|
2
|
+
/** Fictional in-memory host. The controls exercise exactly the source events a real host delivers. */
|
|
3
|
+
export function createFakeChannelSource(seed) {
|
|
4
|
+
const data = structuredClone(seed);
|
|
5
|
+
const listeners = new Set();
|
|
6
|
+
const sends = [], actions = [];
|
|
7
|
+
let online = true;
|
|
8
|
+
function emit(event) {
|
|
9
|
+
if (event.type === 'message') {
|
|
10
|
+
const index = data.messages.findIndex(message => message.id === event.message.id);
|
|
11
|
+
if (index < 0)
|
|
12
|
+
data.messages.push(structuredClone(event.message));
|
|
13
|
+
else
|
|
14
|
+
data.messages[index] = structuredClone(event.message);
|
|
15
|
+
}
|
|
16
|
+
else if (event.type === 'delta') {
|
|
17
|
+
const message = data.messages.find(message => message.id === event.messageId && message.conversationId === event.conversationId);
|
|
18
|
+
if (message) {
|
|
19
|
+
message.text = (message.text ?? '') + event.text;
|
|
20
|
+
message.streaming = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else if (event.type === 'step') {
|
|
24
|
+
const message = data.messages.find(message => message.id === event.messageId && message.conversationId === event.conversationId);
|
|
25
|
+
if (message && Number.isSafeInteger(event.index) && event.index >= 0) {
|
|
26
|
+
const steps = [...(message.steps ?? [])];
|
|
27
|
+
steps[event.index] = { ...event.step };
|
|
28
|
+
message.steps = steps;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (event.type === 'presence')
|
|
32
|
+
for (const update of event.members) {
|
|
33
|
+
const member = data.members.find(member => member.id === update.id);
|
|
34
|
+
if (member)
|
|
35
|
+
member.presence = update.presence;
|
|
36
|
+
}
|
|
37
|
+
else if (event.type === 'members') {
|
|
38
|
+
for (const member of event.members) {
|
|
39
|
+
const index = data.members.findIndex(existing => existing.id === member.id);
|
|
40
|
+
if (index < 0)
|
|
41
|
+
data.members.push(structuredClone(member));
|
|
42
|
+
else
|
|
43
|
+
data.members[index] = structuredClone(member);
|
|
44
|
+
}
|
|
45
|
+
const conversation = data.conversations.find(conversation => conversation.id === event.conversationId);
|
|
46
|
+
if (conversation)
|
|
47
|
+
conversation.members = event.members.map(member => member.id);
|
|
48
|
+
}
|
|
49
|
+
if (online)
|
|
50
|
+
for (const listener of [...listeners]) {
|
|
51
|
+
const id = event.type === 'message' ? event.message.conversationId : event.type === 'presence' ? undefined : event.conversationId;
|
|
52
|
+
if (id === undefined || id === listener.id)
|
|
53
|
+
listener.event(structuredClone(event));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
me: data.me,
|
|
58
|
+
conversations: async () => structuredClone(data.conversations),
|
|
59
|
+
members: async (id) => structuredClone(data.members.filter(member => data.conversations.find(conversation => conversation.id === id)?.members.includes(member.id))),
|
|
60
|
+
history: async (id, opts) => {
|
|
61
|
+
const all = data.messages.filter(message => message.conversationId === id);
|
|
62
|
+
const end = opts.before ? all.findIndex(message => message.id === opts.before) : all.length;
|
|
63
|
+
if (end < 0)
|
|
64
|
+
throw new Error('Unknown history cursor');
|
|
65
|
+
const start = Math.max(0, end - opts.limit);
|
|
66
|
+
return { messages: structuredClone(all.slice(start, end)), ...(start > 0 && all[start] ? { before: all[start].id } : {}) };
|
|
67
|
+
},
|
|
68
|
+
send: async (input) => {
|
|
69
|
+
sends.push({ ...input, mentions: [...input.mentions], ...(input.attachments ? { attachments: [...input.attachments] } : {}) });
|
|
70
|
+
if (!online)
|
|
71
|
+
throw new ChannelRequestError('offline', 'Offline', true);
|
|
72
|
+
const duplicate = data.messages.find(message => message.clientId === input.clientId && message.conversationId === input.conversationId);
|
|
73
|
+
if (duplicate)
|
|
74
|
+
return structuredClone(duplicate);
|
|
75
|
+
const message = { id: `sent-${input.clientId}`, conversationId: input.conversationId, author: data.me.id, at: new Date().toISOString(), text: input.text, mentions: [...input.mentions], clientId: input.clientId, ...(input.replyTo ? { replyTo: input.replyTo } : {}) };
|
|
76
|
+
emit({ type: 'message', message });
|
|
77
|
+
return structuredClone(message);
|
|
78
|
+
},
|
|
79
|
+
action: async (input) => { actions.push(structuredClone(input)); },
|
|
80
|
+
subscribe: (id, event, status) => {
|
|
81
|
+
const listener = { id, event, status };
|
|
82
|
+
listeners.add(listener);
|
|
83
|
+
status?.(online ? 'live' : 'offline');
|
|
84
|
+
return () => { listeners.delete(listener); };
|
|
85
|
+
},
|
|
86
|
+
emit, sends, actions,
|
|
87
|
+
setStatus: (status) => { online = status === 'live'; for (const listener of [...listeners])
|
|
88
|
+
listener.status?.(status); },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ChatAttempt, ChatDraft, ChatSendInput, NativeChatSnapshot } from '../native-chat.js';
|
|
2
|
+
export { ChannelRequestError } from './error.js';
|
|
3
|
+
import type { Card, ChannelMessage, ChannelSource, Member, SharedChatState } from './source.js';
|
|
4
|
+
export type * from './source.js';
|
|
5
|
+
export { channelAvatarStates } from './source.js';
|
|
6
|
+
export type SharedChatSnapshot = Omit<NativeChatSnapshot, 'conversationId' | 'send' | 'shared'> & {
|
|
7
|
+
readonly conversationId: string;
|
|
8
|
+
readonly shared: SharedChatState;
|
|
9
|
+
readonly send: {
|
|
10
|
+
readonly kind: 'idle' | 'submitting' | 'admitted';
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: 'unknown';
|
|
13
|
+
readonly reason: string;
|
|
14
|
+
readonly retryable: boolean;
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: 'blocked';
|
|
17
|
+
readonly reason: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export type { ChatController, ChatSnapshot } from '../native-chat.js';
|
|
21
|
+
/** Host-owned multi-party log, not a Pi conversation or an agent runtime. One adapter per selected conversation. */
|
|
22
|
+
export declare function createSharedChat(source: ChannelSource, options: {
|
|
23
|
+
readonly conversationId: string;
|
|
24
|
+
readonly limit?: number;
|
|
25
|
+
}): {
|
|
26
|
+
getSnapshot: () => SharedChatSnapshot;
|
|
27
|
+
subscribe: (listener: () => void) => () => void;
|
|
28
|
+
setText: (text: string) => void;
|
|
29
|
+
setAttachments: (attachments: ChatDraft["attachments"]) => void;
|
|
30
|
+
setFiles: (files: readonly File[]) => void;
|
|
31
|
+
setReplyTo: (messageId: string | undefined) => void;
|
|
32
|
+
clearHistory: () => void;
|
|
33
|
+
loadEarlier: () => Promise<void>;
|
|
34
|
+
connect: () => Promise<void>;
|
|
35
|
+
send: (_whenBusy?: ChatAttempt["whenBusy"], input?: ChatSendInput) => Promise<ChannelMessage | undefined>;
|
|
36
|
+
reconcile: () => Promise<void>;
|
|
37
|
+
retrySameRequest: () => Promise<void>;
|
|
38
|
+
stop: () => Promise<never>;
|
|
39
|
+
dispose: () => Promise<undefined>;
|
|
40
|
+
action: (card: Card) => Promise<void>;
|
|
41
|
+
remove: ((message: ChannelMessage) => Promise<void>) | undefined;
|
|
42
|
+
canRemove: (message: ChannelMessage) => boolean;
|
|
43
|
+
};
|
|
44
|
+
export type SharedChatController = ReturnType<typeof createSharedChat>;
|
|
45
|
+
/** The composer uses the existing quoted @ token grammar. Tokens carry stable member ids; picker labels show names. */
|
|
46
|
+
export declare function memberMention(member: Member): string;
|
|
47
|
+
export { createFakeChannelSource } from './fake.js';
|
|
48
|
+
export type { FakeChannelSeed } from './fake.js';
|
|
49
|
+
export { createChannelSocket } from './socket.js';
|
|
50
|
+
export type { ChannelSocketOptions, ChannelSocketSource } from './socket.js';
|
|
51
|
+
//# sourceMappingURL=index.d.ts.map
|