kfc-code-cli 0.0.1-alpha.17 → 0.0.1-alpha.19

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/dist/main.mjs CHANGED
@@ -5214,6 +5214,7 @@ var AgentTool = class {
5214
5214
  name = "Agent";
5215
5215
  description;
5216
5216
  inputSchema = AgentToolInputSchema;
5217
+ isConcurrencySafe = (_input) => true;
5217
5218
  constructor(subagentHost, parentAgentId, backgroundManager, typeRegistry) {
5218
5219
  this.subagentHost = subagentHost;
5219
5220
  this.parentAgentId = parentAgentId;
@@ -23912,7 +23913,7 @@ const IMAGE_MIME_BY_SUFFIX = Object.freeze({
23912
23913
  ".avif": "image/avif",
23913
23914
  ".svgz": "image/svg+xml"
23914
23915
  });
23915
- const VIDEO_MIME_BY_SUFFIX = Object.freeze({
23916
+ const VIDEO_MIME_BY_SUFFIX$1 = Object.freeze({
23916
23917
  ".mp4": "video/mp4",
23917
23918
  ".mpg": "video/mpeg",
23918
23919
  ".mpeg": "video/mpeg",
@@ -24183,9 +24184,9 @@ function detectFileType(path, header) {
24183
24184
  kind: "image",
24184
24185
  mimeType: IMAGE_MIME_BY_SUFFIX[suffix]
24185
24186
  };
24186
- else if (suffix in VIDEO_MIME_BY_SUFFIX) mediaHint = {
24187
+ else if (suffix in VIDEO_MIME_BY_SUFFIX$1) mediaHint = {
24187
24188
  kind: "video",
24188
- mimeType: VIDEO_MIME_BY_SUFFIX[suffix]
24189
+ mimeType: VIDEO_MIME_BY_SUFFIX$1[suffix]
24189
24190
  };
24190
24191
  if (header !== void 0) {
24191
24192
  const buf = toBuffer(header);
@@ -95567,17 +95568,32 @@ const TuiThemeSchema = z.enum([
95567
95568
  "light",
95568
95569
  "auto"
95569
95570
  ]);
95571
+ const NotificationConditionSchema = z.enum(["unfocused", "always"]);
95572
+ const NotificationsConfigSchema = z.object({
95573
+ enabled: z.boolean(),
95574
+ condition: NotificationConditionSchema
95575
+ });
95570
95576
  const TuiConfigFileSchema = z.object({
95571
95577
  theme: TuiThemeSchema.optional(),
95572
- editor: z.object({ command: z.string().optional() }).optional()
95578
+ editor: z.object({ command: z.string().optional() }).optional(),
95579
+ notifications: z.object({
95580
+ enabled: z.boolean().optional(),
95581
+ notification_condition: NotificationConditionSchema.optional()
95582
+ }).optional()
95573
95583
  });
95574
95584
  const TuiConfigSchema = z.object({
95575
95585
  theme: TuiThemeSchema,
95576
- editorCommand: z.string().nullable()
95586
+ editorCommand: z.string().nullable(),
95587
+ notifications: NotificationsConfigSchema
95577
95588
  });
95589
+ const DEFAULT_NOTIFICATIONS_CONFIG = {
95590
+ enabled: true,
95591
+ condition: "unfocused"
95592
+ };
95578
95593
  const DEFAULT_TUI_CONFIG = TuiConfigSchema.parse({
95579
95594
  theme: "auto",
95580
- editorCommand: null
95595
+ editorCommand: null,
95596
+ notifications: DEFAULT_NOTIFICATIONS_CONFIG
95581
95597
  });
95582
95598
  /**
95583
95599
  * Thrown by `loadTuiConfig` when the on-disk TOML cannot be parsed.
@@ -95620,7 +95636,11 @@ function normalizeTuiConfig(config) {
95620
95636
  const command = config.editor?.command?.trim();
95621
95637
  return TuiConfigSchema.parse({
95622
95638
  theme: config.theme ?? DEFAULT_TUI_CONFIG.theme,
95623
- editorCommand: command === void 0 || command.length === 0 ? null : command
95639
+ editorCommand: command === void 0 || command.length === 0 ? null : command,
95640
+ notifications: {
95641
+ enabled: config.notifications?.enabled ?? DEFAULT_NOTIFICATIONS_CONFIG.enabled,
95642
+ condition: config.notifications?.notification_condition ?? DEFAULT_NOTIFICATIONS_CONFIG.condition
95643
+ }
95624
95644
  });
95625
95645
  }
95626
95646
  function renderTuiConfig(config) {
@@ -95632,6 +95652,10 @@ theme = "${config.theme}" # "auto" | "dark" | "light"
95632
95652
 
95633
95653
  [editor]
95634
95654
  command = "${escapeTomlBasicString(config.editorCommand ?? "")}" # Empty uses $VISUAL / $EDITOR
95655
+
95656
+ [notifications]
95657
+ enabled = ${String(config.notifications.enabled)} # true | false
95658
+ notification_condition = "${config.notifications.condition}" # "unfocused" | "always"
95635
95659
  `;
95636
95660
  }
95637
95661
  function escapeTomlBasicString(value) {
@@ -95803,45 +95827,11 @@ function parseSlashInput(input) {
95803
95827
  };
95804
95828
  }
95805
95829
  //#endregion
95806
- //#region src/tui/components/media/image-thumbnail.ts
95807
- /**
95808
- * Transcript-side rendering of a pasted image.
95809
- *
95810
- * On terminals that speak the Kitty graphics protocol or iTerm2 inline
95811
- * image protocol (detected by pi-tui's `getCapabilities()`), we show
95812
- * the actual image. Everywhere else we fall back to a one-line text
95813
- * marker matching the placeholder the user sees in the input box —
95814
- * this keeps the transcript readable on Terminal.app / Linux default
95815
- * terminals / `script` recordings without extra chrome.
95816
- *
95817
- * Height is capped at ~12 rows so a single screenshot can't monopolize
95818
- * the viewport; pi-tui handles proportional scaling internally.
95819
- */
95820
- const MAX_IMAGE_ROWS = 12;
95821
- var ImageThumbnail = class extends Container {
95822
- constructor(attachment, colors) {
95823
- super();
95824
- const caps = getCapabilities();
95825
- if (!(caps.images === "kitty" || caps.images === "iterm2")) {
95826
- this.addChild(new Text(chalk.hex(colors.accent)(` ${attachment.placeholder}`), 0, 0));
95827
- return;
95828
- }
95829
- const image = new Image(Buffer.from(attachment.bytes).toString("base64"), attachment.mime, { fallbackColor: (s) => chalk.hex(colors.textDim)(s) }, {
95830
- maxHeightCells: MAX_IMAGE_ROWS,
95831
- filename: attachment.placeholder
95832
- }, {
95833
- widthPx: attachment.width,
95834
- heightPx: attachment.height
95835
- });
95836
- this.addChild(image);
95837
- }
95838
- };
95839
- //#endregion
95840
95830
  //#region src/tui/symbols.ts
95841
95831
  const STATUS_BULLET = "⏺︎ ";
95842
95832
  //#endregion
95843
95833
  //#region src/tui/components/messages/assistant-message.ts
95844
- const INDENT$1 = " ";
95834
+ const INDENT$2 = " ";
95845
95835
  var AssistantMessageComponent = class {
95846
95836
  contentContainer;
95847
95837
  markdownTheme;
@@ -95868,18 +95858,36 @@ var AssistantMessageComponent = class {
95868
95858
  }
95869
95859
  render(width) {
95870
95860
  if (this.lastText.trim().length === 0) return [];
95871
- const prefix = this.showBullet ? STATUS_BULLET : INDENT$1;
95861
+ const prefix = this.showBullet ? STATUS_BULLET : INDENT$2;
95872
95862
  const contentWidth = Math.max(1, width - visibleWidth(prefix));
95873
95863
  const contentLines = this.contentContainer.render(contentWidth);
95874
95864
  const lines = [""];
95875
95865
  for (let i = 0; i < contentLines.length; i++) {
95876
- const p = i === 0 && this.showBullet ? chalk.hex(this.bulletColor)(STATUS_BULLET) : INDENT$1;
95866
+ const p = i === 0 && this.showBullet ? chalk.hex(this.bulletColor)(STATUS_BULLET) : INDENT$2;
95877
95867
  lines.push(p + contentLines[i]);
95878
95868
  }
95879
95869
  return lines;
95880
95870
  }
95881
95871
  };
95882
95872
  //#endregion
95873
+ //#region src/tui/components/messages/background-agent-status.ts
95874
+ const INDENT$1 = " ";
95875
+ const FAIL_MARK = "✗ ";
95876
+ var BackgroundAgentStatusComponent = class {
95877
+ constructor(data, colors) {
95878
+ this.data = data;
95879
+ this.colors = colors;
95880
+ }
95881
+ invalidate() {}
95882
+ render(width) {
95883
+ const tone = this.data.phase === "started" ? this.colors.primary : this.data.phase === "completed" ? this.colors.success : this.colors.error;
95884
+ const bullet = this.data.phase === "failed" ? chalk.hex(tone)(FAIL_MARK) : chalk.hex(tone)(STATUS_BULLET);
95885
+ const textComponent = new Text(chalk.hex(tone)(this.data.headline) + (this.data.detail !== void 0 && this.data.detail.length > 0 ? chalk.hex(this.colors.textDim)(` (${this.data.detail})`) : ""), 0, 0);
95886
+ const contentWidth = Math.max(1, width - 2);
95887
+ return ["", ...textComponent.render(contentWidth).map((line, index) => (index === 0 ? bullet : INDENT$1) + line)];
95888
+ }
95889
+ };
95890
+ //#endregion
95883
95891
  //#region src/tui/components/messages/skill-activation.ts
95884
95892
  /**
95885
95893
  * Skill activation card.
@@ -97322,15 +97330,77 @@ function formatActivityLine(verb, toolName, args) {
97322
97330
  return keyArg ? `${verb} ${toolName} (${keyArg})` : `${verb} ${toolName}`;
97323
97331
  }
97324
97332
  //#endregion
97325
- //#region src/tui/components/messages/user-message.ts
97333
+ //#region src/tui/components/media/image-thumbnail.ts
97326
97334
  /**
97327
- * Renders a user message in the transcript.
97335
+ * Transcript-side rendering of a pasted image.
97336
+ *
97337
+ * On terminals that speak the Kitty graphics protocol or iTerm2 inline
97338
+ * image protocol (detected by pi-tui's `getCapabilities()`), we show
97339
+ * the actual image. Everywhere else we fall back to a one-line text
97340
+ * marker matching the placeholder the user sees in the input box —
97341
+ * this keeps the transcript readable on Terminal.app / Linux default
97342
+ * terminals / `script` recordings without extra chrome.
97343
+ *
97344
+ * Height is capped at ~12 rows so a single screenshot can't monopolize
97345
+ * the viewport; pi-tui handles proportional scaling internally.
97328
97346
  */
97329
- var UserMessageComponent = class extends Container {
97330
- constructor(text, colors) {
97347
+ const MAX_IMAGE_ROWS = 12;
97348
+ const MAX_IMAGE_WIDTH = 40;
97349
+ var ImageThumbnail = class extends Container {
97350
+ constructor(attachment, colors) {
97331
97351
  super();
97332
- this.addChild(new Spacer(1));
97333
- this.addChild(new Text(chalk.hex(colors.roleUser).bold("✨ " + text), 0, 0));
97352
+ const caps = getCapabilities();
97353
+ if (!(caps.images === "kitty" || caps.images === "iterm2")) {
97354
+ this.addChild(new Text(chalk.hex(colors.accent)(attachment.placeholder), 0, 0));
97355
+ return;
97356
+ }
97357
+ const image = new Image(Buffer.from(attachment.bytes).toString("base64"), attachment.mime, { fallbackColor: (s) => chalk.hex(colors.textDim)(s) }, {
97358
+ maxHeightCells: MAX_IMAGE_ROWS,
97359
+ maxWidthCells: MAX_IMAGE_WIDTH,
97360
+ filename: attachment.placeholder
97361
+ }, {
97362
+ widthPx: attachment.width,
97363
+ heightPx: attachment.height
97364
+ });
97365
+ this.addChild(image);
97366
+ }
97367
+ };
97368
+ //#endregion
97369
+ //#region src/tui/components/messages/user-message.ts
97370
+ const BULLET = "✨ ";
97371
+ var UserMessageComponent = class {
97372
+ text;
97373
+ color;
97374
+ textComponent;
97375
+ spacerComponent;
97376
+ imageThumbnails;
97377
+ constructor(text, colors, images) {
97378
+ this.text = text;
97379
+ this.color = colors.roleUser;
97380
+ this.textComponent = new Text(chalk.hex(colors.roleUser).bold(text), 0, 0);
97381
+ this.spacerComponent = new Spacer(1);
97382
+ this.imageThumbnails = images?.map((img) => new ImageThumbnail(img, colors)) ?? [];
97383
+ }
97384
+ invalidate() {
97385
+ this.textComponent.invalidate();
97386
+ for (const img of this.imageThumbnails) img.invalidate?.();
97387
+ }
97388
+ render(width) {
97389
+ const bullet = chalk.hex(this.color).bold(BULLET);
97390
+ const bulletWidth = visibleWidth(bullet);
97391
+ const contentWidth = Math.max(1, width - bulletWidth);
97392
+ const lines = [];
97393
+ for (const line of this.spacerComponent.render(width)) lines.push(line);
97394
+ const textLines = this.textComponent.render(contentWidth);
97395
+ for (let i = 0; i < textLines.length; i++) {
97396
+ const prefix = i === 0 ? bullet : " ".repeat(bulletWidth);
97397
+ lines.push(prefix + textLines[i]);
97398
+ }
97399
+ for (const thumbnail of this.imageThumbnails) {
97400
+ const imageLines = thumbnail.render(contentWidth);
97401
+ for (const line of imageLines) lines.push(" ".repeat(bulletWidth) + line);
97402
+ }
97403
+ return lines;
97334
97404
  }
97335
97405
  };
97336
97406
  //#endregion
@@ -97387,6 +97457,82 @@ var WelcomeComponent = class {
97387
97457
  }
97388
97458
  };
97389
97459
  //#endregion
97460
+ //#region src/tui/utils/terminal-notification.ts
97461
+ const MAX_MESSAGE_LENGTH = 240;
97462
+ const ESC$1 = "\x1B";
97463
+ const BEL = "\x07";
97464
+ const ST = "\\";
97465
+ function notifyTerminalOnce(state, key, notification) {
97466
+ const { enabled, condition } = state.appState.notifications;
97467
+ if (!enabled) return;
97468
+ if (state.terminalNotificationKeys.has(key)) return;
97469
+ state.terminalNotificationKeys.add(key);
97470
+ if (condition === "unfocused" && state.terminalFocused) return;
97471
+ emitTerminalNotification(state.terminal, notification, {
97472
+ supportsOsc9: state.osc9Supported,
97473
+ insideTmux: state.insideTmux
97474
+ });
97475
+ }
97476
+ function emitTerminalNotification(terminal, notification, options = {}) {
97477
+ const sequences = buildTerminalNotificationSequences(notification, {
97478
+ supportsOsc9: options.supportsOsc9 ?? supportsOsc9Notification(),
97479
+ insideTmux: options.insideTmux ?? isInsideTmux()
97480
+ });
97481
+ for (const sequence of sequences) terminal.write(sequence);
97482
+ }
97483
+ function formatNotification(notification) {
97484
+ const title = sanitizeNotificationText(notification.title);
97485
+ const body = sanitizeNotificationText(notification.body ?? "");
97486
+ return (title.length > 0 && body.length > 0 ? `${title}: ${body}` : title.length > 0 ? title : body).slice(0, MAX_MESSAGE_LENGTH);
97487
+ }
97488
+ /**
97489
+ * Build the wire bytes for a terminal notification.
97490
+ *
97491
+ * - `supportsOsc9 === true`: emit a single OSC 9 sequence — the modern
97492
+ * desktop-notification path used by iTerm2, WezTerm, Kitty, Ghostty
97493
+ * and Warp.
97494
+ * - `supportsOsc9 === false`: fall back to a bare BEL so the user still
97495
+ * gets the system bell on terminals that don't recognize OSC 9.
97496
+ *
97497
+ * When `insideTmux === true` and we're emitting OSC 9, wrap the sequence
97498
+ * in a tmux DCS passthrough (`ESC P tmux ; <payload> ESC \`) and double
97499
+ * any `ESC` bytes inside the payload — otherwise tmux swallows the OSC.
97500
+ * BEL is single-byte and passes through tmux unchanged, so no wrap is
97501
+ * needed in the fallback path.
97502
+ */
97503
+ function buildTerminalNotificationSequences(notification, options) {
97504
+ const message = formatNotification(notification);
97505
+ if (message.length === 0) return [];
97506
+ if (!options.supportsOsc9) return [BEL];
97507
+ const osc9 = `${ESC$1}]9;${message}${BEL}`;
97508
+ if (options.insideTmux) return [`${ESC$1}Ptmux;${osc9.replaceAll(ESC$1, `${ESC$1}${ESC$1}`)}${ESC$1}${ST}`];
97509
+ return [osc9];
97510
+ }
97511
+ /**
97512
+ * Best-effort detection of OSC 9 desktop-notification support, driven
97513
+ * entirely off well-known environment variables. The allow-list mirrors
97514
+ * Codex's terminal-detection crate — it is intentionally short and
97515
+ * conservative because BEL is safe everywhere, while shipping OSC 9 to
97516
+ * a terminal that doesn't grok it would print escape garbage on screen.
97517
+ */
97518
+ function supportsOsc9Notification(env = process.env) {
97519
+ const termProgram = env.TERM_PROGRAM ?? "";
97520
+ if (termProgram === "iTerm.app" || termProgram === "WezTerm" || termProgram === "ghostty" || termProgram === "WarpTerminal") return true;
97521
+ const term = env.TERM ?? "";
97522
+ if (term === "xterm-kitty" || term === "xterm-ghostty") return true;
97523
+ return false;
97524
+ }
97525
+ function isInsideTmux(env = process.env) {
97526
+ return (env.TMUX ?? "").length > 0;
97527
+ }
97528
+ function sanitizeNotificationText(value) {
97529
+ return Array.from(value).map((ch) => isControlCharacter(ch) ? " " : ch).join("").replaceAll(/\s+/g, " ").trim();
97530
+ }
97531
+ function isControlCharacter(ch) {
97532
+ const code = ch.codePointAt(0) ?? 0;
97533
+ return code >= 0 && code <= 31 || code >= 127 && code <= 159;
97534
+ }
97535
+ //#endregion
97390
97536
  //#region src/tui/handlers/helpers.ts
97391
97537
  const TOAST_TTL_MS = 5e3;
97392
97538
  let transcriptIdCounter = 0;
@@ -97448,6 +97594,7 @@ function flushTurnBuffers(ectx, nextMode = "idle") {
97448
97594
  */
97449
97595
  function finalizeTurn(ectx, sendQueued) {
97450
97596
  if (!ectx.state.appState.isStreaming) return;
97597
+ const completedTurnKey = ectx.state.currentTurnId ?? `local:${String(ectx.state.appState.streamingStartTime)}`;
97451
97598
  flushTurnBuffers(ectx, "idle");
97452
97599
  ectx.state.activeToolCalls.clear();
97453
97600
  ectx.state.currentTurnId = void 0;
@@ -97459,7 +97606,9 @@ function finalizeTurn(ectx, sendQueued) {
97459
97606
  streamingPhase: "idle"
97460
97607
  });
97461
97608
  ectx.resetLivePane();
97462
- setTimeout(() => sendQueued(next), 0);
97609
+ if (next !== void 0) setTimeout(() => {
97610
+ sendQueued(next);
97611
+ }, 0);
97463
97612
  return;
97464
97613
  }
97465
97614
  ectx.setAppState({
@@ -97467,6 +97616,10 @@ function finalizeTurn(ectx, sendQueued) {
97467
97616
  streamingPhase: "idle"
97468
97617
  });
97469
97618
  ectx.resetLivePane();
97619
+ notifyTerminalOnce(ectx.state, `turn-complete:${completedTurnKey}`, {
97620
+ title: "Kimi Code task complete",
97621
+ body: ectx.state.appState.sessionTitle ?? void 0
97622
+ });
97470
97623
  }
97471
97624
  /** Push a toast into the live list and schedule auto-dismiss. */
97472
97625
  function pushToast(ectx, notification) {
@@ -97507,17 +97660,8 @@ function pushToast(ectx, notification) {
97507
97660
  function createTranscriptComponent(state, entry) {
97508
97661
  switch (entry.kind) {
97509
97662
  case "user": {
97510
- const msg = new UserMessageComponent(entry.content, state.colors);
97511
- if (entry.imageAttachmentIds !== void 0 && entry.imageAttachmentIds.length > 0) {
97512
- const wrap = new Container();
97513
- wrap.addChild(msg);
97514
- for (const id of entry.imageAttachmentIds) {
97515
- const attachment = state.imageStore.get(id);
97516
- if (attachment !== void 0) wrap.addChild(new ImageThumbnail(attachment, state.colors));
97517
- }
97518
- return wrap;
97519
- }
97520
- return msg;
97663
+ const images = entry.imageAttachmentIds?.map((id) => state.imageStore.get(id)).filter((a) => a?.kind === "image");
97664
+ return new UserMessageComponent(entry.content, state.colors, images);
97521
97665
  }
97522
97666
  case "skill_activation": return new SkillActivationComponent(entry.skillName ?? entry.content, entry.skillArgs, state.colors);
97523
97667
  case "assistant": return createAssistantEntry(state, entry.content);
@@ -97528,8 +97672,11 @@ function createTranscriptComponent(state, entry) {
97528
97672
  if (state.toolOutputExpanded) tc.setExpanded(true);
97529
97673
  return tc;
97530
97674
  }
97675
+ if (entry.backgroundAgentStatus !== void 0) return new BackgroundAgentStatusComponent(entry.backgroundAgentStatus, state.colors);
97676
+ return entry.renderMode === "notice" ? createNoticeEntry(entry.content, entry.detail, state.colors) : createStatusEntry(entry.content, state.colors, entry.color);
97677
+ case "status":
97678
+ if (entry.backgroundAgentStatus !== void 0) return new BackgroundAgentStatusComponent(entry.backgroundAgentStatus, state.colors);
97531
97679
  return entry.renderMode === "notice" ? createNoticeEntry(entry.content, entry.detail, state.colors) : createStatusEntry(entry.content, state.colors, entry.color);
97532
- case "status": return entry.renderMode === "notice" ? createNoticeEntry(entry.content, entry.detail, state.colors) : createStatusEntry(entry.content, state.colors, entry.color);
97533
97680
  default: return null;
97534
97681
  }
97535
97682
  }
@@ -97612,42 +97759,54 @@ function emitMuted(state, message) {
97612
97759
  }
97613
97760
  //#endregion
97614
97761
  //#region src/tui/input/image-placeholder.ts
97615
- const PLACEHOLDER_REGEX = /\[image #(\d+) \((\d+)×(\d+)\)\]/g;
97616
- function extractImageAttachments(text, store) {
97762
+ const PLACEHOLDER_REGEX = /\[(image|video) #(\d+) (?:(\(\d+×\d+\))|([^\]]+))\]/g;
97763
+ function extractMediaAttachments(text, store) {
97617
97764
  const parts = [];
97618
97765
  const cleanedSegments = [];
97619
- const attachmentIds = [];
97766
+ const imageAttachmentIds = [];
97620
97767
  let cursor = 0;
97621
- let hasImages = false;
97768
+ let hasMedia = false;
97622
97769
  PLACEHOLDER_REGEX.lastIndex = 0;
97623
97770
  let match;
97624
97771
  while ((match = PLACEHOLDER_REGEX.exec(text)) !== null) {
97625
- const [literal, idStr] = match;
97772
+ const [literal, kind, idStr] = match;
97773
+ if (kind !== "image" && kind !== "video") continue;
97626
97774
  if (idStr === void 0) continue;
97627
97775
  const id = Number.parseInt(idStr, 10);
97628
97776
  const attachment = store.get(id);
97629
97777
  if (attachment === void 0) continue;
97778
+ if (attachment.kind !== kind) continue;
97630
97779
  const before = text.slice(cursor, match.index);
97631
97780
  pushText(parts, before);
97632
97781
  cleanedSegments.push(before);
97633
- parts.push(buildImagePart(attachment));
97634
- attachmentIds.push(id);
97635
- hasImages = true;
97782
+ if (attachment.kind === "video") {
97783
+ pushText(parts, pathTextForVideo(attachment));
97784
+ cleanedSegments.push(pathTextForVideo(attachment));
97785
+ } else {
97786
+ parts.push(buildImagePart(attachment));
97787
+ imageAttachmentIds.push(id);
97788
+ }
97789
+ hasMedia = true;
97636
97790
  cursor = match.index + literal.length;
97637
97791
  }
97638
97792
  const tail = text.slice(cursor);
97639
97793
  pushText(parts, tail);
97640
97794
  cleanedSegments.push(tail);
97641
97795
  return {
97642
- parts: hasImages ? parts : [],
97643
- hasImages,
97644
- cleanedText: cleanedSegments.join("").replaceAll(/\s+/g, " ").trim(),
97645
- attachmentIds
97796
+ parts: hasMedia ? parts : [],
97797
+ hasMedia,
97798
+ cleanedText: cleanedSegments.join("").trim(),
97799
+ imageAttachmentIds
97646
97800
  };
97647
97801
  }
97648
97802
  function pushText(parts, segment) {
97649
97803
  if (segment.length === 0) return;
97650
97804
  if (segment.trim().length === 0) return;
97805
+ const last = parts.at(-1);
97806
+ if (last?.type === "text") {
97807
+ last.text += segment;
97808
+ return;
97809
+ }
97651
97810
  parts.push({
97652
97811
  type: "text",
97653
97812
  text: segment
@@ -97660,6 +97819,9 @@ function buildImagePart(att) {
97660
97819
  image_url: { url: `data:${att.mime};base64,${base64}` }
97661
97820
  };
97662
97821
  }
97822
+ function pathTextForVideo(att) {
97823
+ return att.sourcePath;
97824
+ }
97663
97825
  //#endregion
97664
97826
  //#region src/tui/theme/colors.ts
97665
97827
  /**
@@ -97893,7 +98055,7 @@ function createMarkdownTheme(colors) {
97893
98055
  linkUrl: (text) => muted(text),
97894
98056
  code: (text) => chalk.hex(colors.primary)(text),
97895
98057
  codeBlock: (text) => text,
97896
- codeBlockBorder: (text) => border(text),
98058
+ codeBlockBorder: (text) => muted(text),
97897
98059
  quote: (text) => dim(text),
97898
98060
  quoteBorder: (text) => dim(text),
97899
98061
  hr: (text) => border(text),
@@ -97956,7 +98118,8 @@ function mutateBottomSlot(state, slot, mutate) {
97956
98118
  const width = state.terminal?.columns ?? 80;
97957
98119
  const previousHeight = renderHeight(slot, width);
97958
98120
  mutate();
97959
- if (previousHeight !== renderHeight(slot, width)) state.ui.requestRender(true);
98121
+ if (previousHeight !== renderHeight(slot, width)) if (state.bottomLayoutState.hasFilledViewport) state.ui.requestRender(true);
98122
+ else state.ui.requestRender();
97960
98123
  else state.ui.requestRender();
97961
98124
  }
97962
98125
  function renderHeight(component, width) {
@@ -98017,6 +98180,7 @@ function clearTranscriptAndRedraw(state) {
98017
98180
  state.pendingToolComponents.clear();
98018
98181
  state.streamingComponent = void 0;
98019
98182
  state.streamingTranscriptEntry = void 0;
98183
+ state.bottomLayoutState.hasFilledViewport = false;
98020
98184
  state.todoPanel.clear();
98021
98185
  state.todoPanelContainer.clear();
98022
98186
  state.imageStore.clear();
@@ -98050,6 +98214,7 @@ function applyTheme(state, theme, hooks, resolved) {
98050
98214
  }
98051
98215
  //#endregion
98052
98216
  //#region src/tui/handlers/subagent.ts
98217
+ const MAX_BACKGROUND_FIELD_LENGTH = 240;
98053
98218
  function handleSubagentSourceEvent(ectx, payload) {
98054
98219
  const tc = ectx.state.pendingToolComponents.get(payload.parent_tool_call_id);
98055
98220
  if (tc === void 0) return;
@@ -98089,7 +98254,10 @@ function handleSubagentSourceEvent(ectx, payload) {
98089
98254
  }
98090
98255
  function handleSubagentSpawned(ectx, data) {
98091
98256
  if (data.run_in_background) {
98257
+ const meta = buildBackgroundAgentMetadata(ectx, data);
98258
+ ectx.state.backgroundAgentMetadata.set(data.agent_id, meta);
98092
98259
  ectx.state.backgroundAgents.add(data.agent_id);
98260
+ appendBackgroundAgentEntry(ectx, "started", meta);
98093
98261
  syncBackgroundAgentBadge(ectx);
98094
98262
  return;
98095
98263
  }
@@ -98109,7 +98277,13 @@ function handleSubagentSpawned(ectx, data) {
98109
98277
  });
98110
98278
  }
98111
98279
  function handleSubagentCompleted(ectx, data) {
98280
+ const backgroundMeta = ectx.state.backgroundAgentMetadata.get(data.agent_id);
98112
98281
  if (ectx.state.backgroundAgents.delete(data.agent_id)) syncBackgroundAgentBadge(ectx);
98282
+ if (backgroundMeta !== void 0) {
98283
+ ectx.state.backgroundAgentMetadata.delete(data.agent_id);
98284
+ appendBackgroundAgentEntry(ectx, "completed", backgroundMeta, { resultSummary: data.result_summary });
98285
+ return;
98286
+ }
98113
98287
  const tc = ectx.state.pendingToolComponents.get(data.parent_tool_call_id);
98114
98288
  if (tc === void 0) return;
98115
98289
  tc.onSubagentCompleted({
@@ -98118,15 +98292,66 @@ function handleSubagentCompleted(ectx, data) {
98118
98292
  });
98119
98293
  }
98120
98294
  function handleSubagentFailed(ectx, data) {
98295
+ const backgroundMeta = ectx.state.backgroundAgentMetadata.get(data.agent_id);
98121
98296
  if (ectx.state.backgroundAgents.delete(data.agent_id)) syncBackgroundAgentBadge(ectx);
98297
+ if (backgroundMeta !== void 0) {
98298
+ ectx.state.backgroundAgentMetadata.delete(data.agent_id);
98299
+ appendBackgroundAgentEntry(ectx, "failed", backgroundMeta, { error: data.error });
98300
+ return;
98301
+ }
98122
98302
  const tc = ectx.state.pendingToolComponents.get(data.parent_tool_call_id);
98123
98303
  if (tc === void 0) return;
98124
98304
  tc.onSubagentFailed({ error: data.error });
98125
98305
  }
98306
+ function buildBackgroundAgentMetadata(ectx, data) {
98307
+ const parent = ectx.state.activeToolCalls.get(data.parent_tool_call_id);
98308
+ return {
98309
+ agentId: data.agent_id,
98310
+ parentToolCallId: data.parent_tool_call_id,
98311
+ agentName: data.agent_name,
98312
+ description: getAgentDescription$1(parent)
98313
+ };
98314
+ }
98315
+ function getAgentDescription$1(toolCall) {
98316
+ if (toolCall === void 0) return void 0;
98317
+ const description = toolCall.args["description"];
98318
+ return typeof description === "string" ? description : void 0;
98319
+ }
98320
+ function appendBackgroundAgentEntry(ectx, phase, meta, extras = void 0) {
98321
+ const status = formatBackgroundAgentTranscript(phase, meta, extras);
98322
+ const entry = {
98323
+ id: nextTranscriptId(),
98324
+ kind: "status",
98325
+ turnId: ectx.state.currentTurnId,
98326
+ renderMode: "plain",
98327
+ content: status.headline,
98328
+ detail: status.detail,
98329
+ backgroundAgentStatus: status
98330
+ };
98331
+ ectx.addTranscriptEntry(entry);
98332
+ }
98126
98333
  function syncBackgroundAgentBadge(ectx) {
98127
98334
  ectx.state.footer.setBackgroundAgentCount(ectx.state.backgroundAgents.size);
98128
98335
  ectx.state.ui.requestRender();
98129
98336
  }
98337
+ function normalizeBackgroundField(value) {
98338
+ if (value === void 0) return void 0;
98339
+ const collapsed = value.trim().replaceAll(/\s+/g, " ");
98340
+ if (collapsed.length === 0) return void 0;
98341
+ if (collapsed.length <= MAX_BACKGROUND_FIELD_LENGTH) return collapsed;
98342
+ return `${collapsed.slice(0, MAX_BACKGROUND_FIELD_LENGTH - 3)}...`;
98343
+ }
98344
+ function formatBackgroundAgentTranscript(phase, meta, extras = void 0) {
98345
+ const subject = normalizeBackgroundField(meta.agentName) !== void 0 ? `${normalizeBackgroundField(meta.agentName)} agent` : "agent";
98346
+ const headline = phase === "started" ? `${subject} started in background` : phase === "completed" ? `${subject} completed in background` : `${subject} failed in background`;
98347
+ const tail = phase === "failed" ? normalizeBackgroundField(extras?.error) : void 0;
98348
+ const detailParts = [normalizeBackgroundField(meta.description), tail].filter((part) => part !== void 0);
98349
+ return {
98350
+ phase,
98351
+ headline,
98352
+ detail: detailParts.length > 0 ? detailParts.join(" · ") : void 0
98353
+ };
98354
+ }
98130
98355
  //#endregion
98131
98356
  //#region src/utils/git/git-ls-files.ts
98132
98357
  /**
@@ -99328,10 +99553,14 @@ var ImageAttachmentStore = class {
99328
99553
  nextId = 1;
99329
99554
  byId = /* @__PURE__ */ new Map();
99330
99555
  add(bytes, mime, width, height) {
99556
+ return this.addImage(bytes, mime, width, height);
99557
+ }
99558
+ addImage(bytes, mime, width, height) {
99331
99559
  const id = this.nextId;
99332
99560
  this.nextId += 1;
99333
99561
  const attachment = {
99334
99562
  id,
99563
+ kind: "image",
99335
99564
  bytes,
99336
99565
  mime,
99337
99566
  width,
@@ -99341,6 +99570,23 @@ var ImageAttachmentStore = class {
99341
99570
  this.byId.set(id, attachment);
99342
99571
  return attachment;
99343
99572
  }
99573
+ addVideo(mime, sourcePath, filename) {
99574
+ const id = this.nextId;
99575
+ this.nextId += 1;
99576
+ const normalizedFilename = basenameLike(filename !== void 0 && filename !== "" ? filename : sourcePath);
99577
+ const label = sanitizeVideoLabel(normalizedFilename.length > 0 ? normalizedFilename : mime);
99578
+ const attachment = {
99579
+ id,
99580
+ kind: "video",
99581
+ mime,
99582
+ filename: normalizedFilename,
99583
+ sourcePath,
99584
+ label,
99585
+ placeholder: formatVideoPlaceholder(id, label)
99586
+ };
99587
+ this.byId.set(id, attachment);
99588
+ return attachment;
99589
+ }
99344
99590
  get(id) {
99345
99591
  return this.byId.get(id);
99346
99592
  }
@@ -99355,6 +99601,21 @@ var ImageAttachmentStore = class {
99355
99601
  function formatPlaceholder(id, width, height) {
99356
99602
  return `[image #${String(id)} (${String(width)}×${String(height)})]`;
99357
99603
  }
99604
+ function formatVideoPlaceholder(id, label) {
99605
+ return `[video #${String(id)} ${sanitizeVideoLabel(label)}]`;
99606
+ }
99607
+ function sanitizeVideoLabel(raw) {
99608
+ let label = "";
99609
+ for (const char of raw) {
99610
+ const code = char.codePointAt(0);
99611
+ label += code < 32 || code === 127 || char === "[" || char === "]" ? "_" : char;
99612
+ }
99613
+ label = label.trim();
99614
+ return label.length > 0 ? label : "video";
99615
+ }
99616
+ function basenameLike(raw) {
99617
+ return raw.split(/[\\/]/).filter((part) => part.length > 0).at(-1) ?? raw;
99618
+ }
99358
99619
  //#endregion
99359
99620
  //#region src/tui/reverse-rpc/base-controller.ts
99360
99621
  var ReverseRpcController = class {
@@ -99461,6 +99722,7 @@ function createTUIState(options) {
99461
99722
  colors,
99462
99723
  styles,
99463
99724
  markdownTheme,
99725
+ bottomLayoutState: { hasFilledViewport: false },
99464
99726
  resolvedTheme,
99465
99727
  appState: { ...initialAppState },
99466
99728
  startupState: "pending",
@@ -99468,6 +99730,10 @@ function createTUIState(options) {
99468
99730
  livePane: { ...INITIAL_LIVE_PANE },
99469
99731
  transcriptEntries: [],
99470
99732
  toasts: [],
99733
+ terminalNotificationKeys: /* @__PURE__ */ new Set(),
99734
+ terminalFocused: true,
99735
+ osc9Supported: supportsOsc9Notification(),
99736
+ insideTmux: isInsideTmux(),
99471
99737
  loadingAnimation: void 0,
99472
99738
  phaseSpinner: void 0,
99473
99739
  activeThinkingComponent: void 0,
@@ -99476,11 +99742,13 @@ function createTUIState(options) {
99476
99742
  activeCompactionBlock: void 0,
99477
99743
  toolOutputExpanded: false,
99478
99744
  lastActivityMode: void 0,
99745
+ terminalProgressActive: false,
99479
99746
  lastHistoryContent: void 0,
99480
99747
  pendingToolComponents: /* @__PURE__ */ new Map(),
99481
99748
  pendingAgentGroup: null,
99482
99749
  pendingReadGroup: null,
99483
99750
  backgroundAgents: /* @__PURE__ */ new Set(),
99751
+ backgroundAgentMetadata: /* @__PURE__ */ new Map(),
99484
99752
  sessions: [],
99485
99753
  loadingSessions: false,
99486
99754
  showingSessionPicker: false,
@@ -99629,11 +99897,13 @@ function nextQueueId(state) {
99629
99897
  state.queueIdCounter += 1;
99630
99898
  return `q-${String(state.queueIdCounter)}`;
99631
99899
  }
99632
- function enqueueMessage(state, text) {
99900
+ function enqueueMessage(state, text, options) {
99633
99901
  state.queuedMessages.push({
99634
99902
  id: nextQueueId(state),
99635
99903
  kind: "message",
99636
- text
99904
+ text,
99905
+ ...options?.parts !== void 0 ? { parts: options.parts } : {},
99906
+ ...options?.imageAttachmentIds !== void 0 && options.imageAttachmentIds.length > 0 ? { imageAttachmentIds: options.imageAttachmentIds } : {}
99637
99907
  });
99638
99908
  }
99639
99909
  function queuedMessageText(item) {
@@ -99776,7 +100046,7 @@ function sendSkillActivation(state, addEntry, skillName, skillArgs, fullPrompt)
99776
100046
  /** Send from user input: enqueue if busy, otherwise send immediately. */
99777
100047
  function sendMessage(state, addEntry, input, options) {
99778
100048
  if (state.appState.isStreaming || state.appState.isCompacting) {
99779
- enqueueMessage(state, input);
100049
+ enqueueMessage(state, input, options);
99780
100050
  return;
99781
100051
  }
99782
100052
  sendMessageInternal(state, addEntry, input, options);
@@ -99836,11 +100106,11 @@ function handleUserInput(state, text, hooks, onSlash) {
99836
100106
  sendNormalUserInput(state, text, hooks);
99837
100107
  }
99838
100108
  function sendNormalUserInput(state, text, hooks) {
99839
- const extraction = extractImageAttachments(text, state.imageStore);
100109
+ const extraction = extractMediaAttachments(text, state.imageStore);
99840
100110
  const addEntry = (e) => addTranscriptEntry(state, e);
99841
- if (extraction.hasImages) sendMessage(state, addEntry, text, {
100111
+ if (extraction.hasMedia) sendMessage(state, addEntry, text, {
99842
100112
  parts: extraction.parts,
99843
- imageAttachmentIds: extraction.attachmentIds
100113
+ imageAttachmentIds: extraction.imageAttachmentIds
99844
100114
  });
99845
100115
  else sendMessage(state, addEntry, text);
99846
100116
  hooks.refreshQueuePane();
@@ -100090,6 +100360,7 @@ async function hydrateTranscriptFromReplay(state, hooks, sessionId) {
100090
100360
  const projection = projectReplayRecords(replay.records);
100091
100361
  hydrateProjectedEntries(state, projection.entries);
100092
100362
  state.backgroundAgents = new Set(projection.backgroundAgents);
100363
+ state.backgroundAgentMetadata = new Map(projection.backgroundAgentMetadata);
100093
100364
  state.footer.setBackgroundAgentCount(state.backgroundAgents.size);
100094
100365
  state.ui.requestRender();
100095
100366
  if (replay.warnings !== void 0 && replay.warnings.length > 0) emitError(state, `Replay completed with ${String(replay.warnings.length)} warning(s).`);
@@ -100110,7 +100381,8 @@ function projectReplayRecords(records) {
100110
100381
  thinking: [],
100111
100382
  text: []
100112
100383
  },
100113
- backgroundAgents: /* @__PURE__ */ new Set()
100384
+ backgroundAgents: /* @__PURE__ */ new Set(),
100385
+ backgroundAgentMetadata: /* @__PURE__ */ new Map()
100114
100386
  };
100115
100387
  for (const envelope of records) {
100116
100388
  if (envelope.source?.kind === "subagent") {
@@ -100122,7 +100394,8 @@ function projectReplayRecords(records) {
100122
100394
  flushAssistant(state);
100123
100395
  return {
100124
100396
  entries: state.entries,
100125
- backgroundAgents: state.backgroundAgents
100397
+ backgroundAgents: state.backgroundAgents,
100398
+ backgroundAgentMetadata: state.backgroundAgentMetadata
100126
100399
  };
100127
100400
  }
100128
100401
  function projectMainRecord(state, rawRecord) {
@@ -100170,15 +100443,54 @@ function projectMainRecord(state, rawRecord) {
100170
100443
  if (!isObject(data)) return;
100171
100444
  if (data["run_in_background"] !== true) return;
100172
100445
  const agentId = stringValue(data["agent_id"]);
100446
+ const parentToolCallId = stringValue(data["parent_tool_call_id"]);
100173
100447
  if (agentId !== void 0) state.backgroundAgents.add(agentId);
100448
+ if (agentId === void 0 || parentToolCallId === void 0) return;
100449
+ removeReplayToolCallEntry(state.entries, parentToolCallId);
100450
+ const meta = {
100451
+ agentId,
100452
+ parentToolCallId,
100453
+ agentName: stringValue(data["agent_name"]),
100454
+ description: getAgentDescription(state.toolCalls.get(parentToolCallId))
100455
+ };
100456
+ state.backgroundAgentMetadata.set(agentId, meta);
100457
+ const status = formatBackgroundAgentTranscript("started", meta);
100458
+ state.entries.push(entry("status", status.headline, "plain", {
100459
+ detail: status.detail,
100460
+ backgroundAgentStatus: status
100461
+ }));
100462
+ return;
100463
+ }
100464
+ case "subagent_completed": {
100465
+ const data = record.data;
100466
+ if (!isObject(data)) return;
100467
+ const agentId = stringValue(data["agent_id"]);
100468
+ if (agentId !== void 0) state.backgroundAgents.delete(agentId);
100469
+ if (agentId === void 0) return;
100470
+ const meta = state.backgroundAgentMetadata.get(agentId);
100471
+ if (meta === void 0) return;
100472
+ state.backgroundAgentMetadata.delete(agentId);
100473
+ const status = formatBackgroundAgentTranscript("completed", meta, { resultSummary: stringValue(data["result_summary"]) });
100474
+ state.entries.push(entry("status", status.headline, "plain", {
100475
+ detail: status.detail,
100476
+ backgroundAgentStatus: status
100477
+ }));
100174
100478
  return;
100175
100479
  }
100176
- case "subagent_completed":
100177
100480
  case "subagent_failed": {
100178
100481
  const data = record.data;
100179
100482
  if (!isObject(data)) return;
100180
100483
  const agentId = stringValue(data["agent_id"]);
100181
100484
  if (agentId !== void 0) state.backgroundAgents.delete(agentId);
100485
+ if (agentId === void 0) return;
100486
+ const meta = state.backgroundAgentMetadata.get(agentId);
100487
+ if (meta === void 0) return;
100488
+ state.backgroundAgentMetadata.delete(agentId);
100489
+ const status = formatBackgroundAgentTranscript("failed", meta, { error: stringValue(data["error"]) });
100490
+ state.entries.push(entry("status", status.headline, "plain", {
100491
+ detail: status.detail,
100492
+ backgroundAgentStatus: status
100493
+ }));
100182
100494
  return;
100183
100495
  }
100184
100496
  default: return;
@@ -100294,6 +100606,11 @@ function toolCallFromRecord(record) {
100294
100606
  description: stringValue(data["activity_description"])
100295
100607
  };
100296
100608
  }
100609
+ function getAgentDescription(toolCall) {
100610
+ if (toolCall === void 0) return void 0;
100611
+ const description = toolCall.args["description"];
100612
+ return typeof description === "string" ? description : void 0;
100613
+ }
100297
100614
  function toolResultFromRecord(record) {
100298
100615
  const id = stringValue(record.tool_call_id);
100299
100616
  if (id === void 0) return void 0;
@@ -100318,9 +100635,16 @@ function entry(kind, content, renderMode, extras) {
100318
100635
  renderMode,
100319
100636
  content,
100320
100637
  turnId: extras?.turnId,
100321
- toolCallData: extras?.toolCallData
100638
+ detail: extras?.detail,
100639
+ color: extras?.color,
100640
+ toolCallData: extras?.toolCallData,
100641
+ backgroundAgentStatus: extras?.backgroundAgentStatus
100322
100642
  };
100323
100643
  }
100644
+ function removeReplayToolCallEntry(entries, toolCallId) {
100645
+ const idx = entries.findIndex((entry) => entry.kind === "tool_call" && entry.toolCallData?.id === toolCallId);
100646
+ if (idx >= 0) entries.splice(idx, 1);
100647
+ }
100324
100648
  function userContentToText(content) {
100325
100649
  if (typeof content === "string") return content;
100326
100650
  if (!Array.isArray(content)) return "";
@@ -100568,6 +100892,9 @@ function releaseSessionSideEffects(state) {
100568
100892
  state.queueIdCounter = 0;
100569
100893
  state.activeToolCalls.clear();
100570
100894
  state.streamingToolCallArguments?.clear();
100895
+ state.backgroundAgents.clear();
100896
+ state.backgroundAgentMetadata.clear();
100897
+ state.footer.setBackgroundAgentCount(0);
100571
100898
  state.currentTurnId = void 0;
100572
100899
  state.assistantDraft = "";
100573
100900
  state.assistantStreamActive = false;
@@ -101173,7 +101500,8 @@ async function applyThemeChoice(state, theme, hooks) {
101173
101500
  try {
101174
101501
  await saveTuiConfig({
101175
101502
  theme,
101176
- editorCommand: state.appState.editorCommand
101503
+ editorCommand: state.appState.editorCommand,
101504
+ notifications: state.appState.notifications
101177
101505
  });
101178
101506
  } catch (error) {
101179
101507
  emitStatus(state, `Failed to save theme: ${error instanceof Error ? error.message : String(error)}`, state.colors.error);
@@ -101739,6 +102067,10 @@ function handleCompactionEnd(ectx, data, sendQueued) {
101739
102067
  //#region src/tui/handlers/notification.ts
101740
102068
  function handleNotification(ectx, data) {
101741
102069
  pushToast(ectx, data);
102070
+ notifyTerminalOnce(ectx.state, `wire:${data.id}`, {
102071
+ title: data.title,
102072
+ body: data.body
102073
+ });
101742
102074
  }
101743
102075
  //#endregion
101744
102076
  //#region src/tui/handlers/dispatch.ts
@@ -101874,7 +102206,8 @@ async function applyEditorChoice(state, value, hooks) {
101874
102206
  try {
101875
102207
  await saveTuiConfig({
101876
102208
  theme: state.appState.theme,
101877
- editorCommand
102209
+ editorCommand,
102210
+ notifications: state.appState.notifications
101878
102211
  });
101879
102212
  } catch (error) {
101880
102213
  emitStatus(state, `Failed to save editor: ${error instanceof Error ? error.message : String(error)}`, state.colors.error);
@@ -102507,7 +102840,8 @@ var MoonLoader = class extends Text {
102507
102840
  * progress across renders.
102508
102841
  */
102509
102842
  function updateActivityPane(state) {
102510
- const effectiveMode = state.showingSessionPicker ? "hidden" : state.livePane.pendingApproval !== null ? "hidden" : state.appState.isCompacting ? "hidden" : state.livePane.pendingQuestion !== null ? "hidden" : state.livePane.mode === "idle" && state.appState.streamingPhase === "composing" ? "composing" : state.livePane.mode;
102843
+ const effectiveMode = state.showingSessionPicker ? "hidden" : state.livePane.pendingApproval !== null ? "hidden" : state.appState.isCompacting ? "hidden" : state.livePane.pendingQuestion !== null ? "hidden" : state.livePane.mode === "idle" && isVisibleStreamingPhase(state.appState.streamingPhase) ? state.appState.streamingPhase : state.livePane.mode;
102844
+ syncTerminalProgress(state, shouldShowTerminalProgress(state, effectiveMode));
102511
102845
  if (effectiveMode === state.lastActivityMode && (effectiveMode === "waiting" || effectiveMode === "tool")) return;
102512
102846
  state.lastActivityMode = effectiveMode;
102513
102847
  mutateBottomSlot(state, state.activityContainer, () => {
@@ -102550,6 +102884,18 @@ function updateActivityPane(state) {
102550
102884
  }
102551
102885
  });
102552
102886
  }
102887
+ function shouldShowTerminalProgress(state, effectiveMode) {
102888
+ if (state.appState.isCompacting) return true;
102889
+ return effectiveMode === "waiting" || effectiveMode === "thinking" || effectiveMode === "composing" || effectiveMode === "tool";
102890
+ }
102891
+ function isVisibleStreamingPhase(phase) {
102892
+ return phase === "thinking" || phase === "composing";
102893
+ }
102894
+ function syncTerminalProgress(state, active) {
102895
+ if (state.terminalProgressActive === active) return;
102896
+ state.terminal.setProgress(active);
102897
+ state.terminalProgressActive = active;
102898
+ }
102553
102899
  function stopLoader(state) {
102554
102900
  if (state.loadingAnimation) {
102555
102901
  state.loadingAnimation.stop();
@@ -103140,6 +103486,10 @@ function buildNumericHint(count) {
103140
103486
  //#region src/tui/reverse-rpc/approval/ui.ts
103141
103487
  function showApprovalPanel(state, payload) {
103142
103488
  state.livePane.pendingApproval = { data: payload };
103489
+ notifyTerminalOnce(state, `approval:${payload.id}`, {
103490
+ title: "Kimi Code approval required",
103491
+ body: payload.tool_name
103492
+ });
103143
103493
  updateActivityPane(state);
103144
103494
  mountPanel(state, new ApprovalPanelComponent({ data: payload }, (response) => {
103145
103495
  state.approvalController.respond(adaptPanelResponse(response));
@@ -103721,6 +104071,10 @@ var QuestionDialogComponent = class extends Container {
103721
104071
  //#region src/tui/reverse-rpc/question/ui.ts
103722
104072
  function showQuestionDialog(state, payload) {
103723
104073
  state.livePane.pendingQuestion = { data: payload };
104074
+ notifyTerminalOnce(state, `question:${payload.id}`, {
104075
+ title: "Kimi Code needs your answer",
104076
+ body: payload.questions[0]?.question
104077
+ });
103724
104078
  updateActivityPane(state);
103725
104079
  mountPanel(state, new QuestionDialogComponent({ data: payload }, (answers) => {
103726
104080
  state.questionController.respond(answers);
@@ -103745,6 +104099,136 @@ function registerReverseRPCHandlers(state, client) {
103745
104099
  return [client.onRequest("approval.request", createApprovalRequestHandler(state)), client.onRequest("question.ask", createQuestionAskHandler(state))];
103746
104100
  }
103747
104101
  //#endregion
104102
+ //#region src/utils/image/image-mime.ts
104103
+ function parseImageMeta(bytes) {
104104
+ if (isPng(bytes)) return parsePng(bytes);
104105
+ if (isJpeg(bytes)) return parseJpeg(bytes);
104106
+ if (isGif(bytes)) return parseGif(bytes);
104107
+ if (isWebp(bytes)) return parseWebp(bytes);
104108
+ return null;
104109
+ }
104110
+ function isPng(b) {
104111
+ return b.length >= 8 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71 && b[4] === 13 && b[5] === 10 && b[6] === 26 && b[7] === 10;
104112
+ }
104113
+ function parsePng(b) {
104114
+ if (b.length < 24) return null;
104115
+ const width = readUInt32BE(b, 16);
104116
+ const height = readUInt32BE(b, 20);
104117
+ if (width <= 0 || height <= 0) return null;
104118
+ return {
104119
+ mime: "image/png",
104120
+ width,
104121
+ height
104122
+ };
104123
+ }
104124
+ function isJpeg(b) {
104125
+ return b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255;
104126
+ }
104127
+ function parseJpeg(b) {
104128
+ let i = 2;
104129
+ while (i < b.length) {
104130
+ if (b[i] !== 255) {
104131
+ i += 1;
104132
+ continue;
104133
+ }
104134
+ while (i < b.length && b[i] === 255) i += 1;
104135
+ if (i >= b.length) return null;
104136
+ const marker = b[i];
104137
+ i += 1;
104138
+ if (marker === 216 || marker === 217) continue;
104139
+ if (i + 1 >= b.length) return null;
104140
+ const segLen = readUInt16BE(b, i);
104141
+ if (isSofMarker(marker)) {
104142
+ if (i + 7 >= b.length) return null;
104143
+ const height = readUInt16BE(b, i + 3);
104144
+ const width = readUInt16BE(b, i + 5);
104145
+ if (width <= 0 || height <= 0) return null;
104146
+ return {
104147
+ mime: "image/jpeg",
104148
+ width,
104149
+ height
104150
+ };
104151
+ }
104152
+ i += segLen;
104153
+ }
104154
+ return null;
104155
+ }
104156
+ function isSofMarker(marker) {
104157
+ if (marker < 192 || marker > 207) return false;
104158
+ return marker !== 196 && marker !== 200 && marker !== 204;
104159
+ }
104160
+ function isGif(b) {
104161
+ return b.length >= 6 && b[0] === 71 && b[1] === 73 && b[2] === 70 && b[3] === 56 && (b[4] === 55 || b[4] === 57) && b[5] === 97;
104162
+ }
104163
+ function parseGif(b) {
104164
+ if (b.length < 10) return null;
104165
+ const width = readUInt16LE(b, 6);
104166
+ const height = readUInt16LE(b, 8);
104167
+ if (width <= 0 || height <= 0) return null;
104168
+ return {
104169
+ mime: "image/gif",
104170
+ width,
104171
+ height
104172
+ };
104173
+ }
104174
+ function isWebp(b) {
104175
+ return b.length >= 12 && b[0] === 82 && b[1] === 73 && b[2] === 70 && b[3] === 70 && b[8] === 87 && b[9] === 69 && b[10] === 66 && b[11] === 80;
104176
+ }
104177
+ function parseWebp(b) {
104178
+ if (b.length < 30) return null;
104179
+ const chunk = String.fromCharCode(b[12], b[13], b[14], b[15]);
104180
+ if (chunk === "VP8 ") {
104181
+ const widthRaw = readUInt16LE(b, 26);
104182
+ const heightRaw = readUInt16LE(b, 28);
104183
+ const width = widthRaw & 16383;
104184
+ const height = heightRaw & 16383;
104185
+ if (width <= 0 || height <= 0) return null;
104186
+ return {
104187
+ mime: "image/webp",
104188
+ width,
104189
+ height
104190
+ };
104191
+ }
104192
+ if (chunk === "VP8L") {
104193
+ if (b[20] !== 47) return null;
104194
+ const b1 = b[21];
104195
+ const b2 = b[22];
104196
+ const b3 = b[23];
104197
+ const b4 = b[24];
104198
+ const width = 1 + ((b2 & 63) << 8 | b1);
104199
+ const height = 1 + ((b4 & 15) << 10 | b3 << 2 | (b2 & 192) >> 6);
104200
+ if (width <= 0 || height <= 0) return null;
104201
+ return {
104202
+ mime: "image/webp",
104203
+ width,
104204
+ height
104205
+ };
104206
+ }
104207
+ if (chunk === "VP8X") {
104208
+ const width = 1 + readUInt24LE(b, 24);
104209
+ const height = 1 + readUInt24LE(b, 27);
104210
+ if (width <= 0 || height <= 0) return null;
104211
+ return {
104212
+ mime: "image/webp",
104213
+ width,
104214
+ height
104215
+ };
104216
+ }
104217
+ return null;
104218
+ }
104219
+ function readUInt16BE(b, off) {
104220
+ return b[off] << 8 | b[off + 1];
104221
+ }
104222
+ function readUInt16LE(b, off) {
104223
+ return b[off] | b[off + 1] << 8;
104224
+ }
104225
+ function readUInt24LE(b, off) {
104226
+ return b[off] | b[off + 1] << 8 | b[off + 2] << 16;
104227
+ }
104228
+ function readUInt32BE(b, off) {
104229
+ return b[off] * 16777216 + (b[off + 1] << 16) + (b[off + 2] << 8) + b[off + 3] >>> 0;
104230
+ }
104231
+ //#endregion
103748
104232
  //#region src/utils/clipboard/clipboard-native.ts
103749
104233
  /**
103750
104234
  * Optional native clipboard binding.
@@ -103766,32 +104250,89 @@ if (process.env["TERMUX_VERSION"] === void 0 && hasDisplay) try {
103766
104250
  //#endregion
103767
104251
  //#region src/utils/clipboard/clipboard-image.ts
103768
104252
  /**
103769
- * Read an image from the system clipboard with graceful platform
104253
+ * Read media from the system clipboard with graceful platform
103770
104254
  * fallbacks. Ported from `libs/pi-mono/packages/coding-agent/src/utils/
103771
- * clipboard-image.ts` and trimmed we don't carry the Photon WASM
103772
- * BMPPNG converter because kimi-core's LLM pipeline only accepts
104255
+ * clipboard-image.ts` and trimmed -- we don't carry the Photon WASM
104256
+ * BMP-to-PNG converter because kimi-core's LLM pipeline only accepts
103773
104257
  * PNG/JPEG/GIF/WebP, and the clipboard sources we query already emit
103774
104258
  * those formats on the supported platforms.
103775
104259
  *
103776
104260
  * Lookup order:
103777
- * macOS / Windows → native `@mariozechner/clipboard`
103778
- * Linux Wayland → wl-paste
103779
- * Linux X11 → xclip
103780
- * WSL (image not on Linux cb)→ PowerShell fallback via wslpath
104261
+ * macOS file clipboard -> osascript/AppKit file URLs
104262
+ * macOS / Windows -> native `@mariozechner/clipboard`
104263
+ * Linux Wayland -> wl-paste
104264
+ * Linux X11 -> xclip
104265
+ * WSL (image not on Linux cb) -> PowerShell fallback via wslpath
103781
104266
  *
103782
- * Returns `null` when no image is available, the format isn't
104267
+ * Returns `null` when no supported media is available, the format isn't
103783
104268
  * supported, or every fallback fails.
103784
104269
  */
104270
+ var ClipboardMediaError = class extends Error {
104271
+ constructor(message) {
104272
+ super(message);
104273
+ this.name = "ClipboardMediaError";
104274
+ }
104275
+ };
103785
104276
  const SUPPORTED_IMAGE_MIME_TYPES = [
103786
104277
  "image/png",
103787
104278
  "image/jpeg",
103788
104279
  "image/webp",
103789
104280
  "image/gif"
103790
104281
  ];
104282
+ const MAX_VIDEO_BYTES = 100 * 1024 * 1024;
104283
+ const VIDEO_MIME_BY_SUFFIX = Object.freeze({
104284
+ ".mp4": "video/mp4",
104285
+ ".mpg": "video/mpeg",
104286
+ ".mpeg": "video/mpeg",
104287
+ ".mkv": "video/x-matroska",
104288
+ ".avi": "video/x-msvideo",
104289
+ ".mov": "video/quicktime",
104290
+ ".ogv": "video/ogg",
104291
+ ".wmv": "video/x-ms-wmv",
104292
+ ".webm": "video/webm",
104293
+ ".m4v": "video/x-m4v",
104294
+ ".flv": "video/x-flv",
104295
+ ".3gp": "video/3gpp",
104296
+ ".3g2": "video/3gpp2"
104297
+ });
103791
104298
  const DEFAULT_LIST_TIMEOUT_MS = 1e3;
103792
104299
  const DEFAULT_READ_TIMEOUT_MS = 3e3;
103793
104300
  const DEFAULT_POWERSHELL_TIMEOUT_MS = 5e3;
103794
104301
  const DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
104302
+ const MACOS_FILE_PATH_SCRIPT = String.raw`
104303
+ ObjC.import('AppKit');
104304
+ ObjC.import('Foundation');
104305
+
104306
+ const out = [];
104307
+ const pb = $.NSPasteboard.generalPasteboard;
104308
+ if (String(pb) !== '[id nil]') {
104309
+ try {
104310
+ const options = $.NSMutableDictionary.dictionary;
104311
+ options.setObjectForKey($.NSNumber.numberWithBool(true), $.NSPasteboardURLReadingFileURLsOnlyKey);
104312
+ const urls = pb.readObjectsForClassesOptions([$.NSURL], options);
104313
+ const count = urls ? urls.count : 0;
104314
+ for (let i = 0; i < count; i++) {
104315
+ const value = urls.objectAtIndex(i).path;
104316
+ const path = value ? ObjC.unwrap(value) : '';
104317
+ if (path) out.push(path);
104318
+ }
104319
+ } catch (error) {}
104320
+
104321
+ if (out.length === 0) {
104322
+ try {
104323
+ const files = ObjC.deepUnwrap(pb.propertyListForType('NSFilenamesPboardType'));
104324
+ if (Array.isArray(files)) {
104325
+ for (const path of files) {
104326
+ if (path) out.push(String(path));
104327
+ }
104328
+ } else if (files) {
104329
+ out.push(String(files));
104330
+ }
104331
+ } catch (error) {}
104332
+ }
104333
+ }
104334
+ out.join('\n');
104335
+ `.trim();
103795
104336
  function isWaylandSession(env) {
103796
104337
  return Boolean(env["WAYLAND_DISPLAY"]) || env["XDG_SESSION_TYPE"] === "wayland";
103797
104338
  }
@@ -103821,6 +104362,90 @@ function selectPreferredImageMimeType(candidates) {
103821
104362
  }
103822
104363
  return normalized.find((t) => t.base.startsWith("image/"))?.raw ?? null;
103823
104364
  }
104365
+ function videoMimeFromPath(path) {
104366
+ const dot = path.lastIndexOf(".");
104367
+ if (dot < 0) return null;
104368
+ return VIDEO_MIME_BY_SUFFIX[path.slice(dot).toLowerCase()] ?? null;
104369
+ }
104370
+ function parseClipboardPaths(text) {
104371
+ return splitClipboardPathLines(text).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((line) => {
104372
+ if (line.startsWith("file://")) try {
104373
+ return fileURLToPath(line);
104374
+ } catch {
104375
+ return "";
104376
+ }
104377
+ return line;
104378
+ }).filter((line) => line.length > 0 && isAbsolute(line));
104379
+ }
104380
+ function splitClipboardPathLines(text) {
104381
+ const lines = [];
104382
+ let start = 0;
104383
+ for (let i = 0; i < text.length; i += 1) {
104384
+ const char = text[i];
104385
+ if (char === "\r" || char === "\n" || text.codePointAt(i) === 0) {
104386
+ lines.push(text.slice(start, i));
104387
+ start = i + 1;
104388
+ }
104389
+ }
104390
+ lines.push(text.slice(start));
104391
+ return lines;
104392
+ }
104393
+ function readImagePath(path) {
104394
+ let stat;
104395
+ try {
104396
+ stat = statSync(path);
104397
+ } catch {
104398
+ return null;
104399
+ }
104400
+ if (!stat.isFile()) return null;
104401
+ let bytes;
104402
+ try {
104403
+ bytes = readFileSync(path);
104404
+ } catch {
104405
+ return null;
104406
+ }
104407
+ if (bytes.length === 0) return null;
104408
+ const meta = parseImageMeta(bytes);
104409
+ if (meta === null) return null;
104410
+ return {
104411
+ kind: "image",
104412
+ bytes: new Uint8Array(bytes),
104413
+ mimeType: meta.mime
104414
+ };
104415
+ }
104416
+ function readVideoPath(path) {
104417
+ const mimeType = videoMimeFromPath(path);
104418
+ if (mimeType === null) return null;
104419
+ let stat;
104420
+ try {
104421
+ stat = statSync(path);
104422
+ } catch {
104423
+ return null;
104424
+ }
104425
+ if (!stat.isFile()) return null;
104426
+ if (stat.size > MAX_VIDEO_BYTES) throw new ClipboardMediaError(`Video is ${(stat.size / 1024 / 1024).toFixed(1)} MB; maximum supported size is 100 MB.`);
104427
+ return {
104428
+ kind: "video",
104429
+ mimeType,
104430
+ filename: basename(path),
104431
+ sourcePath: path
104432
+ };
104433
+ }
104434
+ function readMediaPath(path) {
104435
+ const video = readVideoPath(path);
104436
+ if (video !== null) return video;
104437
+ return readImagePath(path);
104438
+ }
104439
+ function readMediaFromPaths(paths) {
104440
+ for (const path of paths) {
104441
+ const media = readMediaPath(path);
104442
+ if (media !== null) return media;
104443
+ }
104444
+ return null;
104445
+ }
104446
+ function readMediaFromText(text) {
104447
+ return readMediaFromPaths(parseClipboardPaths(text));
104448
+ }
103824
104449
  function runCommand(command, args, options) {
103825
104450
  const result = spawnSync(command, args, {
103826
104451
  timeout: options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS,
@@ -103836,10 +104461,25 @@ function runCommand(command, args, options) {
103836
104461
  stdout: Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout ?? "")
103837
104462
  };
103838
104463
  }
104464
+ function parseTargetList(output) {
104465
+ return output.toString("utf-8").split(/\r?\n/).map((t) => t.trim()).filter((t) => t.length > 0);
104466
+ }
104467
+ function readClipboardFileMediaViaWlPaste() {
104468
+ const list = runCommand("wl-paste", ["--list-types"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });
104469
+ if (!list.ok) return null;
104470
+ const uriType = parseTargetList(list.stdout).find((t) => baseMimeType(t) === "text/uri-list");
104471
+ if (uriType === void 0) return null;
104472
+ const uris = runCommand("wl-paste", [
104473
+ "--type",
104474
+ uriType,
104475
+ "--no-newline"
104476
+ ]);
104477
+ return uris.ok ? readMediaFromText(uris.stdout.toString("utf-8")) : null;
104478
+ }
103839
104479
  function readClipboardImageViaWlPaste() {
103840
104480
  const list = runCommand("wl-paste", ["--list-types"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });
103841
104481
  if (!list.ok) return null;
103842
- const selected = selectPreferredImageMimeType(list.stdout.toString("utf-8").split(/\r?\n/).map((t) => t.trim()).filter((t) => t.length > 0));
104482
+ const selected = selectPreferredImageMimeType(parseTargetList(list.stdout));
103843
104483
  if (selected === null) return null;
103844
104484
  const data = runCommand("wl-paste", [
103845
104485
  "--type",
@@ -103848,10 +104488,31 @@ function readClipboardImageViaWlPaste() {
103848
104488
  ]);
103849
104489
  if (!data.ok || data.stdout.length === 0) return null;
103850
104490
  return {
104491
+ kind: "image",
103851
104492
  bytes: data.stdout,
103852
104493
  mimeType: baseMimeType(selected)
103853
104494
  };
103854
104495
  }
104496
+ function readClipboardFileMediaViaXclip() {
104497
+ const targets = runCommand("xclip", [
104498
+ "-selection",
104499
+ "clipboard",
104500
+ "-t",
104501
+ "TARGETS",
104502
+ "-o"
104503
+ ], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });
104504
+ if (!targets.ok) return null;
104505
+ const uriType = parseTargetList(targets.stdout).find((t) => baseMimeType(t) === "text/uri-list");
104506
+ if (uriType === void 0) return null;
104507
+ const uris = runCommand("xclip", [
104508
+ "-selection",
104509
+ "clipboard",
104510
+ "-t",
104511
+ uriType,
104512
+ "-o"
104513
+ ]);
104514
+ return uris.ok ? readMediaFromText(uris.stdout.toString("utf-8")) : null;
104515
+ }
103855
104516
  function readClipboardImageViaXclip() {
103856
104517
  const targets = runCommand("xclip", [
103857
104518
  "-selection",
@@ -103860,7 +104521,7 @@ function readClipboardImageViaXclip() {
103860
104521
  "TARGETS",
103861
104522
  "-o"
103862
104523
  ], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });
103863
- const candidates = targets.ok ? targets.stdout.toString("utf-8").split(/\r?\n/).map((t) => t.trim()).filter((t) => t.length > 0) : [];
104524
+ const candidates = targets.ok ? parseTargetList(targets.stdout) : [];
103864
104525
  const preferred = candidates.length > 0 ? selectPreferredImageMimeType(candidates) : null;
103865
104526
  const tryTypes = preferred !== null ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES] : [...SUPPORTED_IMAGE_MIME_TYPES];
103866
104527
  for (const mime of tryTypes) {
@@ -103872,6 +104533,7 @@ function readClipboardImageViaXclip() {
103872
104533
  "-o"
103873
104534
  ]);
103874
104535
  if (data.ok && data.stdout.length > 0) return {
104536
+ kind: "image",
103875
104537
  bytes: data.stdout,
103876
104538
  mimeType: baseMimeType(mime)
103877
104539
  };
@@ -103913,6 +104575,7 @@ function readClipboardImageViaPowerShell() {
103913
104575
  const bytes = readFileSync(tmpFile);
103914
104576
  if (bytes.length === 0) return null;
103915
104577
  return {
104578
+ kind: "image",
103916
104579
  bytes: new Uint8Array(bytes),
103917
104580
  mimeType: "image/png"
103918
104581
  };
@@ -103924,12 +104587,66 @@ function readClipboardImageViaPowerShell() {
103924
104587
  } catch {}
103925
104588
  }
103926
104589
  }
103927
- async function readClipboardImageViaNative() {
103928
- if (clipboard === null || !clipboard.hasImage()) return null;
104590
+ function readClipboardFilePathsViaMacOs(run) {
104591
+ const result = run("osascript", [
104592
+ "-l",
104593
+ "JavaScript",
104594
+ "-e",
104595
+ MACOS_FILE_PATH_SCRIPT
104596
+ ], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });
104597
+ if (!result.ok || result.stdout.length === 0) return [];
104598
+ return parseClipboardPaths(result.stdout.toString("utf-8"));
104599
+ }
104600
+ function isFileLikeNativeFormat(format) {
104601
+ const f = format.toLowerCase();
104602
+ const base = baseMimeType(format);
104603
+ return f.includes("file-url") || f.includes("file url") || f.includes("nsfilenames") || f.includes("com.apple.finder") || base === "text/uri-list" || base === "public.url";
104604
+ }
104605
+ function safeAvailableFormats(clip) {
104606
+ if (clip?.availableFormats === void 0) return [];
104607
+ try {
104608
+ return clip.availableFormats();
104609
+ } catch {
104610
+ return [];
104611
+ }
104612
+ }
104613
+ async function readClipboardFileMediaViaNativeText(clip) {
104614
+ if (clip === null) return {
104615
+ media: null,
104616
+ lookedFileLike: false
104617
+ };
104618
+ const lookedFileLike = safeAvailableFormats(clip).some(isFileLikeNativeFormat);
104619
+ if (!lookedFileLike || clip.getText === void 0) return {
104620
+ media: null,
104621
+ lookedFileLike
104622
+ };
104623
+ try {
104624
+ return {
104625
+ media: readMediaFromText(await clip.getText()),
104626
+ lookedFileLike
104627
+ };
104628
+ } catch (error) {
104629
+ if (error instanceof ClipboardMediaError) throw error;
104630
+ return {
104631
+ media: null,
104632
+ lookedFileLike
104633
+ };
104634
+ }
104635
+ }
104636
+ async function readClipboardImageViaNative(clip = clipboard) {
104637
+ if (clip === null) return null;
104638
+ let hasImage = false;
103929
104639
  try {
103930
- const data = await clipboard.getImageBinary();
104640
+ hasImage = clip.hasImage();
104641
+ } catch {
104642
+ return null;
104643
+ }
104644
+ if (!hasImage) return null;
104645
+ try {
104646
+ const data = await clip.getImageBinary();
103931
104647
  if (data.length === 0) return null;
103932
104648
  return {
104649
+ kind: "image",
103933
104650
  bytes: Uint8Array.from(data),
103934
104651
  mimeType: "image/png"
103935
104652
  };
@@ -103937,151 +104654,41 @@ async function readClipboardImageViaNative() {
103937
104654
  return null;
103938
104655
  }
103939
104656
  }
103940
- async function readClipboardImage(options) {
104657
+ async function readClipboardMedia(options) {
103941
104658
  const env = options?.env ?? process.env;
103942
104659
  const platform = options?.platform ?? process.platform;
104660
+ const clip = options?.clipboard ?? clipboard;
104661
+ const run = options?.runCommand ?? runCommand;
103943
104662
  if (env["TERMUX_VERSION"] !== void 0) return null;
103944
104663
  let image = null;
103945
104664
  if (platform === "linux") {
103946
104665
  const wayland = isWaylandSession(env);
103947
104666
  const wsl = isWSL(env);
103948
- if (wayland || wsl) image = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip();
104667
+ if (wayland || wsl) {
104668
+ const fileMedia = readClipboardFileMediaViaWlPaste() ?? readClipboardFileMediaViaXclip();
104669
+ if (fileMedia !== null) return fileMedia;
104670
+ image = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip();
104671
+ }
103949
104672
  if (image === null && wsl) image = readClipboardImageViaPowerShell();
103950
- if (image === null && !wayland) image = await readClipboardImageViaNative();
103951
- } else image = await readClipboardImageViaNative();
103952
- if (image === null) return null;
103953
- if (!isSupportedImageMimeType(image.mimeType)) return null;
103954
- return image;
103955
- }
103956
- //#endregion
103957
- //#region src/utils/image/image-mime.ts
103958
- function parseImageMeta(bytes) {
103959
- if (isPng(bytes)) return parsePng(bytes);
103960
- if (isJpeg(bytes)) return parseJpeg(bytes);
103961
- if (isGif(bytes)) return parseGif(bytes);
103962
- if (isWebp(bytes)) return parseWebp(bytes);
103963
- return null;
103964
- }
103965
- function isPng(b) {
103966
- return b.length >= 8 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71 && b[4] === 13 && b[5] === 10 && b[6] === 26 && b[7] === 10;
103967
- }
103968
- function parsePng(b) {
103969
- if (b.length < 24) return null;
103970
- const width = readUInt32BE(b, 16);
103971
- const height = readUInt32BE(b, 20);
103972
- if (width <= 0 || height <= 0) return null;
103973
- return {
103974
- mime: "image/png",
103975
- width,
103976
- height
103977
- };
103978
- }
103979
- function isJpeg(b) {
103980
- return b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255;
103981
- }
103982
- function parseJpeg(b) {
103983
- let i = 2;
103984
- while (i < b.length) {
103985
- if (b[i] !== 255) {
103986
- i += 1;
103987
- continue;
104673
+ if (image === null && !wayland) {
104674
+ const nativeFileMedia = await readClipboardFileMediaViaNativeText(clip);
104675
+ if (nativeFileMedia.media !== null) return nativeFileMedia.media;
104676
+ if (nativeFileMedia.lookedFileLike) return null;
104677
+ image = await readClipboardImageViaNative(clip);
103988
104678
  }
103989
- while (i < b.length && b[i] === 255) i += 1;
103990
- if (i >= b.length) return null;
103991
- const marker = b[i];
103992
- i += 1;
103993
- if (marker === 216 || marker === 217) continue;
103994
- if (i + 1 >= b.length) return null;
103995
- const segLen = readUInt16BE(b, i);
103996
- if (isSofMarker(marker)) {
103997
- if (i + 7 >= b.length) return null;
103998
- const height = readUInt16BE(b, i + 3);
103999
- const width = readUInt16BE(b, i + 5);
104000
- if (width <= 0 || height <= 0) return null;
104001
- return {
104002
- mime: "image/jpeg",
104003
- width,
104004
- height
104005
- };
104679
+ } else {
104680
+ if (platform === "darwin") {
104681
+ const fileMedia = readMediaFromPaths(readClipboardFilePathsViaMacOs(run));
104682
+ if (fileMedia !== null) return fileMedia;
104006
104683
  }
104007
- i += segLen;
104008
- }
104009
- return null;
104010
- }
104011
- function isSofMarker(marker) {
104012
- if (marker < 192 || marker > 207) return false;
104013
- return marker !== 196 && marker !== 200 && marker !== 204;
104014
- }
104015
- function isGif(b) {
104016
- return b.length >= 6 && b[0] === 71 && b[1] === 73 && b[2] === 70 && b[3] === 56 && (b[4] === 55 || b[4] === 57) && b[5] === 97;
104017
- }
104018
- function parseGif(b) {
104019
- if (b.length < 10) return null;
104020
- const width = readUInt16LE(b, 6);
104021
- const height = readUInt16LE(b, 8);
104022
- if (width <= 0 || height <= 0) return null;
104023
- return {
104024
- mime: "image/gif",
104025
- width,
104026
- height
104027
- };
104028
- }
104029
- function isWebp(b) {
104030
- return b.length >= 12 && b[0] === 82 && b[1] === 73 && b[2] === 70 && b[3] === 70 && b[8] === 87 && b[9] === 69 && b[10] === 66 && b[11] === 80;
104031
- }
104032
- function parseWebp(b) {
104033
- if (b.length < 30) return null;
104034
- const chunk = String.fromCharCode(b[12], b[13], b[14], b[15]);
104035
- if (chunk === "VP8 ") {
104036
- const widthRaw = readUInt16LE(b, 26);
104037
- const heightRaw = readUInt16LE(b, 28);
104038
- const width = widthRaw & 16383;
104039
- const height = heightRaw & 16383;
104040
- if (width <= 0 || height <= 0) return null;
104041
- return {
104042
- mime: "image/webp",
104043
- width,
104044
- height
104045
- };
104046
- }
104047
- if (chunk === "VP8L") {
104048
- if (b[20] !== 47) return null;
104049
- const b1 = b[21];
104050
- const b2 = b[22];
104051
- const b3 = b[23];
104052
- const b4 = b[24];
104053
- const width = 1 + ((b2 & 63) << 8 | b1);
104054
- const height = 1 + ((b4 & 15) << 10 | b3 << 2 | (b2 & 192) >> 6);
104055
- if (width <= 0 || height <= 0) return null;
104056
- return {
104057
- mime: "image/webp",
104058
- width,
104059
- height
104060
- };
104061
- }
104062
- if (chunk === "VP8X") {
104063
- const width = 1 + readUInt24LE(b, 24);
104064
- const height = 1 + readUInt24LE(b, 27);
104065
- if (width <= 0 || height <= 0) return null;
104066
- return {
104067
- mime: "image/webp",
104068
- width,
104069
- height
104070
- };
104684
+ const nativeFileMedia = await readClipboardFileMediaViaNativeText(clip);
104685
+ if (nativeFileMedia.media !== null) return nativeFileMedia.media;
104686
+ if (platform === "darwin" && nativeFileMedia.lookedFileLike) return null;
104687
+ image = await readClipboardImageViaNative(clip);
104071
104688
  }
104072
- return null;
104073
- }
104074
- function readUInt16BE(b, off) {
104075
- return b[off] << 8 | b[off + 1];
104076
- }
104077
- function readUInt16LE(b, off) {
104078
- return b[off] | b[off + 1] << 8;
104079
- }
104080
- function readUInt24LE(b, off) {
104081
- return b[off] | b[off + 1] << 8 | b[off + 2] << 16;
104082
- }
104083
- function readUInt32BE(b, off) {
104084
- return b[off] * 16777216 + (b[off + 1] << 16) + (b[off + 2] << 8) + b[off + 3] >>> 0;
104689
+ if (image === null) return null;
104690
+ if (!isSupportedImageMimeType(image.mimeType)) return null;
104691
+ return image;
104085
104692
  }
104086
104693
  //#endregion
104087
104694
  //#region src/tui/components/editor/file-mention-provider.ts
@@ -104278,15 +104885,18 @@ function toItem(path) {
104278
104885
  //#endregion
104279
104886
  //#region src/tui/components/layout/bottom-pinned-layout.ts
104280
104887
  var BottomPinnedLayout = class {
104281
- constructor(terminal, body, bottom) {
104888
+ constructor(terminal, body, bottom, state) {
104282
104889
  this.terminal = terminal;
104283
104890
  this.body = body;
104284
104891
  this.bottom = bottom;
104892
+ this.state = state;
104285
104893
  }
104286
104894
  render(width) {
104287
104895
  const bodyLines = this.body.render(width);
104288
104896
  const bottomLines = this.bottom.flatMap((component) => component.render(width));
104289
- if (bodyLines.length + bottomLines.length < this.terminal.rows) return [...bodyLines, ...bottomLines];
104897
+ const totalHeight = bodyLines.length + bottomLines.length;
104898
+ if (!this.state.hasFilledViewport && totalHeight < this.terminal.rows) return [...bodyLines, ...bottomLines];
104899
+ this.state.hasFilledViewport = true;
104290
104900
  const spacerHeight = Math.max(0, this.terminal.rows - bodyLines.length - bottomLines.length);
104291
104901
  return [
104292
104902
  ...bodyLines,
@@ -104387,11 +104997,26 @@ function setupEditor(state, cb) {
104387
104997
  editor.onPasteImage = async () => handleClipboardImagePaste(state);
104388
104998
  }
104389
104999
  async function handleClipboardImagePaste(state) {
104390
- const image = await readClipboardImage();
104391
- if (image === null) return false;
104392
- const meta = parseImageMeta(image.bytes);
105000
+ let media;
105001
+ try {
105002
+ media = await readClipboardMedia();
105003
+ } catch (error) {
105004
+ if (error instanceof ClipboardMediaError) {
105005
+ emitError(state, error.message);
105006
+ return true;
105007
+ }
105008
+ return false;
105009
+ }
105010
+ if (media === null) return false;
105011
+ if (media.kind === "video") {
105012
+ const attachment = state.imageStore.addVideo(media.mimeType, media.sourcePath, media.filename);
105013
+ state.editor.insertTextAtCursor?.(`${attachment.placeholder} `);
105014
+ state.ui.requestRender();
105015
+ return true;
105016
+ }
105017
+ const meta = parseImageMeta(media.bytes);
104393
105018
  if (meta === null) return false;
104394
- const attachment = state.imageStore.add(image.bytes, meta.mime, meta.width, meta.height);
105019
+ const attachment = state.imageStore.addImage(media.bytes, meta.mime, meta.width, meta.height);
104395
105020
  state.editor.insertTextAtCursor?.(`${attachment.placeholder} `);
104396
105021
  state.ui.requestRender();
104397
105022
  return true;
@@ -104410,7 +105035,7 @@ function buildLayout(state) {
104410
105035
  state.queueContainer,
104411
105036
  state.editorContainer,
104412
105037
  state.footer
104413
- ]));
105038
+ ], state.bottomLayoutState));
104414
105039
  }
104415
105040
  /**
104416
105041
  * Register the file-mention + slash-command autocomplete provider on
@@ -104497,12 +105122,40 @@ async function loadPersistedInputHistory(state) {
104497
105122
  } catch {}
104498
105123
  }
104499
105124
  //#endregion
105125
+ //#region src/tui/utils/terminal-focus.ts
105126
+ const ESC = "\x1B";
105127
+ const TERMINAL_FOCUS_IN = `${ESC}[I`;
105128
+ const TERMINAL_FOCUS_OUT = `${ESC}[O`;
105129
+ const ENABLE_TERMINAL_FOCUS_REPORTING = `${ESC}[?1004h`;
105130
+ const DISABLE_TERMINAL_FOCUS_REPORTING = `${ESC}[?1004l`;
105131
+ function installTerminalFocusTracking(state) {
105132
+ state.terminalFocused = true;
105133
+ const disposeInputListener = state.ui.addInputListener((data) => handleTerminalFocusInput(state, data));
105134
+ state.terminal.write(ENABLE_TERMINAL_FOCUS_REPORTING);
105135
+ return () => {
105136
+ disposeInputListener();
105137
+ state.terminal.write(DISABLE_TERMINAL_FOCUS_REPORTING);
105138
+ state.terminalFocused = true;
105139
+ };
105140
+ }
105141
+ function handleTerminalFocusInput(state, data) {
105142
+ if (data === TERMINAL_FOCUS_IN) {
105143
+ state.terminalFocused = true;
105144
+ return { consume: true };
105145
+ }
105146
+ if (data === TERMINAL_FOCUS_OUT) {
105147
+ state.terminalFocused = false;
105148
+ return { consume: true };
105149
+ }
105150
+ }
105151
+ //#endregion
104500
105152
  //#region src/tui/kimi-tui.ts
104501
105153
  var KimiTUI = class {
104502
105154
  state;
104503
105155
  stateHooks;
104504
105156
  streamCallbacks;
104505
105157
  reverseRpcDisposers = [];
105158
+ terminalFocusTrackingDispose;
104506
105159
  onExit;
104507
105160
  constructor(client, initialState, options) {
104508
105161
  this.state = createTUIState({
@@ -104558,6 +105211,7 @@ var KimiTUI = class {
104558
105211
  this.state.editorContainer.addChild(this.state.editor);
104559
105212
  this.state.ui.setFocus(this.state.editor);
104560
105213
  this.state.ui.start();
105214
+ this.terminalFocusTrackingDispose = installTerminalFocusTracking(this.state);
104561
105215
  attachFooterFeed(this.state, buildFooterFeedHandlers(this.state));
104562
105216
  if (this.state.startupNotice !== void 0) {
104563
105217
  emitMuted(this.state, this.state.startupNotice);
@@ -104661,6 +105315,8 @@ var KimiTUI = class {
104661
105315
  this.reverseRpcDisposers.length = 0;
104662
105316
  this.state.approvalController.cancelAll("shutting down");
104663
105317
  this.state.questionController.cancelAll("shutting down");
105318
+ this.terminalFocusTrackingDispose?.();
105319
+ this.terminalFocusTrackingDispose = void 0;
104664
105320
  this.state.ui.stop();
104665
105321
  if (this.onExit) await this.onExit();
104666
105322
  }
@@ -104677,7 +105333,10 @@ var KimiTUI = class {
104677
105333
  sendSkillActivation(this.state, (e) => this.addEntry(e), item.skillName, item.skillArgs, item.fullPrompt);
104678
105334
  return;
104679
105335
  }
104680
- sendMessageInternal(this.state, (e) => this.addEntry(e), item.text);
105336
+ sendMessageInternal(this.state, (e) => this.addEntry(e), item.text, {
105337
+ ...item.parts !== void 0 ? { parts: item.parts } : {},
105338
+ ...item.imageAttachmentIds !== void 0 ? { imageAttachmentIds: item.imageAttachmentIds } : {}
105339
+ });
104681
105340
  };
104682
105341
  subscribeToWire(this.state, (event) => {
104683
105342
  dispatchEvent(event, ectx, sendQueued);
@@ -104805,6 +105464,7 @@ async function runShell(opts, version) {
104805
105464
  theme: tuiConfig.theme,
104806
105465
  version,
104807
105466
  editorCommand: tuiConfig.editorCommand,
105467
+ notifications: tuiConfig.notifications,
104808
105468
  availableModels: ctx.availableModels,
104809
105469
  sessionTitle: null
104810
105470
  };