@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
- const kind = response.headers.get('content-type') ?? '';
83
- if (!response.ok || !response.body || !kind.includes('text/event-stream')) {
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
- // `data:` only — the type lives inside the payload where Claude Code
115
- // puts it, so there is no second place to look.
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
- try {
124
- const event = JSON.parse(data);
125
- if (event.type === 'result' || event.type === 'library_error')
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 var(--ds-color-border);
366
+ border: 1px solid transparent;
359
367
  border-radius: var(--ds-radius-xl);
360
- background: var(--ds-color-surface-3);
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
- background: color-mix(in oklab, var(--ds-color-primary) 18%, transparent);
404
- color: var(--ds-color-foreground);
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.15",
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",