Skip to content

Commit a38b2bc

Browse files
authored
Adds stream API (#231)
1 parent d444439 commit a38b2bc

File tree

5 files changed

+106
-0
lines changed

5 files changed

+106
-0
lines changed

.changeset/gentle-jokes-fail.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@clack/prompts": minor
3+
---
4+
5+
Adds `stream` API which provides the same methods as `log`, but for iterable (even async) message streams. This is particularly useful for AI responses which are dynamically generated by LLMs.
6+
7+
```ts
8+
import * as p from '@clack/prompts';
9+
10+
await p.stream.step((async function* () {
11+
yield* generateLLMResponse(question);
12+
})())
13+
```

examples/basic/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
},
1111
"scripts": {
1212
"start": "jiti ./index.ts",
13+
"stream": "jiti ./stream.ts",
1314
"spinner": "jiti ./spinner.ts",
1415
"spinner-ci": "npx cross-env CI=\"true\" jiti ./spinner-ci.ts"
1516
},

examples/basic/stream.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { setTimeout } from 'node:timers/promises';
2+
import * as p from '@clack/prompts';
3+
import color from 'picocolors';
4+
5+
async function main() {
6+
console.clear();
7+
8+
await setTimeout(1000);
9+
10+
p.intro(`${color.bgCyan(color.black(' create-app '))}`);
11+
12+
await p.stream.step((async function* () {
13+
for (const line of lorem) {
14+
for (const word of line.split(' ')) {
15+
yield word;
16+
yield ' ';
17+
await setTimeout(200);
18+
}
19+
yield '\n';
20+
if (line !== lorem.at(-1)) {
21+
await setTimeout(1000);
22+
}
23+
}
24+
})())
25+
26+
p.outro(`Problems? ${color.underline(color.cyan('https://example.com/issues'))}`);
27+
}
28+
29+
const lorem = [
30+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
31+
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
32+
]
33+
34+
main().catch(console.error);

packages/prompts/README.md

+16
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,20 @@ log.error('Error!');
188188
log.message('Hello, World', { symbol: color.cyan('~') });
189189
```
190190

191+
192+
### Stream
193+
194+
When interacting with dynamic LLMs or other streaming message providers, use the `stream` APIs to log messages from an iterable, even an async one.
195+
196+
```js
197+
import { stream } from '@clack/prompts';
198+
199+
stream.info((function *() { yield 'Info!'; })());
200+
stream.success((function *() { yield 'Success!'; })());
201+
stream.step((function *() { yield 'Step!'; })());
202+
stream.warn((function *() { yield 'Warn!'; })());
203+
stream.error((function *() { yield 'Error!'; })());
204+
stream.message((function *() { yield 'Hello'; yield ", World" })(), { symbol: color.cyan('~') });
205+
```
206+
191207
[clack-log-prompts](https://github.com/natemoo-re/clack/blob/main/.github/assets/clack-logs.png)

packages/prompts/src/index.ts

+42
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,48 @@ export const log = {
675675
},
676676
};
677677

678+
const prefix = `${color.gray(S_BAR)} `;
679+
export const stream = {
680+
message: async (iterable: Iterable<string>|AsyncIterable<string>, { symbol = color.gray(S_BAR) }: LogMessageOptions = {}) => {
681+
process.stdout.write(`${color.gray(S_BAR)}\n${symbol} `);
682+
let lineWidth = 3;
683+
for await (let chunk of iterable) {
684+
chunk = chunk.replace(/\n/g, `\n${prefix}`);
685+
if (chunk.includes('\n')) {
686+
lineWidth = 3 + strip(chunk.slice(chunk.lastIndexOf('\n'))).length;
687+
}
688+
const chunkLen = strip(chunk).length;
689+
if ((lineWidth + chunkLen) < process.stdout.columns) {
690+
lineWidth += chunkLen;
691+
process.stdout.write(chunk);
692+
} else {
693+
process.stdout.write(`\n${prefix}${chunk.trimStart()}`);
694+
lineWidth = 3 + strip(chunk.trimStart()).length;
695+
}
696+
}
697+
process.stdout.write('\n');
698+
},
699+
info: (iterable: Iterable<string>|AsyncIterable<string>) => {
700+
return stream.message(iterable, { symbol: color.blue(S_INFO) });
701+
},
702+
success: (iterable: Iterable<string>|AsyncIterable<string>) => {
703+
return stream.message(iterable, { symbol: color.green(S_SUCCESS) });
704+
},
705+
step: (iterable: Iterable<string>|AsyncIterable<string>) => {
706+
return stream.message(iterable, { symbol: color.green(S_STEP_SUBMIT) });
707+
},
708+
warn: (iterable: Iterable<string>|AsyncIterable<string>) => {
709+
return stream.message(iterable, { symbol: color.yellow(S_WARN) });
710+
},
711+
/** alias for `log.warn()`. */
712+
warning: (iterable: Iterable<string>|AsyncIterable<string>) => {
713+
return stream.warn(iterable);
714+
},
715+
error: (iterable: Iterable<string>|AsyncIterable<string>) => {
716+
return stream.message(iterable, { symbol: color.red(S_ERROR) });
717+
},
718+
}
719+
678720
export const spinner = () => {
679721
const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0'];
680722
const delay = unicode ? 80 : 120;

0 commit comments

Comments
 (0)