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 +3 -0
- package/package.json +39 -39
- package/src/main.mjs +33 -2
- package/src/tui.mjs +3 -1
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.
|
|
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')
|
|
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 };
|