blun-king-cli 9.1.33 → 9.1.35

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.
Files changed (4) hide show
  1. package/LIESMICH.txt +1 -1
  2. package/README.md +1 -1
  3. package/blun.mjs +1014 -483
  4. package/package.json +1 -1
package/blun.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // BLUN_BUILD_INPUT_SHA256:cf0e1b17c2ea28e2e1baaa88f43c3a551cddc9c62e205a307c513cfd60c19a5a
2
+ // BLUN_BUILD_INPUT_SHA256:760e619c78a4f7144ea03820db15402ea00e8058f86f431c12fb1bbb633de6b9
3
3
  import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';
4
4
  import { dirname as __cjsShimDirname } from 'node:path';
5
5
  const __filename = __cjsShimFileURLToPath(import.meta.url);
@@ -569,6 +569,7 @@ var init_codes = __esmMin((() => {
569
569
  GOAL_NOT_RESUMABLE: "goal.not_resumable",
570
570
  MODEL_NOT_CONFIGURED: "model.not_configured",
571
571
  MODEL_CONFIG_INVALID: "model.config_invalid",
572
+ MODEL_EMPTY_RESPONSE: "model.empty_response",
572
573
  AUTH_LOGIN_REQUIRED: "auth.login_required",
573
574
  CONTEXT_OVERFLOW: "context.overflow",
574
575
  LOOP_ALREADY_EXISTS: "loop.already_exists",
@@ -613,6 +614,12 @@ var init_codes = __esmMin((() => {
613
614
  public: true,
614
615
  action: "Check config.toml and provider/model settings."
615
616
  },
617
+ "model.empty_response": {
618
+ title: "Model returned no content",
619
+ retryable: true,
620
+ public: true,
621
+ action: "Send the message again."
622
+ },
616
623
  "session.not_found": {
617
624
  title: "Session not found",
618
625
  retryable: false,
@@ -1663,14 +1670,35 @@ var init_errors$10 = __esmMin((() => {
1663
1670
  this.retryAfterMs = typeof metadata.retryAfterMs === "number" && Number.isFinite(metadata.retryAfterMs) && metadata.retryAfterMs >= 0 ? metadata.retryAfterMs : null;
1664
1671
  }
1665
1672
  };
1666
- APIEmptyResponseError = class extends ChatProviderError {
1673
+ APIEmptyResponseError = class APIEmptyResponseError extends ChatProviderError {
1667
1674
  finishReason;
1668
1675
  rawFinishReason;
1676
+ emptyResponseKind;
1677
+ completionTokens;
1678
+ reasoningLength;
1679
+ maxCompletionTokens;
1680
+ attempts;
1669
1681
  constructor(message, options = {}) {
1670
1682
  super(message);
1671
1683
  this.name = "APIEmptyResponseError";
1672
1684
  this.finishReason = options.finishReason ?? null;
1673
1685
  this.rawFinishReason = options.rawFinishReason ?? null;
1686
+ this.emptyResponseKind = options.emptyResponseKind ?? "other";
1687
+ this.completionTokens = options.completionTokens ?? null;
1688
+ this.reasoningLength = options.reasoningLength ?? 0;
1689
+ this.maxCompletionTokens = options.maxCompletionTokens ?? null;
1690
+ this.attempts = options.attempts ?? 1;
1691
+ }
1692
+ withMetadata(options) {
1693
+ return new APIEmptyResponseError(this.message, {
1694
+ finishReason: this.finishReason,
1695
+ rawFinishReason: this.rawFinishReason,
1696
+ emptyResponseKind: this.emptyResponseKind,
1697
+ completionTokens: this.completionTokens,
1698
+ reasoningLength: this.reasoningLength,
1699
+ maxCompletionTokens: options.maxCompletionTokens ?? this.maxCompletionTokens,
1700
+ attempts: options.attempts ?? this.attempts
1701
+ });
1674
1702
  }
1675
1703
  };
1676
1704
  CompactionStallError$1 = class extends ChatProviderError {
@@ -2996,14 +3024,19 @@ async function generate(provider, systemPrompt, tools, history, callbacks, optio
2996
3024
  if (pendingPart !== null) flushPart(message, pendingPart, toolCallIndexMap);
2997
3025
  if (message.content.length === 0 && message.toolCalls.length === 0) throw new APIEmptyResponseError("The API returned an empty response (no content, no tool calls)." + formatFinishReasonHint(stream) + ` Provider: ${provider.name}, model: ${provider.modelName}`, {
2998
3026
  finishReason: stream.finishReason,
2999
- rawFinishReason: stream.rawFinishReason
3027
+ rawFinishReason: stream.rawFinishReason,
3028
+ emptyResponseKind: classifyEmptyResponse(stream),
3029
+ completionTokens: stream.usage?.output ?? null
3000
3030
  });
3001
3031
  const hasThink = message.content.some((p) => p.type === "think");
3002
3032
  const hasText = message.content.some((p) => p.type === "text" && p.text.trim().length > 0);
3003
3033
  const hasToolCalls = message.toolCalls.length > 0;
3004
3034
  if (hasThink && !hasText && !hasToolCalls) throw new APIEmptyResponseError("The API returned a response containing only thinking content without any text or tool calls. This usually indicates the stream was interrupted or the output token budget was exhausted during reasoning." + formatFinishReasonHint(stream) + ` Provider: ${provider.name}, model: ${provider.modelName}`, {
3005
3035
  finishReason: stream.finishReason,
3006
- rawFinishReason: stream.rawFinishReason
3036
+ rawFinishReason: stream.rawFinishReason,
3037
+ emptyResponseKind: classifyEmptyResponse(stream),
3038
+ completionTokens: stream.usage?.output ?? null,
3039
+ reasoningLength: message.content.reduce((total, part) => total + (part.type === "think" ? part.think.length : 0), 0)
3007
3040
  });
3008
3041
  if (callbacks?.onToolCall !== void 0) for (const toolCall of message.toolCalls) {
3009
3042
  await throwIfAborted$2(signal, stream);
@@ -3022,6 +3055,14 @@ async function generate(provider, systemPrompt, tools, history, callbacks, optio
3022
3055
  signal?.removeEventListener("abort", abortListener);
3023
3056
  }
3024
3057
  }
3058
+ function classifyEmptyResponse(stream) {
3059
+ if (stream.finishReason === "truncated") return "length";
3060
+ if (stream.finishReason === "completed") return "stop";
3061
+ const raw = stream.rawFinishReason?.toLowerCase() ?? "";
3062
+ if (raw === "length" || raw.includes("max_token") || raw.includes("max_output")) return "length";
3063
+ if (raw === "stop" || raw === "completed") return "stop";
3064
+ return "other";
3065
+ }
3025
3066
  function throwAbortError() {
3026
3067
  throw new DOMException("The operation was aborted.", "AbortError");
3027
3068
  }
@@ -3255,16 +3296,24 @@ function toBlunErrorPayload(error) {
3255
3296
  name: error.name,
3256
3297
  retryable: BLUN_ERROR_INFO[ErrorCodes.PROVIDER_CONNECTION_ERROR].retryable
3257
3298
  };
3258
- if (error instanceof APIEmptyResponseError) return {
3259
- code: ErrorCodes.PROVIDER_API_ERROR,
3260
- message: error.message,
3261
- name: error.name,
3262
- details: {
3263
- finishReason: error.finishReason,
3264
- rawFinishReason: error.rawFinishReason
3265
- },
3266
- retryable: BLUN_ERROR_INFO[ErrorCodes.PROVIDER_API_ERROR].retryable
3267
- };
3299
+ if (error instanceof APIEmptyResponseError) {
3300
+ const isRepeatedStop = error.emptyResponseKind === "stop" && error.attempts >= 2;
3301
+ const code = isRepeatedStop ? ErrorCodes.MODEL_EMPTY_RESPONSE : ErrorCodes.PROVIDER_API_ERROR;
3302
+ return {
3303
+ code,
3304
+ message: isRepeatedStop ? "King ended twice without producing content. Please send the message again." : error.message,
3305
+ name: error.name,
3306
+ details: {
3307
+ finishReason: error.finishReason,
3308
+ rawFinishReason: error.rawFinishReason,
3309
+ emptyResponseKind: error.emptyResponseKind,
3310
+ attempts: error.attempts,
3311
+ completionTokens: error.completionTokens,
3312
+ maxCompletionTokens: error.maxCompletionTokens
3313
+ },
3314
+ retryable: BLUN_ERROR_INFO[code].retryable
3315
+ };
3316
+ }
3268
3317
  if (error instanceof ChatProviderError) return {
3269
3318
  code: ErrorCodes.PROVIDER_API_ERROR,
3270
3319
  message: error.message,
@@ -9923,6 +9972,7 @@ var init_schema = __esmMin((() => {
9923
9972
  ]);
9924
9973
  ActionStyleSchema = _enum([
9925
9974
  "default",
9975
+ "concise",
9926
9976
  "proactive",
9927
9977
  "explanatory",
9928
9978
  "learning"
@@ -30052,9 +30102,38 @@ async function chatWithRetry(input) {
30052
30102
  }
30053
30103
  }
30054
30104
  const delays = retryBackoffDelays(maxAttempts);
30105
+ let completionBudgetRetry;
30106
+ let emptyRetryStarted = false;
30055
30107
  for (let attempt = 1;; attempt += 1) try {
30056
- return await input.llm.chat(paramsForAttempt(input, attempt, maxAttempts));
30108
+ return await input.llm.chat(paramsForAttempt(input, attempt, maxAttempts, completionBudgetRetry));
30057
30109
  } catch (error) {
30110
+ if (error instanceof APIEmptyResponseError && (emptyRetryStarted || error.emptyResponseKind === "length" || error.emptyResponseKind === "stop")) {
30111
+ const emptyAttemptLimit = Math.min(maxAttempts, 2);
30112
+ logEmptyResponse(input, error, attempt);
30113
+ if (attempt >= emptyAttemptLimit) {
30114
+ const terminal = error.withMetadata({ attempts: attempt });
30115
+ logRequestFailure(input, terminal, attempt, emptyAttemptLimit);
30116
+ throw terminal;
30117
+ }
30118
+ completionBudgetRetry = error.emptyResponseKind === "length" ? {
30119
+ minimumCompletionTokens: 1024,
30120
+ multiplier: 2
30121
+ } : void 0;
30122
+ emptyRetryStarted = true;
30123
+ input.params.signal.throwIfAborted();
30124
+ input.dispatchEvent({
30125
+ type: "step.retrying",
30126
+ turnId: input.turnId,
30127
+ step: input.currentStep,
30128
+ stepUuid: input.stepUuid,
30129
+ failedAttempt: attempt,
30130
+ nextAttempt: attempt + 1,
30131
+ maxAttempts: emptyAttemptLimit,
30132
+ delayMs: 0,
30133
+ ...retryErrorFields(error)
30134
+ });
30135
+ continue;
30136
+ }
30058
30137
  if (attempt >= maxAttempts || !input.llm.isRetryableError(error)) {
30059
30138
  logRequestFailure(input, error, attempt, maxAttempts);
30060
30139
  throw error;
@@ -30084,16 +30163,26 @@ function logRequestFailure(input, error, attempt, maxAttempts) {
30084
30163
  ...retryErrorFields(error)
30085
30164
  });
30086
30165
  }
30087
- function paramsForAttempt(input, attempt, maxAttempts) {
30166
+ function paramsForAttempt(input, attempt, maxAttempts, completionBudgetRetry) {
30088
30167
  const turnStep = `${input.turnId}.${String(input.currentStep)}`;
30089
30168
  return {
30090
30169
  ...input.params,
30170
+ completionBudgetRetry,
30091
30171
  requestLogFields: attempt === 1 ? { turnStep } : {
30092
30172
  turnStep,
30093
30173
  attempt: `${String(attempt)}/${String(maxAttempts)}`
30094
30174
  }
30095
30175
  };
30096
30176
  }
30177
+ function logEmptyResponse(input, error, attempt) {
30178
+ input.log?.warn(`[leer] fall=${error.emptyResponseKind}`, {
30179
+ turnStep: `${input.turnId}.${String(input.currentStep)}`,
30180
+ attempt,
30181
+ budget: error.maxCompletionTokens,
30182
+ completion: error.completionTokens,
30183
+ reasoningLength: error.reasoningLength
30184
+ });
30185
+ }
30097
30186
  function retryBackoffDelays(maxAttempts) {
30098
30187
  return import_retry$1.timeouts({
30099
30188
  retries: Math.max(maxAttempts - 1, 0),
@@ -30122,6 +30211,7 @@ function maybeStatusCode(error) {
30122
30211
  var import_retry$1, RETRY_MIN_TIMEOUT_MS, RETRY_MAX_TIMEOUT_MS, RETRY_FACTOR;
30123
30212
  var init_retry = __esmMin((() => {
30124
30213
  init_dist$4();
30214
+ init_src$4();
30125
30215
  import_retry$1 = /* @__PURE__ */ __toESM(require_retry$1(), 1);
30126
30216
  init_abort();
30127
30217
  init_errors$4();
@@ -30783,18 +30873,25 @@ function computeCompletionBudgetCap(args) {
30783
30873
  * in `BlunChatProvider._clone()`.
30784
30874
  */
30785
30875
  function applyCompletionBudget(args) {
30786
- if (args.budget === void 0) return args.provider;
30787
- if (args.provider.withMaxCompletionTokens === void 0) return args.provider;
30876
+ return applyCompletionBudgetWithDetails(args).provider;
30877
+ }
30878
+ function applyCompletionBudgetWithDetails(args) {
30879
+ if (args.budget === void 0) return { provider: args.provider };
30880
+ if (args.provider.withMaxCompletionTokens === void 0) return { provider: args.provider };
30788
30881
  let cap = computeCompletionBudgetCap({
30789
30882
  budget: args.budget,
30790
30883
  capability: args.capability
30791
30884
  });
30885
+ if (args.retry !== void 0) cap = Math.max(args.retry.minimumCompletionTokens, Math.ceil(cap * args.retry.multiplier));
30792
30886
  const maxContextTokens = args.capability?.max_context_tokens;
30793
30887
  if (args.usedContextTokens !== void 0 && maxContextTokens !== void 0 && maxContextTokens > 0) cap = Math.max(MIN_FLOOR, Math.min(cap, maxContextTokens - args.usedContextTokens));
30794
- return args.provider.withMaxCompletionTokens(cap, {
30795
- usedContextTokens: args.usedContextTokens,
30796
- maxContextTokens
30797
- });
30888
+ return {
30889
+ provider: args.provider.withMaxCompletionTokens(cap, {
30890
+ usedContextTokens: args.usedContextTokens,
30891
+ maxContextTokens
30892
+ }),
30893
+ maxCompletionTokens: cap
30894
+ };
30798
30895
  }
30799
30896
  var MIN_FLOOR, DEFAULT_UNKNOWN_CONTEXT_FALLBACK;
30800
30897
  var init_completion_budget = __esmMin((() => {
@@ -74499,6 +74596,7 @@ var init_kosong_llm = __esmMin((() => {
74499
74596
  requestLogFields: params.requestLogFields
74500
74597
  };
74501
74598
  let result;
74599
+ let completionBudget;
74502
74600
  try {
74503
74601
  const enrichedMessages = this.visionReader === void 0 ? params.messages : await enrichMessagesWithVision(params.messages, this.visionReader, params.signal, this.onVisionUsage);
74504
74602
  const tools = [...params.tools];
@@ -74508,14 +74606,16 @@ var init_kosong_llm = __esmMin((() => {
74508
74606
  const outgoingRequestTokens = estimateTokens$1(this.systemPrompt) + estimateTokensForTools(tools) + estimateTokensForMessages(outgoingMessages);
74509
74607
  const reportedContextTokens = this.reportedContextTokens?.() ?? 0;
74510
74608
  const usedContextTokens = Math.max(outgoingRequestTokens, reportedContextTokens);
74511
- const effectiveProvider = applyCompletionBudget({
74609
+ completionBudget = applyCompletionBudgetWithDetails({
74512
74610
  provider: this.provider,
74513
74611
  budget: this.completionBudgetConfig,
74514
74612
  capability: this.capability,
74515
- usedContextTokens
74613
+ usedContextTokens,
74614
+ retry: params.completionBudgetRetry
74516
74615
  });
74517
- result = await this.generate(effectiveProvider, this.systemPrompt, tools, outgoingMessages, callbacks, options);
74616
+ result = await this.generate(completionBudget.provider, this.systemPrompt, tools, outgoingMessages, callbacks, options);
74518
74617
  } catch (error) {
74618
+ if (error instanceof APIEmptyResponseError) throw error.withMetadata({ maxCompletionTokens: completionBudget === void 0 ? null : completionBudget.maxCompletionTokens ?? null });
74519
74619
  if (error instanceof APIPaymentRequiredError) {
74520
74620
  const accountQuotaExhausted = error.statusCode === 429 && error.apiCode === "quota_exhausted";
74521
74621
  throw new BlunError(ErrorCodes.PROVIDER_QUOTA_EXHAUSTED, accountQuotaExhausted ? "Das Kontingent ist aufgebraucht." : "Das Modell-Guthaben ist erschöpft. Bitte lade dein Konto beim Anbieter nach und versuche es erneut.", {
@@ -74630,8 +74730,8 @@ var init_compaction_instruction = __esmMin((() => {
74630
74730
  var DEFAULT_COMPACTION_CONFIG, DefaultCompactionStrategy;
74631
74731
  var init_strategy = __esmMin((() => {
74632
74732
  DEFAULT_COMPACTION_CONFIG = {
74633
- triggerRatio: .65,
74634
- blockRatio: .65,
74733
+ triggerRatio: .9,
74734
+ blockRatio: .95,
74635
74735
  reservedContextSize: 1e4,
74636
74736
  maxCompactionPerTurn: Infinity,
74637
74737
  maxOverflowCompactionAttempts: 3
@@ -74919,6 +75019,42 @@ function isModelFallbackStatus(error) {
74919
75019
  function compactionTimingKey(provider) {
74920
75020
  return `${provider.name}\u0000${provider.modelName}`;
74921
75021
  }
75022
+ function estimateCompactionStageCount(initialInputTokens, safeRequestLimitTokens, currentStage) {
75023
+ if (safeRequestLimitTokens <= 0) return currentStage;
75024
+ return Math.max(currentStage, Math.ceil(initialInputTokens / safeRequestLimitTokens));
75025
+ }
75026
+ function estimateCompactionProgressPercent(stage, estimatedStageCount) {
75027
+ if (estimatedStageCount <= 0) return 0;
75028
+ return Math.min(99, Math.floor(stage / estimatedStageCount * 100));
75029
+ }
75030
+ function estimateCompactionWindowUsagePercent(requestTokens, maxContextTokens) {
75031
+ if (maxContextTokens <= 0) return void 0;
75032
+ return Math.min(100, Math.max(0, Math.ceil(requestTokens / maxContextTokens * 100)));
75033
+ }
75034
+ function selectHierarchicalCompactionChunk(history, targetRequestTokens, hardRequestLimit, build) {
75035
+ const safeEnds = [];
75036
+ for (let end = 1; end < history.length; end++) if (history[end]?.role !== "tool") safeEnds.push(end);
75037
+ if (safeEnds.length === 0) return void 0;
75038
+ const pickLargestWithin = (limit) => {
75039
+ let low = 0;
75040
+ let high = safeEnds.length - 1;
75041
+ let best;
75042
+ while (low <= high) {
75043
+ const middle = Math.floor((low + high) / 2);
75044
+ const end = safeEnds[middle];
75045
+ const request = build(history.slice(0, end));
75046
+ if (request.estimatedTokens < limit) {
75047
+ best = {
75048
+ end,
75049
+ ...request
75050
+ };
75051
+ low = middle + 1;
75052
+ } else high = middle - 1;
75053
+ }
75054
+ return best;
75055
+ };
75056
+ return pickLargestWithin(targetRequestTokens) ?? pickLargestWithin(hardRequestLimit);
75057
+ }
74922
75058
  function shrinkCompactionHistoryAfterOverflow(messages, attempt) {
74923
75059
  if (messages.length <= 1) return messages.slice();
74924
75060
  const ratio = COMPACTION_OVERFLOW_SHRINK_RATIOS[Math.min(attempt - 1, COMPACTION_OVERFLOW_SHRINK_RATIOS.length - 1)];
@@ -75002,7 +75138,7 @@ function extractCompactionSummary(response) {
75002
75138
  if (summary.trim().length === 0) throw new APIEmptyResponseError("The compaction response did not contain a non-empty summary.");
75003
75139
  return summary;
75004
75140
  }
75005
- var DEFAULT_COMPACTION_MAX_COMPLETION_TOKENS, CompactionTruncatedError, CompactionStallError, COMPACTION_STALL_MEASUREMENT_MULTIPLIER, REBUILT_AFTER_COMPACTION_INJECTION_VARIANTS, FullCompaction, MAX_COMPACTION_OVERFLOW_SHRINK_ATTEMPTS, COMPACTION_OVERFLOW_SHRINK_RATIOS;
75141
+ var DEFAULT_COMPACTION_MAX_COMPLETION_TOKENS, COMPACTION_SUMMARY_RESERVE_RATIO, MAX_HIERARCHICAL_COMPACTION_PASSES, HIERARCHICAL_COMPACTION_PREFIX, CompactionTruncatedError, CompactionStallError, COMPACTION_STALL_MEASUREMENT_MULTIPLIER, REBUILT_AFTER_COMPACTION_INJECTION_VARIANTS, FullCompaction, MAX_COMPACTION_OVERFLOW_SHRINK_ATTEMPTS, COMPACTION_OVERFLOW_SHRINK_RATIOS;
75006
75142
  var init_full = __esmMin((() => {
75007
75143
  init_errors$8();
75008
75144
  init_src$4();
@@ -75018,6 +75154,9 @@ var init_full = __esmMin((() => {
75018
75154
  init_strategy();
75019
75155
  init_handoff();
75020
75156
  DEFAULT_COMPACTION_MAX_COMPLETION_TOKENS = 128 * 1024;
75157
+ COMPACTION_SUMMARY_RESERVE_RATIO = .1;
75158
+ MAX_HIERARCHICAL_COMPACTION_PASSES = 64;
75159
+ HIERARCHICAL_COMPACTION_PREFIX = "This is a complete summary of an earlier chronological segment. Preserve it as source material when merging it with the following conversation:";
75021
75160
  CompactionTruncatedError = class extends Error {
75022
75161
  constructor() {
75023
75162
  super("Compaction response was truncated before producing a complete summary.");
@@ -75067,7 +75206,7 @@ var init_full = __esmMin((() => {
75067
75206
  ...DEFAULT_COMPACTION_CONFIG,
75068
75207
  reservedContextSize,
75069
75208
  triggerRatio,
75070
- blockRatio: triggerRatio
75209
+ blockRatio: Math.max(triggerRatio, DEFAULT_COMPACTION_CONFIG.blockRatio)
75071
75210
  });
75072
75211
  }
75073
75212
  get isCompacting() {
@@ -75236,8 +75375,9 @@ var init_full = __esmMin((() => {
75236
75375
  }
75237
75376
  async compactionWorker(signal, data) {
75238
75377
  try {
75239
- const result = await this.compactionRound(signal, data);
75240
- if (!result) return;
75378
+ const output = await this.compactionRound(signal, data);
75379
+ if (!output) return;
75380
+ const { result, stageCount } = output;
75241
75381
  try {
75242
75382
  await this.agent.refreshSystemPrompt();
75243
75383
  } catch (error) {
@@ -75255,7 +75395,8 @@ var init_full = __esmMin((() => {
75255
75395
  this.agent.emitEvent({
75256
75396
  type: "compaction.completed",
75257
75397
  result: eventResult,
75258
- projectedContextTokens
75398
+ projectedContextTokens,
75399
+ ...stageCount > 1 ? { stageCount } : {}
75259
75400
  });
75260
75401
  this.triggerPostCompactHook(data, result);
75261
75402
  } catch (error) {
@@ -75320,35 +75461,79 @@ var init_full = __esmMin((() => {
75320
75461
  let droppedCount = 0;
75321
75462
  let overflowShrinkCount = 0;
75322
75463
  let emptyOrTruncatedShrinkCount = 0;
75464
+ let hierarchicalPassCount = 0;
75323
75465
  const typicalDurationMs = readTypicalCompactionDuration(provider.name, provider.modelName, this.agent.blunHomeDir)?.typicalDurationMs;
75324
75466
  let attemptCount = 0;
75325
75467
  let charsReceived = 0;
75326
75468
  let lastProgressEmitAt = 0;
75469
+ let initialCompactionRequestTokens;
75327
75470
  const compactionTools = [];
75328
- while (true) {
75329
- const compactionRequestLimit = this.getEffectiveMaxContextTokens();
75330
- const messages = stripMediaForCompaction(downgradeUnsupportedMedia([...this.agent.context.project(historyForModel, {
75471
+ const buildRequestMessages = (history, requestInstruction) => {
75472
+ return stripMediaForCompaction(downgradeUnsupportedMedia([...this.agent.context.project(history, {
75331
75473
  synthesizeMissing: true,
75332
75474
  dropOrphanResults: true
75333
- }), createUserMessage(instruction)], capability));
75334
- const estimatedCompactionRequestTokens = this.estimateRequestTokens(messages, this.agent.effectiveSystemPrompt, compactionTools);
75335
- if (compactionRequestLimit > 0 && estimatedCompactionRequestTokens >= compactionRequestLimit) throw new BlunError(ErrorCodes.CONTEXT_OVERFLOW, `Compaction request requires about ${String(estimatedCompactionRequestTokens)} tokens but the active model window is ${String(compactionRequestLimit)}.`, { details: {
75336
- contextBrakeBlocked: true,
75337
- estimatedRequestTokens: estimatedCompactionRequestTokens,
75338
- maxContextTokens: compactionRequestLimit,
75339
- requestBudgetTokens: compactionRequestLimit,
75340
- contextUnchanged: true
75341
- } });
75475
+ }), createUserMessage(requestInstruction)], capability));
75476
+ };
75477
+ while (true) {
75478
+ const compactionRequestLimit = this.getEffectiveMaxContextTokens();
75479
+ const safeCompactionRequestLimit = compactionRequestLimit > 0 ? Math.max(1, Math.floor(compactionRequestLimit * (1 - COMPACTION_SUMMARY_RESERVE_RATIO))) : compactionRequestLimit;
75480
+ let messages = buildRequestMessages(historyForModel, instruction);
75481
+ let estimatedCompactionRequestTokens = this.estimateRequestTokens(messages, this.agent.effectiveSystemPrompt, compactionTools);
75482
+ initialCompactionRequestTokens ??= estimatedCompactionRequestTokens;
75483
+ let hierarchicalChunkEnd;
75484
+ if (safeCompactionRequestLimit > 0 && estimatedCompactionRequestTokens >= safeCompactionRequestLimit) {
75485
+ if (historyForModel.length > 1) {
75486
+ const chunkInstruction = `${instruction}\n\nThis request contains only the oldest chronological segment. Summarize every fact needed by a later merge pass. Do not assume later messages are visible.`;
75487
+ const chunk = selectHierarchicalCompactionChunk(historyForModel, safeCompactionRequestLimit, safeCompactionRequestLimit, (candidate) => {
75488
+ const candidateMessages = buildRequestMessages(candidate, chunkInstruction);
75489
+ return {
75490
+ messages: candidateMessages,
75491
+ estimatedTokens: this.estimateRequestTokens(candidateMessages, this.agent.effectiveSystemPrompt, compactionTools)
75492
+ };
75493
+ });
75494
+ if (chunk !== void 0) {
75495
+ hierarchicalChunkEnd = chunk.end;
75496
+ messages = chunk.messages;
75497
+ estimatedCompactionRequestTokens = chunk.estimatedTokens;
75498
+ }
75499
+ }
75500
+ if (hierarchicalChunkEnd === void 0) throw new BlunError(ErrorCodes.CONTEXT_OVERFLOW, `Compaction stopped before upload: the conversation (${String(tokensBefore)} tokens) could not be divided into a request below the active model window (${String(compactionRequestLimit)} tokens).`, { details: {
75501
+ contextBrakeBlocked: true,
75502
+ estimatedRequestTokens: estimatedCompactionRequestTokens,
75503
+ maxContextTokens: compactionRequestLimit,
75504
+ requestBudgetTokens: safeCompactionRequestLimit,
75505
+ summaryReserveTokens: compactionRequestLimit - safeCompactionRequestLimit,
75506
+ contextUnchanged: true
75507
+ } });
75508
+ }
75342
75509
  provider = buildCompactionProvider(estimatedCompactionRequestTokens);
75343
75510
  attemptCount += 1;
75344
75511
  charsReceived = 0;
75512
+ const stage = hierarchicalPassCount + 1;
75513
+ const estimatedStageCount = estimateCompactionStageCount(initialCompactionRequestTokens, safeCompactionRequestLimit, stage);
75514
+ const estimatedProgressPercent = estimateCompactionProgressPercent(stage, estimatedStageCount);
75515
+ const windowUsagePercent = estimateCompactionWindowUsagePercent(estimatedCompactionRequestTokens, compactionRequestLimit);
75516
+ this.agent.log.info("compaction stage request", {
75517
+ source: data.source,
75518
+ stage,
75519
+ estimatedStageCount,
75520
+ activeContextTokens: this.estimateProjectedRequestTokens(),
75521
+ estimatedInputTokens: estimatedCompactionRequestTokens,
75522
+ windowUsagePercent,
75523
+ safeInputLimitTokens: safeCompactionRequestLimit,
75524
+ maxContextTokens: compactionRequestLimit
75525
+ });
75345
75526
  const emitCompactionProgress = (force = false) => {
75346
75527
  const now = Date.now();
75347
75528
  if (!force && now - lastProgressEmitAt < 150) return;
75348
75529
  lastProgressEmitAt = now;
75349
75530
  this.agent.emitEvent({
75350
75531
  type: "compaction.progress",
75532
+ stage,
75533
+ estimatedStageCount,
75351
75534
  estimatedInputTokens: estimatedCompactionRequestTokens,
75535
+ estimatedProgressPercent,
75536
+ ...windowUsagePercent === void 0 ? {} : { windowUsagePercent },
75352
75537
  charsReceived,
75353
75538
  attempt: attemptCount,
75354
75539
  ...typicalDurationMs !== void 0 ? { typicalDurationMs } : {}
@@ -75389,9 +75574,29 @@ var init_full = __esmMin((() => {
75389
75574
  if (stalled && !signal.aborted && stallPolicy !== void 0) throw new CompactionStallError(stallPolicy.timeoutMs, stallPolicy.measuredIdleMs);
75390
75575
  maxObservedIdleMs = Math.max(maxObservedIdleMs, Date.now() - lastProgressAt);
75391
75576
  if (response.finishReason === "truncated") throw new CompactionTruncatedError();
75392
- usage = response.usage;
75393
- summary = extractCompactionSummary(response);
75577
+ if (response.usage !== null) usage = usage === null ? response.usage : addUsage$1(usage, response.usage);
75578
+ const extractedSummary = extractCompactionSummary(response);
75394
75579
  this.observeCompactionTiming(timingKey, maxObservedIdleMs);
75580
+ if (hierarchicalChunkEnd !== void 0) {
75581
+ hierarchicalPassCount += 1;
75582
+ if (hierarchicalPassCount > MAX_HIERARCHICAL_COMPACTION_PASSES) throw new BlunError(ErrorCodes.COMPACTION_FAILED, `Compaction stopped after ${String(MAX_HIERARCHICAL_COMPACTION_PASSES)} staged requests without reaching a final summary. The conversation history was not changed.`, { details: {
75583
+ contextUnchanged: true,
75584
+ hierarchicalPassCount
75585
+ } });
75586
+ const previousTokens = estimateTokensForMessages(historyForModel);
75587
+ const nextHistory = [createUserMessage(`${HIERARCHICAL_COMPACTION_PREFIX}\n${extractedSummary}`), ...historyForModel.slice(hierarchicalChunkEnd)];
75588
+ const nextTokens = estimateTokensForMessages(nextHistory);
75589
+ if (nextTokens >= previousTokens) throw new BlunError(ErrorCodes.COMPACTION_FAILED, "A staged compaction response did not reduce the pending history. The conversation history was not changed.", { details: {
75590
+ contextUnchanged: true,
75591
+ hierarchicalPassCount,
75592
+ previousTokens,
75593
+ nextTokens
75594
+ } });
75595
+ historyForModel = nextHistory;
75596
+ retryCount = 0;
75597
+ continue;
75598
+ }
75599
+ summary = extractedSummary;
75395
75600
  appendCompactionTiming({
75396
75601
  ts: Date.now(),
75397
75602
  provider: provider.name,
@@ -75408,6 +75613,10 @@ var init_full = __esmMin((() => {
75408
75613
  if (isContextOverflow && historyForModel.length > 1) {
75409
75614
  if (data.source === "auto") {
75410
75615
  const learnedMaxContextTokens = this.getEffectiveMaxContextTokens();
75616
+ if (learnedMaxContextTokens > 0 && learnedMaxContextTokens < compactionRequestLimit) {
75617
+ retryCount = 0;
75618
+ continue;
75619
+ }
75411
75620
  throw new BlunError(ErrorCodes.CONTEXT_OVERFLOW, "The active model rejected the full compaction request; refusing to drop unsummarized history.", {
75412
75621
  cause: error,
75413
75622
  details: {
@@ -75486,7 +75695,10 @@ var init_full = __esmMin((() => {
75486
75695
  output_tokens: usage.output
75487
75696
  }
75488
75697
  });
75489
- return result;
75698
+ return {
75699
+ result,
75700
+ stageCount: hierarchicalPassCount + 1
75701
+ };
75490
75702
  } catch (error) {
75491
75703
  if (isAbortError$4(error) || signal.aborted) return void 0;
75492
75704
  this.agent.telemetry.track("compaction_failed", {
@@ -75520,6 +75732,15 @@ var init_full = __esmMin((() => {
75520
75732
  contextUnchanged: true
75521
75733
  }
75522
75734
  });
75735
+ if (error instanceof APIStatusError && error.statusCode === 408) throw new BlunError(ErrorCodes.COMPACTION_FAILED, `Compaction stopped: the conversation (${String(tokensBefore)} tokens) could not be sent completely. The conversation history was not changed.`, {
75736
+ cause: error,
75737
+ details: {
75738
+ statusCode: error.statusCode,
75739
+ requestId: error.requestId,
75740
+ estimatedHistoryTokens: tokensBefore,
75741
+ contextUnchanged: true
75742
+ }
75743
+ });
75523
75744
  throw new BlunError(ErrorCodes.COMPACTION_FAILED, String(error), { cause: error });
75524
75745
  }
75525
75746
  }
@@ -230311,6 +230532,7 @@ var init_action_style = __esmMin((() => {
230311
230532
  init_injector();
230312
230533
  STYLE_GUIDANCE = {
230313
230534
  default: "Complete coding tasks efficiently. Keep responses concise while still reporting concrete results, blockers, and decisions.",
230535
+ concise: "Answer briefly and directly. Include only the information needed to act, while still reporting concrete results, blockers, and decisions.",
230314
230536
  proactive: "Act immediately when the path is safe and clear. Minimize interruptions, continue through ordinary implementation decisions, and ask only when missing information or a higher-priority rule makes it necessary.",
230315
230537
  explanatory: "Explain implementation choices and relevant codebase patterns while you work. Keep the explanation tied to the concrete task and avoid delaying safe progress.",
230316
230538
  learning: "Create hands-on learning moments by inviting the user to write small, useful pieces of code. Do this only when the higher-priority rules allow a pause and it will not obstruct the requested result."
@@ -244836,6 +245058,7 @@ var init_events$1 = __esmMin((() => {
244836
245058
  "goal.not_resumable",
244837
245059
  "model.not_configured",
244838
245060
  "model.config_invalid",
245061
+ "model.empty_response",
244839
245062
  "auth.login_required",
244840
245063
  "context.overflow",
244841
245064
  "loop.already_exists",
@@ -245186,6 +245409,10 @@ var init_events$1 = __esmMin((() => {
245186
245409
  compactionCancelledEventSchema = object({ type: literal("compaction.cancelled") });
245187
245410
  compactionProgressEventSchema = object({
245188
245411
  type: literal("compaction.progress"),
245412
+ stage: number$1().int().positive().optional(),
245413
+ estimatedStageCount: number$1().int().positive().optional(),
245414
+ estimatedProgressPercent: number$1().int().min(0).max(99).optional(),
245415
+ windowUsagePercent: number$1().int().min(0).max(100).optional(),
245189
245416
  estimatedInputTokens: number$1().optional(),
245190
245417
  charsReceived: number$1(),
245191
245418
  attempt: number$1(),
@@ -245194,7 +245421,8 @@ var init_events$1 = __esmMin((() => {
245194
245421
  compactionCompletedEventSchema = object({
245195
245422
  type: literal("compaction.completed"),
245196
245423
  result: compactionResultSchema,
245197
- projectedContextTokens: number$1().optional()
245424
+ projectedContextTokens: number$1().optional(),
245425
+ stageCount: number$1().int().positive().optional()
245198
245426
  });
245199
245427
  backgroundTaskStartedEventSchema = object({
245200
245428
  type: literal("background.task.started"),
@@ -261611,7 +261839,7 @@ var init_blun_media$1 = __esmMin((() => {
261611
261839
  return {
261612
261840
  output: [{
261613
261841
  type: "text",
261614
- text: `Media job ${result.id} is complete. Local file: ${localPath}. Attach this absolute path with the channel reply tool; do not search for another copy.`
261842
+ text: `Media job ${result.id} is complete. Local file: ${localPath}. The BLUN host automatically delivers this file for channel-origin turns; do not attach it again. Outside a channel-origin turn, use this local path as needed.`
261615
261843
  }, mediaPart],
261616
261844
  isError: false
261617
261845
  };
@@ -410954,12 +411182,30 @@ const STREAMING_ARGS_PREVIEW_MAX_CHARS = 64 * 1024;
410954
411182
  //#endregion
410955
411183
  //#region src/tui/utils/event-payload.copy.ts
410956
411184
  registerUiCatalogFragment({
410957
- en: { "eventPayload.providerFiltered": "Provider filtered the response before visible output (finishReason={finishReason}{raw})." },
410958
- de: { "eventPayload.providerFiltered": "Der Anbieter hat die Antwort vor der sichtbaren Ausgabe gefiltert (finishReason={finishReason}{raw})." },
410959
- es: { "eventPayload.providerFiltered": "El proveedor filtró la respuesta antes de que se mostrara la salida (finishReason={finishReason}{raw})." },
410960
- fr: { "eventPayload.providerFiltered": "Le fournisseur a filtré la réponse avant l’affichage de la sortie (finishReason={finishReason}{raw})." },
410961
- sv: { "eventPayload.providerFiltered": "Leverantören filtrerade svaret innan någon utdata visades (finishReason={finishReason}{raw})." },
410962
- cs: { "eventPayload.providerFiltered": "Poskytovatel filtroval odpověď před viditelným výstupem (finishReason={finishReason}{raw})." }
411185
+ en: {
411186
+ "eventPayload.providerFiltered": "Provider filtered the response before visible output (finishReason={finishReason}{raw}).",
411187
+ "eventPayload.modelEmptyResponse": "King ended twice without producing content. Please send the message again."
411188
+ },
411189
+ de: {
411190
+ "eventPayload.providerFiltered": "Der Anbieter hat die Antwort vor der sichtbaren Ausgabe gefiltert (finishReason={finishReason}{raw}).",
411191
+ "eventPayload.modelEmptyResponse": "King hat die Antwort zweimal ohne Inhalt beendet. Bitte sende die Nachricht noch einmal."
411192
+ },
411193
+ es: {
411194
+ "eventPayload.providerFiltered": "El proveedor filtró la respuesta antes de que se mostrara la salida (finishReason={finishReason}{raw}).",
411195
+ "eventPayload.modelEmptyResponse": "King terminó dos veces sin generar contenido. Envía el mensaje de nuevo."
411196
+ },
411197
+ fr: {
411198
+ "eventPayload.providerFiltered": "Le fournisseur a filtré la réponse avant l’affichage de la sortie (finishReason={finishReason}{raw}).",
411199
+ "eventPayload.modelEmptyResponse": "King a terminé deux fois sans produire de contenu. Envoyez à nouveau le message."
411200
+ },
411201
+ sv: {
411202
+ "eventPayload.providerFiltered": "Leverantören filtrerade svaret innan någon utdata visades (finishReason={finishReason}{raw}).",
411203
+ "eventPayload.modelEmptyResponse": "King avslutade två gånger utan att skapa något innehåll. Skicka meddelandet igen."
411204
+ },
411205
+ cs: {
411206
+ "eventPayload.providerFiltered": "Poskytovatel filtroval odpověď před viditelným výstupem (finishReason={finishReason}{raw}).",
411207
+ "eventPayload.modelEmptyResponse": "King dvakrát ukončil odpověď bez obsahu. Odešlete zprávu znovu."
411208
+ }
410963
411209
  });
410964
411210
  //#endregion
410965
411211
  //#region src/tui/utils/event-payload.ts
@@ -411032,6 +411278,7 @@ function formatErrorMessage$2(error) {
411032
411278
  return projectBlunIdentity(error instanceof Error ? error.message : String(error));
411033
411279
  }
411034
411280
  function formatErrorPayload(error) {
411281
+ if (error.code === "model.empty_response") return uiText("eventPayload.modelEmptyResponse");
411035
411282
  const filteredMessage = formatProviderFilteredMessage(error.details);
411036
411283
  if (filteredMessage !== void 0) return projectBlunIdentity(`[${error.code}] ${filteredMessage}`);
411037
411284
  return projectBlunIdentity(`[${error.code}] ${error.message}`);
@@ -411120,6 +411367,8 @@ registerUiCatalogFragment({
411120
411367
  "actionStyle.scope": "This setting changes how BLUN responds. Plan and permission rules still take priority.",
411121
411368
  "actionStyle.default.label": "Default",
411122
411369
  "actionStyle.default.description": "Completes coding tasks efficiently and keeps responses concise.",
411370
+ "actionStyle.concise.label": "Concise",
411371
+ "actionStyle.concise.description": "Answers briefly and directly, including only the information needed to act.",
411123
411372
  "actionStyle.proactive.label": "Proactive",
411124
411373
  "actionStyle.proactive.description": "Acts immediately when the path is clear, minimizes interruptions, and asks only when necessary.",
411125
411374
  "actionStyle.explanatory.label": "Explanatory",
@@ -411194,6 +411443,8 @@ registerUiCatalogFragment({
411194
411443
  "actionStyle.scope": "Diese Einstellung bestimmt, wie BLUN antwortet. Plan- und Berechtigungsregeln haben weiterhin Vorrang.",
411195
411444
  "actionStyle.default.label": "Standard",
411196
411445
  "actionStyle.default.description": "Erledigt Programmieraufgaben effizient und hält Antworten knapp.",
411446
+ "actionStyle.concise.label": "Knapp",
411447
+ "actionStyle.concise.description": "Antwortet kurz und direkt und nennt nur die Informationen, die zum Handeln nötig sind.",
411197
411448
  "actionStyle.proactive.label": "Proaktiv",
411198
411449
  "actionStyle.proactive.description": "Handelt sofort, wenn der Weg klar ist, unterbricht dich so selten wie möglich und fragt nur nach, wenn es nötig ist.",
411199
411450
  "actionStyle.explanatory.label": "Erklärend",
@@ -411268,6 +411519,8 @@ registerUiCatalogFragment({
411268
411519
  "actionStyle.scope": "Esta opción determina cómo responde BLUN. Las reglas del plan y de permisos siguen teniendo prioridad.",
411269
411520
  "actionStyle.default.label": "Predeterminado",
411270
411521
  "actionStyle.default.description": "Completa las tareas de programación con eficiencia y mantiene las respuestas concisas.",
411522
+ "actionStyle.concise.label": "Conciso",
411523
+ "actionStyle.concise.description": "Responde de forma breve y directa e incluye solo la información necesaria para actuar.",
411271
411524
  "actionStyle.proactive.label": "Proactivo",
411272
411525
  "actionStyle.proactive.description": "Actúa de inmediato cuando el camino está claro, reduce al mínimo las interrupciones y solo pregunta cuando es necesario.",
411273
411526
  "actionStyle.explanatory.label": "Explicativo",
@@ -411342,6 +411595,8 @@ registerUiCatalogFragment({
411342
411595
  "actionStyle.scope": "Ce réglage détermine la manière dont BLUN répond. Les règles du plan et des autorisations restent prioritaires.",
411343
411596
  "actionStyle.default.label": "Par défaut",
411344
411597
  "actionStyle.default.description": "Réalise efficacement les tâches de programmation et fournit des réponses concises.",
411598
+ "actionStyle.concise.label": "Concis",
411599
+ "actionStyle.concise.description": "Répond brièvement et directement, en indiquant uniquement les informations nécessaires pour agir.",
411345
411600
  "actionStyle.proactive.label": "Proactif",
411346
411601
  "actionStyle.proactive.description": "Agit immédiatement lorsque la marche à suivre est claire, réduit les interruptions au minimum et ne pose une question que si nécessaire.",
411347
411602
  "actionStyle.explanatory.label": "Explicatif",
@@ -411416,6 +411671,8 @@ registerUiCatalogFragment({
411416
411671
  "actionStyle.scope": "Den här inställningen styr hur BLUN svarar. Plan- och behörighetsregler har fortfarande företräde.",
411417
411672
  "actionStyle.default.label": "Standard",
411418
411673
  "actionStyle.default.description": "Slutför programmeringsuppgifter effektivt och håller svaren kortfattade.",
411674
+ "actionStyle.concise.label": "Kortfattad",
411675
+ "actionStyle.concise.description": "Svarar kort och direkt och tar bara med den information som behövs för att agera.",
411419
411676
  "actionStyle.proactive.label": "Proaktiv",
411420
411677
  "actionStyle.proactive.description": "Agerar direkt när vägen framåt är tydlig, minimerar avbrott och frågar bara när det behövs.",
411421
411678
  "actionStyle.explanatory.label": "Förklarande",
@@ -411490,6 +411747,8 @@ registerUiCatalogFragment({
411490
411747
  "actionStyle.scope": "Toto nastavení určuje, jak BLUN odpovídá. Pravidla plánu a oprávnění mají i nadále přednost.",
411491
411748
  "actionStyle.default.label": "Výchozí",
411492
411749
  "actionStyle.default.description": "Efektivně plní programátorské úkoly a odpovídá stručně.",
411750
+ "actionStyle.concise.label": "Stručný",
411751
+ "actionStyle.concise.description": "Odpovídá krátce a přímo a uvádí pouze informace potřebné k dalšímu postupu.",
411493
411752
  "actionStyle.proactive.label": "Proaktivní",
411494
411753
  "actionStyle.proactive.description": "Jedná okamžitě, když je další postup jasný, omezuje vyrušování na minimum a ptá se jen tehdy, když je to nutné.",
411495
411754
  "actionStyle.explanatory.label": "Vysvětlující",
@@ -412420,6 +412679,8 @@ registerUiCatalogFragment({
412420
412679
  "btw.error.send": "Failed to send /btw prompt: {error}",
412421
412680
  "btw.error.cancel": "Failed to cancel /btw: {error}",
412422
412681
  "btw.busy": "Wait for /btw to finish before sending another question.",
412682
+ "btw.retrying": "BTW did not send any activity for two minutes. Retrying once...",
412683
+ "btw.timeout": "BTW did not respond after the automatic retry. Press Esc to close it and send the question again.",
412423
412684
  "btw.turn.cancelled": "Interrupted by user",
412424
412685
  "btw.turn.filtered": "Provider safety policy blocked the response.",
412425
412686
  "btw.turn.ended": "BTW turn ended with reason: {reason}"
@@ -412429,6 +412690,8 @@ registerUiCatalogFragment({
412429
412690
  "btw.error.send": "Die /btw-Frage konnte nicht übermittelt werden: {error}",
412430
412691
  "btw.error.cancel": "/btw konnte nicht abgebrochen werden: {error}",
412431
412692
  "btw.busy": "Warte, bis /btw beendet ist, bevor du eine weitere Frage sendest.",
412693
+ "btw.retrying": "BTW hat zwei Minuten lang keine Aktivität gesendet. Ein automatischer Wiederholungsversuch wird gestartet ...",
412694
+ "btw.timeout": "BTW hat auch nach dem automatischen Wiederholungsversuch nicht geantwortet. Drücke Esc, um das Fenster zu schließen, und sende die Frage erneut.",
412432
412695
  "btw.turn.cancelled": "Vom Benutzer unterbrochen",
412433
412696
  "btw.turn.filtered": "Die Sicherheitsrichtlinie des Anbieters hat die Antwort blockiert.",
412434
412697
  "btw.turn.ended": "Die BTW-Runde wurde mit folgendem Grund beendet: {reason}"
@@ -412438,6 +412701,8 @@ registerUiCatalogFragment({
412438
412701
  "btw.error.send": "No se pudo enviar la pregunta de /btw: {error}",
412439
412702
  "btw.error.cancel": "No se pudo cancelar /btw: {error}",
412440
412703
  "btw.busy": "Espera a que termine /btw antes de enviar otra pregunta.",
412704
+ "btw.retrying": "BTW no ha enviado ninguna actividad durante dos minutos. Se realizará un reintento automático...",
412705
+ "btw.timeout": "BTW tampoco ha respondido tras el reintento automático. Pulsa Esc para cerrar el panel y vuelve a enviar la pregunta.",
412441
412706
  "btw.turn.cancelled": "Interrumpido por el usuario",
412442
412707
  "btw.turn.filtered": "La política de seguridad del proveedor bloqueó la respuesta.",
412443
412708
  "btw.turn.ended": "El turno de BTW terminó por este motivo: {reason}"
@@ -412447,6 +412712,8 @@ registerUiCatalogFragment({
412447
412712
  "btw.error.send": "Impossible d’envoyer la question /btw : {error}",
412448
412713
  "btw.error.cancel": "Impossible d’annuler /btw : {error}",
412449
412714
  "btw.busy": "Attendez la fin de /btw avant d’envoyer une autre question.",
412715
+ "btw.retrying": "BTW n’a envoyé aucune activité pendant deux minutes. Une nouvelle tentative automatique va être effectuée…",
412716
+ "btw.timeout": "BTW n’a toujours pas répondu après la nouvelle tentative automatique. Appuyez sur Échap pour fermer le panneau, puis renvoyez la question.",
412450
412717
  "btw.turn.cancelled": "Interrompu par l’utilisateur",
412451
412718
  "btw.turn.filtered": "La politique de sécurité du fournisseur a bloqué la réponse.",
412452
412719
  "btw.turn.ended": "Le tour BTW s’est terminé pour la raison suivante : {reason}"
@@ -412456,6 +412723,8 @@ registerUiCatalogFragment({
412456
412723
  "btw.error.send": "Det gick inte att skicka /btw-frågan: {error}",
412457
412724
  "btw.error.cancel": "Det gick inte att avbryta /btw: {error}",
412458
412725
  "btw.busy": "Vänta tills /btw är klart innan du skickar en ny fråga.",
412726
+ "btw.retrying": "BTW har inte skickat någon aktivitet på två minuter. Ett automatiskt nytt försök görs ...",
412727
+ "btw.timeout": "BTW svarade inte heller efter det automatiska försöket. Tryck på Esc för att stänga panelen och skicka frågan igen.",
412459
412728
  "btw.turn.cancelled": "Avbröts av användaren",
412460
412729
  "btw.turn.filtered": "Leverantörens säkerhetspolicy blockerade svaret.",
412461
412730
  "btw.turn.ended": "BTW-rundan avslutades av följande orsak: {reason}"
@@ -412465,6 +412734,8 @@ registerUiCatalogFragment({
412465
412734
  "btw.error.send": "Odeslání dotazu /btw selhalo: {error}",
412466
412735
  "btw.error.cancel": "Nepodařilo se zrušit /btw: {error}",
412467
412736
  "btw.busy": "Než odešlete další otázku, počkejte na dokončení /btw.",
412737
+ "btw.retrying": "BTW dvě minuty nevykázal žádnou aktivitu. Proběhne jeden automatický opakovaný pokus…",
412738
+ "btw.timeout": "BTW neodpověděl ani po automatickém opakovaném pokusu. Stisknutím Esc panel zavřete a poté otázku odešlete znovu.",
412468
412739
  "btw.turn.cancelled": "Přerušeno uživatelem",
412469
412740
  "btw.turn.filtered": "Bezpečnostní zásada poskytovatele zablokovala odpověď.",
412470
412741
  "btw.turn.ended": "Kolo BTW skončilo z důvodu: {reason}"
@@ -413285,6 +413556,7 @@ var EffortSelectorComponent = class extends Container {
413285
413556
  //#region src/tui/components/dialogs/action-style-selector.ts
413286
413557
  const ACTION_STYLES = [
413287
413558
  "default",
413559
+ "concise",
413288
413560
  "proactive",
413289
413561
  "explanatory",
413290
413562
  "learning"
@@ -425450,6 +425722,14 @@ function formatCompactCount(n) {
425450
425722
  if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
425451
425723
  return String(n);
425452
425724
  }
425725
+ function formatCompactionStageText(progress) {
425726
+ const stage = progress.stage;
425727
+ const total = progress.estimatedStageCount;
425728
+ const parts = [];
425729
+ if (stage !== void 0 && total !== void 0) parts.push(`${String(stage)}/~${String(total)}`);
425730
+ if (progress.windowUsagePercent !== void 0) parts.push(`${String(progress.windowUsagePercent)} %`);
425731
+ return parts.join(" · ");
425732
+ }
425453
425733
  function formatCompactionRunningText(inputTokens) {
425454
425734
  return inputTokens === void 0 ? uiText("compaction.running") : uiText("compaction.runningWithSize", { tokens: formatCompactCount(inputTokens) });
425455
425735
  }
@@ -425490,6 +425770,11 @@ var CompactionComponent = class extends Container {
425490
425770
  tokensAfter;
425491
425771
  estimatedInputTokens;
425492
425772
  attempt = 1;
425773
+ stage = 1;
425774
+ estimatedStageCount;
425775
+ stageCount;
425776
+ estimatedProgressPercent;
425777
+ windowUsagePercent;
425493
425778
  constructor(ui, instruction, tip, showRunning = true) {
425494
425779
  super();
425495
425780
  this.showRunning = showRunning;
@@ -425511,11 +425796,12 @@ var CompactionComponent = class extends Container {
425511
425796
  if (!this.showRunning && !this.done && !this.canceled && !this.failed) return [];
425512
425797
  return super.render(width);
425513
425798
  }
425514
- markDone(tokensBefore, tokensAfter) {
425799
+ markDone(tokensBefore, tokensAfter, stageCount) {
425515
425800
  if (this.done || this.canceled || this.failed) return;
425516
425801
  this.done = true;
425517
425802
  this.tokensBefore = tokensBefore;
425518
425803
  this.tokensAfter = tokensAfter;
425804
+ this.stageCount = stageCount;
425519
425805
  this.stopTicking();
425520
425806
  this.statusText.setText(this.buildStatusLine());
425521
425807
  this.ui?.requestRender();
@@ -425542,6 +425828,10 @@ var CompactionComponent = class extends Container {
425542
425828
  if (progress.estimatedInputTokens !== void 0) this.estimatedInputTokens = progress.estimatedInputTokens;
425543
425829
  if (progress.attempt !== this.attempt) this.attemptStartedAtMs = Date.now();
425544
425830
  this.attempt = progress.attempt;
425831
+ this.stage = progress.stage ?? this.stage;
425832
+ this.estimatedStageCount = progress.estimatedStageCount;
425833
+ this.estimatedProgressPercent = progress.estimatedProgressPercent;
425834
+ this.windowUsagePercent = progress.windowUsagePercent;
425545
425835
  this.statusText.setText(this.buildStatusLine());
425546
425836
  if (this.showRunning) this.ui?.requestRender();
425547
425837
  }
@@ -425552,17 +425842,31 @@ var CompactionComponent = class extends Container {
425552
425842
  return Math.max(0, Math.round((Date.now() - this.startedAtMs) / 1e3));
425553
425843
  }
425554
425844
  buildStatusLine() {
425555
- if (this.done) return `${currentTheme.fg("success", `[█${"█".repeat(BAR_WIDTH - 1)}]`)} ${currentTheme.boldFg("success", `${uiText("compaction.complete")} 100 %`)}${this.tokensBefore !== void 0 && this.tokensAfter !== void 0 ? currentTheme.dim(uiText("compaction.tokens", {
425556
- before: this.tokensBefore.toLocaleString(getCurrentUiLocale()),
425557
- after: this.tokensAfter.toLocaleString(getCurrentUiLocale())
425558
- })) : ""}`;
425845
+ if (this.done) {
425846
+ const bar = currentTheme.fg("success", `[█${"█".repeat(BAR_WIDTH - 1)}]`);
425847
+ const completeText = `${uiText("compaction.complete")}${this.stageCount === void 0 ? "" : ` · ${String(this.stageCount)}/${String(this.stageCount)}`}`;
425848
+ return `${bar} ${currentTheme.boldFg("success", `${completeText} 100 %`)}${this.tokensBefore !== void 0 && this.tokensAfter !== void 0 ? currentTheme.dim(uiText("compaction.tokens", {
425849
+ before: this.tokensBefore.toLocaleString(getCurrentUiLocale()),
425850
+ after: this.tokensAfter.toLocaleString(getCurrentUiLocale())
425851
+ })) : ""}`;
425852
+ }
425559
425853
  if (this.failed) return `${currentTheme.fg("error", STATUS_BULLET)}${currentTheme.boldFg("error", this.failureTitle ?? uiText("compaction.failed"))}${currentTheme.fg("textDim", ` · ${this.failureDetail ?? uiText("compaction.failureDetail")}`)}`;
425560
425854
  if (this.canceled) return `${currentTheme.fg("warning", STATUS_BULLET)}${currentTheme.boldFg("warning", uiText("compaction.canceled"))}`;
425561
- const percent = estimateCompactionPercent(Math.max(0, Date.now() - this.attemptStartedAtMs), this.estimatedInputTokens);
425855
+ const elapsedMs = Math.max(0, Date.now() - this.attemptStartedAtMs);
425856
+ const percent = this.estimatedProgressPercent ?? estimateCompactionPercent(elapsedMs, this.estimatedInputTokens);
425562
425857
  const filled = Math.round(percent / 100 * BAR_WIDTH);
425563
425858
  const bar = `[${"█".repeat(filled)}${"░".repeat(BAR_WIDTH - filled)}]`;
425564
- const runningText = formatCompactionRunningText(this.estimatedInputTokens);
425565
- return `${currentTheme.fg("primary", bar)} ${currentTheme.boldFg("primary", runningText)}${currentTheme.fg("textDim", ` ~${String(percent)} %`)}${currentTheme.fg("textDim", ` · ${String(this.elapsedSeconds())}s`)}${this.attempt > 1 ? currentTheme.fg("textDim", uiText("compaction.attempt", { attempt: this.attempt })) : ""}${currentTheme.fg("textDim", ` · ${uiText("compaction.cancelHint")}`)}${this.instruction ? currentTheme.fg("textDim", ` · ${this.instruction}`) : ""}${this.tip ? currentTheme.fg("textDim", ` · ${this.tip}`) : ""}`;
425859
+ const stageText = formatCompactionStageText({
425860
+ stage: this.stage,
425861
+ estimatedStageCount: this.estimatedStageCount,
425862
+ estimatedProgressPercent: this.estimatedProgressPercent,
425863
+ windowUsagePercent: this.windowUsagePercent,
425864
+ estimatedInputTokens: this.estimatedInputTokens,
425865
+ charsReceived: 0,
425866
+ attempt: this.attempt
425867
+ });
425868
+ const runningText = `${formatCompactionRunningText(this.estimatedInputTokens)}${stageText.length === 0 ? "" : ` · ${stageText}`}`;
425869
+ return `${currentTheme.fg("primary", bar)} ${currentTheme.boldFg("primary", runningText)}${this.windowUsagePercent === void 0 ? currentTheme.fg("textDim", ` ~${String(percent)} %`) : ""}${currentTheme.fg("textDim", ` · ${String(this.elapsedSeconds())}s`)}${this.attempt > 1 ? currentTheme.fg("textDim", uiText("compaction.attempt", { attempt: this.attempt })) : ""}${currentTheme.fg("textDim", ` · ${uiText("compaction.cancelHint")}`)}${this.instruction ? currentTheme.fg("textDim", ` · ${this.instruction}`) : ""}${this.tip ? currentTheme.fg("textDim", ` · ${this.tip}`) : ""}`;
425566
425870
  }
425567
425871
  startTicking() {
425568
425872
  this.timer = setInterval(() => {
@@ -498107,6 +498411,406 @@ registerUiCatalogFragment({
498107
498411
  }
498108
498412
  });
498109
498413
  //#endregion
498414
+ //#region src/tui/blun-tui.copy.ts
498415
+ registerUiCatalogFragment({
498416
+ en: {
498417
+ "blunTui.provider.modelAdded.one": "{providerName} · +{count} model.",
498418
+ "blunTui.provider.modelAdded.other": "{providerName} · +{count} models.",
498419
+ "blunTui.provider.refreshSkipped": "Skipped refreshing {provider}: {reason}",
498420
+ "blunTui.warning": "Warning: {warning}",
498421
+ "blunTui.startup.sessionNotFound": "Session \"{sessionId}\" not found.",
498422
+ "blunTui.startup.sessionDifferentDirectory": "Session \"{sessionId}\" was created under a different directory.",
498423
+ "blunTui.startup.noSessionsToContinue": "No sessions to continue under \"{workDir}\"; starting a fresh session.",
498424
+ "blunTui.startup.sessionNotInitialized": "Startup session was not initialized.",
498425
+ "blunTui.input.replayBlocked": "Cannot send input while session history is replaying.",
498426
+ "blunTui.shell.noSession": "No active session for shell command.",
498427
+ "blunTui.shell.runFailed": "Shell command failed: {error}",
498428
+ "blunTui.shell.cancelFailed": "Failed to cancel shell command: {error}",
498429
+ "blunTui.channel.steerFailed": "Failed to steer channel message: {error}",
498430
+ "blunTui.session.sendFailed": "Failed to send: {error}",
498431
+ "blunTui.media.imageUnsupported": "Current model does not support image input.",
498432
+ "blunTui.media.videoUnsupported": "Current model does not support video input.",
498433
+ "blunTui.skill.failed": "Skill \"{skillName}\" failed: {error}",
498434
+ "blunTui.pluginCommand.failed": "Command \"{command}\" failed: {error}",
498435
+ "blunTui.steer.failed": "Failed to steer: {error}",
498436
+ "blunTui.session.otherWorkDir": "Current session is in a different working directory.",
498437
+ "blunTui.session.resumeCommand": "To resume, run: {command}",
498438
+ "blunTui.clipboard.commandCopied": "Command copied to clipboard",
498439
+ "blunTui.clipboard.commandCopyFailed": "Failed to copy command to clipboard",
498440
+ "blunTui.session.alreadyCurrent": "Already on this session.",
498441
+ "blunTui.session.switchStreamingBlocked": "Cannot switch sessions while streaming — press Esc or Ctrl-C first.",
498442
+ "blunTui.session.switchReplayBlocked": "Cannot switch sessions while history is replaying.",
498443
+ "blunTui.session.resumeFailed": "Failed to resume session {sessionId}: {error}",
498444
+ "blunTui.session.resumed": "Resumed session ({sessionId}).",
498445
+ "blunTui.session.replayFailed": "Failed to replay session history: {error}",
498446
+ "blunTui.session.createReplayBlocked": "Cannot start a new session while history is replaying.",
498447
+ "blunTui.session.createFailed": "Failed to start a new session: {error}",
498448
+ "blunTui.session.postCreateFailed": "Post-create setup failed: {error}",
498449
+ "blunTui.session.started": "Started a new session ({sessionId}).",
498450
+ "blunTui.error": "Error: {message}",
498451
+ "blunTui.login.title": "Sign in to BLUN",
498452
+ "blunTui.login.hint": "Press Ctrl-C to cancel",
498453
+ "blunTui.login.waiting": "Waiting for authorization…",
498454
+ "blunTui.detach.noShell": "No shell command running.",
498455
+ "blunTui.detach.shellStarting": "Command is still starting — try again.",
498456
+ "blunTui.detach.shellFinished": "Command already finished.",
498457
+ "blunTui.detach.moveFailed": "Failed to move to background: {error}",
498458
+ "blunTui.detach.movedTranscript": "Moved to background.",
498459
+ "blunTui.detach.movedView": "Moved to background. /tasks to view.",
498460
+ "blunTui.detach.noForeground": "No foreground task running.",
498461
+ "blunTui.detach.listFailed": "Failed to list tasks: {error}",
498462
+ "blunTui.detach.taskFailed": "Failed to detach {taskId}: {error}",
498463
+ "blunTui.detach.finished.one": "Task already finished.",
498464
+ "blunTui.detach.finished.other": "Tasks already finished.",
498465
+ "blunTui.detach.moved.one": "Moved {count} task to background.",
498466
+ "blunTui.detach.moved.other": "Moved {count} tasks to background.",
498467
+ "blunTui.detach.partial": "Moved {detached} of {total} tasks to background.",
498468
+ "blunTui.detach.viewSuffix": "/tasks to view.",
498469
+ "blunTui.startup.flagsFailed": "Failed to apply startup flags: {error}",
498470
+ "blunTui.notification.approvalRequired": "BLUN approval required",
498471
+ "blunTui.notification.answerRequired": "BLUN needs your answer",
498472
+ "blunTui.telegram.fallbackDelivered": "Reply delivered to Telegram automatically (fallback).",
498473
+ "blunTui.telegram.attachDisabled": "Telegram attachment disabled by BLUN_TELEGRAM_ATTACH=off — headless mode active.",
498474
+ "blunTui.telegram.noToken": "Telegram attachment: no token detected — headless mode active.",
498475
+ "blunTui.telegram.attached": "Telegram channel attached (lease PID {pid}) — messages appear in this window.",
498476
+ "blunTui.auto.status": "Auto: {label}",
498477
+ "blunTui.activity.thinking": "{name} is thinking…",
498478
+ "blunTui.activity.working": "{name} is working…",
498479
+ "blunTui.activity.composing": "working...",
498480
+ "blunTui.activity.tokens": "Tokens"
498481
+ },
498482
+ de: {
498483
+ "blunTui.provider.modelAdded.one": "{providerName} · +{count} Modell.",
498484
+ "blunTui.provider.modelAdded.other": "{providerName} · +{count} Modelle.",
498485
+ "blunTui.provider.refreshSkipped": "Aktualisierung von {provider} übersprungen: {reason}",
498486
+ "blunTui.warning": "Warnung: {warning}",
498487
+ "blunTui.startup.sessionNotFound": "Sitzung „{sessionId}“ wurde nicht gefunden.",
498488
+ "blunTui.startup.sessionDifferentDirectory": "Sitzung „{sessionId}“ wurde in einem anderen Arbeitsverzeichnis erstellt.",
498489
+ "blunTui.startup.noSessionsToContinue": "Unter „{workDir}“ gibt es keine Sitzung zum Fortsetzen; eine neue Sitzung wird gestartet.",
498490
+ "blunTui.startup.sessionNotInitialized": "Die Sitzung konnte beim Start nicht initialisiert werden.",
498491
+ "blunTui.input.replayBlocked": "Während der Wiedergabe des Sitzungsverlaufs können keine Eingaben gesendet werden.",
498492
+ "blunTui.shell.noSession": "Keine aktive Sitzung für den Shell-Befehl.",
498493
+ "blunTui.shell.runFailed": "Shell-Befehl fehlgeschlagen: {error}",
498494
+ "blunTui.shell.cancelFailed": "Shell-Befehl konnte nicht abgebrochen werden: {error}",
498495
+ "blunTui.channel.steerFailed": "Die Kanalnachricht konnte nicht zur laufenden Antwort hinzugefügt werden: {error}",
498496
+ "blunTui.session.sendFailed": "Senden fehlgeschlagen: {error}",
498497
+ "blunTui.media.imageUnsupported": "Das aktuelle Modell unterstützt keine Bildeingaben.",
498498
+ "blunTui.media.videoUnsupported": "Das aktuelle Modell unterstützt keine Videoeingaben.",
498499
+ "blunTui.skill.failed": "Skill „{skillName}“ fehlgeschlagen: {error}",
498500
+ "blunTui.pluginCommand.failed": "Befehl „{command}“ fehlgeschlagen: {error}",
498501
+ "blunTui.steer.failed": "Nachsteuern fehlgeschlagen: {error}",
498502
+ "blunTui.session.otherWorkDir": "Die aktuelle Sitzung befindet sich in einem anderen Arbeitsverzeichnis.",
498503
+ "blunTui.session.resumeCommand": "Zum Fortsetzen ausführen: {command}",
498504
+ "blunTui.clipboard.commandCopied": "Befehl in die Zwischenablage kopiert",
498505
+ "blunTui.clipboard.commandCopyFailed": "Befehl konnte nicht in die Zwischenablage kopiert werden",
498506
+ "blunTui.session.alreadyCurrent": "Diese Sitzung ist bereits aktiv.",
498507
+ "blunTui.session.switchStreamingBlocked": "Während einer laufenden Antwort kann die Sitzung nicht gewechselt werden. Drücke zuerst Esc oder Ctrl-C.",
498508
+ "blunTui.session.switchReplayBlocked": "Während der Wiedergabe des Sitzungsverlaufs kann die Sitzung nicht gewechselt werden.",
498509
+ "blunTui.session.resumeFailed": "Sitzung {sessionId} konnte nicht fortgesetzt werden: {error}",
498510
+ "blunTui.session.resumed": "Sitzung fortgesetzt ({sessionId}).",
498511
+ "blunTui.session.replayFailed": "Der Sitzungsverlauf konnte nicht wiedergegeben werden: {error}",
498512
+ "blunTui.session.createReplayBlocked": "Während der Wiedergabe des Sitzungsverlaufs kann keine neue Sitzung gestartet werden.",
498513
+ "blunTui.session.createFailed": "Neue Sitzung konnte nicht gestartet werden: {error}",
498514
+ "blunTui.session.postCreateFailed": "Die neue Sitzung konnte nicht eingerichtet werden: {error}",
498515
+ "blunTui.session.started": "Neue Sitzung gestartet ({sessionId}).",
498516
+ "blunTui.error": "Fehler: {message}",
498517
+ "blunTui.login.title": "Bei BLUN anmelden",
498518
+ "blunTui.login.hint": "Zum Abbrechen Ctrl-C drücken",
498519
+ "blunTui.login.waiting": "Warten auf Autorisierung…",
498520
+ "blunTui.detach.noShell": "Es wird kein Shell-Befehl ausgeführt.",
498521
+ "blunTui.detach.shellStarting": "Der Befehl wird noch gestartet — versuche es erneut.",
498522
+ "blunTui.detach.shellFinished": "Der Befehl ist bereits beendet.",
498523
+ "blunTui.detach.moveFailed": "Verschieben in den Hintergrund fehlgeschlagen: {error}",
498524
+ "blunTui.detach.movedTranscript": "In den Hintergrund verschoben.",
498525
+ "blunTui.detach.movedView": "In den Hintergrund verschoben. Mit /tasks anzeigen.",
498526
+ "blunTui.detach.noForeground": "Es wird keine Aufgabe im Vordergrund ausgeführt.",
498527
+ "blunTui.detach.listFailed": "Aufgaben konnten nicht aufgelistet werden: {error}",
498528
+ "blunTui.detach.taskFailed": "Aufgabe {taskId} konnte nicht in den Hintergrund verschoben werden: {error}",
498529
+ "blunTui.detach.finished.one": "Aufgabe ist bereits beendet.",
498530
+ "blunTui.detach.finished.other": "Aufgaben sind bereits beendet.",
498531
+ "blunTui.detach.moved.one": "{count} Aufgabe in den Hintergrund verschoben.",
498532
+ "blunTui.detach.moved.other": "{count} Aufgaben in den Hintergrund verschoben.",
498533
+ "blunTui.detach.partial": "{detached} von {total} Aufgaben in den Hintergrund verschoben.",
498534
+ "blunTui.detach.viewSuffix": "Mit /tasks anzeigen.",
498535
+ "blunTui.startup.flagsFailed": "Startoptionen konnten nicht angewendet werden: {error}",
498536
+ "blunTui.notification.approvalRequired": "BLUN-Genehmigung erforderlich",
498537
+ "blunTui.notification.answerRequired": "BLUN benötigt deine Antwort",
498538
+ "blunTui.telegram.fallbackDelivered": "Antwort automatisch nach Telegram zugestellt (Fallback).",
498539
+ "blunTui.telegram.attachDisabled": "Telegram-Anbindung durch BLUN_TELEGRAM_ATTACH=off deaktiviert — Headless-Modus aktiv.",
498540
+ "blunTui.telegram.noToken": "Telegram-Anbindung: kein Token erkannt — Headless-Modus aktiv.",
498541
+ "blunTui.telegram.attached": "Telegram-Kanal angebunden (Lease-PID {pid}) — Nachrichten erscheinen in diesem Fenster.",
498542
+ "blunTui.auto.status": "Auto: {label}",
498543
+ "blunTui.activity.thinking": "{name} denkt…",
498544
+ "blunTui.activity.working": "{name} arbeitet…",
498545
+ "blunTui.activity.composing": "arbeitet...",
498546
+ "blunTui.activity.tokens": "Token"
498547
+ },
498548
+ es: {
498549
+ "blunTui.provider.modelAdded.one": "{providerName} · +{count} modelo.",
498550
+ "blunTui.provider.modelAdded.other": "{providerName} · +{count} modelos.",
498551
+ "blunTui.provider.refreshSkipped": "Se omitió la actualización de {provider}: {reason}",
498552
+ "blunTui.warning": "Advertencia: {warning}",
498553
+ "blunTui.startup.sessionNotFound": "No se encontró la sesión «{sessionId}».",
498554
+ "blunTui.startup.sessionDifferentDirectory": "La sesión «{sessionId}» se creó en otro directorio de trabajo.",
498555
+ "blunTui.startup.noSessionsToContinue": "No hay sesiones que reanudar en «{workDir}»; se iniciará una sesión nueva.",
498556
+ "blunTui.startup.sessionNotInitialized": "No se pudo inicializar la sesión durante el arranque.",
498557
+ "blunTui.input.replayBlocked": "No se puede enviar ninguna entrada mientras se reproduce el historial de la sesión.",
498558
+ "blunTui.shell.noSession": "No hay ninguna sesión activa para el comando de shell.",
498559
+ "blunTui.shell.runFailed": "El comando de shell falló: {error}",
498560
+ "blunTui.shell.cancelFailed": "No se pudo cancelar el comando de shell: {error}",
498561
+ "blunTui.channel.steerFailed": "No se pudo añadir el mensaje del canal a la respuesta en curso: {error}",
498562
+ "blunTui.session.sendFailed": "No se pudo enviar: {error}",
498563
+ "blunTui.media.imageUnsupported": "El modelo actual no admite entradas de imagen.",
498564
+ "blunTui.media.videoUnsupported": "El modelo actual no admite entradas de vídeo.",
498565
+ "blunTui.skill.failed": "El skill «{skillName}» falló: {error}",
498566
+ "blunTui.pluginCommand.failed": "El comando «{command}» falló: {error}",
498567
+ "blunTui.steer.failed": "No se pudo reorientar la respuesta: {error}",
498568
+ "blunTui.session.otherWorkDir": "La sesión actual se encuentra en otro directorio de trabajo.",
498569
+ "blunTui.session.resumeCommand": "Para reanudarla, ejecuta: {command}",
498570
+ "blunTui.clipboard.commandCopied": "Comando copiado al portapapeles",
498571
+ "blunTui.clipboard.commandCopyFailed": "No se pudo copiar el comando al portapapeles",
498572
+ "blunTui.session.alreadyCurrent": "Esta sesión ya está activa.",
498573
+ "blunTui.session.switchStreamingBlocked": "No se puede cambiar de sesión mientras se genera una respuesta. Pulsa primero Esc o Ctrl-C.",
498574
+ "blunTui.session.switchReplayBlocked": "No se puede cambiar de sesión mientras se reproduce el historial.",
498575
+ "blunTui.session.resumeFailed": "No se pudo reanudar la sesión {sessionId}: {error}",
498576
+ "blunTui.session.resumed": "Sesión reanudada ({sessionId}).",
498577
+ "blunTui.session.replayFailed": "No se pudo reproducir el historial de la sesión: {error}",
498578
+ "blunTui.session.createReplayBlocked": "No se puede iniciar una sesión nueva mientras se reproduce el historial.",
498579
+ "blunTui.session.createFailed": "No se pudo iniciar una sesión nueva: {error}",
498580
+ "blunTui.session.postCreateFailed": "No se pudo configurar la sesión recién creada: {error}",
498581
+ "blunTui.session.started": "Se inició una sesión nueva ({sessionId}).",
498582
+ "blunTui.error": "Error: {message}",
498583
+ "blunTui.login.title": "Iniciar sesión en BLUN",
498584
+ "blunTui.login.hint": "Pulsa Ctrl-C para cancelar",
498585
+ "blunTui.login.waiting": "Esperando autorización…",
498586
+ "blunTui.detach.noShell": "No hay ningún comando de shell en ejecución.",
498587
+ "blunTui.detach.shellStarting": "El comando todavía se está iniciando; inténtalo de nuevo.",
498588
+ "blunTui.detach.shellFinished": "El comando ya ha finalizado.",
498589
+ "blunTui.detach.moveFailed": "No se pudo mover a segundo plano: {error}",
498590
+ "blunTui.detach.movedTranscript": "Se movió a segundo plano.",
498591
+ "blunTui.detach.movedView": "Se movió a segundo plano. Consulta /tasks.",
498592
+ "blunTui.detach.noForeground": "No hay ninguna tarea en ejecución en primer plano.",
498593
+ "blunTui.detach.listFailed": "No se pudieron obtener las tareas: {error}",
498594
+ "blunTui.detach.taskFailed": "No se pudo mover la tarea {taskId} a segundo plano: {error}",
498595
+ "blunTui.detach.finished.one": "La tarea ya ha finalizado.",
498596
+ "blunTui.detach.finished.other": "Las tareas ya han finalizado.",
498597
+ "blunTui.detach.moved.one": "Se ha movido {count} tarea a segundo plano.",
498598
+ "blunTui.detach.moved.other": "Se han movido {count} tareas a segundo plano.",
498599
+ "blunTui.detach.partial": "Se han movido {detached} de {total} tareas a segundo plano.",
498600
+ "blunTui.detach.viewSuffix": "Consulta /tasks.",
498601
+ "blunTui.startup.flagsFailed": "No se pudieron aplicar las opciones de inicio: {error}",
498602
+ "blunTui.notification.approvalRequired": "Se requiere aprobación de BLUN",
498603
+ "blunTui.notification.answerRequired": "BLUN necesita tu respuesta",
498604
+ "blunTui.telegram.fallbackDelivered": "La respuesta se envió automáticamente a Telegram (modo alternativo).",
498605
+ "blunTui.telegram.attachDisabled": "Conexión con Telegram desactivada mediante BLUN_TELEGRAM_ATTACH=off — modo headless activo.",
498606
+ "blunTui.telegram.noToken": "Conexión con Telegram: no se detectó ningún token — modo headless activo.",
498607
+ "blunTui.telegram.attached": "Canal de Telegram conectado (PID de lease {pid}) — los mensajes aparecen en esta ventana.",
498608
+ "blunTui.auto.status": "Automático: {label}",
498609
+ "blunTui.activity.thinking": "{name} está pensando…",
498610
+ "blunTui.activity.working": "{name} está trabajando…",
498611
+ "blunTui.activity.composing": "trabajando...",
498612
+ "blunTui.activity.tokens": "tokens"
498613
+ },
498614
+ fr: {
498615
+ "blunTui.provider.modelAdded.one": "{providerName} · +{count} modèle.",
498616
+ "blunTui.provider.modelAdded.other": "{providerName} · +{count} modèles.",
498617
+ "blunTui.provider.refreshSkipped": "Actualisation de {provider} ignorée : {reason}",
498618
+ "blunTui.warning": "Avertissement : {warning}",
498619
+ "blunTui.startup.sessionNotFound": "Session « {sessionId} » introuvable.",
498620
+ "blunTui.startup.sessionDifferentDirectory": "La session « {sessionId} » a été créée dans un autre répertoire de travail.",
498621
+ "blunTui.startup.noSessionsToContinue": "Aucune session à reprendre dans « {workDir} » ; démarrage d’une nouvelle session.",
498622
+ "blunTui.startup.sessionNotInitialized": "La session de démarrage n’a pas été initialisée.",
498623
+ "blunTui.input.replayBlocked": "Impossible d’envoyer une saisie pendant la relecture de l’historique de la session.",
498624
+ "blunTui.shell.noSession": "Aucune session active pour la commande shell.",
498625
+ "blunTui.shell.runFailed": "Échec de la commande shell : {error}",
498626
+ "blunTui.shell.cancelFailed": "Impossible d’annuler la commande shell : {error}",
498627
+ "blunTui.channel.steerFailed": "Impossible d’ajouter le message du canal à la réponse en cours : {error}",
498628
+ "blunTui.session.sendFailed": "Échec de l’envoi : {error}",
498629
+ "blunTui.media.imageUnsupported": "Le modèle actuel ne prend pas en charge les images en entrée.",
498630
+ "blunTui.media.videoUnsupported": "Le modèle actuel ne prend pas en charge les vidéos en entrée.",
498631
+ "blunTui.skill.failed": "Échec du skill « {skillName} » : {error}",
498632
+ "blunTui.pluginCommand.failed": "Échec de la commande « {command} » : {error}",
498633
+ "blunTui.steer.failed": "Impossible de réorienter la réponse : {error}",
498634
+ "blunTui.session.otherWorkDir": "La session actuelle se trouve dans un autre répertoire de travail.",
498635
+ "blunTui.session.resumeCommand": "Pour la reprendre, exécutez : {command}",
498636
+ "blunTui.clipboard.commandCopied": "Commande copiée dans le presse-papiers",
498637
+ "blunTui.clipboard.commandCopyFailed": "Impossible de copier la commande dans le presse-papiers",
498638
+ "blunTui.session.alreadyCurrent": "Cette session est déjà active.",
498639
+ "blunTui.session.switchStreamingBlocked": "Impossible de changer de session pendant la génération d’une réponse. Appuyez d’abord sur Esc ou Ctrl-C.",
498640
+ "blunTui.session.switchReplayBlocked": "Impossible de changer de session pendant la relecture de l’historique.",
498641
+ "blunTui.session.resumeFailed": "Impossible de reprendre la session {sessionId} : {error}",
498642
+ "blunTui.session.resumed": "Session reprise ({sessionId}).",
498643
+ "blunTui.session.replayFailed": "Impossible de relire l’historique de la session : {error}",
498644
+ "blunTui.session.createReplayBlocked": "Impossible de démarrer une nouvelle session pendant la relecture de l’historique.",
498645
+ "blunTui.session.createFailed": "Impossible de démarrer une nouvelle session : {error}",
498646
+ "blunTui.session.postCreateFailed": "Impossible de configurer la nouvelle session : {error}",
498647
+ "blunTui.session.started": "Nouvelle session démarrée ({sessionId}).",
498648
+ "blunTui.error": "Erreur : {message}",
498649
+ "blunTui.login.title": "Se connecter à BLUN",
498650
+ "blunTui.login.hint": "Appuyez sur Ctrl-C pour annuler",
498651
+ "blunTui.login.waiting": "En attente de l’autorisation…",
498652
+ "blunTui.detach.noShell": "Aucune commande shell en cours.",
498653
+ "blunTui.detach.shellStarting": "La commande est encore en cours de démarrage — réessayez.",
498654
+ "blunTui.detach.shellFinished": "La commande est déjà terminée.",
498655
+ "blunTui.detach.moveFailed": "Impossible de passer la commande en arrière-plan : {error}",
498656
+ "blunTui.detach.movedTranscript": "Commande passée en arrière-plan.",
498657
+ "blunTui.detach.movedView": "Commande passée en arrière-plan. Consultez /tasks.",
498658
+ "blunTui.detach.noForeground": "Aucune tâche en cours au premier plan.",
498659
+ "blunTui.detach.listFailed": "Impossible de répertorier les tâches : {error}",
498660
+ "blunTui.detach.taskFailed": "Impossible de passer la tâche {taskId} en arrière-plan : {error}",
498661
+ "blunTui.detach.finished.one": "La tâche est déjà terminée.",
498662
+ "blunTui.detach.finished.other": "Les tâches sont déjà terminées.",
498663
+ "blunTui.detach.moved.one": "{count} tâche passée en arrière-plan.",
498664
+ "blunTui.detach.moved.other": "{count} tâches passées en arrière-plan.",
498665
+ "blunTui.detach.partial": "{detached} tâches sur {total} passées en arrière-plan.",
498666
+ "blunTui.detach.viewSuffix": "Consultez /tasks.",
498667
+ "blunTui.startup.flagsFailed": "Impossible d’appliquer les options de démarrage : {error}",
498668
+ "blunTui.notification.approvalRequired": "Approbation BLUN requise",
498669
+ "blunTui.notification.answerRequired": "BLUN attend votre réponse",
498670
+ "blunTui.telegram.fallbackDelivered": "Réponse envoyée automatiquement sur Telegram (solution de secours).",
498671
+ "blunTui.telegram.attachDisabled": "Connexion à Telegram désactivée via BLUN_TELEGRAM_ATTACH=off — mode headless actif.",
498672
+ "blunTui.telegram.noToken": "Connexion à Telegram\xA0: aucun jeton détecté — mode headless actif.",
498673
+ "blunTui.telegram.attached": "Canal Telegram connecté (PID de lease\xA0: {pid}) — les messages apparaissent dans cette fenêtre.",
498674
+ "blunTui.auto.status": "Auto\xA0: {label}",
498675
+ "blunTui.activity.thinking": "{name} réfléchit…",
498676
+ "blunTui.activity.working": "{name} travaille…",
498677
+ "blunTui.activity.composing": "travail en cours...",
498678
+ "blunTui.activity.tokens": "jetons"
498679
+ },
498680
+ sv: {
498681
+ "blunTui.provider.modelAdded.one": "{providerName} · +{count} modell.",
498682
+ "blunTui.provider.modelAdded.other": "{providerName} · +{count} modeller.",
498683
+ "blunTui.provider.refreshSkipped": "Uppdateringen av {provider} hoppades över: {reason}",
498684
+ "blunTui.warning": "Varning: {warning}",
498685
+ "blunTui.startup.sessionNotFound": "Sessionen ”{sessionId}” hittades inte.",
498686
+ "blunTui.startup.sessionDifferentDirectory": "Sessionen ”{sessionId}” skapades i en annan arbetskatalog.",
498687
+ "blunTui.startup.noSessionsToContinue": "Det finns inga sessioner att återuppta i ”{workDir}”; en ny session startas.",
498688
+ "blunTui.startup.sessionNotInitialized": "Sessionen initierades inte vid start.",
498689
+ "blunTui.input.replayBlocked": "Det går inte att skicka indata medan sessionshistoriken spelas upp.",
498690
+ "blunTui.shell.noSession": "Det finns ingen aktiv session för skalkommandot.",
498691
+ "blunTui.shell.runFailed": "Skalkommandot misslyckades: {error}",
498692
+ "blunTui.shell.cancelFailed": "Det gick inte att avbryta skalkommandot: {error}",
498693
+ "blunTui.channel.steerFailed": "Det gick inte att lägga till kanalmeddelandet i det pågående svaret: {error}",
498694
+ "blunTui.session.sendFailed": "Det gick inte att skicka: {error}",
498695
+ "blunTui.media.imageUnsupported": "Den aktuella modellen stöder inte bildindata.",
498696
+ "blunTui.media.videoUnsupported": "Den aktuella modellen stöder inte videoindata.",
498697
+ "blunTui.skill.failed": "Skill ”{skillName}” misslyckades: {error}",
498698
+ "blunTui.pluginCommand.failed": "Kommandot ”{command}” misslyckades: {error}",
498699
+ "blunTui.steer.failed": "Det gick inte att styra om svaret: {error}",
498700
+ "blunTui.session.otherWorkDir": "Den aktuella sessionen finns i en annan arbetskatalog.",
498701
+ "blunTui.session.resumeCommand": "Kör följande för att återuppta den: {command}",
498702
+ "blunTui.clipboard.commandCopied": "Kommandot kopierades till urklipp",
498703
+ "blunTui.clipboard.commandCopyFailed": "Det gick inte att kopiera kommandot till urklipp",
498704
+ "blunTui.session.alreadyCurrent": "Den här sessionen är redan aktiv.",
498705
+ "blunTui.session.switchStreamingBlocked": "Det går inte att byta session medan ett svar genereras. Tryck först på Esc eller Ctrl-C.",
498706
+ "blunTui.session.switchReplayBlocked": "Det går inte att byta session medan historiken spelas upp.",
498707
+ "blunTui.session.resumeFailed": "Det gick inte att återuppta sessionen {sessionId}: {error}",
498708
+ "blunTui.session.resumed": "Sessionen återupptogs ({sessionId}).",
498709
+ "blunTui.session.replayFailed": "Det gick inte att spela upp sessionshistoriken: {error}",
498710
+ "blunTui.session.createReplayBlocked": "Det går inte att starta en ny session medan historiken spelas upp.",
498711
+ "blunTui.session.createFailed": "Det gick inte att starta en ny session: {error}",
498712
+ "blunTui.session.postCreateFailed": "Det gick inte att konfigurera den nya sessionen: {error}",
498713
+ "blunTui.session.started": "En ny session startades ({sessionId}).",
498714
+ "blunTui.error": "Fel: {message}",
498715
+ "blunTui.login.title": "Logga in på BLUN",
498716
+ "blunTui.login.hint": "Tryck på Ctrl-C för att avbryta",
498717
+ "blunTui.login.waiting": "Väntar på auktorisering…",
498718
+ "blunTui.detach.noShell": "Inget skalkommando körs.",
498719
+ "blunTui.detach.shellStarting": "Kommandot håller fortfarande på att startas – försök igen.",
498720
+ "blunTui.detach.shellFinished": "Kommandot är redan slutfört.",
498721
+ "blunTui.detach.moveFailed": "Det gick inte att flytta kommandot till bakgrunden: {error}",
498722
+ "blunTui.detach.movedTranscript": "Flyttades till bakgrunden.",
498723
+ "blunTui.detach.movedView": "Flyttades till bakgrunden. Visa med /tasks.",
498724
+ "blunTui.detach.noForeground": "Ingen uppgift körs i förgrunden.",
498725
+ "blunTui.detach.listFailed": "Det gick inte att lista uppgifterna: {error}",
498726
+ "blunTui.detach.taskFailed": "Det gick inte att flytta uppgiften {taskId} till bakgrunden: {error}",
498727
+ "blunTui.detach.finished.one": "Uppgiften är redan slutförd.",
498728
+ "blunTui.detach.finished.other": "Uppgifterna är redan slutförda.",
498729
+ "blunTui.detach.moved.one": "{count} uppgift flyttades till bakgrunden.",
498730
+ "blunTui.detach.moved.other": "{count} uppgifter flyttades till bakgrunden.",
498731
+ "blunTui.detach.partial": "{detached} av {total} uppgifter flyttades till bakgrunden.",
498732
+ "blunTui.detach.viewSuffix": "Visa med /tasks.",
498733
+ "blunTui.startup.flagsFailed": "Det gick inte att tillämpa startalternativen: {error}",
498734
+ "blunTui.notification.approvalRequired": "BLUN-godkännande krävs",
498735
+ "blunTui.notification.answerRequired": "BLUN behöver ditt svar",
498736
+ "blunTui.telegram.fallbackDelivered": "Svaret skickades automatiskt till Telegram (reservlösning).",
498737
+ "blunTui.telegram.attachDisabled": "Telegram-anslutningen inaktiverades via BLUN_TELEGRAM_ATTACH=off — headless-läget är aktivt.",
498738
+ "blunTui.telegram.noToken": "Telegram-anslutning: ingen token hittades — headless-läget är aktivt.",
498739
+ "blunTui.telegram.attached": "Telegram-kanalen är ansluten (lease-PID {pid}) — meddelanden visas i det här fönstret.",
498740
+ "blunTui.auto.status": "Automatiskt: {label}",
498741
+ "blunTui.activity.thinking": "{name} tänker…",
498742
+ "blunTui.activity.working": "{name} arbetar…",
498743
+ "blunTui.activity.composing": "arbetar...",
498744
+ "blunTui.activity.tokens": "token"
498745
+ },
498746
+ cs: {
498747
+ "blunTui.provider.modelAdded.one": "{providerName} · +{count} model.",
498748
+ "blunTui.provider.modelAdded.other": "{providerName} · nové modely: +{count}.",
498749
+ "blunTui.provider.refreshSkipped": "Přeskočeno obnovení {provider}: {reason}",
498750
+ "blunTui.warning": "Upozornění: {warning}",
498751
+ "blunTui.startup.sessionNotFound": "Relace \"{sessionId}\" nebyla nalezena.",
498752
+ "blunTui.startup.sessionDifferentDirectory": "Relace \"{sessionId}\" byla vytvořena v jiném adresáři.",
498753
+ "blunTui.startup.noSessionsToContinue": "V adresáři \"{workDir}\" nejsou žádné relace, ve kterých by bylo možné pokračovat; spouští se nová relace.",
498754
+ "blunTui.startup.sessionNotInitialized": "Relace při spuštění nebyla inicializována.",
498755
+ "blunTui.input.replayBlocked": "Nelze odeslat vstup během přehrávání historie relace.",
498756
+ "blunTui.shell.noSession": "Žádná aktivní relace pro příkaz shellu.",
498757
+ "blunTui.shell.runFailed": "Příkaz shellu selhal: {error}",
498758
+ "blunTui.shell.cancelFailed": "Selhalo zrušení příkazu shellu: {error}",
498759
+ "blunTui.channel.steerFailed": "Předání zprávy z kanálu do probíhající odpovědi selhalo: {error}",
498760
+ "blunTui.session.sendFailed": "Selhalo odeslání: {error}",
498761
+ "blunTui.media.imageUnsupported": "Aktuální model nepodporuje vstup obrázku.",
498762
+ "blunTui.media.videoUnsupported": "Aktuální model nepodporuje vstup videa.",
498763
+ "blunTui.skill.failed": "Dovednost \"{skillName}\" selhala: {error}",
498764
+ "blunTui.pluginCommand.failed": "Příkaz \"{command}\" selhal: {error}",
498765
+ "blunTui.steer.failed": "Doplnění pokynu selhalo: {error}",
498766
+ "blunTui.session.otherWorkDir": "Aktuální relace je v jiném pracovním adresáři.",
498767
+ "blunTui.session.resumeCommand": "Chcete-li pokračovat, spusťte: {command}",
498768
+ "blunTui.clipboard.commandCopied": "Příkaz zkopírován do schránky",
498769
+ "blunTui.clipboard.commandCopyFailed": "Selhalo kopírování příkazu do schránky",
498770
+ "blunTui.session.alreadyCurrent": "Již jste v této relaci.",
498771
+ "blunTui.session.switchStreamingBlocked": "Nelze přepínat relace během streamování — nejdříve stiskněte Esc nebo Ctrl-C.",
498772
+ "blunTui.session.switchReplayBlocked": "Nelze přepínat relace během přehrávání historie.",
498773
+ "blunTui.session.resumeFailed": "Selhalo obnovení relace {sessionId}: {error}",
498774
+ "blunTui.session.resumed": "Obnovena relace ({sessionId}).",
498775
+ "blunTui.session.replayFailed": "Selhalo přehrávání historie relace: {error}",
498776
+ "blunTui.session.createReplayBlocked": "Nelze spustit novou relaci během přehrávání historie.",
498777
+ "blunTui.session.createFailed": "Selhalo spuštění nové relace: {error}",
498778
+ "blunTui.session.postCreateFailed": "Selhalo nastavení po vytvoření: {error}",
498779
+ "blunTui.session.started": "Spuštěna nová relace ({sessionId}).",
498780
+ "blunTui.error": "Chyba: {message}",
498781
+ "blunTui.login.title": "Přihlaste se do BLUN",
498782
+ "blunTui.login.hint": "Stiskněte Ctrl-C pro zrušení",
498783
+ "blunTui.login.waiting": "Čekání na autorizaci…",
498784
+ "blunTui.detach.noShell": "Žádný příkaz shellu není spuštěn.",
498785
+ "blunTui.detach.shellStarting": "Příkaz se stále spouští — zkuste znovu.",
498786
+ "blunTui.detach.shellFinished": "Příkaz již skončil.",
498787
+ "blunTui.detach.moveFailed": "Selhalo přesunutí na pozadí: {error}",
498788
+ "blunTui.detach.movedTranscript": "Přesunuto na pozadí.",
498789
+ "blunTui.detach.movedView": "Přesunuto na pozadí. Zobrazíte příkazem /tasks.",
498790
+ "blunTui.detach.noForeground": "Žádný úkol na popředí není spuštěn.",
498791
+ "blunTui.detach.listFailed": "Selhalo vypsání úkolů: {error}",
498792
+ "blunTui.detach.taskFailed": "Přesunutí úlohy {taskId} na pozadí selhalo: {error}",
498793
+ "blunTui.detach.finished.one": "Úkol již skončil.",
498794
+ "blunTui.detach.finished.other": "Úkoly již skončily.",
498795
+ "blunTui.detach.moved.one": "Přesunut {count} úkol na pozadí.",
498796
+ "blunTui.detach.moved.other": "Úkoly přesunuté na pozadí: {count}.",
498797
+ "blunTui.detach.partial": "Přesunuto {detached} z {total} úkolů na pozadí.",
498798
+ "blunTui.detach.viewSuffix": "/tasks k zobrazení.",
498799
+ "blunTui.startup.flagsFailed": "Nepodařilo se použít spouštěcí příznaky: {error}",
498800
+ "blunTui.notification.approvalRequired": "Vyžadováno schválení BLUN",
498801
+ "blunTui.notification.answerRequired": "BLUN potřebuje vaši odpověď",
498802
+ "blunTui.telegram.fallbackDelivered": "Odpověď byla automaticky doručena do Telegramu (náhradním způsobem).",
498803
+ "blunTui.telegram.attachDisabled": "Připojení Telegramu je zakázáno nastavením BLUN_TELEGRAM_ATTACH=off — aktivní je režim bez uživatelského rozhraní.",
498804
+ "blunTui.telegram.noToken": "Připojení Telegramu: nebyl nalezen žádný token — aktivní je režim bez uživatelského rozhraní.",
498805
+ "blunTui.telegram.attached": "Kanál Telegramu je připojen (PID držitele připojení {pid}) — zprávy se zobrazují v tomto okně.",
498806
+ "blunTui.auto.status": "Automaticky: {label}",
498807
+ "blunTui.activity.thinking": "{name} přemýšlí…",
498808
+ "blunTui.activity.working": "{name} pracuje…",
498809
+ "blunTui.activity.composing": "pracuje…",
498810
+ "blunTui.activity.tokens": "Tokeny"
498811
+ }
498812
+ });
498813
+ //#endregion
498110
498814
  //#region src/tui/components/panes/btw-panel.ts
498111
498815
  const MIN_COLLAPSED_PANEL_LINES = 3;
498112
498816
  var BtwPanelComponent = class {
@@ -498117,6 +498821,8 @@ var BtwPanelComponent = class {
498117
498821
  followTail = true;
498118
498822
  scrollTop = 0;
498119
498823
  maxScrollTop = 0;
498824
+ spinnerFrame = 0;
498825
+ liveStatusTimer;
498120
498826
  constructor(options) {
498121
498827
  this.options = options;
498122
498828
  }
@@ -498128,10 +498834,12 @@ var BtwPanelComponent = class {
498128
498834
  this.transientNotices.length = 0;
498129
498835
  this.turns.push({
498130
498836
  prompt: normalized,
498837
+ startedAtMs: Date.now(),
498131
498838
  answer: "",
498132
498839
  thinking: "",
498133
498840
  phase: "running"
498134
498841
  });
498842
+ this.startLiveStatusTimer();
498135
498843
  this.options.onPrompt(normalized);
498136
498844
  }
498137
498845
  addTransientNotice(message) {
@@ -498148,31 +498856,48 @@ var BtwPanelComponent = class {
498148
498856
  if (turn === void 0) return;
498149
498857
  turn.thinking += delta;
498150
498858
  }
498859
+ restartCurrentTurn(notice) {
498860
+ const turn = this.currentTurn();
498861
+ if (turn === void 0 || turn.phase !== "running") return;
498862
+ turn.startedAtMs = Date.now();
498863
+ turn.answer = "";
498864
+ turn.thinking = "";
498865
+ turn.error = void 0;
498866
+ this.transientNotices.length = 0;
498867
+ this.transientNotices.push(notice);
498868
+ }
498151
498869
  markDone(resultSummary) {
498152
498870
  const turn = this.currentTurn();
498153
498871
  if (turn === void 0) return;
498154
498872
  if (turn.answer.trim().length === 0 && resultSummary !== void 0) turn.answer = resultSummary;
498155
498873
  this.transientNotices.length = 0;
498156
498874
  turn.phase = "done";
498875
+ this.stopLiveStatusTimer();
498157
498876
  }
498158
498877
  markFailed(error) {
498159
498878
  const turn = this.currentTurn();
498160
498879
  if (turn === void 0 || turn.phase !== "running") {
498161
498880
  this.turns.push({
498162
498881
  prompt: "",
498882
+ startedAtMs: Date.now(),
498163
498883
  answer: "",
498164
498884
  thinking: "",
498165
498885
  error,
498166
498886
  phase: "failed"
498167
498887
  });
498168
498888
  this.transientNotices.length = 0;
498889
+ this.stopLiveStatusTimer();
498169
498890
  return;
498170
498891
  }
498171
498892
  turn.error = error;
498172
498893
  this.transientNotices.length = 0;
498173
498894
  turn.phase = "failed";
498895
+ this.stopLiveStatusTimer();
498174
498896
  }
498175
498897
  invalidate() {}
498898
+ dispose() {
498899
+ this.stopLiveStatusTimer();
498900
+ }
498176
498901
  render(width) {
498177
498902
  const safeWidth = Math.max(4, width);
498178
498903
  const contentWidth = Math.max(1, safeWidth - 4);
@@ -498245,7 +498970,8 @@ var BtwPanelComponent = class {
498245
498970
  const thinkingLines = new Text(chalk.hex(currentTheme.palette.textDim)(thinking), 0, 0).render(width);
498246
498971
  const visibleThinking = thinkingLines.length > 2 ? thinkingLines.slice(thinkingLines.length - 2) : thinkingLines;
498247
498972
  lines.push(...visibleThinking);
498248
- } else if (turn.error === void 0) lines.push(chalk.hex(currentTheme.palette.textDim)(uiText("btw.panel.waiting")));
498973
+ }
498974
+ if (turn.phase === "running" && turn.error === void 0) lines.push(this.renderLiveStatus(turn));
498249
498975
  if (turn.error !== void 0) {
498250
498976
  const error = chalk.hex(currentTheme.palette.error)(turn.error);
498251
498977
  lines.push(...new Text(error, 0, 0).render(width));
@@ -498262,6 +498988,26 @@ var BtwPanelComponent = class {
498262
498988
  currentTurn() {
498263
498989
  return this.turns.at(-1);
498264
498990
  }
498991
+ renderLiveStatus(turn) {
498992
+ const elapsedSeconds = Math.max(0, Math.floor((Date.now() - turn.startedAtMs) / 1e3));
498993
+ const outputTokens = estimateLiveOutputTokens(turn.thinking + turn.answer);
498994
+ const frame = BLUN_SPINNER_FRAMES[this.spinnerFrame] ?? BLUN_SPINNER_FRAMES[0];
498995
+ const label = uiText("blunTui.activity.thinking", { name: "BTW" });
498996
+ const metrics = `(${formatLiveElapsed(elapsedSeconds)} · ↓ ~${formatLiveTokenCount(outputTokens)} ${uiText("blunTui.activity.tokens")})`;
498997
+ return chalk.hex(currentTheme.palette.accent)(`${frame} `) + chalk.hex(currentTheme.palette.accent).bold(label) + " " + chalk.hex(currentTheme.palette.text)(metrics);
498998
+ }
498999
+ startLiveStatusTimer() {
499000
+ if (this.liveStatusTimer !== void 0) return;
499001
+ this.liveStatusTimer = setInterval(() => {
499002
+ this.spinnerFrame = (this.spinnerFrame + 1) % BLUN_SPINNER_FRAMES.length;
499003
+ this.options.requestRender();
499004
+ }, 120);
499005
+ }
499006
+ stopLiveStatusTimer() {
499007
+ if (this.liveStatusTimer === void 0) return;
499008
+ clearInterval(this.liveStatusTimer);
499009
+ this.liveStatusTimer = void 0;
499010
+ }
498265
499011
  isRunning() {
498266
499012
  return this.currentTurn()?.phase === "running";
498267
499013
  }
@@ -498328,6 +499074,7 @@ function formatHookResultBody(event) {
498328
499074
  }
498329
499075
  //#endregion
498330
499076
  //#region src/tui/controllers/btw-panel.ts
499077
+ const BTW_INACTIVITY_TIMEOUT_MS = 12e4;
498331
499078
  var BtwPanelController = class {
498332
499079
  host;
498333
499080
  active;
@@ -498341,13 +499088,16 @@ var BtwPanelController = class {
498341
499088
  markdownTheme: createMarkdownTheme(),
498342
499089
  canUseScrollKeys: () => this.host.state.editor.getText().length === 0,
498343
499090
  terminalRows: () => this.host.state.terminal.rows,
499091
+ requestRender: () => this.host.state.ui.requestRender(),
498344
499092
  onPrompt: (prompt) => {
498345
- this.promptAgent(agentId, prompt, panel);
499093
+ this.promptPanel(panel, prompt);
498346
499094
  }
498347
499095
  });
498348
499096
  this.active = {
498349
499097
  agentId,
498350
- panel
499098
+ panel,
499099
+ prompt: "",
499100
+ retryCount: 0
498351
499101
  };
498352
499102
  this.panelsByAgentId.set(agentId, panel);
498353
499103
  this.mount(panel);
@@ -498356,6 +499106,8 @@ var BtwPanelController = class {
498356
499106
  clear() {
498357
499107
  const active = this.active;
498358
499108
  if (active !== void 0 && this.shouldCancelOnUnmount(active.panel)) this.cancelAgent(active.agentId);
499109
+ for (const panel of this.panelsByAgentId.values()) panel.dispose();
499110
+ this.clearInactivityTimer(active);
498359
499111
  this.active = void 0;
498360
499112
  this.panelsByAgentId.clear();
498361
499113
  this.host.state.btwPanelContainer.clear();
@@ -498398,19 +499150,23 @@ var BtwPanelController = class {
498398
499150
  if (panel === void 0) return false;
498399
499151
  switch (event.type) {
498400
499152
  case "assistant.delta":
499153
+ this.clearInactivityTimer(this.activeForAgent(event.agentId));
498401
499154
  panel.appendAnswer(event.delta);
498402
499155
  this.host.state.ui.requestRender();
498403
499156
  return true;
498404
499157
  case "thinking.delta":
498405
499158
  panel.appendThinking(event.delta);
499159
+ this.armInactivityTimer(this.activeForAgent(event.agentId));
498406
499160
  this.host.state.ui.requestRender();
498407
499161
  return true;
498408
499162
  case "hook.result":
499163
+ this.clearInactivityTimer(this.activeForAgent(event.agentId));
498409
499164
  panel.appendAnswer(formatHookResultPlain(event));
498410
499165
  this.host.state.ui.requestRender();
498411
499166
  return true;
498412
499167
  case "turn.ended":
498413
- if (event.reason === "completed") panel.markDone();
499168
+ this.clearInactivityTimer(this.activeForAgent(event.agentId));
499169
+ if (event.reason === "completed") panel.markDone(uiText("eventPayload.modelEmptyResponse"));
498414
499170
  else panel.markFailed(formatBtwTurnEnd(event));
498415
499171
  this.host.state.ui.requestRender();
498416
499172
  return true;
@@ -498427,7 +499183,9 @@ var BtwPanelController = class {
498427
499183
  }
498428
499184
  close(panel) {
498429
499185
  if (!this.host.state.btwPanelContainer.children.includes(panel)) return;
499186
+ this.clearInactivityTimer(this.active?.panel === panel ? this.active : void 0);
498430
499187
  this.unregister(panel);
499188
+ panel.dispose();
498431
499189
  this.host.state.btwPanelContainer.clear();
498432
499190
  this.host.state.editor.connectedAbove = false;
498433
499191
  this.host.state.ui.setFocus(this.host.state.editor);
@@ -498442,6 +499200,14 @@ var BtwPanelController = class {
498442
499200
  active.panel.addTransientNotice(uiText("btw.busy"));
498443
499201
  this.host.state.ui.requestRender();
498444
499202
  }
499203
+ promptPanel(panel, prompt) {
499204
+ const active = this.active;
499205
+ if (active === void 0 || active.panel !== panel) return;
499206
+ active.prompt = prompt;
499207
+ active.retryCount = 0;
499208
+ this.promptAgent(active.agentId, prompt, panel);
499209
+ this.armInactivityTimer(active);
499210
+ }
498445
499211
  promptAgent(agentId, prompt, panel) {
498446
499212
  const session = this.host.session;
498447
499213
  if (session === void 0) {
@@ -498450,16 +499216,76 @@ var BtwPanelController = class {
498450
499216
  return;
498451
499217
  }
498452
499218
  this.withInteractiveAgent(agentId, () => session.prompt(prompt)).catch((error) => {
499219
+ if (this.panelsByAgentId.get(agentId) !== panel) return;
499220
+ this.clearInactivityTimer(this.activeForAgent(agentId));
498453
499221
  panel.markFailed(uiText("btw.error.send", { error: formatErrorMessage$2(error) }));
498454
499222
  this.host.state.ui.requestRender();
498455
499223
  });
498456
499224
  }
498457
499225
  async cancelAgent(agentId) {
498458
499226
  const session = this.host.session;
498459
- if (session === void 0) return;
498460
- await this.withInteractiveAgent(agentId, () => session.cancel()).catch((error) => {
498461
- this.host.showError(uiText("btw.error.cancel", { error: formatErrorMessage$2(error) }));
498462
- });
499227
+ if (session === void 0) return noActiveSessionMessage();
499228
+ try {
499229
+ await this.withInteractiveAgent(agentId, () => session.cancel());
499230
+ return;
499231
+ } catch (error) {
499232
+ const message = formatErrorMessage$2(error);
499233
+ this.host.showError(uiText("btw.error.cancel", { error: message }));
499234
+ return message;
499235
+ }
499236
+ }
499237
+ activeForAgent(agentId) {
499238
+ return this.active?.agentId === agentId ? this.active : void 0;
499239
+ }
499240
+ armInactivityTimer(active) {
499241
+ if (active === void 0 || !active.panel.isRunning()) return;
499242
+ this.clearInactivityTimer(active);
499243
+ active.inactivityTimer = setTimeout(() => {
499244
+ active.inactivityTimer = void 0;
499245
+ this.handleInactivity(active);
499246
+ }, BTW_INACTIVITY_TIMEOUT_MS);
499247
+ active.inactivityTimer.unref?.();
499248
+ }
499249
+ clearInactivityTimer(active) {
499250
+ if (active?.inactivityTimer === void 0) return;
499251
+ clearTimeout(active.inactivityTimer);
499252
+ active.inactivityTimer = void 0;
499253
+ }
499254
+ async handleInactivity(active) {
499255
+ if (this.active !== active || !active.panel.isRunning()) return;
499256
+ const staleAgentId = active.agentId;
499257
+ this.panelsByAgentId.delete(staleAgentId);
499258
+ const cancelError = await this.cancelAgent(staleAgentId);
499259
+ if (this.active !== active || !active.panel.isRunning()) return;
499260
+ if (cancelError !== void 0) {
499261
+ active.panel.markFailed(uiText("btw.error.cancel", { error: cancelError }));
499262
+ this.host.state.ui.requestRender();
499263
+ return;
499264
+ }
499265
+ if (active.retryCount >= 1) {
499266
+ active.panel.markFailed(uiText("btw.timeout"));
499267
+ this.host.state.ui.requestRender();
499268
+ return;
499269
+ }
499270
+ active.retryCount += 1;
499271
+ active.panel.restartCurrentTurn(uiText("btw.retrying"));
499272
+ try {
499273
+ const session = this.host.session;
499274
+ if (session === void 0) throw new Error(noActiveSessionMessage());
499275
+ const nextAgentId = await session.startBtw();
499276
+ if (this.active !== active || !active.panel.isRunning()) {
499277
+ await this.cancelAgent(nextAgentId);
499278
+ return;
499279
+ }
499280
+ active.agentId = nextAgentId;
499281
+ this.panelsByAgentId.set(nextAgentId, active.panel);
499282
+ this.promptAgent(nextAgentId, active.prompt, active.panel);
499283
+ this.armInactivityTimer(active);
499284
+ this.host.state.ui.requestRender();
499285
+ } catch (error) {
499286
+ active.panel.markFailed(uiText("btw.error.start", { error: formatErrorMessage$2(error) }));
499287
+ this.host.state.ui.requestRender();
499288
+ }
498463
499289
  }
498464
499290
  shouldCancelOnUnmount(panel) {
498465
499291
  return panel.isRunning() || panel.isEmpty();
@@ -503010,6 +503836,7 @@ var SessionEventHandler = class {
503010
503836
  synthetic: event.synthetic
503011
503837
  };
503012
503838
  const matchedCall = streamingUI.completeToolResult(event.toolCallId, resultData);
503839
+ if (matchedCall?.name === "GetMedia" && event.isError !== true) this.host.runChannelMediaFallback(event.output);
503013
503840
  this.subAgentEventHandler.handleAgentSwarmToolResult(event.toolCallId, resultData, event.isError === true);
503014
503841
  if (matchedCall !== void 0 && matchedCall.name === "TodoList" && !event.isError) {
503015
503842
  const rawTodos = matchedCall.args.todos;
@@ -503398,7 +504225,7 @@ var SessionEventHandler = class {
503398
504225
  }
503399
504226
  handleCompactionEnd(event) {
503400
504227
  this.host.setAppState({ contextTokens: event.projectedContextTokens ?? event.result.tokensAfter });
503401
- this.host.streamingUI.endCompaction(event.result.tokensBefore, event.result.tokensAfter);
504228
+ this.host.streamingUI.endCompaction(event.result.tokensBefore, event.result.tokensAfter, event.stageCount);
503402
504229
  this.lastCompactionInstruction = void 0;
503403
504230
  this.finishCompaction();
503404
504231
  }
@@ -505353,17 +506180,21 @@ function formatContextStatus(usage, tokens, maxTokens, colors, width, now = /* @
505353
506180
  for (const variant of variants) if (visibleWidth(variant) <= available) return variant;
505354
506181
  return truncateToWidth(current, available, "…");
505355
506182
  }
505356
- function formatCompactionStatus(elapsedMs, estimatedInputTokens, colors, width, now = /* @__PURE__ */ new Date()) {
505357
- const percent = estimateCompactionPercent(elapsedMs, estimatedInputTokens);
506183
+ function formatCompactionStatus(elapsedMs, estimatedInputTokens, progress, colors, width, now = /* @__PURE__ */ new Date()) {
506184
+ const percent = progress?.estimatedProgressPercent ?? estimateCompactionPercent(elapsedMs, estimatedInputTokens);
505358
506185
  const filled = Math.max(1, Math.ceil((1 - percent / 100) * CONTEXT_BAR_WIDTH));
505359
506186
  const bar = chalk.hex(colors.primary)(`${"█".repeat(filled)}${"░".repeat(CONTEXT_BAR_WIDTH - filled)}`);
505360
506187
  const label = formatCompactionRunningText(estimatedInputTokens);
505361
- const estimate = `~${String(percent)} %`;
506188
+ const stageText = progress === void 0 ? "" : formatCompactionStageText(progress);
506189
+ const compactStageText = progress?.stage === void 0 || progress.estimatedStageCount === void 0 ? "" : `${String(progress.stage)}/~${String(progress.estimatedStageCount)}${progress.windowUsagePercent === void 0 ? "" : ` · ${String(progress.windowUsagePercent)}%`}`;
506190
+ const estimate = progress?.windowUsagePercent === void 0 ? `~${String(percent)} %` : "";
506191
+ const clock = chalk.hex(colors.textDim)(` · ${clockHhmm(now)}`);
505362
506192
  const variants = [
505363
- `${label} ${bar} ${estimate}${chalk.hex(colors.textDim)(` · ${clockHhmm(now)}`)}`,
505364
- `${label} ${bar} ${estimate}`,
505365
- `${bar} ${estimate}`,
505366
- estimate
506193
+ `${label}${stageText.length === 0 ? "" : ` · ${stageText}`} ${bar}${estimate.length === 0 ? "" : ` ${estimate}`}${clock}`,
506194
+ `${label}${stageText.length === 0 ? "" : ` · ${stageText}`}`,
506195
+ compactStageText,
506196
+ `${bar}${estimate.length === 0 ? "" : ` ${estimate}`}`,
506197
+ stageText.length === 0 ? estimate : stageText
505367
506198
  ];
505368
506199
  const available = Math.max(0, width);
505369
506200
  for (const variant of variants) if (visibleWidth(variant) <= available) return variant;
@@ -505398,6 +506229,7 @@ var FooterComponent = class {
505398
506229
  goalTimer = null;
505399
506230
  compactionAttemptStartedAtMs = null;
505400
506231
  compactionEstimatedInputTokens;
506232
+ compactionProgress;
505401
506233
  compactionAttempt = 1;
505402
506234
  compactionTimer = null;
505403
506235
  /**
@@ -505432,6 +506264,7 @@ var FooterComponent = class {
505432
506264
  startCompaction() {
505433
506265
  this.compactionAttemptStartedAtMs = Date.now();
505434
506266
  this.compactionEstimatedInputTokens = void 0;
506267
+ this.compactionProgress = void 0;
505435
506268
  this.compactionAttempt = 1;
505436
506269
  if (this.compactionTimer === null) {
505437
506270
  this.compactionTimer = setInterval(() => {
@@ -505445,11 +506278,13 @@ var FooterComponent = class {
505445
506278
  if (progress.estimatedInputTokens !== void 0) this.compactionEstimatedInputTokens = progress.estimatedInputTokens;
505446
506279
  if (progress.attempt !== this.compactionAttempt) this.compactionAttemptStartedAtMs = Date.now();
505447
506280
  this.compactionAttempt = progress.attempt;
506281
+ this.compactionProgress = progress;
505448
506282
  this.onRefresh();
505449
506283
  }
505450
506284
  finishCompaction() {
505451
506285
  this.compactionAttemptStartedAtMs = null;
505452
506286
  this.compactionEstimatedInputTokens = void 0;
506287
+ this.compactionProgress = void 0;
505453
506288
  this.compactionAttempt = 1;
505454
506289
  if (this.compactionTimer !== null) {
505455
506290
  clearInterval(this.compactionTimer);
@@ -505516,7 +506351,7 @@ var FooterComponent = class {
505516
506351
  const leftLine = left.join(" ");
505517
506352
  const leftWidth = visibleWidth(leftLine);
505518
506353
  const context = contextMetrics(state);
505519
- const contextText = this.compactionAttemptStartedAtMs === null ? formatContextStatus(context.usage, context.tokens, context.maxTokens, colors, width) : formatCompactionStatus(Math.max(0, Date.now() - this.compactionAttemptStartedAtMs), this.compactionEstimatedInputTokens, colors, width);
506354
+ const contextText = this.compactionAttemptStartedAtMs === null ? formatContextStatus(context.usage, context.tokens, context.maxTokens, colors, width) : formatCompactionStatus(Math.max(0, Date.now() - this.compactionAttemptStartedAtMs), this.compactionEstimatedInputTokens, this.compactionProgress, colors, width);
505520
506355
  const right = this.transientHint && this.compactionAttemptStartedAtMs === null ? chalk.hex(colors.warning).bold(this.transientHint) : chalk.hex(colors.text)(contextText);
505521
506356
  const rightWidth = visibleWidth(right);
505522
506357
  let line1;
@@ -506358,7 +507193,7 @@ var StreamingUIController = class {
506358
507193
  this._activeCompactionBlock.markDone();
506359
507194
  this._activeCompactionBlock = void 0;
506360
507195
  }
506361
- const block = new CompactionComponent(state.ui, instruction, currentWorkingTip()?.text, false);
507196
+ const block = new CompactionComponent(state.ui, instruction, currentWorkingTip()?.text, true);
506362
507197
  this._activeCompactionBlock = block;
506363
507198
  state.transcriptContainer.addChild(block);
506364
507199
  state.ui.requestRender();
@@ -506370,11 +507205,11 @@ var StreamingUIController = class {
506370
507205
  block.setProgress(progress);
506371
507206
  this.host.state.ui.requestRender();
506372
507207
  }
506373
- endCompaction(tokensBefore, tokensAfter) {
507208
+ endCompaction(tokensBefore, tokensAfter, stageCount) {
506374
507209
  this.host.state.footer.finishCompaction();
506375
507210
  const block = this._activeCompactionBlock;
506376
507211
  if (block === void 0) return;
506377
- block.markDone(tokensBefore, tokensAfter);
507212
+ block.markDone(tokensBefore, tokensAfter, stageCount);
506378
507213
  this._activeCompactionBlock = void 0;
506379
507214
  this._cancelledCompactionBlock = void 0;
506380
507215
  this.host.state.ui.requestRender();
@@ -509353,6 +510188,10 @@ function channelDir() {
509353
510188
  function outboxPath() {
509354
510189
  return join$4(channelDir(), "outbox.jsonl");
509355
510190
  }
510191
+ function mediaDir() {
510192
+ const base = process.env["BLUN_HOME"]?.trim();
510193
+ return join$4(base !== void 0 && base.length > 0 ? base : join$4(homedir(), ".blun"), "media");
510194
+ }
509356
510195
  /** Host-owned channel setting from process env or the channel .env. */
509357
510196
  function channelSetting(name) {
509358
510197
  const configured = process.env[name]?.trim();
@@ -509395,6 +510234,49 @@ function outboxGrewForChat(marker, chatId) {
509395
510234
  return false;
509396
510235
  }
509397
510236
  const TELEGRAM_TEXT_LIMIT = 4096;
510237
+ const TELEGRAM_ATTACHMENT_LIMIT = 50 * 1024 * 1024;
510238
+ function mediaTelegramTarget(filePath) {
510239
+ const lower = filePath.toLowerCase();
510240
+ if (/\.(?:png|jpe?g|webp|gif)$/u.test(lower)) return {
510241
+ method: "sendPhoto",
510242
+ field: "photo",
510243
+ mimeType: lower.endsWith(".png") ? "image/png" : lower.endsWith(".webp") ? "image/webp" : lower.endsWith(".gif") ? "image/gif" : "image/jpeg"
510244
+ };
510245
+ if (lower.endsWith(".mp4")) return {
510246
+ method: "sendVideo",
510247
+ field: "video",
510248
+ mimeType: "video/mp4"
510249
+ };
510250
+ if (/\.(?:mp3|wav)$/u.test(lower)) return {
510251
+ method: "sendAudio",
510252
+ field: "audio",
510253
+ mimeType: lower.endsWith(".mp3") ? "audio/mpeg" : "audio/wav"
510254
+ };
510255
+ return {
510256
+ method: "sendDocument",
510257
+ field: "document",
510258
+ mimeType: "application/octet-stream"
510259
+ };
510260
+ }
510261
+ function safeCompletedMediaPath(filePath) {
510262
+ try {
510263
+ const root = realpathSync(mediaDir());
510264
+ const resolved = realpathSync(filePath);
510265
+ const withinRoot = relative(root, resolved);
510266
+ if (withinRoot.length === 0 || withinRoot.startsWith("..") || isAbsolute(withinRoot)) return;
510267
+ const info = statSync(resolved);
510268
+ if (!info.isFile() || info.size <= 0 || info.size > TELEGRAM_ATTACHMENT_LIMIT) return void 0;
510269
+ return resolved;
510270
+ } catch {
510271
+ return;
510272
+ }
510273
+ }
510274
+ /** Extract the host-owned local result path from a successful GetMedia output. */
510275
+ function completedMediaLocalPath(output) {
510276
+ const text = Array.isArray(output) ? output.filter((part) => typeof part === "object" && part !== null && part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n") : typeof output === "string" ? output : "";
510277
+ const candidate = /Media job [^\r\n]+ is complete\. Local file: (.+?)\. The BLUN host/u.exec(text)?.[1]?.trim();
510278
+ return candidate === void 0 || candidate.length === 0 ? void 0 : safeCompletedMediaPath(candidate);
510279
+ }
509398
510280
  /** Telegram group/supergroup ids are negative; DMs are the positive user id. */
509399
510281
  function isGroupChat(chatId) {
509400
510282
  return chatId.startsWith("-");
@@ -509467,6 +510349,40 @@ async function sendReplyFallback(chatId, text, contextOnly = false) {
509467
510349
  return false;
509468
510350
  }
509469
510351
  }
510352
+ /**
510353
+ * Deliver a completed BLUN media file immediately for a channel-origin turn.
510354
+ * The path must resolve to a non-empty file below BLUN_HOME/media. Returns true
510355
+ * only after Telegram accepted the upload and the outbox receipt was appended.
510356
+ */
510357
+ async function sendMediaReplyFallback(chatId, filePath) {
510358
+ const safePath = safeCompletedMediaPath(filePath);
510359
+ const token = botToken();
510360
+ if (safePath === void 0 || token === void 0) return false;
510361
+ const target = mediaTelegramTarget(safePath);
510362
+ const form = new FormData();
510363
+ form.append("chat_id", chatId);
510364
+ form.append(target.field, new Blob([new Uint8Array(readFileSync(safePath))], { type: target.mimeType }), basename(safePath));
510365
+ try {
510366
+ const payload = await (await fetch(`https://api.telegram.org/bot${token}/${target.method}`, {
510367
+ method: "POST",
510368
+ body: form
510369
+ })).json();
510370
+ if (payload.ok !== true) return false;
510371
+ try {
510372
+ appendFileSync(outboxPath(), `${JSON.stringify({
510373
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
510374
+ direction: "out",
510375
+ kind: "media-reply-fallback",
510376
+ chat_id: String(chatId),
510377
+ message_ids: payload.result?.message_id === void 0 ? [] : [payload.result.message_id],
510378
+ files: [safePath]
510379
+ })}\n`);
510380
+ } catch {}
510381
+ return true;
510382
+ } catch {
510383
+ return false;
510384
+ }
510385
+ }
509470
510386
  //#endregion
509471
510387
  //#region src/tui/utils/dead-terminal.ts
509472
510388
  /**
@@ -510427,406 +511343,6 @@ function turnsToTrim(turns, maxTurns, hysteresis) {
510427
511343
  return toRemove;
510428
511344
  }
510429
511345
  //#endregion
510430
- //#region src/tui/blun-tui.copy.ts
510431
- registerUiCatalogFragment({
510432
- en: {
510433
- "blunTui.provider.modelAdded.one": "{providerName} · +{count} model.",
510434
- "blunTui.provider.modelAdded.other": "{providerName} · +{count} models.",
510435
- "blunTui.provider.refreshSkipped": "Skipped refreshing {provider}: {reason}",
510436
- "blunTui.warning": "Warning: {warning}",
510437
- "blunTui.startup.sessionNotFound": "Session \"{sessionId}\" not found.",
510438
- "blunTui.startup.sessionDifferentDirectory": "Session \"{sessionId}\" was created under a different directory.",
510439
- "blunTui.startup.noSessionsToContinue": "No sessions to continue under \"{workDir}\"; starting a fresh session.",
510440
- "blunTui.startup.sessionNotInitialized": "Startup session was not initialized.",
510441
- "blunTui.input.replayBlocked": "Cannot send input while session history is replaying.",
510442
- "blunTui.shell.noSession": "No active session for shell command.",
510443
- "blunTui.shell.runFailed": "Shell command failed: {error}",
510444
- "blunTui.shell.cancelFailed": "Failed to cancel shell command: {error}",
510445
- "blunTui.channel.steerFailed": "Failed to steer channel message: {error}",
510446
- "blunTui.session.sendFailed": "Failed to send: {error}",
510447
- "blunTui.media.imageUnsupported": "Current model does not support image input.",
510448
- "blunTui.media.videoUnsupported": "Current model does not support video input.",
510449
- "blunTui.skill.failed": "Skill \"{skillName}\" failed: {error}",
510450
- "blunTui.pluginCommand.failed": "Command \"{command}\" failed: {error}",
510451
- "blunTui.steer.failed": "Failed to steer: {error}",
510452
- "blunTui.session.otherWorkDir": "Current session is in a different working directory.",
510453
- "blunTui.session.resumeCommand": "To resume, run: {command}",
510454
- "blunTui.clipboard.commandCopied": "Command copied to clipboard",
510455
- "blunTui.clipboard.commandCopyFailed": "Failed to copy command to clipboard",
510456
- "blunTui.session.alreadyCurrent": "Already on this session.",
510457
- "blunTui.session.switchStreamingBlocked": "Cannot switch sessions while streaming — press Esc or Ctrl-C first.",
510458
- "blunTui.session.switchReplayBlocked": "Cannot switch sessions while history is replaying.",
510459
- "blunTui.session.resumeFailed": "Failed to resume session {sessionId}: {error}",
510460
- "blunTui.session.resumed": "Resumed session ({sessionId}).",
510461
- "blunTui.session.replayFailed": "Failed to replay session history: {error}",
510462
- "blunTui.session.createReplayBlocked": "Cannot start a new session while history is replaying.",
510463
- "blunTui.session.createFailed": "Failed to start a new session: {error}",
510464
- "blunTui.session.postCreateFailed": "Post-create setup failed: {error}",
510465
- "blunTui.session.started": "Started a new session ({sessionId}).",
510466
- "blunTui.error": "Error: {message}",
510467
- "blunTui.login.title": "Sign in to BLUN",
510468
- "blunTui.login.hint": "Press Ctrl-C to cancel",
510469
- "blunTui.login.waiting": "Waiting for authorization…",
510470
- "blunTui.detach.noShell": "No shell command running.",
510471
- "blunTui.detach.shellStarting": "Command is still starting — try again.",
510472
- "blunTui.detach.shellFinished": "Command already finished.",
510473
- "blunTui.detach.moveFailed": "Failed to move to background: {error}",
510474
- "blunTui.detach.movedTranscript": "Moved to background.",
510475
- "blunTui.detach.movedView": "Moved to background. /tasks to view.",
510476
- "blunTui.detach.noForeground": "No foreground task running.",
510477
- "blunTui.detach.listFailed": "Failed to list tasks: {error}",
510478
- "blunTui.detach.taskFailed": "Failed to detach {taskId}: {error}",
510479
- "blunTui.detach.finished.one": "Task already finished.",
510480
- "blunTui.detach.finished.other": "Tasks already finished.",
510481
- "blunTui.detach.moved.one": "Moved {count} task to background.",
510482
- "blunTui.detach.moved.other": "Moved {count} tasks to background.",
510483
- "blunTui.detach.partial": "Moved {detached} of {total} tasks to background.",
510484
- "blunTui.detach.viewSuffix": "/tasks to view.",
510485
- "blunTui.startup.flagsFailed": "Failed to apply startup flags: {error}",
510486
- "blunTui.notification.approvalRequired": "BLUN approval required",
510487
- "blunTui.notification.answerRequired": "BLUN needs your answer",
510488
- "blunTui.telegram.fallbackDelivered": "Reply delivered to Telegram automatically (fallback).",
510489
- "blunTui.telegram.attachDisabled": "Telegram attachment disabled by BLUN_TELEGRAM_ATTACH=off — headless mode active.",
510490
- "blunTui.telegram.noToken": "Telegram attachment: no token detected — headless mode active.",
510491
- "blunTui.telegram.attached": "Telegram channel attached (lease PID {pid}) — messages appear in this window.",
510492
- "blunTui.auto.status": "Auto: {label}",
510493
- "blunTui.activity.thinking": "{name} is thinking…",
510494
- "blunTui.activity.working": "{name} is working…",
510495
- "blunTui.activity.composing": "working...",
510496
- "blunTui.activity.tokens": "Tokens"
510497
- },
510498
- de: {
510499
- "blunTui.provider.modelAdded.one": "{providerName} · +{count} Modell.",
510500
- "blunTui.provider.modelAdded.other": "{providerName} · +{count} Modelle.",
510501
- "blunTui.provider.refreshSkipped": "Aktualisierung von {provider} übersprungen: {reason}",
510502
- "blunTui.warning": "Warnung: {warning}",
510503
- "blunTui.startup.sessionNotFound": "Sitzung „{sessionId}“ wurde nicht gefunden.",
510504
- "blunTui.startup.sessionDifferentDirectory": "Sitzung „{sessionId}“ wurde in einem anderen Arbeitsverzeichnis erstellt.",
510505
- "blunTui.startup.noSessionsToContinue": "Unter „{workDir}“ gibt es keine Sitzung zum Fortsetzen; eine neue Sitzung wird gestartet.",
510506
- "blunTui.startup.sessionNotInitialized": "Die Sitzung konnte beim Start nicht initialisiert werden.",
510507
- "blunTui.input.replayBlocked": "Während der Wiedergabe des Sitzungsverlaufs können keine Eingaben gesendet werden.",
510508
- "blunTui.shell.noSession": "Keine aktive Sitzung für den Shell-Befehl.",
510509
- "blunTui.shell.runFailed": "Shell-Befehl fehlgeschlagen: {error}",
510510
- "blunTui.shell.cancelFailed": "Shell-Befehl konnte nicht abgebrochen werden: {error}",
510511
- "blunTui.channel.steerFailed": "Die Kanalnachricht konnte nicht zur laufenden Antwort hinzugefügt werden: {error}",
510512
- "blunTui.session.sendFailed": "Senden fehlgeschlagen: {error}",
510513
- "blunTui.media.imageUnsupported": "Das aktuelle Modell unterstützt keine Bildeingaben.",
510514
- "blunTui.media.videoUnsupported": "Das aktuelle Modell unterstützt keine Videoeingaben.",
510515
- "blunTui.skill.failed": "Skill „{skillName}“ fehlgeschlagen: {error}",
510516
- "blunTui.pluginCommand.failed": "Befehl „{command}“ fehlgeschlagen: {error}",
510517
- "blunTui.steer.failed": "Nachsteuern fehlgeschlagen: {error}",
510518
- "blunTui.session.otherWorkDir": "Die aktuelle Sitzung befindet sich in einem anderen Arbeitsverzeichnis.",
510519
- "blunTui.session.resumeCommand": "Zum Fortsetzen ausführen: {command}",
510520
- "blunTui.clipboard.commandCopied": "Befehl in die Zwischenablage kopiert",
510521
- "blunTui.clipboard.commandCopyFailed": "Befehl konnte nicht in die Zwischenablage kopiert werden",
510522
- "blunTui.session.alreadyCurrent": "Diese Sitzung ist bereits aktiv.",
510523
- "blunTui.session.switchStreamingBlocked": "Während einer laufenden Antwort kann die Sitzung nicht gewechselt werden. Drücke zuerst Esc oder Ctrl-C.",
510524
- "blunTui.session.switchReplayBlocked": "Während der Wiedergabe des Sitzungsverlaufs kann die Sitzung nicht gewechselt werden.",
510525
- "blunTui.session.resumeFailed": "Sitzung {sessionId} konnte nicht fortgesetzt werden: {error}",
510526
- "blunTui.session.resumed": "Sitzung fortgesetzt ({sessionId}).",
510527
- "blunTui.session.replayFailed": "Der Sitzungsverlauf konnte nicht wiedergegeben werden: {error}",
510528
- "blunTui.session.createReplayBlocked": "Während der Wiedergabe des Sitzungsverlaufs kann keine neue Sitzung gestartet werden.",
510529
- "blunTui.session.createFailed": "Neue Sitzung konnte nicht gestartet werden: {error}",
510530
- "blunTui.session.postCreateFailed": "Die neue Sitzung konnte nicht eingerichtet werden: {error}",
510531
- "blunTui.session.started": "Neue Sitzung gestartet ({sessionId}).",
510532
- "blunTui.error": "Fehler: {message}",
510533
- "blunTui.login.title": "Bei BLUN anmelden",
510534
- "blunTui.login.hint": "Zum Abbrechen Ctrl-C drücken",
510535
- "blunTui.login.waiting": "Warten auf Autorisierung…",
510536
- "blunTui.detach.noShell": "Es wird kein Shell-Befehl ausgeführt.",
510537
- "blunTui.detach.shellStarting": "Der Befehl wird noch gestartet — versuche es erneut.",
510538
- "blunTui.detach.shellFinished": "Der Befehl ist bereits beendet.",
510539
- "blunTui.detach.moveFailed": "Verschieben in den Hintergrund fehlgeschlagen: {error}",
510540
- "blunTui.detach.movedTranscript": "In den Hintergrund verschoben.",
510541
- "blunTui.detach.movedView": "In den Hintergrund verschoben. Mit /tasks anzeigen.",
510542
- "blunTui.detach.noForeground": "Es wird keine Aufgabe im Vordergrund ausgeführt.",
510543
- "blunTui.detach.listFailed": "Aufgaben konnten nicht aufgelistet werden: {error}",
510544
- "blunTui.detach.taskFailed": "Aufgabe {taskId} konnte nicht in den Hintergrund verschoben werden: {error}",
510545
- "blunTui.detach.finished.one": "Aufgabe ist bereits beendet.",
510546
- "blunTui.detach.finished.other": "Aufgaben sind bereits beendet.",
510547
- "blunTui.detach.moved.one": "{count} Aufgabe in den Hintergrund verschoben.",
510548
- "blunTui.detach.moved.other": "{count} Aufgaben in den Hintergrund verschoben.",
510549
- "blunTui.detach.partial": "{detached} von {total} Aufgaben in den Hintergrund verschoben.",
510550
- "blunTui.detach.viewSuffix": "Mit /tasks anzeigen.",
510551
- "blunTui.startup.flagsFailed": "Startoptionen konnten nicht angewendet werden: {error}",
510552
- "blunTui.notification.approvalRequired": "BLUN-Genehmigung erforderlich",
510553
- "blunTui.notification.answerRequired": "BLUN benötigt deine Antwort",
510554
- "blunTui.telegram.fallbackDelivered": "Antwort automatisch nach Telegram zugestellt (Fallback).",
510555
- "blunTui.telegram.attachDisabled": "Telegram-Anbindung durch BLUN_TELEGRAM_ATTACH=off deaktiviert — Headless-Modus aktiv.",
510556
- "blunTui.telegram.noToken": "Telegram-Anbindung: kein Token erkannt — Headless-Modus aktiv.",
510557
- "blunTui.telegram.attached": "Telegram-Kanal angebunden (Lease-PID {pid}) — Nachrichten erscheinen in diesem Fenster.",
510558
- "blunTui.auto.status": "Auto: {label}",
510559
- "blunTui.activity.thinking": "{name} denkt…",
510560
- "blunTui.activity.working": "{name} arbeitet…",
510561
- "blunTui.activity.composing": "arbeitet...",
510562
- "blunTui.activity.tokens": "Token"
510563
- },
510564
- es: {
510565
- "blunTui.provider.modelAdded.one": "{providerName} · +{count} modelo.",
510566
- "blunTui.provider.modelAdded.other": "{providerName} · +{count} modelos.",
510567
- "blunTui.provider.refreshSkipped": "Se omitió la actualización de {provider}: {reason}",
510568
- "blunTui.warning": "Advertencia: {warning}",
510569
- "blunTui.startup.sessionNotFound": "No se encontró la sesión «{sessionId}».",
510570
- "blunTui.startup.sessionDifferentDirectory": "La sesión «{sessionId}» se creó en otro directorio de trabajo.",
510571
- "blunTui.startup.noSessionsToContinue": "No hay sesiones que reanudar en «{workDir}»; se iniciará una sesión nueva.",
510572
- "blunTui.startup.sessionNotInitialized": "No se pudo inicializar la sesión durante el arranque.",
510573
- "blunTui.input.replayBlocked": "No se puede enviar ninguna entrada mientras se reproduce el historial de la sesión.",
510574
- "blunTui.shell.noSession": "No hay ninguna sesión activa para el comando de shell.",
510575
- "blunTui.shell.runFailed": "El comando de shell falló: {error}",
510576
- "blunTui.shell.cancelFailed": "No se pudo cancelar el comando de shell: {error}",
510577
- "blunTui.channel.steerFailed": "No se pudo añadir el mensaje del canal a la respuesta en curso: {error}",
510578
- "blunTui.session.sendFailed": "No se pudo enviar: {error}",
510579
- "blunTui.media.imageUnsupported": "El modelo actual no admite entradas de imagen.",
510580
- "blunTui.media.videoUnsupported": "El modelo actual no admite entradas de vídeo.",
510581
- "blunTui.skill.failed": "El skill «{skillName}» falló: {error}",
510582
- "blunTui.pluginCommand.failed": "El comando «{command}» falló: {error}",
510583
- "blunTui.steer.failed": "No se pudo reorientar la respuesta: {error}",
510584
- "blunTui.session.otherWorkDir": "La sesión actual se encuentra en otro directorio de trabajo.",
510585
- "blunTui.session.resumeCommand": "Para reanudarla, ejecuta: {command}",
510586
- "blunTui.clipboard.commandCopied": "Comando copiado al portapapeles",
510587
- "blunTui.clipboard.commandCopyFailed": "No se pudo copiar el comando al portapapeles",
510588
- "blunTui.session.alreadyCurrent": "Esta sesión ya está activa.",
510589
- "blunTui.session.switchStreamingBlocked": "No se puede cambiar de sesión mientras se genera una respuesta. Pulsa primero Esc o Ctrl-C.",
510590
- "blunTui.session.switchReplayBlocked": "No se puede cambiar de sesión mientras se reproduce el historial.",
510591
- "blunTui.session.resumeFailed": "No se pudo reanudar la sesión {sessionId}: {error}",
510592
- "blunTui.session.resumed": "Sesión reanudada ({sessionId}).",
510593
- "blunTui.session.replayFailed": "No se pudo reproducir el historial de la sesión: {error}",
510594
- "blunTui.session.createReplayBlocked": "No se puede iniciar una sesión nueva mientras se reproduce el historial.",
510595
- "blunTui.session.createFailed": "No se pudo iniciar una sesión nueva: {error}",
510596
- "blunTui.session.postCreateFailed": "No se pudo configurar la sesión recién creada: {error}",
510597
- "blunTui.session.started": "Se inició una sesión nueva ({sessionId}).",
510598
- "blunTui.error": "Error: {message}",
510599
- "blunTui.login.title": "Iniciar sesión en BLUN",
510600
- "blunTui.login.hint": "Pulsa Ctrl-C para cancelar",
510601
- "blunTui.login.waiting": "Esperando autorización…",
510602
- "blunTui.detach.noShell": "No hay ningún comando de shell en ejecución.",
510603
- "blunTui.detach.shellStarting": "El comando todavía se está iniciando; inténtalo de nuevo.",
510604
- "blunTui.detach.shellFinished": "El comando ya ha finalizado.",
510605
- "blunTui.detach.moveFailed": "No se pudo mover a segundo plano: {error}",
510606
- "blunTui.detach.movedTranscript": "Se movió a segundo plano.",
510607
- "blunTui.detach.movedView": "Se movió a segundo plano. Consulta /tasks.",
510608
- "blunTui.detach.noForeground": "No hay ninguna tarea en ejecución en primer plano.",
510609
- "blunTui.detach.listFailed": "No se pudieron obtener las tareas: {error}",
510610
- "blunTui.detach.taskFailed": "No se pudo mover la tarea {taskId} a segundo plano: {error}",
510611
- "blunTui.detach.finished.one": "La tarea ya ha finalizado.",
510612
- "blunTui.detach.finished.other": "Las tareas ya han finalizado.",
510613
- "blunTui.detach.moved.one": "Se ha movido {count} tarea a segundo plano.",
510614
- "blunTui.detach.moved.other": "Se han movido {count} tareas a segundo plano.",
510615
- "blunTui.detach.partial": "Se han movido {detached} de {total} tareas a segundo plano.",
510616
- "blunTui.detach.viewSuffix": "Consulta /tasks.",
510617
- "blunTui.startup.flagsFailed": "No se pudieron aplicar las opciones de inicio: {error}",
510618
- "blunTui.notification.approvalRequired": "Se requiere aprobación de BLUN",
510619
- "blunTui.notification.answerRequired": "BLUN necesita tu respuesta",
510620
- "blunTui.telegram.fallbackDelivered": "La respuesta se envió automáticamente a Telegram (modo alternativo).",
510621
- "blunTui.telegram.attachDisabled": "Conexión con Telegram desactivada mediante BLUN_TELEGRAM_ATTACH=off — modo headless activo.",
510622
- "blunTui.telegram.noToken": "Conexión con Telegram: no se detectó ningún token — modo headless activo.",
510623
- "blunTui.telegram.attached": "Canal de Telegram conectado (PID de lease {pid}) — los mensajes aparecen en esta ventana.",
510624
- "blunTui.auto.status": "Automático: {label}",
510625
- "blunTui.activity.thinking": "{name} está pensando…",
510626
- "blunTui.activity.working": "{name} está trabajando…",
510627
- "blunTui.activity.composing": "trabajando...",
510628
- "blunTui.activity.tokens": "tokens"
510629
- },
510630
- fr: {
510631
- "blunTui.provider.modelAdded.one": "{providerName} · +{count} modèle.",
510632
- "blunTui.provider.modelAdded.other": "{providerName} · +{count} modèles.",
510633
- "blunTui.provider.refreshSkipped": "Actualisation de {provider} ignorée : {reason}",
510634
- "blunTui.warning": "Avertissement : {warning}",
510635
- "blunTui.startup.sessionNotFound": "Session « {sessionId} » introuvable.",
510636
- "blunTui.startup.sessionDifferentDirectory": "La session « {sessionId} » a été créée dans un autre répertoire de travail.",
510637
- "blunTui.startup.noSessionsToContinue": "Aucune session à reprendre dans « {workDir} » ; démarrage d’une nouvelle session.",
510638
- "blunTui.startup.sessionNotInitialized": "La session de démarrage n’a pas été initialisée.",
510639
- "blunTui.input.replayBlocked": "Impossible d’envoyer une saisie pendant la relecture de l’historique de la session.",
510640
- "blunTui.shell.noSession": "Aucune session active pour la commande shell.",
510641
- "blunTui.shell.runFailed": "Échec de la commande shell : {error}",
510642
- "blunTui.shell.cancelFailed": "Impossible d’annuler la commande shell : {error}",
510643
- "blunTui.channel.steerFailed": "Impossible d’ajouter le message du canal à la réponse en cours : {error}",
510644
- "blunTui.session.sendFailed": "Échec de l’envoi : {error}",
510645
- "blunTui.media.imageUnsupported": "Le modèle actuel ne prend pas en charge les images en entrée.",
510646
- "blunTui.media.videoUnsupported": "Le modèle actuel ne prend pas en charge les vidéos en entrée.",
510647
- "blunTui.skill.failed": "Échec du skill « {skillName} » : {error}",
510648
- "blunTui.pluginCommand.failed": "Échec de la commande « {command} » : {error}",
510649
- "blunTui.steer.failed": "Impossible de réorienter la réponse : {error}",
510650
- "blunTui.session.otherWorkDir": "La session actuelle se trouve dans un autre répertoire de travail.",
510651
- "blunTui.session.resumeCommand": "Pour la reprendre, exécutez : {command}",
510652
- "blunTui.clipboard.commandCopied": "Commande copiée dans le presse-papiers",
510653
- "blunTui.clipboard.commandCopyFailed": "Impossible de copier la commande dans le presse-papiers",
510654
- "blunTui.session.alreadyCurrent": "Cette session est déjà active.",
510655
- "blunTui.session.switchStreamingBlocked": "Impossible de changer de session pendant la génération d’une réponse. Appuyez d’abord sur Esc ou Ctrl-C.",
510656
- "blunTui.session.switchReplayBlocked": "Impossible de changer de session pendant la relecture de l’historique.",
510657
- "blunTui.session.resumeFailed": "Impossible de reprendre la session {sessionId} : {error}",
510658
- "blunTui.session.resumed": "Session reprise ({sessionId}).",
510659
- "blunTui.session.replayFailed": "Impossible de relire l’historique de la session : {error}",
510660
- "blunTui.session.createReplayBlocked": "Impossible de démarrer une nouvelle session pendant la relecture de l’historique.",
510661
- "blunTui.session.createFailed": "Impossible de démarrer une nouvelle session : {error}",
510662
- "blunTui.session.postCreateFailed": "Impossible de configurer la nouvelle session : {error}",
510663
- "blunTui.session.started": "Nouvelle session démarrée ({sessionId}).",
510664
- "blunTui.error": "Erreur : {message}",
510665
- "blunTui.login.title": "Se connecter à BLUN",
510666
- "blunTui.login.hint": "Appuyez sur Ctrl-C pour annuler",
510667
- "blunTui.login.waiting": "En attente de l’autorisation…",
510668
- "blunTui.detach.noShell": "Aucune commande shell en cours.",
510669
- "blunTui.detach.shellStarting": "La commande est encore en cours de démarrage — réessayez.",
510670
- "blunTui.detach.shellFinished": "La commande est déjà terminée.",
510671
- "blunTui.detach.moveFailed": "Impossible de passer la commande en arrière-plan : {error}",
510672
- "blunTui.detach.movedTranscript": "Commande passée en arrière-plan.",
510673
- "blunTui.detach.movedView": "Commande passée en arrière-plan. Consultez /tasks.",
510674
- "blunTui.detach.noForeground": "Aucune tâche en cours au premier plan.",
510675
- "blunTui.detach.listFailed": "Impossible de répertorier les tâches : {error}",
510676
- "blunTui.detach.taskFailed": "Impossible de passer la tâche {taskId} en arrière-plan : {error}",
510677
- "blunTui.detach.finished.one": "La tâche est déjà terminée.",
510678
- "blunTui.detach.finished.other": "Les tâches sont déjà terminées.",
510679
- "blunTui.detach.moved.one": "{count} tâche passée en arrière-plan.",
510680
- "blunTui.detach.moved.other": "{count} tâches passées en arrière-plan.",
510681
- "blunTui.detach.partial": "{detached} tâches sur {total} passées en arrière-plan.",
510682
- "blunTui.detach.viewSuffix": "Consultez /tasks.",
510683
- "blunTui.startup.flagsFailed": "Impossible d’appliquer les options de démarrage : {error}",
510684
- "blunTui.notification.approvalRequired": "Approbation BLUN requise",
510685
- "blunTui.notification.answerRequired": "BLUN attend votre réponse",
510686
- "blunTui.telegram.fallbackDelivered": "Réponse envoyée automatiquement sur Telegram (solution de secours).",
510687
- "blunTui.telegram.attachDisabled": "Connexion à Telegram désactivée via BLUN_TELEGRAM_ATTACH=off — mode headless actif.",
510688
- "blunTui.telegram.noToken": "Connexion à Telegram\xA0: aucun jeton détecté — mode headless actif.",
510689
- "blunTui.telegram.attached": "Canal Telegram connecté (PID de lease\xA0: {pid}) — les messages apparaissent dans cette fenêtre.",
510690
- "blunTui.auto.status": "Auto\xA0: {label}",
510691
- "blunTui.activity.thinking": "{name} réfléchit…",
510692
- "blunTui.activity.working": "{name} travaille…",
510693
- "blunTui.activity.composing": "travail en cours...",
510694
- "blunTui.activity.tokens": "jetons"
510695
- },
510696
- sv: {
510697
- "blunTui.provider.modelAdded.one": "{providerName} · +{count} modell.",
510698
- "blunTui.provider.modelAdded.other": "{providerName} · +{count} modeller.",
510699
- "blunTui.provider.refreshSkipped": "Uppdateringen av {provider} hoppades över: {reason}",
510700
- "blunTui.warning": "Varning: {warning}",
510701
- "blunTui.startup.sessionNotFound": "Sessionen ”{sessionId}” hittades inte.",
510702
- "blunTui.startup.sessionDifferentDirectory": "Sessionen ”{sessionId}” skapades i en annan arbetskatalog.",
510703
- "blunTui.startup.noSessionsToContinue": "Det finns inga sessioner att återuppta i ”{workDir}”; en ny session startas.",
510704
- "blunTui.startup.sessionNotInitialized": "Sessionen initierades inte vid start.",
510705
- "blunTui.input.replayBlocked": "Det går inte att skicka indata medan sessionshistoriken spelas upp.",
510706
- "blunTui.shell.noSession": "Det finns ingen aktiv session för skalkommandot.",
510707
- "blunTui.shell.runFailed": "Skalkommandot misslyckades: {error}",
510708
- "blunTui.shell.cancelFailed": "Det gick inte att avbryta skalkommandot: {error}",
510709
- "blunTui.channel.steerFailed": "Det gick inte att lägga till kanalmeddelandet i det pågående svaret: {error}",
510710
- "blunTui.session.sendFailed": "Det gick inte att skicka: {error}",
510711
- "blunTui.media.imageUnsupported": "Den aktuella modellen stöder inte bildindata.",
510712
- "blunTui.media.videoUnsupported": "Den aktuella modellen stöder inte videoindata.",
510713
- "blunTui.skill.failed": "Skill ”{skillName}” misslyckades: {error}",
510714
- "blunTui.pluginCommand.failed": "Kommandot ”{command}” misslyckades: {error}",
510715
- "blunTui.steer.failed": "Det gick inte att styra om svaret: {error}",
510716
- "blunTui.session.otherWorkDir": "Den aktuella sessionen finns i en annan arbetskatalog.",
510717
- "blunTui.session.resumeCommand": "Kör följande för att återuppta den: {command}",
510718
- "blunTui.clipboard.commandCopied": "Kommandot kopierades till urklipp",
510719
- "blunTui.clipboard.commandCopyFailed": "Det gick inte att kopiera kommandot till urklipp",
510720
- "blunTui.session.alreadyCurrent": "Den här sessionen är redan aktiv.",
510721
- "blunTui.session.switchStreamingBlocked": "Det går inte att byta session medan ett svar genereras. Tryck först på Esc eller Ctrl-C.",
510722
- "blunTui.session.switchReplayBlocked": "Det går inte att byta session medan historiken spelas upp.",
510723
- "blunTui.session.resumeFailed": "Det gick inte att återuppta sessionen {sessionId}: {error}",
510724
- "blunTui.session.resumed": "Sessionen återupptogs ({sessionId}).",
510725
- "blunTui.session.replayFailed": "Det gick inte att spela upp sessionshistoriken: {error}",
510726
- "blunTui.session.createReplayBlocked": "Det går inte att starta en ny session medan historiken spelas upp.",
510727
- "blunTui.session.createFailed": "Det gick inte att starta en ny session: {error}",
510728
- "blunTui.session.postCreateFailed": "Det gick inte att konfigurera den nya sessionen: {error}",
510729
- "blunTui.session.started": "En ny session startades ({sessionId}).",
510730
- "blunTui.error": "Fel: {message}",
510731
- "blunTui.login.title": "Logga in på BLUN",
510732
- "blunTui.login.hint": "Tryck på Ctrl-C för att avbryta",
510733
- "blunTui.login.waiting": "Väntar på auktorisering…",
510734
- "blunTui.detach.noShell": "Inget skalkommando körs.",
510735
- "blunTui.detach.shellStarting": "Kommandot håller fortfarande på att startas – försök igen.",
510736
- "blunTui.detach.shellFinished": "Kommandot är redan slutfört.",
510737
- "blunTui.detach.moveFailed": "Det gick inte att flytta kommandot till bakgrunden: {error}",
510738
- "blunTui.detach.movedTranscript": "Flyttades till bakgrunden.",
510739
- "blunTui.detach.movedView": "Flyttades till bakgrunden. Visa med /tasks.",
510740
- "blunTui.detach.noForeground": "Ingen uppgift körs i förgrunden.",
510741
- "blunTui.detach.listFailed": "Det gick inte att lista uppgifterna: {error}",
510742
- "blunTui.detach.taskFailed": "Det gick inte att flytta uppgiften {taskId} till bakgrunden: {error}",
510743
- "blunTui.detach.finished.one": "Uppgiften är redan slutförd.",
510744
- "blunTui.detach.finished.other": "Uppgifterna är redan slutförda.",
510745
- "blunTui.detach.moved.one": "{count} uppgift flyttades till bakgrunden.",
510746
- "blunTui.detach.moved.other": "{count} uppgifter flyttades till bakgrunden.",
510747
- "blunTui.detach.partial": "{detached} av {total} uppgifter flyttades till bakgrunden.",
510748
- "blunTui.detach.viewSuffix": "Visa med /tasks.",
510749
- "blunTui.startup.flagsFailed": "Det gick inte att tillämpa startalternativen: {error}",
510750
- "blunTui.notification.approvalRequired": "BLUN-godkännande krävs",
510751
- "blunTui.notification.answerRequired": "BLUN behöver ditt svar",
510752
- "blunTui.telegram.fallbackDelivered": "Svaret skickades automatiskt till Telegram (reservlösning).",
510753
- "blunTui.telegram.attachDisabled": "Telegram-anslutningen inaktiverades via BLUN_TELEGRAM_ATTACH=off — headless-läget är aktivt.",
510754
- "blunTui.telegram.noToken": "Telegram-anslutning: ingen token hittades — headless-läget är aktivt.",
510755
- "blunTui.telegram.attached": "Telegram-kanalen är ansluten (lease-PID {pid}) — meddelanden visas i det här fönstret.",
510756
- "blunTui.auto.status": "Automatiskt: {label}",
510757
- "blunTui.activity.thinking": "{name} tänker…",
510758
- "blunTui.activity.working": "{name} arbetar…",
510759
- "blunTui.activity.composing": "arbetar...",
510760
- "blunTui.activity.tokens": "token"
510761
- },
510762
- cs: {
510763
- "blunTui.provider.modelAdded.one": "{providerName} · +{count} model.",
510764
- "blunTui.provider.modelAdded.other": "{providerName} · nové modely: +{count}.",
510765
- "blunTui.provider.refreshSkipped": "Přeskočeno obnovení {provider}: {reason}",
510766
- "blunTui.warning": "Upozornění: {warning}",
510767
- "blunTui.startup.sessionNotFound": "Relace \"{sessionId}\" nebyla nalezena.",
510768
- "blunTui.startup.sessionDifferentDirectory": "Relace \"{sessionId}\" byla vytvořena v jiném adresáři.",
510769
- "blunTui.startup.noSessionsToContinue": "V adresáři \"{workDir}\" nejsou žádné relace, ve kterých by bylo možné pokračovat; spouští se nová relace.",
510770
- "blunTui.startup.sessionNotInitialized": "Relace při spuštění nebyla inicializována.",
510771
- "blunTui.input.replayBlocked": "Nelze odeslat vstup během přehrávání historie relace.",
510772
- "blunTui.shell.noSession": "Žádná aktivní relace pro příkaz shellu.",
510773
- "blunTui.shell.runFailed": "Příkaz shellu selhal: {error}",
510774
- "blunTui.shell.cancelFailed": "Selhalo zrušení příkazu shellu: {error}",
510775
- "blunTui.channel.steerFailed": "Předání zprávy z kanálu do probíhající odpovědi selhalo: {error}",
510776
- "blunTui.session.sendFailed": "Selhalo odeslání: {error}",
510777
- "blunTui.media.imageUnsupported": "Aktuální model nepodporuje vstup obrázku.",
510778
- "blunTui.media.videoUnsupported": "Aktuální model nepodporuje vstup videa.",
510779
- "blunTui.skill.failed": "Dovednost \"{skillName}\" selhala: {error}",
510780
- "blunTui.pluginCommand.failed": "Příkaz \"{command}\" selhal: {error}",
510781
- "blunTui.steer.failed": "Doplnění pokynu selhalo: {error}",
510782
- "blunTui.session.otherWorkDir": "Aktuální relace je v jiném pracovním adresáři.",
510783
- "blunTui.session.resumeCommand": "Chcete-li pokračovat, spusťte: {command}",
510784
- "blunTui.clipboard.commandCopied": "Příkaz zkopírován do schránky",
510785
- "blunTui.clipboard.commandCopyFailed": "Selhalo kopírování příkazu do schránky",
510786
- "blunTui.session.alreadyCurrent": "Již jste v této relaci.",
510787
- "blunTui.session.switchStreamingBlocked": "Nelze přepínat relace během streamování — nejdříve stiskněte Esc nebo Ctrl-C.",
510788
- "blunTui.session.switchReplayBlocked": "Nelze přepínat relace během přehrávání historie.",
510789
- "blunTui.session.resumeFailed": "Selhalo obnovení relace {sessionId}: {error}",
510790
- "blunTui.session.resumed": "Obnovena relace ({sessionId}).",
510791
- "blunTui.session.replayFailed": "Selhalo přehrávání historie relace: {error}",
510792
- "blunTui.session.createReplayBlocked": "Nelze spustit novou relaci během přehrávání historie.",
510793
- "blunTui.session.createFailed": "Selhalo spuštění nové relace: {error}",
510794
- "blunTui.session.postCreateFailed": "Selhalo nastavení po vytvoření: {error}",
510795
- "blunTui.session.started": "Spuštěna nová relace ({sessionId}).",
510796
- "blunTui.error": "Chyba: {message}",
510797
- "blunTui.login.title": "Přihlaste se do BLUN",
510798
- "blunTui.login.hint": "Stiskněte Ctrl-C pro zrušení",
510799
- "blunTui.login.waiting": "Čekání na autorizaci…",
510800
- "blunTui.detach.noShell": "Žádný příkaz shellu není spuštěn.",
510801
- "blunTui.detach.shellStarting": "Příkaz se stále spouští — zkuste znovu.",
510802
- "blunTui.detach.shellFinished": "Příkaz již skončil.",
510803
- "blunTui.detach.moveFailed": "Selhalo přesunutí na pozadí: {error}",
510804
- "blunTui.detach.movedTranscript": "Přesunuto na pozadí.",
510805
- "blunTui.detach.movedView": "Přesunuto na pozadí. Zobrazíte příkazem /tasks.",
510806
- "blunTui.detach.noForeground": "Žádný úkol na popředí není spuštěn.",
510807
- "blunTui.detach.listFailed": "Selhalo vypsání úkolů: {error}",
510808
- "blunTui.detach.taskFailed": "Přesunutí úlohy {taskId} na pozadí selhalo: {error}",
510809
- "blunTui.detach.finished.one": "Úkol již skončil.",
510810
- "blunTui.detach.finished.other": "Úkoly již skončily.",
510811
- "blunTui.detach.moved.one": "Přesunut {count} úkol na pozadí.",
510812
- "blunTui.detach.moved.other": "Úkoly přesunuté na pozadí: {count}.",
510813
- "blunTui.detach.partial": "Přesunuto {detached} z {total} úkolů na pozadí.",
510814
- "blunTui.detach.viewSuffix": "/tasks k zobrazení.",
510815
- "blunTui.startup.flagsFailed": "Nepodařilo se použít spouštěcí příznaky: {error}",
510816
- "blunTui.notification.approvalRequired": "Vyžadováno schválení BLUN",
510817
- "blunTui.notification.answerRequired": "BLUN potřebuje vaši odpověď",
510818
- "blunTui.telegram.fallbackDelivered": "Odpověď byla automaticky doručena do Telegramu (náhradním způsobem).",
510819
- "blunTui.telegram.attachDisabled": "Připojení Telegramu je zakázáno nastavením BLUN_TELEGRAM_ATTACH=off — aktivní je režim bez uživatelského rozhraní.",
510820
- "blunTui.telegram.noToken": "Připojení Telegramu: nebyl nalezen žádný token — aktivní je režim bez uživatelského rozhraní.",
510821
- "blunTui.telegram.attached": "Kanál Telegramu je připojen (PID držitele připojení {pid}) — zprávy se zobrazují v tomto okně.",
510822
- "blunTui.auto.status": "Automaticky: {label}",
510823
- "blunTui.activity.thinking": "{name} přemýšlí…",
510824
- "blunTui.activity.working": "{name} pracuje…",
510825
- "blunTui.activity.composing": "pracuje…",
510826
- "blunTui.activity.tokens": "Tokeny"
510827
- }
510828
- });
510829
- //#endregion
510830
511346
  //#region src/tui/blun-tui.ts
510831
511347
  function loadingTipKind(mode) {
510832
511348
  if (mode === "waiting" || mode === "tool") return "blun";
@@ -512001,6 +512517,21 @@ var BlunTUI = class {
512001
512517
  /** Pending delivery guard for the channel-origin turn currently running. */
512002
512518
  pendingChannelReplyGuard;
512003
512519
  /** See SessionEventHost.runChannelReplyFallback — called at turn end. */
512520
+ channelMediaDeliveries = /* @__PURE__ */ new Set();
512521
+ /** Deliver completed media at tool-result time so later queued work cannot hide it. */
512522
+ runChannelMediaFallback(output) {
512523
+ const guard = this.pendingChannelReplyGuard;
512524
+ const filePath = completedMediaLocalPath(output);
512525
+ if (guard === void 0 || filePath === void 0) return;
512526
+ const deliveryKey = `${guard.chatId}\0${filePath}`;
512527
+ if (this.channelMediaDeliveries.has(deliveryKey)) return;
512528
+ this.channelMediaDeliveries.add(deliveryKey);
512529
+ sendMediaReplyFallback(guard.chatId, filePath).then((sent) => {
512530
+ if (sent) return;
512531
+ this.channelMediaDeliveries.delete(deliveryKey);
512532
+ this.showError(uiText("blunTui.session.sendFailed", { error: "Telegram media" }));
512533
+ });
512534
+ }
512004
512535
  runChannelReplyFallback(reason) {
512005
512536
  const guard = this.pendingChannelReplyGuard;
512006
512537
  if (guard === void 0) return;