cue-console 0.1.17 → 0.1.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cue-console",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Cue Hub console launcher (Next.js UI)",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": ["mcp", "cue", "console", "nextjs"],
@@ -12,9 +12,15 @@ import {
12
12
  type SetStateAction,
13
13
  } from "react";
14
14
  import { Button } from "@/components/ui/button";
15
+ import {
16
+ Dialog,
17
+ DialogContent,
18
+ DialogHeader,
19
+ DialogTitle,
20
+ } from "@/components/ui/dialog";
15
21
  import { cn, getAgentEmoji } from "@/lib/utils";
16
22
  import { setAgentDisplayName } from "@/lib/actions";
17
- import { CornerUpLeft, GripVertical, Plus, Send, Trash2, X } from "lucide-react";
23
+ import { Bot, CornerUpLeft, GripVertical, Plus, Send, Trash2, X } from "lucide-react";
18
24
 
19
25
  type MentionDraft = {
20
26
  userId: string;
@@ -71,6 +77,8 @@ export function ChatComposer({
71
77
  setImages,
72
78
  setNotice,
73
79
  setPreviewImage,
80
+ botEnabled,
81
+ onToggleBot,
74
82
  handleSend,
75
83
  enqueueCurrent,
76
84
  queue,
@@ -113,6 +121,8 @@ export function ChatComposer({
113
121
  setImages: Dispatch<SetStateAction<{ mime_type: string; base64_data: string; file_name?: string }[]>>;
114
122
  setNotice: Dispatch<SetStateAction<string | null>>;
115
123
  setPreviewImage: Dispatch<SetStateAction<{ mime_type: string; base64_data: string } | null>>;
124
+ botEnabled: boolean;
125
+ onToggleBot: () => Promise<boolean>;
116
126
  handleSend: () => void | Promise<void>;
117
127
  enqueueCurrent: () => void;
118
128
  queue: QueuedMessage[];
@@ -152,6 +162,8 @@ export function ChatComposer({
152
162
  }, [onBack]);
153
163
 
154
164
  const [dragIndex, setDragIndex] = useState<number | null>(null);
165
+ const [botToggling, setBotToggling] = useState(false);
166
+ const [botConfirmOpen, setBotConfirmOpen] = useState(false);
155
167
  const isComposingRef = useRef(false);
156
168
 
157
169
  const submitOrQueue = () => {
@@ -251,6 +263,54 @@ export function ChatComposer({
251
263
  </div>
252
264
  )}
253
265
 
266
+ <Dialog open={botConfirmOpen} onOpenChange={setBotConfirmOpen}>
267
+ <DialogContent className="sm:max-w-110">
268
+ <DialogHeader>
269
+ <DialogTitle>Enable bot mode?</DialogTitle>
270
+ </DialogHeader>
271
+
272
+ <div className="text-sm text-muted-foreground space-y-3">
273
+ <p>
274
+ Bot mode will automatically reply to <span className="text-foreground font-medium">cue</span> requests
275
+ in this conversation.
276
+ </p>
277
+ <ul className="list-disc pl-5 space-y-1">
278
+ <li>Only affects the current {type === "group" ? "group" : "agent"} conversation.</li>
279
+ <li>May immediately reply to currently pending cue requests.</li>
280
+ <li>Does not reply to pause confirmations.</li>
281
+ <li>You can turn it off anytime.</li>
282
+ </ul>
283
+ </div>
284
+
285
+ <div className="mt-4 flex items-center justify-end gap-2">
286
+ <Button
287
+ type="button"
288
+ variant="outline"
289
+ disabled={botToggling}
290
+ onClick={() => setBotConfirmOpen(false)}
291
+ >
292
+ Cancel
293
+ </Button>
294
+ <Button
295
+ type="button"
296
+ disabled={botToggling}
297
+ onClick={async () => {
298
+ if (botToggling) return;
299
+ setBotToggling(true);
300
+ try {
301
+ await onToggleBot();
302
+ setBotConfirmOpen(false);
303
+ } finally {
304
+ setBotToggling(false);
305
+ }
306
+ }}
307
+ >
308
+ {botToggling ? "Enabling…" : "Enable"}
309
+ </Button>
310
+ </div>
311
+ </DialogContent>
312
+ </Dialog>
313
+
254
314
  {/* Image Preview */}
255
315
  {images.length > 0 && (
256
316
  <div className="flex max-w-full gap-2 overflow-x-auto px-0.5 pt-0.5">
@@ -614,6 +674,48 @@ export function ChatComposer({
614
674
  >
615
675
  Queue
616
676
  </Button>
677
+
678
+ <div className="relative group">
679
+ <Button
680
+ type="button"
681
+ variant="ghost"
682
+ size="icon"
683
+ disabled={busy || botToggling}
684
+ className={cn(
685
+ "relative h-9 w-9 rounded-2xl",
686
+ "hover:bg-white/40",
687
+ botEnabled ? "text-primary" : "text-muted-foreground",
688
+ (busy || botToggling) && "opacity-60 cursor-not-allowed"
689
+ )}
690
+ onClick={async () => {
691
+ if (busy || botToggling) return;
692
+ if (!botEnabled) {
693
+ setBotConfirmOpen(true);
694
+ return;
695
+ }
696
+ setBotToggling(true);
697
+ try {
698
+ await onToggleBot();
699
+ } finally {
700
+ setBotToggling(false);
701
+ }
702
+ }}
703
+ aria-label={botEnabled ? "Stop bot" : "Start bot"}
704
+ title={botToggling ? "Turning…" : botEnabled ? "Stop bot" : "Start bot"}
705
+ >
706
+ {botEnabled && (
707
+ <span className="pointer-events-none absolute inset-0 rounded-xl">
708
+ <span className="absolute inset-0 rounded-2xl bg-primary/15 blur-md animate-pulse" />
709
+ </span>
710
+ )}
711
+ <Bot
712
+ className={cn(
713
+ "relative z-10 h-5 w-5",
714
+ botEnabled && "drop-shadow-[0_0_12px_rgba(99,102,241,0.45)]"
715
+ )}
716
+ />
717
+ </Button>
718
+ </div>
617
719
  </div>
618
720
 
619
721
  <Button
@@ -25,6 +25,7 @@ import {
25
25
  submitResponse,
26
26
  cancelRequest,
27
27
  batchRespond,
28
+ processBotTick,
28
29
  type CueRequest,
29
30
  } from "@/lib/actions";
30
31
  import { ChatComposer } from "@/components/chat-composer";
@@ -77,6 +78,12 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
77
78
  const deferredInput = useDeferredValue(input);
78
79
  const imagesRef = useRef(images);
79
80
 
81
+ const botStorageKey = useMemo(() => {
82
+ return `cue-console:botEnabled:${type}:${id}`;
83
+ }, [type, id]);
84
+
85
+ const [botEnabled, setBotEnabled] = useState(false);
86
+
80
87
  const { soundEnabled, setSoundEnabled, playDing } = useAudioNotification();
81
88
 
82
89
  const {
@@ -103,6 +110,17 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
103
110
 
104
111
  const [composerPadPx, setComposerPadPx] = useState(36 * 4);
105
112
 
113
+ const botHolderIdRef = useRef<string>(
114
+ (() => {
115
+ try {
116
+ return globalThis.crypto?.randomUUID?.() || `bot-${Date.now()}-${Math.random().toString(16).slice(2)}`;
117
+ } catch {
118
+ return `bot-${Date.now()}-${Math.random().toString(16).slice(2)}`;
119
+ }
120
+ })()
121
+ );
122
+ const botTickBusyRef = useRef(false);
123
+
106
124
  const nextCursorRef = useRef<string | null>(null);
107
125
  const loadingMoreRef = useRef(false);
108
126
 
@@ -207,6 +225,85 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
207
225
  setError,
208
226
  });
209
227
 
228
+ const triggerBotTickOnce = useCallback(async () => {
229
+ if (document.visibilityState !== "visible") return;
230
+ if (botTickBusyRef.current) return;
231
+ botTickBusyRef.current = true;
232
+ try {
233
+ const res = await processBotTick({
234
+ holderId: botHolderIdRef.current,
235
+ convType: type,
236
+ convId: id,
237
+ limit: 80,
238
+ });
239
+ if (res.success && res.replied > 0) {
240
+ await refreshLatest();
241
+ }
242
+ } catch {
243
+ } finally {
244
+ botTickBusyRef.current = false;
245
+ }
246
+ }, [id, refreshLatest, type]);
247
+
248
+ const toggleBot = useCallback(async (): Promise<boolean> => {
249
+ const prev = botEnabled;
250
+ const next = !prev;
251
+ setBotEnabled(next);
252
+ try {
253
+ window.localStorage.setItem(botStorageKey, next ? "1" : "0");
254
+ if (next) void triggerBotTickOnce();
255
+ return next;
256
+ } catch {
257
+ setBotEnabled(prev);
258
+ setNotice("Failed to toggle bot");
259
+ return prev;
260
+ }
261
+ }, [botEnabled, botStorageKey, setNotice, triggerBotTickOnce]);
262
+
263
+ useEffect(() => {
264
+ try {
265
+ const raw = window.localStorage.getItem(botStorageKey);
266
+ setBotEnabled(raw === "1");
267
+ } catch {
268
+ setBotEnabled(false);
269
+ }
270
+ }, [botStorageKey]);
271
+
272
+ useEffect(() => {
273
+ if (!botEnabled) return;
274
+
275
+ let cancelled = false;
276
+
277
+ const tick = async () => {
278
+ if (cancelled) return;
279
+ if (document.visibilityState !== "visible") return;
280
+ if (botTickBusyRef.current) return;
281
+ botTickBusyRef.current = true;
282
+ try {
283
+ const res = await processBotTick({
284
+ holderId: botHolderIdRef.current,
285
+ convType: type,
286
+ convId: id,
287
+ limit: 80,
288
+ });
289
+ if (cancelled) return;
290
+ if (res.success && res.replied > 0) {
291
+ await refreshLatest();
292
+ }
293
+ } catch {
294
+ } finally {
295
+ botTickBusyRef.current = false;
296
+ }
297
+ };
298
+
299
+ void tick();
300
+ const interval = setInterval(() => void tick(), 2500);
301
+ return () => {
302
+ cancelled = true;
303
+ clearInterval(interval);
304
+ };
305
+ }, [botEnabled, id, refreshLatest, type]);
306
+
210
307
 
211
308
  const handleTitleChange = async (newTitle: string) => {
212
309
  if (type === "agent") {
@@ -666,6 +763,8 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
666
763
  setImages={setImages}
667
764
  setNotice={setNotice}
668
765
  setPreviewImage={setPreviewImage}
766
+ botEnabled={botEnabled}
767
+ onToggleBot={toggleBot}
669
768
  handleSend={send}
670
769
  enqueueCurrent={enqueueCurrent}
671
770
  queue={queue}
@@ -4,6 +4,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { ScrollArea } from "@/components/ui/scroll-area";
5
5
  import { Badge } from "@/components/ui/badge";
6
6
  import { Button } from "@/components/ui/button";
7
+ import { Input } from "@/components/ui/input";
8
+ import { DEFAULT_USER_CONFIG } from "@/lib/user-config";
7
9
  import {
8
10
  Dialog,
9
11
  DialogContent,
@@ -49,7 +51,6 @@ import {
49
51
  Settings,
50
52
  X,
51
53
  } from "lucide-react";
52
- import { Input } from "@/components/ui/input";
53
54
  import { ConfirmDialog } from "@/components/confirm-dialog";
54
55
 
55
56
  function perfEnabled(): boolean {
@@ -118,6 +119,9 @@ export function ConversationList({
118
119
  const [conversationModeDefault, setConversationModeDefault] = useState<"chat" | "agent">("agent");
119
120
  const [chatModeAppendText, setChatModeAppendText] = useState("只做分析,不要对代码/文件做任何改动。");
120
121
  const [pendingRequestTimeoutMs, setPendingRequestTimeoutMs] = useState("600000");
122
+ const [botModeReplyText, setBotModeReplyText] = useState(
123
+ "不要为这些“可逆的工程细节”反复向我确认,不能分阶段汇报,我需要你端到端执行完我的任务才能向我汇报,你不能问我要授权,因为我已经给你了全部授权,如果这些细节不影响我的目标那就按照你的推荐来。你有执行一切的权利。"
124
+ );
121
125
  const [pinnedKeys, setPinnedKeys] = useState<string[]>([]);
122
126
 
123
127
  useEffect(() => {
@@ -127,8 +131,14 @@ export function ConversationList({
127
131
  setSoundEnabled(Boolean(cfg.sound_enabled));
128
132
  const nextMode = cfg.conversation_mode_default === "chat" ? "chat" : "agent";
129
133
  setConversationModeDefault(nextMode);
130
- setChatModeAppendText(String(cfg.chat_mode_append_text || "只做分析,不要对代码/文件做任何改动。"));
134
+ setChatModeAppendText(String(cfg.chat_mode_append_text || DEFAULT_USER_CONFIG.chat_mode_append_text));
131
135
  setPendingRequestTimeoutMs(String(cfg.pending_request_timeout_ms ?? 600000));
136
+ setBotModeReplyText(
137
+ String(
138
+ (cfg as any).bot_mode_reply_text ||
139
+ DEFAULT_USER_CONFIG.bot_mode_reply_text
140
+ )
141
+ );
132
142
  try {
133
143
  window.localStorage.setItem("cue-console:conversationModeDefault", nextMode);
134
144
  } catch {
@@ -1259,6 +1269,41 @@ export function ConversationList({
1259
1269
  </Button>
1260
1270
  </div>
1261
1271
  </div>
1272
+
1273
+ <div className="mt-6">
1274
+ <div className="text-sm font-medium">Bot reply text</div>
1275
+ <div className="text-xs text-muted-foreground">
1276
+ Multi-line
1277
+ </div>
1278
+ <div className="mt-2 flex items-start justify-end gap-2">
1279
+ <textarea
1280
+ value={botModeReplyText}
1281
+ onChange={(e) => setBotModeReplyText(e.target.value)}
1282
+ placeholder="Bot reply"
1283
+ className="min-h-24 flex-1 min-w-0 resize-y rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
1284
+ />
1285
+ <Button
1286
+ type="button"
1287
+ variant="secondary"
1288
+ size="sm"
1289
+ className="h-9 rounded-md px-3 text-xs"
1290
+ onClick={async () => {
1291
+ const next = botModeReplyText;
1292
+ try {
1293
+ await setUserConfig({ bot_mode_reply_text: next });
1294
+ } catch {
1295
+ }
1296
+ window.dispatchEvent(
1297
+ new CustomEvent("cue-console:configUpdated", {
1298
+ detail: { bot_mode_reply_text: next },
1299
+ })
1300
+ );
1301
+ }}
1302
+ >
1303
+ Save
1304
+ </Button>
1305
+ </div>
1306
+ </div>
1262
1307
  </DialogContent>
1263
1308
  </Dialog>
1264
1309
 
@@ -2,13 +2,7 @@
2
2
 
3
3
  import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
4
4
  import { getUserConfig, type UserConfig } from "@/lib/actions";
5
-
6
- const defaultConfig: UserConfig = {
7
- sound_enabled: true,
8
- conversation_mode_default: "agent",
9
- chat_mode_append_text: "只做分析,不要对代码/文件做任何改动。",
10
- pending_request_timeout_ms: 10 * 60 * 1000,
11
- };
5
+ import { DEFAULT_USER_CONFIG } from "@/lib/user-config";
12
6
 
13
7
  type ConfigContextValue = {
14
8
  config: UserConfig;
@@ -25,7 +19,7 @@ export function useConfig() {
25
19
  }
26
20
 
27
21
  export function ConfigProvider({ children }: { children: ReactNode }) {
28
- const [config, setConfig] = useState<UserConfig>(defaultConfig);
22
+ const [config, setConfig] = useState<UserConfig>(DEFAULT_USER_CONFIG);
29
23
 
30
24
  useEffect(() => {
31
25
  let cancelled = false;
@@ -69,7 +63,26 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
69
63
  );
70
64
  } catch {
71
65
  }
72
- }, [config.pending_request_timeout_ms, config.chat_mode_append_text]);
66
+ try {
67
+ window.localStorage.setItem(
68
+ "cue-console:bot_mode_enabled",
69
+ config.bot_mode_enabled ? "1" : "0"
70
+ );
71
+ } catch {
72
+ }
73
+ try {
74
+ window.localStorage.setItem(
75
+ "cue-console:bot_mode_reply_text",
76
+ String(config.bot_mode_reply_text || "")
77
+ );
78
+ } catch {
79
+ }
80
+ }, [
81
+ config.pending_request_timeout_ms,
82
+ config.chat_mode_append_text,
83
+ config.bot_mode_enabled,
84
+ config.bot_mode_reply_text,
85
+ ]);
73
86
 
74
87
  const value = useMemo<ConfigContextValue>(() => ({ config }), [config]);
75
88
 
@@ -56,12 +56,15 @@ export type { Group, UserResponse, ImageContent, ConversationItem } from "./type
56
56
  export type { CueRequest, CueResponse, AgentTimelineItem } from "./db";
57
57
  import { v4 as uuidv4 } from "uuid";
58
58
 
59
- export type UserConfig = {
60
- sound_enabled: boolean;
61
- conversation_mode_default: "chat" | "agent";
62
- chat_mode_append_text: string;
63
- pending_request_timeout_ms: number;
64
- };
59
+ import {
60
+ DEFAULT_USER_CONFIG,
61
+ clampNumber,
62
+ normalizeMultiline,
63
+ normalizeSingleLine,
64
+ type UserConfig,
65
+ } from "./user-config";
66
+
67
+ export type { UserConfig } from "./user-config";
65
68
 
66
69
  export type QueuedMessage = {
67
70
  id: string;
@@ -70,23 +73,6 @@ export type QueuedMessage = {
70
73
  createdAt: number;
71
74
  };
72
75
 
73
- const defaultUserConfig: UserConfig = {
74
- sound_enabled: true,
75
- conversation_mode_default: "agent",
76
- chat_mode_append_text: "只做分析,不要对代码/文件做任何改动。",
77
- pending_request_timeout_ms: 10 * 60 * 1000,
78
- };
79
-
80
- function clampNumber(n: number, min: number, max: number): number {
81
- if (!Number.isFinite(n)) return min;
82
- return Math.max(min, Math.min(max, n));
83
- }
84
-
85
- function normalizeSingleLine(s: string): string {
86
- const t = String(s ?? "").replace(/\r?\n/g, " ").trim();
87
- return t;
88
- }
89
-
90
76
  function getUserConfigPath(): string {
91
77
  return path.join(os.homedir(), ".cue", "config.json");
92
78
  }
@@ -100,24 +86,34 @@ export async function getUserConfig(): Promise<UserConfig> {
100
86
  sound_enabled:
101
87
  typeof parsed.sound_enabled === "boolean"
102
88
  ? parsed.sound_enabled
103
- : defaultUserConfig.sound_enabled,
89
+ : DEFAULT_USER_CONFIG.sound_enabled,
104
90
  conversation_mode_default:
105
91
  parsed.conversation_mode_default === "chat" || parsed.conversation_mode_default === "agent"
106
92
  ? parsed.conversation_mode_default
107
- : defaultUserConfig.conversation_mode_default,
93
+ : DEFAULT_USER_CONFIG.conversation_mode_default,
108
94
 
109
95
  chat_mode_append_text:
110
96
  typeof parsed.chat_mode_append_text === "string" && normalizeSingleLine(parsed.chat_mode_append_text).length > 0
111
97
  ? normalizeSingleLine(parsed.chat_mode_append_text)
112
- : defaultUserConfig.chat_mode_append_text,
98
+ : DEFAULT_USER_CONFIG.chat_mode_append_text,
113
99
 
114
100
  pending_request_timeout_ms:
115
101
  typeof parsed.pending_request_timeout_ms === "number"
116
102
  ? clampNumber(parsed.pending_request_timeout_ms, 60_000, 86_400_000)
117
- : defaultUserConfig.pending_request_timeout_ms,
103
+ : DEFAULT_USER_CONFIG.pending_request_timeout_ms,
104
+
105
+ bot_mode_enabled:
106
+ typeof parsed.bot_mode_enabled === "boolean"
107
+ ? parsed.bot_mode_enabled
108
+ : DEFAULT_USER_CONFIG.bot_mode_enabled,
109
+
110
+ bot_mode_reply_text:
111
+ typeof parsed.bot_mode_reply_text === "string" && normalizeMultiline(parsed.bot_mode_reply_text).length > 0
112
+ ? normalizeMultiline(parsed.bot_mode_reply_text)
113
+ : DEFAULT_USER_CONFIG.bot_mode_reply_text,
118
114
  };
119
115
  } catch {
120
- return defaultUserConfig;
116
+ return DEFAULT_USER_CONFIG;
121
117
  }
122
118
  }
123
119
 
@@ -140,6 +136,14 @@ export async function setUserConfig(next: Partial<UserConfig>): Promise<UserConf
140
136
  typeof next.pending_request_timeout_ms === "number"
141
137
  ? clampNumber(next.pending_request_timeout_ms, 60_000, 86_400_000)
142
138
  : prev.pending_request_timeout_ms,
139
+
140
+ bot_mode_enabled:
141
+ typeof next.bot_mode_enabled === "boolean" ? next.bot_mode_enabled : prev.bot_mode_enabled,
142
+
143
+ bot_mode_reply_text:
144
+ typeof next.bot_mode_reply_text === "string" && normalizeMultiline(next.bot_mode_reply_text).length > 0
145
+ ? normalizeMultiline(next.bot_mode_reply_text)
146
+ : prev.bot_mode_reply_text,
143
147
  };
144
148
  const p = getUserConfigPath();
145
149
  await fs.mkdir(path.dirname(p), { recursive: true });
@@ -147,6 +151,63 @@ export async function setUserConfig(next: Partial<UserConfig>): Promise<UserConf
147
151
  return merged;
148
152
  }
149
153
 
154
+ export async function processBotTick(args: {
155
+ holderId: string;
156
+ convType: ConversationType;
157
+ convId: string;
158
+ limit?: number;
159
+ }): Promise<{ success: true; acquired: boolean; replied: number } | { success: false; error: string }> {
160
+ try {
161
+ const holderId = String(args.holderId || "").trim();
162
+ if (!holderId) return { success: false, error: "holderId required" } as const;
163
+
164
+ const convType = args.convType === "group" ? "group" : "agent";
165
+ const convId = String(args.convId || "").trim();
166
+ if (!convId) return { success: false, error: "convId required" } as const;
167
+
168
+ const cfg = await getUserConfig();
169
+
170
+ const lease = acquireWorkerLease({
171
+ leaseKey: `cue-console:bot-mode:${convType}:${convId}`,
172
+ holderId,
173
+ ttlMs: 5_000,
174
+ });
175
+ if (!lease.acquired) return { success: true, acquired: false, replied: 0 } as const;
176
+
177
+ const limit = Math.max(1, Math.min(200, args.limit ?? 50));
178
+ const pending =
179
+ convType === "agent"
180
+ ? getRequestsByAgent(convId)
181
+ .filter((r) => r.status === "PENDING")
182
+ .filter((r) => {
183
+ if (!r.payload) return true;
184
+ try {
185
+ const obj = JSON.parse(r.payload) as Record<string, unknown>;
186
+ return !(obj?.type === "confirm" && obj?.variant === "pause");
187
+ } catch {
188
+ return true;
189
+ }
190
+ })
191
+ .slice(0, limit)
192
+ : getGroupPendingRequests(convId).slice(0, limit);
193
+
194
+ let replied = 0;
195
+ const text = cfg.bot_mode_reply_text;
196
+ for (const r of pending) {
197
+ try {
198
+ sendResponse(String(r.request_id), { text }, false);
199
+ replied += 1;
200
+ } catch {
201
+ // ignore per-request failures
202
+ }
203
+ }
204
+
205
+ return { success: true, acquired: true, replied } as const;
206
+ } catch (e) {
207
+ return { success: false, error: e instanceof Error ? e.message : String(e) } as const;
208
+ }
209
+ }
210
+
150
211
  export async function fetchAgentDisplayNames(agentIds: string[]) {
151
212
  return getAgentDisplayNames(agentIds);
152
213
  }
@@ -0,0 +1,32 @@
1
+ export type UserConfig = {
2
+ sound_enabled: boolean;
3
+ conversation_mode_default: "chat" | "agent";
4
+ chat_mode_append_text: string;
5
+ pending_request_timeout_ms: number;
6
+ bot_mode_enabled: boolean;
7
+ bot_mode_reply_text: string;
8
+ };
9
+
10
+ export const DEFAULT_USER_CONFIG: UserConfig = {
11
+ sound_enabled: true,
12
+ conversation_mode_default: "agent",
13
+ chat_mode_append_text: "只做分析,不要对代码/文件做任何改动。",
14
+ pending_request_timeout_ms: 10 * 60 * 1000,
15
+ bot_mode_enabled: false,
16
+ bot_mode_reply_text:
17
+ "不要为“可逆的工程细节”反复向我确认,不能分阶段汇报,我需要你端到端执行完我的任务才能向我汇报,你不能问我要授权,因为我已经给你了除了 push以外的全部授权,,如果这些细节不影响我的目标那就按照你的推荐来。你有执行一切的能力。最后,如果你执行完我的任务请使用 cue pause 来回复我.",
18
+ };
19
+
20
+ export function clampNumber(n: number, min: number, max: number): number {
21
+ if (!Number.isFinite(n)) return min;
22
+ return Math.max(min, Math.min(max, n));
23
+ }
24
+
25
+ export function normalizeSingleLine(s: string): string {
26
+ const t = String(s ?? "").replace(/\r?\n/g, " ").trim();
27
+ return t;
28
+ }
29
+
30
+ export function normalizeMultiline(s: string): string {
31
+ return String(s ?? "").replace(/\r\n/g, "\n").trim();
32
+ }