scream-code 0.19.3 → 0.19.5

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.
@@ -5,8 +5,8 @@ const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
6
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
7
7
  import { A as sharedMemoStore } from "./src-Cu3sUIJK.mjs";
8
- import { $ as clearGoalState, $n as validateFeishuCredentials, A as langFromPath, An as embeddingFailureDetail, Ar as resolveGlobalLogPath, At as isTurnElapsedEnabled, B as charsForTokenBudget, Bn as WeixinApiError, Bt as ENABLE_TERMINAL_THEME_REPORTING, Cn as getNoActiveSessionMessage, Cr as ScreamHarness, Ct as clearInfoPanelState, D as renderDiffLines, Dn as isExperimentalFlagEnabled, Dr as resolveScreamHome, Dt as createEditorTheme, E as ToolCallComponent, En as sortSlashCommands, Er as isRealUserPrompt, Et as resolveThemeSync, F as replaceTabs, Fn as PULSE_WAVE_FRAMES, Ft as detectTerminalTheme, G as lerpHex, Gn as buildCdnDownloadUrl, Gt as QUERY_TERMINAL_THEME, H as getSharedSpeedTracker, Hn as isSendThrottled, Ht as OSC11_RESPONSE, I as sanitizeShellOutput, In as readLineDaemonStatus, It as parseOsc11BackgroundTheme, J as WelcomeComponent, Jn as TuiConfigParseError, Jt as TERMINAL_THEME_DARK, K as BackgroundAgentStatusComponent, Kn as decryptAesEcb, Kt as TERMINAL_FOCUS_IN, L as SkillActivationComponent, Ln as startLineDaemonDetached, Lt as DISABLE_TERMINAL_FOCUS_REPORTING, M as ThinkingComponent, Mn as hasEmbeddingModelCache, Mr as isOrphanedToolCallError, Mt as createThemeStyles, N as NoticeMessageComponent, Nn as startManualEmbeddingDownload, Nr as SCREAM_ERROR_INFO, Nt as contrastTextHex, O as renderDiffLinesClustered, On as setExperimentalFlags, Or as flushDiagnosticLogs, Ot as createMarkdownTheme, P as StatusMessageComponent, Pn as PIXEL_PULSE_FRAMES, Pt as getColorPalette, Q as CompactionComponent, Qn as feishuFileType, R as AssistantMessageComponent, Rn as stopLineDaemon, Rt as DISABLE_TERMINAL_THEME_REPORTING, S as readJsonlFile, Sn as getLlmNotSetMessage, Sr as saveCatalogCache, St as supportsBalance, T as UserMessageComponent, Tn as BUILTIN_SLASH_COMMANDS, U as GRADIENT_CYCLE_MS, Un as validateWeixinToken, Ut as OSC11_RESPONSE_PREFIX, V as estimateTokens, Vn as WeixinClient, Vt as OSC11_QUERY, W as lerpGradient, Wt as OSC11_RESPONSE_PREFIX_NO_ESC, X as getBreathingFrame, Xn as FeishuClient, Xt as safeUsageRatio, Y as BREATHE_CYCLE_MS, Yn as loadTuiConfig, Yt as TERMINAL_THEME_LIGHT, Z as resetBreathingClock, Zn as FeishuCredentialError, Zt as SubagentSlots, _n as MAIN_AGENT_ID, _r as CLI_UI_MODE, a as writeTextClipboard, an as STATUS_BULLET, ar as getWeixinCursorFile, at as toTerminalHyperlink, b as readUpdateCache, bn as getCtrlCHint, br as DEFAULT_CATALOG_URL, cn as argsRecord, cr as resolveCustomCommands, d as isExpandable, dn as parseStreamingArgs, dr as detectShellEnvironment, er as LineConfigError, f as isPlanExpandable, fn as serializeToolResultOutput, fr as getDataDir, ft as handleFusionPlanCommand, gn as EXIT_CONFIRM_WINDOW_MS, gr as CLI_COMMAND_NAME, hr as detectInstallSource, i as openUrl, in as printableChar, ir as getWeixinContextTokensFile, j as CachedContainer, jn as getKnowledgeStore, jr as isScreamError, jt as toggleEmptySessionHint, k as highlightLines, kn as embeddingProgressLabel, kr as log, kt as isEmptySessionHintDismissed, l as disposeChildren, ln as formatErrorMessage, lr as resolveFeishuOptions, m as formatMemoryMemoForInjection, mn as truncateErrorMessage, mr as getLogDir, mt as handlePlanCommand, n as clearEvalPanelState, nn as isBusy, nr as getLineConfigPath, o as syncDiagramCapabilityPrompt, on as appendSessionHeaderHint, or as getWeixinStateDir, ot as changeThinkingLevel, p as MoonLoader, pn as stringValue, pr as getInputHistoryFile, q as AgentGroupComponent, qn as parseAesKey, qt as TERMINAL_FOCUS_OUT, rn as isStreaming, rr as getLinePidFile, sn as appendStreamingArgsPreview, sr as loadLineConfig, st as getModelCycleLevel, t as dispatchInput, tr as assertSinglePlatformConfig, u as hasDispose, un as isTodoItemShape, ur as resolveWeixinOptions, v as selectUpdateTarget, vn as SESSION_TIPS, vr as CLI_USER_AGENT_PRODUCT, wn as buildSkillSlashCommands, wr as normalizeWorkDir, x as appendJsonlLine, xn as getCtrlDHint, xr as fetchCatalog, xt as refreshProviderBalance, y as refreshUpdateCache, yn as TIP_ROTATION_INTERVAL_MS, yr as PRODUCT_NAME, z as ActivityGroupComponent, zn as UPLOAD_MEDIA_TYPE, zt as ENABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-BziMqJ_r.mjs";
9
- import { i as t, n as getLocale, r as setLocale } from "./text-input-dialog-yvsx17gg.mjs";
8
+ import { $n as validateFeishuCredentials, A as langFromPath, An as hasEmbeddingModelCache, Ar as resolveGlobalLogPath, At as createThemeStyles, B as getSharedSpeedTracker, Bn as isSendThrottled, Bt as OSC11_RESPONSE, Cn as BUILTIN_SLASH_COMMANDS, Cr as ScreamHarness, D as renderDiffLines, Dn as embeddingProgressLabel, Dr as resolveScreamHome, Dt as isEmptySessionHintDismissed, E as ToolCallComponent, En as setExperimentalFlags, Er as isRealUserPrompt, Et as createMarkdownTheme, F as SkillActivationComponent, Fn as startLineDaemonDetached, Ft as DISABLE_TERMINAL_FOCUS_REPORTING, G as AgentGroupComponent, Gn as parseAesKey, Gt as TERMINAL_FOCUS_OUT, H as lerpGradient, Ht as OSC11_RESPONSE_PREFIX_NO_ESC, I as AssistantMessageComponent, In as stopLineDaemon, It as DISABLE_TERMINAL_THEME_REPORTING, J as getBreathingFrame, Jn as TuiConfigParseError, Jt as safeUsageRatio, K as WelcomeComponent, Kn as replaceTabs, Kt as TERMINAL_THEME_DARK, L as ActivityGroupComponent, Ln as UPLOAD_MEDIA_TYPE, Lt as ENABLE_TERMINAL_FOCUS_REPORTING, M as ThinkingComponent, Mn as PIXEL_PULSE_FRAMES, Mr as isOrphanedToolCallError, Mt as getColorPalette, N as NoticeMessageComponent, Nn as PULSE_WAVE_FRAMES, Nr as SCREAM_ERROR_INFO, Nt as detectTerminalTheme, O as renderDiffLinesClustered, On as embeddingFailureDetail, Or as flushDiagnosticLogs, Ot as isTurnElapsedEnabled, P as StatusMessageComponent, Pn as readLineDaemonStatus, Pt as parseOsc11BackgroundTheme, Qn as feishuFileType, R as charsForTokenBudget, Rn as WeixinApiError, Rt as ENABLE_TERMINAL_THEME_REPORTING, S as readJsonlFile, Sn as buildSkillSlashCommands, Sr as saveCatalogCache, T as UserMessageComponent, Tn as isExperimentalFlagEnabled, Tt as createEditorTheme, U as lerpHex, Un as buildCdnDownloadUrl, Ut as QUERY_TERMINAL_THEME, V as GRADIENT_CYCLE_MS, Vn as validateWeixinToken, Vt as OSC11_RESPONSE_PREFIX, W as BackgroundAgentStatusComponent, Wn as decryptAesEcb, Wt as TERMINAL_FOCUS_IN, X as CompactionComponent, Xn as FeishuClient, Y as resetBreathingClock, Yn as loadTuiConfig, Yt as SubagentSlots, Z as clearGoalState, Zn as FeishuCredentialError, _n as TIP_ROTATION_INTERVAL_MS, _r as CLI_UI_MODE, a as writeTextClipboard, an as appendStreamingArgsPreview, ar as getWeixinCursorFile, at as getModelCycleLevel, b as readUpdateCache, bn as getLlmNotSetMessage, br as DEFAULT_CATALOG_URL, bt as supportsBalance, cn as isTodoItemShape, cr as resolveCustomCommands, d as isExpandable, dn as stringValue, dr as detectShellEnvironment, en as isBusy, er as LineConfigError, f as isPlanExpandable, fn as truncateErrorMessage, fr as getDataDir, ft as handlePlanCommand, gn as SESSION_TIPS, gr as CLI_COMMAND_NAME, hn as MAIN_AGENT_ID, hr as detectInstallSource, i as openUrl, in as appendSessionHeaderHint, ir as getWeixinContextTokensFile, it as changeThinkingLevel, j as CachedContainer, jn as startManualEmbeddingDownload, jr as isScreamError, jt as contrastTextHex, k as highlightLines, kn as getKnowledgeStore, kr as log, kt as toggleEmptySessionHint, l as disposeChildren, ln as parseStreamingArgs, lr as resolveFeishuOptions, m as formatMemoryMemoForInjection, mn as EXIT_CONFIRM_WINDOW_MS, mr as getLogDir, n as clearEvalPanelState, nn as printableChar, nr as getLineConfigPath, o as syncDiagramCapabilityPrompt, on as argsRecord, or as getWeixinStateDir, p as MoonLoader, pr as getInputHistoryFile, q as BREATHE_CYCLE_MS, qn as sanitizeShellOutput, qt as TERMINAL_THEME_LIGHT, rn as STATUS_BULLET, rr as getLinePidFile, rt as toTerminalHyperlink, sn as formatErrorMessage, sr as loadLineConfig, t as dispatchInput, tn as isStreaming, tr as assertSinglePlatformConfig, u as hasDispose, un as serializeToolResultOutput, ur as resolveWeixinOptions, ut as handleFusionPlanCommand, v as selectUpdateTarget, vn as getCtrlCHint, vr as CLI_USER_AGENT_PRODUCT, wn as sortSlashCommands, wr as normalizeWorkDir, wt as resolveThemeSync, x as appendJsonlLine, xn as getNoActiveSessionMessage, xr as fetchCatalog, xt as clearInfoPanelState, y as refreshUpdateCache, yn as getCtrlDHint, yr as PRODUCT_NAME, yt as refreshProviderBalance, z as estimateTokens, zn as WeixinClient, zt as OSC11_QUERY } from "./dispatch-Brb_yK-J.mjs";
9
+ import { i as t, n as getLocale, r as setLocale } from "./text-input-dialog-D6PfbOws.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { createHash, randomInt, randomUUID } from "node:crypto";
12
12
  import { mkdir, mkdtemp, open, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
@@ -21,14 +21,14 @@ import { fileURLToPath, pathToFileURL } from "node:url";
21
21
  import { Command, Option } from "commander";
22
22
  import { createInterface } from "node:readline/promises";
23
23
  import { createInterface as createInterface$1 } from "node:readline";
24
- import chalk, { chalkStderr } from "chalk";
25
24
  import { CombinedAutocompleteProvider, Container, Editor, HStack, Input, Key, ProcessTerminal, ScrollView, Spacer, Text, TuiAltScreen, VStack, decodeKittyPrintable, deleteAllKittyImages, fuzzyFilter, fuzzyMatch, getCapabilities, isKeyRelease, matchesKey, sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@liutod-scream/pi-tui";
25
+ import chalk, { chalkStderr } from "chalk";
26
26
  //#region src/cli/build-info.ts
27
27
  function optionalBuildString(value) {
28
28
  return typeof value === "string" && value.length > 0 ? value : void 0;
29
29
  }
30
30
  const SCREAM_BUILD_INFO = {
31
- version: optionalBuildString("0.19.3"),
31
+ version: optionalBuildString("0.19.5"),
32
32
  channel: optionalBuildString(""),
33
33
  commit: optionalBuildString(""),
34
34
  buildTarget: optionalBuildString("darwin-arm64")
@@ -719,6 +719,31 @@ var FeishuPlatform = class {
719
719
  await this.client.sendToChat(chatId, "file", JSON.stringify({ file_key: fileKey }));
720
720
  return kind === "voice" ? "(飞书按文件发送,语音气泡仅微信支持)" : "";
721
721
  }
722
+ /**
723
+ * Sends text to the chat that owns this session with no inbound message to
724
+ * reply to — the scheduled-task path. Lark only needs the chat id, which is
725
+ * cached per session on every inbound message, so a fire at 3am still lands.
726
+ */
727
+ async sendProactive(sessionKey, text) {
728
+ const body = text.trim();
729
+ if (body === "") return "no-target";
730
+ const chatId = this.chatBySession.get(sessionKey);
731
+ if (chatId === void 0) {
732
+ log.warn("line-feishu: no chat recorded for proactive send", { sessionKey });
733
+ return "no-target";
734
+ }
735
+ try {
736
+ const outbound = buildReplyContent(truncateForLine(body));
737
+ await this.client.sendToChat(chatId, outbound.msgType, outbound.body);
738
+ return "sent";
739
+ } catch (error) {
740
+ log.warn("line-feishu: proactive send failed", {
741
+ sessionKey,
742
+ error: String(error)
743
+ });
744
+ return "failed";
745
+ }
746
+ }
722
747
  /** Cancels pending batch timers (daemon shutdown). */
723
748
  stop() {
724
749
  for (const batch of this.pendingImages.values()) clearTimeout(batch.timer);
@@ -1667,12 +1692,66 @@ function buildChannelPrompt(platform) {
1667
1692
  ].join("\n");
1668
1693
  }
1669
1694
  //#endregion
1695
+ //#region src/line/cron-commands.ts
1696
+ const PROMPT_PREVIEW_CHARS = 60;
1697
+ /** `add <min hour dom mon dow> <prompt…>` → the two fields the RPC wants. */
1698
+ function parseCronAdd(parts) {
1699
+ const rest = [...parts];
1700
+ let silent = false;
1701
+ let mute = false;
1702
+ while (rest[0] === "--silent" || rest[0] === "--mute") if (rest.shift() === "--silent") silent = true;
1703
+ else mute = true;
1704
+ if (rest.length < 6) return null;
1705
+ const cron = rest.slice(0, 5).join(" ");
1706
+ const prompt = rest.slice(5).join(" ").trim();
1707
+ if (prompt === "") return null;
1708
+ return {
1709
+ cron,
1710
+ prompt,
1711
+ ...silent ? { silent: true } : {},
1712
+ ...mute ? { mute: true } : {}
1713
+ };
1714
+ }
1715
+ function describeNext(task, now) {
1716
+ if (task.nextFireAt === null) return task.recurring ? "不再触发" : "已触发";
1717
+ const minutes = Math.max(0, Math.round((task.nextFireAt - now) / 6e4));
1718
+ if (minutes <= 1) return "即将触发";
1719
+ return `下次 ${minutes} 分钟后`;
1720
+ }
1721
+ function shorten$1(text) {
1722
+ const chars = Array.from(text);
1723
+ return chars.length > PROMPT_PREVIEW_CHARS ? `${chars.slice(0, PROMPT_PREVIEW_CHARS - 1).join("")}…` : text;
1724
+ }
1725
+ /** Listing for `/cron` / `/cron list`; `now` is injectable for tests. */
1726
+ function formatCronList(tasks, now = Date.now()) {
1727
+ if (tasks.length === 0) return "还没有定时任务。\n/cron add <分 时 日 月 周> <要做的事>";
1728
+ const lines = [`定时任务(${tasks.length})`];
1729
+ for (const task of tasks) {
1730
+ const flags = [
1731
+ describeNext(task, now),
1732
+ task.stale ? "已过期" : "",
1733
+ task.recurring ? "" : "一次性",
1734
+ task.silent === true ? "静默" : "",
1735
+ task.mute === true ? "静音" : ""
1736
+ ].filter((part) => part !== "").join(" · ");
1737
+ lines.push(`${task.id} ${task.cron} ${flags}`);
1738
+ lines.push(` ${shorten$1(task.prompt)}`);
1739
+ if (task.lastError !== void 0 && task.lastError !== null) lines.push(` 上次失败:${shorten$1(task.lastError)}`);
1740
+ }
1741
+ lines.push("回复 /cron rm <id> 删除。");
1742
+ return lines.join("\n");
1743
+ }
1744
+ const CRON_USAGE = "用法:/cron list · /cron add [--silent|--mute] <分 时 日 月 周> <要做的事> · /cron rm <id>";
1745
+ //#endregion
1670
1746
  //#region src/line/command-router.ts
1671
1747
  const HELP_TEXT = [
1672
1748
  "可用命令:",
1673
1749
  "/status 查看当前模型、权限与上下文占用",
1674
1750
  "/model <名称> 切换模型(不带参数查看当前值)",
1675
1751
  "/compact 压缩上下文",
1752
+ "/stop 停止正在运行的任务",
1753
+ "/cron 查看定时任务(add/rm 见用法)",
1754
+ "/cancel 停止并开始新会话",
1676
1755
  "/new 开启新会话",
1677
1756
  "/help 显示本帮助",
1678
1757
  "",
@@ -1686,7 +1765,7 @@ const DESKTOP_ONLY = new Set([
1686
1765
  "/login",
1687
1766
  "/image"
1688
1767
  ]);
1689
- const BUSY_TEXT = "当前有任务正在处理中,请等这一轮结束后再发该命令。";
1768
+ const BUSY_TEXT = "当前有任务正在处理中:回复 /stop 可中止,或等这一轮结束后再发该命令。";
1690
1769
  /** Built-ins plus the user's configured commands. */
1691
1770
  function helpText(commands) {
1692
1771
  if (commands.length === 0) return HELP_TEXT;
@@ -1738,6 +1817,71 @@ async function runLineCommand$1(rawText, deps) {
1738
1817
  handled: true,
1739
1818
  text: await modelText(deps, arg)
1740
1819
  };
1820
+ case "/stop":
1821
+ if (!(deps.stopTurn?.() ?? false)) return {
1822
+ handled: true,
1823
+ text: "当前没有正在运行的任务。"
1824
+ };
1825
+ return {
1826
+ handled: true,
1827
+ text: "已停止当前任务。"
1828
+ };
1829
+ case "/cron": {
1830
+ let session;
1831
+ try {
1832
+ session = await deps.getSession();
1833
+ } catch (error) {
1834
+ return {
1835
+ handled: true,
1836
+ text: `读取会话失败:${describeError(error)}`
1837
+ };
1838
+ }
1839
+ const tokens = arg.trim() === "" ? [] : arg.trim().split(/\s+/);
1840
+ const sub = tokens[0];
1841
+ const parts = tokens.slice(1);
1842
+ try {
1843
+ if (sub === void 0 || sub === "list") return {
1844
+ handled: true,
1845
+ text: formatCronList(await session.listCronTasks())
1846
+ };
1847
+ if (sub === "add") {
1848
+ const parsed = parseCronAdd(parts);
1849
+ if (parsed === null) return {
1850
+ handled: true,
1851
+ text: CRON_USAGE
1852
+ };
1853
+ const task = await session.createCronTask(parsed);
1854
+ return {
1855
+ handled: true,
1856
+ text: `已创建定时任务 ${task.cron}:\n${task.prompt}`
1857
+ };
1858
+ }
1859
+ if (sub === "rm" || sub === "remove") {
1860
+ if (parts.length === 0) return {
1861
+ handled: true,
1862
+ text: CRON_USAGE
1863
+ };
1864
+ const removed = await session.removeCronTasks(parts);
1865
+ return {
1866
+ handled: true,
1867
+ text: removed.length === 0 ? "没有找到该任务。" : `已删除 ${removed.length} 个任务。`
1868
+ };
1869
+ }
1870
+ return {
1871
+ handled: true,
1872
+ text: CRON_USAGE
1873
+ };
1874
+ } catch (error) {
1875
+ return {
1876
+ handled: true,
1877
+ text: `定时任务操作失败:${describeError(error)}`
1878
+ };
1879
+ }
1880
+ }
1881
+ case "/cancel": return {
1882
+ handled: true,
1883
+ text: await deps.cancelSession?.() ?? false ? "已停止当前任务并开始新会话。" : "已开始新会话。"
1884
+ };
1741
1885
  case "/compact":
1742
1886
  if (deps.isBusy()) return {
1743
1887
  handled: true,
@@ -1813,7 +1957,8 @@ async function statusText$1(deps) {
1813
1957
  `模型:${status.model ?? (deps.defaultModel || "未知")}`,
1814
1958
  `模式:${modes.join(" · ")}`,
1815
1959
  `上下文:${percent}(${formatTokens(status.contextTokens)}/${formatTokens(status.maxContextTokens)})`,
1816
- `思考级别:${status.thinkingLevel}`
1960
+ `思考级别:${status.thinkingLevel}`,
1961
+ ...deps.voiceStatus === void 0 || deps.voiceStatus === null ? [] : [deps.voiceStatus]
1817
1962
  ].join("\n");
1818
1963
  } catch (error) {
1819
1964
  return `读取状态失败:${describeError(error)}`;
@@ -1846,19 +1991,174 @@ function expandPrompt(template, arg) {
1846
1991
  return arg.length > 0 ? `${template}\n\n${arg}` : template;
1847
1992
  }
1848
1993
  //#endregion
1849
- //#region src/line/session-driver.ts
1994
+ //#region src/line/voice-support.ts
1850
1995
  /**
1851
- * Line session driver — maps Feishu chats to scream agent sessions.
1996
+ * Startup capability probe for outbound voice.
1852
1997
  *
1853
- * Uses the same headless layer as `scream stream-json` (`ScreamHarness`),
1854
- * including the create/resume/repair session lifecycle (ported from
1855
- * `run-stream-json.ts` L659-726) and the approval-handler mapping. Turns are
1856
- * serialized per session (one FIFO chain each) and run in parallel across
1857
- * sessions; a bounded queue rejects new work with a friendly busy reply
1858
- * instead of growing unbounded.
1998
+ * Voice sending is the one path that depends on the host's ffmpeg being built
1999
+ * with an AMR-NB *encoder* — plenty of package-manager builds ship the decoder
2000
+ * only. Discovering that at send time turns every voice reply into a fallback
2001
+ * file plus an apology, so the daemon probes once at startup and reports the
2002
+ * answer in `/status`.
2003
+ *
2004
+ * The probe never throws: a failure to answer is `unknown`, which is treated as
2005
+ * "try anyway" rather than "unavailable".
1859
2006
  */
1860
- const MAX_QUEUE_PER_SESSION = 3;
1861
- const ORPHAN_RESET_NOTICE = "会话已自动重置,请重新发送你的消息。";
2007
+ const execFileAsync$2 = promisify(execFile);
2008
+ const PROBE_TIMEOUT_MS = 5e3;
2009
+ /** Parses `ffmpeg -encoders` output for an AMR-NB encoder. */
2010
+ function parseAmrEncoderListing(stdout) {
2011
+ return stdout.split(/\r?\n/).some((line) => /\bamr_nb\b|\blibopencore_amrnb\b/i.test(line) && /^\s*A/.test(line));
2012
+ }
2013
+ /** One-shot probe; callers cache the result (ffmpeg does not appear mid-run). */
2014
+ async function probeVoiceSupport() {
2015
+ try {
2016
+ const { stdout } = await execFileAsync$2("ffmpeg", ["-hide_banner", "-encoders"], {
2017
+ timeout: PROBE_TIMEOUT_MS,
2018
+ maxBuffer: 4 * 1024 * 1024,
2019
+ windowsHide: true
2020
+ });
2021
+ return parseAmrEncoderListing(stdout) ? "ready" : "no-encoder";
2022
+ } catch (error) {
2023
+ return error.code === "ENOENT" ? "no-ffmpeg" : "unknown";
2024
+ }
2025
+ }
2026
+ /** One-line description for `/status`; `null` when no probe has run yet. */
2027
+ function describeVoiceSupport(support) {
2028
+ switch (support) {
2029
+ case "ready": return "语音:可用(AMR-NB)";
2030
+ case "no-ffmpeg": return "语音:不可用(未找到 ffmpeg,将改发文件)";
2031
+ case "no-encoder": return "语音:不可用(ffmpeg 缺少 AMR-NB 编码器,将改发文件)";
2032
+ case "unknown": return "语音:未确认(ffmpeg 探测失败,发送时再判断)";
2033
+ case null: return null;
2034
+ }
2035
+ }
2036
+ //#endregion
2037
+ //#region src/tui/utils/child-request-notice.ts
2038
+ const CHILD_REQUEST_KINDS = [
2039
+ "info",
2040
+ "handoff",
2041
+ "escalate"
2042
+ ];
2043
+ /**
2044
+ * Longest previewed value kept on one row. The kernel allows 8192 chars
2045
+ * (`contact-parent.ts`); the ticker is one terminal line, so anything longer is
2046
+ * cut here and the full text remains reachable via the agent card.
2047
+ */
2048
+ const TEXT_PREVIEW = 200;
2049
+ /**
2050
+ * Collapse the request onto previewable single-paragraph form.
2051
+ *
2052
+ * The text comes from a child agent's model output, i.e. untrusted input that
2053
+ * is about to be written into a terminal row: control sequences (CSI/OSC/DCS —
2054
+ * screen clears, cursor moves, OSC 52 clipboard writes) are stripped first, the
2055
+ * same way shell output is sanitized elsewhere. The cap counts code points, not
2056
+ * UTF-16 units, so a surrogate pair is never split into a replacement glyph.
2057
+ */
2058
+ function previewText(value) {
2059
+ const flattened = sanitizeShellOutput(value).replaceAll(/\s*\n\s*/g, " ").trim();
2060
+ const codePoints = Array.from(flattened);
2061
+ return codePoints.length > TEXT_PREVIEW ? `${codePoints.slice(0, TEXT_PREVIEW).join("")}…` : flattened;
2062
+ }
2063
+ function kindOf(raw) {
2064
+ return typeof raw === "string" && CHILD_REQUEST_KINDS.includes(raw) ? raw : "info";
2065
+ }
2066
+ function textOf(raw) {
2067
+ if (typeof raw !== "string") return void 0;
2068
+ const trimmed = raw.trim();
2069
+ return trimmed.length > 0 ? trimmed : void 0;
2070
+ }
2071
+ /**
2072
+ * Live producer: the delivery event the kernel emits once a request landed in
2073
+ * the parent's turn. Events cross the wire as untyped JSON, so every field is
2074
+ * re-checked here rather than trusted from the declared type.
2075
+ */
2076
+ function childRequestFieldsFromEvent(event) {
2077
+ const record = event;
2078
+ const message = textOf(record["message"]);
2079
+ if (message === void 0) return null;
2080
+ const listOf = (key) => {
2081
+ const raw = record[key];
2082
+ if (!Array.isArray(raw)) return void 0;
2083
+ const items = raw.map((item) => textOf(item)).filter((item) => item !== void 0);
2084
+ return items.length > 0 ? items : void 0;
2085
+ };
2086
+ return {
2087
+ requestType: kindOf(record["requestType"]),
2088
+ message,
2089
+ needs: textOf(record["needs"]),
2090
+ expecting: textOf(record["expecting"]),
2091
+ artifacts: listOf("artifacts"),
2092
+ evidence: listOf("evidence"),
2093
+ missing: listOf("missing")
2094
+ };
2095
+ }
2096
+ //#endregion
2097
+ //#region src/tui/utils/cron-chip.ts
2098
+ /**
2099
+ * Scheduled-task reminder → one-line status chip.
2100
+ *
2101
+ * A fired cron task already owns a transcript block (`CronMessageComponent`) and
2102
+ * a turn. The status bar adds the *live* signal: the reminder is time-sensitive,
2103
+ * so it must be visible above the editor for a few seconds even when the user is
2104
+ * looking at the input box, not the transcript.
2105
+ *
2106
+ * Copy follows the shared chip grammar `<symbol> <label>:<value>` — the symbol
2107
+ * lives in the copy, the colour comes from the caller (`accent` for a normal
2108
+ * fire, `warning` for a stale/overdue one).
2109
+ */
2110
+ /** Longest task prompt shown on the row. The full text stays in the transcript
2111
+ * block, so the chip only needs enough words to recognise the task. */
2112
+ const CRON_PREVIEW_CHARS = 40;
2113
+ function shorten(value, max) {
2114
+ const codePoints = Array.from(value);
2115
+ return codePoints.length > max ? `${codePoints.slice(0, max - 1).join("")}…` : value;
2116
+ }
2117
+ /**
2118
+ * Build the chip for one `cron.fired` delivery.
2119
+ *
2120
+ * `previewText` runs first: the prompt is model/tool-authored text about to be
2121
+ * written into a terminal row, so control sequences are stripped and newlines
2122
+ * collapse before the length cap is applied.
2123
+ *
2124
+ * Precedence, most notable first:
2125
+ * stale (7-day cutoff) → coalesced (missed theoretical fires) → one-shot →
2126
+ * plain recurring fire.
2127
+ */
2128
+ function buildCronReminderChip(event) {
2129
+ const origin = event.origin;
2130
+ const preview = shorten(previewText(event.prompt), CRON_PREVIEW_CHARS) || origin.cron;
2131
+ if (origin.stale) return {
2132
+ text: t("status.cron_stale", { preview }),
2133
+ tone: "missed"
2134
+ };
2135
+ if (origin.coalescedCount > 1) return {
2136
+ text: t("status.cron_fired_coalesced", {
2137
+ count: String(origin.coalescedCount),
2138
+ preview
2139
+ }),
2140
+ tone: "fired"
2141
+ };
2142
+ if (!origin.recurring) return {
2143
+ text: t("status.cron_once", { preview }),
2144
+ tone: "fired"
2145
+ };
2146
+ return {
2147
+ text: t("status.cron_fired", { preview }),
2148
+ tone: "fired"
2149
+ };
2150
+ }
2151
+ /**
2152
+ * Chat-side notice for a fired task that finished without any reply text — an
2153
+ * error swallowed by the agent, or a turn the model ended silently. The chat
2154
+ * should hear *something*; silence is the one outcome a reminder must not have.
2155
+ */
2156
+ function buildCronFailureNotice(event) {
2157
+ const origin = event.origin;
2158
+ return t("status.cron_empty", { preview: shorten(previewText(event.prompt), CRON_PREVIEW_CHARS) || origin.cron });
2159
+ }
2160
+ //#endregion
2161
+ //#region src/line/turn-collector.ts
1862
2162
  /**
1863
2163
  * Collects the final content output of one turn from the session event
1864
2164
  * stream (main agent only).
@@ -1912,8 +2212,124 @@ var LineTurnCollector = class {
1912
2212
  }
1913
2213
  }
1914
2214
  };
2215
+ //#endregion
2216
+ //#region src/line/cron-relay.ts
2217
+ /**
2218
+ * Scheduled-task relay.
2219
+ *
2220
+ * A fired cron task runs *inside* the session, not through `submit()`, so its
2221
+ * turn carries no inbound context for the platform to reply to. This class
2222
+ * turns that turn into a delivery: announce the fire, collect the final text,
2223
+ * hand it to the caller's `send`.
2224
+ *
2225
+ * Two rules keep it from double-posting:
2226
+ * - a fire that lands while a user turn is running is dropped — `steer()` has
2227
+ * already merged it into that turn, whose reply reaches the chat anyway;
2228
+ * - one forward at a time per session; a second fire during collection is
2229
+ * skipped rather than interleaved.
2230
+ */
2231
+ /** A fired turn that never reports its end must not block later fires. */
2232
+ const DEFAULT_COLLECT_TIMEOUT_MS = 15 * 6e4;
2233
+ var CronRelay = class {
2234
+ options;
2235
+ collector;
2236
+ guard;
2237
+ constructor(options) {
2238
+ this.options = options;
2239
+ }
2240
+ /**
2241
+ * A scheduled task fired; `notice` is the one-liner shown before the result.
2242
+ *
2243
+ * `pendingStart` is the `turn.started` event the core emits *before* the
2244
+ * `cron.fired` event (`steer()` launches the turn synchronously). Without
2245
+ * replaying it the collector would ignore every delta of that turn and never
2246
+ * settle, so no result would ever be delivered.
2247
+ */
2248
+ onFire(notice, pendingStart, failureNotice) {
2249
+ if (this.options.isBusy()) return;
2250
+ if (this.collector !== void 0) {
2251
+ log.warn("line: cron fire ignored, previous forward still collecting");
2252
+ return;
2253
+ }
2254
+ const collector = new LineTurnCollector(() => {
2255
+ this.clearGuard();
2256
+ this.collector = void 0;
2257
+ this.deliver(collector, failureNotice);
2258
+ });
2259
+ this.collector = collector;
2260
+ this.guard = setTimeout(() => {
2261
+ if (this.collector !== collector) return;
2262
+ this.collector = void 0;
2263
+ log.warn("line: scheduled task turn produced no end event", { timeoutMs: this.options.collectTimeoutMs ?? DEFAULT_COLLECT_TIMEOUT_MS });
2264
+ }, this.options.collectTimeoutMs ?? DEFAULT_COLLECT_TIMEOUT_MS);
2265
+ this.guard.unref?.();
2266
+ if (pendingStart !== void 0) collector.handleEvent(pendingStart);
2267
+ if (notice !== null) this.options.send(notice).then((result) => {
2268
+ if (result !== "sent") log.warn("line: cron notice not delivered", { result });
2269
+ });
2270
+ }
2271
+ /** True while a fired task's turn is being collected. */
2272
+ isCollecting() {
2273
+ return this.collector !== void 0;
2274
+ }
2275
+ /** Drops any in-flight collection (session reset / shutdown). */
2276
+ dispose() {
2277
+ this.clearGuard();
2278
+ this.collector = void 0;
2279
+ }
2280
+ clearGuard() {
2281
+ if (this.guard === void 0) return;
2282
+ clearTimeout(this.guard);
2283
+ this.guard = void 0;
2284
+ }
2285
+ /** Every other event of the session; ignored unless a fire is being collected. */
2286
+ onEvent(event) {
2287
+ if (this.collector === void 0) return;
2288
+ if (this.options.isBusy()) {
2289
+ this.clearGuard();
2290
+ this.collector = void 0;
2291
+ return;
2292
+ }
2293
+ this.collector.handleEvent(event);
2294
+ }
2295
+ async deliver(collector, failureNotice) {
2296
+ if (this.options.isBusy()) return;
2297
+ const text = collector.text.trim();
2298
+ if (text === "") {
2299
+ log.warn("line: scheduled task produced no reply text", { ended: collector.ended });
2300
+ if (failureNotice !== void 0) {
2301
+ const result = await this.options.send(failureNotice);
2302
+ if (result !== "sent") log.warn("line: cron failure notice not delivered", { result });
2303
+ }
2304
+ return;
2305
+ }
2306
+ const result = await this.options.send(text);
2307
+ if (result !== "sent") log.warn("line: cron result not delivered", { result });
2308
+ }
2309
+ };
2310
+ //#endregion
2311
+ //#region src/line/session-driver.ts
2312
+ /**
2313
+ * Line session driver — maps Feishu chats to scream agent sessions.
2314
+ *
2315
+ * Uses the same headless layer as `scream stream-json` (`ScreamHarness`),
2316
+ * including the create/resume/repair session lifecycle (ported from
2317
+ * `run-stream-json.ts` L659-726) and the approval-handler mapping. Turns are
2318
+ * serialized per session (one FIFO chain each) and run in parallel across
2319
+ * sessions; a bounded queue rejects new work with a friendly busy reply
2320
+ * instead of growing unbounded.
2321
+ */
2322
+ const MAX_QUEUE_PER_SESSION = 3;
2323
+ const ORPHAN_RESET_NOTICE = "会话已自动重置,请重新发送你的消息。";
1915
2324
  /** A turn that never settles is cancelled after this long (product default). */
1916
2325
  const TURN_TIMEOUT_MS = 900 * 1e3;
2326
+ /**
2327
+ * `/stop` interrupts in-process. A tool that ignores the abort would otherwise
2328
+ * keep the chat busy until the 15-minute cap, so after this window the live
2329
+ * session is closed: history stays on disk and the next message resumes the same
2330
+ * session id — the same end result as tearing down the agent process.
2331
+ */
2332
+ const STOP_ESCALATION_MS = 1e4;
1917
2333
  const TURN_TIMEOUT_TEXT = "本轮执行超时(15 分钟),已取消。请重试或拆分任务。";
1918
2334
  /** `exec`-type custom commands are bounded like any other IM command. */
1919
2335
  const CUSTOM_COMMAND_TIMEOUT_MS = 3e4;
@@ -1939,12 +2355,31 @@ var LineSessionDriver = class {
1939
2355
  promptedSessions = /* @__PURE__ */ new WeakSet();
1940
2356
  /** sessionKey → platform sender for outbound media (registered by the daemon). */
1941
2357
  mediaSenders = /* @__PURE__ */ new Map();
2358
+ /** Fallback sender resolved from the platform's own target cache, so a
2359
+ * scheduled-task turn can still send media after a reset or restart. */
2360
+ mediaSenderResolver;
1942
2361
  /** Session id → sessionKey, so a host tool call can find its channel. */
1943
2362
  sessionKeyBySessionId = /* @__PURE__ */ new Map();
1944
2363
  /** Chats currently executing `/new`; a turn must not start meanwhile. */
1945
2364
  resetting = /* @__PURE__ */ new Set();
1946
- /** Deadline timer for the running turn (see `turnDeadline`). */
1947
- turnTimer;
2365
+ /** The turn executing right now per chat, so `/stop` has something to abort.
2366
+ * Chats run in parallel, so this is keyed by session — a single slot would
2367
+ * let one chat's turn hide another's from `/stop`. */
2368
+ activeTurns = /* @__PURE__ */ new Map();
2369
+ /** Bumped by `/stop`: turns queued before the bump are dropped instead of
2370
+ * running. "Stop" has to mean the messages I just sent, not only the one
2371
+ * that happens to be executing. */
2372
+ stopEpoch = /* @__PURE__ */ new Map();
2373
+ /** Escalation timers for a `/stop` whose turn never settles, per chat. */
2374
+ stopEscalations = /* @__PURE__ */ new Map();
2375
+ /** Injected by the daemon: sends to a chat with no inbound message to reply
2376
+ * to. Undefined when no platform supports proactive delivery. */
2377
+ proactiveSender;
2378
+ /** One relay + event subscription per live session, so scheduled tasks can
2379
+ * reach the chat. Replaced on session reset, dropped on close. */
2380
+ cronForwarders = /* @__PURE__ */ new Map();
2381
+ /** Cached ffmpeg capability answer (see `probeVoice`). */
2382
+ voiceSupport = null;
1948
2383
  /** In-flight creations, so a command and a turn cannot create two sessions. */
1949
2384
  sessionCreations = /* @__PURE__ */ new Map();
1950
2385
  /** Per-chat lifecycle mutex: session creation must never interleave a reset. */
@@ -1966,6 +2401,100 @@ var LineSessionDriver = class {
1966
2401
  this.harness = harness;
1967
2402
  }
1968
2403
  /**
2404
+ * Probes the host for an AMR-NB encoder once. Called by the daemon at startup
2405
+ * so `/status` can warn before the first voice reply falls back to a file.
2406
+ * Never throws.
2407
+ */
2408
+ async probeVoice() {
2409
+ try {
2410
+ this.voiceSupport = await probeVoiceSupport();
2411
+ if (describeVoiceSupport(this.voiceSupport) !== null) log.info("line: voice capability", { support: this.voiceSupport });
2412
+ } catch (error) {
2413
+ this.voiceSupport = "unknown";
2414
+ log.warn("line: voice probe failed", { error: String(error) });
2415
+ }
2416
+ }
2417
+ /**
2418
+ * `/stop`: aborts the turn that is running now. The cancel itself happens
2419
+ * inside `runTurn` so the queue unwinds in one place; this only requests it.
2420
+ */
2421
+ stopActiveTurn(sessionKey) {
2422
+ const running = this.activeTurns.get(sessionKey);
2423
+ const pending = this.queues.get(sessionKey)?.pending ?? 0;
2424
+ const forwarder = this.cronForwarders.get(sessionKey);
2425
+ const forwarding = forwarder?.relay.isCollecting() ?? false;
2426
+ const cronTurn = forwarder?.isCronTurnRunning() ?? false;
2427
+ if (running === void 0 && pending === 0 && !forwarding && !cronTurn) return false;
2428
+ this.stopEpoch.set(sessionKey, (this.stopEpoch.get(sessionKey) ?? 0) + 1);
2429
+ if (running !== void 0) {
2430
+ running.stop();
2431
+ this.armStopEscalation(sessionKey);
2432
+ }
2433
+ if (forwarding || cronTurn) {
2434
+ forwarder?.relay.dispose();
2435
+ this.sessions.get(sessionKey)?.cancel().catch(() => {});
2436
+ log.info("line: dropped a scheduled-task turn", { sessionKey });
2437
+ }
2438
+ log.info("line: stop requested", {
2439
+ sessionKey,
2440
+ running: running !== void 0,
2441
+ pending
2442
+ });
2443
+ return true;
2444
+ }
2445
+ /**
2446
+ * `/cancel`: stop whatever runs and start a clean session. Falls back to
2447
+ * closing the live session when a hung tool refuses to settle, then resets.
2448
+ */
2449
+ async cancelSession(sessionKey) {
2450
+ const stopped = this.stopActiveTurn(sessionKey);
2451
+ const timeout = new Promise((resolve) => {
2452
+ setTimeout(() => {
2453
+ resolve("timeout");
2454
+ }, STOP_ESCALATION_MS).unref?.();
2455
+ });
2456
+ const reset = this.resetSession(sessionKey).then(() => "done", () => "done");
2457
+ if (await Promise.race([reset, timeout]) === "timeout") {
2458
+ log.warn("line: cancel could not reset in time, closing live session", { sessionKey });
2459
+ await this.forceCloseSession(sessionKey);
2460
+ await this.resetSession(sessionKey).catch((error) => {
2461
+ log.warn("line: cancel reset failed after close", {
2462
+ sessionKey,
2463
+ error: String(error)
2464
+ });
2465
+ });
2466
+ }
2467
+ return stopped;
2468
+ }
2469
+ armStopEscalation(sessionKey) {
2470
+ this.clearStopEscalation(sessionKey);
2471
+ const timer = setTimeout(() => {
2472
+ this.stopEscalations.delete(sessionKey);
2473
+ if (!this.activeTurns.has(sessionKey)) return;
2474
+ log.warn("line: /stop did not settle, closing the live session", { sessionKey });
2475
+ this.forceCloseSession(sessionKey);
2476
+ }, STOP_ESCALATION_MS);
2477
+ timer.unref?.();
2478
+ this.stopEscalations.set(sessionKey, timer);
2479
+ }
2480
+ clearStopEscalation(sessionKey) {
2481
+ const timer = this.stopEscalations.get(sessionKey);
2482
+ if (timer === void 0) return;
2483
+ clearTimeout(timer);
2484
+ this.stopEscalations.delete(sessionKey);
2485
+ }
2486
+ /** Closes the live session without deleting it: the next message resumes. */
2487
+ async forceCloseSession(sessionKey) {
2488
+ this.stopCronForwarder(sessionKey);
2489
+ const session = this.sessions.get(sessionKey);
2490
+ this.sessions.delete(sessionKey);
2491
+ if (session !== void 0) {
2492
+ this.sessionKeyBySessionId.delete(session.id);
2493
+ await session.close().catch(() => {});
2494
+ }
2495
+ this.mediaSenders.delete(sessionKey);
2496
+ }
2497
+ /**
1969
2498
  * Submit a turn on the session for `sessionKey`. Serialized per session;
1970
2499
  * resolves with the turn outcome (final text or a friendly error).
1971
2500
  */
@@ -2003,10 +2532,28 @@ var LineSessionDriver = class {
2003
2532
  text: "上一条消息还在处理中,队列已满,请稍后再发。"
2004
2533
  };
2005
2534
  }
2535
+ const epoch = this.stopEpoch.get(effectiveJob.sessionKey) ?? 0;
2536
+ const waitingBehind = queue.pending;
2006
2537
  queue.pending += 1;
2007
2538
  this.queues.set(effectiveJob.sessionKey, queue);
2539
+ if (waitingBehind > 0) try {
2540
+ effectiveJob.onQueued?.(waitingBehind);
2541
+ } catch (error) {
2542
+ log.warn("line: queued notice failed", {
2543
+ sessionKey: effectiveJob.sessionKey,
2544
+ error: String(error)
2545
+ });
2546
+ }
2008
2547
  let outcome;
2009
2548
  const next = queue.chain.then(async () => {
2549
+ if ((this.stopEpoch.get(effectiveJob.sessionKey) ?? 0) !== epoch) {
2550
+ outcome = {
2551
+ ok: false,
2552
+ text: "",
2553
+ silent: true
2554
+ };
2555
+ return;
2556
+ }
2010
2557
  try {
2011
2558
  outcome = await this.runTurn(effectiveJob);
2012
2559
  } catch (error) {
@@ -2030,22 +2577,35 @@ var LineSessionDriver = class {
2030
2577
  /**
2031
2578
  * Resolves after `TURN_TIMEOUT_MS`. Raced against the turn promise so a
2032
2579
  * provider that never emits a settlement event cannot wedge the chat queue.
2580
+ *
2581
+ * The timer belongs to this turn, not to the driver: chats run in parallel,
2582
+ * so a shared field would let the first chat to finish cancel the second
2583
+ * chat's deadline and leave that turn with no way out.
2033
2584
  */
2034
2585
  turnDeadline() {
2035
- return new Promise((resolve) => {
2036
- this.turnTimer = setTimeout(resolve, TURN_TIMEOUT_MS);
2037
- this.turnTimer.unref?.();
2038
- });
2039
- }
2040
- clearTurnDeadline() {
2041
- if (this.turnTimer !== void 0) {
2042
- clearTimeout(this.turnTimer);
2043
- this.turnTimer = void 0;
2044
- }
2586
+ let timer;
2587
+ return {
2588
+ promise: new Promise((resolve) => {
2589
+ timer = setTimeout(resolve, TURN_TIMEOUT_MS);
2590
+ timer.unref?.();
2591
+ }),
2592
+ clear: () => {
2593
+ if (timer !== void 0) clearTimeout(timer);
2594
+ timer = void 0;
2595
+ }
2596
+ };
2045
2597
  }
2046
2598
  /** Close all sessions and the harness (daemon shutdown). */
2047
2599
  async close() {
2048
2600
  this.closed = true;
2601
+ for (const timer of this.stopEscalations.values()) clearTimeout(timer);
2602
+ this.stopEscalations.clear();
2603
+ const forwarders = [...this.cronForwarders.values()];
2604
+ this.cronForwarders.clear();
2605
+ for (const forwarder of forwarders) {
2606
+ forwarder.unsubscribe();
2607
+ forwarder.relay.dispose();
2608
+ }
2049
2609
  const sessions = [...this.sessions.values()];
2050
2610
  this.sessions.clear();
2051
2611
  for (const session of sessions) await session.close().catch(() => {});
@@ -2066,16 +2626,42 @@ var LineSessionDriver = class {
2066
2626
  const turnPromise = new Promise((resolve) => {
2067
2627
  resolveTurn = resolve;
2068
2628
  });
2629
+ let stopRequested = false;
2630
+ let requestStop;
2631
+ const stopPromise = new Promise((resolve) => {
2632
+ requestStop = () => {
2633
+ stopRequested = true;
2634
+ resolve();
2635
+ };
2636
+ });
2069
2637
  const collector = new LineTurnCollector(resolveTurn);
2070
2638
  const unsubscribe = session.onEvent((event) => {
2071
2639
  collector.handleEvent(event);
2072
2640
  });
2641
+ const registration = {
2642
+ sessionKey: job.sessionKey,
2643
+ stop: requestStop
2644
+ };
2645
+ this.activeTurns.set(job.sessionKey, registration);
2646
+ const deadline = this.turnDeadline();
2073
2647
  try {
2074
2648
  session.prompt(buildPromptParts(job)).catch((error) => {
2075
2649
  promptError = error;
2076
2650
  resolveTurn();
2077
2651
  });
2078
- await Promise.race([turnPromise, this.turnDeadline()]);
2652
+ await Promise.race([
2653
+ turnPromise,
2654
+ deadline.promise,
2655
+ stopPromise
2656
+ ]);
2657
+ if (stopRequested) {
2658
+ await session.cancel().catch(() => {});
2659
+ return {
2660
+ ok: false,
2661
+ text: "",
2662
+ silent: true
2663
+ };
2664
+ }
2079
2665
  if (!collector.ended) {
2080
2666
  await session.cancel().catch(() => {});
2081
2667
  return {
@@ -2084,8 +2670,10 @@ var LineSessionDriver = class {
2084
2670
  };
2085
2671
  }
2086
2672
  } finally {
2673
+ if (this.activeTurns.get(job.sessionKey) === registration) this.activeTurns.delete(job.sessionKey);
2674
+ this.clearStopEscalation(job.sessionKey);
2087
2675
  unsubscribe();
2088
- this.clearTurnDeadline();
2676
+ deadline.clear();
2089
2677
  }
2090
2678
  if (promptError !== void 0 && isOrphanedToolCallError(promptError)) {
2091
2679
  log.warn("line: resetting session after tool call mismatch", {
@@ -2133,6 +2721,13 @@ var LineSessionDriver = class {
2133
2721
  * Waits for in-flight turns to finish (bounded), so a shutdown does not cut a
2134
2722
  * reply mid-flight. Only the per-chat queues are awaited.
2135
2723
  */
2724
+ /** How many turns are queued or running. Read by the shutdown path so a
2725
+ * timed-out drain is reported instead of looking like a clean exit. */
2726
+ pendingTurnCount() {
2727
+ let total = 0;
2728
+ for (const queue of this.queues.values()) total += queue.pending;
2729
+ return total;
2730
+ }
2136
2731
  async waitIdle(timeoutMs) {
2137
2732
  this.closed = true;
2138
2733
  const chains = [...[...this.queues.values()].map((queue) => queue.chain.catch(() => {})), ...[...this.sessionCreations.values()].map((creation) => creation.then(() => {}).catch(() => {}))];
@@ -2203,6 +2798,63 @@ var LineSessionDriver = class {
2203
2798
  }
2204
2799
  }
2205
2800
  /** Registers the channel's outbound-media sender for one chat. */
2801
+ /** Wired once by the daemon after the platforms are up. */
2802
+ setProactiveSender(sender) {
2803
+ this.proactiveSender = sender;
2804
+ }
2805
+ /**
2806
+ * Watches one session for `cron.fired`.
2807
+ *
2808
+ * A scheduled task runs inside the session, not through `submit()`, so this
2809
+ * subscription is the only path that carries its output back to the chat.
2810
+ */
2811
+ attachCronForwarder(sessionKey, session) {
2812
+ this.stopCronForwarder(sessionKey);
2813
+ const relay = new CronRelay({
2814
+ send: (text) => this.proactiveSender?.(sessionKey, text) ?? Promise.resolve("no-target"),
2815
+ isBusy: () => this.activeTurns.has(sessionKey)
2816
+ });
2817
+ let pendingStart;
2818
+ let cronTurnId;
2819
+ const unsubscribe = session.onEvent((event) => {
2820
+ if (event.type === "turn.started") {
2821
+ pendingStart = event;
2822
+ if (event.agentId === "main") cronTurnId = event.origin.kind === "cron_job" ? event.turnId : void 0;
2823
+ relay.onEvent(event);
2824
+ return;
2825
+ }
2826
+ if (event.type === "turn.ended") {
2827
+ pendingStart = void 0;
2828
+ if (event.turnId === cronTurnId) cronTurnId = void 0;
2829
+ relay.onEvent(event);
2830
+ return;
2831
+ }
2832
+ if (event.type === "cron.fired") {
2833
+ if (event.agentId !== "main") return;
2834
+ if (event.origin.mute === true) return;
2835
+ relay.onFire(event.origin.silent === true ? null : buildCronReminderChip(event).text, pendingStart, event.origin.silent === true ? void 0 : buildCronFailureNotice(event));
2836
+ return;
2837
+ }
2838
+ relay.onEvent(event);
2839
+ });
2840
+ this.cronForwarders.set(sessionKey, {
2841
+ unsubscribe,
2842
+ relay,
2843
+ isCronTurnRunning: () => cronTurnId !== void 0
2844
+ });
2845
+ }
2846
+ stopCronForwarder(sessionKey) {
2847
+ const entry = this.cronForwarders.get(sessionKey);
2848
+ if (entry === void 0) return;
2849
+ entry.unsubscribe();
2850
+ entry.relay.dispose();
2851
+ this.cronForwarders.delete(sessionKey);
2852
+ }
2853
+ /** Wired once by the daemon; covers sessions whose per-message sender is gone
2854
+ * (scheduled tasks, after `/stop`, after a restart). */
2855
+ setMediaSenderResolver(resolver) {
2856
+ this.mediaSenderResolver = resolver;
2857
+ }
2206
2858
  setMediaSender(sessionKey, sender) {
2207
2859
  this.mediaSenders.set(sessionKey, sender);
2208
2860
  }
@@ -2214,7 +2866,10 @@ var LineSessionDriver = class {
2214
2866
  };
2215
2867
  return runLineCommand$1(text, {
2216
2868
  defaultModel: this.defaultModel,
2217
- isBusy: () => (this.queues.get(sessionKey)?.pending ?? 0) > 0,
2869
+ isBusy: () => (this.queues.get(sessionKey)?.pending ?? 0) > 0 || (this.cronForwarders.get(sessionKey)?.relay.isCollecting() ?? false),
2870
+ stopTurn: () => this.stopActiveTurn(sessionKey),
2871
+ cancelSession: () => this.cancelSession(sessionKey),
2872
+ voiceStatus: describeVoiceSupport(this.voiceSupport),
2218
2873
  getSession: () => this.resolveSession(sessionKey),
2219
2874
  resetSession: () => this.resetSession(sessionKey),
2220
2875
  commands: this.options.commands ?? [],
@@ -2272,12 +2927,14 @@ var LineSessionDriver = class {
2272
2927
  const session = await this.getOrCreateSession(harness, sessionKey, workDir, permission, planMode);
2273
2928
  this.sessions.set(sessionKey, session);
2274
2929
  this.sessionKeyBySessionId.set(session.id, sessionKey);
2930
+ this.attachCronForwarder(sessionKey, session);
2275
2931
  await this.applyChannelPrompt(session);
2276
2932
  return session;
2277
2933
  }
2278
2934
  /** Drops the chat's session so the next message starts a fresh one. */
2279
2935
  async resetSession(sessionKey) {
2280
2936
  this.resetting.add(sessionKey);
2937
+ this.stopCronForwarder(sessionKey);
2281
2938
  try {
2282
2939
  const queue = this.queues.get(sessionKey);
2283
2940
  if (queue !== void 0) {
@@ -2340,7 +2997,7 @@ var LineSessionDriver = class {
2340
2997
  isError: true
2341
2998
  };
2342
2999
  const sessionKey = request.sessionId === void 0 ? void 0 : this.sessionKeyBySessionId.get(request.sessionId);
2343
- const sender = sessionKey === void 0 ? void 0 : this.mediaSenders.get(sessionKey);
3000
+ const sender = sessionKey === void 0 ? void 0 : this.mediaSenders.get(sessionKey) ?? this.mediaSenderResolver?.(sessionKey);
2344
3001
  if (sender === void 0) return {
2345
3002
  output: "当前会话没有可用的发送通道(该工具仅在 IM 会话中可用)。",
2346
3003
  isError: true
@@ -3077,6 +3734,41 @@ var WeixinPlatform = class {
3077
3734
  }
3078
3735
  /** Reply text to a peer; chunked, fence-aware, throttle fail-fast. */
3079
3736
  /**
3737
+ * Sends text to the peer that owns this session with no inbound message to
3738
+ * reply to — the scheduled-task path. WeChat requires a `context_token`,
3739
+ * cached from the last inbound message; without one there is nothing we can
3740
+ * legally address, so this reports `no-target` instead of failing a send.
3741
+ */
3742
+ async sendProactive(sessionKey, text) {
3743
+ const body = text.trim();
3744
+ if (body === "") return "no-target";
3745
+ const target = this.latestTargets.get(sessionKey);
3746
+ const peer = target?.peerUserId;
3747
+ if (peer === void 0) {
3748
+ log.warn("line-weixin: no peer recorded for proactive send", { sessionKey });
3749
+ return "no-target";
3750
+ }
3751
+ const token = (target?.contextToken ?? "").trim() || (this.contextTokens.get(peer) ?? "");
3752
+ if (token === "") {
3753
+ log.warn("line-weixin: no context_token for proactive send (user must message first)", { sessionKey });
3754
+ return "no-target";
3755
+ }
3756
+ try {
3757
+ const chunks = splitMessageCodeFenceAware(body);
3758
+ for (let i = 0; i < chunks.length; i++) {
3759
+ if (i > 0) await sleep(100);
3760
+ await this.client.sendText(peer, chunks[i] ?? "", token, this.client.newClientId());
3761
+ }
3762
+ return "sent";
3763
+ } catch (error) {
3764
+ log.warn("line-weixin: proactive send failed", {
3765
+ sessionKey,
3766
+ error: String(error)
3767
+ });
3768
+ return "failed";
3769
+ }
3770
+ }
3771
+ /**
3080
3772
  * Sends a local image or file to the chat that owns this session: upload to
3081
3773
  * the CDN (AES-128-ECB), then `sendmessage` with the media item. Ported from
3082
3774
  * the reference `SendImage`/`SendFile`.
@@ -3136,6 +3828,11 @@ var WeixinPlatform = class {
3136
3828
  }
3137
3829
  };
3138
3830
  await this.client.sendMediaItem(target.peerUserId, target.contextToken, this.client.newClientId(), item);
3831
+ if (effectiveKind === "voice") log.warn("line: voice submitted without delivery receipt", {
3832
+ sessionKey,
3833
+ bytes: data.length,
3834
+ note: "gateway ret=0 is not a delivery confirmation"
3835
+ });
3139
3836
  return note;
3140
3837
  }
3141
3838
  /** Indirection so tests can exercise the ffmpeg-missing fallback. */
@@ -3255,6 +3952,24 @@ function sleep(ms) {
3255
3952
  });
3256
3953
  }
3257
3954
  //#endregion
3955
+ //#region src/line/notices.ts
3956
+ /**
3957
+ * Short receipts the IM channels send outside a turn's own reply.
3958
+ *
3959
+ * Kept apart from the routers so the wording has one home: the same sentence is
3960
+ * sent from the Feishu and WeChat paths, and both need to answer the two
3961
+ * questions a silent queue leaves open — "did you get my message" and "can I get
3962
+ * out of this".
3963
+ */
3964
+ /**
3965
+ * Sent when a message had to wait behind a running turn.
3966
+ *
3967
+ * @param position how many turns are ahead of this message (1 = one running).
3968
+ */
3969
+ function queuedNotice(position) {
3970
+ return `收到,前面还有 ${position} 条在处理中。回复 /stop 可中止当前任务。`;
3971
+ }
3972
+ //#endregion
3258
3973
  //#region src/line/daemon.ts
3259
3974
  /**
3260
3975
  * Line daemon entry point (`scream line`).
@@ -3270,12 +3985,21 @@ function sleep(ms) {
3270
3985
  * indicator) — all platform-specific behavior lives in the platform modules.
3271
3986
  */
3272
3987
  const GROUP_FILTER_RETRY_INTERVAL_MS = 300 * 1e3;
3273
- const SHUTDOWN_GRACE_MS = 1e4;
3988
+ /**
3989
+ * How long a shutdown waits for the in-flight turn to finish before forcing an
3990
+ * exit. Ten seconds used to be enough in theory but cut real turns mid-tool, so
3991
+ * the window matches a slow tool call; the wait is announced on stderr.
3992
+ */
3993
+ const SHUTDOWN_GRACE_MS = 6e4;
3994
+ /** `daemon.log` is read long after the fact, so every line carries a stamp. */
3995
+ function stamp() {
3996
+ return (/* @__PURE__ */ new Date()).toISOString();
3997
+ }
3274
3998
  function out$1(message) {
3275
- process.stdout.write(`${message}\n`);
3999
+ process.stdout.write(`${stamp()} ${message}\n`);
3276
4000
  }
3277
4001
  function err(message) {
3278
- process.stderr.write(`${message}\n`);
4002
+ process.stderr.write(`${stamp()} ${message}\n`);
3279
4003
  }
3280
4004
  /** Run the daemon in the foreground; resolves on graceful shutdown. */
3281
4005
  async function runLineDaemon() {
@@ -3302,6 +4026,9 @@ async function runLineDaemon() {
3302
4026
  process.exitCode = 1;
3303
4027
  return;
3304
4028
  }
4029
+ const resolvedWorkDir = lineConfig.workDir ?? process.cwd();
4030
+ if (lineConfig.workDir === void 0) err(`警告(line):配置未设置 workDir,本次以启动目录作为工作目录:${resolvedWorkDir}(建议在 /line 向导里固定它)`);
4031
+ else out$1(`工作目录:${resolvedWorkDir}`);
3305
4032
  const driver = new LineSessionDriver({
3306
4033
  permissionMode: lineConfig.permissionMode,
3307
4034
  workDir: lineConfig.workDir,
@@ -3315,6 +4042,7 @@ async function runLineDaemon() {
3315
4042
  if (shuttingDown) return;
3316
4043
  shuttingDown = true;
3317
4044
  log.info("line: shutting down", { signal });
4045
+ err(`正在等待当前回合结束(最多 ${SHUTDOWN_GRACE_MS / 1e3}s),收到再次信号将立即退出`);
3318
4046
  const forceTimer = setTimeout(() => {
3319
4047
  err(`line: shutdown exceeded ${SHUTDOWN_GRACE_MS / 1e3}s, forcing exit`);
3320
4048
  process.exit(1);
@@ -3350,6 +4078,7 @@ async function runLineDaemon() {
3350
4078
  await gracefulShutdown(handles, driver);
3351
4079
  process.exit(1);
3352
4080
  }
4081
+ await driver.probeVoice();
3353
4082
  if (lineConfig.feishu) try {
3354
4083
  handles.push(await startFeishu(lineConfig.feishu, driver));
3355
4084
  } catch (error) {
@@ -3360,6 +4089,19 @@ async function runLineDaemon() {
3360
4089
  } catch (error) {
3361
4090
  failures.push(`微信:${error instanceof Error ? error.message : String(error)}`);
3362
4091
  }
4092
+ const proactivePlatforms = new Map(handles.map((handle) => [handle.name, handle]));
4093
+ driver.setProactiveSender(async (sessionKey, text) => {
4094
+ const name = sessionKey.startsWith("line-feishu-") ? "feishu" : sessionKey.startsWith("line-weixin-") ? "weixin" : void 0;
4095
+ const platform = name === void 0 ? void 0 : proactivePlatforms.get(name);
4096
+ if (platform === void 0) return "no-target";
4097
+ return platform.sendProactive(sessionKey, text);
4098
+ });
4099
+ driver.setMediaSenderResolver((sessionKey) => {
4100
+ const name = sessionKey.startsWith("line-feishu-") ? "feishu" : sessionKey.startsWith("line-weixin-") ? "weixin" : void 0;
4101
+ const platform = name === void 0 ? void 0 : proactivePlatforms.get(name);
4102
+ if (platform === void 0) return void 0;
4103
+ return (kind, path) => platform.sendMedia(sessionKey, kind, path);
4104
+ });
3363
4105
  for (const failure of failures) err(`平台启动失败:${failure}`);
3364
4106
  if (handles.length === 0) {
3365
4107
  err("所有平台启动失败,line 守护进程退出。");
@@ -3387,20 +4129,29 @@ async function startFeishu(cfg, driver) {
3387
4129
  const platform = new FeishuPlatform(client, options, async (message) => {
3388
4130
  try {
3389
4131
  driver.setMediaSender(message.sessionKey, (kind, path) => platform.sendMedia(message.sessionKey, kind, path));
3390
- const outbound = buildReplyContent(truncateForLine((await driver.submit({
4132
+ const outcome = await driver.submit({
3391
4133
  sessionKey: message.sessionKey,
3392
4134
  text: message.text,
3393
4135
  images: message.images,
3394
- files: message.files
3395
- })).text));
4136
+ files: message.files,
4137
+ onQueued: (position) => {
4138
+ const notice = buildReplyContent(queuedNotice(position));
4139
+ client.reply(message.context.messageId, notice.msgType, notice.body).catch((error) => {
4140
+ log.warn("line: feishu queued notice failed", { error: String(error) });
4141
+ });
4142
+ }
4143
+ });
4144
+ if (outcome.silent === true) return;
4145
+ const outbound = buildReplyContent(truncateForLine(outcome.text));
3396
4146
  await client.reply(message.context.messageId, outbound.msgType, outbound.body);
3397
4147
  } catch (error) {
3398
4148
  log.error("line: feishu reply failed", {
3399
4149
  messageId: message.context.messageId,
3400
4150
  error: String(error)
3401
4151
  });
4152
+ } finally {
4153
+ await platform.finishMessage(message.context.messageId).catch(() => {});
3402
4154
  }
3403
- await platform.finishMessage(message.context.messageId).catch(() => {});
3404
4155
  });
3405
4156
  await client.connect(async (event) => {
3406
4157
  try {
@@ -3422,6 +4173,8 @@ async function startFeishu(cfg, driver) {
3422
4173
  });
3423
4174
  return {
3424
4175
  name: "feishu",
4176
+ sendProactive: (sessionKey, text) => platform.sendProactive(sessionKey, text),
4177
+ sendMedia: (sessionKey, kind, path) => platform.sendMedia(sessionKey, kind, path),
3425
4178
  close: async () => {
3426
4179
  platform.stop();
3427
4180
  await client.close();
@@ -3441,8 +4194,14 @@ async function startWeixin(cfg, driver) {
3441
4194
  sessionKey: message.sessionKey,
3442
4195
  text: message.text,
3443
4196
  images: message.images,
3444
- files: message.files
4197
+ files: message.files,
4198
+ onQueued: (position) => {
4199
+ platform.replyText(message.context, queuedNotice(position)).catch((error) => {
4200
+ log.warn("line: weixin queued notice failed", { error: String(error) });
4201
+ });
4202
+ }
3445
4203
  });
4204
+ if (outcome.silent === true) return;
3446
4205
  await platform.replyText(message.context, outcome.text);
3447
4206
  } catch (error) {
3448
4207
  log.error("line: weixin reply failed", {
@@ -3459,6 +4218,8 @@ async function startWeixin(cfg, driver) {
3459
4218
  log.info("line: weixin platform started", { baseUrl: options.baseUrl });
3460
4219
  return {
3461
4220
  name: "weixin",
4221
+ sendProactive: (sessionKey, text) => platform.sendProactive(sessionKey, text),
4222
+ sendMedia: (sessionKey, kind, path) => platform.sendMedia(sessionKey, kind, path),
3462
4223
  close: async () => {
3463
4224
  await platform.stop();
3464
4225
  }
@@ -3502,6 +4263,11 @@ function scheduleGroupFilterSupervisor(client) {
3502
4263
  }
3503
4264
  async function gracefulShutdown(handles, driver) {
3504
4265
  await driver.waitIdle(SHUTDOWN_GRACE_MS).catch(() => {});
4266
+ const abandoned = driver.pendingTurnCount();
4267
+ if (abandoned > 0) {
4268
+ log.warn("line: shutdown timed out with pending turns", { pending: abandoned });
4269
+ err(`警告:仍有 ${abandoned} 个回合未完成,退出后这些消息不会有回复`);
4270
+ }
3505
4271
  for (const handle of handles) try {
3506
4272
  await handle.close();
3507
4273
  } catch {}
@@ -5413,121 +6179,6 @@ var EditorKeyboardController = class {
5413
6179
  }
5414
6180
  };
5415
6181
  //#endregion
5416
- //#region src/tui/utils/child-request-notice.ts
5417
- const CHILD_REQUEST_KINDS = [
5418
- "info",
5419
- "handoff",
5420
- "escalate"
5421
- ];
5422
- /**
5423
- * Longest previewed value kept on one row. The kernel allows 8192 chars
5424
- * (`contact-parent.ts`); the ticker is one terminal line, so anything longer is
5425
- * cut here and the full text remains reachable via the agent card.
5426
- */
5427
- const TEXT_PREVIEW = 200;
5428
- /**
5429
- * Collapse the request onto previewable single-paragraph form.
5430
- *
5431
- * The text comes from a child agent's model output, i.e. untrusted input that
5432
- * is about to be written into a terminal row: control sequences (CSI/OSC/DCS —
5433
- * screen clears, cursor moves, OSC 52 clipboard writes) are stripped first, the
5434
- * same way shell output is sanitized elsewhere. The cap counts code points, not
5435
- * UTF-16 units, so a surrogate pair is never split into a replacement glyph.
5436
- */
5437
- function previewText(value) {
5438
- const flattened = sanitizeShellOutput(value).replaceAll(/\s*\n\s*/g, " ").trim();
5439
- const codePoints = Array.from(flattened);
5440
- return codePoints.length > TEXT_PREVIEW ? `${codePoints.slice(0, TEXT_PREVIEW).join("")}…` : flattened;
5441
- }
5442
- function kindOf(raw) {
5443
- return typeof raw === "string" && CHILD_REQUEST_KINDS.includes(raw) ? raw : "info";
5444
- }
5445
- function textOf(raw) {
5446
- if (typeof raw !== "string") return void 0;
5447
- const trimmed = raw.trim();
5448
- return trimmed.length > 0 ? trimmed : void 0;
5449
- }
5450
- /**
5451
- * Live producer: the delivery event the kernel emits once a request landed in
5452
- * the parent's turn. Events cross the wire as untyped JSON, so every field is
5453
- * re-checked here rather than trusted from the declared type.
5454
- */
5455
- function childRequestFieldsFromEvent(event) {
5456
- const record = event;
5457
- const message = textOf(record["message"]);
5458
- if (message === void 0) return null;
5459
- const listOf = (key) => {
5460
- const raw = record[key];
5461
- if (!Array.isArray(raw)) return void 0;
5462
- const items = raw.map((item) => textOf(item)).filter((item) => item !== void 0);
5463
- return items.length > 0 ? items : void 0;
5464
- };
5465
- return {
5466
- requestType: kindOf(record["requestType"]),
5467
- message,
5468
- needs: textOf(record["needs"]),
5469
- expecting: textOf(record["expecting"]),
5470
- artifacts: listOf("artifacts"),
5471
- evidence: listOf("evidence"),
5472
- missing: listOf("missing")
5473
- };
5474
- }
5475
- //#endregion
5476
- //#region src/tui/utils/cron-chip.ts
5477
- /**
5478
- * Scheduled-task reminder → one-line status chip.
5479
- *
5480
- * A fired cron task already owns a transcript block (`CronMessageComponent`) and
5481
- * a turn. The status bar adds the *live* signal: the reminder is time-sensitive,
5482
- * so it must be visible above the editor for a few seconds even when the user is
5483
- * looking at the input box, not the transcript.
5484
- *
5485
- * Copy follows the shared chip grammar `<symbol> <label>:<value>` — the symbol
5486
- * lives in the copy, the colour comes from the caller (`accent` for a normal
5487
- * fire, `warning` for a stale/overdue one).
5488
- */
5489
- /** Longest task prompt shown on the row. The full text stays in the transcript
5490
- * block, so the chip only needs enough words to recognise the task. */
5491
- const CRON_PREVIEW_CHARS = 40;
5492
- function shorten(value, max) {
5493
- const codePoints = Array.from(value);
5494
- return codePoints.length > max ? `${codePoints.slice(0, max - 1).join("")}…` : value;
5495
- }
5496
- /**
5497
- * Build the chip for one `cron.fired` delivery.
5498
- *
5499
- * `previewText` runs first: the prompt is model/tool-authored text about to be
5500
- * written into a terminal row, so control sequences are stripped and newlines
5501
- * collapse before the length cap is applied.
5502
- *
5503
- * Precedence, most notable first:
5504
- * stale (7-day cutoff) → coalesced (missed theoretical fires) → one-shot →
5505
- * plain recurring fire.
5506
- */
5507
- function buildCronReminderChip(event) {
5508
- const origin = event.origin;
5509
- const preview = shorten(previewText(event.prompt), CRON_PREVIEW_CHARS) || origin.cron;
5510
- if (origin.stale) return {
5511
- text: t("status.cron_stale", { preview }),
5512
- tone: "missed"
5513
- };
5514
- if (origin.coalescedCount > 1) return {
5515
- text: t("status.cron_fired_coalesced", {
5516
- count: String(origin.coalescedCount),
5517
- preview
5518
- }),
5519
- tone: "fired"
5520
- };
5521
- if (!origin.recurring) return {
5522
- text: t("status.cron_once", { preview }),
5523
- tone: "fired"
5524
- };
5525
- return {
5526
- text: t("status.cron_fired", { preview }),
5527
- tone: "fired"
5528
- };
5529
- }
5530
- //#endregion
5531
6182
  //#region src/tui/utils/background-agent-status.ts
5532
6183
  const MAX_BACKGROUND_FIELD_LENGTH = 240;
5533
6184
  function normalizeBackgroundField(value) {
@@ -19536,7 +20187,7 @@ var ScreamTUI = class {
19536
20187
  const card = new OnboardingCardComponent({
19537
20188
  colors: this.state.theme.colors,
19538
20189
  onConfigure: () => {
19539
- import("./dispatch-CB9ZdDBD.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
20190
+ import("./dispatch-Cqw-jMk5.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
19540
20191
  this.restoreEditor();
19541
20192
  onDone?.();
19542
20193
  });