stdout-chat 0.4.0 → 0.4.1
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/CHANGELOG.md +13 -0
- package/bin/stdout-chat.js +4 -1
- package/lib/session.js +23 -3
- package/lib/ui.js +20 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `stdout-chat` (the CLI). Dates are release dates.
|
|
4
4
|
|
|
5
|
+
## [0.4.1] — 2026-09-20
|
|
6
|
+
|
|
7
|
+
### What's New
|
|
8
|
+
|
|
9
|
+
- Your own line shows up once. Hitting Enter used to leave `> hi` on screen and then the feed printed `in dmitrii hi` right under it — two copies of everything you said, while the app showed one. Now the typed line is wiped the moment you submit and the feed's copy (with its id, ready for `/r`) is the only one. If the post fails (`slow down · retry in 2s`, revoked key), the line comes back dim above the error so you can see what did not go out — ↑ still recalls it.
|
|
10
|
+
|
|
11
|
+
### Technical
|
|
12
|
+
|
|
13
|
+
- `lib/ui.js`: `eraseSubmitted(line)` — cursor up + clear for every row the echoed `> line` took (`ceil((prompt + line) / columns)`), then column 0. Must run synchronously from `onLine`, before any await: readline has just written the newline, so the row above the cursor is exactly the echo; the next `print`/`prompt(true)` re-draws the prompt as usual.
|
|
14
|
+
- `lib/session.js`: `echoesViaStream(line)` — true for plain text and `/r|/reply <id> text`, false for commands, `/r` usage and unknown slashes (the server's 422 keeps its `> /dance` context). `post(text, reply, { unsent })`: on failure prints `> <unsent>` via `info` (dim) before the error; `handleInput` passes the raw line for posts and replies only.
|
|
15
|
+
- `bin/stdout-chat.js`: `onLine` calls `ui.eraseSubmitted(line)` when `echoesViaStream(line)`, then `handleInput`.
|
|
16
|
+
- Tests: 96 → 99 (erase sequences for one row and a wrapped line; `echoesViaStream` table; failed post restores the line, a failed command does not).
|
|
17
|
+
|
|
5
18
|
## [0.4.0] — 2026-09-20
|
|
6
19
|
|
|
7
20
|
### What's New
|
package/bin/stdout-chat.js
CHANGED
|
@@ -5,7 +5,7 @@ import process from 'node:process';
|
|
|
5
5
|
import { createApi, DEFAULT_API, errorMessage } from '../lib/api.js';
|
|
6
6
|
import { loadConfig } from '../lib/config.js';
|
|
7
7
|
import { createUI } from '../lib/ui.js';
|
|
8
|
-
import { Session, SLASH_HINT } from '../lib/session.js';
|
|
8
|
+
import { Session, SLASH_HINT, echoesViaStream } from '../lib/session.js';
|
|
9
9
|
import { complete } from '../lib/complete.js';
|
|
10
10
|
import { renderError, renderInfo } from '../lib/render.js';
|
|
11
11
|
import { createNotifier } from '../lib/notify.js';
|
|
@@ -178,6 +178,9 @@ async function main() {
|
|
|
178
178
|
if (mode === 'interactive') {
|
|
179
179
|
ui.start({
|
|
180
180
|
onLine: async (line) => {
|
|
181
|
+
// Synchronous, before any await: the row above the cursor is still
|
|
182
|
+
// the echoed input. Its rendering arrives over the stream instead.
|
|
183
|
+
if (echoesViaStream(line)) ui.eraseSubmitted(line);
|
|
181
184
|
try {
|
|
182
185
|
await session.handleInput(line);
|
|
183
186
|
} catch (err) {
|
package/lib/session.js
CHANGED
|
@@ -12,6 +12,19 @@ import { NOTIFY_LEVELS } from './notify.js';
|
|
|
12
12
|
export const HINT_NO_KEY = 'type /key sc_… to post · get it in the app: /key';
|
|
13
13
|
// Printed once above the prompt when a line starts with `/` (see lib/ui.js).
|
|
14
14
|
export const SLASH_HINT = 'commands · /help · /r <id> text · /dm <nick|sid> · /top · /who · /key · /notify · /clear · /quit';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* True for input whose rendering comes back over the stream (a post, or a
|
|
18
|
+
* `/r <id> text` reply): the typed line is erased on submit so the feed shows
|
|
19
|
+
* the message once. Commands (`/top`, `/key …`, unknown slashes the server
|
|
20
|
+
* answers with 422) keep their `> /cmd` line as context.
|
|
21
|
+
*/
|
|
22
|
+
export function echoesViaStream(raw) {
|
|
23
|
+
const line = String(raw == null ? '' : raw).trim();
|
|
24
|
+
if (!line) return false;
|
|
25
|
+
if (line[0] !== '/') return true;
|
|
26
|
+
return /^\/(r|reply)\s+\S+\s+\S/.test(line);
|
|
27
|
+
}
|
|
15
28
|
export const HELP_TEXT = [
|
|
16
29
|
'/help this list',
|
|
17
30
|
'/r <id> text reply to a line (ids are the dim column on the left)',
|
|
@@ -293,7 +306,7 @@ export class Session {
|
|
|
293
306
|
async handleInput(raw) {
|
|
294
307
|
const line = String(raw == null ? '' : raw).trim();
|
|
295
308
|
if (!line) return;
|
|
296
|
-
if (line[0] !== '/') { await this.post(line); return; }
|
|
309
|
+
if (line[0] !== '/') { await this.post(line, null, { unsent: line }); return; }
|
|
297
310
|
const cmd = line.split(/\s+/, 1)[0].toLowerCase();
|
|
298
311
|
const rest = line.slice(cmd.length).trim();
|
|
299
312
|
switch (cmd) {
|
|
@@ -324,7 +337,7 @@ export class Session {
|
|
|
324
337
|
case '/r': case '/reply': {
|
|
325
338
|
const m = rest.match(/^(\S+)\s+([\s\S]+)$/);
|
|
326
339
|
if (!m) { this.info('usage: /r <id> text'); return; }
|
|
327
|
-
await this.post(m[2].trim(), m[1]);
|
|
340
|
+
await this.post(m[2].trim(), m[1], { unsent: line });
|
|
328
341
|
return;
|
|
329
342
|
}
|
|
330
343
|
default:
|
|
@@ -332,12 +345,19 @@ export class Session {
|
|
|
332
345
|
}
|
|
333
346
|
}
|
|
334
347
|
|
|
335
|
-
|
|
348
|
+
/**
|
|
349
|
+
* `unsent` is the raw input line to put back (dim, with its prompt) when the
|
|
350
|
+
* post fails: the UI erased the typed line on submit for lines that echo via
|
|
351
|
+
* the stream (see `echoesViaStream`), so an error alone would leave "slow
|
|
352
|
+
* down · retry in 2s" with nothing above it to retry.
|
|
353
|
+
*/
|
|
354
|
+
async post(text, reply = null, { unsent = null } = {}) {
|
|
336
355
|
if (!this.key) { this.info(HINT_NO_KEY); return false; }
|
|
337
356
|
try {
|
|
338
357
|
await this.api.post({ text, reply }, this.key); // echo arrives via SSE, not from the response
|
|
339
358
|
return true;
|
|
340
359
|
} catch (err) {
|
|
360
|
+
if (unsent) this.info(`> ${unsent}`);
|
|
341
361
|
this.error(errorMessage(err));
|
|
342
362
|
return false;
|
|
343
363
|
}
|
package/lib/ui.js
CHANGED
|
@@ -67,6 +67,25 @@ export function createUI({ input = process.stdin, output = process.stdout, promp
|
|
|
67
67
|
if (rl) rl.prompt(true);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Erase the line the user just submitted (readline has already echoed
|
|
72
|
+
* `> text` and moved to a fresh row). Call it synchronously from `onLine`,
|
|
73
|
+
* before anything else prints — the row above the cursor is then exactly
|
|
74
|
+
* that echo. A long input that wrapped takes several rows; all of them go.
|
|
75
|
+
* Used for lines whose real rendering comes back over the stream, so the
|
|
76
|
+
* feed shows a message once, not "> hi" and then "in dmitrii hi".
|
|
77
|
+
*/
|
|
78
|
+
function eraseSubmitted(line) {
|
|
79
|
+
if (!rl) return;
|
|
80
|
+
const cols = Math.max(1, output.columns || 80);
|
|
81
|
+
const rows = Math.max(1, Math.ceil((prompt.length + String(line == null ? '' : line).length) / cols));
|
|
82
|
+
for (let i = 0; i < rows; i++) {
|
|
83
|
+
readline.moveCursor(output, 0, -1);
|
|
84
|
+
readline.clearLine(output, 0);
|
|
85
|
+
}
|
|
86
|
+
readline.cursorTo(output, 0);
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
/** Remove entries matching `pred` from the in-memory input history (e.g. `/key sc_…`). */
|
|
71
90
|
function scrubHistory(pred) {
|
|
72
91
|
if (rl && Array.isArray(rl.history)) rl.history = rl.history.filter((h) => !pred(h));
|
|
@@ -77,6 +96,7 @@ export function createUI({ input = process.stdin, output = process.stdout, promp
|
|
|
77
96
|
start,
|
|
78
97
|
close,
|
|
79
98
|
clear,
|
|
99
|
+
eraseSubmitted,
|
|
80
100
|
scrubHistory,
|
|
81
101
|
get interactive() { return rl !== null; },
|
|
82
102
|
};
|