@will-17173/telegram-cli 0.3.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/README.md +73 -335
- package/README.zh-CN.md +72 -333
- package/dist/account/account-authenticator.js +95 -0
- package/dist/account/account-authenticator.js.map +1 -0
- package/dist/account/account-context.js +7 -0
- package/dist/account/account-context.js.map +1 -1
- package/dist/account/account-name.js +17 -0
- package/dist/account/account-name.js.map +1 -0
- package/dist/account/account-presets.js +20 -3
- package/dist/account/account-presets.js.map +1 -1
- package/dist/account/account-store.js +277 -37
- package/dist/account/account-store.js.map +1 -1
- package/dist/cli/app.js +12 -1
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/output.js +21 -6
- package/dist/cli/output.js.map +1 -1
- package/dist/cli/secure-input.js +408 -0
- package/dist/cli/secure-input.js.map +1 -0
- package/dist/commands/account-options.js +37 -7
- package/dist/commands/account-options.js.map +1 -1
- package/dist/commands/account.js +144 -62
- package/dist/commands/account.js.map +1 -1
- package/dist/commands/archive.js +151 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/config.js +119 -12
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/contact.js +58 -0
- package/dist/commands/contact.js.map +1 -0
- package/dist/commands/data.js +14 -7
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/dialog.js +156 -0
- package/dist/commands/dialog.js.map +1 -0
- package/dist/commands/folder.js +113 -0
- package/dist/commands/folder.js.map +1 -0
- package/dist/commands/group-write.js +45 -5
- package/dist/commands/group-write.js.map +1 -1
- package/dist/commands/group.js +23 -1
- package/dist/commands/group.js.map +1 -1
- package/dist/commands/notification.js +76 -0
- package/dist/commands/notification.js.map +1 -0
- package/dist/commands/query.js +22 -15
- package/dist/commands/query.js.map +1 -1
- package/dist/commands/telegram-runner.js +40 -18
- package/dist/commands/telegram-runner.js.map +1 -1
- package/dist/commands/telegram.js +82 -38
- package/dist/commands/telegram.js.map +1 -1
- package/dist/commands/time-range.js +39 -0
- package/dist/commands/time-range.js.map +1 -0
- package/dist/commands/types.js +3 -2
- package/dist/commands/types.js.map +1 -1
- package/dist/config/credential-store.js +24 -4
- package/dist/config/credential-store.js.map +1 -1
- package/dist/config/env.js +4 -1
- package/dist/config/env.js.map +1 -1
- package/dist/group-commands/catalog.js +10 -0
- package/dist/group-commands/catalog.js.map +1 -1
- package/dist/group-commands/executor.js +13 -4
- package/dist/group-commands/executor.js.map +1 -1
- package/dist/presenters/group.js +16 -0
- package/dist/presenters/group.js.map +1 -1
- package/dist/presenters/human.js +113 -0
- package/dist/presenters/human.js.map +1 -1
- package/dist/presenters/ink/group-command-result.js +2 -0
- package/dist/presenters/ink/group-command-result.js.map +1 -1
- package/dist/presenters/ink/listen-scroll.js +4 -3
- package/dist/presenters/ink/listen-scroll.js.map +1 -1
- package/dist/presenters/ink/listen.js +245 -57
- package/dist/presenters/ink/listen.js.map +1 -1
- package/dist/presenters/ink/mouse-scroll.js +12 -0
- package/dist/presenters/ink/mouse-scroll.js.map +1 -1
- package/dist/presenters/ink/secure-input.js +68 -0
- package/dist/presenters/ink/secure-input.js.map +1 -0
- package/dist/presenters/ink/use-group-command.js +78 -17
- package/dist/presenters/ink/use-group-command.js.map +1 -1
- package/dist/presenters/markdown.js +54 -0
- package/dist/presenters/markdown.js.map +1 -0
- package/dist/presenters/structured.js +8 -3
- package/dist/presenters/structured.js.map +1 -1
- package/dist/services/account-session-service.js +241 -0
- package/dist/services/account-session-service.js.map +1 -0
- package/dist/services/archive-layout.js +61 -0
- package/dist/services/archive-layout.js.map +1 -0
- package/dist/services/archive-manifest.js +187 -0
- package/dist/services/archive-manifest.js.map +1 -0
- package/dist/services/archive-markdown.js +383 -0
- package/dist/services/archive-markdown.js.map +1 -0
- package/dist/services/archive-service.js +718 -0
- package/dist/services/archive-service.js.map +1 -0
- package/dist/services/archive-types.js +2 -0
- package/dist/services/archive-types.js.map +1 -0
- package/dist/services/attachment-download.js +7 -2
- package/dist/services/attachment-download.js.map +1 -1
- package/dist/services/auto-download-coordinator.js +2 -4
- package/dist/services/auto-download-coordinator.js.map +1 -1
- package/dist/services/contact-service.js +78 -0
- package/dist/services/contact-service.js.map +1 -0
- package/dist/services/dialog-service.js +140 -0
- package/dist/services/dialog-service.js.map +1 -0
- package/dist/services/folder-service.js +200 -0
- package/dist/services/folder-service.js.map +1 -0
- package/dist/services/group-write-service.js +78 -18
- package/dist/services/group-write-service.js.map +1 -1
- package/dist/services/message-service.js +17 -1
- package/dist/services/message-service.js.map +1 -1
- package/dist/services/notification-service.js +142 -0
- package/dist/services/notification-service.js.map +1 -0
- package/dist/services/write-access-policy.js +19 -0
- package/dist/services/write-access-policy.js.map +1 -0
- package/dist/telegram/archive-types.js +2 -0
- package/dist/telegram/archive-types.js.map +1 -0
- package/dist/telegram/client-factory.js +51 -11
- package/dist/telegram/client-factory.js.map +1 -1
- package/dist/telegram/contact-types.js +10 -0
- package/dist/telegram/contact-types.js.map +1 -0
- package/dist/telegram/dialog-types.js +2 -0
- package/dist/telegram/dialog-types.js.map +1 -0
- package/dist/telegram/errors.js +14 -0
- package/dist/telegram/errors.js.map +1 -0
- package/dist/telegram/fake-group-management.js +5 -0
- package/dist/telegram/fake-group-management.js.map +1 -1
- package/dist/telegram/folder-types.js +13 -0
- package/dist/telegram/folder-types.js.map +1 -0
- package/dist/telegram/group-write-types.js +28 -1
- package/dist/telegram/group-write-types.js.map +1 -1
- package/dist/telegram/mtcute-archive.js +190 -0
- package/dist/telegram/mtcute-archive.js.map +1 -0
- package/dist/telegram/mtcute-client.js +72 -11
- package/dist/telegram/mtcute-client.js.map +1 -1
- package/dist/telegram/mtcute-contacts.js +90 -0
- package/dist/telegram/mtcute-contacts.js.map +1 -0
- package/dist/telegram/mtcute-dialogs.js +186 -0
- package/dist/telegram/mtcute-dialogs.js.map +1 -0
- package/dist/telegram/mtcute-folders.js +318 -0
- package/dist/telegram/mtcute-folders.js.map +1 -0
- package/dist/telegram/mtcute-group-members.js +43 -3
- package/dist/telegram/mtcute-group-members.js.map +1 -1
- package/dist/telegram/mtcute-message-normalizer.js +63 -0
- package/dist/telegram/mtcute-message-normalizer.js.map +1 -0
- package/dist/telegram/mtcute-notifications.js +101 -0
- package/dist/telegram/mtcute-notifications.js.map +1 -0
- package/dist/telegram/notification-types.js +11 -0
- package/dist/telegram/notification-types.js.map +1 -0
- package/dist/telegram/types.js +6 -1
- package/dist/telegram/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -5,15 +5,15 @@ import stringWidth from 'string-width';
|
|
|
5
5
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { dirname } from 'node:path';
|
|
8
|
-
import { resolveAttachmentDestination } from '../../services/attachment-download.js';
|
|
8
|
+
import { attachmentDownloadProgress, resolveAttachmentDestination, } from '../../services/attachment-download.js';
|
|
9
9
|
import { ListenAlbumAggregator } from '../../services/listen-album-aggregator.js';
|
|
10
10
|
import { AutoDownloadCoordinator } from '../../services/auto-download-coordinator.js';
|
|
11
11
|
import { attachmentDownloadTarget, attachmentFileName, discoverListenAttachments, listenAttachmentKey } from '../../services/listen-attachment.js';
|
|
12
12
|
import { buildListenMessage } from '../listen-message.js';
|
|
13
|
-
import { applyMessageArrival, takeListenViewport } from './listen-scroll.js';
|
|
13
|
+
import { applyMessageArrival, applyScroll, takeListenViewport } from './listen-scroll.js';
|
|
14
14
|
import { decodeImagePreview } from './image-preview.js';
|
|
15
15
|
import { ListenScrollbar, calculateScrollbar, listenContentWidth, useTransientScrollbar } from './listen-scrollbar.js';
|
|
16
|
-
import { DISABLE_MOUSE_REPORTING, isMouseInput } from './mouse-scroll.js';
|
|
16
|
+
import { DISABLE_MOUSE_REPORTING, ENABLE_MOUSE_REPORTING, isMouseInput, useMouseScroll, withAlternateScroll, } from './mouse-scroll.js';
|
|
17
17
|
import { createListenReplyResolver } from '../../services/listen-reply-resolver.js';
|
|
18
18
|
import { formatReplyContext } from '../../services/reply-context.js';
|
|
19
19
|
import { executeListenReply, parseListenComposerInput } from '../../services/listen-composer-command.js';
|
|
@@ -25,9 +25,12 @@ import { useGroupCommand } from './use-group-command.js';
|
|
|
25
25
|
import { executeGroupCommand } from '../../group-commands/executor.js';
|
|
26
26
|
import { GroupWriteService } from '../../services/group-write-service.js';
|
|
27
27
|
import { ADMIN_RIGHT_KEYS } from '../../services/group-write-service.js';
|
|
28
|
+
import { WriteAccessPolicy } from '../../services/write-access-policy.js';
|
|
28
29
|
import { GroupCommandConfirm } from './group-command-confirm.js';
|
|
29
30
|
import { truncateCell } from './display-width.js';
|
|
31
|
+
import { SecureInput } from './secure-input.js';
|
|
30
32
|
const MESSAGE_SEPARATOR = '────────────────────────────────────────────';
|
|
33
|
+
const OWNERSHIP_SHUTDOWN_GRACE_MS = 250;
|
|
31
34
|
/** Maximum number of grouped messages retained by a long-running interactive listener. */
|
|
32
35
|
export const LISTEN_HISTORY_LIMIT = 500;
|
|
33
36
|
const LISTEN_IMAGE_PREVIEWS_ENABLED = false;
|
|
@@ -60,11 +63,14 @@ export const LISTEN_COMPOSER_THEME = {
|
|
|
60
63
|
target: '#f0d38a',
|
|
61
64
|
hint: '#9bdca8',
|
|
62
65
|
};
|
|
63
|
-
export function
|
|
66
|
+
export function listenComposerCursorColor(visible) {
|
|
67
|
+
return visible ? LISTEN_COMPOSER_THEME.cursor : LISTEN_COMPOSER_THEME.background;
|
|
68
|
+
}
|
|
69
|
+
export function ListenComposer({ input, sendTargetLabel, terminalWidth, sending = false, hint = 'Enter to send · Ctrl+C to exit', cursorVisible = true, }) {
|
|
64
70
|
const inputText = `› ${input}${sending ? ' (sending...)' : ''}`;
|
|
65
71
|
const blankRow = ' '.repeat(Math.max(0, terminalWidth));
|
|
66
72
|
const inputRowFill = ' '.repeat(Math.max(0, terminalWidth - stringWidth(inputText) - 1));
|
|
67
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { backgroundColor: LISTEN_COMPOSER_THEME.background, children: blankRow }), _jsxs(Text, { backgroundColor: LISTEN_COMPOSER_THEME.background, color: LISTEN_COMPOSER_THEME.foreground, children: [inputText, _jsx(Text, { backgroundColor:
|
|
73
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { backgroundColor: LISTEN_COMPOSER_THEME.background, children: blankRow }), _jsxs(Text, { backgroundColor: LISTEN_COMPOSER_THEME.background, color: LISTEN_COMPOSER_THEME.foreground, children: [inputText, _jsx(Text, { backgroundColor: listenComposerCursorColor(cursorVisible), children: " " }), inputRowFill] }), _jsx(Text, { backgroundColor: LISTEN_COMPOSER_THEME.background, children: blankRow }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: LISTEN_COMPOSER_THEME.target, children: ["To: ", sendTargetLabel] }), _jsx(Text, { color: LISTEN_COMPOSER_THEME.hint, children: hint })] })] }));
|
|
68
74
|
}
|
|
69
75
|
export function ListenAttachmentLine({ label, selected, state }) {
|
|
70
76
|
const action = state.status === 'idle'
|
|
@@ -196,16 +202,23 @@ export async function runInteractiveAutoDownloadLifecycle(options) {
|
|
|
196
202
|
: null;
|
|
197
203
|
options.onCoordinator?.(coordinator);
|
|
198
204
|
let currentClient = null;
|
|
205
|
+
let closeCurrentClient = null;
|
|
199
206
|
const abort = () => {
|
|
200
207
|
coordinator?.stop();
|
|
201
|
-
void
|
|
208
|
+
void closeCurrentClient?.();
|
|
202
209
|
};
|
|
203
210
|
options.signal.addEventListener('abort', abort);
|
|
204
211
|
try {
|
|
205
212
|
while (!options.signal.aborted) {
|
|
206
213
|
options.onStatus?.('connecting');
|
|
207
214
|
const client = options.createClient();
|
|
215
|
+
let closePromise;
|
|
216
|
+
const closeClient = () => {
|
|
217
|
+
closePromise ??= Promise.resolve().then(() => client.close()).catch(() => undefined);
|
|
218
|
+
return closePromise;
|
|
219
|
+
};
|
|
208
220
|
currentClient = client;
|
|
221
|
+
closeCurrentClient = closeClient;
|
|
209
222
|
options.onClient?.(client);
|
|
210
223
|
coordinator?.setClient(client);
|
|
211
224
|
let retry = false;
|
|
@@ -249,11 +262,13 @@ export async function runInteractiveAutoDownloadLifecycle(options) {
|
|
|
249
262
|
else {
|
|
250
263
|
coordinator?.stop();
|
|
251
264
|
}
|
|
252
|
-
await
|
|
265
|
+
await closeClient();
|
|
253
266
|
if (options.signal.aborted)
|
|
254
267
|
await coordinator?.waitForActive();
|
|
255
268
|
if (currentClient === client)
|
|
256
269
|
currentClient = null;
|
|
270
|
+
if (closeCurrentClient === closeClient)
|
|
271
|
+
closeCurrentClient = null;
|
|
257
272
|
options.onClient?.(null);
|
|
258
273
|
}
|
|
259
274
|
if (!retry)
|
|
@@ -376,8 +391,7 @@ export function calculateListenMessagePaneHeight(terminalHeight, hasNote, autoDo
|
|
|
376
391
|
return Math.max(2, terminalHeight - reservedLines);
|
|
377
392
|
}
|
|
378
393
|
export async function runInteractiveListen(write, run) {
|
|
379
|
-
write
|
|
380
|
-
return run();
|
|
394
|
+
return withAlternateScroll({ write, run });
|
|
381
395
|
}
|
|
382
396
|
export async function renderInteractiveListen(options) {
|
|
383
397
|
await runInteractiveListen(process.stdout.write.bind(process.stdout), async () => {
|
|
@@ -385,7 +399,7 @@ export async function renderInteractiveListen(options) {
|
|
|
385
399
|
await app.waitUntilExit();
|
|
386
400
|
});
|
|
387
401
|
}
|
|
388
|
-
export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo, showMedia, autoDownload, showChatName, createClient, stopSignal, onRequestStop, createReplyResolver, }) {
|
|
402
|
+
export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo, showMedia, autoDownload, showChatName, createClient, stopSignal, onRequestStop, createReplyResolver, shutdownRequests, }) {
|
|
389
403
|
const { exit } = useApp();
|
|
390
404
|
const { stdout } = useStdout();
|
|
391
405
|
const terminalMetrics = useTerminalMetrics(stdout);
|
|
@@ -395,19 +409,23 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
395
409
|
const [note, setNote] = useState('');
|
|
396
410
|
const [sending, setSending] = useState(false);
|
|
397
411
|
const [focus, setFocus] = useState('input');
|
|
398
|
-
const [
|
|
412
|
+
const [selectedAttachmentKey, setSelectedAttachmentKey] = useState(null);
|
|
399
413
|
const [downloadStates, setDownloadStates] = useState({});
|
|
400
414
|
const [scrollState, setScrollState] = useState({ offset: 0, unseenCount: 0 });
|
|
401
415
|
const [sendTargetLabel, setSendTargetLabel] = useState(sendTo == null ? '' : buildSendTargetLabel(sendTo));
|
|
402
416
|
const [knownGroup, setKnownGroup] = useState(undefined);
|
|
403
417
|
const clientRef = useRef(null);
|
|
404
418
|
const replyExecutionLockRef = useRef(false);
|
|
405
|
-
const inputGenerationRef = useRef(
|
|
419
|
+
const inputGenerationRef = useRef({});
|
|
406
420
|
const commandSelectionRef = useRef(0);
|
|
407
421
|
const knownGroupRef = useRef(undefined);
|
|
408
|
-
const groupLookupGenerationRef = useRef(
|
|
422
|
+
const groupLookupGenerationRef = useRef({});
|
|
423
|
+
const selectedAttachmentKeyRef = useRef(null);
|
|
424
|
+
const scrollOffsetRef = useRef(0);
|
|
425
|
+
selectedAttachmentKeyRef.current = selectedAttachmentKey;
|
|
426
|
+
scrollOffsetRef.current = scrollState.offset;
|
|
409
427
|
useEffect(() => {
|
|
410
|
-
groupLookupGenerationRef.current
|
|
428
|
+
groupLookupGenerationRef.current = {};
|
|
411
429
|
knownGroupRef.current = undefined;
|
|
412
430
|
setKnownGroup(undefined);
|
|
413
431
|
}, [sendTo]);
|
|
@@ -418,19 +436,25 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
418
436
|
if (sendTo == null)
|
|
419
437
|
return { ok: false, error: { code: 'ambiguous_chat', message: 'Select exactly one target chat with --send-to.' } };
|
|
420
438
|
let knownGroup = knownGroupRef.current;
|
|
421
|
-
|
|
422
|
-
|
|
439
|
+
const refreshOwnershipCapability = request.key === 'admin transfer-owner'
|
|
440
|
+
&& options?.confirmed === true
|
|
441
|
+
&& options.ownershipPassword == null;
|
|
442
|
+
if (knownGroup == null || refreshOwnershipCapability) {
|
|
443
|
+
const lookup = {};
|
|
444
|
+
groupLookupGenerationRef.current = lookup;
|
|
423
445
|
knownGroup = await client.groups.getGroup(sendTo);
|
|
424
|
-
if (lookup
|
|
425
|
-
|
|
426
|
-
setKnownGroup(knownGroup);
|
|
446
|
+
if (lookup !== groupLookupGenerationRef.current || clientRef.current !== client) {
|
|
447
|
+
return { ok: false, error: { code: 'connection_not_ready', message: 'Telegram connection changed during group verification.' } };
|
|
427
448
|
}
|
|
449
|
+
knownGroupRef.current = knownGroup;
|
|
450
|
+
setKnownGroup(knownGroup);
|
|
428
451
|
}
|
|
429
452
|
return executeGroupCommand(request, {
|
|
430
453
|
chat: sendTo,
|
|
431
454
|
groups: new GroupWriteService(client.groups),
|
|
432
455
|
confirmed: options?.confirmed ?? false,
|
|
433
456
|
confirmationTitle: options?.confirmationTitle,
|
|
457
|
+
ownershipPassword: options?.ownershipPassword,
|
|
434
458
|
knownGroup,
|
|
435
459
|
connectionReady: true,
|
|
436
460
|
targetAvailable: true,
|
|
@@ -440,7 +464,8 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
440
464
|
setKnownGroup(undefined);
|
|
441
465
|
if (request.key === 'chat delete' || request.key === 'chat leave')
|
|
442
466
|
return;
|
|
443
|
-
const lookup =
|
|
467
|
+
const lookup = {};
|
|
468
|
+
groupLookupGenerationRef.current = lookup;
|
|
444
469
|
try {
|
|
445
470
|
const refreshed = await client.groups.getGroup(sendTo);
|
|
446
471
|
if (lookup === groupLookupGenerationRef.current && clientRef.current === client) {
|
|
@@ -455,6 +480,8 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
455
480
|
},
|
|
456
481
|
});
|
|
457
482
|
}, [sendTo]));
|
|
483
|
+
const irreversibleGroupWriteRef = useRef(false);
|
|
484
|
+
irreversibleGroupWriteRef.current = groupCommand.state.kind === 'executing' && groupCommand.state.irreversible === true;
|
|
458
485
|
const terminalWidth = terminalMetrics.columns;
|
|
459
486
|
const terminalHeight = terminalMetrics.rows;
|
|
460
487
|
const previewColorDepth = interactiveListenPreviewColorDepth(terminalMetrics.colorDepth);
|
|
@@ -479,22 +506,83 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
479
506
|
if (operationControllerRef.current == null)
|
|
480
507
|
operationControllerRef.current = createInteractiveOperationController();
|
|
481
508
|
const stoppingRef = useRef(false);
|
|
509
|
+
const lifecycleStopRef = useRef(null);
|
|
510
|
+
const deferredStopRef = useRef(false);
|
|
511
|
+
const shutdownRequestedRef = useRef(false);
|
|
512
|
+
const shutdownGraceTimerRef = useRef(null);
|
|
513
|
+
function requestOwnershipShutdown(source) {
|
|
514
|
+
if (deferredStopRef.current || shutdownRequestedRef.current) {
|
|
515
|
+
deferredStopRef.current = false;
|
|
516
|
+
shutdownRequestedRef.current = false;
|
|
517
|
+
if (shutdownGraceTimerRef.current)
|
|
518
|
+
clearTimeout(shutdownGraceTimerRef.current);
|
|
519
|
+
shutdownGraceTimerRef.current = null;
|
|
520
|
+
groupCommand.setState({ kind: 'error', message: 'Ownership transfer outcome is indeterminate after forced shutdown.' });
|
|
521
|
+
setTimeout(stopListening, 0);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (source === 'terminal') {
|
|
525
|
+
deferredStopRef.current = true;
|
|
526
|
+
setNote('Group write is already running; wait for its outcome.');
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
shutdownRequestedRef.current = true;
|
|
530
|
+
setNote('Shutdown requested; waiting briefly for the ownership transfer outcome.');
|
|
531
|
+
shutdownGraceTimerRef.current = setTimeout(() => {
|
|
532
|
+
shutdownGraceTimerRef.current = null;
|
|
533
|
+
groupCommand.setState({
|
|
534
|
+
kind: 'error',
|
|
535
|
+
message: 'Ownership transfer outcome is indeterminate because shutdown grace expired.',
|
|
536
|
+
});
|
|
537
|
+
setTimeout(stopListening, 0);
|
|
538
|
+
}, OWNERSHIP_SHUTDOWN_GRACE_MS);
|
|
539
|
+
}
|
|
482
540
|
const seenRef = useRef(new Set());
|
|
483
541
|
const seenOrderRef = useRef([]);
|
|
484
|
-
const downloadableAttachments = collectDownloadableAttachments(
|
|
485
|
-
const
|
|
486
|
-
const
|
|
542
|
+
const downloadableAttachments = collectDownloadableAttachments(messages);
|
|
543
|
+
const visibleDownloadableAttachments = collectDownloadableAttachments(visibleMessages);
|
|
544
|
+
const selectedAttachment = downloadableAttachments.find((item) => item.key === selectedAttachmentKey);
|
|
545
|
+
const selectAttachment = (key) => {
|
|
546
|
+
selectedAttachmentKeyRef.current = key;
|
|
547
|
+
setSelectedAttachmentKey(key);
|
|
548
|
+
};
|
|
549
|
+
const { visible: scrollbarVisible, show: showScrollbar } = useTransientScrollbar();
|
|
487
550
|
const scrollbarGeometry = calculateScrollbar({
|
|
488
551
|
height: terminalHeight,
|
|
489
552
|
total: messages.length,
|
|
490
553
|
visible: visibleMessages.length,
|
|
491
554
|
offset: scrollState.offset,
|
|
492
555
|
});
|
|
556
|
+
const lastScrollDirectionRef = useRef('up');
|
|
557
|
+
const scrollMessages = useCallback((direction, amount = 1) => {
|
|
558
|
+
lastScrollDirectionRef.current = direction;
|
|
559
|
+
const step = Math.max(1, Math.floor(amount));
|
|
560
|
+
const maxOffset = Math.max(0, messages.length - 1);
|
|
561
|
+
scrollOffsetRef.current = direction === 'up'
|
|
562
|
+
? Math.min(maxOffset, scrollOffsetRef.current + step)
|
|
563
|
+
: Math.max(0, scrollOffsetRef.current - step);
|
|
564
|
+
showScrollbar();
|
|
565
|
+
setScrollState((current) => applyScroll(current, direction, maxOffset, amount));
|
|
566
|
+
}, [messages.length, showScrollbar]);
|
|
567
|
+
const handleMouseScroll = useCallback((direction) => {
|
|
568
|
+
if (focus === 'attachments')
|
|
569
|
+
scrollMessages(direction);
|
|
570
|
+
}, [focus, scrollMessages]);
|
|
571
|
+
useMouseScroll(handleMouseScroll);
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
if (focus !== 'attachments')
|
|
574
|
+
return;
|
|
575
|
+
stdout.write(ENABLE_MOUSE_REPORTING);
|
|
576
|
+
return () => {
|
|
577
|
+
stdout.write(DISABLE_MOUSE_REPORTING);
|
|
578
|
+
};
|
|
579
|
+
}, [focus, stdout]);
|
|
493
580
|
useEffect(() => {
|
|
494
581
|
const maxOffset = Math.max(0, messages.length - 1);
|
|
495
582
|
setScrollState((current) => {
|
|
496
583
|
const offset = Math.min(current.offset, maxOffset);
|
|
497
584
|
const unseenCount = offset === 0 ? 0 : Math.min(current.unseenCount, messages.length);
|
|
585
|
+
scrollOffsetRef.current = offset;
|
|
498
586
|
return offset === current.offset && unseenCount === current.unseenCount
|
|
499
587
|
? current
|
|
500
588
|
: { offset, unseenCount };
|
|
@@ -505,18 +593,46 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
505
593
|
setDownloadStates((current) => pruneAttachmentDownloadStates(current, validAttachmentKeys, pendingAttachmentKeysRef.current));
|
|
506
594
|
for (const key of validAttachmentKeys)
|
|
507
595
|
pendingAttachmentKeysRef.current.delete(key);
|
|
508
|
-
|
|
596
|
+
const currentSelectedKey = selectedAttachmentKeyRef.current;
|
|
597
|
+
if (currentSelectedKey != null && !validAttachmentKeys.has(currentSelectedKey)) {
|
|
598
|
+
selectAttachment(downloadableAttachments[0]?.key ?? null);
|
|
599
|
+
}
|
|
509
600
|
if (downloadableAttachments.length === 0)
|
|
510
601
|
setFocus('input');
|
|
511
602
|
}, [messages, downloadableAttachments.length]);
|
|
603
|
+
useEffect(() => {
|
|
604
|
+
if (focus !== 'attachments' || visibleDownloadableAttachments.length === 0)
|
|
605
|
+
return;
|
|
606
|
+
if (visibleDownloadableAttachments.some((item) => item.key === selectedAttachment?.key))
|
|
607
|
+
return;
|
|
608
|
+
const fallback = lastScrollDirectionRef.current === 'up'
|
|
609
|
+
? visibleDownloadableAttachments.at(-1)
|
|
610
|
+
: visibleDownloadableAttachments[0];
|
|
611
|
+
const index = fallback == null
|
|
612
|
+
? -1
|
|
613
|
+
: downloadableAttachments.findIndex((item) => item.key === fallback.key);
|
|
614
|
+
if (index >= 0)
|
|
615
|
+
selectAttachment(downloadableAttachments[index].key);
|
|
616
|
+
}, [focus, messagePaneHeight, messages, scrollState.offset, selectedAttachmentKey]);
|
|
512
617
|
useInput((inputText, key) => {
|
|
513
618
|
if (isMouseInput(inputText))
|
|
514
619
|
return;
|
|
620
|
+
const modal = groupCommand.state;
|
|
621
|
+
if (modal.kind === 'executing' && modal.irreversible === true) {
|
|
622
|
+
if (key.ctrl && (inputText === 'c' || inputText === 'C' || inputText === '\u0003')) {
|
|
623
|
+
requestOwnershipShutdown('terminal');
|
|
624
|
+
}
|
|
625
|
+
else if (key.escape) {
|
|
626
|
+
setNote('Group write is already running; wait for its outcome.');
|
|
627
|
+
}
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
515
630
|
if (key.ctrl && (inputText === 'c' || inputText === 'C' || inputText === '\u0003')) {
|
|
516
631
|
stopListening();
|
|
517
632
|
return;
|
|
518
633
|
}
|
|
519
|
-
|
|
634
|
+
if (modal.kind === 'password')
|
|
635
|
+
return;
|
|
520
636
|
if (modal.kind === 'confirm') {
|
|
521
637
|
if (key.escape) {
|
|
522
638
|
closeGroupCommand();
|
|
@@ -556,7 +672,8 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
556
672
|
}
|
|
557
673
|
else {
|
|
558
674
|
const submittedTitle = modal.confirmText;
|
|
559
|
-
const lookup =
|
|
675
|
+
const lookup = {};
|
|
676
|
+
groupLookupGenerationRef.current = lookup;
|
|
560
677
|
void client.groups.getGroup(sendTo).then((fresh) => {
|
|
561
678
|
if (lookup !== groupLookupGenerationRef.current || clientRef.current !== client)
|
|
562
679
|
return;
|
|
@@ -618,12 +735,17 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
618
735
|
closeGroupCommand();
|
|
619
736
|
return;
|
|
620
737
|
}
|
|
738
|
+
if (replyExecutionLockRef.current) {
|
|
739
|
+
if (key.escape)
|
|
740
|
+
setNote('Reply is still sending; wait for its outcome.');
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (key.pageUp || key.pageDown) {
|
|
744
|
+
scrollMessages(key.pageUp ? 'up' : 'down', Math.max(1, visibleMessages.length));
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
621
747
|
const slashMode = input.trimStart().startsWith('/');
|
|
622
748
|
if (slashMode && key.escape) {
|
|
623
|
-
if (replyExecutionLockRef.current) {
|
|
624
|
-
inputGenerationRef.current++;
|
|
625
|
-
setSending(false);
|
|
626
|
-
}
|
|
627
749
|
closeGroupCommand();
|
|
628
750
|
return;
|
|
629
751
|
}
|
|
@@ -642,7 +764,7 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
642
764
|
setNote(failure.error.message);
|
|
643
765
|
return;
|
|
644
766
|
}
|
|
645
|
-
inputGenerationRef.current
|
|
767
|
+
inputGenerationRef.current = {};
|
|
646
768
|
setInput(completeListenCommand(input, selected));
|
|
647
769
|
commandSelectionRef.current = 0;
|
|
648
770
|
groupCommand.setState({ kind: 'menu', selectedIndex: 0 });
|
|
@@ -653,9 +775,12 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
653
775
|
if (focus === 'attachments') {
|
|
654
776
|
setFocus('input');
|
|
655
777
|
}
|
|
656
|
-
else if (
|
|
778
|
+
else if (visibleDownloadableAttachments.length > 0) {
|
|
779
|
+
const currentSelectedKey = selectedAttachmentKeyRef.current;
|
|
780
|
+
const nextSelection = visibleDownloadableAttachments.find((item) => item.key === currentSelectedKey)
|
|
781
|
+
?? visibleDownloadableAttachments[0];
|
|
657
782
|
setFocus('attachments');
|
|
658
|
-
|
|
783
|
+
selectAttachment(nextSelection.key);
|
|
659
784
|
}
|
|
660
785
|
else {
|
|
661
786
|
setNote('no downloadable attachments');
|
|
@@ -668,20 +793,42 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
668
793
|
return;
|
|
669
794
|
}
|
|
670
795
|
if (key.upArrow || key.downArrow) {
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
796
|
+
const direction = key.upArrow ? 'up' : 'down';
|
|
797
|
+
const delta = direction === 'up' ? -1 : 1;
|
|
798
|
+
const currentIndex = downloadableAttachments.findIndex((item) => item.key === selectedAttachmentKeyRef.current);
|
|
799
|
+
const nextIndex = currentIndex + delta;
|
|
800
|
+
if (currentIndex < 0 || nextIndex < 0 || nextIndex >= downloadableAttachments.length)
|
|
801
|
+
return;
|
|
802
|
+
const nextAttachment = downloadableAttachments[nextIndex];
|
|
803
|
+
selectAttachment(nextAttachment?.key ?? null);
|
|
804
|
+
const effectiveVisibleMessages = takeListenViewport(messages, messagePaneHeight, scrollOffsetRef.current);
|
|
805
|
+
if (nextAttachment != null && !effectiveVisibleMessages.some((message) => message.key === nextAttachment.message.key)) {
|
|
806
|
+
const messageIndex = messages.findIndex((message) => message.key === nextAttachment.message.key);
|
|
807
|
+
if (messageIndex >= 0) {
|
|
808
|
+
const targetOffset = messages.length - 1 - messageIndex;
|
|
809
|
+
scrollOffsetRef.current = targetOffset;
|
|
810
|
+
showScrollbar();
|
|
811
|
+
setScrollState((current) => {
|
|
812
|
+
if (current.offset === targetOffset)
|
|
813
|
+
return current;
|
|
814
|
+
return applyScroll(current, targetOffset > current.offset ? 'up' : 'down', Math.max(0, messages.length - 1), Math.abs(targetOffset - current.offset));
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
}
|
|
676
818
|
return;
|
|
677
819
|
}
|
|
678
|
-
|
|
679
|
-
|
|
820
|
+
const currentSelectedAttachment = downloadableAttachments.find((item) => item.key === selectedAttachmentKeyRef.current);
|
|
821
|
+
if (key.return && currentSelectedAttachment != null) {
|
|
822
|
+
const state = downloadStates[currentSelectedAttachment.key] ?? { status: 'idle' };
|
|
680
823
|
if (canManuallyDownload(state))
|
|
681
|
-
void downloadAttachment(
|
|
824
|
+
void downloadAttachment(currentSelectedAttachment);
|
|
682
825
|
}
|
|
683
826
|
return;
|
|
684
827
|
}
|
|
828
|
+
if (key.upArrow || key.downArrow) {
|
|
829
|
+
scrollMessages(key.upArrow ? 'up' : 'down');
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
685
832
|
if (sending || groupCommand.state.kind === 'executing')
|
|
686
833
|
return;
|
|
687
834
|
if (key.return) {
|
|
@@ -699,7 +846,7 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
699
846
|
}
|
|
700
847
|
const parsed = parseSelectedListenCommand(input, match);
|
|
701
848
|
if (parsed.kind === 'complete') {
|
|
702
|
-
inputGenerationRef.current
|
|
849
|
+
inputGenerationRef.current = {};
|
|
703
850
|
setInput(parsed.input);
|
|
704
851
|
commandSelectionRef.current = 0;
|
|
705
852
|
groupCommand.setState({ kind: 'menu', selectedIndex: 0 });
|
|
@@ -721,6 +868,11 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
721
868
|
setNote('connection is not ready');
|
|
722
869
|
return;
|
|
723
870
|
}
|
|
871
|
+
const access = new WriteAccessPolicy().check();
|
|
872
|
+
if (!access.ok) {
|
|
873
|
+
setNote(access.error.message);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
724
876
|
replyExecutionLockRef.current = true;
|
|
725
877
|
const ownedGeneration = inputGenerationRef.current;
|
|
726
878
|
const originalInput = input;
|
|
@@ -761,7 +913,7 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
761
913
|
return;
|
|
762
914
|
}
|
|
763
915
|
if (key.backspace || key.delete) {
|
|
764
|
-
inputGenerationRef.current
|
|
916
|
+
inputGenerationRef.current = {};
|
|
765
917
|
setInput((current) => current.slice(0, -1));
|
|
766
918
|
return;
|
|
767
919
|
}
|
|
@@ -769,7 +921,7 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
769
921
|
return;
|
|
770
922
|
}
|
|
771
923
|
if (!key.ctrl && !key.meta && inputText.length > 0) {
|
|
772
|
-
inputGenerationRef.current
|
|
924
|
+
inputGenerationRef.current = {};
|
|
773
925
|
setInput((current) => {
|
|
774
926
|
const next = current + inputText;
|
|
775
927
|
if (next.trimStart().startsWith('/')) {
|
|
@@ -784,14 +936,24 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
784
936
|
useEffect(() => {
|
|
785
937
|
const generation = operationControllerRef.current.beginGeneration();
|
|
786
938
|
const isActive = generation.isActive;
|
|
939
|
+
const lifecycleStop = new AbortController();
|
|
940
|
+
lifecycleStopRef.current = lifecycleStop;
|
|
787
941
|
if (stopSignal.aborted) {
|
|
788
942
|
exit();
|
|
789
943
|
return;
|
|
790
944
|
}
|
|
791
945
|
const stopFromSignal = () => {
|
|
946
|
+
if (stoppingRef.current)
|
|
947
|
+
return;
|
|
948
|
+
if (irreversibleGroupWriteRef.current) {
|
|
949
|
+
requestOwnershipShutdown('external');
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
792
952
|
stopListening();
|
|
793
953
|
};
|
|
794
|
-
|
|
954
|
+
const unsubscribeShutdown = shutdownRequests?.subscribe(stopFromSignal);
|
|
955
|
+
if (shutdownRequests == null)
|
|
956
|
+
stopSignal.addEventListener('abort', stopFromSignal);
|
|
795
957
|
const groupQueue = createInteractiveListenRuntime(dbPath, createReplyResolver ?? createListenReplyResolver, {
|
|
796
958
|
isActive,
|
|
797
959
|
onGroup: (group) => {
|
|
@@ -833,7 +995,7 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
833
995
|
chats,
|
|
834
996
|
persist,
|
|
835
997
|
retrySeconds,
|
|
836
|
-
signal:
|
|
998
|
+
signal: lifecycleStop.signal,
|
|
837
999
|
createClient,
|
|
838
1000
|
createCoordinator: () => autoDownloader,
|
|
839
1001
|
onCoordinator: (coordinator) => {
|
|
@@ -843,12 +1005,13 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
843
1005
|
onClient: (client) => {
|
|
844
1006
|
if (!isActive())
|
|
845
1007
|
return;
|
|
846
|
-
groupLookupGenerationRef.current
|
|
1008
|
+
groupLookupGenerationRef.current = {};
|
|
847
1009
|
knownGroupRef.current = undefined;
|
|
848
1010
|
setKnownGroup(undefined);
|
|
849
1011
|
clientRef.current = client;
|
|
850
1012
|
if (client != null && sendTo != null) {
|
|
851
|
-
const lookup =
|
|
1013
|
+
const lookup = {};
|
|
1014
|
+
groupLookupGenerationRef.current = lookup;
|
|
852
1015
|
void client.groups.getGroup(sendTo).then((group) => {
|
|
853
1016
|
if (isActive() && lookup === groupLookupGenerationRef.current && clientRef.current === client) {
|
|
854
1017
|
knownGroupRef.current = group;
|
|
@@ -903,30 +1066,42 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
903
1066
|
albumAggregator.flush();
|
|
904
1067
|
},
|
|
905
1068
|
}).finally(() => {
|
|
906
|
-
if (isActive() && !stopSignal.aborted) {
|
|
1069
|
+
if (isActive() && !stopSignal.aborted && !stoppingRef.current) {
|
|
907
1070
|
onRequestStop();
|
|
908
1071
|
exit();
|
|
909
1072
|
}
|
|
910
1073
|
});
|
|
911
1074
|
return () => {
|
|
912
|
-
groupLookupGenerationRef.current
|
|
1075
|
+
groupLookupGenerationRef.current = {};
|
|
913
1076
|
knownGroupRef.current = undefined;
|
|
914
|
-
|
|
1077
|
+
unsubscribeShutdown?.();
|
|
1078
|
+
if (shutdownRequests == null)
|
|
1079
|
+
stopSignal.removeEventListener('abort', stopFromSignal);
|
|
915
1080
|
albumAggregator.flush();
|
|
916
1081
|
generation.dispose();
|
|
917
1082
|
albumAggregator.dispose();
|
|
918
1083
|
void groupQueue.close();
|
|
919
1084
|
if (albumAggregatorRef.current === albumAggregator)
|
|
920
1085
|
albumAggregatorRef.current = null;
|
|
921
|
-
|
|
1086
|
+
lifecycleStop.abort();
|
|
922
1087
|
clientRef.current = null;
|
|
923
1088
|
autoDownloader?.stop();
|
|
924
1089
|
void autoDownloader?.waitForActive();
|
|
925
1090
|
if (autoDownloaderRef.current === autoDownloader)
|
|
926
1091
|
autoDownloaderRef.current = null;
|
|
1092
|
+
if (lifecycleStopRef.current === lifecycleStop)
|
|
1093
|
+
lifecycleStopRef.current = null;
|
|
1094
|
+
if (shutdownGraceTimerRef.current)
|
|
1095
|
+
clearTimeout(shutdownGraceTimerRef.current);
|
|
1096
|
+
shutdownGraceTimerRef.current = null;
|
|
927
1097
|
};
|
|
928
|
-
}, [autoDownload, chats, createClient, createReplyResolver, dbPath, persist, retrySeconds, sendTo, showMedia, exit, stopSignal, onRequestStop]);
|
|
1098
|
+
}, [autoDownload, chats, createClient, createReplyResolver, dbPath, persist, retrySeconds, sendTo, showMedia, exit, stopSignal, shutdownRequests, onRequestStop]);
|
|
929
1099
|
const sendMessage = async (text) => {
|
|
1100
|
+
const access = new WriteAccessPolicy().check();
|
|
1101
|
+
if (!access.ok) {
|
|
1102
|
+
setNote(access.error.message);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
930
1105
|
const trimmed = text.trim();
|
|
931
1106
|
if (!trimmed)
|
|
932
1107
|
return;
|
|
@@ -997,7 +1172,7 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
997
1172
|
onProgress: (downloaded, total) => {
|
|
998
1173
|
if (!isCurrent())
|
|
999
1174
|
return;
|
|
1000
|
-
const progress =
|
|
1175
|
+
const progress = attachmentDownloadProgress(downloaded, total);
|
|
1001
1176
|
setDownloadStates((current) => ({ ...current, [item.key]: { status: 'downloading', progress } }));
|
|
1002
1177
|
},
|
|
1003
1178
|
});
|
|
@@ -1019,15 +1194,28 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
1019
1194
|
setTimeout(exit, 0);
|
|
1020
1195
|
onRequestStop();
|
|
1021
1196
|
autoDownloaderRef.current?.stop();
|
|
1022
|
-
|
|
1197
|
+
lifecycleStopRef.current?.abort();
|
|
1023
1198
|
};
|
|
1199
|
+
useEffect(() => {
|
|
1200
|
+
if (groupCommand.state.kind !== 'result' && groupCommand.state.kind !== 'error')
|
|
1201
|
+
return;
|
|
1202
|
+
if (!deferredStopRef.current && !shutdownRequestedRef.current)
|
|
1203
|
+
return;
|
|
1204
|
+
deferredStopRef.current = false;
|
|
1205
|
+
shutdownRequestedRef.current = false;
|
|
1206
|
+
if (shutdownGraceTimerRef.current)
|
|
1207
|
+
clearTimeout(shutdownGraceTimerRef.current);
|
|
1208
|
+
shutdownGraceTimerRef.current = null;
|
|
1209
|
+
const pending = setTimeout(stopListening, 0);
|
|
1210
|
+
return () => clearTimeout(pending);
|
|
1211
|
+
}, [groupCommand.state]);
|
|
1024
1212
|
const permissionState = groupCommand.state.kind === 'select-permissions' ? groupCommand.state : null;
|
|
1025
1213
|
return (_jsxs(Box, { flexDirection: "row", width: terminalWidth, height: terminalHeight, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", width: contentWidth, height: terminalHeight, overflow: "hidden", children: [_jsx(ListenStatusArea, { status: status, unseenCount: scrollState.unseenCount, autoDownload: autoDownload }), note ? _jsx(Text, { dimColor: true, children: note }) : null, groupCommand.state.kind === 'result' ? _jsx(Box, { flexGrow: 1, flexDirection: "column", children: _jsx(GroupCommandResult, { state: groupCommand.state, width: contentWidth }) }) : _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsx(GroupCommandResult, { state: groupCommand.state, width: contentWidth }), groupCommand.state.kind === 'confirm' && 'confirmation' in groupCommand.state.pending ? _jsx(GroupCommandConfirm, { confirmation: groupCommand.state.pending.confirmation, selectedIndex: groupCommand.state.selectedIndex, width: contentWidth }) : null, groupCommand.state.kind === 'confirm-title' && 'confirmation' in groupCommand.state.pending ? groupCommand.state.stage === 'confirm'
|
|
1026
1214
|
? _jsx(GroupCommandConfirm, { confirmation: groupCommand.state.pending.confirmation, selectedIndex: groupCommand.state.selectedIndex, width: contentWidth })
|
|
1027
|
-
: _jsxs(Box, { flexDirection: "column", width: contentWidth, children: [_jsx(Text, { color: "yellow", children: truncateCell('Type the exact title to permanently delete this chat:', contentWidth) }), _jsx(Text, { children: truncateCell(groupCommand.state.pending.confirmation.title ?? '', contentWidth) }), _jsx(Text, { color: "#8ecbff", children: truncateCell(`› ${groupCommand.state.confirmText}`, contentWidth) }), groupCommand.state.mismatch ? _jsx(Text, { color: "red", children: truncateCell('Title does not match exactly.', contentWidth) }) : null, _jsx(Text, { dimColor: true, children: truncateCell('Enter verify · Esc back', contentWidth) })] }) : null, permissionState ? _jsxs(Box, { flexDirection: "column", width: contentWidth, children: [_jsx(Text, { color: "#8ecbff", children: truncateCell('Administrator permissions', contentWidth) }), ADMIN_RIGHT_KEYS.map((right, index) => _jsx(Text, { color: index === permissionState.selectedIndex ? '#8ecbff' : undefined, children: truncateCell(`${index === permissionState.selectedIndex ? '› ' : ' '}[${permissionState.selected.includes(right) ? 'x' : ' '}] ${right}`, contentWidth) }, right)), permissionState.warning ? _jsx(Text, { color: "yellow", children: truncateCell(permissionState.warning, contentWidth) }) : null, _jsx(Text, { dimColor: true, children: truncateCell('↑/↓ select · Space toggle · Enter continue · Esc cancel', contentWidth) })] }) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [messages.length === 0 ? _jsx(Text, { dimColor: true, children: "Waiting for new messages..." }) : null, visibleMessages.map((message) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, wrap: "truncate-end", children: formatInteractiveListenHeader(message) }), message.replyContext == null ? null : _jsx(Text, { wrap: "truncate-end", children: formatReplyContext(message.replyContext) }), message.content == null ? null : _jsx(Text, { wrap: "truncate-end", children: message.content }), message.mediaSummary == null ? null : _jsx(Text, { wrap: "truncate-end", children: message.mediaSummary }), message.media.map((item, mediaIndex) => {
|
|
1215
|
+
: _jsxs(Box, { flexDirection: "column", width: contentWidth, children: [_jsx(Text, { color: "yellow", children: truncateCell('Type the exact title to permanently delete this chat:', contentWidth) }), _jsx(Text, { children: truncateCell(groupCommand.state.pending.confirmation.title ?? '', contentWidth) }), _jsx(Text, { color: "#8ecbff", children: truncateCell(`› ${groupCommand.state.confirmText}`, contentWidth) }), groupCommand.state.mismatch ? _jsx(Text, { color: "red", children: truncateCell('Title does not match exactly.', contentWidth) }) : null, _jsx(Text, { dimColor: true, children: truncateCell('Enter verify · Esc back', contentWidth) })] }) : null, groupCommand.state.kind === 'password' ? _jsx(SecureInput, { label: "Telegram 2FA password", onSubmit: (value) => { void groupCommand.runWithOwnershipPassword(value); }, onCancel: closeGroupCommand }) : null, permissionState ? _jsxs(Box, { flexDirection: "column", width: contentWidth, children: [_jsx(Text, { color: "#8ecbff", children: truncateCell('Administrator permissions', contentWidth) }), ADMIN_RIGHT_KEYS.map((right, index) => _jsx(Text, { color: index === permissionState.selectedIndex ? '#8ecbff' : undefined, children: truncateCell(`${index === permissionState.selectedIndex ? '› ' : ' '}[${permissionState.selected.includes(right) ? 'x' : ' '}] ${right}`, contentWidth) }, right)), permissionState.warning ? _jsx(Text, { color: "yellow", children: truncateCell(permissionState.warning, contentWidth) }) : null, _jsx(Text, { dimColor: true, children: truncateCell('↑/↓ select · Space toggle · Enter continue · Esc cancel', contentWidth) })] }) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [messages.length === 0 ? _jsx(Text, { dimColor: true, children: "Waiting for new messages..." }) : null, visibleMessages.map((message) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, wrap: "truncate-end", children: formatInteractiveListenHeader(message) }), message.replyContext == null ? null : _jsx(Text, { wrap: "truncate-end", children: formatReplyContext(message.replyContext) }), message.content == null ? null : _jsx(Text, { wrap: "truncate-end", children: message.content }), message.mediaSummary == null ? null : _jsx(Text, { wrap: "truncate-end", children: message.mediaSummary }), message.media.map((item, mediaIndex) => {
|
|
1028
1216
|
const attachmentKey = attachmentDownloadKeyAt(message.media, mediaIndex);
|
|
1029
1217
|
return (_jsx(ListenAttachmentWithPreview, { label: item.label, downloadable: item.downloadable, selected: focus === 'attachments' && selectedAttachment?.key === attachmentKey, state: downloadStates[attachmentKey] ?? { status: 'idle' }, previewCells: item.previewCells }, attachmentKey));
|
|
1030
|
-
}), _jsx(Text, { dimColor: true, children: MESSAGE_SEPARATOR })] }, message.key)))] })] }), groupCommand.state.kind !== 'result' ? _jsxs(Box, { flexDirection: "column", children: [sendTo == null ? _jsx(Text, { dimColor: true, children: "Set --send-to <chat> (or pass one chat to listen) before sending messages." }) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 0, children: [input.trimStart().startsWith('/') && groupCommand.state.kind === 'menu' ? _jsx(ListenCommandMenu, { input: input, selectedIndex: groupCommand.state.selectedIndex, width: contentWidth, knownGroup: knownGroup }) : null, _jsx(ListenComposer, { input: input, sendTargetLabel: sendTo == null ? '(not selected)' : sendTargetLabel, terminalWidth: contentWidth, sending: sending, hint: focus === 'attachments' ? '↑/↓ select · Enter download · Tab input' : '
|
|
1218
|
+
}), _jsx(Text, { dimColor: true, children: MESSAGE_SEPARATOR })] }, message.key)))] })] }), groupCommand.state.kind !== 'result' ? _jsxs(Box, { flexDirection: "column", children: [sendTo == null ? _jsx(Text, { dimColor: true, children: "Set --send-to <chat> (or pass one chat to listen) before sending messages." }) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 0, children: [input.trimStart().startsWith('/') && groupCommand.state.kind === 'menu' ? _jsx(ListenCommandMenu, { input: input, selectedIndex: groupCommand.state.selectedIndex, width: contentWidth, knownGroup: knownGroup }) : null, _jsx(ListenComposer, { input: input, sendTargetLabel: sendTo == null ? '(not selected)' : sendTargetLabel, terminalWidth: contentWidth, sending: sending, cursorVisible: focus === 'input', hint: focus === 'attachments' ? 'Wheel · ↑/↓ select · Enter download · Tab input' : 'Wheel/↑/↓/PgUp/PgDn scroll · Drag select · Ctrl+C exit' })] })] }) : null] }), _jsx(ListenScrollbar, { height: terminalHeight, visible: scrollbarVisible, geometry: scrollbarGeometry })] }));
|
|
1031
1219
|
}
|
|
1032
1220
|
export class ListenMessageViewCache {
|
|
1033
1221
|
entries = new Map();
|