kfc-code-cli 0.0.1-alpha.3 → 0.0.1-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.mjs +1161 -619
  2. package/package.json +2 -2
package/dist/main.mjs CHANGED
@@ -33,7 +33,7 @@ import { writeFile as writeFile$1 } from "fs/promises";
33
33
  import { AsyncLocalStorage } from "node:async_hooks";
34
34
  import "yazl";
35
35
  import pino from "pino";
36
- import { CombinedAutocompleteProvider, Container, Editor, Image, Key, Markdown, ProcessTerminal, Spacer, TUI, Text, decodeKittyPrintable, fuzzyFilter, fuzzyMatch, getCapabilities, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
36
+ import { CombinedAutocompleteProvider, Container, Editor, Image, Input, Key, Markdown, ProcessTerminal, Spacer, TUI, Text, decodeKittyPrintable, fuzzyFilter, fuzzyMatch, getCapabilities, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
37
37
  import chalk from "chalk";
38
38
  import { highlight } from "cli-highlight";
39
39
  import { gt, valid } from "semver";
@@ -1026,26 +1026,12 @@ var BaseContextState = class {
1026
1026
  this._systemPrompt = event.new_prompt;
1027
1027
  return;
1028
1028
  case "model_changed":
1029
- await this.journalWriter.append({
1030
- type: "model_changed",
1031
- old_model: event.old_model,
1032
- new_model: event.new_model
1033
- });
1034
1029
  this._model = event.new_model;
1035
1030
  return;
1036
1031
  case "thinking_changed":
1037
- await this.journalWriter.append({
1038
- type: "thinking_changed",
1039
- level: event.level
1040
- });
1041
1032
  this._thinkingLevel = event.level;
1042
1033
  return;
1043
- case "plan_mode_changed":
1044
- await this.journalWriter.append({
1045
- type: "plan_mode_changed",
1046
- enabled: event.enabled
1047
- });
1048
- return;
1034
+ case "plan_mode_changed": return;
1049
1035
  case "tools_changed":
1050
1036
  await this.journalWriter.append({
1051
1037
  type: "tools_changed",
@@ -1146,9 +1132,6 @@ var WiredSessionJournalImpl = class {
1146
1132
  async appendTeamMail(data) {
1147
1133
  await this.journalWriter.append(data);
1148
1134
  }
1149
- async appendPermissionModeChanged(data) {
1150
- await this.journalWriter.append(data);
1151
- }
1152
1135
  async appendToolDenied(data) {
1153
1136
  await this.journalWriter.append(data);
1154
1137
  }
@@ -1164,9 +1147,6 @@ var WiredSessionJournalImpl = class {
1164
1147
  async appendOwnershipChanged(data) {
1165
1148
  await this.journalWriter.append(data);
1166
1149
  }
1167
- async appendSessionMetaChanged(data) {
1168
- await this.journalWriter.append(data);
1169
- }
1170
1150
  };
1171
1151
  //#endregion
1172
1152
  //#region ../../packages/kimi-core/src/storage/schema/records.ts
@@ -1284,25 +1264,6 @@ const _rawSystemPromptChangedRecordSchema = z.object({
1284
1264
  time: z.number(),
1285
1265
  new_prompt: z.string()
1286
1266
  });
1287
- const _rawModelChangedRecordSchema = z.object({
1288
- type: z.literal("model_changed"),
1289
- seq: z.number(),
1290
- time: z.number(),
1291
- old_model: z.string(),
1292
- new_model: z.string()
1293
- });
1294
- const _rawThinkingChangedRecordSchema = z.object({
1295
- type: z.literal("thinking_changed"),
1296
- seq: z.number(),
1297
- time: z.number(),
1298
- level: z.string()
1299
- });
1300
- const _rawPlanModeChangedRecordSchema = z.object({
1301
- type: z.literal("plan_mode_changed"),
1302
- seq: z.number(),
1303
- time: z.number(),
1304
- enabled: z.boolean()
1305
- });
1306
1267
  const _rawToolsChangedRecordSchema = z.object({
1307
1268
  type: z.literal("tools_changed"),
1308
1269
  seq: z.number(),
@@ -1359,17 +1320,6 @@ const _rawNotificationRecordSchema = z.object({
1359
1320
  envelope_id: z.string().optional()
1360
1321
  })
1361
1322
  });
1362
- const _rawPermissionModeChangedRecordSchema = z.object({
1363
- type: z.literal("permission_mode_changed"),
1364
- seq: z.number(),
1365
- time: z.number(),
1366
- turn_id: z.string().optional(),
1367
- data: z.object({
1368
- from: z.string(),
1369
- to: z.string(),
1370
- reason: z.string()
1371
- })
1372
- });
1373
1323
  const _rawToolDeniedRecordSchema = z.object({
1374
1324
  type: z.literal("tool_denied"),
1375
1325
  seq: z.number(),
@@ -1611,25 +1561,6 @@ const _rawOwnershipChangedRecordSchema = z.object({
1611
1561
  old_owner: z.string().nullable(),
1612
1562
  new_owner: z.string()
1613
1563
  });
1614
- const _rawSessionMetaChangedRecordSchema = z.object({
1615
- type: z.literal("session_meta_changed"),
1616
- seq: z.number(),
1617
- time: z.number(),
1618
- patch: z.object({
1619
- title: z.string().optional(),
1620
- tags: z.array(z.string()).optional(),
1621
- description: z.string().optional(),
1622
- archived: z.boolean().optional(),
1623
- color: z.string().optional(),
1624
- plan_slug: z.string().optional()
1625
- }),
1626
- source: z.enum([
1627
- "user",
1628
- "auto",
1629
- "system"
1630
- ]),
1631
- reason: z.string().optional()
1632
- });
1633
1564
  const _rawContextEditRecordSchema = z.object({
1634
1565
  type: z.literal("context_edit"),
1635
1566
  seq: z.number(),
@@ -1662,15 +1593,15 @@ const _sessionInitializedCommonShape = {
1662
1593
  seq: z.number(),
1663
1594
  time: z.number(),
1664
1595
  system_prompt: z.string(),
1665
- model: z.string(),
1666
1596
  active_tools: z.array(z.string()),
1597
+ model: z.string().optional(),
1667
1598
  permission_mode: z.enum([
1668
1599
  "default",
1669
1600
  "acceptEdits",
1670
1601
  "bypassPermissions"
1671
- ]),
1672
- plan_mode: z.boolean(),
1673
- workspace_dir: z.string(),
1602
+ ]).optional(),
1603
+ plan_mode: z.boolean().optional(),
1604
+ workspace_dir: z.string().optional(),
1674
1605
  thinking_level: z.string().optional()
1675
1606
  };
1676
1607
  const _rawSessionInitializedMainSchema = z.object({
@@ -1707,13 +1638,9 @@ const WireRecordSchema = z.discriminatedUnion("type", [
1707
1638
  _rawToolResultRecordSchema,
1708
1639
  _rawCompactionRecordSchema,
1709
1640
  _rawSystemPromptChangedRecordSchema,
1710
- _rawModelChangedRecordSchema,
1711
- _rawThinkingChangedRecordSchema,
1712
- _rawPlanModeChangedRecordSchema,
1713
1641
  _rawToolsChangedRecordSchema,
1714
1642
  _rawSystemReminderRecordSchema,
1715
1643
  _rawNotificationRecordSchema,
1716
- _rawPermissionModeChangedRecordSchema,
1717
1644
  _rawToolDeniedRecordSchema,
1718
1645
  _rawStepBeginRecordSchema,
1719
1646
  _rawStepEndRecordSchema,
@@ -1730,7 +1657,6 @@ const WireRecordSchema = z.discriminatedUnion("type", [
1730
1657
  _rawOwnershipChangedRecordSchema,
1731
1658
  _rawContextEditRecordSchema,
1732
1659
  _rawContextClearedRecordSchema,
1733
- _rawSessionMetaChangedRecordSchema,
1734
1660
  _rawSessionInitializedRecordSchema
1735
1661
  ]);
1736
1662
  //#endregion
@@ -1849,13 +1775,9 @@ const KNOWN_RECORD_TYPES = new Set([
1849
1775
  "tool_result",
1850
1776
  "compaction",
1851
1777
  "system_prompt_changed",
1852
- "model_changed",
1853
- "thinking_changed",
1854
- "plan_mode_changed",
1855
1778
  "tools_changed",
1856
1779
  "system_reminder",
1857
1780
  "notification",
1858
- "permission_mode_changed",
1859
1781
  "tool_denied",
1860
1782
  "step_begin",
1861
1783
  "step_end",
@@ -1871,8 +1793,7 @@ const KNOWN_RECORD_TYPES = new Set([
1871
1793
  "subagent_failed",
1872
1794
  "ownership_changed",
1873
1795
  "context_edit",
1874
- "context_cleared",
1875
- "session_meta_changed"
1796
+ "context_cleared"
1876
1797
  ]);
1877
1798
  //#endregion
1878
1799
  //#region ../../packages/kimi-core/src/storage/journal/rotation.ts
@@ -2286,6 +2207,15 @@ async function runSoulTurn(config, context, runtime, sink, signal, overrides) {
2286
2207
  if (steps >= maxSteps) throw new MaxStepsExceededError(maxSteps);
2287
2208
  steps += 1;
2288
2209
  const currentStep = steps;
2210
+ const turnId = context.currentTurnId?.() ?? UNKNOWN_TURN_ID;
2211
+ if (config.beforeStep !== void 0) {
2212
+ const beforeStep = await config.beforeStep({
2213
+ turnId,
2214
+ stepNumber: currentStep,
2215
+ context
2216
+ }, signal);
2217
+ if (beforeStep?.block === true) throw new Error(beforeStep.reason ?? `Step ${String(currentStep)} was blocked`);
2218
+ }
2289
2219
  safeEmit(sink, {
2290
2220
  type: "step.begin",
2291
2221
  step: currentStep
@@ -2298,7 +2228,6 @@ async function runSoulTurn(config, context, runtime, sink, signal, overrides) {
2298
2228
  const visibleTools = buildLLMVisibleTools(config.tools, overrides?.activeTools);
2299
2229
  const messages = context.buildMessages();
2300
2230
  const stepUuid = randomUUID();
2301
- const turnId = context.currentTurnId?.() ?? UNKNOWN_TURN_ID;
2302
2231
  const toolCallByProviderId = /* @__PURE__ */ new Map();
2303
2232
  const prefetchedResults = /* @__PURE__ */ new Map();
2304
2233
  await context.appendStepBegin({
@@ -2420,6 +2349,14 @@ async function runSoulTurn(config, context, runtime, sink, signal, overrides) {
2420
2349
  step: currentStep
2421
2350
  });
2422
2351
  const sr = response.stopReason ?? "end_turn";
2352
+ if (config.afterStep !== void 0) try {
2353
+ await config.afterStep({
2354
+ turnId,
2355
+ stepNumber: currentStep,
2356
+ context,
2357
+ stopReason: sr
2358
+ }, signal);
2359
+ } catch {}
2423
2360
  if (sr === "tool_use") continue;
2424
2361
  stopReason = sr;
2425
2362
  break;
@@ -2929,21 +2866,26 @@ function hookDedupeKey(hook) {
2929
2866
  /**
2930
2867
  * Extracts the string fed to a hook's matcher regex. Event-dependent:
2931
2868
  *
2869
+ * - `PreTurn` / `UserPromptSubmit` — the prompt text itself.
2870
+ * - `PostTurn` / `Stop` — the turn reason (`done` / `cancelled` /
2871
+ * `error`), so hooks can filter e.g. `/^error$/`.
2872
+ * - `PreStep` / `PostStep` — the step number as a string.
2932
2873
  * - `PreToolUse` / `PostToolUse` / `OnToolFailure` — tool name
2933
2874
  * (mirrors Python's `matcher_value=toolCall.name` contract).
2934
- * - `UserPromptSubmit` — the prompt text itself (Python parity).
2935
- * - `Stop` — the turn reason (`done` / `cancelled` / `error`), so
2936
- * hooks can filter e.g. `/^error$/`.
2937
2875
  * - `Notification` — the notification type string, so a single hook
2938
2876
  * can subscribe to an entire notification class via regex.
2939
2877
  */
2940
2878
  function extractMatcherValue(input) {
2941
2879
  switch (input.event) {
2880
+ case "PreTurn":
2881
+ case "UserPromptSubmit": return input.prompt;
2882
+ case "PostTurn":
2883
+ case "Stop": return input.reason;
2884
+ case "PreStep":
2885
+ case "PostStep": return String(input.stepNumber);
2942
2886
  case "PreToolUse":
2943
2887
  case "PostToolUse":
2944
2888
  case "OnToolFailure": return input.toolCall.name;
2945
- case "UserPromptSubmit": return input.prompt;
2946
- case "Stop": return input.reason;
2947
2889
  case "Notification": return input.notificationType;
2948
2890
  case "StopFailure": return input.error;
2949
2891
  case "SubagentStart":
@@ -2953,6 +2895,7 @@ function extractMatcherValue(input) {
2953
2895
  case "PreCompact":
2954
2896
  case "PostCompact": return "";
2955
2897
  }
2898
+ return input;
2956
2899
  }
2957
2900
  const AgentToolInputSchema = z.preprocess((input) => {
2958
2901
  if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
@@ -3252,7 +3195,7 @@ var EnterPlanModeTool = class {
3252
3195
  isError: true,
3253
3196
  content: "Plan mode is already active. Use ExitPlanMode when the plan is ready."
3254
3197
  };
3255
- if (this.deps.isYoloMode()) {
3198
+ if (this.deps.isYoloMode() || this.deps.shouldAutoApprove?.() === true) {
3256
3199
  try {
3257
3200
  await this.deps.setPlanMode(true);
3258
3201
  } catch (error) {
@@ -3588,15 +3531,10 @@ var CompactionOrchestrator = class {
3588
3531
  const summary = await (this.deps.runtimeSlot?.current().compactionProvider ?? this.deps.compactionProvider).run(messages, signal, customInstruction !== void 0 ? { userInstructions: customInstruction } : void 0);
3589
3532
  signal.throwIfAborted();
3590
3533
  const baselineInit = await this.deps.journalCapability.readSessionInitialized();
3591
- const runtime = this.deps.runtimeStateProvider();
3592
3534
  const cs = this.deps.contextState;
3593
3535
  const sessionInitialized = applyRuntimeOverlay(baselineInit, {
3594
3536
  system_prompt: cs.systemPrompt,
3595
- model: cs.model,
3596
- active_tools: [...cs.activeTools],
3597
- permission_mode: runtime.permissionMode,
3598
- plan_mode: runtime.planMode,
3599
- thinking_level: runtime.thinkingLevel
3537
+ active_tools: [...cs.activeTools]
3600
3538
  });
3601
3539
  await this.deps.journalWriter.flush();
3602
3540
  const rotateResult = await this.deps.journalCapability.rotate({
@@ -3663,16 +3601,15 @@ var CompactionOrchestrator = class {
3663
3601
  };
3664
3602
  /**
3665
3603
  * Phase 23 fix — overlay the runtime-mutable subset of a baseline
3666
- * `session_initialized` with the live ContextState + TurnManager state.
3604
+ * `session_initialized` with the live wire-owned ContextState baseline.
3667
3605
  *
3668
3606
  * Identity-class fields (`type`, `seq`, `time`, `agent_type`, `session_id`,
3669
- * `agent_id`, parent lineage, `workspace_dir`) are preserved verbatim
3607
+ * `agent_id`, parent lineage) are preserved verbatim
3670
3608
  * because they cannot legally mutate at runtime — see the discriminated-union
3671
- * shape in `wire-record.ts`. The mutable fields (`system_prompt`, `model`,
3672
- * `active_tools`, `permission_mode`, `plan_mode`, `thinking_level`) are
3673
- * overwritten so the post-rotate baseline reflects the compaction-time
3674
- * snapshot rather than the original startup config. `thinking_level` is
3675
- * mutable: `session.setThinking` (Phase 24 24b) changes it at runtime.
3609
+ * shape in `wire-record.ts`. The mutable wire-owned fields
3610
+ * (`system_prompt`, `active_tools`) are overwritten so the post-rotate
3611
+ * baseline reflects the compaction-time snapshot rather than the original
3612
+ * startup config. Runtime/session fields live in state.json.
3676
3613
  *
3677
3614
  * The generic preserves the discriminated-union narrowing so each branch
3678
3615
  * (main / sub / independent) returns its own concrete type.
@@ -3681,11 +3618,7 @@ function applyRuntimeOverlay(baseline, overlay) {
3681
3618
  return {
3682
3619
  ...baseline,
3683
3620
  system_prompt: overlay.system_prompt,
3684
- model: overlay.model,
3685
- active_tools: [...overlay.active_tools],
3686
- permission_mode: overlay.permission_mode,
3687
- plan_mode: overlay.plan_mode,
3688
- ...overlay.thinking_level !== void 0 ? { thinking_level: overlay.thinking_level } : {}
3621
+ active_tools: [...overlay.active_tools]
3689
3622
  };
3690
3623
  }
3691
3624
  /**
@@ -4449,11 +4382,13 @@ var DefaultSessionControl = class {
4449
4382
  contextState;
4450
4383
  sessionJournal;
4451
4384
  setPlanModeOverride;
4385
+ setYoloOverride;
4452
4386
  constructor(deps) {
4453
4387
  this.turnManager = deps.turnManager;
4454
4388
  this.contextState = deps.contextState;
4455
4389
  this.sessionJournal = deps.sessionJournal;
4456
4390
  this.setPlanModeOverride = deps.setPlanModeOverride;
4391
+ this.setYoloOverride = deps.setYoloOverride;
4457
4392
  }
4458
4393
  async compact(customInstruction) {
4459
4394
  await this.turnManager.triggerCompaction(customInstruction);
@@ -4478,18 +4413,15 @@ var DefaultSessionControl = class {
4478
4413
  this.turnManager.setPlanMode(enabled);
4479
4414
  }
4480
4415
  async setYolo(enabled) {
4416
+ if (this.setYoloOverride !== void 0) {
4417
+ await this.setYoloOverride(enabled);
4418
+ return;
4419
+ }
4481
4420
  const previousMode = this.turnManager.getPermissionMode();
4482
4421
  const newMode = enabled ? "bypassPermissions" : "default";
4483
4422
  if (previousMode === newMode) return;
4484
4423
  this.turnManager.setPermissionMode(newMode);
4485
- await this.sessionJournal.appendPermissionModeChanged({
4486
- type: "permission_mode_changed",
4487
- data: {
4488
- from: previousMode,
4489
- to: newMode,
4490
- reason: enabled ? "/yolo on" : "/yolo off"
4491
- }
4492
- });
4424
+ this.sessionJournal;
4493
4425
  }
4494
4426
  };
4495
4427
  //#endregion
@@ -4591,19 +4523,16 @@ var SessionMetaService = class {
4591
4523
  }
4592
4524
  }
4593
4525
  async applyPatch(patch, source, reason) {
4594
- await this.deps.sessionJournal.appendSessionMetaChanged({
4595
- type: "session_meta_changed",
4596
- patch: toWirePatch(patch),
4597
- source,
4598
- ...reason !== void 0 ? { reason } : {}
4599
- });
4600
- if (patch.title !== void 0) this.meta.title = patch.title;
4601
- if (patch.tags !== void 0) this.meta.tags = [...patch.tags];
4602
- if (patch.description !== void 0) this.meta.description = patch.description;
4603
- if (patch.archived !== void 0) this.meta.archived = patch.archived;
4604
- if (patch.color !== void 0) this.meta.color = patch.color;
4605
- if (patch.plan_slug !== void 0) this.meta.plan_slug = patch.plan_slug;
4606
- this.meta.last_updated = Date.now();
4526
+ const next = cloneMeta(this.meta);
4527
+ if (patch.title !== void 0) next.title = patch.title;
4528
+ if (patch.tags !== void 0) next.tags = [...patch.tags];
4529
+ if (patch.description !== void 0) next.description = patch.description;
4530
+ if (patch.archived !== void 0) next.archived = patch.archived;
4531
+ if (patch.color !== void 0) next.color = patch.color;
4532
+ if (patch.plan_slug !== void 0) next.plan_slug = patch.plan_slug;
4533
+ next.last_updated = Date.now();
4534
+ await this.flushStateJson(next);
4535
+ this.meta = next;
4607
4536
  this.deps.eventBus.emit({
4608
4537
  type: "session_meta.changed",
4609
4538
  data: {
@@ -4636,7 +4565,7 @@ var SessionMetaService = class {
4636
4565
  if (this.flushTimer !== null) return;
4637
4566
  this.flushTimer = setTimeout(() => {
4638
4567
  this.flushTimer = null;
4639
- this.flushStateJson().catch(() => {});
4568
+ this.flushStateJson(this.meta).catch(() => {});
4640
4569
  }, this.flushDebounceMs);
4641
4570
  this.flushTimer.unref?.();
4642
4571
  }
@@ -4651,25 +4580,27 @@ var SessionMetaService = class {
4651
4580
  * because createSession / closeSession / plan_mode / yolo all need
4652
4581
  * to migrate off direct state.json writes first.
4653
4582
  */
4654
- async flushStateJson() {
4655
- const next = {
4656
- ...await this.deps.stateCache.read() ?? {
4657
- session_id: this.meta.session_id,
4658
- created_at: this.meta.created_at,
4659
- updated_at: this.meta.last_updated
4583
+ async flushStateJson(meta = this.meta) {
4584
+ await this.deps.stateCache.update((existing) => ({
4585
+ ...existing ?? {
4586
+ session_id: meta.session_id,
4587
+ created_at: meta.created_at,
4588
+ updated_at: meta.last_updated
4660
4589
  },
4661
- session_id: this.meta.session_id,
4662
- created_at: this.meta.created_at,
4663
- updated_at: this.meta.last_updated,
4664
- ...this.meta.title !== void 0 ? { custom_title: this.meta.title } : {},
4665
- ...this.meta.tags !== void 0 ? { tags: [...this.meta.tags] } : {},
4666
- ...this.meta.description !== void 0 ? { description: this.meta.description } : {},
4667
- ...this.meta.archived !== void 0 ? { archived: this.meta.archived } : {},
4668
- ...this.meta.last_model !== void 0 ? { model: this.meta.last_model } : {},
4669
- ...this.meta.plan_slug !== void 0 ? { plan_slug: this.meta.plan_slug } : {},
4670
- ...this.meta.producer !== void 0 ? { producer: { ...this.meta.producer } } : {}
4671
- };
4672
- await this.deps.stateCache.write(next);
4590
+ session_id: meta.session_id,
4591
+ created_at: meta.created_at,
4592
+ updated_at: meta.last_updated,
4593
+ ...meta.title !== void 0 ? { custom_title: meta.title } : {},
4594
+ ...meta.tags !== void 0 ? { tags: [...meta.tags] } : {},
4595
+ ...meta.description !== void 0 ? { description: meta.description } : {},
4596
+ ...meta.archived !== void 0 ? { archived: meta.archived } : {},
4597
+ ...meta.color !== void 0 ? { color: meta.color } : {},
4598
+ ...meta.last_model !== void 0 ? { model: meta.last_model } : {},
4599
+ turn_count: meta.turn_count,
4600
+ ...meta.plan_slug !== void 0 ? { plan_slug: meta.plan_slug } : {},
4601
+ ...meta.producer !== void 0 ? { producer: { ...meta.producer } } : {},
4602
+ last_exit_code: "dirty"
4603
+ }));
4673
4604
  }
4674
4605
  };
4675
4606
  function cloneMeta(meta) {
@@ -5258,11 +5189,7 @@ async function runSubagentTurn(deps, agentId, request, signal) {
5258
5189
  parent_tool_call_id: request.parentToolCallId,
5259
5190
  run_in_background: request.runInBackground ?? false,
5260
5191
  system_prompt: childSystemPrompt ?? "",
5261
- model: childModel,
5262
- active_tools: childActiveTools,
5263
- permission_mode: "default",
5264
- plan_mode: false,
5265
- workspace_dir: deps.workDir ?? process.cwd()
5192
+ active_tools: childActiveTools
5266
5193
  };
5267
5194
  await childJournalWriter.append(subInitInput);
5268
5195
  childContext = new WiredContextState({
@@ -19848,6 +19775,49 @@ OpenAI.Containers = Containers;
19848
19775
  OpenAI.Skills = Skills$1;
19849
19776
  OpenAI.Videos = Videos;
19850
19777
  //#endregion
19778
+ //#region ../../packages/kimi-core/src/hooks/execution-pipeline.ts
19779
+ const ALLOW = Object.freeze({
19780
+ blockAction: false,
19781
+ additionalContext: []
19782
+ });
19783
+ var ExecutionHookBlockedError = class extends Error {
19784
+ event;
19785
+ reason;
19786
+ constructor(event, reason) {
19787
+ super(reason !== void 0 ? `${event} hook blocked: ${reason}` : `${event} hook blocked`);
19788
+ this.name = "ExecutionHookBlockedError";
19789
+ this.event = event;
19790
+ this.reason = reason;
19791
+ }
19792
+ };
19793
+ /**
19794
+ * Awaited execution-hook adapter.
19795
+ *
19796
+ * This sits on the execution side of the architecture, separate from the
19797
+ * wire-event publication pipeline. Pre hooks can block real work before it
19798
+ * happens; post hooks can still run in order without being able to rewrite
19799
+ * already-durable facts unless their caller explicitly interprets a result.
19800
+ */
19801
+ var ExecutionHookPipeline = class {
19802
+ hookEngine;
19803
+ constructor(options) {
19804
+ this.hookEngine = options.hookEngine;
19805
+ }
19806
+ async run(input, signal) {
19807
+ if (this.hookEngine === void 0) return ALLOW;
19808
+ return this.hookEngine.executeHooks(input.event, input, signal);
19809
+ }
19810
+ async runBlocking(input, signal) {
19811
+ const result = await this.run(input, signal);
19812
+ if (result.blockAction) throw new ExecutionHookBlockedError(input.event, result.reason);
19813
+ return result;
19814
+ }
19815
+ dispatch(input) {
19816
+ const controller = new AbortController();
19817
+ this.run(input, controller.signal).catch(() => {});
19818
+ }
19819
+ };
19820
+ //#endregion
19851
19821
  //#region ../../packages/kimi-core/src/soul-plus/capability-check.ts
19852
19822
  function checkLLMCapabilities(opts) {
19853
19823
  if (opts.model === "") return;
@@ -19944,7 +19914,10 @@ var TurnManager = class {
19944
19914
  runtimeSlot;
19945
19915
  compactionConfig;
19946
19916
  sessionId;
19917
+ executionHooks;
19947
19918
  activeToolExecutionScope;
19919
+ pendingLaunchController;
19920
+ pendingLaunchDrain;
19948
19921
  /**
19949
19922
  * Phase 18 A.13 — terminal reason per turn id for callers that
19950
19923
  * observe the turn lifecycle out-of-band (after the `end` event
@@ -19972,6 +19945,7 @@ var TurnManager = class {
19972
19945
  this.runtimeSlot = deps.runtimeSlot;
19973
19946
  this.compactionConfig = deps.compactionConfig;
19974
19947
  this.sessionId = deps.sessionId ?? "unknown";
19948
+ this.executionHooks = new ExecutionHookPipeline({ hookEngine: deps.hookEngine });
19975
19949
  }
19976
19950
  setPermissionMode(mode) {
19977
19951
  this.permissionMode = mode;
@@ -20154,6 +20128,13 @@ var TurnManager = class {
20154
20128
  };
20155
20129
  const scope = this.activeToolExecutionScope;
20156
20130
  this.deps.approvalRuntime?.cancelBySource(source);
20131
+ if (this.pendingLaunchTurnId === turnId && this.pendingLaunchController !== void 0) {
20132
+ this.pendingLaunchController.abort();
20133
+ try {
20134
+ await this.pendingLaunchDrain;
20135
+ } catch {}
20136
+ return;
20137
+ }
20157
20138
  scope?.discardStreaming?.("aborted");
20158
20139
  await this.deps.lifecycle.cancelTurn(turnId);
20159
20140
  scope?.drainPrefetched?.();
@@ -20178,8 +20159,16 @@ var TurnManager = class {
20178
20159
  if (mismatch !== void 0) throw mismatch;
20179
20160
  }
20180
20161
  const turnId = this.deps.lifecycle.allocateTurnId();
20162
+ const pendingLaunchController = new AbortController();
20181
20163
  this.pendingLaunchTurnId = turnId;
20164
+ this.pendingLaunchController = pendingLaunchController;
20165
+ let releasePendingLaunch;
20166
+ this.pendingLaunchDrain = new Promise((resolve) => {
20167
+ releasePendingLaunch = resolve;
20168
+ });
20182
20169
  try {
20170
+ await this.executePreTurnHook(turnId, input, pendingLaunchController.signal);
20171
+ pendingLaunchController.signal.throwIfAborted();
20183
20172
  await this.deps.sessionJournal.appendTurnBegin({
20184
20173
  type: "turn_begin",
20185
20174
  turn_id: turnId,
@@ -20210,17 +20199,23 @@ var TurnManager = class {
20210
20199
  };
20211
20200
  this.launchTurn(turnId, trigger);
20212
20201
  this.pendingLaunchTurnId = void 0;
20202
+ this.pendingLaunchController = void 0;
20203
+ this.pendingLaunchDrain = void 0;
20204
+ releasePendingLaunch();
20213
20205
  return {
20214
20206
  turn_id: turnId,
20215
20207
  status: "started"
20216
20208
  };
20217
20209
  } catch (error) {
20218
20210
  this.pendingLaunchTurnId = void 0;
20211
+ this.pendingLaunchController = void 0;
20212
+ this.pendingLaunchDrain = void 0;
20213
+ releasePendingLaunch();
20219
20214
  throw error;
20220
20215
  }
20221
20216
  }
20222
20217
  async handleCancel(req) {
20223
- const requestedId = req.data.turn_id ?? this.deps.lifecycle.getCurrentTurnId();
20218
+ const requestedId = req.data.turn_id ?? this.getCurrentTurnId();
20224
20219
  if (requestedId === void 0) return { ok: true };
20225
20220
  await this.abortTurn(requestedId, "dispatch-cancel");
20226
20221
  return { ok: true };
@@ -20258,16 +20253,27 @@ var TurnManager = class {
20258
20253
  const parsed = Number.parseInt(match[1] ?? "1", 10);
20259
20254
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
20260
20255
  }
20256
+ async executePreTurnHook(turnId, input, signal) {
20257
+ try {
20258
+ await this.executionHooks.runBlocking({
20259
+ event: "PreTurn",
20260
+ sessionId: this.sessionId,
20261
+ turnId,
20262
+ agentId: this.agentId,
20263
+ prompt: input.text,
20264
+ inputKind: "user"
20265
+ }, signal);
20266
+ } catch (error) {
20267
+ if (error instanceof ExecutionHookBlockedError) throw error;
20268
+ }
20269
+ }
20261
20270
  /**
20262
- * Fire-and-forget dispatch of a lifecycle hook event. Tool-scoped
20271
+ * Fire-and-forget dispatch for legacy lifecycle hook events. Tool-scoped
20263
20272
  * events go through the actor-local `ToolExecutionScope`; this helper
20264
- * only covers the lifecycle events TurnManager owns.
20273
+ * only covers compatibility events TurnManager owns.
20265
20274
  */
20266
20275
  dispatchLifecycleHook(input) {
20267
- const engine = this.deps.hookEngine;
20268
- if (engine === void 0) return;
20269
- const controller = new AbortController();
20270
- engine.executeHooks(input.event, input, controller.signal).catch(() => {});
20276
+ this.executionHooks.dispatch(input);
20271
20277
  }
20272
20278
  launchTurn(turnId, trigger) {
20273
20279
  const controller = this.deps.soulRegistry.getOrCreate("main").abortController;
@@ -20312,6 +20318,32 @@ var TurnManager = class {
20312
20318
  compactionConfig,
20313
20319
  contextWindow: compactionConfig.maxContextSize
20314
20320
  };
20321
+ if (this.deps.hookEngine !== void 0) soulConfig = {
20322
+ ...soulConfig,
20323
+ beforeStep: async (ctx, signal) => {
20324
+ const result = await this.executionHooks.run({
20325
+ event: "PreStep",
20326
+ sessionId: this.sessionId,
20327
+ turnId: ctx.turnId,
20328
+ stepNumber: ctx.stepNumber,
20329
+ agentId: this.agentId
20330
+ }, signal);
20331
+ if (result.blockAction) return {
20332
+ block: true,
20333
+ ...result.reason !== void 0 ? { reason: result.reason } : {}
20334
+ };
20335
+ },
20336
+ afterStep: async (ctx, signal) => {
20337
+ await this.executionHooks.run({
20338
+ event: "PostStep",
20339
+ sessionId: this.sessionId,
20340
+ turnId: ctx.turnId,
20341
+ stepNumber: ctx.stepNumber,
20342
+ agentId: this.agentId,
20343
+ stopReason: ctx.stopReason
20344
+ }, signal);
20345
+ }
20346
+ };
20315
20347
  const input = trigger.input;
20316
20348
  const runPromise = this.runTurn(turnId, input, soulConfig, runtimeForTurn, controller.signal);
20317
20349
  runPromise.catch(() => {});
@@ -20418,6 +20450,16 @@ var TurnManager = class {
20418
20450
  ...result.usage.cache_write !== void 0 ? { cache_write_tokens: result.usage.cache_write } : {}
20419
20451
  };
20420
20452
  await this.deps.sessionJournal.appendTurnEnd(turnEnd);
20453
+ try {
20454
+ await this.executionHooks.run({
20455
+ event: "PostTurn",
20456
+ sessionId: this.sessionId,
20457
+ turnId,
20458
+ agentId: this.agentId,
20459
+ reason,
20460
+ success: reason === "done"
20461
+ }, new AbortController().signal);
20462
+ } catch {}
20421
20463
  this.dispatchLifecycleHook({
20422
20464
  event: "Stop",
20423
20465
  sessionId: this.sessionId,
@@ -20670,6 +20712,7 @@ var SoulPlus = class {
20670
20712
  hostToolNames;
20671
20713
  planController;
20672
20714
  setPlanModeImpl;
20715
+ stateCache;
20673
20716
  /**
20674
20717
  * Phase 18 A.3–A.6 — lazy `SessionControlHandler` owned by the
20675
20718
  * facade. External hosts (SessionManager) still construct their own
@@ -20681,6 +20724,7 @@ var SoulPlus = class {
20681
20724
  sessionControlInstance;
20682
20725
  constructor(deps) {
20683
20726
  this.sessionId = deps.sessionId;
20727
+ this.stateCache = deps.stateCache;
20684
20728
  const eventBus = deps.eventBus;
20685
20729
  const toolRegistry = [...deps.tools];
20686
20730
  const enabledToolNames = deps.enabledToolNames !== void 0 ? new Set(deps.enabledToolNames) : void 0;
@@ -20703,7 +20747,6 @@ var SoulPlus = class {
20703
20747
  const approvalRuntime = deps.approvalRuntime;
20704
20748
  const sessionMeta = deps.stateCache !== void 0 && deps.initialMeta !== void 0 ? new SessionMetaService({
20705
20749
  sessionId: deps.sessionId,
20706
- sessionJournal,
20707
20750
  eventBus,
20708
20751
  stateCache: deps.stateCache,
20709
20752
  initialMeta: deps.initialMeta
@@ -20838,6 +20881,19 @@ var SoulPlus = class {
20838
20881
  const setPlanMode = async (enabled) => {
20839
20882
  if (enabled) {
20840
20883
  await planController.ensurePlanFilePath();
20884
+ await deps.stateCache?.update((current) => {
20885
+ const now = Date.now();
20886
+ return {
20887
+ ...current ?? {
20888
+ session_id: deps.sessionId,
20889
+ created_at: now,
20890
+ updated_at: now
20891
+ },
20892
+ plan_mode: true,
20893
+ updated_at: now,
20894
+ last_exit_code: "dirty"
20895
+ };
20896
+ });
20841
20897
  await contextState.applyConfigChange({
20842
20898
  type: "plan_mode_changed",
20843
20899
  enabled
@@ -20845,6 +20901,19 @@ var SoulPlus = class {
20845
20901
  turnManager.setPlanMode(enabled);
20846
20902
  return;
20847
20903
  }
20904
+ await deps.stateCache?.update((current) => {
20905
+ const now = Date.now();
20906
+ return {
20907
+ ...current ?? {
20908
+ session_id: deps.sessionId,
20909
+ created_at: now,
20910
+ updated_at: now
20911
+ },
20912
+ plan_mode: false,
20913
+ updated_at: now,
20914
+ last_exit_code: "dirty"
20915
+ };
20916
+ });
20848
20917
  await contextState.applyConfigChange({
20849
20918
  type: "plan_mode_changed",
20850
20919
  enabled
@@ -20862,6 +20931,7 @@ var SoulPlus = class {
20862
20931
  isPlanModeActive: () => turnManager.getPlanMode(),
20863
20932
  setPlanMode,
20864
20933
  isYoloMode: () => turnManager.getPermissionMode() === "bypassPermissions",
20934
+ shouldAutoApprove: () => true,
20865
20935
  requestApproval: enterPlanRequestApproval,
20866
20936
  getPlanFilePath: () => planController.getPlanFilePath()
20867
20937
  }));
@@ -21098,19 +21168,49 @@ var SoulPlus = class {
21098
21168
  turnManager: this.components.turnManager,
21099
21169
  contextState: this.journal.contextState,
21100
21170
  sessionJournal: this.journal.sessionJournal,
21101
- setPlanModeOverride: (enabled) => this.setPlanMode(enabled)
21171
+ setPlanModeOverride: (enabled) => this.setPlanMode(enabled),
21172
+ setYoloOverride: async (enabled) => {
21173
+ const permissionMode = enabled ? "bypassPermissions" : "default";
21174
+ await this.stateCache?.update((current) => {
21175
+ const now = Date.now();
21176
+ return {
21177
+ ...current ?? {
21178
+ session_id: this.sessionId,
21179
+ created_at: now,
21180
+ updated_at: now
21181
+ },
21182
+ permission_mode: permissionMode,
21183
+ updated_at: now,
21184
+ last_exit_code: "dirty"
21185
+ };
21186
+ });
21187
+ this.components.turnManager.setPermissionMode(permissionMode);
21188
+ }
21102
21189
  });
21103
21190
  return this.sessionControlInstance;
21104
21191
  }
21105
21192
  /**
21106
21193
  * Phase 18 A.3 — programmatic model change. Applies the config
21107
- * change event to ContextState (WAL `model_changed`) and emits a
21194
+ * change event to ContextState, persists state.json, and emits a
21108
21195
  * fresh `status.update` so downstream observers pick up the new
21109
21196
  * model without waiting for the next turn boundary.
21110
21197
  */
21111
21198
  async setModel(model) {
21112
21199
  const oldModel = this.journal.contextState.model;
21113
21200
  if (oldModel === model) return;
21201
+ await this.stateCache?.update((current) => {
21202
+ const now = Date.now();
21203
+ return {
21204
+ ...current ?? {
21205
+ session_id: this.sessionId,
21206
+ created_at: now,
21207
+ updated_at: now
21208
+ },
21209
+ model,
21210
+ updated_at: now,
21211
+ last_exit_code: "dirty"
21212
+ };
21213
+ });
21114
21214
  await this.journal.contextState.applyConfigChange({
21115
21215
  type: "model_changed",
21116
21216
  old_model: oldModel,
@@ -21127,8 +21227,7 @@ var SoulPlus = class {
21127
21227
  }
21128
21228
  /**
21129
21229
  * Phase 18 A.6 / Phase 21 §A — programmatic thinking-level change.
21130
- * Journals a `thinking_changed` WAL record (via
21131
- * ContextState.applyConfigChange) and emits two SoulEvents:
21230
+ * Persists state.json, updates ContextState, and emits two SoulEvents:
21132
21231
  * 1. `thinking.changed` — distinct event the wire bridge maps to a
21133
21232
  * `thinking.changed` wire event with the bridge's per-session
21134
21233
  * `seq` counter (replaces the old `seq: 0` direct send from
@@ -21137,6 +21236,19 @@ var SoulPlus = class {
21137
21236
  * can repaint without subscribing to the dedicated event.
21138
21237
  */
21139
21238
  async setThinking(level) {
21239
+ await this.stateCache?.update((current) => {
21240
+ const now = Date.now();
21241
+ return {
21242
+ ...current ?? {
21243
+ session_id: this.sessionId,
21244
+ created_at: now,
21245
+ updated_at: now
21246
+ },
21247
+ thinking_level: level,
21248
+ updated_at: now,
21249
+ last_exit_code: "dirty"
21250
+ };
21251
+ });
21140
21252
  await this.journal.contextState.applyConfigChange({
21141
21253
  type: "thinking_changed",
21142
21254
  level
@@ -21288,17 +21400,35 @@ var KosongAdapter = class {
21288
21400
  }));
21289
21401
  const onDelta = params.onDelta;
21290
21402
  const onThinkDelta = params.onThinkDelta;
21403
+ const onToolCallPart = params.onToolCallPart;
21291
21404
  const onAtomicPart = params.onAtomicPart;
21292
- const needMessagePart = onDelta !== void 0 || onThinkDelta !== void 0;
21405
+ const needMessagePart = onDelta !== void 0 || onThinkDelta !== void 0 || onToolCallPart !== void 0;
21406
+ const toolCallsByStreamIndex = /* @__PURE__ */ new Map();
21407
+ let lastToolCall;
21293
21408
  let result;
21294
21409
  try {
21295
21410
  result = await generate$1(activeProvider, params.systemPrompt, kosongTools, params.messages, needMessagePart ? { onMessagePart: async (part) => {
21296
21411
  if (part.type === "text" && onDelta !== void 0) onDelta(part.text);
21297
21412
  else if (part.type === "think" && onThinkDelta !== void 0) onThinkDelta(part.think);
21413
+ else if (part.type === "function") {
21414
+ lastToolCall = {
21415
+ id: part.id,
21416
+ name: part.function.name
21417
+ };
21418
+ if (part._streamIndex !== void 0) toolCallsByStreamIndex.set(part._streamIndex, lastToolCall);
21419
+ } else if (isToolCallPart(part) && onToolCallPart !== void 0) {
21420
+ const target = part.index !== void 0 ? toolCallsByStreamIndex.get(part.index) : lastToolCall;
21421
+ if (target !== void 0) onToolCallPart({
21422
+ type: "tool_call_part",
21423
+ tool_call_id: target.id,
21424
+ name: target.name,
21425
+ ...part.argumentsPart !== null ? { arguments_chunk: part.argumentsPart } : {}
21426
+ });
21427
+ }
21298
21428
  } } : void 0, { signal: params.signal });
21299
- } catch (err) {
21300
- if (isContextOverflowProviderError(err)) throw new ContextOverflowError(extractMessage(err));
21301
- throw err;
21429
+ } catch (error) {
21430
+ if (isContextOverflowProviderError(error)) throw new ContextOverflowError(extractMessage(error));
21431
+ throw error;
21302
21432
  }
21303
21433
  if (onAtomicPart !== void 0) {
21304
21434
  for (const part of result.message.content) if (part.type === "text" || part.type === "think") await onAtomicPart({
@@ -29313,6 +29443,28 @@ var SetTodoListTool = class {
29313
29443
  }));
29314
29444
  }
29315
29445
  };
29446
+ //#endregion
29447
+ //#region ../../packages/kimi-core/src/hooks/supported-events.ts
29448
+ const SUPPORTED_HOOK_EVENTS = [
29449
+ "PreTurn",
29450
+ "PostTurn",
29451
+ "PreStep",
29452
+ "PostStep",
29453
+ "PreToolUse",
29454
+ "PostToolUse",
29455
+ "OnToolFailure",
29456
+ "UserPromptSubmit",
29457
+ "Stop",
29458
+ "StopFailure",
29459
+ "Notification",
29460
+ "SubagentStart",
29461
+ "SubagentStop",
29462
+ "SessionStart",
29463
+ "SessionEnd",
29464
+ "PreCompact",
29465
+ "PostCompact"
29466
+ ];
29467
+ new Set(SUPPORTED_HOOK_EVENTS);
29316
29468
  const _rawWireErrorSchema = z.object({
29317
29469
  code: z.number(),
29318
29470
  message: z.string(),
@@ -29505,188 +29657,332 @@ function createWireEvent(options) {
29505
29657
  };
29506
29658
  }
29507
29659
  //#endregion
29660
+ //#region ../../packages/kimi-core/src/wire-protocol/event-pipeline.ts
29661
+ /**
29662
+ * WireEventPipeline is the ordered, awaited publication path from a translated
29663
+ * `WireEventDraft` to one or more event consumers.
29664
+ *
29665
+ * `enqueue()` intentionally returns void: upstream Soul/SoulPlus emitters must
29666
+ * not be back-pressured by transport, telemetry, or debug consumers. The
29667
+ * pipeline drains in the background and awaits each middleware/consumer
29668
+ * internally to preserve per-session event ordering.
29669
+ */
29670
+ var WireEventPipeline = class {
29671
+ sessionId;
29672
+ getEventFilter;
29673
+ middlewares;
29674
+ consumers;
29675
+ queue = [];
29676
+ idleWaiters = [];
29677
+ draining = false;
29678
+ seq = 0;
29679
+ constructor(options) {
29680
+ this.sessionId = options.sessionId;
29681
+ this.getEventFilter = options.getEventFilter;
29682
+ this.consumers = options.consumers ?? [createTransportWireEventConsumer({
29683
+ transport: options.transport,
29684
+ codec: options.codec
29685
+ })];
29686
+ this.middlewares = [
29687
+ createWireEventFilterMiddleware(this.getEventFilter),
29688
+ ...options.middlewares ?? [],
29689
+ this.createFrameMiddleware(),
29690
+ createWireEventConsumerMiddleware(this.consumers)
29691
+ ];
29692
+ }
29693
+ enqueue(draft) {
29694
+ this.queue.push(draft);
29695
+ if (!this.draining) this.drain();
29696
+ }
29697
+ /**
29698
+ * Test / controlled-host helper: resolves once the queue that is currently
29699
+ * known to the pipeline has drained. Normal production callers should use
29700
+ * fire-and-forget `enqueue()`.
29701
+ */
29702
+ async flush() {
29703
+ if (!this.draining && this.queue.length === 0) return;
29704
+ await new Promise((resolve) => {
29705
+ this.idleWaiters.push(resolve);
29706
+ });
29707
+ }
29708
+ async drain() {
29709
+ this.draining = true;
29710
+ let restart = false;
29711
+ try {
29712
+ while (this.queue.length > 0) {
29713
+ const draft = this.queue.shift();
29714
+ if (draft === void 0) continue;
29715
+ await this.processOne(draft);
29716
+ }
29717
+ } finally {
29718
+ this.draining = false;
29719
+ if (this.queue.length > 0) restart = true;
29720
+ else {
29721
+ const waiters = this.idleWaiters.splice(0);
29722
+ for (const resolve of waiters) resolve();
29723
+ }
29724
+ }
29725
+ if (restart) this.drain();
29726
+ }
29727
+ async processOne(draft) {
29728
+ const ctx = {
29729
+ sessionId: this.sessionId,
29730
+ draft
29731
+ };
29732
+ try {
29733
+ await composeWireEventMiddlewares(this.middlewares)(ctx);
29734
+ } catch {}
29735
+ }
29736
+ createFrameMiddleware() {
29737
+ return async (ctx, next) => {
29738
+ ctx.event = createWireEvent({
29739
+ method: ctx.draft.method,
29740
+ sessionId: ctx.sessionId,
29741
+ seq: this.seq++,
29742
+ ...ctx.draft.requestId !== void 0 ? { requestId: ctx.draft.requestId } : {},
29743
+ ...ctx.draft.turnId !== void 0 ? { turnId: ctx.draft.turnId } : {},
29744
+ agentType: ctx.draft.agentType ?? "main",
29745
+ ...ctx.draft.source !== void 0 ? { source: ctx.draft.source } : {},
29746
+ data: ctx.draft.data
29747
+ });
29748
+ await next();
29749
+ };
29750
+ }
29751
+ };
29752
+ function createWireEventFilterMiddleware(getEventFilter) {
29753
+ return async (ctx, next) => {
29754
+ if (getEventFilter !== void 0) {
29755
+ const filter = getEventFilter();
29756
+ if (filter !== void 0 && !filter.has(ctx.draft.method)) return;
29757
+ }
29758
+ await next();
29759
+ };
29760
+ }
29761
+ function createTransportWireEventConsumer(options) {
29762
+ const codec = options.codec ?? new WireCodec();
29763
+ return {
29764
+ name: "transport",
29765
+ async consume(event) {
29766
+ try {
29767
+ await options.transport.send(codec.encode(event));
29768
+ } catch {}
29769
+ }
29770
+ };
29771
+ }
29772
+ function createWireEventConsumerMiddleware(consumers) {
29773
+ return async (ctx) => {
29774
+ if (ctx.event === void 0) return;
29775
+ const snapshot = [...consumers];
29776
+ for (const consumer of snapshot) try {
29777
+ await consumer.consume(ctx.event, {
29778
+ sessionId: ctx.sessionId,
29779
+ draft: ctx.draft
29780
+ });
29781
+ } catch {}
29782
+ };
29783
+ }
29784
+ function composeWireEventMiddlewares(middlewares) {
29785
+ return async (ctx) => {
29786
+ let index = -1;
29787
+ const dispatch = async (i) => {
29788
+ if (i <= index) throw new Error("wire event middleware called next() multiple times");
29789
+ index = i;
29790
+ const middleware = middlewares[i];
29791
+ if (middleware === void 0) return;
29792
+ await middleware(ctx, () => dispatch(i + 1));
29793
+ };
29794
+ await dispatch(0);
29795
+ };
29796
+ }
29797
+ //#endregion
29798
+ //#region ../../packages/kimi-core/src/wire-protocol/event-translators.ts
29799
+ function draft(event, ctx, method, data) {
29800
+ return {
29801
+ method,
29802
+ data,
29803
+ ...ctx.currentTurnId !== void 0 ? { turnId: ctx.currentTurnId } : {},
29804
+ ...event.source !== void 0 ? { source: event.source } : {}
29805
+ };
29806
+ }
29807
+ const BUS_EVENT_TRANSLATORS = {
29808
+ "step.begin": (event, ctx) => {
29809
+ return draft(event, ctx, "step.begin", { step: event.step });
29810
+ },
29811
+ "step.end": (event, ctx) => {
29812
+ return draft(event, ctx, "step.end", { step: event.step });
29813
+ },
29814
+ "step.interrupted": (event, ctx) => {
29815
+ return draft(event, ctx, "step.interrupted", {
29816
+ step: event.step,
29817
+ reason: event.reason
29818
+ });
29819
+ },
29820
+ "content.delta": (event, ctx) => {
29821
+ return draft(event, ctx, "content.delta", {
29822
+ type: "text",
29823
+ text: event.delta
29824
+ });
29825
+ },
29826
+ "thinking.delta": (event, ctx) => {
29827
+ return draft(event, ctx, "content.delta", {
29828
+ type: "thinking",
29829
+ thinking: event.delta
29830
+ });
29831
+ },
29832
+ tool_call_part: (event, ctx) => {
29833
+ return draft(event, ctx, "tool.call.delta", {
29834
+ tool_call_id: event.tool_call_id,
29835
+ ...event.name !== void 0 ? { name: event.name } : {},
29836
+ arguments_part: event.arguments_chunk ?? null
29837
+ });
29838
+ },
29839
+ "tool.call": (event, ctx) => {
29840
+ return draft(event, ctx, "tool.call", {
29841
+ id: event.toolCallId,
29842
+ name: event.name,
29843
+ args: event.args
29844
+ });
29845
+ },
29846
+ "tool.progress": (event, ctx) => draft(event, ctx, "tool.progress", {
29847
+ id: event.toolCallId,
29848
+ update: event.update
29849
+ }),
29850
+ "tool.result": (event, ctx) => {
29851
+ return draft(event, ctx, "tool.result", {
29852
+ tool_call_id: event.toolCallId,
29853
+ output: event.output,
29854
+ ...event.isError !== void 0 ? { is_error: event.isError } : {}
29855
+ });
29856
+ },
29857
+ "compaction.begin": (event, ctx) => {
29858
+ return draft(event, ctx, "compaction.begin", {});
29859
+ },
29860
+ "compaction.end": (event, ctx) => {
29861
+ return draft(event, ctx, "compaction.end", {
29862
+ ...event.tokensBefore !== void 0 ? { tokens_before: event.tokensBefore } : {},
29863
+ ...event.tokensAfter !== void 0 ? { tokens_after: event.tokensAfter } : {}
29864
+ });
29865
+ },
29866
+ "session.error": (event, ctx) => {
29867
+ return draft(event, ctx, "session.error", {
29868
+ error: event.error,
29869
+ ...event.error_type !== void 0 ? { error_type: event.error_type } : {},
29870
+ ...event.retry_after_ms !== void 0 ? { retry_after_ms: event.retry_after_ms } : {},
29871
+ ...event.details !== void 0 ? { details: event.details } : {}
29872
+ });
29873
+ },
29874
+ "hook.triggered": (event, ctx) => draft(event, ctx, "hook.triggered", {
29875
+ event: event.event,
29876
+ matchers: event.matchers,
29877
+ matched_count: event.matched_count
29878
+ }),
29879
+ "hook.resolved": (event, ctx) => draft(event, ctx, "hook.resolved", {
29880
+ hook_id: event.hook_id,
29881
+ outcome: event.outcome
29882
+ }),
29883
+ "status.update": (event, ctx) => draft(event, ctx, "status.update", event.data),
29884
+ "session_meta.changed": (event, ctx) => draft(event, ctx, "session_meta.changed", event.data),
29885
+ "thinking.changed": (event, ctx) => draft(event, ctx, "thinking.changed", { level: event.level }),
29886
+ "skill.invoked": (event, ctx) => draft(event, ctx, "skill.invoked", event.data),
29887
+ "skill.completed": (event, ctx) => draft(event, ctx, "skill.completed", event.data),
29888
+ "mcp.loading": (event, ctx) => draft(event, ctx, "mcp.loading", event.data),
29889
+ "mcp.connected": (event, ctx) => draft(event, ctx, "mcp.connected", event.data),
29890
+ "mcp.disconnected": (event, ctx) => draft(event, ctx, "mcp.disconnected", event.data),
29891
+ "mcp.error": (event, ctx) => draft(event, ctx, "mcp.error", event.data),
29892
+ "mcp.tools_changed": (event, ctx) => draft(event, ctx, "mcp.tools_changed", event.data),
29893
+ "mcp.resources_changed": (event, ctx) => draft(event, ctx, "mcp.resources_changed", event.data),
29894
+ "mcp.auth_required": (event, ctx) => draft(event, ctx, "mcp.auth_required", event.data),
29895
+ "status.update.mcp_status": (event, ctx) => draft(event, ctx, "status.update.mcp_status", event.data),
29896
+ "subagent.spawned": (event, ctx) => draft(event, ctx, "subagent.spawned", event.data),
29897
+ "subagent.completed": (event, ctx) => draft(event, ctx, "subagent.completed", event.data),
29898
+ "subagent.failed": (event, ctx) => draft(event, ctx, "subagent.failed", event.data),
29899
+ "model.changed": () => {},
29900
+ "turn.end": () => {}
29901
+ };
29902
+ function translateBusEvent(event, currentTurnId) {
29903
+ const translator = BUS_EVENT_TRANSLATORS[event.type];
29904
+ return translator(event, currentTurnId !== void 0 ? { currentTurnId } : {});
29905
+ }
29906
+ function translateNotificationEvent(notif) {
29907
+ return {
29908
+ method: "notification",
29909
+ data: notif
29910
+ };
29911
+ }
29912
+ function translateTurnLifecycleEvent(event, currentTurnId) {
29913
+ if (event.kind === "begin") return {
29914
+ draft: {
29915
+ method: "turn.begin",
29916
+ data: {
29917
+ turn_id: event.turnId,
29918
+ user_input: event.userInputParts ?? event.userInput,
29919
+ input_kind: event.inputKind
29920
+ },
29921
+ turnId: event.turnId
29922
+ },
29923
+ nextTurnId: event.turnId
29924
+ };
29925
+ return {
29926
+ draft: {
29927
+ method: "turn.end",
29928
+ data: {
29929
+ turn_id: event.turnId,
29930
+ reason: event.reason,
29931
+ success: event.success,
29932
+ ...event.usage !== void 0 ? { usage: {
29933
+ input_tokens: event.usage.input,
29934
+ output_tokens: event.usage.output,
29935
+ ...event.usage.cache_read !== void 0 ? { cache_read_tokens: event.usage.cache_read } : {},
29936
+ ...event.usage.cache_write !== void 0 ? { cache_write_tokens: event.usage.cache_write } : {}
29937
+ } } : {}
29938
+ },
29939
+ turnId: event.turnId
29940
+ },
29941
+ ...currentTurnId === event.turnId && currentTurnId !== void 0 ? {} : currentTurnId !== void 0 ? { nextTurnId: currentTurnId } : {}
29942
+ };
29943
+ }
29944
+ //#endregion
29508
29945
  //#region ../../packages/kimi-core/src/wire-protocol/event-bridge.ts
29509
29946
  /**
29510
- * WireEventBridge — SoulEvent + TurnLifecycleEvent → WireEvent translator
29947
+ * WireEventBridge — SoulEvent + TurnLifecycleEvent → WireEventDraft adapter
29511
29948
  * (Phase 17 §A.1).
29512
29949
  *
29513
- * Subscribes to two input channels (SessionEventBus + TurnLifecycleTracker)
29514
- * and forwards translated frames through a provided transport `send`.
29515
- * Each bridge instance owns its own `seq` counter and `currentTurnId`
29516
- * pointer multi-session safe (fixes the single-ref aliasing in the
29517
- * Phase 10 test helper).
29950
+ * Subscribes to three input channels (SessionEventBus + notification channel
29951
+ * + TurnLifecycleTracker) and forwards translated drafts through a
29952
+ * per-session WireEventPipeline. Each bridge instance owns its own
29953
+ * `currentTurnId` pointer; the pipeline owns the per-session wire `seq`.
29518
29954
  *
29519
29955
  * Invariants (§5.0):
29520
- * - L4: listeners return `void`; the bridge never awaits `transport.send`
29956
+ * - L4: listeners return `void`; the bridge never awaits pipeline drain
29521
29957
  * - L5: listener does not touch wire.jsonl — translation is write-only
29522
29958
  * toward the transport
29523
29959
  */
29524
- const codec = new WireCodec();
29525
29960
  function installWireEventBridge(opts) {
29526
29961
  const { server, eventBus, addTurnLifecycleListener, sessionId, getEventFilter } = opts;
29527
- let seq = 0;
29528
29962
  let currentTurnId;
29529
- const sendWire = (method, data, turnId, source) => {
29530
- if (getEventFilter !== void 0) {
29531
- const filter = getEventFilter();
29532
- if (filter !== void 0 && !filter.has(method)) return;
29533
- }
29534
- const frame = codec.encode(createWireEvent({
29535
- method,
29536
- sessionId,
29537
- seq: seq++,
29538
- ...turnId !== void 0 ? { turnId } : {},
29539
- agentType: "main",
29540
- ...source !== void 0 ? { source } : {},
29541
- data
29542
- }));
29543
- server.send(frame).catch(() => {});
29544
- };
29963
+ const pipeline = new WireEventPipeline({
29964
+ sessionId,
29965
+ transport: server,
29966
+ ...getEventFilter !== void 0 ? { getEventFilter } : {}
29967
+ });
29545
29968
  const soulListener = (event) => {
29546
- switch (event.type) {
29547
- case "step.begin":
29548
- sendWire("step.begin", { step: event.step }, currentTurnId, event.source);
29549
- return;
29550
- case "step.end":
29551
- sendWire("step.end", { step: event.step }, currentTurnId, event.source);
29552
- return;
29553
- case "step.interrupted":
29554
- sendWire("step.interrupted", {
29555
- step: event.step,
29556
- reason: event.reason
29557
- }, currentTurnId, event.source);
29558
- return;
29559
- case "content.delta":
29560
- sendWire("content.delta", {
29561
- type: "text",
29562
- text: event.delta
29563
- }, currentTurnId, event.source);
29564
- return;
29565
- case "thinking.delta":
29566
- sendWire("content.delta", {
29567
- type: "thinking",
29568
- thinking: event.delta
29569
- }, currentTurnId, event.source);
29570
- return;
29571
- case "tool_call_part":
29572
- sendWire("content.delta", {
29573
- type: "tool_call_part",
29574
- tool_call_id: event.tool_call_id,
29575
- ...event.name !== void 0 ? { name: event.name } : {},
29576
- ...event.arguments_chunk !== void 0 ? { arguments_chunk: event.arguments_chunk } : {}
29577
- }, currentTurnId, event.source);
29578
- return;
29579
- case "tool.call":
29580
- sendWire("tool.call", {
29581
- id: event.toolCallId,
29582
- name: event.name,
29583
- args: event.args
29584
- }, currentTurnId, event.source);
29585
- return;
29586
- case "tool.progress":
29587
- sendWire("tool.progress", {
29588
- id: event.toolCallId,
29589
- update: event.update
29590
- }, currentTurnId, event.source);
29591
- return;
29592
- case "tool.result":
29593
- sendWire("tool.result", {
29594
- tool_call_id: event.toolCallId,
29595
- output: event.output,
29596
- ...event.isError !== void 0 ? { is_error: event.isError } : {}
29597
- }, currentTurnId, event.source);
29598
- return;
29599
- case "compaction.begin":
29600
- sendWire("compaction.begin", {}, currentTurnId, event.source);
29601
- return;
29602
- case "compaction.end":
29603
- sendWire("compaction.end", {
29604
- ...event.tokensBefore !== void 0 ? { tokens_before: event.tokensBefore } : {},
29605
- ...event.tokensAfter !== void 0 ? { tokens_after: event.tokensAfter } : {}
29606
- }, currentTurnId, event.source);
29607
- return;
29608
- case "session.error":
29609
- sendWire("session.error", {
29610
- error: event.error,
29611
- ...event.error_type !== void 0 ? { error_type: event.error_type } : {},
29612
- ...event.retry_after_ms !== void 0 ? { retry_after_ms: event.retry_after_ms } : {},
29613
- ...event.details !== void 0 ? { details: event.details } : {}
29614
- }, currentTurnId, event.source);
29615
- return;
29616
- case "hook.triggered":
29617
- sendWire("hook.triggered", {
29618
- event: event.event,
29619
- matchers: event.matchers,
29620
- matched_count: event.matched_count
29621
- }, currentTurnId, event.source);
29622
- return;
29623
- case "hook.resolved":
29624
- sendWire("hook.resolved", {
29625
- hook_id: event.hook_id,
29626
- outcome: event.outcome
29627
- }, currentTurnId, event.source);
29628
- return;
29629
- case "status.update":
29630
- sendWire("status.update", event.data, currentTurnId, event.source);
29631
- return;
29632
- case "session_meta.changed":
29633
- sendWire("session_meta.changed", event.data, currentTurnId, event.source);
29634
- return;
29635
- case "thinking.changed":
29636
- sendWire("thinking.changed", { level: event.level }, currentTurnId, event.source);
29637
- return;
29638
- case "skill.invoked":
29639
- sendWire("skill.invoked", event.data, currentTurnId, event.source);
29640
- return;
29641
- case "skill.completed":
29642
- sendWire("skill.completed", event.data, currentTurnId, event.source);
29643
- return;
29644
- case "mcp.loading":
29645
- case "mcp.connected":
29646
- case "mcp.disconnected":
29647
- case "mcp.error":
29648
- case "mcp.tools_changed":
29649
- case "mcp.resources_changed":
29650
- case "mcp.auth_required":
29651
- case "status.update.mcp_status":
29652
- sendWire(event.type, event.data, currentTurnId, event.source);
29653
- return;
29654
- case "subagent.spawned":
29655
- case "subagent.completed":
29656
- case "subagent.failed":
29657
- sendWire(event.type, event.data, currentTurnId, event.source);
29658
- return;
29659
- default:
29660
- }
29969
+ const draft = translateBusEvent(event, currentTurnId);
29970
+ if (draft !== void 0) pipeline.enqueue(draft);
29971
+ };
29972
+ const notificationListener = (notif) => {
29973
+ pipeline.enqueue(translateNotificationEvent(notif));
29661
29974
  };
29662
29975
  const turnListener = (event) => {
29663
- if (event.kind === "begin") {
29664
- currentTurnId = event.turnId;
29665
- sendWire("turn.begin", {
29666
- turn_id: event.turnId,
29667
- user_input: event.userInputParts ?? event.userInput,
29668
- input_kind: event.inputKind
29669
- }, event.turnId);
29670
- return;
29671
- }
29672
- const turnId = event.turnId;
29673
- sendWire("turn.end", {
29674
- turn_id: turnId,
29675
- reason: event.reason,
29676
- success: event.success,
29677
- ...event.usage !== void 0 ? { usage: {
29678
- input_tokens: event.usage.input,
29679
- output_tokens: event.usage.output,
29680
- ...event.usage.cache_read !== void 0 ? { cache_read_tokens: event.usage.cache_read } : {},
29681
- ...event.usage.cache_write !== void 0 ? { cache_write_tokens: event.usage.cache_write } : {}
29682
- } } : {}
29683
- }, turnId);
29684
- if (currentTurnId === turnId) currentTurnId = void 0;
29976
+ const translated = translateTurnLifecycleEvent(event, currentTurnId);
29977
+ currentTurnId = translated.nextTurnId;
29978
+ pipeline.enqueue(translated.draft);
29685
29979
  };
29686
29980
  const unsubTurn = addTurnLifecycleListener(turnListener);
29687
29981
  eventBus.on(soulListener);
29982
+ eventBus.subscribeNotifications(notificationListener);
29688
29983
  return { dispose() {
29689
29984
  eventBus.off(soulListener);
29985
+ eventBus.unsubscribeNotifications(notificationListener);
29690
29986
  unsubTurn();
29691
29987
  } };
29692
29988
  }
@@ -30116,6 +30412,48 @@ function errorMessage(error) {
30116
30412
  return error instanceof Error ? error.message : String(error);
30117
30413
  }
30118
30414
  //#endregion
30415
+ //#region ../../packages/kimi-core/src/session/state-cache.ts
30416
+ /**
30417
+ * StateCache — state.json derived cache (§9续).
30418
+ *
30419
+ * Reads / writes `state.json` in a session directory. Used by
30420
+ * SessionManager to persist session metadata and runtime state (model,
30421
+ * permission mode, plan mode, last turn index) outside the transcript.
30422
+ *
30423
+ * Writes go through `atomicWrite` (write-tmp-fsync-rename, Decision #104)
30424
+ * so a crash mid-write never leaves a half-truncated state.json that
30425
+ * subsequent reads would treat as "missing". Read-modify-write callers
30426
+ * still need their own concurrency guard for "merge then write" races
30427
+ * across processes — see SessionManager.renameSession.
30428
+ */
30429
+ var StateCache = class {
30430
+ writeQueue = Promise.resolve();
30431
+ constructor(statePath) {
30432
+ this.statePath = statePath;
30433
+ }
30434
+ async read() {
30435
+ try {
30436
+ const raw = await readFile(this.statePath, "utf-8");
30437
+ if (raw.length === 0) return null;
30438
+ return JSON.parse(raw);
30439
+ } catch {
30440
+ return null;
30441
+ }
30442
+ }
30443
+ async write(state) {
30444
+ await atomicWrite(this.statePath, JSON.stringify(state, null, 2));
30445
+ }
30446
+ async update(mutator) {
30447
+ const nextWrite = this.writeQueue.then(async () => {
30448
+ const next = mutator(await this.read());
30449
+ await this.write(next);
30450
+ return next;
30451
+ });
30452
+ this.writeQueue = nextWrite.then(() => {}, () => {});
30453
+ return nextWrite;
30454
+ }
30455
+ };
30456
+ //#endregion
30119
30457
  //#region ../../packages/kimi-core/src/session/session-application-service.ts
30120
30458
  function withCompactionConfigFallback(bundle, fallback) {
30121
30459
  if (bundle.compactionConfig !== void 0 || fallback === void 0) return bundle;
@@ -30192,7 +30530,7 @@ var DefaultSessionApplicationService = class {
30192
30530
  eventBus,
30193
30531
  ...hookEngine !== void 0 ? { hookEngine } : {}
30194
30532
  });
30195
- const initialModel = this.deps.defaultModelProvider();
30533
+ const initialModel = (await new StateCache(this.deps.pathConfig.statePath(sessionId)).read())?.model ?? this.deps.defaultModelProvider();
30196
30534
  const runtimeBundle = await this.deps.runtimeBundleProvider?.({
30197
30535
  sessionId,
30198
30536
  model: initialModel
@@ -30268,22 +30606,13 @@ var DefaultSessionApplicationService = class {
30268
30606
  }
30269
30607
  async resumeSnapshot(sessionId) {
30270
30608
  const managed = this.getManaged(sessionId);
30271
- let turnCount = 0;
30272
- let lastTurnId;
30273
- try {
30274
- const bodyLines = (await readFile(this.deps.pathConfig.wirePath(sessionId), "utf8")).split("\n").filter((line) => line.length > 0).slice(1);
30275
- for (const line of bodyLines) try {
30276
- const rec = JSON.parse(line);
30277
- if (rec.type === "turn_begin") {
30278
- turnCount += 1;
30279
- if (rec.turn_id !== void 0) lastTurnId = rec.turn_id;
30280
- }
30281
- } catch {}
30282
- } catch {}
30609
+ const state = await managed.stateCache.read();
30610
+ const turnCount = state?.turn_count ?? 0;
30611
+ const lastTurnId = state?.last_turn_id;
30283
30612
  managed.eventBus.emit({
30284
30613
  type: "status.update",
30285
30614
  data: {
30286
- model: this.deps.defaultModelProvider(),
30615
+ model: managed.contextState.model,
30287
30616
  plan_mode: managed.soulPlus.getTurnManager().getPlanMode(),
30288
30617
  yolo: managed.soulPlus.getTurnManager().getPermissionMode() === "bypassPermissions"
30289
30618
  }
@@ -30349,7 +30678,21 @@ var DefaultSessionApplicationService = class {
30349
30678
  }
30350
30679
  async setYolo(sessionId, enabled) {
30351
30680
  const managed = this.getManaged(sessionId);
30352
- await managed.sessionControl.setYolo(enabled);
30681
+ const permissionMode = enabled ? "bypassPermissions" : "default";
30682
+ await managed.stateCache.update((current) => {
30683
+ const now = Date.now();
30684
+ return {
30685
+ ...current ?? {
30686
+ session_id: sessionId,
30687
+ created_at: now,
30688
+ updated_at: now
30689
+ },
30690
+ permission_mode: permissionMode,
30691
+ updated_at: now,
30692
+ last_exit_code: "dirty"
30693
+ };
30694
+ });
30695
+ managed.soulPlus.getTurnManager().setPermissionMode(permissionMode);
30353
30696
  if (this.deps.approvalStateStore !== void 0) await this.deps.approvalStateStore.setYolo(enabled);
30354
30697
  managed.soulPlus.getTurnManager().emitStatusUpdate({
30355
30698
  input: 0,
@@ -30384,6 +30727,19 @@ var DefaultSessionApplicationService = class {
30384
30727
  compactionConfig
30385
30728
  });
30386
30729
  }
30730
+ await managed.stateCache.update((current) => {
30731
+ const now = Date.now();
30732
+ return {
30733
+ ...current ?? {
30734
+ session_id: sessionId,
30735
+ created_at: now,
30736
+ updated_at: now
30737
+ },
30738
+ model,
30739
+ updated_at: now,
30740
+ last_exit_code: "dirty"
30741
+ };
30742
+ });
30387
30743
  await managed.soulPlus.setModel(model);
30388
30744
  }
30389
30745
  async setSystemPrompt(sessionId, prompt) {
@@ -30393,7 +30749,21 @@ var DefaultSessionApplicationService = class {
30393
30749
  });
30394
30750
  }
30395
30751
  async setThinking(sessionId, level) {
30396
- await this.getManaged(sessionId).soulPlus.setThinking(level);
30752
+ const managed = this.getManaged(sessionId);
30753
+ await managed.stateCache.update((current) => {
30754
+ const now = Date.now();
30755
+ return {
30756
+ ...current ?? {
30757
+ session_id: sessionId,
30758
+ created_at: now,
30759
+ updated_at: now
30760
+ },
30761
+ thinking_level: level,
30762
+ updated_at: now,
30763
+ last_exit_code: "dirty"
30764
+ };
30765
+ });
30766
+ await managed.soulPlus.setThinking(level);
30397
30767
  }
30398
30768
  async addSystemReminder(sessionId, content) {
30399
30769
  await this.getManaged(sessionId).soulPlus.addSystemReminder(content);
@@ -30448,7 +30818,7 @@ var ChangeListenerRegistry = class {
30448
30818
  }
30449
30819
  };
30450
30820
  /**
30451
- * Production adapter that reads / writes `auto_approve_actions` + `yolo`
30821
+ * Production adapter that reads / writes `auto_approve_actions` + `permission_mode`
30452
30822
  * on the session's `state.json` file via the existing `StateCache`
30453
30823
  * service. The rest of the state.json fields are preserved on write —
30454
30824
  * we only rewrite the approval-state fields.
@@ -30476,7 +30846,7 @@ var SessionStateApprovalStateStore = class {
30476
30846
  const raw = state?.auto_approve_actions;
30477
30847
  const actions = raw === void 0 ? /* @__PURE__ */ new Set() : new Set(raw);
30478
30848
  this.cachedActions = new Set(actions);
30479
- this.cachedYolo = state?.yolo ?? false;
30849
+ this.cachedYolo = permissionModeFromState(state) === "bypassPermissions";
30480
30850
  return actions;
30481
30851
  }
30482
30852
  async save(actions) {
@@ -30487,7 +30857,7 @@ var SessionStateApprovalStateStore = class {
30487
30857
  async getYolo() {
30488
30858
  if (this.cachedYolo !== void 0) return this.cachedYolo;
30489
30859
  const state = await this.stateCache.read();
30490
- this.cachedYolo = state?.yolo ?? false;
30860
+ this.cachedYolo = permissionModeFromState(state) === "bypassPermissions";
30491
30861
  this.cachedActions = new Set(state?.auto_approve_actions ?? []);
30492
30862
  return this.cachedYolo;
30493
30863
  }
@@ -30505,7 +30875,7 @@ var SessionStateApprovalStateStore = class {
30505
30875
  const current = await this.stateCache.read();
30506
30876
  const now = this.now();
30507
30877
  const nextActions = patch.actions !== void 0 ? [...patch.actions] : current?.auto_approve_actions !== void 0 ? [...current.auto_approve_actions] : void 0;
30508
- const nextYolo = patch.yolo !== void 0 ? patch.yolo : current?.yolo;
30878
+ const nextMode = patch.yolo !== void 0 ? patch.yolo ? "bypassPermissions" : "default" : permissionModeFromState(current);
30509
30879
  const next = {
30510
30880
  ...current ? {
30511
30881
  ...current,
@@ -30516,7 +30886,7 @@ var SessionStateApprovalStateStore = class {
30516
30886
  updated_at: now
30517
30887
  },
30518
30888
  ...nextActions !== void 0 ? { auto_approve_actions: nextActions } : {},
30519
- ...nextYolo !== void 0 ? { yolo: nextYolo } : {}
30889
+ permission_mode: nextMode
30520
30890
  };
30521
30891
  await this.stateCache.write(next);
30522
30892
  }
@@ -30527,6 +30897,11 @@ var SessionStateApprovalStateStore = class {
30527
30897
  });
30528
30898
  }
30529
30899
  };
30900
+ function permissionModeFromState(state) {
30901
+ const mode = state?.permission_mode;
30902
+ if (mode === "acceptEdits" || mode === "bypassPermissions") return mode;
30903
+ return "default";
30904
+ }
30530
30905
  //#endregion
30531
30906
  //#region ../../packages/kimi-core/src/wire-protocol/reverse-rpc.ts
30532
30907
  /**
@@ -30686,6 +31061,10 @@ function buildHookRequestData(subscriptionId, input) {
30686
31061
  base["tool_call_id"] = input.toolCall.id;
30687
31062
  base["args"] = input.toolCall.args;
30688
31063
  }
31064
+ if (input.event === "PreTurn" || input.event === "UserPromptSubmit") base["prompt"] = input.prompt;
31065
+ if (input.event === "PostTurn" || input.event === "Stop") base["reason"] = input.reason;
31066
+ if (input.event === "PreStep" || input.event === "PostStep") base["step_number"] = input.stepNumber;
31067
+ if (input.event === "PostStep") base["stop_reason"] = input.stopReason;
30689
31068
  return base;
30690
31069
  }
30691
31070
  //#endregion
@@ -30728,21 +31107,6 @@ function buildHookRequestData(subscriptionId, input) {
30728
31107
  * - `registerDefaultWireHandlers` returns a handle so the host can
30729
31108
  * plug per-session state (event filter for now) into the bridge.
30730
31109
  */
30731
- const HOOK_SUPPORTED_EVENTS = [
30732
- "PreToolUse",
30733
- "PostToolUse",
30734
- "OnToolFailure",
30735
- "UserPromptSubmit",
30736
- "Stop",
30737
- "StopFailure",
30738
- "Notification",
30739
- "SubagentStart",
30740
- "SubagentStop",
30741
- "SessionStart",
30742
- "SessionEnd",
30743
- "PreCompact",
30744
- "PostCompact"
30745
- ];
30746
31110
  const SUPPORTED_WIRE_EVENTS = [
30747
31111
  "turn.begin",
30748
31112
  "turn.end",
@@ -30981,7 +31345,7 @@ function registerDefaultWireHandlers(deps) {
30981
31345
  events: [...SUPPORTED_WIRE_EVENTS],
30982
31346
  methods: [...SUPPORTED_WIRE_METHODS],
30983
31347
  hooks: {
30984
- supported_events: HOOK_SUPPORTED_EVENTS,
31348
+ supported_events: SUPPORTED_HOOK_EVENTS,
30985
31349
  configured: initialHooks.map((h) => ({
30986
31350
  event: h.event,
30987
31351
  ...typeof h.matcher === "string" && h.matcher.length > 0 ? { matcher: h.matcher } : {}
@@ -32063,12 +32427,6 @@ async function repairJournal(options) {
32063
32427
  }
32064
32428
  //#endregion
32065
32429
  //#region ../../packages/kimi-core/src/session/replay-projector.ts
32066
- /** Whitelist of valid PermissionMode values for safe runtime validation. */
32067
- const VALID_PERMISSION_MODES = new Set([
32068
- "default",
32069
- "acceptEdits",
32070
- "bypassPermissions"
32071
- ]);
32072
32430
  function isContentPart(value) {
32073
32431
  if (typeof value !== "object" || value === null) return false;
32074
32432
  const part = value;
@@ -32084,26 +32442,26 @@ function cloneContentPart(part) {
32084
32442
  * Project replayed WireRecords into the initial state needed to hydrate a
32085
32443
  * resumed `WiredContextState`.
32086
32444
  *
32087
- * Phase 23 — the `sessionInitialized` baseline is the truth source for
32088
- * startup config (system_prompt / model / active_tools / permission_mode /
32089
- * plan_mode / workspace_dir). Subsequent `*_changed` records in `records`
32090
- * overlay the baseline.
32445
+ * `sessionInitialized` is the truth source for wire-owned startup context
32446
+ * (system_prompt / active_tools). Runtime/session config (model,
32447
+ * permission_mode, plan_mode, workspace_dir, thinking_level) comes from
32448
+ * `runtimeBaseline`, which is built from state.json by SessionManager.
32091
32449
  *
32092
32450
  * @param records — ordered records from `replayWire().records`
32093
32451
  * @param sessionInitialized — the line-2 baseline from `replayWire().sessionInitialized`
32094
32452
  * @returns Projected state suitable for `WiredContextState` constructor.
32095
32453
  */
32096
- function projectReplayState(records, sessionInitialized) {
32454
+ function projectReplayState(records, sessionInitialized, runtimeBaseline) {
32097
32455
  let messages = [];
32098
- let model = sessionInitialized.model;
32456
+ let model = runtimeBaseline?.model ?? sessionInitialized.model ?? "";
32099
32457
  let systemPrompt = sessionInitialized.system_prompt;
32100
32458
  const activeTools = new Set(sessionInitialized.active_tools);
32101
32459
  let lastSeq = 0;
32102
- let permissionMode = sessionInitialized.permission_mode;
32460
+ let permissionMode = runtimeBaseline?.permissionMode ?? sessionInitialized.permission_mode ?? "default";
32103
32461
  let tokenCount = 0;
32104
- let planMode = sessionInitialized.plan_mode;
32105
- let thinkingLevel = sessionInitialized.thinking_level;
32106
- const workspaceDir = sessionInitialized.workspace_dir;
32462
+ let planMode = runtimeBaseline?.planMode ?? sessionInitialized.plan_mode ?? false;
32463
+ let thinkingLevel = runtimeBaseline?.thinkingLevel ?? sessionInitialized.thinking_level;
32464
+ const workspaceDir = runtimeBaseline?.workspaceDir ?? sessionInitialized.workspace_dir ?? process.cwd();
32107
32465
  const sessionMetaPatch = { turn_count: 0 };
32108
32466
  let todos = [];
32109
32467
  const pendingTodoWrites = /* @__PURE__ */ new Map();
@@ -32184,21 +32542,9 @@ function projectReplayState(records, sessionInitialized) {
32184
32542
  }];
32185
32543
  tokenCount = r.post_compact_tokens;
32186
32544
  break;
32187
- case "model_changed":
32188
- model = r.new_model;
32189
- sessionMetaPatch.last_model = r.new_model;
32190
- break;
32191
32545
  case "turn_begin":
32192
32546
  sessionMetaPatch.turn_count += 1;
32193
32547
  break;
32194
- case "session_meta_changed":
32195
- if (r.patch.title !== void 0) sessionMetaPatch.title = r.patch.title;
32196
- if (r.patch.tags !== void 0) sessionMetaPatch.tags = [...r.patch.tags];
32197
- if (r.patch.description !== void 0) sessionMetaPatch.description = r.patch.description;
32198
- if (r.patch.archived !== void 0) sessionMetaPatch.archived = r.patch.archived;
32199
- if (r.patch.color !== void 0) sessionMetaPatch.color = r.patch.color;
32200
- if (r.patch.plan_slug !== void 0) sessionMetaPatch.plan_slug = r.patch.plan_slug;
32201
- break;
32202
32548
  case "system_prompt_changed":
32203
32549
  systemPrompt = r.new_prompt;
32204
32550
  break;
@@ -32209,15 +32555,6 @@ function projectReplayState(records, sessionInitialized) {
32209
32555
  } else if (r.operation === "register") for (const t of r.tools) activeTools.add(t);
32210
32556
  else if (r.operation === "remove") for (const t of r.tools) activeTools.delete(t);
32211
32557
  break;
32212
- case "permission_mode_changed":
32213
- if (VALID_PERMISSION_MODES.has(r.data.to)) permissionMode = r.data.to;
32214
- break;
32215
- case "plan_mode_changed":
32216
- planMode = r.enabled;
32217
- break;
32218
- case "thinking_changed":
32219
- thinkingLevel = r.level;
32220
- break;
32221
32558
  case "context_cleared":
32222
32559
  flushOpenStep();
32223
32560
  messages = [];
@@ -32362,38 +32699,6 @@ var SessionTodoStore = class {
32362
32699
  }
32363
32700
  };
32364
32701
  //#endregion
32365
- //#region ../../packages/kimi-core/src/session/state-cache.ts
32366
- /**
32367
- * StateCache — state.json derived cache (§9续).
32368
- *
32369
- * Reads / writes `state.json` in a session directory. Used by
32370
- * SessionManager to persist quick-access session metadata (model,
32371
- * status, last turn timestamp) without replaying the full wire.jsonl.
32372
- *
32373
- * Writes go through `atomicWrite` (write-tmp-fsync-rename, Decision #104)
32374
- * so a crash mid-write never leaves a half-truncated state.json that
32375
- * subsequent reads would treat as "missing". Read-modify-write callers
32376
- * still need their own concurrency guard for "merge then write" races
32377
- * across processes — see SessionManager.renameSession.
32378
- */
32379
- var StateCache = class {
32380
- constructor(statePath) {
32381
- this.statePath = statePath;
32382
- }
32383
- async read() {
32384
- try {
32385
- const raw = await readFile(this.statePath, "utf-8");
32386
- if (raw.length === 0) return null;
32387
- return JSON.parse(raw);
32388
- } catch {
32389
- return null;
32390
- }
32391
- }
32392
- async write(state) {
32393
- await atomicWrite(this.statePath, JSON.stringify(state, null, 2));
32394
- }
32395
- };
32396
- //#endregion
32397
32702
  //#region ../../packages/kimi-core/src/session/usage-aggregator.ts
32398
32703
  /**
32399
32704
  * Token usage aggregator — replays a wire.jsonl and sums usage from
@@ -32553,6 +32858,17 @@ function normalizeRuntimeSlot(slot, fallback) {
32553
32858
  });
32554
32859
  return runtimeSlot;
32555
32860
  }
32861
+ function normalizePermissionMode(value) {
32862
+ if (value === "acceptEdits" || value === "bypassPermissions") return value;
32863
+ return "default";
32864
+ }
32865
+ function createFallbackState(sessionId, now) {
32866
+ return {
32867
+ session_id: sessionId,
32868
+ created_at: now,
32869
+ updated_at: now
32870
+ };
32871
+ }
32556
32872
  var SessionManager = class {
32557
32873
  paths;
32558
32874
  producer;
@@ -32590,6 +32906,29 @@ var SessionManager = class {
32590
32906
  this.stateWriteMutex.set(sessionId, next.then(() => {}, () => {}));
32591
32907
  return next;
32592
32908
  }
32909
+ attachStateTracker(sessionId, stateCache, turnManager) {
32910
+ return turnManager.addTurnLifecycleListener((event) => {
32911
+ const now = Date.now();
32912
+ stateCache.update((current) => {
32913
+ const base = current ?? createFallbackState(sessionId, now);
32914
+ if (event.kind === "begin") return {
32915
+ ...base,
32916
+ last_turn_id: event.turnId,
32917
+ last_turn_time: now,
32918
+ updated_at: now,
32919
+ last_exit_code: "dirty"
32920
+ };
32921
+ return {
32922
+ ...base,
32923
+ turn_count: (base.turn_count ?? 0) + 1,
32924
+ last_turn_id: event.turnId,
32925
+ last_turn_time: now,
32926
+ updated_at: now,
32927
+ last_exit_code: "dirty"
32928
+ };
32929
+ }).catch(() => {});
32930
+ });
32931
+ }
32593
32932
  async createSession(options) {
32594
32933
  const sessionId = options.sessionId ?? `ses_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
32595
32934
  if (this.sessions.has(sessionId)) throw new Error(`Session already exists: ${sessionId}`);
@@ -32623,11 +32962,7 @@ var SessionManager = class {
32623
32962
  agent_type: "main",
32624
32963
  session_id: sessionId,
32625
32964
  system_prompt: options.systemPrompt ?? "",
32626
- model: options.model,
32627
- active_tools: sessionTools.map((t) => t.name),
32628
- permission_mode: options.permissionMode ?? "default",
32629
- plan_mode: false,
32630
- workspace_dir: options.workspaceDir
32965
+ active_tools: sessionTools.map((t) => t.name)
32631
32966
  };
32632
32967
  await journalWriter.append(mainInitInput);
32633
32968
  const sessionJournal = new WiredSessionJournalImpl(journalWriter);
@@ -32643,10 +32978,12 @@ var SessionManager = class {
32643
32978
  await stateCache.write({
32644
32979
  session_id: sessionId,
32645
32980
  model: options.model,
32646
- status: "active",
32647
32981
  created_at: now,
32648
32982
  updated_at: now,
32649
32983
  workspace_dir: options.workspaceDir,
32984
+ permission_mode: options.permissionMode ?? "default",
32985
+ plan_mode: false,
32986
+ turn_count: 0,
32650
32987
  last_exit_code: "dirty",
32651
32988
  producer: this.producer,
32652
32989
  todos: []
@@ -32703,6 +33040,7 @@ var SessionManager = class {
32703
33040
  ...options.logger !== void 0 ? { logger: options.logger } : {}
32704
33041
  });
32705
33042
  turnManagerRef = soulPlus.getTurnManager();
33043
+ const disposeStateTracking = this.attachStateTracker(sessionId, stateCache, turnManagerRef);
32706
33044
  await soulPlus.init();
32707
33045
  if (options.permissionMode !== void 0) turnManagerRef.setPermissionMode(options.permissionMode);
32708
33046
  const managed = {
@@ -32712,7 +33050,20 @@ var SessionManager = class {
32712
33050
  turnManager: turnManagerRef,
32713
33051
  contextState,
32714
33052
  sessionJournal,
32715
- setPlanModeOverride: (enabled) => soulPlus.setPlanMode(enabled)
33053
+ setPlanModeOverride: (enabled) => soulPlus.setPlanMode(enabled),
33054
+ setYoloOverride: async (enabled) => {
33055
+ const permissionMode = enabled ? "bypassPermissions" : "default";
33056
+ await stateCache.update((current) => {
33057
+ const now = Date.now();
33058
+ return {
33059
+ ...current ?? createFallbackState(sessionId, now),
33060
+ permission_mode: permissionMode,
33061
+ updated_at: now,
33062
+ last_exit_code: "dirty"
33063
+ };
33064
+ });
33065
+ turnManagerRef.setPermissionMode(permissionMode);
33066
+ }
32716
33067
  }),
32717
33068
  contextState,
32718
33069
  eventBus,
@@ -32721,6 +33072,7 @@ var SessionManager = class {
32721
33072
  journalWriter,
32722
33073
  runtimeSlot,
32723
33074
  lifecycleStateMachine,
33075
+ disposeStateTracking,
32724
33076
  todoStore
32725
33077
  };
32726
33078
  this.sessions.set(sessionId, managed);
@@ -32743,7 +33095,18 @@ var SessionManager = class {
32743
33095
  throw new Error(`Failed to replay session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
32744
33096
  }
32745
33097
  if (replayResult.sessionInitialized.agent_type !== "main") throw new MalformedWireError("agent-type-mismatch", `resumeSession expected agent_type='main' on wire line 2 for "${sessionId}", got agent_type='${replayResult.sessionInitialized.agent_type}'`);
32746
- const projected = projectReplayState(replayResult.records, replayResult.sessionInitialized);
33098
+ const stateCache = new StateCache(this.paths.statePath(sessionId));
33099
+ const stateData = await stateCache.read();
33100
+ if (stateData === null) throw new Error(`resumeSession: state.json missing or unreadable for session "${sessionId}"`);
33101
+ if (stateData.model === void 0 || stateData.model.length === 0) throw new Error(`resumeSession: state.json missing model for session "${sessionId}"`);
33102
+ const statePermissionMode = normalizePermissionMode(stateData.permission_mode);
33103
+ const projected = projectReplayState(replayResult.records, replayResult.sessionInitialized, {
33104
+ model: stateData.model,
33105
+ permissionMode: statePermissionMode,
33106
+ planMode: stateData.plan_mode ?? false,
33107
+ workspaceDir: stateData.workspace_dir ?? process.cwd(),
33108
+ ...stateData.thinking_level !== void 0 ? { thinkingLevel: stateData.thinking_level } : {}
33109
+ });
32747
33110
  const lifecycleStateMachine = new SessionLifecycleStateMachine();
32748
33111
  const lifecycleGate = new SoulLifecycleGate(lifecycleStateMachine);
32749
33112
  let contextStateRef;
@@ -32778,10 +33141,7 @@ var SessionManager = class {
32778
33141
  sessionJournal,
32779
33142
  currentTurnId: () => turnManagerRef?.getCurrentTurnId() ?? "recovery"
32780
33143
  });
32781
- const stateCache = new StateCache(this.paths.statePath(sessionId));
32782
- const stateData = await stateCache.read();
32783
- const isClean = stateData?.last_exit_code === "clean";
32784
- const initialTodos = isClean ? cloneTodos(stateData?.todos ?? projected.todos) : cloneTodos(projected.todos);
33144
+ const initialTodos = stateData.last_exit_code === "clean" ? cloneTodos(stateData.todos ?? projected.todos) : cloneTodos(projected.todos);
32785
33145
  const todoStore = new SessionTodoStore(stateCache, initialTodos);
32786
33146
  const sessionTools = bindTodoStore(options.tools, todoStore);
32787
33147
  const runtimeSlot = normalizeRuntimeSlot(options.runtimeSlot, {
@@ -32791,32 +33151,25 @@ var SessionManager = class {
32791
33151
  ...options.compactionConfig !== void 0 ? { compactionConfig: options.compactionConfig } : {}
32792
33152
  });
32793
33153
  const runtimeBundle = runtimeSlot.current();
32794
- if (stateData !== null) await stateCache.write({
33154
+ await stateCache.write({
32795
33155
  ...stateData,
32796
- status: "active",
32797
33156
  updated_at: Date.now(),
32798
33157
  todos: initialTodos,
32799
33158
  last_exit_code: "dirty"
32800
33159
  });
32801
- const replayedMeta = projected.sessionMetaPatch;
32802
33160
  const now = Date.now();
32803
- const pickTitle = isClean ? stateData?.custom_title ?? replayedMeta.title : replayedMeta.title ?? stateData?.custom_title;
32804
- const pickTags = isClean ? stateData?.tags ?? replayedMeta.tags : replayedMeta.tags ?? stateData?.tags;
32805
- const pickDescription = isClean ? stateData?.description ?? replayedMeta.description : replayedMeta.description ?? stateData?.description;
32806
- const pickArchived = isClean ? stateData?.archived ?? replayedMeta.archived : replayedMeta.archived ?? stateData?.archived;
32807
- const pickLastModel = isClean ? stateData?.model ?? replayedMeta.last_model : replayedMeta.last_model ?? stateData?.model;
32808
- const pickPlanSlug = isClean ? stateData?.plan_slug ?? replayedMeta.plan_slug : replayedMeta.plan_slug ?? stateData?.plan_slug;
32809
33161
  const initialMeta = {
32810
33162
  session_id: sessionId,
32811
- created_at: stateData?.created_at ?? now,
32812
- turn_count: replayedMeta.turn_count,
32813
- last_updated: stateData?.updated_at ?? now,
32814
- ...pickTitle !== void 0 ? { title: pickTitle } : {},
32815
- ...pickTags !== void 0 ? { tags: [...pickTags] } : {},
32816
- ...pickDescription !== void 0 ? { description: pickDescription } : {},
32817
- ...pickArchived !== void 0 ? { archived: pickArchived } : {},
32818
- ...pickLastModel !== void 0 ? { last_model: pickLastModel } : {},
32819
- ...pickPlanSlug !== void 0 ? { plan_slug: pickPlanSlug } : {},
33163
+ created_at: stateData.created_at ?? now,
33164
+ turn_count: stateData.turn_count ?? 0,
33165
+ last_updated: stateData.updated_at ?? now,
33166
+ ...stateData.custom_title !== void 0 ? { title: stateData.custom_title } : {},
33167
+ ...stateData.tags !== void 0 ? { tags: [...stateData.tags] } : {},
33168
+ ...stateData.description !== void 0 ? { description: stateData.description } : {},
33169
+ ...stateData.archived !== void 0 ? { archived: stateData.archived } : {},
33170
+ ...stateData.color !== void 0 ? { color: stateData.color } : {},
33171
+ last_model: stateData.model,
33172
+ ...stateData.plan_slug !== void 0 ? { plan_slug: stateData.plan_slug } : {},
32820
33173
  producer: replayResult.producer,
32821
33174
  last_exit_code: "dirty"
32822
33175
  };
@@ -32865,11 +33218,12 @@ var SessionManager = class {
32865
33218
  subagentStore,
32866
33219
  agentTypeRegistry: options.agentTypeRegistry,
32867
33220
  sessionDir,
32868
- workDir: stateData?.workspace_dir ?? process.cwd()
33221
+ workDir: stateData.workspace_dir ?? process.cwd()
32869
33222
  } : {},
32870
33223
  ...options.logger !== void 0 ? { logger: options.logger } : {}
32871
33224
  });
32872
33225
  turnManagerRef = soulPlus.getTurnManager();
33226
+ const disposeStateTracking = this.attachStateTracker(sessionId, stateCache, turnManagerRef);
32873
33227
  await soulPlus.init();
32874
33228
  turnManagerRef.setPermissionMode(effectivePermissionMode);
32875
33229
  if (projected.thinkingLevel !== void 0) turnManagerRef.setThinkingLevel(projected.thinkingLevel);
@@ -32896,7 +33250,20 @@ var SessionManager = class {
32896
33250
  turnManager: turnManagerRef,
32897
33251
  contextState,
32898
33252
  sessionJournal,
32899
- setPlanModeOverride: (enabled) => soulPlus.setPlanMode(enabled)
33253
+ setPlanModeOverride: (enabled) => soulPlus.setPlanMode(enabled),
33254
+ setYoloOverride: async (enabled) => {
33255
+ const permissionMode = enabled ? "bypassPermissions" : "default";
33256
+ await stateCache.update((current) => {
33257
+ const now = Date.now();
33258
+ return {
33259
+ ...current ?? createFallbackState(sessionId, now),
33260
+ permission_mode: permissionMode,
33261
+ updated_at: now,
33262
+ last_exit_code: "dirty"
33263
+ };
33264
+ });
33265
+ turnManagerRef.setPermissionMode(permissionMode);
33266
+ }
32900
33267
  }),
32901
33268
  contextState,
32902
33269
  eventBus,
@@ -32905,6 +33272,7 @@ var SessionManager = class {
32905
33272
  journalWriter,
32906
33273
  runtimeSlot,
32907
33274
  lifecycleStateMachine,
33275
+ disposeStateTracking,
32908
33276
  todoStore
32909
33277
  };
32910
33278
  this.sessions.set(sessionId, managed);
@@ -32926,7 +33294,6 @@ var SessionManager = class {
32926
33294
  session_id: state.session_id,
32927
33295
  created_at: state.created_at,
32928
33296
  ...state.model !== void 0 ? { model: state.model } : {},
32929
- ...state.status !== void 0 ? { status: state.status } : {},
32930
33297
  ...state.workspace_dir !== void 0 ? { workspace_dir: state.workspace_dir } : {},
32931
33298
  ...state.custom_title !== void 0 ? { title: state.custom_title } : {},
32932
33299
  ...lastActivity !== void 0 ? { last_activity: lastActivity } : {},
@@ -32970,7 +33337,8 @@ var SessionManager = class {
32970
33337
  await cache.write({
32971
33338
  ...existing,
32972
33339
  custom_title: trimmed,
32973
- updated_at: Date.now()
33340
+ updated_at: Date.now(),
33341
+ last_exit_code: "dirty"
32974
33342
  });
32975
33343
  this.usageAggregator.invalidate(this.paths.wirePath(sessionId));
32976
33344
  });
@@ -32995,7 +33363,8 @@ var SessionManager = class {
32995
33363
  await cache.write({
32996
33364
  ...existing,
32997
33365
  tags: next,
32998
- updated_at: Date.now()
33366
+ updated_at: Date.now(),
33367
+ last_exit_code: "dirty"
32999
33368
  });
33000
33369
  this.usageAggregator.invalidate(this.paths.wirePath(sessionId));
33001
33370
  });
@@ -33079,7 +33448,29 @@ var SessionManager = class {
33079
33448
  keepUpToLine = turnBeginIndices[totalTurns - nTurnsBack];
33080
33449
  newTurnCount = totalTurns - nTurnsBack;
33081
33450
  }
33082
- await atomicWrite(wirePath, `${lines.slice(0, keepUpToLine).join("\n")}\n`);
33451
+ const kept = lines.slice(0, keepUpToLine);
33452
+ await atomicWrite(wirePath, `${kept.join("\n")}\n`);
33453
+ let lastTurnId;
33454
+ let lastTurnTime;
33455
+ for (let i = kept.length - 1; i >= 0; i--) try {
33456
+ const rec = JSON.parse(kept[i] ?? "");
33457
+ if (rec.type === "turn_begin" && rec.turn_id !== void 0) {
33458
+ lastTurnId = rec.turn_id;
33459
+ if (typeof rec.time === "number") lastTurnTime = rec.time;
33460
+ break;
33461
+ }
33462
+ } catch {}
33463
+ await new StateCache(this.paths.statePath(sessionId)).update((current) => {
33464
+ const now = Date.now();
33465
+ return {
33466
+ ...current ?? createFallbackState(sessionId, now),
33467
+ turn_count: newTurnCount,
33468
+ last_turn_id: lastTurnId,
33469
+ last_turn_time: lastTurnTime,
33470
+ updated_at: now,
33471
+ last_exit_code: "dirty"
33472
+ };
33473
+ });
33083
33474
  this.usageAggregator.invalidate(wirePath);
33084
33475
  return { new_turn_count: newTurnCount };
33085
33476
  }
@@ -33091,18 +33482,8 @@ var SessionManager = class {
33091
33482
  async getSessionStatus(sessionId) {
33092
33483
  const live = this.sessions.get(sessionId);
33093
33484
  if (live !== void 0) return live.lifecycleStateMachine.state;
33094
- const state = await new StateCache(this.paths.statePath(sessionId)).read();
33095
- if (state === null) throw new Error(`getSessionStatus: session "${sessionId}" not found`);
33096
- const validStatuses = new Set([
33097
- "idle",
33098
- "active",
33099
- "completing",
33100
- "compacting",
33101
- "destroying",
33102
- "closed"
33103
- ]);
33104
- if (state.status !== void 0 && validStatuses.has(state.status)) return state.status;
33105
- return "idle";
33485
+ if (await new StateCache(this.paths.statePath(sessionId)).read() === null) throw new Error(`getSessionStatus: session "${sessionId}" not found`);
33486
+ return "closed";
33106
33487
  }
33107
33488
  /**
33108
33489
  * Return aggregated token usage for a session.
@@ -33141,33 +33522,43 @@ var SessionManager = class {
33141
33522
  await managed.todoStore?.flushPending();
33142
33523
  const existing = await managed.stateCache.read();
33143
33524
  const now = Date.now();
33144
- const currentPlanMode = managed.soulPlus.getTurnManager().getPlanMode();
33525
+ const turnManager = managed.soulPlus.getTurnManager();
33526
+ const currentPlanMode = turnManager.getPlanMode();
33527
+ const currentPermissionMode = turnManager.getPermissionMode();
33528
+ const currentThinkingLevel = turnManager.getThinkingLevel();
33145
33529
  const currentTodos = managed.todoStore?.getTodos() ?? [];
33146
33530
  const meta = sessionMetaService?.get();
33147
33531
  const stateToFlush = existing ? {
33148
33532
  ...existing,
33149
- status: "closed",
33150
33533
  updated_at: now,
33534
+ model: managed.contextState.model,
33535
+ permission_mode: currentPermissionMode,
33151
33536
  ...meta?.title !== void 0 ? { custom_title: meta.title } : {},
33152
33537
  ...meta?.tags !== void 0 ? { tags: [...meta.tags] } : {},
33153
33538
  ...meta?.description !== void 0 ? { description: meta.description } : {},
33154
33539
  ...meta?.archived !== void 0 ? { archived: meta.archived } : {},
33155
- ...meta?.last_model !== void 0 ? { model: meta.last_model } : {},
33540
+ ...meta?.color !== void 0 ? { color: meta.color } : {},
33541
+ turn_count: meta?.turn_count ?? existing.turn_count ?? 0,
33156
33542
  ...meta?.plan_slug !== void 0 ? { plan_slug: meta.plan_slug } : {},
33543
+ ...currentThinkingLevel !== void 0 ? { thinking_level: currentThinkingLevel } : {},
33157
33544
  ...currentPlanMode ? { plan_mode: true } : { plan_mode: false },
33158
33545
  todos: cloneTodos(currentTodos),
33159
33546
  last_exit_code: "clean"
33160
33547
  } : {
33161
33548
  session_id: sessionId,
33162
- status: "closed",
33163
33549
  created_at: now,
33164
33550
  updated_at: now,
33551
+ model: managed.contextState.model,
33552
+ permission_mode: currentPermissionMode,
33165
33553
  plan_mode: currentPlanMode,
33554
+ turn_count: meta?.turn_count ?? 0,
33555
+ ...currentThinkingLevel !== void 0 ? { thinking_level: currentThinkingLevel } : {},
33166
33556
  todos: cloneTodos(currentTodos),
33167
33557
  last_exit_code: "clean"
33168
33558
  };
33169
33559
  await managed.stateCache.write(stateToFlush);
33170
33560
  sessionMetaService?.dispose();
33561
+ managed.disposeStateTracking?.();
33171
33562
  await managed.journalWriter.close();
33172
33563
  this.sessions.delete(sessionId);
33173
33564
  });
@@ -94290,7 +94681,7 @@ function shellQuote(path) {
94290
94681
  }
94291
94682
  //#endregion
94292
94683
  //#region src/tui/components/messages/assistant-message.ts
94293
- const BULLET$1 = " ";
94684
+ const BULLET$1 = " ";
94294
94685
  const INDENT$2 = " ";
94295
94686
  var AssistantMessageComponent = class {
94296
94687
  contentContainer;
@@ -94363,7 +94754,7 @@ var ImageThumbnail = class extends Container {
94363
94754
  };
94364
94755
  //#endregion
94365
94756
  //#region src/tui/components/messages/thinking.ts
94366
- const BULLET = " ";
94757
+ const BULLET = " ";
94367
94758
  const INDENT$1 = " ";
94368
94759
  const PREVIEW_LINES$1 = 3;
94369
94760
  var ThinkingComponent = class {
@@ -94563,6 +94954,19 @@ function extractApprovedPlan(output) {
94563
94954
  if (markerIndex < 0) return "";
94564
94955
  return output.slice(markerIndex + 17).trim();
94565
94956
  }
94957
+ function compactPreview(value) {
94958
+ const oneLine = value.replaceAll(/\s+/g, " ").trim();
94959
+ if (oneLine.length <= 160) return oneLine;
94960
+ return oneLine.slice(0, 157) + "...";
94961
+ }
94962
+ function parseArgsPreview(value) {
94963
+ if (value.trim().length === 0) return {};
94964
+ try {
94965
+ const parsed = JSON.parse(value);
94966
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
94967
+ } catch {}
94968
+ return { arguments: value };
94969
+ }
94566
94970
  function extractKeyArgument(toolName, args) {
94567
94971
  const candidates = {
94568
94972
  Shell: ["command"],
@@ -94650,6 +95054,12 @@ var ToolCallComponent = class extends Container {
94650
95054
  this.headerText.setText(this.buildHeader());
94651
95055
  this.rebuildContent();
94652
95056
  }
95057
+ updateToolCall(toolCall) {
95058
+ this.toolCall = toolCall;
95059
+ this.headerText.setText(this.buildHeader());
95060
+ this.rebuildBody();
95061
+ this.ui?.requestRender();
95062
+ }
94653
95063
  applySubagentReplay(subagent) {
94654
95064
  if (subagent === void 0) return;
94655
95065
  this.subagentAgentId = subagent.id;
@@ -94688,9 +95098,23 @@ var ToolCallComponent = class extends Container {
94688
95098
  this.ui?.requestRender();
94689
95099
  }
94690
95100
  appendSubToolCall(call) {
95101
+ const existing = this.ongoingSubCalls.get(call.id);
94691
95102
  this.ongoingSubCalls.set(call.id, {
94692
95103
  name: call.name,
94693
- args: call.args
95104
+ args: call.args,
95105
+ ...existing?.streamingArguments !== void 0 ? { streamingArguments: existing.streamingArguments } : {}
95106
+ });
95107
+ this.rebuildContent();
95108
+ this.ui?.requestRender();
95109
+ }
95110
+ appendSubToolCallDelta(delta) {
95111
+ const existing = this.ongoingSubCalls.get(delta.id);
95112
+ const nextArgsText = `${existing?.streamingArguments ?? ""}${delta.argumentsPart ?? ""}`;
95113
+ const parsed = parseArgsPreview(nextArgsText);
95114
+ this.ongoingSubCalls.set(delta.id, {
95115
+ name: delta.name ?? existing?.name ?? "Tool",
95116
+ args: parsed,
95117
+ streamingArguments: nextArgsText
94694
95118
  });
94695
95119
  this.rebuildContent();
94696
95120
  this.ui?.requestRender();
@@ -94717,8 +95141,8 @@ var ToolCallComponent = class extends Container {
94717
95141
  const isFinished = result !== void 0;
94718
95142
  const isError = result?.is_error ?? false;
94719
95143
  let bullet;
94720
- if (isFinished) bullet = isError ? chalk.hex(colors.error)("✗ ") : chalk.hex(colors.success)(" ");
94721
- else bullet = this.blinkOn ? chalk.white(" ") : " ";
95144
+ if (isFinished) bullet = isError ? chalk.hex(colors.error)("✗ ") : chalk.hex(colors.success)(" ");
95145
+ else bullet = this.blinkOn ? chalk.white(" ") : " ";
94722
95146
  if (toolCall.name === "ExitPlanMode") return chalk.hex(colors.primary).bold("Current plan");
94723
95147
  if (toolCall.name === "AskUserQuestion") {
94724
95148
  const label = isFinished ? isError ? "Could not collect your input" : "Collected your answers" : "Waiting for your input";
@@ -94736,6 +95160,13 @@ var ToolCallComponent = class extends Container {
94736
95160
  this.buildContent();
94737
95161
  this.buildSubagentBlock();
94738
95162
  }
95163
+ rebuildBody() {
95164
+ while (this.children.length > 2) this.children.pop();
95165
+ this.buildCallPreview();
95166
+ this.callPreviewEndIndex = this.children.length;
95167
+ this.buildContent();
95168
+ this.buildSubagentBlock();
95169
+ }
94739
95170
  buildSubagentBlock() {
94740
95171
  if (this.subagentAgentId === void 0 && this.ongoingSubCalls.size === 0 && this.finishedSubCalls.length === 0 && this.subagentText.length === 0) return;
94741
95172
  const dim = chalk.dim;
@@ -94757,6 +95188,7 @@ var ToolCallComponent = class extends Container {
94757
95188
  const nameCol = chalk.hex(this.colors.primary)(call.name);
94758
95189
  const argCol = keyArg ? dim(` (${keyArg})`) : "";
94759
95190
  this.addChild(new Text(` ${dim("…")} Using ${nameCol}${argCol}`, 0, 0));
95191
+ if (call.streamingArguments !== void 0 && call.streamingArguments.length > 0) this.addChild(new Text(` ${dim(compactPreview(call.streamingArguments))}`, 0, 0));
94760
95192
  }
94761
95193
  if (this.subagentText.length > 0) {
94762
95194
  const tailLines = this.subagentText.split("\n").slice(-3);
@@ -94773,15 +95205,19 @@ var ToolCallComponent = class extends Container {
94773
95205
  this.buildPlanPreview();
94774
95206
  return;
94775
95207
  }
95208
+ if (this.toolCall.streamingArguments !== void 0) {
95209
+ this.addChild(new Text(chalk.dim(compactPreview(this.toolCall.streamingArguments)), 2, 0));
95210
+ return;
95211
+ }
94776
95212
  if (name === "Write" || name === "WriteFile") {
94777
95213
  const content = str(this.toolCall.args["content"]);
94778
95214
  if (content.length === 0) return;
94779
95215
  const allLines = highlightLines(content, langFromPath(str(this.toolCall.args["file_path"] ?? this.toolCall.args["path"])));
94780
95216
  const shown = allLines.slice(0, CALL_PREVIEW_LINES);
94781
95217
  const remaining = allLines.length - shown.length;
94782
- for (let i = 0; i < shown.length; i++) {
95218
+ for (const [i, line] of shown.entries()) {
94783
95219
  const lineNum = chalk.dim(String(i + 1).padStart(4) + " ");
94784
- this.addChild(new Text(lineNum + shown[i], 2, 0));
95220
+ this.addChild(new Text(lineNum + line, 2, 0));
94785
95221
  }
94786
95222
  if (remaining > 0) this.addChild(new Text(chalk.dim(`... (${String(remaining)} more lines, ${String(allLines.length)} total)`), 2, 0));
94787
95223
  } else if (name === "Edit" || name === "EditFile") {
@@ -94815,15 +95251,14 @@ var ToolCallComponent = class extends Container {
94815
95251
  return;
94816
95252
  }
94817
95253
  if (this.toolCall.name === "SetTodoList" && !result.is_error) return;
94818
- if (this.toolCall.name === "AskUserQuestion" && !result.is_error) {
94819
- if (this.renderAskUserQuestionResult(result.output)) return;
94820
- }
95254
+ if (this.toolCall.name === "AskUserQuestion" && !result.is_error && this.renderAskUserQuestionResult(result.output)) return;
95255
+ const tint = result.is_error ? chalk.hex(this.colors.error) : chalk.dim;
94821
95256
  const lines = result.output.split("\n");
94822
- if (this.expanded) this.addChild(new Text(chalk.dim(result.output), 2, 0));
95257
+ if (this.expanded) this.addChild(new Text(tint(result.output), 2, 0));
94823
95258
  else {
94824
95259
  const shown = lines.slice(0, PREVIEW_LINES);
94825
95260
  const remaining = lines.length - shown.length;
94826
- this.addChild(new Text(chalk.dim(shown.join("\n")), 2, 0));
95261
+ this.addChild(new Text(tint(shown.join("\n")), 2, 0));
94827
95262
  if (remaining > 0) this.addChild(new Text(chalk.dim(`... (${String(remaining)} more lines, ctrl+o to expand)`), 2, 0));
94828
95263
  }
94829
95264
  }
@@ -94889,12 +95324,14 @@ var WelcomeComponent = class {
94889
95324
  const gap = " ";
94890
95325
  const textWidth = Math.max(4, innerWidth - logoWidth - 2);
94891
95326
  const rightRow0 = truncateToWidth(chalk.bold.hex(this.colors.primary)("Welcome to Kimi Code!"), textWidth, "…");
94892
- const rightRow1 = truncateToWidth(chalk.dim("Send /help for help information."), textWidth, "…");
95327
+ const isLoggedOut = !this.state.model;
95328
+ const rightRow1 = truncateToWidth(chalk.dim(isLoggedOut ? "Run /login to sign in." : "Send /help for help information."), textWidth, "…");
94893
95329
  const headerLines = [primary(logo[0].padEnd(logoWidth)) + gap + rightRow0, primary(logo[1].padEnd(logoWidth)) + gap + rightRow1];
95330
+ const modelValue = isLoggedOut ? chalk.yellow("(not signed in — run /login)") : this.state.model;
94894
95331
  const infoLines = [
94895
95332
  chalk.dim.bold("Directory: ") + this.state.workDir,
94896
95333
  chalk.dim.bold("Session: ") + this.state.sessionId,
94897
- chalk.dim.bold("Model: ") + this.state.model,
95334
+ chalk.dim.bold("Model: ") + modelValue,
94898
95335
  chalk.dim.bold("Version: ") + this.state.version
94899
95336
  ];
94900
95337
  const contentLines = [
@@ -95060,8 +95497,8 @@ function createTranscriptComponent(state, entry) {
95060
95497
  if (state.toolOutputExpanded) tc.setExpanded(true);
95061
95498
  return tc;
95062
95499
  }
95063
- return createStatusEntry(entry.content, entry.color);
95064
- case "status": return createStatusEntry(entry.content, entry.color);
95500
+ return entry.renderMode === "notice" ? createNoticeEntry(entry.content, entry.detail, state.colors) : createStatusEntry(entry.content, entry.color);
95501
+ case "status": return entry.renderMode === "notice" ? createNoticeEntry(entry.content, entry.detail, state.colors) : createStatusEntry(entry.content, entry.color);
95065
95502
  default: return null;
95066
95503
  }
95067
95504
  }
@@ -95082,6 +95519,13 @@ function createStatusEntry(content, color) {
95082
95519
  container.addChild(new Text(` ${styled}`, 0, 0));
95083
95520
  return container;
95084
95521
  }
95522
+ function createNoticeEntry(title, detail, colors) {
95523
+ const container = new Container();
95524
+ container.addChild(new Spacer(1));
95525
+ container.addChild(new Text(` ${chalk.white(title)}`, 0, 0));
95526
+ if (detail !== void 0 && detail.length > 0) container.addChild(new Text(` ${chalk.hex(colors.textDim)(detail)}`, 0, 0));
95527
+ return container;
95528
+ }
95085
95529
  /**
95086
95530
  * Push a new entry onto the transcript and render the matching
95087
95531
  * component. Equivalent to the old `KimiTUI.addTranscriptEntry`.
@@ -95113,6 +95557,16 @@ function emitStatus(state, message, color) {
95113
95557
  ...color !== void 0 ? { color } : {}
95114
95558
  });
95115
95559
  }
95560
+ function emitNotice(state, title, detail) {
95561
+ appendTranscriptEntry(state, {
95562
+ id: nextTranscriptId(),
95563
+ kind: "status",
95564
+ turnId: state.currentTurnId,
95565
+ renderMode: "notice",
95566
+ content: title,
95567
+ ...detail !== void 0 ? { detail } : {}
95568
+ });
95569
+ }
95116
95570
  /** Red. */
95117
95571
  function emitError(state, message) {
95118
95572
  emitStatus(state, message, state.colors.error);
@@ -95125,10 +95579,6 @@ function emitSuccess(state, message) {
95125
95579
  function emitMuted(state, message) {
95126
95580
  emitStatus(state, message, state.colors.textDim);
95127
95581
  }
95128
- /** Primary (brand) color. */
95129
- function emitPrimary(state, message) {
95130
- emitStatus(state, message, state.colors.primary);
95131
- }
95132
95582
  //#endregion
95133
95583
  //#region src/tui/input/image-placeholder.ts
95134
95584
  const PLACEHOLDER_REGEX = /\[image #(\d+) \((\d+)×(\d+)\)\]/g;
@@ -95200,6 +95650,15 @@ function handleSubagentSourceEvent(ectx, payload) {
95200
95650
  });
95201
95651
  return;
95202
95652
  }
95653
+ if (payload.method === "tool.call.delta") {
95654
+ const d = payload.data;
95655
+ if (typeof d.tool_call_id === "string") tc.appendSubToolCallDelta({
95656
+ id: `${payload.source_id}:${d.tool_call_id}`,
95657
+ ...typeof d.name === "string" ? { name: d.name } : {},
95658
+ argumentsPart: typeof d.arguments_part === "string" ? d.arguments_part : null
95659
+ });
95660
+ return;
95661
+ }
95203
95662
  if (payload.method === "tool.result") {
95204
95663
  const d = payload.data;
95205
95664
  if (typeof d.tool_call_id === "string") tc.finishSubToolCall({
@@ -95494,6 +95953,10 @@ function ok$1(message) {
95494
95953
  };
95495
95954
  return { type: "ok" };
95496
95955
  }
95956
+ function showNotice(ctx, title, detail) {
95957
+ ctx.showNotice(title, detail);
95958
+ return ok$1();
95959
+ }
95497
95960
  const shellCommands = [
95498
95961
  {
95499
95962
  name: "exit",
@@ -95575,7 +96038,8 @@ const shellCommands = [
95575
96038
  else enabled = !ctx.appState.yolo;
95576
96039
  await ctx.client.setYolo(ctx.appState.sessionId, enabled);
95577
96040
  ctx.setAppState({ yolo: enabled });
95578
- return ok$1(`YOLO mode: ${enabled ? "on" : "off"}`);
96041
+ if (enabled) return showNotice(ctx, "YOLO mode: ON", "All actions will be approved automatically. Use with caution.");
96042
+ return showNotice(ctx, "YOLO mode: OFF");
95579
96043
  }
95580
96044
  },
95581
96045
  {
@@ -95600,8 +96064,9 @@ const shellCommands = [
95600
96064
  else enabled = !ctx.appState.planMode;
95601
96065
  const res = await ctx.client.setPlanMode(ctx.appState.sessionId, enabled);
95602
96066
  ctx.setAppState({ planMode: enabled });
95603
- if (enabled && res.plan_path !== void 0) return ok$1(`Plan mode ON. Write your plan to: ${res.plan_path}`);
95604
- return ok$1(`Plan mode: ${enabled ? "on" : "off"}`);
96067
+ if (enabled && res.plan_path !== void 0) return showNotice(ctx, "Plan mode: ON", `Plan will be created here: ${res.plan_path}`);
96068
+ if (enabled) return showNotice(ctx, "Plan mode: ON");
96069
+ return showNotice(ctx, "Plan mode: OFF");
95605
96070
  }
95606
96071
  },
95607
96072
  {
@@ -95739,7 +96204,7 @@ var SlashCommandRegistry = class {
95739
96204
  * 再注册当前 session 下的可用 skills。
95740
96205
  */
95741
96206
  setSkillCommands(defs) {
95742
- for (const [name, def] of [...this.commands]) if (name.startsWith("skill:")) {
96207
+ for (const [name, def] of this.commands) if (name.startsWith("skill:")) {
95743
96208
  this.commands.delete(name);
95744
96209
  this.lookup.delete(name);
95745
96210
  for (const alias of def.aliases) this.lookup.delete(alias);
@@ -96540,6 +97005,7 @@ function createTUIState(options) {
96540
97005
  assistantStreamActive: false,
96541
97006
  thinkingDraft: "",
96542
97007
  activeToolCalls: /* @__PURE__ */ new Map(),
97008
+ streamingToolCallArguments: /* @__PURE__ */ new Map(),
96543
97009
  toastTimers: /* @__PURE__ */ new Map(),
96544
97010
  pendingExit: null,
96545
97011
  queuedMessages: [],
@@ -96701,6 +97167,7 @@ function sendMessageInternal(state, addEntry, input, options) {
96701
97167
  state.assistantStreamActive = false;
96702
97168
  state.thinkingDraft = "";
96703
97169
  state.activeToolCalls.clear();
97170
+ state.streamingToolCallArguments.clear();
96704
97171
  state.livePane = {
96705
97172
  ...state.livePane,
96706
97173
  mode: "waiting",
@@ -96829,7 +97296,9 @@ async function openExternalEditor(state) {
96829
97296
  state.externalEditorRunning = true;
96830
97297
  const seed = state.editor.getExpandedText?.() ?? state.editor.getText();
96831
97298
  state.ui.stop();
96832
- await new Promise((resolve) => setImmediate(resolve));
97299
+ await new Promise((resolve) => {
97300
+ setImmediate(resolve);
97301
+ });
96833
97302
  try {
96834
97303
  const result = await editInExternalEditor(seed, cmd);
96835
97304
  if (result !== void 0) state.editor.setText(result.replaceAll("\r\n", "\n").replace(/\n$/, ""));
@@ -96853,11 +97322,11 @@ async function togglePlanMode(state, hooks) {
96853
97322
  return;
96854
97323
  }
96855
97324
  setState(state, { planMode: enabled }, hooks);
96856
- if (enabled && planPath !== void 0) {
96857
- emitPrimary(state, `Plan mode: ON. Write your plan to: ${planPath}`);
97325
+ if (enabled) {
97326
+ emitNotice(state, "Plan mode: ON", planPath !== void 0 ? `Plan will be created here: ${planPath}` : void 0);
96858
97327
  return;
96859
97328
  }
96860
- emitPrimary(state, `Plan mode: ${enabled ? "ON" : "OFF"}`);
97329
+ emitNotice(state, "Plan mode: OFF");
96861
97330
  }
96862
97331
  async function performReload(state, action, hooks) {
96863
97332
  if (state.appState.isStreaming) {
@@ -97319,6 +97788,7 @@ function releaseSessionSideEffects(state) {
97319
97788
  state.queuedMessages = [];
97320
97789
  state.queueIdCounter = 0;
97321
97790
  state.activeToolCalls.clear();
97791
+ state.streamingToolCallArguments?.clear();
97322
97792
  state.currentTurnId = void 0;
97323
97793
  state.assistantDraft = "";
97324
97794
  state.assistantStreamActive = false;
@@ -97370,8 +97840,8 @@ var CompactionComponent = class extends Container {
97370
97840
  this.stopBlink();
97371
97841
  }
97372
97842
  buildHeader() {
97373
- if (this.done) return `${chalk.hex(this.colors.success)(" ")}${chalk.hex(this.colors.success).bold("Compaction complete")}${this.tokensBefore !== void 0 && this.tokensAfter !== void 0 ? chalk.dim(` (${String(this.tokensBefore)} → ${String(this.tokensAfter)} tokens)`) : ""}`;
97374
- return `${this.blinkOn ? chalk.white(" ") : " "}${chalk.hex(this.colors.primary).bold("Compacting context...")}`;
97843
+ if (this.done) return `${chalk.hex(this.colors.success)(" ")}${chalk.hex(this.colors.success).bold("Compaction complete")}${this.tokensBefore !== void 0 && this.tokensAfter !== void 0 ? chalk.dim(` (${String(this.tokensBefore)} → ${String(this.tokensAfter)} tokens)`) : ""}`;
97844
+ return `${this.blinkOn ? chalk.white(" ") : " "}${chalk.hex(this.colors.primary).bold("Compacting context...")}`;
97375
97845
  }
97376
97846
  startBlink() {
97377
97847
  this.blinkTimer = setInterval(() => {
@@ -97510,7 +97980,8 @@ async function dispatchSlashCommand(input, state, buildCtx, addEntry) {
97510
97980
  client: ctx.client,
97511
97981
  appState: ctx.appState,
97512
97982
  setAppState: ctx.setAppState,
97513
- showStatus: ctx.showStatus
97983
+ showStatus: ctx.showStatus,
97984
+ showNotice: ctx.showNotice
97514
97985
  };
97515
97986
  let result;
97516
97987
  try {
@@ -97577,6 +98048,7 @@ async function dispatchSlashCommand(input, state, buildCtx, addEntry) {
97577
98048
  //#endregion
97578
98049
  //#region src/tui/handlers/turn-lifecycle.ts
97579
98050
  function handleTurnBegin(ectx, _data) {
98051
+ ectx.state.streamingToolCallArguments.clear();
97580
98052
  ectx.patchLivePane({
97581
98053
  mode: "waiting",
97582
98054
  thinkingText: "",
@@ -97594,6 +98066,7 @@ function handleTurnBegin(ectx, _data) {
97594
98066
  function handleTurnEnd(ectx, _data, sendQueued) {
97595
98067
  const todos = ectx.state.todoPanel.getTodos();
97596
98068
  if (todos.length > 0 && todos.every((t) => t.status === "done")) ectx.setTodoList([]);
98069
+ ectx.state.streamingToolCallArguments.clear();
97597
98070
  finalizeTurn(ectx, sendQueued);
97598
98071
  }
97599
98072
  function handleStepBegin(ectx) {
@@ -97645,24 +98118,18 @@ function handleContentDelta(ectx, data) {
97645
98118
  streamingPhase: "composing",
97646
98119
  streamingStartTime: Date.now()
97647
98120
  });
97648
- return;
97649
- }
97650
- if (data.type === "tool_call_part") {
97651
- if (ectx.state.thinkingDraft.length > 0) flushThinkingToTranscript(ectx, "idle");
97652
- ectx.patchLivePane({
97653
- mode: "idle",
97654
- pendingToolCall: null,
97655
- pendingApproval: null,
97656
- pendingQuestion: null
97657
- });
97658
- ectx.setAppState({
97659
- streamingPhase: "composing",
97660
- streamingStartTime: Date.now()
97661
- });
97662
98121
  }
97663
98122
  }
97664
98123
  //#endregion
97665
98124
  //#region src/tui/handlers/tool.ts
98125
+ function parseStreamingArgs(argumentsText) {
98126
+ if (argumentsText.trim().length === 0) return {};
98127
+ try {
98128
+ const parsed = JSON.parse(argumentsText);
98129
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
98130
+ } catch {}
98131
+ return { arguments: argumentsText };
98132
+ }
97666
98133
  function isTodoItemShape(value) {
97667
98134
  if (typeof value !== "object" || value === null) return false;
97668
98135
  const rec = value;
@@ -97676,15 +98143,53 @@ function handleToolCall(ectx, data) {
97676
98143
  args: data.args,
97677
98144
  description: data.description
97678
98145
  };
98146
+ const existing = ectx.state.activeToolCalls.get(data.id);
97679
98147
  ectx.state.activeToolCalls.set(data.id, toolCall);
97680
- flushTurnBuffers(ectx, "tool");
97681
- ectx.onToolCallStart(toolCall);
98148
+ ectx.state.streamingToolCallArguments.delete(data.id);
98149
+ const existingComponent = ectx.state.pendingToolComponents.get(data.id);
98150
+ if (existingComponent !== void 0) existingComponent.updateToolCall(toolCall);
98151
+ else if (existing === void 0) {
98152
+ flushTurnBuffers(ectx, "tool");
98153
+ ectx.onToolCallStart(toolCall);
98154
+ }
98155
+ ectx.patchLivePane({
98156
+ mode: "tool",
98157
+ pendingToolCall: toolCall,
98158
+ pendingApproval: null,
98159
+ pendingQuestion: null
98160
+ });
98161
+ }
98162
+ function handleToolCallDelta(ectx, data) {
98163
+ if (typeof data.tool_call_id !== "string" || data.tool_call_id.length === 0) return;
98164
+ const id = data.tool_call_id;
98165
+ const existing = ectx.state.streamingToolCallArguments.get(id);
98166
+ const argumentsText = `${existing?.argumentsText ?? ""}${data.arguments_part ?? ""}`;
98167
+ const name = data.name ?? existing?.name ?? ectx.state.activeToolCalls.get(id)?.name ?? "Tool";
98168
+ ectx.state.streamingToolCallArguments.set(id, {
98169
+ name,
98170
+ argumentsText
98171
+ });
98172
+ const toolCall = {
98173
+ id,
98174
+ name,
98175
+ args: parseStreamingArgs(argumentsText),
98176
+ streamingArguments: argumentsText
98177
+ };
98178
+ ectx.state.activeToolCalls.set(id, toolCall);
98179
+ if (ectx.state.thinkingDraft.length > 0) flushTurnBuffers(ectx, "tool");
98180
+ const existingComponent = ectx.state.pendingToolComponents.get(id);
98181
+ if (existingComponent !== void 0) existingComponent.updateToolCall(toolCall);
98182
+ else ectx.onToolCallStart(toolCall);
97682
98183
  ectx.patchLivePane({
97683
98184
  mode: "tool",
97684
98185
  pendingToolCall: toolCall,
97685
98186
  pendingApproval: null,
97686
98187
  pendingQuestion: null
97687
98188
  });
98189
+ ectx.setAppState({
98190
+ streamingPhase: "composing",
98191
+ streamingStartTime: Date.now()
98192
+ });
97688
98193
  }
97689
98194
  function handleToolResult(ectx, data) {
97690
98195
  const matchedCall = ectx.state.activeToolCalls.get(data.tool_call_id);
@@ -97698,7 +98203,7 @@ function handleToolResult(ectx, data) {
97698
98203
  if (matchedCall.name === "SetTodoList" && !data.is_error) {
97699
98204
  const rawTodos = matchedCall.args.todos;
97700
98205
  if (Array.isArray(rawTodos)) {
97701
- const sanitized = rawTodos.filter(isTodoItemShape).map((t) => ({
98206
+ const sanitized = rawTodos.filter((todo) => isTodoItemShape(todo)).map((t) => ({
97702
98207
  title: t.title,
97703
98208
  status: t.status
97704
98209
  }));
@@ -97707,6 +98212,7 @@ function handleToolResult(ectx, data) {
97707
98212
  }
97708
98213
  }
97709
98214
  ectx.state.activeToolCalls.delete(data.tool_call_id);
98215
+ ectx.state.streamingToolCallArguments.delete(data.tool_call_id);
97710
98216
  ectx.patchLivePane({
97711
98217
  mode: "waiting",
97712
98218
  pendingToolCall: null
@@ -97823,6 +98329,9 @@ function dispatchEvent(event, ectx, sendQueued) {
97823
98329
  case "tool.call":
97824
98330
  handleToolCall(ectx, event.data);
97825
98331
  break;
98332
+ case "tool.call.delta":
98333
+ handleToolCallDelta(ectx, event.data);
98334
+ break;
97826
98335
  case "tool.result":
97827
98336
  handleToolResult(ectx, event.data);
97828
98337
  break;
@@ -99092,7 +99601,7 @@ var ApprovalPanelComponent = class extends Container {
99092
99601
  focused = false;
99093
99602
  selectedIndex = 0;
99094
99603
  feedbackMode = false;
99095
- feedbackText = "";
99604
+ feedbackInput = new Input();
99096
99605
  expanded = false;
99097
99606
  onResponse;
99098
99607
  request;
@@ -99100,6 +99609,13 @@ var ApprovalPanelComponent = class extends Container {
99100
99609
  super();
99101
99610
  this.request = request;
99102
99611
  this.onResponse = onResponse;
99612
+ this.feedbackInput.onSubmit = (value) => {
99613
+ this.submit(this.selectedIndex, value);
99614
+ };
99615
+ this.feedbackInput.onEscape = () => {
99616
+ this.feedbackMode = false;
99617
+ this.feedbackInput.setValue("");
99618
+ };
99103
99619
  }
99104
99620
  submit(index, feedback = "") {
99105
99621
  const option = this.choiceAt(index);
@@ -99120,21 +99636,16 @@ var ApprovalPanelComponent = class extends Container {
99120
99636
  }
99121
99637
  onToggleToolExpand;
99122
99638
  handleInput(data) {
99639
+ if (matchesKey(data, Key.ctrl("c")) || matchesKey(data, Key.ctrl("d"))) {
99640
+ this.onResponse({ response: "rejected" });
99641
+ return;
99642
+ }
99123
99643
  if (matchesKey(data, Key.ctrl("o"))) {
99124
99644
  this.expanded = !this.expanded;
99125
99645
  this.onToggleToolExpand?.();
99126
99646
  return;
99127
99647
  }
99128
99648
  if (this.feedbackMode) {
99129
- if (matchesKey(data, Key.enter)) {
99130
- this.submit(this.selectedIndex, this.feedbackText);
99131
- return;
99132
- }
99133
- if (matchesKey(data, Key.escape)) {
99134
- this.feedbackMode = false;
99135
- this.feedbackText = "";
99136
- return;
99137
- }
99138
99649
  if (matchesKey(data, Key.up)) {
99139
99650
  this.feedbackMode = false;
99140
99651
  this.selectedIndex = (this.selectedIndex - 1 + this.choiceCount()) % this.choiceCount();
@@ -99145,16 +99656,7 @@ var ApprovalPanelComponent = class extends Container {
99145
99656
  this.selectedIndex = (this.selectedIndex + 1) % this.choiceCount();
99146
99657
  return;
99147
99658
  }
99148
- if (matchesKey(data, Key.backspace)) {
99149
- this.feedbackText = this.feedbackText.slice(0, -1);
99150
- return;
99151
- }
99152
- const printable = decodeKittyPrintable(data);
99153
- if (printable !== void 0) {
99154
- this.feedbackText += printable;
99155
- return;
99156
- }
99157
- if (data.length > 0 && !data.startsWith("\x1B")) this.feedbackText += data;
99659
+ this.feedbackInput.handleInput(data);
99158
99660
  return;
99159
99661
  }
99160
99662
  if (this.choiceCount() === 0) return;
@@ -99177,6 +99679,7 @@ var ApprovalPanelComponent = class extends Container {
99177
99679
  render(width) {
99178
99680
  this.clear();
99179
99681
  this.ensureValidSelection();
99682
+ this.feedbackInput.focused = this.focused && this.feedbackMode;
99180
99683
  const { data } = this.request;
99181
99684
  const horizontalBar = borderColor("─".repeat(width));
99182
99685
  const indent = (s) => ` ${s}`;
@@ -99201,12 +99704,12 @@ var ApprovalPanelComponent = class extends Container {
99201
99704
  const isSelected = idx === this.selectedIndex;
99202
99705
  const num = idx + 1;
99203
99706
  const labelWithNum = `${String(num)}. ${option.label}`;
99204
- if (this.feedbackMode && option.requires_feedback === true && isSelected) lines.push(indent(`${selectColor.bold("▶")} ${selectColor.bold(labelWithNum)} ${this.feedbackText}${selectColor("█")}`));
99707
+ if (this.feedbackMode && option.requires_feedback === true && isSelected) lines.push(indent(this.renderInlineFeedbackLine(width - 2, labelWithNum)));
99205
99708
  else if (isSelected) lines.push(indent(`${selectColor.bold("▶")} ${selectColor.bold(labelWithNum)}`));
99206
99709
  else lines.push(indent(chalk.gray(` ${labelWithNum}`)));
99207
99710
  }
99208
99711
  lines.push("");
99209
- if (this.feedbackMode) lines.push(indent(chalk.dim("Type feedback, then press Enter to submit.")));
99712
+ if (this.feedbackMode) lines.push(indent(chalk.dim("Type feedback · submit.")));
99210
99713
  else {
99211
99714
  const expandHint = hasDiff ? ` · ctrl+o ${this.expanded ? "collapse" : "expand"}` : "";
99212
99715
  lines.push(indent(chalk.dim(`↑/↓ select · ${buildNumericHint(data.choices.length)} choose · ↵ confirm${expandHint}`)));
@@ -99228,6 +99731,16 @@ var ApprovalPanelComponent = class extends Container {
99228
99731
  }
99229
99732
  if (this.selectedIndex < 0 || this.selectedIndex >= count) this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, count - 1));
99230
99733
  }
99734
+ renderInlineFeedbackLine(width, labelWithNum) {
99735
+ const prefix = `${selectColor.bold("▶")} ${selectColor.bold(labelWithNum)} `;
99736
+ const inputWidth = Math.max(4, width - visibleWidth(prefix) + 2);
99737
+ const inputLine = this.feedbackInput.render(inputWidth)[0] ?? "> ";
99738
+ return prefix + (inputLine.startsWith("> ") ? inputLine.slice(2) : inputLine);
99739
+ }
99740
+ invalidate() {
99741
+ super.invalidate();
99742
+ this.feedbackInput.invalidate();
99743
+ }
99231
99744
  };
99232
99745
  function buildNumericHint(count) {
99233
99746
  if (count <= 0) return "↵";
@@ -99314,6 +99827,7 @@ var QuestionDialogComponent = class extends Container {
99314
99827
  colors;
99315
99828
  onAnswer;
99316
99829
  maxVisibleOptions;
99830
+ otherInput = new Input();
99317
99831
  currentTab = 0;
99318
99832
  submitActionIdx = 0;
99319
99833
  editingOther = false;
@@ -99336,6 +99850,9 @@ var QuestionDialogComponent = class extends Container {
99336
99850
  this.onAnswer = onAnswer;
99337
99851
  this.colors = colors;
99338
99852
  this.maxVisibleOptions = maxVisibleOptions;
99853
+ this.otherInput.onSubmit = (value) => {
99854
+ this.commitOtherInput(value);
99855
+ };
99339
99856
  const total = request.data.questions.length;
99340
99857
  this.cursors = Array.from({ length: total }, () => 0);
99341
99858
  this.singleSelections = Array.from({ length: total }, () => void 0);
@@ -99349,6 +99866,10 @@ var QuestionDialogComponent = class extends Container {
99349
99866
  this.onAnswer([]);
99350
99867
  return;
99351
99868
  }
99869
+ if (matchesKey(data, Key.ctrl("c")) || matchesKey(data, Key.ctrl("d"))) {
99870
+ this.onAnswer([]);
99871
+ return;
99872
+ }
99352
99873
  if (this.isEditingOther()) {
99353
99874
  this.handleOtherInput(data);
99354
99875
  return;
@@ -99390,58 +99911,32 @@ var QuestionDialogComponent = class extends Container {
99390
99911
  this.activateQuestionOption(numIdx);
99391
99912
  return;
99392
99913
  }
99393
- if (printable === " " || matchesKey(data, Key.space)) {
99394
- if (question.multi_select) this.activateQuestionOption(this.currentCursor());
99395
- return;
99396
- }
99914
+ if ((printable === " " || matchesKey(data, Key.space)) && question.multi_select) this.activateQuestionOption(this.currentCursor());
99397
99915
  }
99398
99916
  handleOtherInput(data) {
99399
99917
  const questionIdx = this.currentQuestionIndex();
99400
99918
  if (questionIdx === void 0) return;
99401
- if (matchesKey(data, Key.left)) {
99402
- this.editingOther = false;
99403
- this.gotoTab(this.currentTab - 1);
99404
- return;
99405
- }
99406
- if (matchesKey(data, Key.right) || matchesKey(data, Key.tab)) {
99919
+ if (matchesKey(data, Key.tab)) {
99920
+ this.syncOtherDraft(questionIdx);
99407
99921
  this.editingOther = false;
99408
99922
  this.gotoTab(this.currentTab + 1);
99409
99923
  return;
99410
99924
  }
99411
99925
  if (matchesKey(data, Key.up)) {
99926
+ this.syncOtherDraft(questionIdx);
99412
99927
  this.editingOther = false;
99413
99928
  this.moveQuestionCursor(-1);
99414
99929
  return;
99415
99930
  }
99416
99931
  if (matchesKey(data, Key.down)) {
99932
+ this.syncOtherDraft(questionIdx);
99417
99933
  this.editingOther = false;
99418
99934
  this.moveQuestionCursor(1);
99419
99935
  return;
99420
99936
  }
99421
- if (matchesKey(data, Key.enter)) {
99422
- this.commitOtherInput();
99423
- return;
99424
- }
99425
- if (matchesKey(data, Key.backspace)) {
99426
- this.otherDrafts[questionIdx] = this.otherDrafts[questionIdx]?.slice(0, -1) ?? "";
99427
- this.reviewMessage = void 0;
99428
- return;
99429
- }
99430
- const printable = decodeKittyPrintable(data);
99431
- if (printable !== void 0) {
99432
- this.otherDrafts[questionIdx] = (this.otherDrafts[questionIdx] ?? "") + printable;
99433
- this.reviewMessage = void 0;
99434
- return;
99435
- }
99436
- if (data === " " || matchesKey(data, Key.space)) {
99437
- this.otherDrafts[questionIdx] = (this.otherDrafts[questionIdx] ?? "") + " ";
99438
- this.reviewMessage = void 0;
99439
- return;
99440
- }
99441
- if (data.length > 0 && !data.startsWith("\x1B")) {
99442
- this.otherDrafts[questionIdx] = (this.otherDrafts[questionIdx] ?? "") + data;
99443
- this.reviewMessage = void 0;
99444
- }
99937
+ this.otherInput.handleInput(data);
99938
+ this.syncOtherDraft(questionIdx);
99939
+ this.reviewMessage = void 0;
99445
99940
  }
99446
99941
  handleSubmitInput(data) {
99447
99942
  if (matchesKey(data, Key.up)) {
@@ -99475,7 +99970,6 @@ var QuestionDialogComponent = class extends Container {
99475
99970
  if (printable === "2") {
99476
99971
  this.submitActionIdx = 1;
99477
99972
  this.executeSubmitAction(1);
99478
- return;
99479
99973
  }
99480
99974
  }
99481
99975
  gotoTab(target) {
@@ -99524,15 +100018,17 @@ var QuestionDialogComponent = class extends Container {
99524
100018
  enterOtherInput(questionIdx) {
99525
100019
  this.cursors[questionIdx] = this.otherOptionIndex(questionIdx);
99526
100020
  this.editingOther = true;
100021
+ this.otherInput.setValue(this.otherDraftValue(questionIdx));
99527
100022
  this.reviewMessage = void 0;
99528
100023
  }
99529
- commitOtherInput() {
100024
+ commitOtherInput(rawValue) {
99530
100025
  const questionIdx = this.currentQuestionIndex();
99531
100026
  if (questionIdx === void 0) return;
99532
100027
  const question = this.request.data.questions[questionIdx];
99533
100028
  if (question === void 0) return;
99534
- const value = this.otherDrafts[questionIdx]?.trim() ?? "";
100029
+ const value = (rawValue ?? this.otherInput.getValue()).trim();
99535
100030
  if (value.length === 0) return;
100031
+ this.otherInput.setValue(value);
99536
100032
  this.otherDrafts[questionIdx] = value;
99537
100033
  this.committedOtherValues[questionIdx] = value;
99538
100034
  if (question.multi_select) this.multiSelections[questionIdx]?.add(this.otherOptionIndex(questionIdx));
@@ -99599,6 +100095,7 @@ var QuestionDialogComponent = class extends Container {
99599
100095
  this.onAnswer(out);
99600
100096
  }
99601
100097
  render(width) {
100098
+ this.otherInput.focused = this.focused && this.isEditingOther();
99602
100099
  return this.isSubmitTab() ? this.renderSubmitTab(width) : this.renderQuestionTab(width);
99603
100100
  }
99604
100101
  renderQuestionTab(width) {
@@ -99611,10 +100108,11 @@ var QuestionDialogComponent = class extends Container {
99611
100108
  const dim = chalk.hex(colors.textDim);
99612
100109
  const success = chalk.hex(colors.success);
99613
100110
  const renderWidth = Math.max(1, width);
99614
- const lines = [];
99615
- lines.push(accent("─".repeat(renderWidth)));
99616
- lines.push(accent.bold(" question"));
99617
- lines.push("");
100111
+ const lines = [
100112
+ accent("─".repeat(renderWidth)),
100113
+ accent.bold(" question"),
100114
+ ""
100115
+ ];
99618
100116
  this.pushTabs(lines);
99619
100117
  lines.push("");
99620
100118
  lines.push(accent(` ? ${question.question}`));
@@ -99635,10 +100133,15 @@ var QuestionDialogComponent = class extends Container {
99635
100133
  const singleSelection = this.singleSelections[questionIdx];
99636
100134
  for (let i = visibleStart; i < visibleEnd; i++) {
99637
100135
  const option = options[i];
100136
+ if (option === void 0) continue;
99638
100137
  const num = i + 1;
99639
100138
  const isCursor = i === cursor;
99640
100139
  const isOther = option.kind === "other";
99641
100140
  const isSelected = question.multi_select ? multiSet.has(i) : singleSelection === i;
100141
+ if (this.isEditingOther() && isCursor && isOther) {
100142
+ lines.push(this.renderEditingOtherLine(renderWidth, questionIdx, option, num, isSelected));
100143
+ continue;
100144
+ }
99642
100145
  const label = this.renderOptionLabel(questionIdx, option, isCursor);
99643
100146
  let line;
99644
100147
  if (question.multi_select) {
@@ -99666,10 +100169,11 @@ var QuestionDialogComponent = class extends Container {
99666
100169
  const text = chalk.hex(colors.text);
99667
100170
  const warning = chalk.hex(colors.warning);
99668
100171
  const renderWidth = Math.max(1, width);
99669
- const lines = [];
99670
- lines.push(accent("─".repeat(renderWidth)));
99671
- lines.push(accent.bold(" question"));
99672
- lines.push("");
100172
+ const lines = [
100173
+ accent("─".repeat(renderWidth)),
100174
+ accent.bold(" question"),
100175
+ ""
100176
+ ];
99673
100177
  this.pushTabs(lines);
99674
100178
  lines.push("");
99675
100179
  lines.push(text.bold(` ${REVIEW_TITLE}`));
@@ -99678,6 +100182,7 @@ var QuestionDialogComponent = class extends Container {
99678
100182
  lines.push("");
99679
100183
  for (let i = 0; i < this.request.data.questions.length; i++) {
99680
100184
  const question = this.request.data.questions[i];
100185
+ if (question === void 0) continue;
99681
100186
  const answer = this.answers[i];
99682
100187
  lines.push(` ${dim("Q")} ${question.question}`);
99683
100188
  if (answer !== void 0 && answer.length > 0) lines.push(` ${accent("→")} ${text(answer)}`);
@@ -99688,6 +100193,7 @@ var QuestionDialogComponent = class extends Container {
99688
100193
  lines.push("");
99689
100194
  for (let i = 0; i < SUBMIT_ACTIONS.length; i++) {
99690
100195
  const label = SUBMIT_ACTIONS[i];
100196
+ if (label === void 0) continue;
99691
100197
  const num = i + 1;
99692
100198
  if (i === this.submitActionIdx) lines.push(accent(` → [${String(num)}] ${label}`));
99693
100199
  else lines.push(dim(` [${String(num)}] ${label}`));
@@ -99703,6 +100209,7 @@ var QuestionDialogComponent = class extends Container {
99703
100209
  const tabs = [];
99704
100210
  for (let i = 0; i < this.request.data.questions.length; i++) {
99705
100211
  const question = this.request.data.questions[i];
100212
+ if (question === void 0) continue;
99706
100213
  const label = question.header !== void 0 && question.header.length > 0 ? question.header : `Q${String(i + 1)}`;
99707
100214
  if (i === this.currentTab) tabs.push(active(` ${label} `));
99708
100215
  else if (this.isAnswered(i)) tabs.push(chalk.green(`(✓) ${label}`));
@@ -99714,14 +100221,17 @@ var QuestionDialogComponent = class extends Container {
99714
100221
  lines.push(` ${tabs.join(" ")}`);
99715
100222
  }
99716
100223
  buildQuestionHint(dim, questionIdx) {
99717
- if (this.isEditingOther()) {
99718
- const parts = ["type answer", "↵ save"];
99719
- if (this.totalTabs() > 1) parts.push("←/→/tab switch");
99720
- parts.push("esc dismiss");
99721
- return dim(` ${parts.join(" ")}`);
99722
- }
100224
+ if (this.isEditingOther()) return dim(` ${[
100225
+ "type answer",
100226
+ " save",
100227
+ ...this.totalTabs() > 1 ? ["tab switch"] : [],
100228
+ "esc dismiss"
100229
+ ].join(" ")}`);
99723
100230
  const optionCount = Math.min(this.displayOptions(questionIdx).length, NUMBER_KEYS.length);
99724
- const parts = ["▲/▼ select", `${optionCount <= 1 ? "1" : `1-${String(optionCount)}`} / ↵ ${this.request.data.questions[questionIdx].multi_select ? "toggle" : "choose"}`];
100231
+ const numberHint = optionCount <= 1 ? "1" : `1-${String(optionCount)}`;
100232
+ const question = this.request.data.questions[questionIdx];
100233
+ if (question === void 0) return dim(" esc dismiss");
100234
+ const parts = ["▲/▼ select", `${numberHint} / ↵ ${question.multi_select ? "toggle" : "choose"}`];
99725
100235
  if (this.totalTabs() > 1) parts.push("←/→/tab switch");
99726
100236
  parts.push("esc dismiss");
99727
100237
  return dim(` ${parts.join(" ")}`);
@@ -99783,11 +100293,33 @@ var QuestionDialogComponent = class extends Container {
99783
100293
  }
99784
100294
  renderOptionLabel(questionIdx, option, isCursor) {
99785
100295
  if (option.kind !== "other") return option.label;
99786
- const value = this.otherDrafts[questionIdx] || this.committedOtherValues[questionIdx];
100296
+ const value = this.otherDraftValue(questionIdx);
99787
100297
  if (this.isEditingOther() && isCursor) return `${option.label}: ${value ?? ""}█`;
99788
100298
  if (value !== void 0 && value.length > 0) return `${option.label}: ${value}`;
99789
100299
  return option.label;
99790
100300
  }
100301
+ renderEditingOtherLine(width, questionIdx, option, num, isSelected) {
100302
+ const question = this.request.data.questions[questionIdx];
100303
+ if (question === void 0) return option.label;
100304
+ let prefix;
100305
+ if (question.multi_select) {
100306
+ const body = ` [${isSelected ? "✓" : " "}] ${option.label}: `;
100307
+ prefix = isSelected ? chalk.hex(this.colors.success).bold(body) : chalk.hex(this.colors.primary)(body);
100308
+ } else {
100309
+ const body = ` → [${String(num)}] ${option.label}: `;
100310
+ prefix = isSelected && this.isAnswered(questionIdx) ? chalk.hex(this.colors.success).bold(body) : chalk.hex(this.colors.primary)(body);
100311
+ }
100312
+ const inputWidth = Math.max(4, width - visibleWidth(prefix) + 2);
100313
+ const inputLine = this.otherInput.render(inputWidth)[0] ?? "> ";
100314
+ const inlineInput = inputLine.startsWith("> ") ? inputLine.slice(2) : inputLine;
100315
+ return prefix + inlineInput;
100316
+ }
100317
+ otherDraftValue(questionIdx) {
100318
+ return this.otherDrafts[questionIdx] || this.committedOtherValues[questionIdx] || "";
100319
+ }
100320
+ syncOtherDraft(questionIdx) {
100321
+ this.otherDrafts[questionIdx] = this.otherInput.getValue();
100322
+ }
99791
100323
  isAnswered(questionIdx) {
99792
100324
  const answer = this.answers[questionIdx];
99793
100325
  return answer !== void 0 && answer.length > 0;
@@ -99796,6 +100328,10 @@ var QuestionDialogComponent = class extends Container {
99796
100328
  for (let i = 0; i < this.request.data.questions.length; i++) if (!this.isAnswered(i)) return true;
99797
100329
  return false;
99798
100330
  }
100331
+ invalidate() {
100332
+ super.invalidate();
100333
+ this.otherInput.invalidate();
100334
+ }
99799
100335
  };
99800
100336
  //#endregion
99801
100337
  //#region src/tui/reverse-rpc/question/ui.ts
@@ -100804,14 +101340,18 @@ var KimiTUI = class {
100804
101340
  this.stateHooks.syncFooter();
100805
101341
  this.stateHooks.refreshActivityPane();
100806
101342
  },
100807
- showStatus: (message) => {
101343
+ showStatus: (message, color) => {
100808
101344
  addTranscriptEntry(this.state, {
100809
101345
  id: `status-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
100810
101346
  kind: "status",
100811
101347
  renderMode: "plain",
100812
- content: message
101348
+ content: message,
101349
+ ...color !== void 0 ? { color } : {}
100813
101350
  });
100814
101351
  },
101352
+ showNotice: (title, detail) => {
101353
+ emitNotice(this.state, title, detail);
101354
+ },
100815
101355
  stop: () => void this.stop(),
100816
101356
  showHelpPanel: () => showHelpPanel(this.state),
100817
101357
  showSessionPicker: () => showSessionPicker(this.state, this.buildSessionHooks()),
@@ -100942,6 +101482,7 @@ const MUTED = "#999999";
100942
101482
  const TEXT_DIM = "#6B7280";
100943
101483
  const INSTALL_HINT = "Install update now";
100944
101484
  const SKIP_HINT = "Continue with current version";
101485
+ const PRODUCT_NAME = "Kimi Code";
100945
101486
  function createInstallPromptChoices(target) {
100946
101487
  return [{
100947
101488
  value: "install",
@@ -100953,7 +101494,7 @@ function createInstallPromptChoices(target) {
100953
101494
  }
100954
101495
  function getDefaultInstallPromptSelection(choices) {
100955
101496
  const installIndex = choices.findIndex((choice) => choice.value === "install");
100956
- return installIndex >= 0 ? installIndex : 0;
101497
+ return Math.max(installIndex, 0);
100957
101498
  }
100958
101499
  function moveInstallPromptSelection(currentIndex, direction, choiceCount) {
100959
101500
  if (direction === "up") return Math.max(0, currentIndex - 1);
@@ -100967,7 +101508,7 @@ function renderInstallPrompt(options, choices, selectedIndex) {
100967
101508
  const command = chalk.hex(PRIMARY)(options.installCommand);
100968
101509
  const lines = [
100969
101510
  chalk.hex(PRIMARY).bold("Kimi Code Update Available"),
100970
- chalk.hex(MUTED)(`${NPM_PACKAGE_NAME} has a newer release ready.`),
101511
+ chalk.hex(MUTED)(`${PRODUCT_NAME} has a newer release ready.`),
100971
101512
  "",
100972
101513
  `${label("Current")} ${currentVersion}`,
100973
101514
  `${label("Target ")} ${targetVersion} ${channel}`,
@@ -100978,6 +101519,7 @@ function renderInstallPrompt(options, choices, selectedIndex) {
100978
101519
  ];
100979
101520
  for (let i = 0; i < choices.length; i++) {
100980
101521
  const choice = choices[i];
101522
+ if (choice === void 0) continue;
100981
101523
  const isSelected = i === selectedIndex;
100982
101524
  const content = ` ${isSelected ? "❯" : " "} ${choice.label}`;
100983
101525
  if (isSelected) {