u-foo 2.3.32 → 2.4.1

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 (235) hide show
  1. package/README.md +157 -213
  2. package/README.zh-CN.md +151 -197
  3. package/SKILLS/ufoo/SKILL.md +8 -8
  4. package/bin/uagy.js +69 -0
  5. package/bin/uclaude.js +2 -2
  6. package/bin/ucode.js +4 -4
  7. package/bin/ucodex.js +2 -2
  8. package/bin/ufoo.js +5 -23
  9. package/modules/AGENTS.template.md +1 -1
  10. package/modules/bus/SKILLS/ubus/SKILL.md +35 -10
  11. package/package.json +5 -5
  12. package/scripts/chat-app-smoke.js +1 -1
  13. package/scripts/global-chat-switch-benchmark.js +5 -5
  14. package/scripts/ink-demo.js +1 -1
  15. package/scripts/ink-smoke.js +1 -1
  16. package/scripts/ucode-app-smoke.js +1 -1
  17. package/src/{agent → agents/activity}/activityDetector.js +39 -2
  18. package/src/{agent → agents/activity}/activityStatePublisher.js +1 -1
  19. package/src/{agent → agents/activity}/activityStateWriter.js +2 -2
  20. package/src/{agent → agents/activity}/activityTracker.js +1 -1
  21. package/src/agents/activity/index.js +8 -0
  22. package/src/{agent → agents/controller}/controllerToolExecutor.js +4 -4
  23. package/src/agents/controller/index.js +8 -0
  24. package/src/{agent → agents/controller}/loopObservability.js +2 -2
  25. package/src/{agent → agents/controller}/loopRuntime.js +1 -1
  26. package/src/{agent → agents/controller}/ufooAgent.js +9 -9
  27. package/src/agents/index.js +10 -0
  28. package/src/agents/internal/index.js +3 -0
  29. package/src/{agent → agents/internal}/internalRunner.js +45 -22
  30. package/src/agents/launch/agyConversation.js +159 -0
  31. package/src/agents/launch/index.js +12 -0
  32. package/src/{agent → agents/launch}/launchEnvironment.js +2 -3
  33. package/src/{agent → agents/launch}/launcher.js +64 -21
  34. package/src/{agent → agents/launch}/notifier.js +23 -12
  35. package/src/{agent → agents/launch}/ptyRunner.js +44 -12
  36. package/src/{agent → agents/launch}/ptyWrapper.js +2 -2
  37. package/src/{agent → agents/launch}/publisherRouting.js +1 -1
  38. package/src/{agent → agents/launch}/readyDetector.js +23 -0
  39. package/src/{agent → agents/prompts}/defaultBootstrap.js +63 -4
  40. package/src/{group/bootstrap.js → agents/prompts/groupBootstrap.js} +41 -6
  41. package/src/agents/prompts/index.js +8 -0
  42. package/src/{code/prompts → agents/prompts/native}/index.js +1 -1
  43. package/src/{agent → agents/providers}/claudeThreadProvider.js +1 -1
  44. package/src/{agent → agents/providers}/codexThreadProvider.js +1 -1
  45. package/src/{agent → agents/providers}/directAuthStatus.js +184 -1
  46. package/src/agents/providers/index.js +13 -0
  47. package/src/{agent → agents/providers}/upstreamTransport.js +2 -2
  48. package/src/{chat → app/chat}/agentSockets.js +1 -1
  49. package/src/{chat → app/chat}/commandExecutor.js +63 -26
  50. package/src/{chat → app/chat}/commands.js +119 -5
  51. package/src/{chat → app/chat}/daemonConnection.js +1 -1
  52. package/src/{chat → app/chat}/daemonMessageRouter.js +45 -3
  53. package/src/{chat → app/chat}/daemonReconnect.js +3 -0
  54. package/src/{chat → app/chat}/dashboardView.js +2 -1
  55. package/src/app/chat/index.js +6 -0
  56. package/src/{chat → app/chat}/inputSubmitHandler.js +4 -13
  57. package/src/{chat → app/chat}/internalAgentLogHistory.js +1 -1
  58. package/src/app/chat/multiWindow/index.js +268 -0
  59. package/src/app/chat/multiWindow/paneLayout.js +84 -0
  60. package/src/app/chat/multiWindow/paneManager.js +299 -0
  61. package/src/app/chat/multiWindow/renderer.js +384 -0
  62. package/src/app/chat/multiWindow/virtualTerminal.js +327 -0
  63. package/src/{chat → app/chat}/transport.js +1 -1
  64. package/src/{cli → app/cli}/ctxCoreCommands.js +3 -3
  65. package/src/{doctor/index.js → app/cli/features/doctor.js} +1 -1
  66. package/src/{init/index.js → app/cli/features/init.js} +14 -32
  67. package/src/{cli → app/cli}/groupCoreCommands.js +2 -2
  68. package/src/app/cli/index.js +9 -0
  69. package/src/{cli → app/cli}/onlineCoreCommands.js +5 -5
  70. package/src/{cli.js → app/cli/run.js} +59 -57
  71. package/src/app/index.js +6 -0
  72. package/src/code/agent.js +10 -9
  73. package/src/code/index.js +2 -0
  74. package/src/code/launcher/index.js +9 -0
  75. package/src/{agent → code/launcher}/ucode.js +7 -8
  76. package/src/{agent → code/launcher}/ucodeBootstrap.js +3 -3
  77. package/src/{agent → code/launcher}/ucodeBuild.js +2 -2
  78. package/src/{agent → code/launcher}/ucodeDoctor.js +2 -2
  79. package/src/{agent → code/launcher}/ucodeRuntimeConfig.js +1 -2
  80. package/src/code/nativeRunner.js +4 -4
  81. package/src/code/tui.js +3 -1454
  82. package/src/config.js +15 -2
  83. package/src/{bus → coordination/bus}/activate.js +2 -2
  84. package/src/{bus → coordination/bus}/daemon.js +15 -5
  85. package/src/coordination/bus/envelope.js +173 -0
  86. package/src/{bus → coordination/bus}/index.js +7 -3
  87. package/src/{bus → coordination/bus}/inject.js +11 -3
  88. package/src/{bus → coordination/bus}/message.js +1 -1
  89. package/src/coordination/bus/messageMeta.js +130 -0
  90. package/src/coordination/bus/promptEnvelope.js +65 -0
  91. package/src/{bus → coordination/bus}/shake.js +1 -1
  92. package/src/{bus → coordination/bus}/store.js +3 -3
  93. package/src/{bus → coordination/bus}/subscriber.js +2 -2
  94. package/src/{bus → coordination/bus}/utils.js +2 -2
  95. package/src/{history → coordination/history}/inputTimeline.js +5 -5
  96. package/src/coordination/index.js +10 -0
  97. package/src/{memory → coordination/memory}/historySearch.js +1 -1
  98. package/src/{memory → coordination/memory}/index.js +3 -3
  99. package/src/{report → coordination/report}/store.js +2 -2
  100. package/src/{status → coordination/status}/index.js +3 -3
  101. package/src/online/bridge.js +2 -2
  102. package/src/{controller → orchestration/controller}/flags.js +1 -1
  103. package/src/{controller → orchestration/controller}/gateRouter.js +1 -1
  104. package/src/orchestration/controller/index.js +10 -0
  105. package/src/{controller → orchestration/controller}/shadowGuard.js +1 -1
  106. package/src/orchestration/groups/bootstrap.js +3 -0
  107. package/src/orchestration/groups/index.js +10 -0
  108. package/src/orchestration/groups/promptProfiles.js +3 -0
  109. package/src/{group → orchestration/groups}/templates.js +1 -1
  110. package/src/{group → orchestration/groups}/validateTemplate.js +1 -1
  111. package/src/orchestration/index.js +7 -0
  112. package/src/orchestration/solo/index.js +3 -0
  113. package/src/{daemon → runtime/daemon}/agentProcessManager.js +1 -1
  114. package/src/{daemon → runtime/daemon}/cronOps.js +3 -2
  115. package/src/{daemon → runtime/daemon}/groupOrchestrator.js +26 -9
  116. package/src/{daemon → runtime/daemon}/index.js +105 -53
  117. package/src/{daemon → runtime/daemon}/ipcServer.js +1 -1
  118. package/src/{daemon → runtime/daemon}/nicknameScope.js +6 -3
  119. package/src/{daemon → runtime/daemon}/ops.js +48 -61
  120. package/src/{daemon → runtime/daemon}/promptLoop.js +1 -1
  121. package/src/{daemon → runtime/daemon}/promptRequest.js +7 -7
  122. package/src/runtime/daemon/providerSessions.js +230 -0
  123. package/src/{daemon → runtime/daemon}/reporting.js +4 -4
  124. package/src/{daemon → runtime/daemon}/run.js +4 -4
  125. package/src/{daemon → runtime/daemon}/soloBootstrap.js +7 -7
  126. package/src/{daemon → runtime/daemon}/status.js +5 -5
  127. package/src/runtime/index.js +10 -0
  128. package/src/{projects → runtime/projects}/registry.js +1 -1
  129. package/src/{terminal → runtime/terminal}/adapterRouter.js +0 -10
  130. package/src/{terminal → runtime/terminal}/adapters/internalAdapter.js +0 -4
  131. package/src/tools/handlers/common.js +1 -1
  132. package/src/tools/handlers/listAgents.js +1 -1
  133. package/src/tools/handlers/memory.js +3 -3
  134. package/src/tools/handlers/readBusSummary.js +1 -1
  135. package/src/tools/handlers/readOpenDecisions.js +1 -1
  136. package/src/tools/handlers/readProjectRegistry.js +1 -1
  137. package/src/tools/handlers/readPromptHistory.js +2 -2
  138. package/src/tools/schemaFixtures.js +1 -1
  139. package/src/ui/MIGRATION.md +42 -88
  140. package/src/ui/format/index.js +5 -28
  141. package/src/ui/index.js +1 -1
  142. package/src/ui/{components → ink}/ChatApp.js +812 -88
  143. package/src/ui/ink/DashboardBar.js +685 -0
  144. package/src/ui/{components → ink}/MultilineInput.js +230 -5
  145. package/src/ui/{components → ink}/UcodeApp.js +16 -7
  146. package/src/ui/{components → ink}/agentMirror.js +24 -19
  147. package/src/ui/{components → ink}/chatReducer.js +29 -7
  148. package/src/bus/messageMeta.js +0 -52
  149. package/src/chat/agentViewController.js +0 -1072
  150. package/src/chat/chatLogController.js +0 -138
  151. package/src/chat/completionController.js +0 -533
  152. package/src/chat/dashboardKeyController.js +0 -533
  153. package/src/chat/index.js +0 -2222
  154. package/src/chat/inputHistoryController.js +0 -135
  155. package/src/chat/inputListenerController.js +0 -470
  156. package/src/chat/layout.js +0 -186
  157. package/src/chat/pasteController.js +0 -81
  158. package/src/chat/statusLineController.js +0 -223
  159. package/src/chat/streamTracker.js +0 -156
  160. package/src/code/config +0 -0
  161. package/src/daemon/providerSessions.js +0 -488
  162. package/src/terminal/adapters/internalPtyAdapter.js +0 -42
  163. package/src/ui/components/DashboardBar.js +0 -417
  164. /package/src/{code/prompts → agents/prompts/native}/actions.js +0 -0
  165. /package/src/{code/prompts → agents/prompts/native}/efficiency.js +0 -0
  166. /package/src/{code/prompts → agents/prompts/native}/environment.js +0 -0
  167. /package/src/{code/prompts → agents/prompts/native}/identity.js +0 -0
  168. /package/src/{code/prompts → agents/prompts/native}/safety.js +0 -0
  169. /package/src/{code/prompts → agents/prompts/native}/sections.js +0 -0
  170. /package/src/{code/prompts → agents/prompts/native}/system.js +0 -0
  171. /package/src/{code/prompts → agents/prompts/native}/tasks.js +0 -0
  172. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/bash.js +0 -0
  173. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/edit.js +0 -0
  174. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/read.js +0 -0
  175. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/write.js +0 -0
  176. /package/src/{code/prompts → agents/prompts/native}/ufoo.js +0 -0
  177. /package/src/{group → agents/prompts}/promptProfiles.js +0 -0
  178. /package/src/{agent → agents/providers}/claudeEventTranslator.js +0 -0
  179. /package/src/{agent → agents/providers}/claudeOauthTokenReader.js +0 -0
  180. /package/src/{agent → agents/providers}/claudeSessionFiles.js +0 -0
  181. /package/src/{agent → agents/providers}/codexEventTranslator.js +0 -0
  182. /package/src/{agent → agents/providers}/credentials/claude.js +0 -0
  183. /package/src/{agent → agents/providers}/credentials/codex.js +0 -0
  184. /package/src/{agent → agents/providers}/credentials/index.js +0 -0
  185. /package/src/{chat → app/chat}/agentBar.js +0 -0
  186. /package/src/{chat → app/chat}/agentDirectory.js +0 -0
  187. /package/src/{chat → app/chat}/cronScheduler.js +0 -0
  188. /package/src/{chat → app/chat}/daemonCoordinator.js +0 -0
  189. /package/src/{chat → app/chat}/daemonTransport.js +0 -0
  190. /package/src/{chat → app/chat}/daemonTransportDefaults.js +0 -0
  191. /package/src/{chat → app/chat}/inputMath.js +0 -0
  192. /package/src/{chat → app/chat}/projectCloseController.js +0 -0
  193. /package/src/{chat → app/chat}/rawKeyMap.js +0 -0
  194. /package/src/{chat → app/chat}/settingsController.js +0 -0
  195. /package/src/{chat → app/chat}/shellCommand.js +0 -0
  196. /package/src/{chat → app/chat}/text.js +0 -0
  197. /package/src/{chat → app/chat}/transientAgentState.js +0 -0
  198. /package/src/{cli → app/cli}/busCoreCommands.js +0 -0
  199. /package/src/{skills/index.js → app/cli/features/skills.js} +0 -0
  200. /package/src/{bus → coordination/bus}/nickname.js +0 -0
  201. /package/src/{bus → coordination/bus}/queue.js +0 -0
  202. /package/src/{context → coordination/context}/decisions.js +0 -0
  203. /package/src/{context → coordination/context}/doctor.js +0 -0
  204. /package/src/{context → coordination/context}/index.js +0 -0
  205. /package/src/{context → coordination/context}/sync.js +0 -0
  206. /package/src/{ufoo → coordination/state}/agentRegistryDiagnostics.js +0 -0
  207. /package/src/{ufoo → coordination/state}/agentsStore.js +0 -0
  208. /package/src/{ufoo → coordination/state}/paths.js +0 -0
  209. /package/src/{controller → orchestration/controller}/launchRouting.js +0 -0
  210. /package/src/{controller → orchestration/controller}/routerFastPath.js +0 -0
  211. /package/src/{controller → orchestration/controller}/routerFinalize.js +0 -0
  212. /package/src/{group → orchestration/groups}/diagram.js +0 -0
  213. /package/src/{group → orchestration/groups}/templateValidation.js +0 -0
  214. /package/src/{solo → orchestration/solo}/commands.js +0 -0
  215. /package/src/{shared → runtime/contracts}/eventContract.js +0 -0
  216. /package/src/{shared → runtime/contracts}/ptySocketContract.js +0 -0
  217. /package/src/{providerapi → runtime/privacy}/redactor.js +0 -0
  218. /package/src/{providerapi → runtime/privacy}/shadowDiff.js +0 -0
  219. /package/src/{utils → runtime/process}/nodeExecutable.js +0 -0
  220. /package/src/{projects → runtime/projects}/identity.js +0 -0
  221. /package/src/{projects → runtime/projects}/index.js +0 -0
  222. /package/src/{projects → runtime/projects}/projectId.js +0 -0
  223. /package/src/{projects → runtime/projects}/runtimes.js +0 -0
  224. /package/src/{terminal → runtime/terminal}/adapterContract.js +0 -0
  225. /package/src/{terminal → runtime/terminal}/adapters/externalAdapter.js +0 -0
  226. /package/src/{terminal → runtime/terminal}/adapters/hostAdapter.js +0 -0
  227. /package/src/{terminal → runtime/terminal}/adapters/internalQueueAdapter.js +0 -0
  228. /package/src/{terminal → runtime/terminal}/adapters/terminalAdapter.js +0 -0
  229. /package/src/{terminal → runtime/terminal}/adapters/tmuxAdapter.js +0 -0
  230. /package/src/{terminal → runtime/terminal}/detect.js +0 -0
  231. /package/src/{terminal → runtime/terminal}/index.js +0 -0
  232. /package/src/{terminal → runtime/terminal}/iterm2.js +0 -0
  233. /package/src/{utils → ui/format}/banner.js +0 -0
  234. /package/src/{shared → ui/format}/markdownRenderer.js +0 -0
  235. /package/src/ui/{components → ink}/InkDemo.js +0 -0
@@ -1,11 +1,9 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * Ink-based chat TUI. Behaviourally equivalent to runChatBlessed in
5
- * src/chat/index.js but rendered via React + ink.
4
+ * Ink-based chat TUI rendered via React + ink.
6
5
  *
7
- * Activation: Ink is the default chat TUI. Set UFOO_TUI=blessed to use the
8
- * legacy blessed renderer while it remains available as a fallback.
6
+ * Activation: this is the only chat TUI.
9
7
  *
10
8
  * Coverage today: layout shell + dashboard bar (5 modes: projects, agents,
11
9
  * mode, provider, cron) + multiline editor + status line +
@@ -27,15 +25,15 @@ const { createDashboardBar } = require("./DashboardBar");
27
25
  const { reducer, createInitialState } = require("./chatReducer");
28
26
 
29
27
  function bootstrapEnvironment(projectRoot, options = {}) {
30
- // Mirror of the early section of runChatBlessed: ensure ufoo dirs exist
31
- // and that we have a stable subscriber ID. We deliberately keep the
28
+ // Ensure ufoo dirs exist and that we have a stable subscriber ID.
29
+ // We deliberately keep the
32
30
  // non-UI side-effects in their own helper so unit tests can assert on
33
31
  // them without importing ink.
34
- const { canonicalProjectRoot } = require("../../projects");
35
- const { getUfooPaths } = require("../../ufoo/paths");
36
- const UfooInit = require("../../init");
37
- const { isRunning } = require("../../daemon");
38
- const { startDaemon } = require("../../chat/transport");
32
+ const { canonicalProjectRoot } = require("../../runtime/projects");
33
+ const { getUfooPaths } = require("../../coordination/state/paths");
34
+ const UfooInit = require("../../app/cli/features/init");
35
+ const { isRunning } = require("../../runtime/daemon");
36
+ const { startDaemon } = require("../../app/chat/transport");
39
37
 
40
38
  const globalMode = options && options.globalMode === true;
41
39
  let activeProjectRoot = projectRoot;
@@ -67,7 +65,7 @@ function bootstrapEnvironment(projectRoot, options = {}) {
67
65
 
68
66
  async function ensureSubscriberId(projectRoot) {
69
67
  if (process.env.UFOO_SUBSCRIBER_ID) return;
70
- const { getUfooPaths } = require("../../ufoo/paths");
68
+ const { getUfooPaths } = require("../../coordination/state/paths");
71
69
  const sessionFile = path.join(getUfooPaths(projectRoot).ufooDir, "chat", "session-id.txt");
72
70
  const sessionDir = path.dirname(sessionFile);
73
71
  fs.mkdirSync(sessionDir, { recursive: true });
@@ -82,7 +80,7 @@ async function ensureSubscriberId(projectRoot) {
82
80
  }
83
81
 
84
82
  function inputHistoryFilePath(projectRoot, options = {}) {
85
- const { getUfooPaths } = require("../../ufoo/paths");
83
+ const { getUfooPaths } = require("../../coordination/state/paths");
86
84
  const { globalMode } = options || {};
87
85
  if (globalMode) {
88
86
  const os = require("os");
@@ -95,7 +93,7 @@ function inputHistoryFilePath(projectRoot, options = {}) {
95
93
  }
96
94
 
97
95
  function chatHistoryFilePath(projectRoot, options = {}) {
98
- const { getUfooPaths } = require("../../ufoo/paths");
96
+ const { getUfooPaths } = require("../../coordination/state/paths");
99
97
  const { globalMode } = options || {};
100
98
  if (globalMode) {
101
99
  const os = require("os");
@@ -109,13 +107,48 @@ function chatHistoryFilePath(projectRoot, options = {}) {
109
107
 
110
108
  function projectRootToId(projectRoot) {
111
109
  try {
112
- const { buildProjectId } = require("../../projects");
110
+ const { buildProjectId } = require("../../runtime/projects");
113
111
  return buildProjectId(projectRoot || process.cwd());
114
112
  } catch {
115
113
  return crypto.createHash("sha256").update(String(projectRoot || "")).digest("hex").slice(0, 16);
116
114
  }
117
115
  }
118
116
 
117
+ function resolveInjectSockPathForAgent(projectRoot, agentId) {
118
+ const { getUfooPaths } = require("../../coordination/state/paths");
119
+ const { subscriberToSafeName } = require("../../coordination/bus/utils");
120
+ const safeName = subscriberToSafeName(agentId);
121
+ return path.join(getUfooPaths(projectRoot || process.cwd()).busQueuesDir, safeName, "inject.sock");
122
+ }
123
+
124
+ function createInkMultiWindowToggle({
125
+ getController = () => null,
126
+ setActive = () => {},
127
+ logMessage = () => {},
128
+ } = {}) {
129
+ return () => {
130
+ const controller = typeof getController === "function" ? getController() : null;
131
+ if (!controller || typeof controller.enter !== "function" || typeof controller.exit !== "function") {
132
+ logMessage("error", "✗ Multi-window mode is not available");
133
+ return false;
134
+ }
135
+
136
+ if (typeof controller.isActive === "function" && controller.isActive()) {
137
+ controller.exit();
138
+ setActive(false);
139
+ return true;
140
+ }
141
+
142
+ setActive(true);
143
+ if (!controller.enter()) {
144
+ setActive(false);
145
+ logMessage("info", "No active agents for multi-window mode");
146
+ return false;
147
+ }
148
+ return true;
149
+ };
150
+ }
151
+
119
152
  function loadChatHistory(projectRoot, cap = 200, options = {}) {
120
153
  const file = chatHistoryFilePath(projectRoot, options);
121
154
  try {
@@ -205,6 +238,11 @@ function chatHistoryOptionsForScope({ globalMode = false, globalScope = "control
205
238
  }
206
239
 
207
240
  function getAgentLabelFor(meta, agentId) {
241
+ // Prefer the project-stripped display nickname so the dashboard never shows
242
+ // the scoped form ("neptune-builder"); fall back to the raw nickname (which
243
+ // may itself be unscoped depending on write path) and finally to a short
244
+ // form of the subscriber id.
245
+ if (meta && meta.display_nickname) return meta.display_nickname;
208
246
  if (meta && meta.nickname) return meta.nickname;
209
247
  if (!agentId) return "";
210
248
  const colon = agentId.indexOf(":");
@@ -224,7 +262,7 @@ function buildActiveAgentLabelMap(activeAgents = [], activeAgentMeta = new Map()
224
262
  }
225
263
 
226
264
  function resolveActiveAgentId(label, activeAgents = [], activeAgentMeta = new Map()) {
227
- const { resolveAgentId } = require("../../chat/agentDirectory");
265
+ const { resolveAgentId } = require("../../app/chat/agentDirectory");
228
266
  const metaMap = activeAgentMeta instanceof Map ? activeAgentMeta : new Map();
229
267
  return resolveAgentId({
230
268
  label,
@@ -258,7 +296,7 @@ function buildDirectBusSendRequest({
258
296
  };
259
297
  }
260
298
 
261
- const { parseAtTarget } = require("../../chat/commands");
299
+ const { parseAtTarget } = require("../../app/chat/commands");
262
300
  const atTarget = parseAtTarget(trimmed);
263
301
  if (!atTarget || !atTarget.message) return null;
264
302
  const target = resolveActiveAgentId(atTarget.target, activeAgents, activeAgentMeta) || atTarget.target;
@@ -284,7 +322,7 @@ function resolveAgentEnterRequest({
284
322
  ? settings.launchMode
285
323
  : "";
286
324
  const launchMode = String(meta.launch_mode || meta.launchMode || configuredLaunchMode || "").trim();
287
- const { createTerminalAdapterRouter } = require("../../terminal/adapterRouter");
325
+ const { createTerminalAdapterRouter } = require("../../runtime/terminal/adapterRouter");
288
326
  const adapter = createTerminalAdapterRouter().getAdapter({ launchMode, agentId: id, meta });
289
327
  const caps = adapter && adapter.capabilities ? adapter.capabilities : {};
290
328
 
@@ -299,8 +337,26 @@ function resolveAgentEnterRequest({
299
337
  };
300
338
  }
301
339
 
340
+ function resolveDashboardAgentEnterAction(enterRequest = {}) {
341
+ if (!enterRequest || typeof enterRequest !== "object") return "none";
342
+ if (enterRequest.useBus) return "internal";
343
+ if (enterRequest.supportsActivate) return "activate";
344
+ return "agent-view";
345
+ }
346
+
347
+ function buildEmptyProjectsDownActions(state = {}, displayAgents = []) {
348
+ if (!state.emptyProjectsDownArmed) {
349
+ return [{ type: "projects/armEmptyDown" }];
350
+ }
351
+ const actions = [{ type: "view/set", view: "agents" }];
352
+ if (displayAgents.length > 0 && state.selectedAgentIndex < 0) {
353
+ actions.push({ type: "agents/select", index: 0 });
354
+ }
355
+ return actions;
356
+ }
357
+
302
358
  function buildPromptIpcRequest(text) {
303
- const { IPC_REQUEST_TYPES } = require("../../shared/eventContract");
359
+ const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
304
360
  return {
305
361
  type: IPC_REQUEST_TYPES.PROMPT,
306
362
  text,
@@ -323,6 +379,46 @@ function normalizeInkLogLines(text = "") {
323
379
  return clean.split(/\r?\n/);
324
380
  }
325
381
 
382
+ function stripMarkdownDecorators(text = "") {
383
+ return String(text || "")
384
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
385
+ .replace(/`([^`]+)`/g, "$1");
386
+ }
387
+
388
+ function classifyChatLogLine(text = "") {
389
+ const raw = stripBlessedTags(text).replace(/\r/g, "");
390
+ const clean = stripMarkdownDecorators(raw);
391
+ const trimmed = clean.trim();
392
+ if (!trimmed) return { kind: "spacer", marker: " ", speaker: "", body: " " };
393
+ if (/^[█▀▄ ]+$/.test(trimmed) || /^ufoo chat/i.test(trimmed)) {
394
+ return { kind: "banner", marker: " ", speaker: "", body: clean };
395
+ }
396
+ if (/^───.*───$/.test(trimmed)) {
397
+ return { kind: "divider", marker: "─", speaker: "", body: clean };
398
+ }
399
+ if (/^(error:|✗|failed\b)/i.test(trimmed)) {
400
+ return { kind: "error", marker: "!", speaker: "error", body: clean.replace(/^(error:\s*)/i, "") };
401
+ }
402
+ if (/^(✓|✔|done\b|closed\b)/i.test(trimmed)) {
403
+ return { kind: "success", marker: "✓", speaker: "", body: clean.replace(/^[✓✔]\s*/, "") };
404
+ }
405
+ const dotMatch = clean.match(/^([^·:\n]{1,42})\s+·\s+(.*)$/);
406
+ if (dotMatch) {
407
+ const speaker = dotMatch[1].trim();
408
+ const lower = speaker.toLowerCase();
409
+ const kind = lower === "ufoo" ? "assistant" : "agent";
410
+ return { kind, marker: kind === "assistant" ? "◆" : "●", speaker, body: dotMatch[2] || " " };
411
+ }
412
+ const colonMatch = clean.match(/^([A-Za-z0-9_.:@/-]{1,42}):\s+(.*)$/);
413
+ if (colonMatch) {
414
+ return { kind: "agent", marker: "●", speaker: colonMatch[1], body: colonMatch[2] || " " };
415
+ }
416
+ if (/^(CHAT|UCODE)\s+·/i.test(trimmed)) {
417
+ return { kind: "meta", marker: "·", speaker: "", body: clean };
418
+ }
419
+ return { kind: "plain", marker: "│", speaker: "", body: clean };
420
+ }
421
+
326
422
  function createInkStreamState({
327
423
  dispatch,
328
424
  appendHistory,
@@ -661,7 +757,7 @@ function createInternalAgentViewState({
661
757
  } = {}) {
662
758
  let history = [];
663
759
  try {
664
- const { loadInternalAgentLogHistory } = require("../../chat/internalAgentLogHistory");
760
+ const { loadInternalAgentLogHistory } = require("../../app/chat/internalAgentLogHistory");
665
761
  history = loadInternalAgentLogHistory(projectRoot || process.cwd(), agentId, {
666
762
  maxEvents: 400,
667
763
  maxLines: 1000,
@@ -838,7 +934,7 @@ function resolveProjectRowRoot(row = {}) {
838
934
  const raw = String((row && (row.root || row.project_root)) || "").trim();
839
935
  if (!raw) return "";
840
936
  try {
841
- const { canonicalProjectRoot } = require("../../projects");
937
+ const { canonicalProjectRoot } = require("../../runtime/projects");
842
938
  return canonicalProjectRoot(raw);
843
939
  } catch {
844
940
  return path.resolve(raw);
@@ -851,7 +947,7 @@ function loadGlobalProjectRows(activeProjectRoot = "") {
851
947
  filterVisibleProjectRuntimes,
852
948
  isGlobalControllerProjectRoot,
853
949
  markProjectStopped,
854
- } = require("../../projects");
950
+ } = require("../../runtime/projects");
855
951
  let rows = listProjectRuntimes({ validate: true, cleanupTmp: true }) || [];
856
952
  for (const row of rows) {
857
953
  const status = String((row && row.status) || "").trim().toLowerCase();
@@ -874,8 +970,8 @@ function loadGlobalProjectRows(activeProjectRoot = "") {
874
970
  function readProjectAgentSnapshot(projectRoot = "") {
875
971
  if (!projectRoot) return { agents: [], metaMap: new Map() };
876
972
  try {
877
- const { buildStatus } = require("../../daemon/status");
878
- const { buildAgentMaps } = require("../../chat/agentDirectory");
973
+ const { buildStatus } = require("../../runtime/daemon/status");
974
+ const { buildAgentMaps } = require("../../app/chat/agentDirectory");
879
975
  const status = buildStatus(projectRoot);
880
976
  const activeIds = Array.isArray(status.active) ? status.active : [];
881
977
  const metaList = Array.isArray(status.active_meta) ? status.active_meta : [];
@@ -900,6 +996,49 @@ function readProjectAgentSnapshot(projectRoot = "") {
900
996
  }
901
997
  }
902
998
 
999
+ function isCJK(ch) {
1000
+ if (!ch) return false;
1001
+ const code = ch.codePointAt(0);
1002
+ return (code >= 0x2e80 && code <= 0x9fff) ||
1003
+ (code >= 0xac00 && code <= 0xd7af) ||
1004
+ (code >= 0xf900 && code <= 0xfaff) ||
1005
+ (code >= 0xfe30 && code <= 0xfe4f) ||
1006
+ (code >= 0x20000 && code <= 0x2fa1f);
1007
+ }
1008
+
1009
+ function inferStatusType(text = "", requestedType = "") {
1010
+ const type = String(requestedType || "").trim().toLowerCase();
1011
+ if (type === "done" || type === "success" || type === "error" || type === "idle") return type;
1012
+ const clean = stripBlessedTags(String(text || "")).trim();
1013
+ if (/^[✓✔]/.test(clean) || /\bdone\b/i.test(clean) || /\bprocessed\b/i.test(clean)) return "done";
1014
+ if (/^[✗!]/.test(clean) || /\berror\b/i.test(clean) || /\bfailed\b/i.test(clean)) return "error";
1015
+ return type || "typing";
1016
+ }
1017
+
1018
+ function isAnimatedStatusType(type = "") {
1019
+ const value = String(type || "").trim().toLowerCase();
1020
+ return value !== "done" && value !== "success" && value !== "error" && value !== "idle" && value !== "none";
1021
+ }
1022
+
1023
+ function inkKeyToRaw(input, key) {
1024
+ if (key.ctrl && input) {
1025
+ const code = input.charCodeAt(0) - 96;
1026
+ if (code >= 1 && code <= 26) return String.fromCharCode(code);
1027
+ return "";
1028
+ }
1029
+ if (key.return) return "\r";
1030
+ if (key.escape) return "\x1b";
1031
+ if (key.backspace || key.delete) return "\x7f";
1032
+ if (key.tab) return "\t";
1033
+ if (key.upArrow) return "\x1b[A";
1034
+ if (key.downArrow) return "\x1b[B";
1035
+ if (key.rightArrow) return "\x1b[C";
1036
+ if (key.leftArrow) return "\x1b[D";
1037
+ if (input && !key.meta) return input;
1038
+ if (key.meta && input) return `\x1b${input}`;
1039
+ return "";
1040
+ }
1041
+
903
1042
  function createChatApp({ React, ink, props, interactive = true }) {
904
1043
  const { useReducer, useEffect, useState, useCallback, useRef } = React;
905
1044
  const { Box, Text, Static, useInput, useApp, useStdout } = ink;
@@ -933,9 +1072,18 @@ function createChatApp({ React, ink, props, interactive = true }) {
933
1072
  const [spinnerTick, setSpinnerTick] = useState(0);
934
1073
  const [currentProjectRoot, setCurrentProjectRoot] = useState(props.activeProjectRoot || props.projectRoot || "");
935
1074
  const [internalAgentView, setInternalAgentView] = useState(() => createInternalAgentViewState());
1075
+ const [multiWindowActive, setMultiWindowActive] = useState(false);
1076
+ const [mwCursor, setMwCursor] = useState(0);
1077
+ const [mwTerminalFocused, setMwTerminalFocused] = useState(false);
1078
+ const mwTerminalFocusedRef = useRef(false);
1079
+ const mwLastInputRef = useRef({ char: "", time: 0 });
936
1080
  const stateRef = useRef(state);
1081
+ const sizeRef = useRef(size);
937
1082
  const currentProjectRootRef = useRef(currentProjectRoot);
938
1083
  const internalAgentViewRef = useRef(internalAgentView);
1084
+ const multiWindowControllerRef = useRef(null);
1085
+ const multiWindowChromeRef = useRef({ statusText: "", promptPrefix: "› ", draft: "", dashboardLines: [] });
1086
+ const multiWindowWatchedInternalAgentsRef = useRef(new Set());
939
1087
  const pendingRef = useRef(null);
940
1088
  const streamStateRef = useRef(null);
941
1089
  const historyScopeRef = useRef(null);
@@ -952,6 +1100,10 @@ function createChatApp({ React, ink, props, interactive = true }) {
952
1100
  stateRef.current = state;
953
1101
  }, [state]);
954
1102
 
1103
+ useEffect(() => {
1104
+ sizeRef.current = size;
1105
+ }, [size]);
1106
+
955
1107
  useEffect(() => {
956
1108
  currentProjectRootRef.current = currentProjectRoot;
957
1109
  }, [currentProjectRoot]);
@@ -975,7 +1127,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
975
1127
  type: "status/set",
976
1128
  payload: {
977
1129
  message: clean,
978
- type: options.type || "typing",
1130
+ type: inferStatusType(clean, options.type || "typing"),
979
1131
  showTimer: options.showTimer === true,
980
1132
  startedAt: options.startedAt || Date.now(),
981
1133
  },
@@ -1009,6 +1161,151 @@ function createChatApp({ React, ink, props, interactive = true }) {
1009
1161
  });
1010
1162
  }
1011
1163
 
1164
+ const getMultiWindowController = useCallback(() => {
1165
+ if (multiWindowControllerRef.current) return multiWindowControllerRef.current;
1166
+ const processStdout = stdout || (typeof process !== "undefined" ? process.stdout : null);
1167
+ if (!processStdout || typeof processStdout.write !== "function") return null;
1168
+
1169
+ const originalWrite = processStdout.write.bind(processStdout);
1170
+ const { createMultiWindowController } = require("../../app/chat/multiWindow");
1171
+ multiWindowControllerRef.current = createMultiWindowController({
1172
+ processStdout: { write: originalWrite, rows: processStdout.rows, columns: processStdout.columns },
1173
+ getRows: () => {
1174
+ const currentSize = sizeRef.current || {};
1175
+ return currentSize.rows || processStdout.rows || 24;
1176
+ },
1177
+ getCols: () => {
1178
+ const currentSize = sizeRef.current || {};
1179
+ return currentSize.cols || processStdout.columns || 80;
1180
+ },
1181
+ getInjectSockPath: (agentId) =>
1182
+ resolveInjectSockPathForAgent(currentProjectRootRef.current || props.projectRoot, agentId),
1183
+ getActiveAgents: () => {
1184
+ const current = stateRef.current || {};
1185
+ return Array.isArray(current.agents) ? current.agents : [];
1186
+ },
1187
+ getAgentPaneOptions: (agentId) => {
1188
+ const current = stateRef.current || {};
1189
+ const enterRequest = resolveAgentEnterRequest({
1190
+ agentId,
1191
+ projectRoot: currentProjectRootRef.current || props.projectRoot,
1192
+ activeAgentMeta: current.activeAgentMeta,
1193
+ settings: current.settings,
1194
+ });
1195
+ if (!enterRequest || !enterRequest.useBus) return { mode: "socket" };
1196
+ const metaMap = current.activeAgentMeta instanceof Map ? current.activeAgentMeta : new Map();
1197
+ const agentMeta = metaMap.get(agentId) || {};
1198
+ let initialLines = [];
1199
+ try {
1200
+ const { loadInternalAgentLogHistory } = require("../../app/chat/internalAgentLogHistory");
1201
+ initialLines = loadInternalAgentLogHistory(currentProjectRootRef.current || props.projectRoot, agentId, {
1202
+ maxEvents: 200,
1203
+ maxLines: 200,
1204
+ });
1205
+ } catch { initialLines = []; }
1206
+ return {
1207
+ mode: "internal",
1208
+ initialLines: [
1209
+ `ufoo internal agent · ${getAgentLabelFor(agentMeta, agentId)}`,
1210
+ `agent: ${agentId}`,
1211
+ "",
1212
+ ...initialLines,
1213
+ ],
1214
+ };
1215
+ },
1216
+ getChatLogLines: () => {
1217
+ const current = stateRef.current || {};
1218
+ return Array.isArray(current.logLines)
1219
+ ? current.logLines.map((item) => String((item && item.text) || ""))
1220
+ : [];
1221
+ },
1222
+ getStatusText: () => {
1223
+ const chrome = multiWindowChromeRef.current;
1224
+ return chrome ? chrome.statusText : "";
1225
+ },
1226
+ getPromptPrefix: () => {
1227
+ const chrome = multiWindowChromeRef.current;
1228
+ return chrome ? chrome.promptPrefix : "› ";
1229
+ },
1230
+ getCurrentDraft: () => {
1231
+ const chrome = multiWindowChromeRef.current;
1232
+ return chrome ? chrome.draft : "";
1233
+ },
1234
+ getCursorPos: () => {
1235
+ const chrome = multiWindowChromeRef.current;
1236
+ return chrome ? chrome.cursor : 0;
1237
+ },
1238
+ getCompletions: () => {
1239
+ const chrome = multiWindowChromeRef.current;
1240
+ if (!chrome || !chrome.completions || chrome.completions.length === 0) {
1241
+ return { items: [], index: -1, windowStart: 0, pageSize: 8 };
1242
+ }
1243
+ return {
1244
+ items: chrome.completions,
1245
+ index: chrome.completionIndex,
1246
+ windowStart: chrome.completionWindowStart,
1247
+ pageSize: chrome.completionPageSize || 8,
1248
+ };
1249
+ },
1250
+ getAgentLabel: (id) => {
1251
+ const current = stateRef.current || {};
1252
+ const metaMap = current.activeAgentMeta || new Map();
1253
+ return getAgentLabelFor(metaMap.get(id), id);
1254
+ },
1255
+ getInternalPaneInfo: (id) => {
1256
+ const current = stateRef.current || {};
1257
+ const metaMap = current.activeAgentMeta instanceof Map ? current.activeAgentMeta : new Map();
1258
+ const meta = metaMap.get(id) || {};
1259
+ const status = internalStatusLabel(meta.activity_state || meta.state || "");
1260
+ const detail = String(meta.activity_detail || meta.detail || meta.status_text || "").trim();
1261
+ return {
1262
+ status,
1263
+ detail,
1264
+ input: "",
1265
+ cursor: 0,
1266
+ };
1267
+ },
1268
+ getDashboardLines: () => {
1269
+ const chrome = multiWindowChromeRef.current;
1270
+ return chrome ? chrome.dashboardLines : [];
1271
+ },
1272
+ getTerminalFocused: () => mwTerminalFocusedRef.current,
1273
+ freezeScreen: (frozen) => {
1274
+ if (frozen) {
1275
+ processStdout.write = () => true;
1276
+ } else {
1277
+ processStdout.write = originalWrite;
1278
+ }
1279
+ },
1280
+ restoreTerminal: () => {
1281
+ const rows = processStdout.rows || 24;
1282
+ originalWrite(`\x1b[1;${rows}r`);
1283
+ originalWrite("\x1b[2J\x1b[H");
1284
+ },
1285
+ onInternalSubmit: (agentId, message) => {
1286
+ sendInternalAgentMessage(agentId, message);
1287
+ },
1288
+ onExit: () => {
1289
+ setMultiWindowActive(false);
1290
+ },
1291
+ });
1292
+ return multiWindowControllerRef.current;
1293
+ }, [props.projectRoot, stdout]);
1294
+
1295
+ const toggleMultiWindow = useCallback(() => createInkMultiWindowToggle({
1296
+ getController: getMultiWindowController,
1297
+ setActive: setMultiWindowActive,
1298
+ logMessage: logInkMessage,
1299
+ })(), [getMultiWindowController, logInkMessage]);
1300
+
1301
+ useEffect(() => () => {
1302
+ const controller = multiWindowControllerRef.current;
1303
+ if (controller && typeof controller.exit === "function") {
1304
+ try { controller.exit(); } catch { /* ignore */ }
1305
+ }
1306
+ multiWindowControllerRef.current = null;
1307
+ }, []);
1308
+
1012
1309
  useEffect(() => {
1013
1310
  internalAgentViewRef.current = internalAgentView;
1014
1311
  }, [internalAgentView]);
@@ -1034,7 +1331,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1034
1331
  const sendInternalAgentWatch = (agentId, enabled) => {
1035
1332
  if (!agentId || !props.daemonConnection || typeof props.daemonConnection.send !== "function") return;
1036
1333
  try {
1037
- const { IPC_REQUEST_TYPES } = require("../../shared/eventContract");
1334
+ const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
1038
1335
  props.daemonConnection.send({
1039
1336
  type: IPC_REQUEST_TYPES.BUS_WATCH,
1040
1337
  agent_id: agentId,
@@ -1043,10 +1340,49 @@ function createChatApp({ React, ink, props, interactive = true }) {
1043
1340
  } catch { /* ignore */ }
1044
1341
  };
1045
1342
 
1343
+ const reconcileMultiWindowInternalWatches = useCallback(() => {
1344
+ const current = stateRef.current || {};
1345
+ const agents = Array.isArray(current.agents) ? current.agents : [];
1346
+ const next = new Set();
1347
+ if (multiWindowActive) {
1348
+ for (const agentId of agents) {
1349
+ const enterRequest = resolveAgentEnterRequest({
1350
+ agentId,
1351
+ projectRoot: currentProjectRootRef.current || props.projectRoot,
1352
+ activeAgentMeta: current.activeAgentMeta,
1353
+ settings: current.settings,
1354
+ });
1355
+ if (enterRequest && enterRequest.useBus) next.add(agentId);
1356
+ }
1357
+ }
1358
+ const previous = multiWindowWatchedInternalAgentsRef.current;
1359
+ for (const agentId of next) {
1360
+ if (!previous.has(agentId)) sendInternalAgentWatch(agentId, true);
1361
+ }
1362
+ for (const agentId of previous) {
1363
+ if (!next.has(agentId)) sendInternalAgentWatch(agentId, false);
1364
+ }
1365
+ multiWindowWatchedInternalAgentsRef.current = next;
1366
+ }, [multiWindowActive, props.projectRoot, props.daemonConnection]);
1367
+
1368
+ useEffect(() => {
1369
+ if (!multiWindowActive) return;
1370
+ const controller = multiWindowControllerRef.current;
1371
+ if (!controller) return;
1372
+ reconcileMultiWindowInternalWatches();
1373
+ if (typeof controller.syncAgents === "function") controller.syncAgents();
1374
+ if (typeof controller.renderAll === "function") controller.renderAll();
1375
+ }, [multiWindowActive, state.agents, state.logLines, state.draft, state.status, size.cols, size.rows, mwCursor, state.focusMode, state.dashboardView, state.selectedAgentIndex, state.selectedProjectIndex, state.selectedModeIndex, state.selectedProviderIndex, state.selectedCronIndex, mwTerminalFocused, reconcileMultiWindowInternalWatches]);
1376
+
1377
+ useEffect(() => {
1378
+ if (multiWindowActive) return;
1379
+ reconcileMultiWindowInternalWatches();
1380
+ }, [multiWindowActive, reconcileMultiWindowInternalWatches]);
1381
+
1046
1382
  const sendInternalAgentMessage = (agentId, message) => {
1047
1383
  if (!agentId || !message || !props.daemonConnection || typeof props.daemonConnection.send !== "function") return;
1048
1384
  try {
1049
- const { IPC_REQUEST_TYPES } = require("../../shared/eventContract");
1385
+ const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
1050
1386
  props.daemonConnection.send({
1051
1387
  type: IPC_REQUEST_TYPES.BUS_SEND,
1052
1388
  target: agentId,
@@ -1070,6 +1406,74 @@ function createChatApp({ React, ink, props, interactive = true }) {
1070
1406
  return aliases.has(text);
1071
1407
  };
1072
1408
 
1409
+ const buildInternalAgentAliases = (agentId) => {
1410
+ const current = stateRef.current || {};
1411
+ const metaMap = current.activeAgentMeta instanceof Map ? current.activeAgentMeta : new Map();
1412
+ const meta = metaMap.get(agentId) || {};
1413
+ return new Set([
1414
+ agentId,
1415
+ meta.nickname,
1416
+ meta.scoped_nickname,
1417
+ meta.display_nickname,
1418
+ meta.fullId,
1419
+ ].filter(Boolean).map(String));
1420
+ };
1421
+
1422
+ const writeMultiWindowInternalEvent = useCallback((data = {}) => {
1423
+ const controller = multiWindowControllerRef.current;
1424
+ if (!multiWindowActive || !controller || typeof controller.writeToPane !== "function") return false;
1425
+ const watched = multiWindowWatchedInternalAgentsRef.current;
1426
+ if (!watched || watched.size === 0) return false;
1427
+
1428
+ let handled = false;
1429
+ for (const agentId of watched) {
1430
+ const aliases = buildInternalAgentAliases(agentId);
1431
+ const publisher = String(data.publisher || (data.event === "broadcast" ? "broadcast" : "bus"));
1432
+ const target = String(data.target || data.subscriber || "");
1433
+ const fromAgent = aliases.has(publisher);
1434
+ const toAgent = aliases.has(target) || aliases.has(String(data.subscriber || ""));
1435
+ if (!fromAgent && !toAgent) continue;
1436
+ if (data.silent) {
1437
+ handled = true;
1438
+ continue;
1439
+ }
1440
+ if (data.source === "chat-internal-agent-view" && toAgent && !fromAgent) {
1441
+ handled = true;
1442
+ continue;
1443
+ }
1444
+ if (data.event === "activity_state_changed") {
1445
+ const state = internalStatusLabel(data.state || data.activity_state || "");
1446
+ const detail = String(data.detail || (data.data && data.data.detail) || data.message || "").trim();
1447
+ controller.writeToPane(agentId, `\r\n[${state}${detail ? ` · ${detail}` : ""}]\r\n`);
1448
+ handled = true;
1449
+ continue;
1450
+ }
1451
+
1452
+ const { displayMessage, streamPayload } = parseInternalBusPayload(data.message || "");
1453
+ if (streamPayload) {
1454
+ if (!fromAgent) {
1455
+ handled = true;
1456
+ continue;
1457
+ }
1458
+ const delta = typeof streamPayload.delta === "string"
1459
+ ? streamPayload.delta.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\r/g, "\n")
1460
+ : "";
1461
+ if (delta) controller.writeToPane(agentId, delta);
1462
+ if (streamPayload.done) controller.writeToPane(agentId, "\r\n");
1463
+ handled = true;
1464
+ continue;
1465
+ }
1466
+ if (!displayMessage) {
1467
+ handled = true;
1468
+ continue;
1469
+ }
1470
+ const prefix = fromAgent ? "* " : "> ";
1471
+ controller.writeToPane(agentId, `${prefix}${displayMessage.replace(/\n/g, `\r\n `)}\r\n`);
1472
+ handled = true;
1473
+ }
1474
+ return handled;
1475
+ }, [multiWindowActive]);
1476
+
1073
1477
  const handleInternalStatus = (data = {}) => {
1074
1478
  const view = internalAgentViewRef.current;
1075
1479
  if (!view || !view.agentId) return;
@@ -1168,7 +1572,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1168
1572
 
1169
1573
  const requestDaemonStatus = useCallback(() => {
1170
1574
  try {
1171
- const { IPC_REQUEST_TYPES } = require("../../shared/eventContract");
1575
+ const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
1172
1576
  const conn = props.daemonConnection;
1173
1577
  if (conn && typeof conn.send === "function") conn.send({ type: IPC_REQUEST_TYPES.STATUS });
1174
1578
  } catch { /* ignore */ }
@@ -1177,7 +1581,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1177
1581
  const updateDashboardFromStatus = useCallback((data = {}) => {
1178
1582
  const activeIds = Array.isArray(data.active) ? data.active : [];
1179
1583
  const metaList = Array.isArray(data.active_meta) ? data.active_meta : [];
1180
- const { buildAgentMaps } = require("../../chat/agentDirectory");
1584
+ const { buildAgentMaps } = require("../../app/chat/agentDirectory");
1181
1585
  const { labelMap, metaMap } = buildAgentMaps(activeIds, metaList);
1182
1586
  const agentsForDispatch = activeIds.map((id) => {
1183
1587
  const meta = metaMap.get(id) || {};
@@ -1210,8 +1614,8 @@ function createChatApp({ React, ink, props, interactive = true }) {
1210
1614
  if (!conn || typeof conn.connect !== "function" || typeof setHandler !== "function") {
1211
1615
  return undefined;
1212
1616
  }
1213
- const { IPC_RESPONSE_TYPES } = require("../../shared/eventContract");
1214
- const { createDaemonMessageRouter } = require("../../chat/daemonMessageRouter");
1617
+ const { IPC_RESPONSE_TYPES } = require("../../runtime/contracts/eventContract");
1618
+ const { createDaemonMessageRouter } = require("../../app/chat/daemonMessageRouter");
1215
1619
  const streamState = streamStateRef.current;
1216
1620
  const router = createDaemonMessageRouter({
1217
1621
  escapeBlessed: (value) => String(value == null ? "" : value),
@@ -1227,7 +1631,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1227
1631
  });
1228
1632
  },
1229
1633
  enqueueBusStatus: (item = {}) => setStatusText(item.text || "Processing bus message", { type: "typing" }),
1230
- resolveBusStatus: (item = {}) => setStatusText(item.text || "Bus message processed", { type: "typing" }),
1634
+ resolveBusStatus: (item = {}) => setStatusText(item.text || "Bus message processed", { type: "done" }),
1231
1635
  getPending: () => pendingRef.current,
1232
1636
  setPending: (value) => { pendingRef.current = value || null; },
1233
1637
  resolveAgentDisplayName: (value) => {
@@ -1306,13 +1710,16 @@ function createChatApp({ React, ink, props, interactive = true }) {
1306
1710
  requestDaemonStatus();
1307
1711
  return;
1308
1712
  }
1713
+ if (msg.type === IPC_RESPONSE_TYPES.BUS) {
1714
+ writeMultiWindowInternalEvent(msg.data || {});
1715
+ }
1309
1716
  router.handleMessage(msg);
1310
1717
  });
1311
1718
  conn.connect();
1312
1719
  return () => {
1313
1720
  try { if (typeof conn.close === "function") conn.close(); } catch { /* ignore */ }
1314
1721
  };
1315
- }, [interactive, logInkMessage, requestDaemonStatus, setStatusText, updateDashboardFromStatus]);
1722
+ }, [interactive, logInkMessage, requestDaemonStatus, setStatusText, updateDashboardFromStatus, writeMultiWindowInternalEvent]);
1316
1723
 
1317
1724
  // commandExecutor wiring. The blessed implementation reuses this
1318
1725
  // module to dispatch every slash command (~30 callbacks). We adapt
@@ -1323,10 +1730,10 @@ function createChatApp({ React, ink, props, interactive = true }) {
1323
1730
  const commandExecutorRef = useRef(null);
1324
1731
  useEffect(() => {
1325
1732
  if (!interactive) return undefined;
1326
- const { createCommandExecutor } = require("../../chat/commandExecutor");
1327
- const { parseCommand: parseCmd } = require("../../chat/commands");
1328
- const { startDaemon: transportStartDaemon, stopDaemon: transportStopDaemon } = require("../../chat/transport");
1329
- const AgentActivator = require("../../bus/activate");
1733
+ const { createCommandExecutor } = require("../../app/chat/commandExecutor");
1734
+ const { parseCommand: parseCmd } = require("../../app/chat/commands");
1735
+ const { startDaemon: transportStartDaemon, stopDaemon: transportStopDaemon } = require("../../app/chat/transport");
1736
+ const AgentActivator = require("../../coordination/bus/activate");
1330
1737
  const conn = props.daemonConnection;
1331
1738
 
1332
1739
  try {
@@ -1338,6 +1745,31 @@ function createChatApp({ React, ink, props, interactive = true }) {
1338
1745
  logMessage: logInkMessage,
1339
1746
  resolveStatusLine: (text) => setStatusText(text),
1340
1747
  renderScreen: () => {},
1748
+ clearLog: () => {
1749
+ // Clear the persisted chat history file so reopening the chat
1750
+ // doesn't reload old messages.
1751
+ try {
1752
+ const root = currentProjectRootRef.current || props.projectRoot;
1753
+ const historyOptions = chatHistoryOptionsForScope({
1754
+ globalMode: props.globalMode,
1755
+ globalScope: (stateRef.current && stateRef.current.globalScope) || "controller",
1756
+ });
1757
+ const file = chatHistoryFilePath(root, historyOptions);
1758
+ if (file && fs.existsSync(file)) fs.writeFileSync(file, "");
1759
+ } catch { /* ignore */ }
1760
+ // ink redraws by erasing only as many lines as the last frame
1761
+ // emitted. After log/clear the next frame is shorter, so the
1762
+ // older log lines remain in the terminal scrollback. Wipe the
1763
+ // visible screen + scrollback first, then dispatch — ink will
1764
+ // repaint the (now small) frame onto a clean buffer.
1765
+ try {
1766
+ const out = (typeof process !== "undefined" && process.stdout) || null;
1767
+ if (out && out.isTTY && typeof out.write === "function") {
1768
+ out.write("\x1b[2J\x1b[3J\x1b[H");
1769
+ }
1770
+ } catch { /* ignore */ }
1771
+ dispatch({ type: "log/clear" });
1772
+ },
1341
1773
  getActiveAgents: () => (stateRef.current && stateRef.current.agents) || [],
1342
1774
  getActiveAgentMetaMap: () => (stateRef.current && stateRef.current.activeAgentMeta) || new Map(),
1343
1775
  getAgentLabel: (id) => {
@@ -1372,7 +1804,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1372
1804
  requestStatus: requestDaemonStatus,
1373
1805
  requestCron: (payload = {}) => {
1374
1806
  try {
1375
- const { IPC_REQUEST_TYPES } = require("../../shared/eventContract");
1807
+ const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
1376
1808
  if (conn && typeof conn.send === "function") {
1377
1809
  conn.send({ type: IPC_REQUEST_TYPES.CRON, ...payload });
1378
1810
  }
@@ -1399,12 +1831,13 @@ function createChatApp({ React, ink, props, interactive = true }) {
1399
1831
  }
1400
1832
  return switchProject(targetRoot, { focusInput: true });
1401
1833
  },
1834
+ toggleMultiWindow,
1402
1835
  });
1403
1836
  } catch (err) {
1404
1837
  dispatch({ type: "log/append", text: `Error: command executor unavailable (${err && err.message ? err.message : err})` });
1405
1838
  }
1406
1839
  return undefined;
1407
- }, [interactive, logInkMessage, requestDaemonStatus, setStatusText]);
1840
+ }, [interactive, logInkMessage, requestDaemonStatus, setStatusText, toggleMultiWindow]);
1408
1841
 
1409
1842
  // Periodic STATUS poll to keep the agents footer fresh, mirroring
1410
1843
  // blessed's requestStatus on a timer.
@@ -1412,7 +1845,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1412
1845
  if (!interactive) return undefined;
1413
1846
  const conn = props.daemonConnection;
1414
1847
  if (!conn || typeof conn.send !== "function") return undefined;
1415
- const { IPC_REQUEST_TYPES } = require("../../shared/eventContract");
1848
+ const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
1416
1849
  const tick = () => {
1417
1850
  try { conn.send({ type: IPC_REQUEST_TYPES.STATUS }); } catch { /* ignore */ }
1418
1851
  };
@@ -1448,7 +1881,8 @@ function createChatApp({ React, ink, props, interactive = true }) {
1448
1881
  useEffect(() => {
1449
1882
  const internalStatus = state.viewingAgentId ? internalStatusLabel(internalAgentView.status) : "ready";
1450
1883
  const internalActive = internalStatus !== "ready";
1451
- if ((!state.status.message || state.status.type === "none") && !internalActive) return undefined;
1884
+ const statusAnimated = state.status.message && isAnimatedStatusType(state.status.type);
1885
+ if ((!statusAnimated) && !internalActive) return undefined;
1452
1886
  const timer = setInterval(() => setSpinnerTick((t) => t + 1), 100);
1453
1887
  return () => clearInterval(timer);
1454
1888
  }, [state.status.message, state.status.type, state.viewingAgentId, internalAgentView.status]);
@@ -1498,7 +1932,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1498
1932
 
1499
1933
  const clearUfooAgentIdentity = useCallback(() => {
1500
1934
  try {
1501
- const { getUfooPaths } = require("../../ufoo/paths");
1935
+ const { getUfooPaths } = require("../../coordination/state/paths");
1502
1936
  const agentDir = getUfooPaths(props.projectRoot).agentDir;
1503
1937
  fs.rmSync(path.join(agentDir, "ufoo-agent.json"), { force: true });
1504
1938
  fs.rmSync(path.join(agentDir, "ufoo-agent.history.jsonl"), { force: true });
@@ -1526,7 +1960,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1526
1960
  const sendCronStop = useCallback((taskId) => {
1527
1961
  if (!taskId || !props.daemonConnection || typeof props.daemonConnection.send !== "function") return;
1528
1962
  try {
1529
- const { IPC_REQUEST_TYPES } = require("../../shared/eventContract");
1963
+ const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
1530
1964
  props.daemonConnection.send({ type: IPC_REQUEST_TYPES.CRON, operation: "stop", id: taskId });
1531
1965
  } catch (err) {
1532
1966
  dispatch({ type: "log/append", text: `Error: ${err && err.message ? err.message : err}` });
@@ -1538,7 +1972,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1538
1972
  if (!root) return { ok: false, error: "project root unavailable" };
1539
1973
  if (props.globalMode && props.env && typeof props.env.isRunning === "function" && !props.env.isRunning(root)) {
1540
1974
  try {
1541
- const { markProjectStopped } = require("../../projects");
1975
+ const { markProjectStopped } = require("../../runtime/projects");
1542
1976
  markProjectStopped(root);
1543
1977
  } catch { /* ignore */ }
1544
1978
  refreshGlobalProjects(currentProjectRoot);
@@ -1565,7 +1999,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1565
1999
  dispatch({ type: "log/appendMany", lines: persisted });
1566
2000
  }
1567
2001
  if (props.daemonCoordinator && typeof props.daemonCoordinator.switchProject === "function") {
1568
- const { socketPath } = require("../../daemon");
2002
+ const { socketPath } = require("../../runtime/daemon");
1569
2003
  const res = await Promise.resolve(props.daemonCoordinator.switchProject({
1570
2004
  projectRoot: root,
1571
2005
  sockPath: socketPath(root),
@@ -1586,7 +2020,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1586
2020
  refreshGlobalProjects(root);
1587
2021
  if (focusInput) dispatch({ type: "focus/set", mode: "input" });
1588
2022
  try {
1589
- const { IPC_REQUEST_TYPES } = require("../../shared/eventContract");
2023
+ const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
1590
2024
  if (props.daemonConnection && typeof props.daemonConnection.send === "function") {
1591
2025
  props.daemonConnection.send({ type: IPC_REQUEST_TYPES.STATUS });
1592
2026
  }
@@ -1610,7 +2044,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1610
2044
  const root = props.activeProjectRoot || props.projectRoot || "";
1611
2045
  if (!root) return { ok: false, error: "controller root unavailable" };
1612
2046
  if (props.daemonCoordinator && typeof props.daemonCoordinator.switchProject === "function") {
1613
- const { socketPath } = require("../../daemon");
2047
+ const { socketPath } = require("../../runtime/daemon");
1614
2048
  const res = await Promise.resolve(props.daemonCoordinator.switchProject({
1615
2049
  projectRoot: root,
1616
2050
  sockPath: socketPath(root),
@@ -1643,7 +2077,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1643
2077
  const snapshot = readProjectAgentSnapshot(root);
1644
2078
  dispatch({ type: "agents/set", list: snapshot.agents.map((id) => snapshot.metaMap.get(id) || { fullId: id }) });
1645
2079
  try {
1646
- const { IPC_REQUEST_TYPES } = require("../../shared/eventContract");
2080
+ const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
1647
2081
  if (props.daemonConnection && typeof props.daemonConnection.send === "function") {
1648
2082
  props.daemonConnection.send({ type: IPC_REQUEST_TYPES.STATUS });
1649
2083
  }
@@ -1682,7 +2116,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1682
2116
  dispatch({ type: "log/append", text: "Error: project switching unavailable" });
1683
2117
  return;
1684
2118
  }
1685
- const { socketPath } = require("../../daemon");
2119
+ const { socketPath } = require("../../runtime/daemon");
1686
2120
  const switched = await Promise.resolve(props.daemonCoordinator.switchProject({
1687
2121
  projectRoot: fallback,
1688
2122
  sockPath: socketPath(fallback),
@@ -1697,8 +2131,8 @@ function createChatApp({ React, ink, props, interactive = true }) {
1697
2131
  dispatch({ type: "scope/set", scope: "project" });
1698
2132
  }
1699
2133
 
1700
- const { stopDaemon } = require("../../chat/transport");
1701
- const { isRunning } = require("../../daemon");
2134
+ const { stopDaemon } = require("../../app/chat/transport");
2135
+ const { isRunning } = require("../../runtime/daemon");
1702
2136
  stopDaemon(targetRoot, { source: `ink-project-close:${targetRoot}` });
1703
2137
  refreshGlobalProjects(activeRoot);
1704
2138
  if (isRunning(targetRoot)) {
@@ -1726,12 +2160,12 @@ function createChatApp({ React, ink, props, interactive = true }) {
1726
2160
  if (!switched || switched.ok !== true) return;
1727
2161
  }
1728
2162
  dispatch({ type: "draft/clear" });
1729
- const { createInputSubmitHandler } = require("../../chat/inputSubmitHandler");
1730
- const { parseAtTarget } = require("../../chat/commands");
1731
- const { resolveAgentId } = require("../../chat/agentDirectory");
1732
- const { subscriberToSafeName } = require("../../bus/utils");
1733
- const { getUfooPaths } = require("../../ufoo/paths");
1734
- const { createTerminalAdapterRouter } = require("../../terminal/adapterRouter");
2163
+ const { createInputSubmitHandler } = require("../../app/chat/inputSubmitHandler");
2164
+ const { parseAtTarget } = require("../../app/chat/commands");
2165
+ const { resolveAgentId } = require("../../app/chat/agentDirectory");
2166
+ const { subscriberToSafeName } = require("../../coordination/bus/utils");
2167
+ const { getUfooPaths } = require("../../coordination/state/paths");
2168
+ const { createTerminalAdapterRouter } = require("../../runtime/terminal/adapterRouter");
1735
2169
  const submitState = {};
1736
2170
  Object.defineProperties(submitState, {
1737
2171
  targetAgent: {
@@ -1814,7 +2248,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1814
2248
  return createTerminalAdapterRouter().getAdapter({ launchMode, agentId, meta });
1815
2249
  },
1816
2250
  activateAgent: async (agentId) => {
1817
- const AgentActivator = require("../../bus/activate");
2251
+ const AgentActivator = require("../../coordination/bus/activate");
1818
2252
  const activator = new AgentActivator(currentProjectRoot || props.projectRoot);
1819
2253
  await activator.activate(agentId);
1820
2254
  },
@@ -1831,7 +2265,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1831
2265
  renderScreen: () => {},
1832
2266
  getShellCwd: () => activeChatHistoryRoot,
1833
2267
  runShellCommand: async (shellCommand, options = {}) => {
1834
- const { runShellCommand } = require("../../chat/shellCommand");
2268
+ const { runShellCommand } = require("../../app/chat/shellCommand");
1835
2269
  return runShellCommand(shellCommand, options);
1836
2270
  },
1837
2271
  });
@@ -1927,7 +2361,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1927
2361
  // with "/" or "@". Tab/Enter accept the highlighted entry, ↑↓ move the
1928
2362
  // selection. The list reuses the pure buildCompletions helper from
1929
2363
  // src/ui/format so jest can pin the source list without rendering ink.
1930
- const { COMMAND_REGISTRY, COMMAND_TREE } = require("../../chat/commands");
2364
+ const { COMMAND_REGISTRY, COMMAND_TREE } = require("../../app/chat/commands");
1931
2365
  const agentLabels = displayAgents.map((id) =>
1932
2366
  getAgentLabelFor(displayAgentMeta.get(id), id)
1933
2367
  );
@@ -1938,7 +2372,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
1938
2372
  if (!dynamicSourcesRef.current) {
1939
2373
  const sources = { groupTemplates: [], soloProfiles: [] };
1940
2374
  try {
1941
- const { loadTemplateRegistry } = require("../../group/templates");
2375
+ const { loadTemplateRegistry } = require("../../orchestration/groups/templates");
1942
2376
  const reg = typeof loadTemplateRegistry === "function" ? loadTemplateRegistry(props.projectRoot) : null;
1943
2377
  if (reg && Array.isArray(reg.templates)) {
1944
2378
  sources.groupTemplates = reg.templates.map((item) => ({
@@ -1950,8 +2384,8 @@ function createChatApp({ React, ink, props, interactive = true }) {
1950
2384
  }
1951
2385
  } catch { /* ignore */ }
1952
2386
  try {
1953
- const { loadPromptProfileRegistry } = require("../../group/promptProfiles");
1954
- const { buildPromptProfileCandidates } = require("../../solo/commands");
2387
+ const { loadPromptProfileRegistry } = require("../../orchestration/groups/promptProfiles");
2388
+ const { buildPromptProfileCandidates } = require("../../orchestration/solo/commands");
1955
2389
  const reg = typeof loadPromptProfileRegistry === "function" ? loadPromptProfileRegistry(props.projectRoot) : null;
1956
2390
  if (reg && typeof buildPromptProfileCandidates === "function") {
1957
2391
  sources.soloProfiles = buildPromptProfileCandidates(reg) || [];
@@ -1995,6 +2429,9 @@ function createChatApp({ React, ink, props, interactive = true }) {
1995
2429
  setCompletionWindowStart(Math.max(0, completions.length - POPUP_PAGE_SIZE));
1996
2430
  }
1997
2431
  }, [completions.length, completionIndex, completionWindowStart]);
2432
+ useEffect(() => {
2433
+ if (multiWindowActive) setMwCursor(String(state.draft || "").length);
2434
+ }, [draftVersion]);
1998
2435
  const completionsOpen = completions.length > 0 && state.draft !== completionSuppressedDraft;
1999
2436
  const acceptCompletion = useCallback(() => {
2000
2437
  if (!completionsOpen) return false;
@@ -2028,6 +2465,18 @@ function createChatApp({ React, ink, props, interactive = true }) {
2028
2465
  };
2029
2466
  };
2030
2467
 
2468
+ const activateExternalAgent = (agentId) => {
2469
+ const id = String(agentId || "").trim();
2470
+ if (!id) return;
2471
+ try {
2472
+ const AgentActivator = require("../../coordination/bus/activate");
2473
+ const activator = new AgentActivator(currentProjectRoot || props.projectRoot);
2474
+ void activator.activate(id);
2475
+ } catch (err) {
2476
+ logInkMessage("error", `✗ Failed to activate ${id}: ${err && err.message ? err.message : "unknown error"}`);
2477
+ }
2478
+ };
2479
+
2031
2480
  const enterInternalAgentView = (enterRequest = {}) => {
2032
2481
  const agentId = String(enterRequest.agentId || "").trim();
2033
2482
  if (!agentId) return;
@@ -2049,7 +2498,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
2049
2498
  dispatch({ type: "agents/clearTarget" });
2050
2499
  sendInternalAgentWatch(agentId, true);
2051
2500
  try {
2052
- const { IPC_REQUEST_TYPES } = require("../../shared/eventContract");
2501
+ const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
2053
2502
  if (props.daemonConnection && typeof props.daemonConnection.send === "function") {
2054
2503
  props.daemonConnection.send({ type: IPC_REQUEST_TYPES.STATUS });
2055
2504
  }
@@ -2120,10 +2569,19 @@ function createChatApp({ React, ink, props, interactive = true }) {
2120
2569
  return true;
2121
2570
  }
2122
2571
  const payload = buildAgentEnterPayload(agentId);
2123
- if (payload && payload.useBus) {
2572
+ const action = resolveDashboardAgentEnterAction(payload);
2573
+ if (action === "internal") {
2124
2574
  enterInternalAgentView(payload);
2125
2575
  return true;
2126
2576
  }
2577
+ if (action === "activate") {
2578
+ if (state.viewingAgentId) sendInternalAgentWatch(state.viewingAgentId, false);
2579
+ dispatch({ type: "agentView/exit" });
2580
+ dispatch({ type: "view/set", view: "agents" });
2581
+ dispatch({ type: "focus/set", mode: "input" });
2582
+ activateExternalAgent(agentId);
2583
+ return true;
2584
+ }
2127
2585
  if (payload && typeof props.requestEnterAgentView === "function") {
2128
2586
  if (state.viewingAgentId) sendInternalAgentWatch(state.viewingAgentId, false);
2129
2587
  props.requestEnterAgentView(agentId, payload);
@@ -2136,7 +2594,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
2136
2594
  const agentId = displayAgents[currentIndex - 1];
2137
2595
  if (!agentId) return true;
2138
2596
  try {
2139
- const { IPC_REQUEST_TYPES } = require("../../shared/eventContract");
2597
+ const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
2140
2598
  if (props.daemonConnection && typeof props.daemonConnection.send === "function") {
2141
2599
  props.daemonConnection.send({ type: IPC_REQUEST_TYPES.CLOSE_AGENT, agent_id: agentId });
2142
2600
  }
@@ -2248,6 +2706,58 @@ function createChatApp({ React, ink, props, interactive = true }) {
2248
2706
  };
2249
2707
 
2250
2708
  useInput((input, key) => {
2709
+ if (multiWindowActive) {
2710
+ const controller = multiWindowControllerRef.current;
2711
+ const termFocused = mwTerminalFocusedRef.current;
2712
+ if (key.ctrl && input === "q") {
2713
+ if (controller && typeof controller.handleKey === "function") {
2714
+ controller.handleKey({ name: "q", ctrl: true, sequence: "" });
2715
+ }
2716
+ mwTerminalFocusedRef.current = false;
2717
+ setMwTerminalFocused(false);
2718
+ return;
2719
+ }
2720
+ if (key.ctrl && input === "w") {
2721
+ const agents = controller ? controller.getAgentIds() : [];
2722
+ if (agents.length === 0) return;
2723
+ if (!termFocused) {
2724
+ if (controller) controller.focusAgent(agents[0]);
2725
+ mwTerminalFocusedRef.current = true;
2726
+ setMwTerminalFocused(true);
2727
+ } else {
2728
+ const current = controller ? controller.getFocused() : null;
2729
+ const idx = current ? agents.indexOf(current) : -1;
2730
+ if (idx >= 0 && idx < agents.length - 1) {
2731
+ controller.focusAgent(agents[idx + 1]);
2732
+ } else {
2733
+ mwTerminalFocusedRef.current = false;
2734
+ setMwTerminalFocused(false);
2735
+ if (controller) controller.focusAgent(agents[0]);
2736
+ }
2737
+ }
2738
+ if (controller) controller.renderAll();
2739
+ return;
2740
+ }
2741
+ if (termFocused && controller && typeof controller.sendInput === "function") {
2742
+ const now = Date.now();
2743
+ const last = mwLastInputRef.current;
2744
+ if (input === " " && !key.ctrl && !key.meta && isCJK(last.char) && now - last.time < 150) {
2745
+ return;
2746
+ }
2747
+ const raw = inkKeyToRaw(input, key);
2748
+ if (raw) {
2749
+ const cleaned = raw.length > 1 && /[⺀-鿿가-힯豈-﫿︰-﹏]/.test(raw)
2750
+ ? raw.replace(/ +$/, "")
2751
+ : raw;
2752
+ if (cleaned) {
2753
+ controller.sendInput(cleaned);
2754
+ const lastChar = cleaned[cleaned.length - 1];
2755
+ mwLastInputRef.current = { char: lastChar, time: now };
2756
+ }
2757
+ }
2758
+ return;
2759
+ }
2760
+ }
2251
2761
  if (key.ctrl && input === "c") { exit(); return; }
2252
2762
  if (key.ctrl && input === "o") { dispatch({ type: "merge/expand" }); return; }
2253
2763
  if (state.viewingAgentId) {
@@ -2323,14 +2833,33 @@ function createChatApp({ React, ink, props, interactive = true }) {
2323
2833
  && state.agentSelectionMode
2324
2834
  && state.selectedAgentIndex >= 0) {
2325
2835
  const agentId = displayAgents[state.selectedAgentIndex];
2326
- if (agentId && typeof props.requestEnterAgentView === "function") {
2836
+ if (agentId && multiWindowActive) {
2837
+ const controller = multiWindowControllerRef.current;
2838
+ if (controller && typeof controller.focusAgent === "function") {
2839
+ controller.focusAgent(agentId);
2840
+ }
2841
+ setMwTerminalFocused(true);
2842
+ mwTerminalFocusedRef.current = true;
2843
+ dispatch({ type: "focus/set", mode: "input" });
2844
+ return;
2845
+ }
2846
+ if (agentId) {
2327
2847
  const enterPayload = buildAgentEnterPayload(agentId);
2328
- if (enterPayload.useBus) {
2848
+ const action = resolveDashboardAgentEnterAction(enterPayload);
2849
+ if (action === "internal") {
2329
2850
  enterInternalAgentView(enterPayload);
2330
2851
  return;
2331
2852
  }
2332
- props.requestEnterAgentView(agentId, enterPayload);
2333
- exit();
2853
+ if (action === "activate") {
2854
+ dispatch({ type: "agents/clearTarget" });
2855
+ dispatch({ type: "focus/set", mode: "input" });
2856
+ activateExternalAgent(agentId);
2857
+ return;
2858
+ }
2859
+ if (typeof props.requestEnterAgentView === "function") {
2860
+ props.requestEnterAgentView(agentId, enterPayload);
2861
+ exit();
2862
+ }
2334
2863
  }
2335
2864
  return;
2336
2865
  }
@@ -2339,10 +2868,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
2339
2868
  // Ctrl+X stops it.
2340
2869
  if (state.focusMode === "dashboard" && state.dashboardView === "projects" && state.projects.length === 0) {
2341
2870
  if (key.downArrow) {
2342
- dispatch({ type: "view/set", view: "agents" });
2343
- if (displayAgents.length > 0 && state.selectedAgentIndex < 0) {
2344
- dispatch({ type: "agents/select", index: 0 });
2345
- }
2871
+ for (const action of buildEmptyProjectsDownActions(state, displayAgents)) dispatch(action);
2346
2872
  return;
2347
2873
  }
2348
2874
  if (key.upArrow || key.return || key.escape) {
@@ -2444,7 +2970,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
2444
2970
 
2445
2971
  // Dashboard focus on agents/mode/provider/cron — ↑↓ flip between
2446
2972
  // sibling views, ←/→ pick within the active view, Esc returns to
2447
- // the input. Mirrors the blessed handlers in dashboardKeyController.
2973
+ // the input.
2448
2974
  if (state.focusMode === "dashboard"
2449
2975
  && (state.dashboardView === "agents"
2450
2976
  || state.dashboardView === "mode"
@@ -2470,7 +2996,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
2470
2996
  if (state.selectedAgentIndex >= 0 && state.selectedAgentIndex < displayAgents.length) {
2471
2997
  const agentId = displayAgents[state.selectedAgentIndex];
2472
2998
  try {
2473
- const { IPC_REQUEST_TYPES } = require("../../shared/eventContract");
2999
+ const { IPC_REQUEST_TYPES } = require("../../runtime/contracts/eventContract");
2474
3000
  if (props.daemonConnection && typeof props.daemonConnection.send === "function") {
2475
3001
  props.daemonConnection.send({ type: IPC_REQUEST_TYPES.CLOSE_AGENT, agent_id: agentId });
2476
3002
  }
@@ -2573,16 +3099,188 @@ function createChatApp({ React, ink, props, interactive = true }) {
2573
3099
  if (key.return) { dispatch({ type: "focus/set", mode: "input" }); return; }
2574
3100
  }
2575
3101
  }
3102
+
3103
+ // Multi-window typing handler: replicates MultilineInput's key handling
3104
+ // so both modes share the same input behavior.
3105
+ if (multiWindowActive && state.focusMode !== "dashboard") {
3106
+ const intercepted = completionsOpen && (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.return);
3107
+ if (intercepted) return;
3108
+ if (key.return) {
3109
+ if (key.meta) {
3110
+ const before = (state.draft || "").slice(0, mwCursor);
3111
+ const after = (state.draft || "").slice(mwCursor);
3112
+ dispatch({ type: "draft/set", value: `${before}\n${after}` });
3113
+ setMwCursor(mwCursor + 1);
3114
+ return;
3115
+ }
3116
+ const value = String(state.draft || "").trim();
3117
+ if (value) { submit(value); setMwCursor(0); }
3118
+ return;
3119
+ }
3120
+ if (key.escape) {
3121
+ if (state.agentSelectionMode) { dispatch({ type: "agents/clearTarget" }); return; }
3122
+ if (state.draft) { dispatch({ type: "draft/clear" }); setMwCursor(0); }
3123
+ else if (state.status && state.status.message) { dispatch({ type: "status/idle" }); }
3124
+ return;
3125
+ }
3126
+ if (key.ctrl) {
3127
+ if (input === "a") { setMwCursor(fmt.moveCursorToVisualLineBoundary({ cursorPos: mwCursor, inputValue: state.draft || "", width: inputWidth, boundary: "start" })); return; }
3128
+ if (input === "e") { setMwCursor(fmt.moveCursorToVisualLineBoundary({ cursorPos: mwCursor, inputValue: state.draft || "", width: inputWidth, boundary: "end" })); return; }
3129
+ if (input === "b") { setMwCursor(fmt.moveCursorHorizontally(mwCursor, state.draft || "", "left")); return; }
3130
+ if (input === "f") { setMwCursor(fmt.moveCursorHorizontally(mwCursor, state.draft || "", "right")); return; }
3131
+ if (input === "d") { const d = state.draft || ""; if (mwCursor < d.length) { dispatch({ type: "draft/set", value: d.slice(0, mwCursor) + d.slice(mwCursor + 1) }); } return; }
3132
+ if (input === "h") { const d = state.draft || ""; if (mwCursor > 0) { dispatch({ type: "draft/set", value: d.slice(0, mwCursor - 1) + d.slice(mwCursor) }); setMwCursor(mwCursor - 1); } return; }
3133
+ if (input === "k") { dispatch({ type: "draft/set", value: (state.draft || "").slice(0, mwCursor) }); return; }
3134
+ if (input === "u") { dispatch({ type: "draft/set", value: (state.draft || "").slice(mwCursor) }); setMwCursor(0); return; }
3135
+ if (input === "w") { const r = fmt.deleteWordBeforeCursor(state.draft || "", mwCursor); dispatch({ type: "draft/set", value: r.value }); setMwCursor(r.cursorPos); return; }
3136
+ return;
3137
+ }
3138
+ if (key.meta) {
3139
+ if (input === "b") { setMwCursor(fmt.moveCursorByWord(state.draft || "", mwCursor, "backward")); return; }
3140
+ if (input === "f") { setMwCursor(fmt.moveCursorByWord(state.draft || "", mwCursor, "forward")); return; }
3141
+ if (input === "d") { const end = fmt.moveCursorByWord(state.draft || "", mwCursor, "forward"); const d = state.draft || ""; dispatch({ type: "draft/set", value: d.slice(0, mwCursor) + d.slice(end) }); return; }
3142
+ }
3143
+ if (key.backspace || key.delete) {
3144
+ const d = state.draft || "";
3145
+ if (key.meta || key.ctrl) { const r = fmt.deleteWordBeforeCursor(d, mwCursor); dispatch({ type: "draft/set", value: r.value }); setMwCursor(r.cursorPos); }
3146
+ else if (mwCursor > 0) { dispatch({ type: "draft/set", value: d.slice(0, mwCursor - 1) + d.slice(mwCursor) }); setMwCursor(mwCursor - 1); }
3147
+ return;
3148
+ }
3149
+ if (key.leftArrow) {
3150
+ if (!state.draft && typeof onArrowSideAtEmpty === "function") { onArrowSideAtEmpty("left"); return; }
3151
+ setMwCursor(fmt.moveCursorHorizontally(mwCursor, state.draft || "", "left"));
3152
+ return;
3153
+ }
3154
+ if (key.rightArrow) {
3155
+ if (!state.draft && typeof onArrowSideAtEmpty === "function") { onArrowSideAtEmpty("right"); return; }
3156
+ setMwCursor(fmt.moveCursorHorizontally(mwCursor, state.draft || "", "right"));
3157
+ return;
3158
+ }
3159
+ if (key.upArrow) { onArrowUpAtTop(); return; }
3160
+ if (key.downArrow) { onArrowDownAtBottom(state.draft); return; }
3161
+ if (input && !key.ctrl && !key.meta) {
3162
+ const filtered = input.replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]/g, "");
3163
+ if (filtered) {
3164
+ const d = state.draft || "";
3165
+ dispatch({ type: "draft/set", value: d.slice(0, mwCursor) + filtered + d.slice(mwCursor) });
3166
+ setMwCursor(mwCursor + filtered.length);
3167
+ }
3168
+ return;
3169
+ }
3170
+ return;
3171
+ }
2576
3172
  }, { isActive: interactive });
2577
3173
 
2578
3174
  const statusText = computeStatusText(state.status, spinnerTick);
2579
3175
  const inputWidth = Math.max(20, (size.cols || 80) - 4);
2580
3176
  const promptPrefix = (() => {
2581
3177
  const projectPrefix = inCommittedProjectScope && currentProjectLabel ? `${currentProjectLabel} ` : "";
2582
- if (targetAgentLabel) return `${projectPrefix}›@${targetAgentLabel} `;
3178
+ const visibleTargetAgentLabel = state.focusMode === "dashboard" && state.dashboardView !== "agents"
3179
+ ? ""
3180
+ : targetAgentLabel;
3181
+ if (visibleTargetAgentLabel) return `${projectPrefix}›@${visibleTargetAgentLabel} `;
2583
3182
  return `${projectPrefix}› `;
2584
3183
  })();
2585
3184
 
3185
+ if (multiWindowActive) {
3186
+ const { renderDashboardLines } = require("./DashboardBar");
3187
+ const clampedCursor = fmt.clampCursorPos(mwCursor, state.draft || "");
3188
+ multiWindowChromeRef.current = {
3189
+ statusText,
3190
+ promptPrefix,
3191
+ draft: state.draft || "",
3192
+ cursor: clampedCursor,
3193
+ completions: completionsOpen ? completions : [],
3194
+ completionIndex: completionsOpen ? completionIndex : -1,
3195
+ completionWindowStart: completionsOpen ? completionWindowStart : 0,
3196
+ completionPageSize: POPUP_PAGE_SIZE,
3197
+ dashboardLines: renderDashboardLines({
3198
+ dashboardView: state.dashboardView,
3199
+ focusMode: state.focusMode,
3200
+ globalMode: state.globalMode,
3201
+ globalScope: state.globalScope,
3202
+ activeAgents: displayAgents,
3203
+ activeAgentMeta: displayAgentMeta,
3204
+ activeAgentId: targetAgentId || "",
3205
+ selectedAgentIndex: state.selectedAgentIndex,
3206
+ agentListWindowStart: state.agentListWindowStart,
3207
+ projectListWindowStart: state.projectListWindowStart,
3208
+ maxProjectWindow: 5,
3209
+ maxWidth: Math.max(20, size.cols || 80),
3210
+ getAgentLabel: (id) => getAgentLabelFor(displayAgentMeta.get(id), id),
3211
+ getAgentState: (id) => {
3212
+ const meta = displayAgentMeta.get(id);
3213
+ return meta && typeof meta.activity_state === "string" ? meta.activity_state : "";
3214
+ },
3215
+ launchMode: state.settings.launchMode,
3216
+ agentProvider: state.settings.agentProvider,
3217
+ modeOptions: state.modeOptions,
3218
+ selectedModeIndex: state.selectedModeIndex,
3219
+ providerOptions: state.providerOptions,
3220
+ selectedProviderIndex: state.selectedProviderIndex,
3221
+ cronTasks: state.cronTasks,
3222
+ selectedCronIndex: state.selectedCronIndex,
3223
+ projects: state.projects,
3224
+ selectedProjectIndex: state.selectedProjectIndex,
3225
+ activeProjectRoot: currentProjectRoot,
3226
+ dashHints: buildDashHints(state, targetAgentLabel),
3227
+ }),
3228
+ };
3229
+ }
3230
+
3231
+ useEffect(() => {
3232
+ if (!multiWindowActive) return;
3233
+ const controller = multiWindowControllerRef.current;
3234
+ if (controller && typeof controller.renderAll === "function") {
3235
+ controller.renderAll();
3236
+ }
3237
+ }, [multiWindowActive, completionsOpen, completions.length, completionIndex, completionWindowStart]);
3238
+
3239
+ if (multiWindowActive) {
3240
+ return null;
3241
+ }
3242
+
3243
+ const renderChatLogLine = (item) => {
3244
+ const row = classifyChatLogLine((item && item.text) || "");
3245
+ const key = item && item.id ? item.id : `log-${row.body}`;
3246
+ if (row.kind === "spacer") {
3247
+ return h(Text, { key, color: "gray" }, " ");
3248
+ }
3249
+ const palette = {
3250
+ assistant: { marker: "cyan", speaker: "white", body: undefined, bold: true },
3251
+ agent: { marker: "cyan", speaker: "cyan", body: undefined, bold: false },
3252
+ error: { marker: "red", speaker: "red", body: "red", bold: true },
3253
+ success: { marker: "green", speaker: "green", body: "green", bold: false },
3254
+ divider: { marker: "gray", speaker: "gray", body: "gray", bold: false },
3255
+ banner: { marker: "cyan", speaker: "cyan", body: "cyan", bold: true },
3256
+ meta: { marker: "gray", speaker: "gray", body: "gray", bold: false },
3257
+ plain: { marker: "gray", speaker: "gray", body: undefined, bold: false },
3258
+ };
3259
+ const colors = palette[row.kind] || palette.plain;
3260
+ if (row.kind === "divider") {
3261
+ return h(Box, { key, marginBottom: 1 },
3262
+ h(Text, { color: colors.body, wrap: "truncate" }, row.body),
3263
+ );
3264
+ }
3265
+ if (row.kind === "banner") {
3266
+ return h(Box, { key, marginBottom: 1 },
3267
+ h(Text, { color: colors.body, bold: true, wrap: "truncate" }, row.body),
3268
+ );
3269
+ }
3270
+ return h(Box, { key, width: "100%", marginBottom: 1 },
3271
+ h(Box, { width: 2 },
3272
+ h(Text, { color: colors.marker, bold: row.kind === "error" }, row.marker || " "),
3273
+ ),
3274
+ row.speaker
3275
+ ? h(Text, { color: colors.speaker, bold: colors.bold }, row.speaker)
3276
+ : null,
3277
+ row.speaker
3278
+ ? h(Text, { color: "gray" }, " · ")
3279
+ : null,
3280
+ h(Text, { color: colors.body, wrap: "wrap" }, row.body || " "),
3281
+ );
3282
+ };
3283
+
2586
3284
  if (state.viewingAgentId) {
2587
3285
  const maxWidth = Math.max(20, size.cols || 80);
2588
3286
  const logRows = Math.max(1, (size.rows || 24) - 5);
@@ -2659,7 +3357,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
2659
3357
 
2660
3358
  return h(Box, { flexDirection: "column", width: "100%" },
2661
3359
  h(Box, { flexDirection: "column", width: "100%" },
2662
- ...state.logLines.map((item) => h(Text, { key: item.id }, item.text || " ")),
3360
+ ...state.logLines.map(renderChatLogLine),
2663
3361
  ),
2664
3362
  state.activeMerge ? h(Box, null,
2665
3363
  h(Text, { color: state.activeMerge.entries.some((e) => e.isError) ? "red" : "cyan" },
@@ -2671,10 +3369,10 @@ function createChatApp({ React, ink, props, interactive = true }) {
2671
3369
  const prefix = state.activeStream.publisher
2672
3370
  ? `${state.activeStream.publisher}: `
2673
3371
  : "";
2674
- return lines.map((line, idx) => h(Text, {
2675
- key: `s-${idx}`,
2676
- color: "cyan",
2677
- }, idx === 0 ? `${prefix}${line}` : ` ${line}`));
3372
+ return lines.map((line, idx) => renderChatLogLine({
3373
+ id: `s-${idx}`,
3374
+ text: idx === 0 ? `${prefix}${line}` : ` ${line}`,
3375
+ }));
2678
3376
  })(),
2679
3377
  ) : null,
2680
3378
  h(Box, { marginTop: 1, width: "100%" },
@@ -2739,6 +3437,15 @@ function createChatApp({ React, ink, props, interactive = true }) {
2739
3437
  interceptArrowsAndEnter: completionsOpen,
2740
3438
  placeholder: "",
2741
3439
  promptPrefix,
3440
+ // Dashboard renders 2 rows in global mode (always shows the
3441
+ // projects rail) or when an agents/mode/provider/cron view is
3442
+ // focused; otherwise it's a single summary row. Telling
3443
+ // MultilineInput how many UI rows live below it lets the IME
3444
+ // composition popup follow the on-screen caret instead of
3445
+ // appearing at the bottom-right of the terminal.
3446
+ linesBelowInput: props.globalMode
3447
+ ? 2
3448
+ : (state.focusMode === "dashboard" ? 2 : 1),
2742
3449
  }),
2743
3450
  ),
2744
3451
  h(DashboardBar, {
@@ -2795,6 +3502,15 @@ function computeStatusText(status, spinnerTick) {
2795
3502
  const message = String((status && status.message) || "");
2796
3503
  if (!message) return "CHAT · Ready";
2797
3504
  const type = String((status && status.type) || "thinking");
3505
+ if (type === "done" || type === "success") {
3506
+ const clean = stripBlessedTags(message).trim();
3507
+ return /^[✓✔]/.test(clean) ? clean : `✓ ${clean}`;
3508
+ }
3509
+ if (type === "error") {
3510
+ const clean = stripBlessedTags(message).trim();
3511
+ return /^[✗!]/.test(clean) ? clean : `✗ ${clean}`;
3512
+ }
3513
+ if (!isAnimatedStatusType(type)) return stripBlessedTags(message).trim() || "CHAT · Ready";
2798
3514
  const indicators = fmt.STATUS_INDICATORS[type] || fmt.STATUS_INDICATORS.thinking;
2799
3515
  const indicator = indicators[Math.max(0, Math.floor(Number(spinnerTick) || 0)) % indicators.length];
2800
3516
  const startedAt = Number.isFinite(status && status.startedAt) ? status.startedAt : 0;
@@ -2823,12 +3539,12 @@ async function runChatInk(projectRoot, options = {}) {
2823
3539
  env.startDaemon(projectRoot);
2824
3540
  }
2825
3541
 
2826
- const { socketPath } = require("../../daemon");
2827
- const { connectWithRetry } = require("../../chat/transport");
2828
- const { createDaemonTransport } = require("../../chat/daemonTransport");
2829
- const { createDaemonConnection } = require("../../chat/daemonConnection");
2830
- const { createDaemonCoordinator } = require("../../chat/daemonCoordinator");
2831
- const { startDaemon, stopDaemon } = require("../../chat/transport");
3542
+ const { socketPath } = require("../../runtime/daemon");
3543
+ const { connectWithRetry } = require("../../app/chat/transport");
3544
+ const { createDaemonTransport } = require("../../app/chat/daemonTransport");
3545
+ const { createDaemonConnection } = require("../../app/chat/daemonConnection");
3546
+ const { createDaemonCoordinator } = require("../../app/chat/daemonCoordinator");
3547
+ const { startDaemon, stopDaemon } = require("../../app/chat/transport");
2832
3548
  const { loadConfig } = require("../../config");
2833
3549
  const { startAgentMirror, startInternalAgentMirror } = require("./agentMirror");
2834
3550
  const sock = socketPath(projectRoot);
@@ -2939,10 +3655,18 @@ module.exports = {
2939
3655
  buildDirectBusSendRequest,
2940
3656
  buildPromptIpcRequest,
2941
3657
  chatHistoryOptionsForScope,
3658
+ classifyChatLogLine,
3659
+ createInkMultiWindowToggle,
2942
3660
  resolveActiveAgentId,
3661
+ resolveInjectSockPathForAgent,
2943
3662
  resolveAgentEnterRequest,
3663
+ resolveDashboardAgentEnterAction,
3664
+ buildEmptyProjectsDownActions,
2944
3665
  buildInternalLogRows,
3666
+ computeStatusText,
2945
3667
  computeInternalStatusText,
3668
+ inferStatusType,
3669
+ isAnimatedStatusType,
2946
3670
  resolveInternalKeyName,
2947
3671
  isInternalViewingAgent,
2948
3672
  applyInternalAgentTermWrite,