@poodle64/librarian 2026.9.15 → 2026.9.17
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/dist/client.js
CHANGED
|
@@ -79,8 +79,14 @@ export async function* ask(options) {
|
|
|
79
79
|
// A session that expired mid-conversation is the shape this catches: the
|
|
80
80
|
// POST is redirected to a login page, which answers 200 with HTML, and a
|
|
81
81
|
// reader whose stream "opened" then waits for frames that can never come.
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
//
|
|
83
|
+
// A response that DECLARES something other than an event stream is refused;
|
|
84
|
+
// one that declares nothing is read anyway. A route behind a proxy that
|
|
85
|
+
// drops the header is still streaming, and refusing it here would be this
|
|
86
|
+
// layer breaking a working consumer over a header — the terminal-frame
|
|
87
|
+
// check at the end of this function catches it if it really says nothing.
|
|
88
|
+
const kind = (response.headers.get('content-type') ?? '').toLowerCase();
|
|
89
|
+
if (!response.ok || !response.body || (kind && !kind.includes('text/event-stream'))) {
|
|
84
90
|
yield { type: 'library_error' };
|
|
85
91
|
return;
|
|
86
92
|
}
|
|
@@ -92,6 +98,25 @@ export async function* ask(options) {
|
|
|
92
98
|
// rather than left to the renderer, which cannot tell a stream that died
|
|
93
99
|
// from one still arriving.
|
|
94
100
|
let terminal = false;
|
|
101
|
+
/** One frame's `data:` lines, as the event they carry. */
|
|
102
|
+
function parse(frame) {
|
|
103
|
+
// `data:` only — the type lives inside the payload where Claude Code
|
|
104
|
+
// puts it, so there is no second place to look.
|
|
105
|
+
const data = frame
|
|
106
|
+
.split('\n')
|
|
107
|
+
.filter((l) => l.startsWith('data:'))
|
|
108
|
+
.map((l) => l.slice(5).trim())
|
|
109
|
+
.join('');
|
|
110
|
+
if (!data)
|
|
111
|
+
return null;
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(data);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// A partial frame at the end of a stream is not an error.
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
95
120
|
for (;;) {
|
|
96
121
|
let chunk;
|
|
97
122
|
try {
|
|
@@ -111,26 +136,24 @@ export async function* ask(options) {
|
|
|
111
136
|
const frame = buffered.slice(0, boundary);
|
|
112
137
|
buffered = buffered.slice(boundary + 2);
|
|
113
138
|
boundary = buffered.indexOf('\n\n');
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const data = frame
|
|
117
|
-
.split('\n')
|
|
118
|
-
.filter((l) => l.startsWith('data:'))
|
|
119
|
-
.map((l) => l.slice(5).trim())
|
|
120
|
-
.join('');
|
|
121
|
-
if (!data)
|
|
139
|
+
const event = parse(frame);
|
|
140
|
+
if (!event)
|
|
122
141
|
continue;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
terminal = true;
|
|
127
|
-
yield event;
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
/* a partial frame at end of stream is not an error */
|
|
131
|
-
}
|
|
142
|
+
if (event.type === 'result' || event.type === 'library_error')
|
|
143
|
+
terminal = true;
|
|
144
|
+
yield event;
|
|
132
145
|
}
|
|
133
146
|
}
|
|
147
|
+
// The last frame of a stream that closed cleanly without its blank line.
|
|
148
|
+
// It is usually the terminal `result`, carrying the duration and the
|
|
149
|
+
// sources, so dropping it both loses the answer's furniture and makes a
|
|
150
|
+
// finished turn look like one that died.
|
|
151
|
+
const last = parse(buffered);
|
|
152
|
+
if (last) {
|
|
153
|
+
if (last.type === 'result' || last.type === 'library_error')
|
|
154
|
+
terminal = true;
|
|
155
|
+
yield last;
|
|
156
|
+
}
|
|
134
157
|
if (!terminal && !options.signal?.aborted)
|
|
135
158
|
yield { type: 'library_error' };
|
|
136
159
|
}
|
|
@@ -353,13 +353,21 @@
|
|
|
353
353
|
align-self: flex-end;
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
/* The reader's own words carry the consuming app's hue, the way every chat
|
|
357
|
+
a person already uses colours THEIR side and leaves the other party
|
|
358
|
+
neutral. It is an identity mark, not decoration: scanning back for "what
|
|
359
|
+
did I ask" is a colour search, not a reading task. The hue is whatever
|
|
360
|
+
the host set --ds-color-primary to, so no consumer is named here.
|
|
361
|
+
It stays on the SHORT side of the transcript on purpose — a question is
|
|
362
|
+
a line or two, an answer is paragraphs, so filling the question is an
|
|
363
|
+
accent and filling the answer would be a wall. */
|
|
356
364
|
.ds-lib-bubble {
|
|
357
365
|
max-width: 85%;
|
|
358
|
-
border: 1px solid
|
|
366
|
+
border: 1px solid transparent;
|
|
359
367
|
border-radius: var(--ds-radius-xl);
|
|
360
|
-
background: var(--ds-color-
|
|
368
|
+
background: var(--ds-color-primary);
|
|
361
369
|
padding: 0.625rem 1rem;
|
|
362
|
-
color: var(--ds-color-foreground);
|
|
370
|
+
color: var(--ds-color-primary-foreground);
|
|
363
371
|
font-size: 1rem;
|
|
364
372
|
line-height: 1.5rem;
|
|
365
373
|
overflow-wrap: break-word;
|
|
@@ -381,6 +389,7 @@
|
|
|
381
389
|
flex-direction: column;
|
|
382
390
|
gap: 0.75rem;
|
|
383
391
|
border: 1px solid var(--ds-color-border);
|
|
392
|
+
border-inline-start: 3px solid var(--ds-color-primary);
|
|
384
393
|
border-radius: var(--ds-radius-xl);
|
|
385
394
|
background: var(--ds-color-surface-2);
|
|
386
395
|
padding: 0.875rem 1rem 0.75rem;
|
|
@@ -400,8 +409,13 @@
|
|
|
400
409
|
align-items: center;
|
|
401
410
|
justify-content: center;
|
|
402
411
|
border-radius: var(--ds-radius-full);
|
|
403
|
-
|
|
404
|
-
|
|
412
|
+
/* Opaque, never a translucent tint. An 18% wash of a dark primary over a
|
|
413
|
+
dark surface has almost no luminance separation, so the initial fell
|
|
414
|
+
under the 3:1 floor for a non-text graphic and read as a smudge —
|
|
415
|
+
measured 2.55:1 on a dark-mode host. A solid fill carries the hue AND
|
|
416
|
+
the contrast. */
|
|
417
|
+
background: var(--ds-color-primary);
|
|
418
|
+
color: var(--ds-color-primary-foreground);
|
|
405
419
|
font-size: var(--ds-text-2xs);
|
|
406
420
|
font-weight: 600;
|
|
407
421
|
text-transform: uppercase;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/librarian",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.17",
|
|
4
4
|
"description": "An agent's conversation surface as a consumable Svelte 5 package: the stream client, transcript state and self-styling chat components (transcript, composer, markdown) every household app renders instead of rebuilding, speaking as whichever persona the app names.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|