@webpresso/agent-kit 3.1.28 → 3.1.30

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 (98) hide show
  1. package/catalog/AGENTS.md.tpl +4 -4
  2. package/catalog/agent/rules/ci-cost-local-first.md +139 -0
  3. package/catalog/agent/rules/ci-test-perf.md +5 -1
  4. package/catalog/agent/rules/cmd-execution.md +9 -0
  5. package/catalog/agent/rules/pre-implementation.md +2 -1
  6. package/catalog/agent/rules/test-scan-perf.md +80 -0
  7. package/catalog/agent/rules/workflow-skills-routing.md +4 -2
  8. package/catalog/agent/skills/fix/SKILL.md +3 -0
  9. package/catalog/agent/skills/verify/SKILL.md +18 -0
  10. package/dist/esm/audit/blueprint-pr-coverage.js +68 -7
  11. package/dist/esm/audit/registry.d.ts +4 -4
  12. package/dist/esm/audit/registry.js +3 -0
  13. package/dist/esm/audit/roadmap-links.js +6 -0
  14. package/dist/esm/audit/test-scan-perf.d.ts +18 -0
  15. package/dist/esm/audit/test-scan-perf.js +275 -0
  16. package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
  17. package/dist/esm/blueprint/lifecycle/engine.js +53 -6
  18. package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
  19. package/dist/esm/blueprint/lifecycle/review-provenance.js +93 -12
  20. package/dist/esm/blueprint/markdown/helpers.js +6 -2
  21. package/dist/esm/ci/act-replay.js +110 -7
  22. package/dist/esm/ci/act-runner.d.ts +4 -0
  23. package/dist/esm/ci/act-runner.js +85 -26
  24. package/dist/esm/ci/act-worktree-git.d.ts +11 -0
  25. package/dist/esm/ci/act-worktree-git.js +219 -0
  26. package/dist/esm/cli/commands/audit.js +3 -0
  27. package/dist/esm/cli/commands/blueprint/mutations.js +35 -18
  28. package/dist/esm/cli/commands/blueprint/router.js +27 -2
  29. package/dist/esm/cli/commands/ci.d.ts +8 -1
  30. package/dist/esm/cli/commands/ci.js +80 -21
  31. package/dist/esm/cli/commands/dash/keymap.js +1 -0
  32. package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
  33. package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
  34. package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
  35. package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
  36. package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
  37. package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
  38. package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
  39. package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
  40. package/dist/esm/cli/commands/dash/plan-once.d.ts +22 -2
  41. package/dist/esm/cli/commands/dash/plan-once.js +41 -4
  42. package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
  43. package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
  44. package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
  45. package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
  46. package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
  47. package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
  48. package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
  49. package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
  50. package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
  51. package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
  52. package/dist/esm/cli/commands/dash/tui/runtime.js +239 -4
  53. package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
  54. package/dist/esm/cli/commands/dash/tui/state.js +257 -4
  55. package/dist/esm/cli/commands/format.js +0 -2
  56. package/dist/esm/cli/commands/init/config.d.ts +0 -1
  57. package/dist/esm/cli/commands/init/config.js +1 -4
  58. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
  59. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
  60. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
  61. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
  62. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
  63. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
  64. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +61 -23
  65. package/dist/esm/cli/commands/logs.d.ts +15 -0
  66. package/dist/esm/cli/commands/logs.js +192 -3
  67. package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
  68. package/dist/esm/cli/commands/quality-log-store.js +187 -51
  69. package/dist/esm/cli/commands/review.js +5 -8
  70. package/dist/esm/cli/commands/secrets.d.ts +1 -0
  71. package/dist/esm/cli/commands/secrets.js +4 -1
  72. package/dist/esm/cli/direct-provider-launch.js +25 -1
  73. package/dist/esm/hooks/doctor.d.ts +15 -0
  74. package/dist/esm/hooks/doctor.js +56 -0
  75. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +4 -4
  76. package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
  77. package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
  78. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
  79. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
  80. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  81. package/dist/esm/mcp/tools/audit.d.ts +2 -1
  82. package/dist/esm/mcp/tools/audit.js +10 -0
  83. package/dist/esm/mcp/tools/audits.d.ts +1 -0
  84. package/dist/esm/mcp/tools/ci-act.js +9 -11
  85. package/dist/esm/paths/state-root.js +3 -0
  86. package/dist/esm/review/authority.js +17 -2
  87. package/dist/esm/review/availability.js +18 -5
  88. package/dist/esm/review/subject.d.ts +15 -0
  89. package/dist/esm/review/subject.js +105 -10
  90. package/dist/esm/runtime/executor.d.ts +1 -0
  91. package/dist/esm/runtime/executor.js +2 -1
  92. package/dist/esm/secret-gate/runner.d.ts +1 -0
  93. package/dist/esm/secret-gate/runner.js +4 -1
  94. package/dist/esm/worktrees/current.d.ts +1 -0
  95. package/dist/esm/worktrees/current.js +1 -0
  96. package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
  97. package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
  98. package/package.json +14 -14
@@ -5,6 +5,7 @@ import { buildRows } from "#cli/commands/dash/menu-state.js";
5
5
  import { footerHintsFor, helpEntriesFor } from "#cli/commands/dash/keymap.js";
6
6
  import { filterPaletteCommands } from "#cli/commands/dash/tui/palette.js";
7
7
  import { HOST_CAPABILITIES } from "#cli/commands/dash/tui/hosts.js";
8
+ import { DEFAULT_CONTROL_PLANE_CHAT_MODE, formatChatModePill, } from "#cli/commands/dash/tui/chat-mode.js";
8
9
  import { sanitizeTuiText } from "#cli/commands/dash/tui/sanitize.js";
9
10
  function padLine(text, width) {
10
11
  return fitDashText(text, width).padEnd(Math.max(0, width)).slice(0, Math.max(0, width));
@@ -68,7 +69,8 @@ export function browserFrameChromeRows(input) {
68
69
  export function renderFrame(view) {
69
70
  const width = Math.max(24, Math.min(view.width, 120));
70
71
  const lines = [];
71
- const title = ` wp dash · ${view.repoLabel} · ${formatModePill(view.permissionMode)} `;
72
+ const chatMode = view.chatMode ?? DEFAULT_CONTROL_PLANE_CHAT_MODE;
73
+ const title = ` wp dash · ${view.repoLabel} · ${formatModePill(view.permissionMode)} · ${formatChatModePill(chatMode)} `;
72
74
  lines.push(padLine(title, width));
73
75
  lines.push(hrule(width));
74
76
  if (view.ultragoalLine)
@@ -1,4 +1,4 @@
1
- export type PaletteCommandId = "palette.open" | "host.new.claude" | "host.new.codex" | "host.new.grok" | "host.new.opencode" | "sessions.focus" | "wizard.open" | "blueprints.open" | "fleet.open" | "permission.toggle" | "help.open" | "app.quit";
1
+ export type PaletteCommandId = "palette.open" | "host.new.claude" | "host.new.codex" | "host.new.grok" | "host.new.opencode" | "sessions.focus" | "wizard.open" | "blueprints.open" | "fleet.open" | "merge.status" | "chat.mode.toggle" | "chat.mode.open" | "chat.mode.send" | "permission.toggle" | "help.open" | "app.quit";
2
2
  export type PaletteCategory = "Host" | "Session" | "Navigate" | "System";
3
3
  export type PaletteCommand = {
4
4
  readonly id: PaletteCommandId;
@@ -37,6 +37,20 @@ const STATIC_PALETTE_COMMANDS = [
37
37
  keys: "f",
38
38
  keywords: ["fleet", "watch", "status"],
39
39
  },
40
+ {
41
+ id: "merge.status",
42
+ title: "Plan-once merge status matrix",
43
+ category: "Navigate",
44
+ keys: "m",
45
+ keywords: ["merge", "plan-once", "multi-host", "rollup", "status", "matrix"],
46
+ },
47
+ {
48
+ id: "chat.mode.toggle",
49
+ title: "Toggle host-pane / integrated chat",
50
+ category: "Session",
51
+ keys: "c",
52
+ keywords: ["chat", "acp", "integrated", "host-pane", "mode", "pane"],
53
+ },
40
54
  {
41
55
  id: "permission.toggle",
42
56
  title: "Toggle permission mode (safe / always-approve)",
@@ -3,8 +3,8 @@
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { DashPaneController } from "#cli/commands/dash/pane-controller.js";
5
5
  import { DASH_REFRESH_INTERVALS_MS, initialAgentEntries, initialLaunchSelection, loadAgentEntriesWithDiagnostics, projectDashboardRuns, projectRunState, shouldRunInitialLaunch, } from "#cli/commands/dash/menu-helpers.js";
6
- import { parseDashPermissionMode, } from "#cli/commands/dash/provider-capabilities.js";
7
- import { resolveLaunchPermissionMode } from "#cli/commands/dash/launch-preferences.js";
6
+ import { isDashProvider, parseDashPermissionMode, } from "#cli/commands/dash/provider-capabilities.js";
7
+ import { readChatModePreference, resolveLaunchPermissionMode, writeChatModePreference, } from "#cli/commands/dash/launch-preferences.js";
8
8
  import { buildDashRuntimeArgv } from "#cli/commands/dash/runtime-invocation.js";
9
9
  import { emitDashE2eState } from "#cli/commands/dash/e2e-state.js";
10
10
  import { loadDashUsageSnapshot } from "#cli/commands/dash/usage.js";
@@ -12,16 +12,18 @@ import { formatUltragoalSummaryLine } from "#cli/commands/dash/menu-state.js";
12
12
  import { readUltragoalSummary } from "#ultragoal/summary.js";
13
13
  import { browserFrameChromeRows, renderFrame, } from "#cli/commands/dash/tui/frame.js";
14
14
  import { CONTROL_PLANE_HOSTS, HOST_CAPABILITIES, projectHostAvailability, } from "#cli/commands/dash/tui/hosts.js";
15
- import { applyWizardNewWorktreeProbe, createInitialTuiState, reduceTuiChunk, rebindPermissionMode, seedBlueprintBrowser, seedWizardItems, withAgents, } from "#cli/commands/dash/tui/state.js";
15
+ import { applyChatBridgeSession, applyPreferredChatModeSeed, applyWizardNewWorktreeProbe, createInitialTuiState, reduceTuiChunk, rebindPermissionMode, seedBlueprintBrowser, seedWizardItems, withAgents, } from "#cli/commands/dash/tui/state.js";
16
+ import { createHermeticChatBridge } from "#cli/commands/dash/tui/chat-bridge.js";
16
17
  import { BlueprintViewLoader } from "#cli/commands/dash/blueprints.js";
17
18
  import { DashLaunchCoordinator } from "#cli/commands/dash/launch-coordinator.js";
18
19
  import { sanitizeTuiText } from "#cli/commands/dash/tui/sanitize.js";
19
20
  import { defaultLoadWorktreePickerItems, isProjectClean } from "#cli/commands/dash/menu-helpers.js";
20
- import { attentionItemsFromRunStatuses, formatAttentionLine, formatResourceLine, } from "#cli/commands/dash/tui/attention-hud.js";
21
+ import { attentionItemsFromRunStatuses, formatAttentionLine, formatResourceLine, selectAttentionLine, } from "#cli/commands/dash/tui/attention-hud.js";
21
22
  import { blueprintRowsFromView, browserOverlayLines } from "#cli/commands/dash/tui/browser.js";
22
23
  import { BlueprintActionController } from "#cli/commands/dash/blueprint-actions.js";
23
24
  import { controlBlueprintExec, executeBlueprint, readBlueprintExecutionLogs, startBlueprint, } from "#cli/commands/blueprint/router.js";
24
25
  import { isDashRunProvider } from "#cli/commands/dash/run-types.js";
26
+ import { formatMergeOperatorSurface, formatPrimaryMergeAttentionLine, projectAllMergeSets, } from "#cli/commands/dash/plan-once-merge.js";
25
27
  function whichOnPath(binary) {
26
28
  const result = spawnSync("which", [binary], { encoding: "utf8" });
27
29
  return result.status === 0 && Boolean(result.stdout?.trim());
@@ -117,6 +119,7 @@ function toViewModel(state, input) {
117
119
  width: input.width,
118
120
  height: input.height,
119
121
  permissionMode: state.permissionMode,
122
+ chatMode: state.chatMode,
120
123
  agents: state.agents,
121
124
  menu: state.menu,
122
125
  hostAvailability: state.hostAvailability,
@@ -219,11 +222,16 @@ export async function runTuiMenu(input) {
219
222
  });
220
223
  }
221
224
  const hostAvailability = projectHostAvailability(detectHostBinaries());
225
+ // Preferred integrated-chat never seeds chrome here — only after agents load
226
+ // when host/run gates can pass (applyPreferredChatModeSeed).
227
+ const preferredChatMode = readChatModePreference();
222
228
  let state = createInitialTuiState({
223
229
  agents: initialAgentEntries(seededMode),
224
230
  permissionMode: seededMode,
225
231
  hostAvailability,
232
+ chatMode: "host-pane",
226
233
  });
234
+ let preferredChatModeSeeded = false;
227
235
  // Seed prepared-launch diagnostics (optional-tool refresh failures, etc.).
228
236
  if (input.initialLaunch?.warnings.length) {
229
237
  state = {
@@ -284,6 +292,32 @@ export async function runTuiMenu(input) {
284
292
  }
285
293
  dirty = false;
286
294
  };
295
+ let mergeHudGeneration = 0;
296
+ /**
297
+ * Prefer plan-once merge rollup only for incomplete/partial/all-failed.
298
+ * all-ok is omitted so generic per-run attention stays visible.
299
+ */
300
+ const refreshMergeAttention = async () => {
301
+ const gen = ++mergeHudGeneration;
302
+ try {
303
+ const projections = await projectAllMergeSets({ cwd: input.cwd });
304
+ if (gen !== mergeHudGeneration || shuttingDown)
305
+ return;
306
+ // null when no memberships or only all-ok — keep generic attention.
307
+ const mergeLine = formatPrimaryMergeAttentionLine(projections);
308
+ const next = selectAttentionLine({
309
+ mergeLine,
310
+ genericLine: state.attentionLine,
311
+ });
312
+ if (next === state.attentionLine)
313
+ return;
314
+ state = { ...state, attentionLine: next };
315
+ dirty = true;
316
+ }
317
+ catch {
318
+ // Merge HUD is best-effort; generic attention remains.
319
+ }
320
+ };
287
321
  const mergeRunStatuses = (statuses) => {
288
322
  runStatuses = statuses;
289
323
  // Always rebind from the discovery snapshot so async status refresh cannot
@@ -298,6 +332,7 @@ export async function runTuiMenu(input) {
298
332
  }, projected);
299
333
  void projectRunState;
300
334
  dirty = true;
335
+ void refreshMergeAttention();
301
336
  };
302
337
  const applyEffects = async (effects) => {
303
338
  for (const effect of effects) {
@@ -305,6 +340,15 @@ export async function runTuiMenu(input) {
305
340
  running = false;
306
341
  continue;
307
342
  }
343
+ if (effect.kind === "persist-chat-mode") {
344
+ try {
345
+ writeChatModePreference(effect.mode);
346
+ }
347
+ catch {
348
+ // Preference write is best-effort; never block the control plane.
349
+ }
350
+ continue;
351
+ }
308
352
  if (effect.kind === "e2e-group") {
309
353
  emitDashE2eState({
310
354
  kind: "group",
@@ -490,6 +534,188 @@ export async function runTuiMenu(input) {
490
534
  dirty = true;
491
535
  continue;
492
536
  }
537
+ if (effect.kind === "palette-command" && effect.id === "merge.status") {
538
+ const loadGen = ++browserLoadGeneration;
539
+ try {
540
+ const projections = await projectAllMergeSets({ cwd: input.cwd });
541
+ if (loadGen !== browserLoadGeneration || shuttingDown)
542
+ continue;
543
+ if (state.overlay.kind !== "browser")
544
+ continue;
545
+ if (projections.length === 0) {
546
+ state = {
547
+ ...state,
548
+ browser: null,
549
+ overlay: {
550
+ kind: "browser",
551
+ title: "Plan-once merge",
552
+ lines: [
553
+ "No plan-once membership records.",
554
+ "Confirm a multi-host plan first.",
555
+ "git/PR auto-merge out of scope — status/summary only",
556
+ ],
557
+ },
558
+ };
559
+ }
560
+ else {
561
+ const lines = [];
562
+ for (const projection of projections) {
563
+ const surface = formatMergeOperatorSurface(projection);
564
+ lines.push(surface.summary);
565
+ for (const row of surface.matrix)
566
+ lines.push(` ${row}`);
567
+ lines.push(surface.scopeNote);
568
+ lines.push("");
569
+ }
570
+ if (lines.at(-1) === "")
571
+ lines.pop();
572
+ state = {
573
+ ...state,
574
+ browser: null,
575
+ overlay: {
576
+ kind: "browser",
577
+ title: "Plan-once merge",
578
+ lines: lines.map((line) => sanitizeTuiText(line)),
579
+ },
580
+ };
581
+ }
582
+ }
583
+ catch (error) {
584
+ if (loadGen !== browserLoadGeneration || shuttingDown)
585
+ continue;
586
+ if (state.overlay.kind !== "browser")
587
+ continue;
588
+ state = {
589
+ ...state,
590
+ browser: null,
591
+ overlay: {
592
+ kind: "browser",
593
+ title: "Plan-once merge",
594
+ lines: [
595
+ sanitizeTuiText(error instanceof Error ? error.message : "failed to load merge status"),
596
+ ],
597
+ },
598
+ };
599
+ }
600
+ dirty = true;
601
+ continue;
602
+ }
603
+ if (effect.kind === "palette-command" && effect.id === "chat.mode.open") {
604
+ // Hermetic bridge for CI/MVP: uses existing dash run identity only.
605
+ const expectedGeneration = state.chatOpenGeneration;
606
+ try {
607
+ const hostKey = state.menu.selectedKey?.startsWith("group:")
608
+ ? state.menu.selectedKey.slice("group:".length)
609
+ : state.agents.find((agent) => agent.sessions.some((session) => state.menu.selectedKey === `run:${session.id}` ||
610
+ state.menu.selectedKey === `run:${session.runId ?? ""}`))?.key;
611
+ const host = hostKey && isDashProvider(hostKey)
612
+ ? hostKey
613
+ : state.chatSession?.host && isDashProvider(state.chatSession.host)
614
+ ? state.chatSession.host
615
+ : null;
616
+ let runId = state.chatSession?.runId ?? null;
617
+ let label = state.chatSession?.label ?? null;
618
+ if (!runId) {
619
+ const key = state.menu.selectedKey;
620
+ if (key?.startsWith("run:")) {
621
+ const id = key.slice("run:".length);
622
+ for (const agent of state.agents) {
623
+ const session = agent.sessions.find((s) => s.id === id || s.runId === id);
624
+ if (session) {
625
+ runId = session.runId ?? session.id;
626
+ label = session.label;
627
+ break;
628
+ }
629
+ }
630
+ }
631
+ else if (host) {
632
+ const agent = state.agents.find((entry) => entry.key === host);
633
+ const owned = agent?.sessions.find((s) => s.runId);
634
+ if (owned) {
635
+ runId = owned.runId ?? owned.id;
636
+ label = owned.label;
637
+ }
638
+ }
639
+ }
640
+ if (!host || !runId) {
641
+ state = applyChatBridgeSession(state, null, "Select a host session (run) before switching to integrated chat", { expectedGeneration });
642
+ }
643
+ else {
644
+ const bridge = createHermeticChatBridge();
645
+ const opened = await bridge.open({ host, runId, label });
646
+ if (state.chatOpenGeneration !== expectedGeneration ||
647
+ state.chatMode !== "integrated-chat") {
648
+ // Operator left chat while open was in flight — discard.
649
+ continue;
650
+ }
651
+ if (!opened.ok) {
652
+ state = applyChatBridgeSession(state, null, opened.error, { expectedGeneration });
653
+ }
654
+ else {
655
+ // Open only (no auto-send); operator composes and presses Enter.
656
+ state = applyChatBridgeSession(state, opened.session, undefined, {
657
+ expectedGeneration,
658
+ });
659
+ // Successful enter: persist preferred integrated-chat.
660
+ if (state.chatMode === "integrated-chat") {
661
+ try {
662
+ writeChatModePreference("integrated-chat");
663
+ }
664
+ catch {
665
+ /* best-effort */
666
+ }
667
+ }
668
+ }
669
+ }
670
+ }
671
+ catch (error) {
672
+ state = applyChatBridgeSession(state, null, error instanceof Error ? error.message : "chat bridge failed", { expectedGeneration });
673
+ }
674
+ dirty = true;
675
+ continue;
676
+ }
677
+ if (effect.kind === "palette-command" && effect.id === "chat.mode.send") {
678
+ const expectedGeneration = state.chatOpenGeneration;
679
+ const session = state.chatSession;
680
+ const text = state.chatDraft.trim();
681
+ if (!session || !text) {
682
+ state = {
683
+ ...state,
684
+ status: !session ? "chat session not open yet" : "type a message before send",
685
+ };
686
+ dirty = true;
687
+ continue;
688
+ }
689
+ try {
690
+ const bridge = createHermeticChatBridge();
691
+ const sent = await bridge.send(session, text);
692
+ if (state.chatOpenGeneration !== expectedGeneration ||
693
+ state.chatMode !== "integrated-chat") {
694
+ continue;
695
+ }
696
+ if (!sent.ok) {
697
+ state = {
698
+ ...state,
699
+ status: sanitizeTuiText(sent.error),
700
+ };
701
+ }
702
+ else {
703
+ state = applyChatBridgeSession({ ...state, chatDraft: "" }, sent.session, undefined, {
704
+ expectedGeneration,
705
+ });
706
+ }
707
+ }
708
+ catch (error) {
709
+ if (state.chatOpenGeneration === expectedGeneration) {
710
+ state = {
711
+ ...state,
712
+ status: sanitizeTuiText(error instanceof Error ? error.message : "chat send failed"),
713
+ };
714
+ }
715
+ }
716
+ dirty = true;
717
+ continue;
718
+ }
493
719
  if (effect.kind === "palette-command" && effect.id === "wizard.open") {
494
720
  // Seed worktree picker items when the wizard opens (or re-emit cursor).
495
721
  if (state.wizard && state.wizard.allItems.length === 0) {
@@ -537,6 +763,15 @@ export async function runTuiMenu(input) {
537
763
  return;
538
764
  baseAgents = [...loaded.agents];
539
765
  state = withAgents({ ...state, warnings: [...state.warnings, ...loaded.warnings.map(sanitizeTuiText)] }, rebindPermissionMode(loaded.agents, state.permissionMode));
766
+ // One post-load preferred-chat seed when gates pass (no continuous retry).
767
+ if (!preferredChatModeSeeded && !isSmoke && !shuttingDown) {
768
+ preferredChatModeSeeded = true;
769
+ const seeded = applyPreferredChatModeSeed(state, preferredChatMode);
770
+ state = seeded.state;
771
+ if (seeded.effects.length > 0) {
772
+ await enqueueEffects(seeded.effects);
773
+ }
774
+ }
540
775
  emitDashE2eState({ kind: "providers-rendered" });
541
776
  dirty = true;
542
777
  paint();
@@ -8,6 +8,8 @@ import type { LifecycleAction } from "#cli/commands/dash/pane-controller.js";
8
8
  import { type DashWizardProvider } from "#cli/commands/dash/session-wizard.js";
9
9
  import { type BlueprintBrowserState, type TuiBlueprintRow } from "#cli/commands/dash/tui/browser.js";
10
10
  import type { BlueprintAction } from "#cli/commands/dash/blueprint-actions.js";
11
+ import type { ChatSession } from "#cli/commands/dash/tui/chat-bridge.js";
12
+ import { type ControlPlaneChatMode } from "#cli/commands/dash/tui/chat-mode.js";
11
13
  export type TuiWizardItem = {
12
14
  readonly kind: "current" | "existing" | "new";
13
15
  readonly path?: string;
@@ -17,6 +19,17 @@ export type TuiAppState = {
17
19
  readonly menu: MenuState;
18
20
  readonly agents: readonly AgentEntry[];
19
21
  readonly permissionMode: DashPermissionMode;
22
+ /** host-pane (default escape hatch) vs integrated-chat ACP surface */
23
+ readonly chatMode: ControlPlaneChatMode;
24
+ /** Active hermetic/real chat session when in integrated-chat (uses dash run identity). */
25
+ readonly chatSession: ChatSession | null;
26
+ /** Composer draft while integrated-chat overlay is active. */
27
+ readonly chatDraft: string;
28
+ /**
29
+ * Monotonic open generation: runtime open/send completions only apply when
30
+ * this still matches the generation that issued the effect (stale-cancel).
31
+ */
32
+ readonly chatOpenGeneration: number;
20
33
  readonly overlay: TuiOverlay;
21
34
  readonly status: string | null;
22
35
  readonly warnings: readonly string[];
@@ -83,6 +96,9 @@ export type TuiEffect = {
83
96
  } | {
84
97
  readonly kind: "palette-command";
85
98
  readonly id: PaletteCommandId;
99
+ } | {
100
+ readonly kind: "persist-chat-mode";
101
+ readonly mode: ControlPlaneChatMode;
86
102
  } | {
87
103
  readonly kind: "e2e-group";
88
104
  readonly agent: string;
@@ -100,6 +116,11 @@ export declare function createInitialTuiState(input: {
100
116
  readonly agents: readonly AgentEntry[];
101
117
  readonly permissionMode: DashPermissionMode;
102
118
  readonly hostAvailability?: readonly HostAvailability[];
119
+ /**
120
+ * Cold-start seed. Callers must only pass host-pane for preferred integrated-chat
121
+ * until host/run gates can open chat (see applyPreferredChatModeSeed).
122
+ */
123
+ readonly chatMode?: ControlPlaneChatMode;
103
124
  }): TuiAppState;
104
125
  export declare function reduceTuiChunk(state: TuiAppState, chunk: string): {
105
126
  readonly state: TuiAppState;
@@ -113,6 +134,26 @@ export declare function applyPaletteCommand(state: TuiAppState, id: PaletteComma
113
134
  readonly state: TuiAppState;
114
135
  readonly effects: readonly TuiEffect[];
115
136
  };
137
+ /**
138
+ * Toggle host-pane ↔ integrated-chat. Integrated chat requires an ACP-capable
139
+ * selected host and an existing dash run identity (no second registry).
140
+ */
141
+ export declare function toggleChatMode(state: TuiAppState): {
142
+ readonly state: TuiAppState;
143
+ readonly effects: readonly TuiEffect[];
144
+ };
145
+ /**
146
+ * After agents load: if preferred is integrated-chat and gates pass, apply the
147
+ * normal toggle path once. Never forces integrated-chat chrome without host/run.
148
+ */
149
+ export declare function applyPreferredChatModeSeed(state: TuiAppState, preferred: ControlPlaneChatMode): {
150
+ readonly state: TuiAppState;
151
+ readonly effects: readonly TuiEffect[];
152
+ };
153
+ /** Runtime applies hermetic/real bridge result into state + overlay. */
154
+ export declare function applyChatBridgeSession(state: TuiAppState, session: ChatSession | null, error?: string, options?: {
155
+ readonly expectedGeneration?: number;
156
+ }): TuiAppState;
116
157
  /** Open the worktree/permission wizard with a real wizard object (not overlay-only). */
117
158
  export declare function openLaunchWizard(state: TuiAppState, provider: DashWizardProvider): {
118
159
  readonly state: TuiAppState;