dsh-connect 0.5.2 → 0.6.3
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +11 -5
- package/README.zh.md +203 -0
- package/lib/binding.d.ts +2 -0
- package/lib/binding.d.ts.map +1 -1
- package/lib/binding.js +1 -0
- package/lib/binding.js.map +1 -1
- package/lib/i18n.d.ts +21 -9
- package/lib/i18n.d.ts.map +1 -1
- package/lib/i18n.js +52 -18
- package/lib/i18n.js.map +1 -1
- package/lib/index.d.ts +6 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/lib/interaction.d.ts +4 -1
- package/lib/interaction.d.ts.map +1 -1
- package/lib/interaction.js +33 -7
- package/lib/interaction.js.map +1 -1
- package/lib/runner.d.ts +12 -2
- package/lib/runner.d.ts.map +1 -1
- package/lib/runner.js +205 -68
- package/lib/runner.js.map +1 -1
- package/lib/service.d.ts +7 -1
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +12 -3
- package/lib/service.js.map +1 -1
- package/lib/types.d.ts +2 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +65 -58
package/lib/runner.js
CHANGED
|
@@ -276,6 +276,17 @@ export function summarizeTurn(events, firstSeq) {
|
|
|
276
276
|
function truncate(text, max = 500) {
|
|
277
277
|
return text.length <= max ? text : `${text.slice(0, max)}…`;
|
|
278
278
|
}
|
|
279
|
+
/** Classify an error message into an advice bucket (keyword-based, best-effort). */
|
|
280
|
+
export function classifyError(detail) {
|
|
281
|
+
const d = detail.toLowerCase();
|
|
282
|
+
if (/(permission|forbidden|denied|not have access|no access|unauthorized|403|sandbox|acl |isn't permitted|not permitted|access denied)/.test(d))
|
|
283
|
+
return "permission";
|
|
284
|
+
if (/(timeout|econnrefused|econnreset|enotfound|enetunreach|etimedout|socket|network|proxy|tls|certificate|fetch failed|connect |unreachable|dns)/.test(d))
|
|
285
|
+
return "network";
|
|
286
|
+
if (/(model|quota|rate ?limit|429|insufficient|balance|token(s)? (limit|length)|max_tokens|context length|credit)/.test(d))
|
|
287
|
+
return "model";
|
|
288
|
+
return "generic";
|
|
289
|
+
}
|
|
279
290
|
/** Thousands-separated token count for display. */
|
|
280
291
|
function fmtTokens(n) {
|
|
281
292
|
return n.toLocaleString("en-US");
|
|
@@ -355,8 +366,6 @@ export class AgentRunner {
|
|
|
355
366
|
handle;
|
|
356
367
|
turn;
|
|
357
368
|
workDir;
|
|
358
|
-
streamEnabled = true;
|
|
359
|
-
summaryEnabled = true;
|
|
360
369
|
language;
|
|
361
370
|
notifyLevel;
|
|
362
371
|
progressTimeoutMs;
|
|
@@ -435,7 +444,12 @@ export class AgentRunner {
|
|
|
435
444
|
enqueue(msg) {
|
|
436
445
|
const command = parseCommand(msg.text);
|
|
437
446
|
if (command.kind !== "message") {
|
|
438
|
-
|
|
447
|
+
// Command handlers touch the adapter directly (no per-call catch) — a
|
|
448
|
+
// transient channel error here must never become an unhandled rejection
|
|
449
|
+
// (Node ≥ 15 crashes the process on those by default).
|
|
450
|
+
void this.handleCommand(command, msg).catch((error) => {
|
|
451
|
+
this.log(`connect: command ${command.kind} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
452
|
+
});
|
|
439
453
|
return;
|
|
440
454
|
}
|
|
441
455
|
this.queue.push(msg);
|
|
@@ -464,6 +478,7 @@ export class AgentRunner {
|
|
|
464
478
|
if (typeof name !== "string" || name === "")
|
|
465
479
|
return;
|
|
466
480
|
const summary = toolCallSummary(event.data.arguments);
|
|
481
|
+
turn.toolCount += 1;
|
|
467
482
|
// The proactive progress notice reports the latest milestone even when
|
|
468
483
|
// `result` mode hides the streaming details.
|
|
469
484
|
if (name === "ask_user_question") {
|
|
@@ -474,14 +489,15 @@ export class AgentRunner {
|
|
|
474
489
|
turn.milestone = this.t.toolCalling(name, undefined);
|
|
475
490
|
}
|
|
476
491
|
// `result` mode shows nothing until the final answer; `important` shows
|
|
477
|
-
// the tool name
|
|
492
|
+
// the tool name with a step counter, `full` also carries the summary.
|
|
478
493
|
if (this.notifyLevel === "result")
|
|
479
494
|
return;
|
|
480
|
-
const
|
|
495
|
+
const base = name === "ask_user_question"
|
|
481
496
|
? this.t.questionToolCall(questionTextOf(event.data.arguments))
|
|
482
|
-
: this.
|
|
483
|
-
|
|
484
|
-
|
|
497
|
+
: this.t.toolStepLabel(turn.toolCount, name);
|
|
498
|
+
const label = name === "ask_user_question" || this.notifyLevel !== "full" || summary === undefined
|
|
499
|
+
? base
|
|
500
|
+
: `${base} — ${summary}`;
|
|
485
501
|
applyToolCall(turn, label);
|
|
486
502
|
}
|
|
487
503
|
}
|
|
@@ -492,6 +508,57 @@ export class AgentRunner {
|
|
|
492
508
|
...(msg.replyRef === undefined ? {} : { replyRef: msg.replyRef }),
|
|
493
509
|
};
|
|
494
510
|
}
|
|
511
|
+
/** Completion-card target: in groups, @ the requester so they notice the result. */
|
|
512
|
+
taskTarget(msg) {
|
|
513
|
+
return this.chatType === "group"
|
|
514
|
+
? { ...this.target(msg), atUsers: [msg.senderKey] }
|
|
515
|
+
: this.target(msg);
|
|
516
|
+
}
|
|
517
|
+
/** Send the one-time welcome card on a chat's first message (persisted marker). */
|
|
518
|
+
async maybeSendWelcome(msg) {
|
|
519
|
+
const binding = this.bindings.get(this.channel, this.chatKey);
|
|
520
|
+
if (binding !== undefined && binding.welcomedAt !== undefined)
|
|
521
|
+
return;
|
|
522
|
+
const base = binding ?? {
|
|
523
|
+
channel: this.channel,
|
|
524
|
+
chatKey: this.chatKey,
|
|
525
|
+
chatType: this.chatType,
|
|
526
|
+
sessionId: "",
|
|
527
|
+
ownerKey: msg.senderKey,
|
|
528
|
+
createdAt: Date.now(),
|
|
529
|
+
lastActiveAt: Date.now(),
|
|
530
|
+
sessions: [],
|
|
531
|
+
};
|
|
532
|
+
this.bindings.put({ ...base, welcomedAt: Date.now(), lastActiveAt: Date.now() });
|
|
533
|
+
await this.adapter
|
|
534
|
+
.sendCard(this.target(msg), { markdown: `${this.t.welcomeTitle}\n\n${this.t.welcomeBody(this.workDir)}` })
|
|
535
|
+
.catch(() => undefined);
|
|
536
|
+
}
|
|
537
|
+
/** Pick the localized advice text for a failed turn. */
|
|
538
|
+
errorAdvice(detail) {
|
|
539
|
+
switch (classifyError(detail)) {
|
|
540
|
+
case "permission":
|
|
541
|
+
return this.t.errorAdvicePermission;
|
|
542
|
+
case "network":
|
|
543
|
+
return this.t.errorAdviceNetwork;
|
|
544
|
+
case "model":
|
|
545
|
+
return this.t.errorAdviceModel;
|
|
546
|
+
default:
|
|
547
|
+
return this.t.errorAdviceGeneric;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/** Present a destructive-action confirmation; true only when the user confirms. */
|
|
551
|
+
async confirmAction(target, promptText) {
|
|
552
|
+
const { choice } = await this.adapter.promptChoice(target, {
|
|
553
|
+
title: this.t.confirmTitle,
|
|
554
|
+
description: promptText,
|
|
555
|
+
options: [
|
|
556
|
+
{ id: "confirm:yes", label: this.t.confirmYes },
|
|
557
|
+
{ id: "confirm:no", label: this.t.confirmNo },
|
|
558
|
+
],
|
|
559
|
+
});
|
|
560
|
+
return choice === "confirm:yes";
|
|
561
|
+
}
|
|
495
562
|
async drain() {
|
|
496
563
|
if (this.running)
|
|
497
564
|
return;
|
|
@@ -509,11 +576,16 @@ export class AgentRunner {
|
|
|
509
576
|
}
|
|
510
577
|
}
|
|
511
578
|
async runTurn(msg) {
|
|
512
|
-
//
|
|
579
|
+
// The mutual-exclusion lock arbitrates between the Feishu side and the Web
|
|
580
|
+
// mirror of the *same* session. Channels without a mirror (Telegram /
|
|
581
|
+
// DingTalk) must not be classified as "web" and must not take part in the
|
|
582
|
+
// lock at all.
|
|
583
|
+
const usesLock = this.channel === "feishu" || this.channel === "web";
|
|
513
584
|
const currentChannel = this.channel === "feishu" ? "feishu" : "web";
|
|
514
585
|
// Check timeout before attempting to acquire
|
|
515
|
-
|
|
516
|
-
|
|
586
|
+
if (usesLock)
|
|
587
|
+
this.checkAndReleaseTimeoutLock();
|
|
588
|
+
if (usesLock && !this.acquireLock(currentChannel)) {
|
|
517
589
|
const binding = this.bindings.get(this.channel, this.chatKey);
|
|
518
590
|
const lockedBy = binding?.lockOwner ?? "unknown";
|
|
519
591
|
// If from Web channel, queue the message instead of rejecting
|
|
@@ -533,27 +605,33 @@ export class AgentRunner {
|
|
|
533
605
|
}
|
|
534
606
|
try {
|
|
535
607
|
// Acknowledge receipt before the (possibly slow) agent spin-up so the
|
|
536
|
-
// user knows processing started.
|
|
608
|
+
// user knows processing started. When more messages are queued, say so.
|
|
609
|
+
const ack = this.t.processingStarted(truncate(msg.text, 60));
|
|
610
|
+
const ackText = this.queue.length > 0 ? `${ack}\n${this.t.queuedHint(this.queue.length)}` : ack;
|
|
537
611
|
await this.adapter
|
|
538
|
-
.sendText(this.target(msg),
|
|
612
|
+
.sendText(this.target(msg), ackText)
|
|
539
613
|
.catch(() => undefined);
|
|
614
|
+
// First message in this chat: one-time welcome card.
|
|
615
|
+
await this.maybeSendWelcome(msg);
|
|
540
616
|
const agent = await this.ensureAgent(msg);
|
|
541
617
|
const outcome = await this.driveAgent(agent, msg);
|
|
542
618
|
this.touchBinding(msg);
|
|
543
619
|
// Task-end stats: model, tokens, elapsed, and a compaction suggestion.
|
|
544
620
|
await this.sendTurnStats(msg, outcome);
|
|
545
621
|
// Release lock after task completion
|
|
546
|
-
|
|
547
|
-
|
|
622
|
+
if (usesLock)
|
|
623
|
+
this.releaseLock();
|
|
624
|
+
if (outcome.reason !== "completed") {
|
|
548
625
|
await this.sendSummary(msg, outcome);
|
|
549
626
|
}
|
|
550
627
|
}
|
|
551
628
|
catch (error) {
|
|
552
629
|
// Release lock even on error
|
|
553
|
-
|
|
630
|
+
if (usesLock)
|
|
631
|
+
this.releaseLock();
|
|
554
632
|
const detail = error instanceof Error ? error.message : String(error);
|
|
555
633
|
await this.adapter
|
|
556
|
-
.sendText(this.target(msg), this.t.
|
|
634
|
+
.sendText(this.target(msg), this.t.processingFailedAdvice(truncate(detail, 400), this.errorAdvice(detail)))
|
|
557
635
|
.catch(() => undefined);
|
|
558
636
|
}
|
|
559
637
|
}
|
|
@@ -690,22 +768,12 @@ export class AgentRunner {
|
|
|
690
768
|
const content = await this.buildUserContent(msg);
|
|
691
769
|
return createUserMessage({ content, source: { kind: "user" } });
|
|
692
770
|
};
|
|
693
|
-
if (!this.streamEnabled) {
|
|
694
|
-
agent.followup(await userMessage());
|
|
695
|
-
await agent.whenIdle();
|
|
696
|
-
await this.sessions.flush(agent.session);
|
|
697
|
-
const outcome = summarizeTurn(agent.session.events, firstSeq);
|
|
698
|
-
if (outcome.text !== "") {
|
|
699
|
-
await this.adapter.sendText(this.target(msg), outcome.text).catch(() => undefined);
|
|
700
|
-
}
|
|
701
|
-
return outcome;
|
|
702
|
-
}
|
|
703
771
|
const chunks = createAsyncQueue();
|
|
704
772
|
const now = Date.now();
|
|
705
773
|
this.turn = {
|
|
706
774
|
firstSeq, chunks, lastText: "",
|
|
707
775
|
reasoning: false, hintPushed: false, lastIndex: undefined, pushedAny: false,
|
|
708
|
-
startedAt: now, lastPushAt: now,
|
|
776
|
+
startedAt: now, lastPushAt: now, toolCount: 0,
|
|
709
777
|
};
|
|
710
778
|
// Liveness heartbeat: while the agent is working, keep the streaming card
|
|
711
779
|
// visibly alive even through long reasoning or tool-execution stretches
|
|
@@ -748,13 +816,31 @@ export class AgentRunner {
|
|
|
748
816
|
.catch(() => undefined);
|
|
749
817
|
}, PROGRESS_WATCHDOG_CHECK_MS)
|
|
750
818
|
: undefined;
|
|
819
|
+
// End the chunk stream unconditionally: if `followup` / `whenIdle` /
|
|
820
|
+
// `flush` throws, the adapter's `for await` over `chunks` must still be
|
|
821
|
+
// released, otherwise the streaming card hangs in "streaming" forever and
|
|
822
|
+
// the chunk queue / adapter promise leak. On the error path we also await
|
|
823
|
+
// (and swallow) the stream promise so its rejection can't escape as an
|
|
824
|
+
// unhandled rejection while the original error propagates.
|
|
825
|
+
const endChunks = () => {
|
|
826
|
+
try {
|
|
827
|
+
chunks.end();
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
// no-op
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
let streamPromise;
|
|
751
834
|
try {
|
|
752
|
-
|
|
835
|
+
streamPromise = this.adapter.streamText(this.target(msg), chunks);
|
|
753
836
|
agent.followup(await userMessage());
|
|
754
837
|
await agent.whenIdle();
|
|
755
838
|
await this.sessions.flush(agent.session);
|
|
756
|
-
|
|
757
|
-
|
|
839
|
+
}
|
|
840
|
+
catch (error) {
|
|
841
|
+
endChunks();
|
|
842
|
+
await streamPromise?.catch(() => undefined);
|
|
843
|
+
throw error;
|
|
758
844
|
}
|
|
759
845
|
finally {
|
|
760
846
|
if (heartbeat !== undefined)
|
|
@@ -762,6 +848,8 @@ export class AgentRunner {
|
|
|
762
848
|
if (progressWatchdog !== undefined)
|
|
763
849
|
clearInterval(progressWatchdog);
|
|
764
850
|
}
|
|
851
|
+
endChunks();
|
|
852
|
+
await streamPromise;
|
|
765
853
|
const outcome = summarizeTurn(agent.session.events, firstSeq);
|
|
766
854
|
const text = outcome.text !== "" ? outcome.text : this.turn.lastText;
|
|
767
855
|
this.turn = undefined;
|
|
@@ -776,30 +864,35 @@ export class AgentRunner {
|
|
|
776
864
|
async buildUserContent(msg) {
|
|
777
865
|
const content = [{ type: "text", text: msg.text }];
|
|
778
866
|
const rawImages = msg.images;
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
867
|
+
// Images: pass directly to the main model when it supports vision,
|
|
868
|
+
// otherwise run a vision sub-task and inject its description as text —
|
|
869
|
+
// a text-only main model never stalls on images.
|
|
870
|
+
if (rawImages !== undefined && rawImages.length > 0) {
|
|
871
|
+
const paths = await this.stageImages(rawImages);
|
|
872
|
+
const locationText = paths.map((p) => `- ${p}`).join("\n");
|
|
873
|
+
const mainVision = await this.modelSupportsVision(this.agent?.options.provider, this.agent?.options.model);
|
|
874
|
+
if (mainVision) {
|
|
875
|
+
const imageBlocks = await this.imageBlocks(paths);
|
|
876
|
+
if (imageBlocks.length > 0) {
|
|
877
|
+
this.log(`connect: main model supports vision — passing ${imageBlocks.length} image block(s) directly`);
|
|
878
|
+
content.push(...imageBlocks);
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
this.log("connect: main model supports vision but image blocks failed to attach — falling back to paths");
|
|
882
|
+
content.push({ type: "text", text: this.t.imagesStaged(locationText, "") });
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
const description = await this.describeImages(paths);
|
|
887
|
+
content.push({ type: "text", text: this.t.imagesStaged(locationText, description) });
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
else if (msg.imageError !== undefined) {
|
|
891
|
+
content.push({ type: "text", text: this.t.imageDownloadFailed(msg.imageError) });
|
|
892
|
+
}
|
|
893
|
+
// Non-image attachments (files / audio / video) are staged independently of
|
|
894
|
+
// images: a pure-file message, or a vision-capable main model, must never
|
|
895
|
+
// silently drop them.
|
|
803
896
|
const rawFiles = msg.files;
|
|
804
897
|
if (rawFiles !== undefined && rawFiles.length > 0) {
|
|
805
898
|
const stagedFiles = await this.stageFiles(rawFiles);
|
|
@@ -1010,7 +1103,7 @@ export class AgentRunner {
|
|
|
1010
1103
|
if (outcome.text !== "")
|
|
1011
1104
|
lines.push(this.t.produced(truncate(outcome.text, 300)));
|
|
1012
1105
|
const card = { markdown: lines.join("\n") };
|
|
1013
|
-
await this.adapter.sendCard(this.
|
|
1106
|
+
await this.adapter.sendCard(this.taskTarget(msg), card).catch(() => undefined);
|
|
1014
1107
|
}
|
|
1015
1108
|
/** Context-window usage at or above this percentage suggests compaction. */
|
|
1016
1109
|
static COMPACT_THRESHOLD_PCT = 75;
|
|
@@ -1039,17 +1132,25 @@ export class AgentRunner {
|
|
|
1039
1132
|
lines.push(pct >= AgentRunner.COMPACT_THRESHOLD_PCT ? this.t.taskStatsCompactSuggest : this.t.taskStatsCompactOk);
|
|
1040
1133
|
}
|
|
1041
1134
|
const card = { markdown: lines.join("\n") };
|
|
1042
|
-
await this.adapter.sendCard(this.
|
|
1135
|
+
await this.adapter.sendCard(this.taskTarget(msg), card).catch(() => undefined);
|
|
1043
1136
|
}
|
|
1044
1137
|
async handleCommand(command, msg) {
|
|
1045
1138
|
const target = this.target(msg);
|
|
1046
1139
|
switch (command.kind) {
|
|
1047
1140
|
case "new": {
|
|
1141
|
+
if (!(await this.confirmAction(target, this.t.confirmNewText))) {
|
|
1142
|
+
await this.adapter.sendText(target, this.t.actionCancelled);
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1048
1145
|
await this.newChat(msg);
|
|
1049
1146
|
await this.adapter.sendText(target, this.t.newChatDone);
|
|
1050
1147
|
break;
|
|
1051
1148
|
}
|
|
1052
1149
|
case "clear": {
|
|
1150
|
+
if (!(await this.confirmAction(target, this.t.confirmClearText))) {
|
|
1151
|
+
await this.adapter.sendText(target, this.t.actionCancelled);
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1053
1154
|
await this.clearChat(msg);
|
|
1054
1155
|
await this.adapter.sendText(target, this.t.chatCleared);
|
|
1055
1156
|
break;
|
|
@@ -1191,10 +1292,10 @@ export class AgentRunner {
|
|
|
1191
1292
|
{ sessionId, title, createdAt: Date.now(), lastActiveAt: Date.now(), workDir: this.workDir },
|
|
1192
1293
|
...others,
|
|
1193
1294
|
].slice(0, 50);
|
|
1295
|
+
// Spread the existing binding so per-chat settings (language, notify
|
|
1296
|
+
// level, progress interval, welcomedAt…) survive the first session record.
|
|
1194
1297
|
this.bindings.put({
|
|
1195
|
-
channel: this.channel,
|
|
1196
|
-
chatKey: this.chatKey,
|
|
1197
|
-
chatType: this.chatType,
|
|
1298
|
+
...(binding ?? { channel: this.channel, chatKey: this.chatKey, chatType: this.chatType }),
|
|
1198
1299
|
sessionId,
|
|
1199
1300
|
ownerKey,
|
|
1200
1301
|
createdAt: binding?.createdAt ?? Date.now(),
|
|
@@ -1237,7 +1338,18 @@ export class AgentRunner {
|
|
|
1237
1338
|
await this.disposeAgent();
|
|
1238
1339
|
const binding = this.bindings.get(this.channel, this.chatKey);
|
|
1239
1340
|
if (binding !== undefined) {
|
|
1240
|
-
|
|
1341
|
+
// A new chat gets a brand-new session; the previous mirror points at the
|
|
1342
|
+
// abandoned session and must be cleared so the next session re-mirrors.
|
|
1343
|
+
this.bindings.put({
|
|
1344
|
+
...binding,
|
|
1345
|
+
sessionId: "",
|
|
1346
|
+
webMirrorSessionId: undefined,
|
|
1347
|
+
lockOwner: undefined,
|
|
1348
|
+
lockAcquiredAt: undefined,
|
|
1349
|
+
queuedMessages: [],
|
|
1350
|
+
ownerKey: msg.senderKey,
|
|
1351
|
+
lastActiveAt: Date.now(),
|
|
1352
|
+
});
|
|
1241
1353
|
}
|
|
1242
1354
|
}
|
|
1243
1355
|
async clearChat(msg) {
|
|
@@ -1246,7 +1358,17 @@ export class AgentRunner {
|
|
|
1246
1358
|
await this.disposeAgent();
|
|
1247
1359
|
if (binding !== undefined && current !== undefined && current !== "") {
|
|
1248
1360
|
const sessions = binding.sessions.filter((s) => s.sessionId !== current);
|
|
1249
|
-
this.bindings.put({
|
|
1361
|
+
this.bindings.put({
|
|
1362
|
+
...binding,
|
|
1363
|
+
sessionId: "",
|
|
1364
|
+
sessions,
|
|
1365
|
+
webMirrorSessionId: undefined,
|
|
1366
|
+
lockOwner: undefined,
|
|
1367
|
+
lockAcquiredAt: undefined,
|
|
1368
|
+
queuedMessages: [],
|
|
1369
|
+
ownerKey: msg.senderKey,
|
|
1370
|
+
lastActiveAt: Date.now(),
|
|
1371
|
+
});
|
|
1250
1372
|
}
|
|
1251
1373
|
}
|
|
1252
1374
|
async switchTo(sessionId, ownerKey) {
|
|
@@ -1254,7 +1376,19 @@ export class AgentRunner {
|
|
|
1254
1376
|
if (binding === undefined)
|
|
1255
1377
|
return;
|
|
1256
1378
|
const sessions = binding.sessions.map((s) => (s.sessionId === sessionId ? { ...s, lastActiveAt: Date.now() } : s));
|
|
1257
|
-
|
|
1379
|
+
// The mirror follows the active session: clear the stale mapping and let
|
|
1380
|
+
// `autoCreateWebMirror` re-create it for the newly selected session.
|
|
1381
|
+
this.bindings.put({
|
|
1382
|
+
...binding,
|
|
1383
|
+
sessionId,
|
|
1384
|
+
sessions,
|
|
1385
|
+
webMirrorSessionId: undefined,
|
|
1386
|
+
lockOwner: undefined,
|
|
1387
|
+
lockAcquiredAt: undefined,
|
|
1388
|
+
queuedMessages: [],
|
|
1389
|
+
ownerKey,
|
|
1390
|
+
lastActiveAt: Date.now(),
|
|
1391
|
+
});
|
|
1258
1392
|
await this.disposeAgent();
|
|
1259
1393
|
}
|
|
1260
1394
|
listWorkspaces() {
|
|
@@ -1363,7 +1497,12 @@ export class AgentRunner {
|
|
|
1363
1497
|
label: this.t.menuNewChat,
|
|
1364
1498
|
leaf: true,
|
|
1365
1499
|
onSelect: async (t, m) => {
|
|
1366
|
-
await this.
|
|
1500
|
+
if (await this.confirmAction(t, this.t.confirmNewText)) {
|
|
1501
|
+
await this.newChat(m);
|
|
1502
|
+
}
|
|
1503
|
+
else {
|
|
1504
|
+
await this.adapter.sendText(t, this.t.actionCancelled);
|
|
1505
|
+
}
|
|
1367
1506
|
},
|
|
1368
1507
|
});
|
|
1369
1508
|
return items;
|
|
@@ -1488,7 +1627,7 @@ export class AgentRunner {
|
|
|
1488
1627
|
if (event.type === "turn/end") {
|
|
1489
1628
|
const reason = mapReason(event.data.reason.kind);
|
|
1490
1629
|
// Use the event's timestamp if available, otherwise approximate
|
|
1491
|
-
const completedAt = new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
1630
|
+
const completedAt = new Date().toLocaleTimeString(this.language === "en" ? "en-US" : "zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
1492
1631
|
return { reason, completedAt };
|
|
1493
1632
|
}
|
|
1494
1633
|
}
|
|
@@ -1522,8 +1661,6 @@ export class AgentRunner {
|
|
|
1522
1661
|
for (const w of this.config.workspaces)
|
|
1523
1662
|
lines.push(` - ${w}`);
|
|
1524
1663
|
lines.push(this.t.chatCountField(binding?.sessions.length ?? 0));
|
|
1525
|
-
lines.push(this.t.streamLabel(this.streamEnabled));
|
|
1526
|
-
lines.push(this.t.summaryLabel(this.summaryEnabled));
|
|
1527
1664
|
lines.push(this.t.progressSetting(this.progressTimeoutMs === 0 ? this.t.progressOff : this.t.progressMinutes(Math.round(this.progressTimeoutMs / 60_000))));
|
|
1528
1665
|
lines.push(this.t.allowUsersField(this.config.allowUsers.length, this.config.allowUsers.length === 0));
|
|
1529
1666
|
lines.push(this.t.allowChatsField(this.config.allowChats.length, this.config.allowChats.length === 0));
|
|
@@ -1876,7 +2013,7 @@ export class AgentRunner {
|
|
|
1876
2013
|
return;
|
|
1877
2014
|
}
|
|
1878
2015
|
const now = Date.now();
|
|
1879
|
-
const locale = this.
|
|
2016
|
+
const locale = this.language === "en" ? "en-US" : "zh-CN";
|
|
1880
2017
|
const lines = reminders.map((r) => {
|
|
1881
2018
|
const due = Date.parse(r.scheduledAt) <= now;
|
|
1882
2019
|
const state = due ? this.t.reminderDue : this.t.reminderClock;
|