blun-king-cli 9.1.49 → 9.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/blun.mjs CHANGED
@@ -30172,7 +30172,8 @@ function parseEnvBudget(raw) {
30172
30172
  */
30173
30173
  function computeCompletionBudgetCap(args) {
30174
30174
  const maxCtx = args.capability?.max_context_tokens ?? 0;
30175
- const cap = args.budget.hardCap ?? (maxCtx > 0 ? maxCtx : args.budget.fallback ?? DEFAULT_UNKNOWN_CONTEXT_FALLBACK);
30175
+ const fallback = args.budget.fallback ?? DEFAULT_UNKNOWN_CONTEXT_FALLBACK;
30176
+ const cap = args.budget.hardCap ?? (maxCtx > 0 ? Math.min(maxCtx, fallback) : fallback);
30176
30177
  return Math.max(MIN_FLOOR, cap);
30177
30178
  }
30178
30179
  /**
@@ -30197,7 +30198,7 @@ function applyCompletionBudgetWithDetails(args) {
30197
30198
  });
30198
30199
  if (args.retry !== void 0) cap = Math.max(args.retry.minimumCompletionTokens, Math.ceil(cap * args.retry.multiplier));
30199
30200
  const maxContextTokens = args.capability?.max_context_tokens;
30200
- if (args.usedContextTokens !== void 0 && maxContextTokens !== void 0 && maxContextTokens > 0) cap = Math.max(MIN_FLOOR, Math.min(cap, maxContextTokens - args.usedContextTokens));
30201
+ if (args.usedContextTokens !== void 0 && maxContextTokens !== void 0 && maxContextTokens > 0) cap = Math.max(MIN_FLOOR, Math.min(cap, maxContextTokens - args.usedContextTokens - COMPLETION_CONTEXT_SAFETY_MARGIN));
30201
30202
  return {
30202
30203
  provider: args.provider.withMaxCompletionTokens(cap, {
30203
30204
  usedContextTokens: args.usedContextTokens,
@@ -30206,11 +30207,12 @@ function applyCompletionBudgetWithDetails(args) {
30206
30207
  maxCompletionTokens: cap
30207
30208
  };
30208
30209
  }
30209
- var MIN_FLOOR, DEFAULT_UNKNOWN_CONTEXT_FALLBACK, MIN_THINKING_COMPLETION_TOKENS;
30210
+ var MIN_FLOOR, DEFAULT_UNKNOWN_CONTEXT_FALLBACK, MIN_THINKING_COMPLETION_TOKENS, COMPLETION_CONTEXT_SAFETY_MARGIN;
30210
30211
  var init_completion_budget = __esmMin((() => {
30211
30212
  MIN_FLOOR = 1;
30212
30213
  DEFAULT_UNKNOWN_CONTEXT_FALLBACK = 32e3;
30213
30214
  MIN_THINKING_COMPLETION_TOKENS = 1024;
30215
+ COMPLETION_CONTEXT_SAFETY_MARGIN = 1e4;
30214
30216
  }));
30215
30217
  //#endregion
30216
30218
  //#region ../../packages/agent-core/src/loop/retry.ts
@@ -30390,7 +30392,7 @@ var init_input_schema = __esmMin((() => {
30390
30392
  //#region ../../packages/agent-core/src/tools/builtin/state/todo-list.md?raw
30391
30393
  var todo_list_default;
30392
30394
  var init_todo_list$1 = __esmMin((() => {
30393
- todo_list_default = "Use this tool to maintain a structured TODO list as you work through a multi-step task. Use it proactively and often when progress tracking helps the current work. This is especially useful in long-running investigations and implementation tasks with several tool calls; in plan mode, write the plan to the plan file rather than tracking it here.\n\n**When to use:**\n- Multi-step tasks that span several tool calls\n- Tracking investigation progress across a large codebase search\n- Planning a sequence of edits before making them\n- After receiving new multi-step instructions, capture the requirements as todos\n- Before starting a tracked task, mark exactly one item as `in_progress`\n- Immediately after finishing a tracked task, mark it `done`; do not batch completions at the end\n\n**When NOT to use:**\n- Single-shot answers that complete in one or two tool calls\n- Trivial requests where tracking adds no clarity\n- Purely conversational or informational replies\n\n**Avoid churn:**\n- Do not re-call this tool when nothing meaningful has changed since the last call — update the list only after real progress.\n- When unsure of the current state, call query mode first (omit `todos`) to check the list before deciding what to update.\n- If no available tool can move any task forward, tell the user where you are stuck instead of repeatedly re-ordering the same todos.\n\n**How to use:**\n- Call with `todos: [...]` to replace the full list. Statuses: pending / in_progress / done.\n- Call with no `todos` argument to retrieve the current list without changing it.\n- Call with `todos: []` to clear the list.\n- Keep titles short and actionable (e.g. \"Read session-control.ts\", \"Add planMode flag to TurnManager\").\n- Update statuses as you make progress.\n- When work is underway, keep exactly one task `in_progress`.\n- Only mark a task `done` when it is fully accomplished.\n- Never mark a task `done` if tests are failing, implementation is partial, unresolved errors remain, or required files/dependencies could not be found.\n- If you encounter a blocker, keep the blocked task `in_progress` or add a new pending task describing what must be resolved.\n";
30395
+ todo_list_default = "Use this tool to maintain a structured TODO list as you work through a multi-step task. Use it proactively and often when progress tracking helps the current work. This is especially useful in long-running investigations and implementation tasks with several tool calls; in plan mode, write the plan to the plan file rather than tracking it here.\n\n**When to use:**\n- Multi-step tasks that span several tool calls\n- Tracking investigation progress across a large codebase search\n- Planning a sequence of edits before making them\n- After receiving new multi-step instructions, capture the requirements as todos\n- Before starting a tracked task, mark exactly one item as `in_progress`\n- Immediately after finishing a tracked task, record its truthful terminal state; do not batch completions at the end\n\n**When NOT to use:**\n- Single-shot answers that complete in one or two tool calls\n- Trivial requests where tracking adds no clarity\n- Purely conversational or informational replies\n\n**Avoid churn:**\n- Do not re-call this tool when nothing meaningful has changed since the last call — update the list only after real progress.\n- When unsure of the current state, call query mode first (omit `todos`) to check the list before deciding what to update.\n- If no available tool can move any task forward, tell the user where you are stuck instead of repeatedly re-ordering the same todos.\n\n**How to use:**\n- Call with `todos: [...]` to replace the full list. Statuses: pending / in_progress / done / blocked / waiting_approval / aborted.\n- Call with no `todos` argument to retrieve the current list without changing it.\n- Call with `todos: []` to clear the list.\n- Keep titles short and actionable (e.g. \"Read session-control.ts\", \"Add planMode flag to TurnManager\").\n- Update statuses as you make progress.\n- When work is underway, keep exactly one task `in_progress`.\n- Use `done` only when fully accomplished. Use `blocked` with the reason and the action that would unblock it, `waiting_approval` with the prepared result that awaits approval, and `aborted` with the last reached state.\n- Never mark a task `done` if tests are failing, implementation is partial, unresolved errors remain, or required files/dependencies could not be found.\n";
30394
30396
  }));
30395
30397
  //#endregion
30396
30398
  //#region ../../packages/agent-core/src/tools/builtin/state/todo-list.ts
@@ -30405,6 +30407,9 @@ function statusMarker(status) {
30405
30407
  case "pending": return "[pending]";
30406
30408
  case "in_progress": return "[in_progress]";
30407
30409
  case "done": return "[done]";
30410
+ case "blocked": return "[blocked]";
30411
+ case "waiting_approval": return "[waiting_approval]";
30412
+ case "aborted": return "[aborted]";
30408
30413
  default: return status;
30409
30414
  }
30410
30415
  }
@@ -30415,13 +30420,16 @@ var init_todo_list = __esmMin((() => {
30415
30420
  init_todo_list$1();
30416
30421
  TODO_LIST_TOOL_NAME = "TodoList";
30417
30422
  TODO_STORE_KEY = "todo";
30418
- TODO_LIST_WRITE_REMINDER = "Keep the task list aligned with the work. Mark finished items done promptly, keep one item in_progress while work is active, and clear items that no longer apply.";
30423
+ TODO_LIST_WRITE_REMINDER = "Keep the task list aligned with the work. Record a truthful terminal status for every finished item, keep one item in_progress while work is active, and clear items that no longer apply.";
30419
30424
  TodoItemSchema = object({
30420
30425
  title: string().min(1).describe("Short, actionable title for the todo."),
30421
30426
  status: _enum([
30422
30427
  "pending",
30423
30428
  "in_progress",
30424
- "done"
30429
+ "done",
30430
+ "blocked",
30431
+ "waiting_approval",
30432
+ "aborted"
30425
30433
  ]).describe("Current status of the todo.")
30426
30434
  });
30427
30435
  TodoListInputSchema = object({ todos: array(TodoItemSchema).optional().describe("The updated todo list. Omit to read the current todo list without making changes. Pass an empty array to clear the list.") });
@@ -76040,6 +76048,16 @@ function renderCronFireXml(origin, prompt) {
76040
76048
  "</cron-fire>"
76041
76049
  ].join("\n");
76042
76050
  }
76051
+ function renderSessionLoopWakeupXml(origin, prompt, interval) {
76052
+ return [
76053
+ `<session-loop-wakeup interval="${stringAttr(interval, "auto")}">`,
76054
+ "This is an autonomous scheduled wake-up, not a new user message.",
76055
+ "Resume responsibility for the recurring assignment below. Inspect the current state, research or act with the available tools, and make concrete progress independently.",
76056
+ "Do not ask the user what to do next unless a real decision, permission, or access blocker prevents progress. If no action is possible, report exactly what you checked and what condition you are waiting for.",
76057
+ "</session-loop-wakeup>",
76058
+ renderCronFireXml(origin, prompt)
76059
+ ].join("\n");
76060
+ }
76043
76061
  function stringAttr(value, fallback) {
76044
76062
  if (typeof value !== "string" || value.length === 0) return fallback;
76045
76063
  return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;");
@@ -77101,7 +77119,7 @@ var init_manager$3 = __esmMin((() => {
77101
77119
  };
77102
77120
  const content = [{
77103
77121
  type: "text",
77104
- text: renderCronFireXml(origin, task.prompt)
77122
+ text: task.owner === "session-loop" ? renderSessionLoopWakeupXml(origin, task.prompt, task.loopInterval ?? "auto") : renderCronFireXml(origin, task.prompt)
77105
77123
  }];
77106
77124
  this.agent.emitEvent({
77107
77125
  type: "cron.fired",
@@ -262017,8 +262035,9 @@ var init_blun_media$1 = __esmMin((() => {
262017
262035
  GenerateImageInputSchema = object({ prompt: PromptSchema.describe("A complete visual description of the image to generate.") });
262018
262036
  GenerateVideoInputSchema = object({
262019
262037
  prompt: PromptSchema.describe("A complete visual description of the video or motion to generate."),
262020
- image_id: MediaIdSchema.optional().describe("An optional completed PNG or JPEG media job id to animate into the video.")
262021
- });
262038
+ image_id: MediaIdSchema.optional().describe("An optional completed PNG or JPEG media job id to animate into the video."),
262039
+ path: string().trim().min(1).optional().describe("An optional path to a local PNG or JPEG file to upload and animate. Relative paths resolve against the working directory.")
262040
+ }).refine((args) => !(args.image_id && args.path), { message: "Provide either image_id or path, not both." });
262022
262041
  GenerateSpeechInputSchema = object({ input: PromptSchema.describe("The exact text to synthesize as speech.") });
262023
262042
  GetMediaInputSchema = object({ id: MediaIdSchema.describe("The media job id returned by a generation tool.") });
262024
262043
  UnderstandImageInputSchema = object({
@@ -262092,11 +262111,79 @@ var init_blun_media$1 = __esmMin((() => {
262092
262111
  }
262093
262112
  };
262094
262113
  GenerateVideoTool = class extends MediaGenerationTool {
262114
+ kaos;
262115
+ workspace;
262095
262116
  name = "GenerateVideo";
262096
- description = "Generate a new video from text or animate an existing completed image job with BLUN media models. Pass image_id for image-to-video requests. Do not use this for understanding an existing video. This starts a billed asynchronous media job.";
262117
+ description = "Generate a new video from text, animate an existing completed image job, or animate a local PNG/JPEG file with BLUN media models. Pass image_id for an existing media job or path for a local or Telegram-provided image. Do not use this for understanding an existing video. This starts a billed asynchronous media job.";
262097
262118
  parameters = toInputJsonSchema(GenerateVideoInputSchema);
262119
+ constructor(provider, kaos, workspace) {
262120
+ super(provider);
262121
+ this.kaos = kaos;
262122
+ this.workspace = workspace;
262123
+ }
262098
262124
  subject(args) {
262099
- return args.image_id ? `${args.image_id}: ${args.prompt}` : args.prompt;
262125
+ return args.path ? `${args.path}: ${args.prompt}` : args.image_id ? `${args.image_id}: ${args.prompt}` : args.prompt;
262126
+ }
262127
+ resolveExecution(args) {
262128
+ if (!args.path) return super.resolveExecution(args);
262129
+ const path = resolvePathAccessPath(args.path, {
262130
+ kaos: this.kaos,
262131
+ workspace: this.workspace,
262132
+ operation: "read"
262133
+ });
262134
+ return {
262135
+ accesses: ToolAccesses.readFile(path),
262136
+ description: `${this.name}: ${args.path}`,
262137
+ display: {
262138
+ kind: "file_io",
262139
+ operation: "read",
262140
+ path
262141
+ },
262142
+ approvalRule: literalRulePattern(this.name, path),
262143
+ matchesRule: (ruleArgs) => matchesPathRuleSubject(ruleArgs, path, {
262144
+ cwd: this.workspace.workspaceDir,
262145
+ pathClass: this.kaos.pathClass(),
262146
+ homeDir: this.kaos.gethome()
262147
+ }),
262148
+ execute: (ctx) => this.executionFromPath(args, path, ctx)
262149
+ };
262150
+ }
262151
+ async executionFromPath(args, safePath, ctx) {
262152
+ emitMediaProgress(ctx, this.name, "uploading");
262153
+ try {
262154
+ const fileType = detectFileType(safePath, await this.kaos.readBytes(safePath, 512), "media");
262155
+ if (fileType.mimeType !== "image/png" && fileType.mimeType !== "image/jpeg") return {
262156
+ isError: true,
262157
+ output: "Image-to-video accepts only PNG or JPEG files."
262158
+ };
262159
+ const data = await this.kaos.readBytes(safePath);
262160
+ if (data.byteLength === 0) return {
262161
+ isError: true,
262162
+ output: `"${args.path}" is empty.`
262163
+ };
262164
+ const options = {
262165
+ signal: ctx.signal,
262166
+ toolCallId: ctx.toolCallId
262167
+ };
262168
+ const upload = await this.provider.uploadMedia(data, fileType.mimeType, options);
262169
+ emitMediaProgress(ctx, this.name, "submitting", { sourceId: upload.id });
262170
+ const job = await this.provider.generateVideo(args.prompt, options, upload.id);
262171
+ emitMediaProgress(ctx, this.name, job.status, {
262172
+ id: job.id,
262173
+ sourceId: upload.id,
262174
+ ...job.progress
262175
+ });
262176
+ return {
262177
+ output: `Media job ${job.id} accepted with status ${job.status}. ${this.requestNote}`,
262178
+ isError: false
262179
+ };
262180
+ } catch (error) {
262181
+ emitMediaProgress(ctx, this.name, "failed", { error: errorMessage$10(error) });
262182
+ return {
262183
+ isError: true,
262184
+ output: `Media request failed: ${errorMessage$10(error)}`
262185
+ };
262186
+ }
262100
262187
  }
262101
262188
  submit(args, options) {
262102
262189
  return this.provider.generateVideo(args.prompt, options, args.image_id);
@@ -262764,7 +262851,7 @@ var init_tool$1 = __esmMin((() => {
262764
262851
  toolServices?.webSearcher && new WebSearchTool(toolServices.webSearcher),
262765
262852
  toolServices?.urlFetcher && new FetchURLTool(toolServices.urlFetcher),
262766
262853
  toolServices?.media && new GenerateImageTool(toolServices.media),
262767
- toolServices?.media && new GenerateVideoTool(toolServices.media),
262854
+ toolServices?.media && new GenerateVideoTool(toolServices.media, kaos, workspace),
262768
262855
  toolServices?.media && new GenerateSpeechTool(toolServices.media),
262769
262856
  toolServices?.media && new UnderstandImageTool(toolServices.media, kaos, workspace),
262770
262857
  toolServices?.media && new UnderstandVideoTool(toolServices.media),
@@ -310846,31 +310933,36 @@ function mediaPayloadNumberArray(payload, keys) {
310846
310933
  }
310847
310934
  }
310848
310935
  function mediaProgressFromPayload(payload) {
310849
- const rawPercent = mediaPayloadNumber(payload, ["percent", "progress_percent", "progressPercent", "progress"]);
310936
+ const nestedProgress = payload["progress"];
310937
+ const source = typeof nestedProgress === "object" && nestedProgress !== null && !Array.isArray(nestedProgress) ? {
310938
+ ...payload,
310939
+ ...nestedProgress
310940
+ } : payload;
310941
+ const rawPercent = mediaPayloadNumber(source, ["percent", "progress_percent", "progressPercent", "progress"]);
310850
310942
  const percent = rawPercent === void 0 ? void 0 : rawPercent <= 1 ? rawPercent * 100 : rawPercent;
310851
310943
  const progress = {
310852
- phaseLabel: mediaPayloadText(payload, ["phase", "stage", "step_name", "stepName"]),
310944
+ phaseLabel: mediaPayloadText(source, ["phase", "stage", "step_name", "stepName"]),
310853
310945
  percent: percent === void 0 ? void 0 : Math.min(100, Math.max(0, percent)),
310854
- currentFrame: mediaPayloadNumber(payload, ["current_frame", "currentFrame", "frames_completed", "framesCompleted"]),
310855
- totalFrames: mediaPayloadNumber(payload, ["total_frames", "totalFrames", "frame_count", "frameCount"]),
310856
- fps: mediaPayloadNumber(payload, ["fps", "frames_per_second", "framesPerSecond"]),
310857
- currentStep: mediaPayloadNumber(payload, ["current_step", "currentStep", "step"]),
310858
- totalSteps: mediaPayloadNumber(payload, ["total_steps", "totalSteps", "steps"]),
310859
- currentSegment: mediaPayloadNumber(payload, ["current_segment", "currentSegment", "segment"]),
310860
- totalSegments: mediaPayloadNumber(payload, ["total_segments", "totalSegments", "segments"]),
310861
- generatedSeconds: mediaPayloadNumber(payload, ["generated_seconds", "generatedSeconds", "audio_seconds", "audioSeconds"]),
310862
- queuePosition: mediaPayloadNumber(payload, ["queue_position", "queuePosition"]),
310863
- elapsedSeconds: mediaPayloadNumber(payload, ["elapsed_seconds", "elapsedSeconds"]),
310864
- remainingSeconds: mediaPayloadNumber(payload, ["remaining_seconds", "remainingSeconds", "eta_seconds", "etaSeconds"]),
310865
- durationSeconds: mediaPayloadNumber(payload, ["duration_seconds", "durationSeconds"]),
310866
- width: mediaPayloadNumber(payload, ["width", "width_px", "widthPx"]),
310867
- height: mediaPayloadNumber(payload, ["height", "height_px", "heightPx"]),
310868
- previewUrl: mediaPayloadText(payload, ["preview_url", "previewUrl", "thumbnail_url", "thumbnailUrl"]),
310869
- previewDataBase64: mediaPayloadText(payload, ["preview_base64", "previewBase64", "thumbnail_base64", "thumbnailBase64"]),
310870
- previewMimeType: mediaPayloadText(payload, ["preview_mime_type", "previewMimeType", "thumbnail_mime_type", "thumbnailMimeType"]),
310871
- sampleUrl: mediaPayloadText(payload, ["sample_url", "sampleUrl", "audio_preview_url", "audioPreviewUrl"]),
310872
- localPath: mediaPayloadText(payload, ["local_path", "localPath"]),
310873
- waveform: mediaPayloadNumberArray(payload, ["waveform", "waveform_samples", "waveformSamples"])
310946
+ currentFrame: mediaPayloadNumber(source, ["current_frame", "currentFrame", "frames_completed", "framesCompleted"]),
310947
+ totalFrames: mediaPayloadNumber(source, ["total_frames", "totalFrames", "frame_count", "frameCount"]),
310948
+ fps: mediaPayloadNumber(source, ["fps", "frames_per_second", "framesPerSecond"]),
310949
+ currentStep: mediaPayloadNumber(source, ["current_step", "currentStep", "step"]),
310950
+ totalSteps: mediaPayloadNumber(source, ["total_steps", "totalSteps", "steps"]),
310951
+ currentSegment: mediaPayloadNumber(source, ["current_segment", "currentSegment", "segment"]),
310952
+ totalSegments: mediaPayloadNumber(source, ["total_segments", "totalSegments", "segments"]),
310953
+ generatedSeconds: mediaPayloadNumber(source, ["generated_seconds", "generatedSeconds", "audio_seconds", "audioSeconds"]),
310954
+ queuePosition: mediaPayloadNumber(source, ["queue_position", "queuePosition"]),
310955
+ elapsedSeconds: mediaPayloadNumber(source, ["elapsed_seconds", "elapsedSeconds"]),
310956
+ remainingSeconds: mediaPayloadNumber(source, ["remaining_seconds", "remainingSeconds", "eta_seconds", "etaSeconds"]),
310957
+ durationSeconds: mediaPayloadNumber(source, ["duration_seconds", "durationSeconds"]),
310958
+ width: mediaPayloadNumber(source, ["width", "width_px", "widthPx"]),
310959
+ height: mediaPayloadNumber(source, ["height", "height_px", "heightPx"]),
310960
+ previewUrl: mediaPayloadText(source, ["preview_url", "previewUrl", "thumbnail_url", "thumbnailUrl"]),
310961
+ previewDataBase64: mediaPayloadText(source, ["preview_base64", "previewBase64", "thumbnail_base64", "thumbnailBase64"]),
310962
+ previewMimeType: mediaPayloadText(source, ["preview_mime_type", "previewMimeType", "thumbnail_mime_type", "thumbnailMimeType"]),
310963
+ sampleUrl: mediaPayloadText(source, ["sample_url", "sampleUrl", "audio_preview_url", "audioPreviewUrl"]),
310964
+ localPath: mediaPayloadText(source, ["local_path", "localPath"]),
310965
+ waveform: mediaPayloadNumberArray(source, ["waveform", "waveform_samples", "waveformSamples"])
310874
310966
  };
310875
310967
  return Object.fromEntries(Object.entries(progress).filter(([, value]) => value !== void 0));
310876
310968
  }
@@ -334380,8 +334472,9 @@ function toolResultToSessionUpdate(sessionId, event) {
334380
334472
  * Mapping rules (anchored at types.gen.d.ts:3530-3569 / :4849):
334381
334473
  * - The `todo_list` input-display block carries
334382
334474
  * `items: { title, status }[]` (schemas.ts:60). The status is the
334383
- * three-state TodoStatus union (todo-list.ts:26):
334384
- * `pending` | `in_progress` | `done`.
334475
+ * TodoStatus union (todo-list.ts:26):
334476
+ * `pending` | `in_progress` | `done` | `blocked` |
334477
+ * `waiting_approval` | `aborted`.
334385
334478
  * - ACP {@link PlanEntryStatus} is `pending` | `in_progress` | `completed`,
334386
334479
  * so `done` rewrites to `completed`. Anything outside the known
334387
334480
  * enum lands on `pending` as a safe default — we never want a
@@ -334415,6 +334508,9 @@ function mapTodoStatus(status) {
334415
334508
  case "in_progress": return "in_progress";
334416
334509
  case "done":
334417
334510
  case "completed": return "completed";
334511
+ case "blocked":
334512
+ case "waiting_approval":
334513
+ case "aborted": return "pending";
334418
334514
  default: return "pending";
334419
334515
  }
334420
334516
  }
@@ -401290,6 +401386,21 @@ const BUILTIN_SLASH_COMMAND_DEFINITIONS = [
401290
401386
  return "idle-only";
401291
401387
  }
401292
401388
  },
401389
+ {
401390
+ name: "idea",
401391
+ aliases: [],
401392
+ descriptionKey: "command.goal.description",
401393
+ priority: 80,
401394
+ argumentHint: "<objective>",
401395
+ availability: (args) => {
401396
+ const trimmed = args.trim();
401397
+ if (trimmed === "") return "always";
401398
+ const tokens = trimmed.split(/\s+/);
401399
+ const action = tokens[0]?.toLowerCase();
401400
+ if (tokens.length === 1 && (action === "status" || action === "pause" || action === "paused" || action === "resume" || action === "cancel" || action === "clear" || action === "stop")) return "always";
401401
+ return "idle-only";
401402
+ }
401403
+ },
401293
401404
  {
401294
401405
  name: "loop",
401295
401406
  aliases: [],
@@ -410038,6 +410149,16 @@ function trimPartialClosingFences(tokens) {
410038
410149
  if (!marker || !lastLine || lastLine.length >= marker.length || lastLine !== marker[0]?.repeat(lastLine.length)) return;
410039
410150
  token.text = token.text.slice(0, -lastLine.length).replace(/\n$/, "");
410040
410151
  }
410152
+ function terminalHyperlinkTarget(value) {
410153
+ const candidate = value.trim();
410154
+ if (/^https?:\/\//iu.test(candidate) || /^file:\/\//iu.test(candidate)) try {
410155
+ const parsed = new URL(candidate);
410156
+ if (["http:", "https:", "file:"].includes(parsed.protocol)) return parsed.href;
410157
+ } catch {
410158
+ return;
410159
+ }
410160
+ if (/^[A-Za-z]:[\\/]/u.test(candidate)) return pathToFileURL(candidate).href;
410161
+ }
410041
410162
  const markdownParser = new q();
410042
410163
  markdownParser.setOptions({ tokenizer: new StrictStrikethroughTokenizer() });
410043
410164
  var Markdown = class {
@@ -410291,7 +410412,11 @@ var Markdown = class {
410291
410412
  break;
410292
410413
  }
410293
410414
  case "codespan":
410294
- result += this.theme.code(token.text) + stylePrefix;
410415
+ {
410416
+ const styledCode = this.theme.code(token.text);
410417
+ const linkTarget = terminalHyperlinkTarget(token.text);
410418
+ result += (linkTarget !== void 0 && getCapabilities().hyperlinks ? hyperlink(styledCode, linkTarget) : styledCode) + stylePrefix;
410419
+ }
410295
410420
  break;
410296
410421
  case "link": {
410297
410422
  const linkText = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
@@ -411788,7 +411913,7 @@ function isTodoItemShape(value) {
411788
411913
  if (typeof value !== "object" || value === null) return false;
411789
411914
  const rec = value;
411790
411915
  if (typeof rec.title !== "string" || rec.title.length === 0) return false;
411791
- return rec.status === "pending" || rec.status === "in_progress" || rec.status === "done";
411916
+ return rec.status === "pending" || rec.status === "in_progress" || rec.status === "done" || rec.status === "blocked" || rec.status === "waiting_approval" || rec.status === "aborted";
411792
411917
  }
411793
411918
  const PUBLIC_IDENTITY_ASSIGNMENT_RE = /\b(provider|model)(["']?\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*'|[^,;\s)\]}]+)/giu;
411794
411919
  /** Project structured runtime identity fields onto the public BLUN identity. */
@@ -412574,6 +412699,7 @@ var ChoicePickerComponent = class extends Container {
412574
412699
  const descriptionColor = isSelected && opt.descriptionTone !== void 0 ? opt.descriptionTone : "textMuted";
412575
412700
  for (const descLine of wrapDescription$1(opt.description, descriptionWidth)) lines.push(currentTheme.fg(descriptionColor, ` ${descLine}`));
412576
412701
  }
412702
+ if (this.opts.spaced === true && i < view.page.end - 1) lines.push("");
412577
412703
  }
412578
412704
  lines.push("");
412579
412705
  if (view.page.pageCount > 1) lines.push(currentTheme.fg("textMuted", ` ${uiText("common.pageCount", {
@@ -416564,6 +416690,19 @@ function bufferContext(state, chatId, tag) {
416564
416690
  function channelOrigin(envelope) {
416565
416691
  return `Telegram · ${envelope.meta.user ?? envelope.meta.chat_id}`;
416566
416692
  }
416693
+ const TELEGRAM_REMOTE_COMMANDS = Object.freeze(["loop", "goal", "idea", "befehle"]);
416694
+ function telegramRemoteCommand(text) {
416695
+ const trimmed = text.trim();
416696
+ const parsed = parseSlashInput(trimmed);
416697
+ if (parsed === null) return;
416698
+ const name = parsed.name.toLowerCase().split("@", 1)[0];
416699
+ if (!TELEGRAM_REMOTE_COMMANDS.includes(name)) return;
416700
+ return {
416701
+ name,
416702
+ input: name === "befehle" ? "/befehle" : `/${name}${parsed.args.length > 0 ? ` ${parsed.args}` : ""}`,
416703
+ displayText: trimmed
416704
+ };
416705
+ }
416567
416706
  /**
416568
416707
  * Route one envelope into the visible session. Unaddressed group traffic is
416569
416708
  * shown and buffered per chat without a model turn. Addressed traffic queues
@@ -419991,6 +420130,51 @@ async function handleGoalCommand(host, args) {
419991
420130
  return;
419992
420131
  }
419993
420132
  }
420133
+ const IDEA_ALLOWED_CHANNELS = Object.freeze([]);
420134
+ const IDEA_TERMINAL_TODO_STATUSES = Object.freeze([
420135
+ "done",
420136
+ "blocked",
420137
+ "waiting_approval",
420138
+ "aborted"
420139
+ ]);
420140
+ function buildIdeaPrompt(objective) {
420141
+ const allowedChannels = IDEA_ALLOWED_CHANNELS.length === 0 ? "none" : IDEA_ALLOWED_CHANNELS.join(", ");
420142
+ return `<idea-mode>
420143
+ The user's objective is:
420144
+ ${objective}
420145
+
420146
+ Your first tool call MUST be TodoList. Publish a task-specific plan there before any research or action.
420147
+
420148
+ ${buildIdeaContract(allowedChannels)}
420149
+ </idea-mode>`;
420150
+ }
420151
+ function buildIdeaContract(allowedChannels = IDEA_ALLOWED_CHANNELS.length === 0 ? "none" : IDEA_ALLOWED_CHANNELS.join(", ")) {
420152
+ return `Work as a self-directing employee: discover a useful route, research current examples and real user problems when relevant, decide, implement reversible local work, verify results, and keep going without waiting for step-by-step instructions.
420153
+
420154
+ Maintain the task-specific plan in TodoList. Keep exactly one item in_progress. Every plan item must end in exactly one truthful terminal status from: ${IDEA_TERMINAL_TODO_STATUSES.join(", ")}. A blocked title must include the reason and what would unblock it. A waiting_approval title must include the prepared result and the exact approval needed. An aborted title must include the last reached state.
420155
+
420156
+ Ask a focused question with AskUserQuestion only when the answer changes the next action or when access is missing. Otherwise act.
420157
+
420158
+ Read-only research through search and fetch tools is allowed. External publishing, messages to third parties, account creation, purchases, or other irreversible external actions are allowed only for an exact channel listed in IDEA_ALLOWED_CHANNELS and only with the user's own required access. Currently allowed channels: ${allowedChannels}. If a later step would leave the computer and is not preapproved, prepare everything possible, mark the step waiting_approval, and ask for the precise approval or access. Never skip the step silently.
420159
+
420160
+ Do not stop after proposing ideas. Carry safe local work through implementation and verification. The assignment is complete only when every plan item has a truthful terminal status and every claimed result has been verified. Report concrete results, blockers, or decisions.`;
420161
+ }
420162
+ async function handleIdeaCommand(host, args) {
420163
+ const parsed = parseGoalCommand(args);
420164
+ if (parsed.kind !== "create") {
420165
+ await handleGoalCommand(host, args);
420166
+ return;
420167
+ }
420168
+ await createGoal(host, {
420169
+ ...parsed,
420170
+ completionCriterion: buildIdeaContract()
420171
+ }, args, {
420172
+ commandName: "idea",
420173
+ sendInput: (objective) => {
420174
+ host.sendMessage(host.requireSession(), objective, { parts: buildIdeaPrompt(objective) });
420175
+ }
420176
+ });
420177
+ }
419994
420178
  function parseNextGoalCommand(tokens) {
419995
420179
  if (tokens.length === 2 && tokens[1]?.toLowerCase() === "manage") return { kind: "next-manage" };
419996
420180
  let index = 1;
@@ -420133,7 +420317,7 @@ async function createGoal(host, parsed, rawArgs, options = {}) {
420133
420317
  return startGoal(host, parsed, options);
420134
420318
  }
420135
420319
  function showGoalStartPermissionPrompt(host, parsed, rawArgs, options) {
420136
- const commandText = `/goal ${rawArgs.trim()}`;
420320
+ const commandText = `/${options.commandName ?? "goal"} ${rawArgs.trim()}`;
420137
420321
  const cancelStart = () => {
420138
420322
  host.restoreInputText(commandText);
420139
420323
  host.showStatus(uiText("goal.start.notStarted"));
@@ -420170,6 +420354,7 @@ async function startGoal(host, parsed, options) {
420170
420354
  try {
420171
420355
  await host.requireSession().createGoal({
420172
420356
  objective: parsed.objective,
420357
+ completionCriterion: parsed.completionCriterion,
420173
420358
  replace: parsed.replace
420174
420359
  });
420175
420360
  } catch (error) {
@@ -420244,6 +420429,11 @@ async function showGoalStatus(host) {
420244
420429
  host.showStatus(uiText("goal.status.noneSet"));
420245
420430
  return;
420246
420431
  }
420432
+ const telegramCommand = host.telegramRemoteCommandContext?.item.telegramCommandName;
420433
+ if (telegramCommand === "goal" || telegramCommand === "idea") {
420434
+ host.showStatus(`/${telegramCommand} ${goal.status} · ${goal.objective}`);
420435
+ return;
420436
+ }
420247
420437
  host.state.transcriptContainer.addChild(new GoalStatusMessageComponent(goal));
420248
420438
  host.state.ui.requestRender();
420249
420439
  }
@@ -420395,6 +420585,7 @@ async function handleLoopCommand(host, args) {
420395
420585
  switch (parsed.kind) {
420396
420586
  case "status": {
420397
420587
  const { loop } = await session.getLoop();
420588
+ host.setAppState?.({ loop });
420398
420589
  host.track("loop_status", { status: loop?.status ?? "none" });
420399
420590
  host.showStatus(`/loop ${loop?.status ?? "none"}`);
420400
420591
  return;
@@ -420404,6 +420595,7 @@ async function handleLoopCommand(host, args) {
420404
420595
  interval: parsed.interval,
420405
420596
  prompt: parsed.prompt
420406
420597
  });
420598
+ host.setAppState?.({ loop });
420407
420599
  host.track("loop_start", { interval: loop.interval });
420408
420600
  host.showStatus(`/loop ${loop.status}`);
420409
420601
  host.sendNormalUserInput(loop.prompt);
@@ -420411,6 +420603,7 @@ async function handleLoopCommand(host, args) {
420411
420603
  }
420412
420604
  case "pause": {
420413
420605
  const loop = await session.pauseLoop();
420606
+ host.setAppState?.({ loop });
420414
420607
  if (isStreaming(host)) await session.cancel();
420415
420608
  host.track("loop_pause");
420416
420609
  host.showStatus(`/loop ${loop.status}`);
@@ -420418,12 +420611,14 @@ async function handleLoopCommand(host, args) {
420418
420611
  }
420419
420612
  case "resume": {
420420
420613
  const loop = await session.resumeLoop();
420614
+ host.setAppState?.({ loop });
420421
420615
  host.track("loop_resume");
420422
420616
  host.showStatus(`/loop ${loop.status}`);
420423
420617
  return;
420424
420618
  }
420425
420619
  case "stop":
420426
420620
  await session.stopLoop();
420621
+ host.setAppState?.({ loop: null });
420427
420622
  if (isStreaming(host)) await session.cancel();
420428
420623
  host.track("loop_stop");
420429
420624
  host.showStatus("/loop stopped");
@@ -492994,6 +493189,9 @@ async function handleBuiltInSlashCommand(host, name, args) {
492994
493189
  case "goal":
492995
493190
  await handleGoalCommand(host, args);
492996
493191
  return;
493192
+ case "idea":
493193
+ await handleIdeaCommand(host, args);
493194
+ return;
492997
493195
  case "loop":
492998
493196
  await handleLoopCommand(host, args);
492999
493197
  return;
@@ -498171,22 +498369,25 @@ function mediaActivityCanonicalPhase(value) {
498171
498369
  }
498172
498370
  function mediaActivityPhaseSequence(mediaKind) {
498173
498371
  switch (mediaActivityStatusKey(mediaKind)) {
498174
- case "image": return ["queued", "generating", "upscaling", "saving"];
498175
- case "video": return ["queued", "generating", "rendering", "saving"];
498372
+ case "image": return ["queued", "processing", "generating", "upscaling", "saving"];
498373
+ case "video": return ["queued", "processing", "generating", "rendering", "saving"];
498176
498374
  case "voice": return ["queued", "processing", "saving"];
498177
- case "dubbing": return ["queued", "audio-analysis", "rendering", "saving"];
498178
- case "lipsync": return ["queued", "audio-analysis", "face-tracking", "lip-sync-running", "rendering", "exporting", "saving"];
498375
+ case "dubbing": return ["queued", "processing", "audio-analysis", "rendering", "saving"];
498376
+ case "lipsync": return ["queued", "processing", "audio-analysis", "face-tracking", "lip-sync-running", "rendering", "exporting", "saving"];
498179
498377
  case "image-analysis":
498180
- case "video-analysis": return ["queued", "analyzing"];
498378
+ case "video-analysis": return ["queued", "processing", "analyzing"];
498181
498379
  default: return ["queued", "processing", "saving"];
498182
498380
  }
498183
498381
  }
498184
498382
  function mediaActivityChainStates(job) {
498185
- const current = mediaActivityCanonicalPhase(job.phaseLabel ?? job.phase);
498186
- const history = new Set((job.phaseHistory ?? []).map((phase) => mediaActivityCanonicalPhase(phase)));
498187
- return mediaActivityPhaseSequence(job.mediaKind).map((phase) => ({
498383
+ const sequence = mediaActivityPhaseSequence(job.mediaKind);
498384
+ const normalize = (value) => mediaActivityCanonicalPhase(value);
498385
+ const current = normalize(job.phaseLabel ?? job.phase);
498386
+ const currentIndex = sequence.indexOf(current);
498387
+ const history = new Set((job.phaseHistory ?? []).map(normalize));
498388
+ return sequence.map((phase, index) => ({
498188
498389
  phase,
498189
- state: phase === current ? "active" : history.has(phase) ? "done" : "pending"
498390
+ state: phase === current ? "active" : history.has(phase) || currentIndex > index ? "done" : "pending"
498190
498391
  }));
498191
498392
  }
498192
498393
  function renderMediaWaveform(samples, width) {
@@ -498340,25 +498541,42 @@ var MediaActivityComponent = class {
498340
498541
  const percent = Math.min(100, Math.max(0, job.percent));
498341
498542
  progress = includeBar ? ` · ${renderProgressBar(percent / 100, 12)} · ${String(Math.round(percent))} %` : ` · ${String(Math.round(percent))} %`;
498342
498543
  }
498343
- const line = `${currentTheme.boldFg("primary", kind)} · ${phase}${details.length === 0 ? "" : ` · ${details.join(" · ")}`}${progress}`;
498544
+ const line = `${currentTheme.boldFg("primary", "●")} ${currentTheme.boldFg("primary", kind)} · ${phase}${details.length === 0 ? "" : ` · ${details.join(" · ")}`}${progress}`;
498344
498545
  return truncateToWidth(line, Math.max(1, width), "…");
498345
498546
  }
498346
498547
  chainLine(job, width) {
498347
498548
  const chain = mediaActivityChainStates(job).map(({ phase, state }) => {
498348
- const symbol = state === "done" ? "[x]" : state === "active" ? "[>]" : "[ ]";
498549
+ const symbol = state === "done" ? "" : state === "active" ? "" : "";
498349
498550
  const label = mediaActivityLabel("media.phase", phase, phase);
498350
- return `${symbol} ${label}`;
498551
+ const text = `${symbol} ${label}`;
498552
+ if (state === "done") return currentTheme.fg("success", text);
498553
+ if (state === "active") return currentTheme.boldFg("primary", text);
498554
+ return currentTheme.fg("textDim", text);
498351
498555
  });
498352
- return truncateToWidth(` ${mediaUiText("media.activity.chain")} · ${chain.join(" · ")}`, Math.max(1, width), "…");
498556
+ return truncateToWidth(` ${currentTheme.fg("textDim", mediaUiText("media.activity.chain"))} · ${chain.join(currentTheme.fg("textDim", " · "))}`, Math.max(1, width), "…");
498353
498557
  }
498354
498558
  previewImage(job) {
498355
- if (typeof job.previewDataBase64 !== "string" || job.previewDataBase64.length === 0 || job.previewDataBase64.length > 2 * 1024 * 1024 || !/^[A-Za-z0-9+/=\r\n]+$/u.test(job.previewDataBase64)) return;
498356
- const mimeType = typeof job.previewMimeType === "string" ? job.previewMimeType.toLowerCase() : "";
498559
+ let previewDataBase64 = typeof job.previewDataBase64 === "string" ? job.previewDataBase64 : "";
498560
+ let mimeType = typeof job.previewMimeType === "string" ? job.previewMimeType.toLowerCase() : "";
498561
+ if (previewDataBase64.length === 0 && typeof job.previewUrl === "string") {
498562
+ const dataUrl = /^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/=\r\n]+)$/iu.exec(job.previewUrl.trim());
498563
+ if (dataUrl !== null) {
498564
+ mimeType = dataUrl[1].toLowerCase();
498565
+ previewDataBase64 = dataUrl[2];
498566
+ }
498567
+ }
498568
+ if (previewDataBase64.length === 0 || previewDataBase64.length > 2 * 1024 * 1024 || !/^[A-Za-z0-9+/=\r\n]+$/u.test(previewDataBase64)) return;
498357
498569
  if (!["image/png", "image/jpeg", "image/webp", "image/gif"].includes(mimeType)) return;
498358
- const key = `${job.id ?? job.toolCallId}:${mimeType}:${job.previewDataBase64.length}`;
498570
+ const identity = String(job.id ?? job.toolCallId);
498571
+ const prefix = `${identity}:`;
498572
+ const key = `${prefix}${mimeType}:${String(job.observedAt ?? job.updatedAt ?? previewDataBase64.length)}`;
498359
498573
  let image = this.previewImages.get(key);
498360
498574
  if (image === void 0) {
498361
- image = new Image(job.previewDataBase64, mimeType, { fallbackColor: (text) => currentTheme.fg("textDim", text) }, {
498575
+ for (const [previousKey, previousImage] of this.previewImages) if (previousKey.startsWith(prefix)) {
498576
+ previousImage.invalidate();
498577
+ this.previewImages.delete(previousKey);
498578
+ }
498579
+ image = new Image(previewDataBase64, mimeType, { fallbackColor: (text) => currentTheme.fg("textDim", text) }, {
498362
498580
  maxHeightCells: 5,
498363
498581
  maxWidthCells: 24,
498364
498582
  filename: mediaUiText("media.detail.preview")
@@ -504742,6 +504960,9 @@ var SessionEventHandler = class {
504742
504960
  stale: event.origin.stale
504743
504961
  }
504744
504962
  });
504963
+ setTimeout(() => {
504964
+ this.host.refreshLoopState();
504965
+ }, 0);
504745
504966
  }
504746
504967
  handleTurnEnd(event, sendQueued) {
504747
504968
  this.turnWatchdog.stop();
@@ -507190,6 +507411,30 @@ function formatGoalBadge(goal, colors, wallClockMs) {
507190
507411
  const label = `${uiText(`goal.panel.status.${goal.status}`)} · ${formatBadgeElapsed(wallClockMs ?? goal.wallClockMs)} · ${turns}`;
507191
507412
  return chalk.hex(colors.textMuted)(`[${uiText("footer.goal.label")} `) + chalk.hex(dotColor)("●") + chalk.hex(colors.textMuted)(` ${label}]`);
507192
507413
  }
507414
+ function formatLoopBadge(loop, colors, nowMs = Date.now()) {
507415
+ if (loop === null || loop === void 0 || loop.status !== "active" || !Number.isFinite(loop.nextFireAt)) return null;
507416
+ const remainingMs = Math.max(0, loop.nextFireAt - nowMs);
507417
+ let value;
507418
+ let unit;
507419
+ if (remainingMs < 9e4) {
507420
+ value = Math.ceil(remainingMs / 1e3);
507421
+ unit = "second";
507422
+ } else if (remainingMs < 54e5) {
507423
+ value = Math.ceil(remainingMs / 6e4);
507424
+ unit = "minute";
507425
+ } else if (remainingMs < 1296e5) {
507426
+ value = Math.ceil(remainingMs / 36e5);
507427
+ unit = "hour";
507428
+ } else {
507429
+ value = Math.ceil(remainingMs / 864e5);
507430
+ unit = "day";
507431
+ }
507432
+ const relative = new Intl.RelativeTimeFormat(getCurrentUiLocale(), {
507433
+ numeric: "always",
507434
+ style: "short"
507435
+ }).format(value, unit);
507436
+ return chalk.hex(colors.textMuted)(`[/loop · ${relative}]`);
507437
+ }
507193
507438
  function formatBadgeElapsed(ms) {
507194
507439
  const totalSeconds = Math.round(ms / 1e3);
507195
507440
  if (totalSeconds < 60) return `${totalSeconds}s`;
@@ -507368,6 +507613,7 @@ var FooterComponent = class {
507368
507613
  goalSnapshotKey = null;
507369
507614
  goalObservedAtMs = Date.now();
507370
507615
  goalTimer = null;
507616
+ loopTimer = null;
507371
507617
  compactionAttemptStartedAtMs = null;
507372
507618
  compactionEstimatedInputTokens;
507373
507619
  compactionProgress;
@@ -507389,6 +507635,7 @@ var FooterComponent = class {
507389
507635
  this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onRefresh });
507390
507636
  this.syncGoalClock(state.goal);
507391
507637
  this.syncGoalTimer(state.goal);
507638
+ this.syncLoopTimer(state.loop);
507392
507639
  if (state.isCompacting) this.startCompaction();
507393
507640
  }
507394
507641
  setState(state) {
@@ -507398,6 +507645,7 @@ var FooterComponent = class {
507398
507645
  }
507399
507646
  this.syncGoalClock(state.goal);
507400
507647
  this.syncGoalTimer(state.goal);
507648
+ this.syncLoopTimer(state.loop);
507401
507649
  this.state = state;
507402
507650
  if (state.isCompacting && this.compactionAttemptStartedAtMs === null) this.startCompaction();
507403
507651
  else if (!state.isCompacting) this.finishCompaction();
@@ -507464,6 +507712,8 @@ var FooterComponent = class {
507464
507712
  if (modes.length > 0) left.push(modes.join(" "));
507465
507713
  const goalBadge = formatGoalBadge(state.goal, colors, this.goalWallClockMs(state.goal));
507466
507714
  if (goalBadge !== null) left.push(goalBadge);
507715
+ const loopBadge = formatLoopBadge(state.loop, colors);
507716
+ if (loopBadge !== null) left.push(loopBadge);
507467
507717
  const model = modelDisplayName(state);
507468
507718
  if (model) {
507469
507719
  const sep = chalk.hex(colors.textMuted)(" · ");
@@ -507528,11 +507778,29 @@ var FooterComponent = class {
507528
507778
  this.goalTimer = null;
507529
507779
  }
507530
507780
  }
507781
+ syncLoopTimer(loop) {
507782
+ if (loop?.status === "active" && Number.isFinite(loop.nextFireAt)) {
507783
+ if (this.loopTimer !== null) return;
507784
+ this.loopTimer = setInterval(() => {
507785
+ this.onRefresh();
507786
+ }, GOAL_TIMER_INTERVAL_MS);
507787
+ this.loopTimer.unref?.();
507788
+ return;
507789
+ }
507790
+ if (this.loopTimer !== null) {
507791
+ clearInterval(this.loopTimer);
507792
+ this.loopTimer = null;
507793
+ }
507794
+ }
507531
507795
  dispose() {
507532
507796
  if (this.goalTimer !== null) {
507533
507797
  clearInterval(this.goalTimer);
507534
507798
  this.goalTimer = null;
507535
507799
  }
507800
+ if (this.loopTimer !== null) {
507801
+ clearInterval(this.loopTimer);
507802
+ this.loopTimer = null;
507803
+ }
507536
507804
  this.finishCompaction();
507537
507805
  }
507538
507806
  goalWallClockMs(goal) {
@@ -510301,6 +510569,8 @@ const MAX_VISIBLE = 5;
510301
510569
  const BLUN_BLUE = "#0261ff";
510302
510570
  const BLUN_BRIGHT = "#4f8dff";
510303
510571
  const DONE_GREEN = "#22c55e";
510572
+ const BLOCKED_RED = "#ef4444";
510573
+ const WAITING_AMBER = "#f59e0b";
510304
510574
  const BOX_EMPTY = "#2a3550";
510305
510575
  const PENDING_TEXT = "#aeb8c6";
510306
510576
  const LEFT_BAR = "▎";
@@ -510314,7 +510584,9 @@ const LEFT_BAR = "▎";
510314
510584
  *
510315
510585
  * Strategy:
510316
510586
  * 1. Include every `in_progress` item (capped at MAX_VISIBLE).
510317
- * 2. Fill remaining slots with "what's next" — the earliest `pending`
510587
+ * 2. Include terminal attention states (`blocked`, `waiting_approval`,
510588
+ * `aborted`) before ordinary pending and done work.
510589
+ * 3. Fill remaining slots with "what's next" — the earliest `pending`
510318
510590
  * items in their original positions — while reserving one slot for
510319
510591
  * "what just finished" — the latest `done` item — when both kinds
510320
510592
  * exist. If one side has too few candidates, the other expands.
@@ -510328,17 +510600,26 @@ function selectVisibleTodos(todos) {
510328
510600
  hiddenCounts: {
510329
510601
  done: 0,
510330
510602
  in_progress: 0,
510331
- pending: 0
510603
+ pending: 0,
510604
+ blocked: 0,
510605
+ waiting_approval: 0,
510606
+ aborted: 0
510332
510607
  }
510333
510608
  };
510334
510609
  const inProgress = [];
510335
510610
  const pending = [];
510336
510611
  const done = [];
510612
+ const attention = [];
510337
510613
  for (const [i, todo] of todos.entries()) if (todo.status === "in_progress") inProgress.push(i);
510338
510614
  else if (todo.status === "pending") pending.push(i);
510339
- else done.push(i);
510615
+ else if (todo.status === "done") done.push(i);
510616
+ else attention.push(i);
510340
510617
  const picked = /* @__PURE__ */ new Set();
510341
510618
  for (const i of inProgress.slice(0, MAX_VISIBLE)) picked.add(i);
510619
+ for (const i of attention) {
510620
+ if (picked.size >= MAX_VISIBLE) break;
510621
+ picked.add(i);
510622
+ }
510342
510623
  if (picked.size < MAX_VISIBLE) {
510343
510624
  const doneCandidates = done.toReversed();
510344
510625
  const pendingCandidates = pending;
@@ -510363,7 +510644,10 @@ function selectVisibleTodos(todos) {
510363
510644
  const hiddenCounts = {
510364
510645
  done: 0,
510365
510646
  in_progress: 0,
510366
- pending: 0
510647
+ pending: 0,
510648
+ blocked: 0,
510649
+ waiting_approval: 0,
510650
+ aborted: 0
510367
510651
  };
510368
510652
  for (const [i, todo] of todos.entries()) if (!picked.has(i)) hiddenCounts[todo.status] += 1;
510369
510653
  return {
@@ -510440,6 +510724,9 @@ function renderRow(todo, width) {
510440
510724
  return truncateToWidth(leftPart + chalk.hex("#ffffff")(title) + " ".repeat(gap) + chalk.hex(currentTheme.palette.textDim)(runningLabel), width);
510441
510725
  }
510442
510726
  if (todo.status === "done") return " " + chalk.hex(DONE_GREEN)("▪") + " " + chalk.hex(currentTheme.palette.textDim).strikethrough(todo.title);
510727
+ if (todo.status === "blocked") return " " + chalk.hex(BLOCKED_RED)("■") + " " + chalk.hex("#fecaca")(todo.title);
510728
+ if (todo.status === "waiting_approval") return " " + chalk.hex(WAITING_AMBER)("◆") + " " + chalk.hex("#fde68a")(todo.title);
510729
+ if (todo.status === "aborted") return " " + chalk.hex(currentTheme.palette.textDim)("×") + " " + chalk.hex(currentTheme.palette.textDim).strikethrough(todo.title);
510443
510730
  return " " + chalk.hex(BOX_EMPTY)("☐") + " " + chalk.hex(PENDING_TEXT)(todo.title);
510444
510731
  }
510445
510732
  //#endregion
@@ -511394,6 +511681,22 @@ function outboxGrewForChat(marker, chatId) {
511394
511681
  } catch {}
511395
511682
  return false;
511396
511683
  }
511684
+ function outboxDeliveredFile(marker, chatId, filePath) {
511685
+ try {
511686
+ if (statSync(outboxPath()).size <= marker) return false;
511687
+ const fresh = readFileSync(outboxPath()).subarray(marker).toString("utf8");
511688
+ const wanted = resolve(filePath).toLowerCase();
511689
+ for (const line of fresh.split(/\r?\n/)) {
511690
+ if (line.trim().length === 0) continue;
511691
+ try {
511692
+ const entry = JSON.parse(line);
511693
+ if (String(entry.chat_id) !== String(chatId) || !Array.isArray(entry.files)) continue;
511694
+ if (entry.files.some((file) => typeof file === "string" && resolve(file).toLowerCase() === wanted)) return true;
511695
+ } catch {}
511696
+ }
511697
+ } catch {}
511698
+ return false;
511699
+ }
511397
511700
  const TELEGRAM_TEXT_LIMIT = 4096;
511398
511701
  const TELEGRAM_ATTACHMENT_LIMIT = 50 * 1024 * 1024;
511399
511702
  function mediaTelegramTarget(filePath) {
@@ -512121,19 +512424,20 @@ function promptStartupWorkspaceChoice(host, currentPath, copy) {
512121
512424
  };
512122
512425
  host.mountEditorReplacement(new ChoicePickerComponent({
512123
512426
  title: copy.choiceTitle,
512427
+ spaced: true,
512124
512428
  options: [
512125
512429
  {
512126
512430
  value: "use",
512127
- label: copy.useCurrent,
512431
+ label: `1. ${copy.useCurrent}`,
512128
512432
  description: currentPath
512129
512433
  },
512130
512434
  {
512131
512435
  value: "change",
512132
- label: copy.chooseOther
512436
+ label: `2. ${copy.chooseOther}`
512133
512437
  },
512134
512438
  {
512135
512439
  value: "exit",
512136
- label: copy.exit,
512440
+ label: `3. ${copy.exit}`,
512137
512441
  tone: "danger"
512138
512442
  }
512139
512443
  ],
@@ -512547,6 +512851,7 @@ function createInitialAppState(input) {
512547
512851
  availableProviders: {},
512548
512852
  sessionTitle: null,
512549
512853
  goal: null,
512854
+ loop: null,
512550
512855
  mcpServersSummary: null,
512551
512856
  banner: void 0
512552
512857
  };
@@ -512588,6 +512893,7 @@ var BlunTUI = class {
512588
512893
  startupLoginRequired = false;
512589
512894
  startupWorkspaceSelectionPending = false;
512590
512895
  startupGoalPromptedSessionId;
512896
+ startupPhaseMs = {};
512591
512897
  lastActivityMode;
512592
512898
  currentLoadingTip = void 0;
512593
512899
  mediaActivityStore = new MediaActivityStore();
@@ -512621,6 +512927,17 @@ var BlunTUI = class {
512621
512927
  track(event, properties) {
512622
512928
  this.harness.track(event, properties);
512623
512929
  }
512930
+ async measureStartupPhase(name, action) {
512931
+ const startedAt = Date.now();
512932
+ try {
512933
+ return await action();
512934
+ } finally {
512935
+ this.startupPhaseMs[name] = Date.now() - startedAt;
512936
+ }
512937
+ }
512938
+ getStartupPhaseMs() {
512939
+ return { ...this.startupPhaseMs };
512940
+ }
512624
512941
  constructor(harness, startupInput) {
512625
512942
  this.harness = harness;
512626
512943
  const initialAppState = createInitialAppState(startupInput);
@@ -512982,9 +513299,12 @@ var BlunTUI = class {
512982
513299
  this.showStatus(warning, "warning");
512983
513300
  }
512984
513301
  async init() {
512985
- setExperimentalFeatures(await this.harness.getExperimentalFeatures());
512986
- await this.authFlow.refreshAvailableModels();
512987
- const accountContext = await this.authFlow.refreshManagedAccountContextResult();
513302
+ if (this.startupWorkspaceSelectionPending) return false;
513303
+ await this.measureStartupPhase("experimental_ms", async () => {
513304
+ setExperimentalFeatures(await this.harness.getExperimentalFeatures());
513305
+ });
513306
+ await this.measureStartupPhase("models_ms", () => this.authFlow.refreshAvailableModels());
513307
+ const accountContext = await this.measureStartupPhase("account_ms", () => this.authFlow.refreshManagedAccountContextResult());
512988
513308
  if (accountContext.kind !== "ready") {
512989
513309
  if (accountContext.kind === "unauthenticated") {
512990
513310
  this.authFlow.enterLoginRequiredStartupState();
@@ -512992,8 +513312,7 @@ var BlunTUI = class {
512992
513312
  } else this.authFlow.enterManagedAccountUnavailableStartupState(accountContext.kind);
512993
513313
  return false;
512994
513314
  }
512995
- if (this.startupWorkspaceSelectionPending) return false;
512996
- await this.refreshProviderModelsBeforeSession();
513315
+ await this.measureStartupPhase("provider_models_ms", () => this.refreshProviderModelsBeforeSession());
512997
513316
  const { startup } = this.options;
512998
513317
  const { workDir } = this.state.appState;
512999
513318
  let session;
@@ -513008,6 +513327,7 @@ var BlunTUI = class {
513008
513327
  const channelMcpServers = sessionMcpServers();
513009
513328
  if (channelMcpServers !== void 0) createSessionOptions.mcpServers = channelMcpServers;
513010
513329
  if (this.state.appState.additionalDirs.length > 0) createSessionOptions.additionalDirs = [...this.state.appState.additionalDirs];
513330
+ const sessionStartedAt = Date.now();
513011
513331
  try {
513012
513332
  if (isResumeStartup) {
513013
513333
  if (startup.sessionFlag === "") {
@@ -513053,10 +513373,12 @@ var BlunTUI = class {
513053
513373
  this.authFlow.enterLoginRequiredStartupState();
513054
513374
  this.startupLoginRequired = true;
513055
513375
  return false;
513376
+ } finally {
513377
+ this.startupPhaseMs["session_open_ms"] = Date.now() - sessionStartedAt;
513056
513378
  }
513057
513379
  if (session === void 0) throw new Error(uiText("blunTui.startup.sessionNotInitialized"));
513058
- await this.setSession(session);
513059
- await this.syncRuntimeState(session);
513380
+ await this.measureStartupPhase("session_bind_ms", () => this.setSession(session, { refreshPersonalMemory: false }));
513381
+ await this.measureStartupPhase("runtime_state_ms", () => this.syncRuntimeState(session));
513060
513382
  this.applyStartupPermissionAndPlanToAppState();
513061
513383
  this.state.startupState = "ready";
513062
513384
  return shouldReplayHistory;
@@ -513339,6 +513661,7 @@ var BlunTUI = class {
513339
513661
  }
513340
513662
  queueDrainTimer;
513341
513663
  queueCommandRunning = false;
513664
+ telegramRemoteCommandContext;
513342
513665
  queueFlushBatchRemaining = 0;
513343
513666
  queueSteerInFlight;
513344
513667
  editorReplacementActive = false;
@@ -513589,14 +513912,20 @@ var BlunTUI = class {
513589
513912
  }
513590
513913
  /**
513591
513914
  * Remote channel inbound (Telegram) into THIS visible session.
513592
- * Never routes through handleUserInput: no slash/bash dispatch (a Telegram
513593
- * "/new" is just text for the model), no local editor history. The transcript
513915
+ * Only the explicit Telegram allowlist routes through slash dispatch; every
513916
+ * other slash-looking message (including "/new") remains plain model text.
513917
+ * There is no bash dispatch and no local editor history. The transcript
513594
513918
  * shows a clean user line with a muted origin prefix; the
513595
513919
  * model receives the full <channel …> payload. Busy turns queue via the same
513596
513920
  * queuedMessages mechanic as typed input; a completed tool/step boundary
513597
513921
  * steers one FIFO head into the active turn without interrupting it.
513598
513922
  */
513599
513923
  injectChannelMessage(envelope, acknowledge) {
513924
+ const remoteCommand = channelMessageAddressed(envelope) ? telegramRemoteCommand(envelope.text) : void 0;
513925
+ if (remoteCommand !== void 0) {
513926
+ this.injectTelegramRemoteCommand(envelope, remoteCommand, acknowledge);
513927
+ return;
513928
+ }
513600
513929
  injectChannelEnvelope({
513601
513930
  canDeliver: () => this.session !== void 0 && this.state.appState.model.trim().length > 0,
513602
513931
  isBusy: () => this.state.queuedMessages.length > 0 || this.queueSteerInFlight !== void 0 || this.deferUserMessages || this.state.appState.streamingPhase !== "idle" || this.state.appState.isCompacting,
@@ -513637,6 +513966,99 @@ var BlunTUI = class {
513637
513966
  }
513638
513967
  }, envelope, this.channelPreamble);
513639
513968
  }
513969
+ injectTelegramRemoteCommand(envelope, command, acknowledge) {
513970
+ if (this.session === void 0 || this.state.appState.model.trim().length === 0) {
513971
+ this.showStatus(uiText("channelInjection.noActiveSession"), "warning");
513972
+ return;
513973
+ }
513974
+ const item = {
513975
+ text: command.input,
513976
+ displayText: command.displayText,
513977
+ origin: channelOrigin(envelope),
513978
+ agentId: this.harness.interactiveAgentId,
513979
+ mode: "channel-command",
513980
+ channelChatId: envelope.meta.chat_id,
513981
+ channelAcknowledge: acknowledge,
513982
+ telegramCommandName: command.name
513983
+ };
513984
+ const busy = this.state.queuedMessages.length > 0 || this.queueSteerInFlight !== void 0 || this.pendingChannelReplyGuard !== void 0 || this.deferUserMessages || this.state.appState.streamingPhase !== "idle" || this.state.appState.isCompacting;
513985
+ if (busy) {
513986
+ this.state.queuedMessages.push(item);
513987
+ this.track("input_queue", { kind: "channel-command" });
513988
+ this.updateQueueDisplay();
513989
+ this.state.ui.requestRender();
513990
+ return;
513991
+ }
513992
+ this.runTelegramRemoteCommand(item);
513993
+ }
513994
+ runTelegramRemoteCommand(item) {
513995
+ this.queueCommandRunning = true;
513996
+ this.preserveQueueAcrossSessionReset = true;
513997
+ if (item.channelTranscriptRendered !== true) this.appendTranscriptEntry({
513998
+ id: nextTranscriptId(),
513999
+ kind: "user",
514000
+ turnId: void 0,
514001
+ renderMode: "plain",
514002
+ content: item.displayText ?? item.text,
514003
+ origin: item.origin
514004
+ });
514005
+ const context = {
514006
+ item,
514007
+ responses: [],
514008
+ sentModelTurn: false
514009
+ };
514010
+ this.telegramRemoteCommandContext = context;
514011
+ const execution = item.telegramCommandName === "befehle" ? Promise.resolve().then(() => {
514012
+ context.responses.push("/loop\n/goal\n/idea\n/befehle");
514013
+ }) : dispatchInput(this, item.text);
514014
+ execution.catch((error) => {
514015
+ this.showError(formatErrorMessage$2(error));
514016
+ }).finally(async () => {
514017
+ if (this.telegramRemoteCommandContext === context) this.telegramRemoteCommandContext = void 0;
514018
+ if (!context.sentModelTurn) {
514019
+ const response = context.responses.at(-1) ?? item.displayText ?? item.text;
514020
+ if (await sendReplyFallback(item.channelChatId, response, false)) item.channelAcknowledge?.();
514021
+ }
514022
+ this.queueCommandRunning = false;
514023
+ if (!this.editorReplacementActive) this.preserveQueueAcrossSessionReset = false;
514024
+ this.updateQueueDisplay();
514025
+ this.state.ui.requestRender();
514026
+ this.scheduleQueueDrain();
514027
+ });
514028
+ }
514029
+ sendTelegramRemoteCommandMessage(session, input, options, context) {
514030
+ context.sentModelTurn = true;
514031
+ this.beginSessionRequest();
514032
+ const previousGuard = this.pendingChannelReplyGuard;
514033
+ const installedGuard = {
514034
+ chatId: context.item.channelChatId,
514035
+ outboxMarker: outboxMarker(),
514036
+ transcriptStart: this.state.transcriptEntries.length,
514037
+ contextOnly: false
514038
+ };
514039
+ this.pendingChannelReplyGuard = installedGuard;
514040
+ this.setAppState({
514041
+ model: BLUN_KING_MODEL_ALIAS,
514042
+ modelFallbackAllowed: false
514043
+ });
514044
+ session.promptAccepted(options?.parts ?? input).then((result) => {
514045
+ if (result.accepted) {
514046
+ context.item.channelAcknowledge?.();
514047
+ return;
514048
+ }
514049
+ if (this.pendingChannelReplyGuard === installedGuard) this.pendingChannelReplyGuard = previousGuard;
514050
+ this.state.queuedMessages = [{
514051
+ ...context.item,
514052
+ channelTranscriptRendered: true
514053
+ }, ...this.state.queuedMessages];
514054
+ this.track("input_queue", { kind: "channel-command" });
514055
+ this.updateQueueDisplay();
514056
+ this.state.ui.requestRender();
514057
+ }).catch((error) => {
514058
+ if (this.pendingChannelReplyGuard === installedGuard) this.pendingChannelReplyGuard = previousGuard;
514059
+ this.failSessionRequest(uiText("blunTui.session.sendFailed", { error: formatErrorMessage$2(error) }));
514060
+ });
514061
+ }
513640
514062
  sendChannelMessageInternal(session, modelInput, displayText, origin, channelChatId, channelImagePath, contextOnly = false, transcriptRendered = false, acknowledge) {
513641
514063
  if (!transcriptRendered) this.appendTranscriptEntry({
513642
514064
  id: nextTranscriptId(),
@@ -513696,6 +514118,7 @@ var BlunTUI = class {
513696
514118
  pendingChannelReplyGuard;
513697
514119
  /** See SessionEventHost.runChannelReplyFallback — called at turn end. */
513698
514120
  channelMediaDeliveries = /* @__PURE__ */ new Set();
514121
+ channelMediaDeliveryFailures = /* @__PURE__ */ new Set();
513699
514122
  /** Deliver completed media at tool-result time so later queued work cannot hide it. */
513700
514123
  runChannelMediaFallback(output) {
513701
514124
  const guard = this.pendingChannelReplyGuard;
@@ -513705,9 +514128,12 @@ var BlunTUI = class {
513705
514128
  if (this.channelMediaDeliveries.has(deliveryKey)) return;
513706
514129
  this.channelMediaDeliveries.add(deliveryKey);
513707
514130
  sendMediaReplyFallback(guard.chatId, filePath).then((sent) => {
513708
- if (sent) return;
514131
+ if (sent) {
514132
+ this.channelMediaDeliveryFailures.delete(deliveryKey);
514133
+ return;
514134
+ }
513709
514135
  this.channelMediaDeliveries.delete(deliveryKey);
513710
- this.showError(uiText("blunTui.session.sendFailed", { error: "Telegram media" }));
514136
+ this.channelMediaDeliveryFailures.add(deliveryKey);
513711
514137
  });
513712
514138
  }
513713
514139
  sendChannelTurnStatus(message) {
@@ -513721,6 +514147,13 @@ var BlunTUI = class {
513721
514147
  const guard = this.pendingChannelReplyGuard;
513722
514148
  if (guard === void 0) return;
513723
514149
  this.pendingChannelReplyGuard = void 0;
514150
+ for (const deliveryKey of [...this.channelMediaDeliveryFailures]) {
514151
+ const separator = deliveryKey.indexOf("\0");
514152
+ if (separator < 0 || deliveryKey.slice(0, separator) !== guard.chatId) continue;
514153
+ const filePath = deliveryKey.slice(separator + 1);
514154
+ this.channelMediaDeliveryFailures.delete(deliveryKey);
514155
+ if (!outboxDeliveredFile(guard.outboxMarker, guard.chatId, filePath)) this.showError(uiText("blunTui.session.sendFailed", { error: "Telegram media" }));
514156
+ }
513724
514157
  if (reason !== "completed") return;
513725
514158
  if (this.state.transcriptEntries.slice(guard.transcriptStart).some((entry) => entry.kind === "tool_call" && /reply|edit_message/i.test(entry.toolCallData?.name ?? ""))) return;
513726
514159
  if (outboxGrewForChat(guard.outboxMarker, guard.chatId)) return;
@@ -513826,7 +514259,7 @@ var BlunTUI = class {
513826
514259
  recallLastQueued() {
513827
514260
  if (this.state.queuedMessages.length === 0) return void 0;
513828
514261
  const last = this.state.queuedMessages.at(-1);
513829
- if (last.mode === "channel") return void 0;
514262
+ if (last.mode === "channel" || last.mode === "channel-command") return void 0;
513830
514263
  const queuedBatchCount = Math.max(0, this.queueFlushBatchRemaining - (this.queueSteerInFlight?.items.length ?? 0));
513831
514264
  const removesBatchItem = this.state.queuedMessages.length <= queuedBatchCount;
513832
514265
  this.state.queuedMessages = this.state.queuedMessages.slice(0, -1);
@@ -513884,6 +514317,10 @@ var BlunTUI = class {
513884
514317
  this.runSlashCommand(item.text);
513885
514318
  return;
513886
514319
  }
514320
+ if (item.mode === "channel-command") {
514321
+ this.runTelegramRemoteCommand(item);
514322
+ return;
514323
+ }
513887
514324
  const activeSession = this.session ?? session;
513888
514325
  if (item.mode === "channel") {
513889
514326
  this.harness.withInteractiveAgent(item.agentId ?? "main", () => {
@@ -513956,6 +514393,11 @@ var BlunTUI = class {
513956
514393
  });
513957
514394
  }
513958
514395
  sendMessage(session, input, options) {
514396
+ const telegramContext = this.telegramRemoteCommandContext;
514397
+ if (telegramContext !== void 0) {
514398
+ this.sendTelegramRemoteCommandMessage(session, input, options, telegramContext);
514399
+ return;
514400
+ }
513959
514401
  if (this.deferUserMessages || this.state.appState.streamingPhase !== "idle" || this.state.appState.isCompacting) {
513960
514402
  this.enqueueMessage(input, options);
513961
514403
  return;
@@ -514119,7 +514561,7 @@ var BlunTUI = class {
514119
514561
  if (this.state.appState.additionalDirs.length > 0) options.additionalDirs = [...this.state.appState.additionalDirs];
514120
514562
  return this.harness.createSession(options);
514121
514563
  }
514122
- async setSession(session) {
514564
+ async setSession(session, options = {}) {
514123
514565
  await session.setModel(BLUN_KING_MODEL_ALIAS, { allowFallback: false });
514124
514566
  await this.personalMemoryController.clear(this.session);
514125
514567
  const previous = this.unloadCurrentSession(approvalCancellationFeedback("switching_session"));
@@ -514137,10 +514579,10 @@ var BlunTUI = class {
514137
514579
  this.syncAdditionalDirs(session);
514138
514580
  await this.refreshManagedImageReaderAvailability(session);
514139
514581
  await this.authFlow.applyManagedAccountContextToSession();
514140
- await this.personalMemoryController.refresh();
514582
+ if (options.refreshPersonalMemory !== false) await this.personalMemoryController.refresh();
514141
514583
  }
514142
514584
  async syncRuntimeState(session = this.requireSession()) {
514143
- const [status, goalResult] = await Promise.all([session.getStatus(), session.getGoal()]);
514585
+ const [status, goalResult, loopResult] = await Promise.all([session.getStatus(), session.getGoal(), session.getLoop()]);
514144
514586
  this.setAppState({
514145
514587
  sessionId: session.id,
514146
514588
  model: BLUN_KING_MODEL_ALIAS,
@@ -514157,11 +514599,22 @@ var BlunTUI = class {
514157
514599
  compactionBudgetTokens: status.compactionBudgetTokens ?? 0,
514158
514600
  contextUsage: status.contextUsage,
514159
514601
  sessionTitle: session.summary?.title ?? null,
514160
- goal: goalResult.goal
514602
+ goal: goalResult.goal,
514603
+ loop: loopResult.loop
514161
514604
  });
514162
514605
  this.syncAdditionalDirs(session);
514163
514606
  this.startTelegramChannel();
514164
514607
  }
514608
+ async refreshLoopState(session = this.session) {
514609
+ if (session === void 0) {
514610
+ this.setAppState({ loop: null });
514611
+ return;
514612
+ }
514613
+ try {
514614
+ const { loop } = await session.getLoop();
514615
+ if (this.session === session) this.setAppState({ loop });
514616
+ } catch {}
514617
+ }
514165
514618
  async applyStartupModesToResumedSession(session) {
514166
514619
  const { startup } = this.options;
514167
514620
  if (startup.auto) await session.setPermission("auto");
@@ -514202,6 +514655,7 @@ var BlunTUI = class {
514202
514655
  this.harness.setTelemetryContext({ sessionId: null });
514203
514656
  this.setAppState({
514204
514657
  goal: null,
514658
+ loop: null,
514205
514659
  swarmModeEntry: void 0
514206
514660
  });
514207
514661
  return previous;
@@ -514248,7 +514702,7 @@ var BlunTUI = class {
514248
514702
  streamingPhase: "idle"
514249
514703
  });
514250
514704
  if (!this.preserveQueueAcrossSessionReset) {
514251
- this.state.queuedMessages = this.state.queuedMessages.filter((item) => item.mode === "channel");
514705
+ this.state.queuedMessages = this.state.queuedMessages.filter((item) => item.mode === "channel" || item.mode === "channel-command");
514252
514706
  this.queueFlushBatchRemaining = 0;
514253
514707
  this.queueSteerInFlight = void 0;
514254
514708
  }
@@ -514677,10 +515131,12 @@ var BlunTUI = class {
514677
515131
  children.splice(0, children.length, ...newChildren);
514678
515132
  }
514679
515133
  showStatus(message, color) {
515134
+ this.telegramRemoteCommandContext?.responses.push(message);
514680
515135
  this.state.transcriptContainer.addChild(new StatusMessageComponent(message, color));
514681
515136
  this.state.ui.requestRender();
514682
515137
  }
514683
515138
  showNotice(title, detail) {
515139
+ this.telegramRemoteCommandContext?.responses.push(detail === void 0 ? title : `${title}\n${detail}`);
514684
515140
  this.state.transcriptContainer.addChild(new NoticeMessageComponent(title, detail));
514685
515141
  this.state.ui.requestRender();
514686
515142
  }
@@ -515669,7 +516125,8 @@ async function runShell(opts, version, updateStartupNotice) {
515669
516125
  duration_ms: Date.now() - startedAt,
515670
516126
  config_ms: configMs,
515671
516127
  init_ms: initMs,
515672
- mcp_ms: mcpMs
516128
+ mcp_ms: mcpMs,
516129
+ ...tui.getStartupPhaseMs()
515673
516130
  });
515674
516131
  } catch (error) {
515675
516132
  removeCrashHandlers();