sidekick-chat 0.2.3 → 0.2.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 CHANGED
@@ -81,6 +81,9 @@ sidekick --device 家里的电脑 "…" # 换一个署名(默认「终端」)
81
81
 
82
82
  其它会话来消息时只在终端里提一句,`/to` 切过去看。其它设备上聊的、手机上聊的,这里都能看到历史的最近 30 条。
83
83
 
84
+ **已读只在“人在终端前面”的时候推进**:没过按键的终端收到新消息只把进度攒着,等你下次敲键、发消息或切会话才一起报成已读。
85
+ 这样调下来的碎片(或被遗忘的)终端窗口不会把手机上刚弹出的通知瞬间顶掉——已读是多端同步的,任一客户端一推,其他设备就撤通知。
86
+
84
87
  ## 环境变量
85
88
 
86
89
  参数优先于环境变量,环境变量优先于配置文件:
package/package.json CHANGED
@@ -1,39 +1,39 @@
1
- {
2
- "name": "sidekick-chat",
3
- "version": "0.2.3",
4
- "description": "Sidekick 终端客户端:在终端里和秘书对话",
5
- "type": "module",
6
- "license": "PolyForm-Noncommercial-1.0.0",
7
- "engines": {
8
- "node": ">=22.19.0"
9
- },
10
- "bin": {
11
- "sidekick": "bin/sidekick.mjs"
12
- },
13
- "files": [
14
- "bin",
15
- "src",
16
- "README.md",
17
- "LICENSE"
18
- ],
19
- "scripts": {
20
- "test": "node --test"
21
- },
22
- "keywords": [
23
- "sidekick",
24
- "cli",
25
- "chat",
26
- "assistant"
27
- ],
28
- "repository": {
29
- "type": "git",
30
- "url": "git+https://github.com/Cznorth/sidekick.git",
31
- "directory": "cli"
32
- },
33
- "dependencies": {
34
- "@earendil-works/pi-tui": "1.0.0"
35
- },
36
- "devDependencies": {
37
- "@xterm/headless": "5.5.0"
38
- }
39
- }
1
+ {
2
+ "name": "sidekick-chat",
3
+ "version": "0.2.4",
4
+ "description": "Sidekick 终端客户端:在终端里和秘书对话",
5
+ "type": "module",
6
+ "license": "PolyForm-Noncommercial-1.0.0",
7
+ "engines": {
8
+ "node": ">=22.19.0"
9
+ },
10
+ "bin": {
11
+ "sidekick": "bin/sidekick.mjs"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "src",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "test": "node --test"
21
+ },
22
+ "keywords": [
23
+ "sidekick",
24
+ "cli",
25
+ "chat",
26
+ "assistant"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/Cznorth/sidekick.git",
31
+ "directory": "cli"
32
+ },
33
+ "dependencies": {
34
+ "@earendil-works/pi-tui": "1.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@xterm/headless": "5.5.0"
38
+ }
39
+ }
package/src/main.mjs CHANGED
@@ -271,6 +271,12 @@ async function startChat(flags) {
271
271
  let convId = pickConv(state, settings.conv);
272
272
  let latest = state.latest || 0;
273
273
  let everConnected = false;
274
+ // 「有人在看」才推进已读:调试时终端可以一直挂在那儿,而挂着的终端不该把手机上刚弹出的通知顶掉。
275
+ // 有按键才算在场(120 秒内),不在场时只把进度攒在 seenSeq 里,等下次动手再一起报。
276
+ const PRESENT_MS = 120_000;
277
+ // 0 = 启动以来还没人碰过这台终端:刚起来的窗口也不算在场,只靠 printHistory 那个显式动作推一次。
278
+ let lastInputAt = 0;
279
+ let seenSeq = 0;
274
280
  const displayed = new Set();
275
281
  const ctl = new AbortController();
276
282
  let rl = null;
@@ -285,6 +291,22 @@ async function startChat(flags) {
285
291
  };
286
292
  const typing = (on) => ui?.typing(on);
287
293
 
294
+ /** 把「已经打到屏幕上」的最新 seq 报成已读。 */
295
+ function reportSeen() {
296
+ if (!seenSeq) return;
297
+ const seq = seenSeq;
298
+ seenSeq = 0;
299
+ api.read(convId, seq).catch(() => {});
300
+ }
301
+
302
+ /** 用户在这台终端上动了一下(敲键、发消息、切会话):算在场,并把攒着的已读补报上去。 */
303
+ function markPresent() {
304
+ lastInputAt = Date.now();
305
+ reportSeen();
306
+ }
307
+
308
+ const present = () => lastInputAt > 0 && Date.now() - lastInputAt < PRESENT_MS;
309
+
288
310
  const printMessage = (message) => {
289
311
  const options = { bots: state.bots, color, imageUrl: (name) => api.imageUrl(name) };
290
312
  if (ui) ui.message(message, options);
@@ -292,7 +314,8 @@ async function startChat(flags) {
292
314
  };
293
315
  if (vt) {
294
316
  const { createChatUI } = await import('./tui.mjs');
295
- ui = createChatUI({ color, onSubmit: (text) => {
317
+ ui = createChatUI({ color, onInput: () => markPresent(), onSubmit: (text) => {
318
+ markPresent();
296
319
  queue.push(() => text.startsWith('/') ? handleSlash(text) : sendText(text));
297
320
  void drain();
298
321
  }, onQuit: () => shutdown(0) });
@@ -310,6 +333,8 @@ async function startChat(flags) {
310
333
  printMessage(m);
311
334
  }
312
335
  latest = Math.max(latest, body.latest || 0);
336
+ // 刚把这段历史打到屏幕上(启动、切会话、/history),已读位置就到这里。
337
+ seenSeq = 0;
313
338
  if (body.latest) api.read(convId, body.latest).catch(() => {});
314
339
  if (body.messages.length === 0) out(paint(color, 'gray', '(这里还没有消息)'));
315
340
  }
@@ -344,7 +369,10 @@ async function startChat(flags) {
344
369
  return;
345
370
  }
346
371
  printMessage(m);
347
- if (typeof m.seq === 'number') api.read(convId, m.seq).catch(() => {});
372
+ if (typeof m.seq === 'number') {
373
+ if (present()) api.read(convId, m.seq).catch(() => {});
374
+ else seenSeq = Math.max(seenSeq, m.seq);
375
+ }
348
376
  }
349
377
 
350
378
  streamTask = api
@@ -382,6 +410,7 @@ async function startChat(flags) {
382
410
 
383
411
  function switchConv(next) {
384
412
  ui?.clearDrafts();
413
+ markPresent();
385
414
  convId = next;
386
415
  const cfg = { ...readConfig(), url: settings.url, conv: convId, device: settings.device };
387
416
  writeConfig(cfg);
@@ -485,6 +514,7 @@ async function startChat(flags) {
485
514
  rl.on('line', (line) => {
486
515
  const text = line.trim();
487
516
  if (!text) return;
517
+ markPresent();
488
518
  queue.push(() => (text.startsWith('/') ? handleSlash(text) : sendText(text)));
489
519
  void drain();
490
520
  });
@@ -497,6 +527,7 @@ async function startChat(flags) {
497
527
  function shutdown(code = 0) {
498
528
  if (closed) return;
499
529
  closed = true;
530
+ reportSeen();
500
531
  typing(false);
501
532
  quotas.stop();
502
533
  ui?.stop();
package/src/tui.mjs CHANGED
@@ -12,7 +12,7 @@ export function markdownTheme(color) {
12
12
  }
13
13
 
14
14
  /** Pi's renderer owns every screen write, including the editor and busy status. */
15
- export function createChatUI({ color, onSubmit, onQuit, terminal = new ProcessTerminal() }) {
15
+ export function createChatUI({ color, onSubmit, onQuit, onInput, terminal = new ProcessTerminal() }) {
16
16
  const tui = new TuiMainScreen(terminal);
17
17
  const transcript = new Container();
18
18
  const live = new Container();
@@ -36,6 +36,8 @@ export function createChatUI({ color, onSubmit, onQuit, terminal = new ProcessTe
36
36
  tui.addChild(new Text(paint(color, 'gray', 'Enter 发送 · Shift+Enter 换行 · ↑ 历史 · /help · Ctrl+C 退出'), 1, 0));
37
37
  tui.setFocus(editor);
38
38
  tui.addInputListener((data) => {
39
+ // 任何一个按键都算「人在终端前面」:调用方据此决定要不要把新消息标成已读。
40
+ onInput?.(data);
39
41
  if (data === '\u0003' || data === '\u0004' && !editor.getText()) {
40
42
  onQuit();
41
43
  return { consume: true };