maka-agent 0.2.0-dev.23.20260906 → 0.2.0-dev.25.20260908

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 (215) hide show
  1. package/README.md +38 -19
  2. package/README.zh-CN.md +34 -19
  3. package/dist/acp/maka-acp-agent.js +2 -1
  4. package/dist/acp/session-configuration.js +143 -0
  5. package/dist/acp/session-registry.js +90 -4
  6. package/dist/cli-core.js +2 -0
  7. package/dist/pi-transcript-tools.js +6 -0
  8. package/dist/pi-transcript.js +5 -12
  9. package/dist/pi-tui-layout.js +56 -10
  10. package/dist/pi-tui-pickers.js +102 -1
  11. package/dist/pi-tui-runner.js +230 -46
  12. package/dist/pi-tui-todo.js +221 -0
  13. package/dist/pi-tui-transcript-viewer.js +134 -7
  14. package/dist/runtime-host-cli-context.js +224 -78
  15. package/dist/runtime-host-cli-installation.js +8 -2
  16. package/dist/runtime-host-cli.js +31 -1
  17. package/dist/runtime-host-handoff-surface.js +106 -0
  18. package/dist/runtime-host-installed-update-bootstrap.js +13 -0
  19. package/dist/runtime-host-installed-update-coordinator.js +33 -0
  20. package/dist/runtime-host-lifecycle-transaction.js +9 -1
  21. package/dist/runtime-host-local-handoff.js +48 -5
  22. package/dist/runtime-host-local-managed-activation.js +84 -0
  23. package/dist/runtime-host-local-source-retirement.js +18 -2
  24. package/dist/runtime-host-managed-lifecycle-manager.js +1 -0
  25. package/dist/runtime-host-operator-retirement.js +50 -0
  26. package/dist/runtime-host-service-manager.js +3 -3
  27. package/dist/runtime-host-session-channel.js +13 -0
  28. package/dist/runtime-host-session-driver.js +46 -26
  29. package/dist/runtime-host-session-update.js +62 -0
  30. package/dist/runtime-host-tui-command.js +38 -73
  31. package/dist/runtime-host-tui-context.js +1 -0
  32. package/dist/runtime-host-tui-owner.js +226 -0
  33. package/dist/tui-copy-catalog.js +219 -6
  34. package/dist/tui-host-owner-copy.js +24 -0
  35. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  36. package/node_modules/@maka/core/dist/agent-run.js +1 -0
  37. package/node_modules/@maka/core/dist/attachments.js +3 -0
  38. package/node_modules/@maka/core/dist/canonical-json.js +27 -0
  39. package/node_modules/@maka/core/dist/canonical-runtime-event.js +3 -1
  40. package/node_modules/@maka/core/dist/capabilities.js +35 -0
  41. package/node_modules/@maka/core/dist/computer-use.js +0 -1
  42. package/node_modules/@maka/core/dist/display-redaction.js +18 -0
  43. package/node_modules/@maka/core/dist/llm-connections.js +13 -0
  44. package/node_modules/@maka/core/dist/model-call-attempt.js +2 -0
  45. package/node_modules/@maka/core/dist/model-failure.js +40 -0
  46. package/node_modules/@maka/core/dist/model-metadata.generated.js +4 -4
  47. package/node_modules/@maka/core/dist/model-metadata.js +7 -3
  48. package/node_modules/@maka/core/dist/model-web-search.js +5 -4
  49. package/node_modules/@maka/core/dist/provider-registry.js +45 -4
  50. package/node_modules/@maka/core/dist/record-schema.js +9 -2
  51. package/node_modules/@maka/core/dist/redaction.js +35 -0
  52. package/node_modules/@maka/core/dist/run-composition.js +77 -2
  53. package/node_modules/@maka/core/dist/runtime-boundary.js +31 -11
  54. package/node_modules/@maka/core/dist/runtime-event.js +37 -6
  55. package/node_modules/@maka/core/dist/runtime-handoff.js +98 -0
  56. package/node_modules/@maka/core/dist/runtime-invocation.js +1 -1
  57. package/node_modules/@maka/core/dist/runtime-logical-execution.js +112 -0
  58. package/node_modules/@maka/core/dist/session.js +84 -56
  59. package/node_modules/@maka/core/dist/slash-command-catalog.js +3 -0
  60. package/node_modules/@maka/core/dist/tool-result-archive-evidence.js +20 -0
  61. package/node_modules/@maka/core/dist/tool-result-record-schema.js +2 -1
  62. package/node_modules/@maka/core/dist/usage-record-schema.js +26 -35
  63. package/node_modules/@maka/core/package.json +5 -0
  64. package/node_modules/@maka/eval/dist/experiment-directory.js +4 -13
  65. package/node_modules/@maka/mcp/dist/index.js +131 -1
  66. package/node_modules/@maka/runtime/dist/agent-run-inspect.js +0 -13
  67. package/node_modules/@maka/runtime/dist/agent-run.js +351 -105
  68. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +29 -13
  69. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +109 -34
  70. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +184 -183
  71. package/node_modules/@maka/runtime/dist/archive-read-tool.js +2 -2
  72. package/node_modules/@maka/runtime/dist/context-budget-helpers.js +0 -16
  73. package/node_modules/@maka/runtime/dist/context-budget.js +14 -9
  74. package/node_modules/@maka/runtime/dist/conversation-copy.js +158 -15
  75. package/node_modules/@maka/runtime/dist/github-copilot-oauth-enrollment.js +3 -3
  76. package/node_modules/@maka/runtime/dist/goal-continuation.js +36 -4
  77. package/node_modules/@maka/runtime/dist/history-compact-checkpoint.js +18 -9
  78. package/node_modules/@maka/runtime/dist/history-compact-summarizer.js +30 -22
  79. package/node_modules/@maka/runtime/dist/history-compaction.js +29 -4
  80. package/node_modules/@maka/runtime/dist/ledger-tool-result-archive-reader.js +174 -0
  81. package/node_modules/@maka/runtime/dist/mcp-tools.js +7 -2
  82. package/node_modules/@maka/runtime/dist/message-authority.js +9 -0
  83. package/node_modules/@maka/runtime/dist/model-adapter.js +19 -98
  84. package/node_modules/@maka/runtime/dist/model-factory.js +19 -81
  85. package/node_modules/@maka/runtime/dist/model-fetcher.js +49 -2
  86. package/node_modules/@maka/runtime/dist/model-history.js +0 -112
  87. package/node_modules/@maka/runtime/dist/model-runtime.js +85 -111
  88. package/node_modules/@maka/runtime/dist/native-web-search-tool.js +2 -1
  89. package/node_modules/@maka/runtime/dist/openai-codex-history-compactor.js +2 -2
  90. package/node_modules/@maka/runtime/dist/opencode-session-header.js +4 -2
  91. package/node_modules/@maka/runtime/dist/plugin-runtime.js +20 -10
  92. package/node_modules/@maka/runtime/dist/plugin-tool-service.js +205 -0
  93. package/node_modules/@maka/runtime/dist/provider-error-classification.js +84 -123
  94. package/node_modules/@maka/runtime/dist/provider-request-telemetry.js +76 -109
  95. package/node_modules/@maka/runtime/dist/request-shape.js +23 -0
  96. package/node_modules/@maka/runtime/dist/run-handoff-gate.js +97 -0
  97. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +83 -55
  98. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +121 -159
  99. package/node_modules/@maka/runtime/dist/runtime-kernel.js +157 -112
  100. package/node_modules/@maka/runtime/dist/runtime-ledger-repair.js +160 -79
  101. package/node_modules/@maka/runtime/dist/runtime-read-model.js +30 -89
  102. package/node_modules/@maka/runtime/dist/runtime-resume.js +80 -4
  103. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +5 -0
  104. package/node_modules/@maka/runtime/dist/session-manager.js +171 -288
  105. package/node_modules/@maka/runtime/dist/session-projection-helpers.js +0 -27
  106. package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +21 -0
  107. package/node_modules/@maka/runtime/dist/stream-graph-supervisor-tools.js +0 -5
  108. package/node_modules/@maka/runtime/dist/subscription-model-fetch.js +3 -4
  109. package/node_modules/@maka/runtime/dist/test-connection.js +16 -17
  110. package/node_modules/@maka/runtime/dist/tool-activation-identity.js +41 -0
  111. package/node_modules/@maka/runtime/dist/tool-availability.js +11 -1
  112. package/node_modules/@maka/runtime/dist/tool-result-archive-encoding.js +38 -0
  113. package/node_modules/@maka/runtime/dist/tool-result-archive-resource.js +63 -1
  114. package/node_modules/@maka/runtime/dist/tool-result-archive-transition.js +42 -12
  115. package/node_modules/@maka/runtime/dist/tool-result-archive.js +27 -4
  116. package/node_modules/@maka/runtime/dist/tool-runtime.js +3 -61
  117. package/node_modules/@maka/runtime/package.json +2 -0
  118. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +35 -0
  119. package/node_modules/@maka/runtime-host/dist/client/connect-or-spawn.js +9 -1
  120. package/node_modules/@maka/runtime-host/dist/client/connection.js +3 -0
  121. package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +179 -0
  122. package/node_modules/@maka/runtime-host/dist/client/host-handoff.js +275 -0
  123. package/node_modules/@maka/runtime-host/dist/client/host-retirement.js +26 -5
  124. package/node_modules/@maka/runtime-host/dist/client/index.js +2 -0
  125. package/node_modules/@maka/runtime-host/dist/client/launcher.js +3 -0
  126. package/node_modules/@maka/runtime-host/dist/client/remote-compatibility-error.js +2 -0
  127. package/node_modules/@maka/runtime-host/dist/client/session-subscription.js +46 -5
  128. package/node_modules/@maka/runtime-host/dist/control/endpoint.js +1 -3
  129. package/node_modules/@maka/runtime-host/dist/node-error.js +22 -0
  130. package/node_modules/@maka/runtime-host/dist/operator/index.js +2 -1
  131. package/node_modules/@maka/runtime-host/dist/operator/managed-deployment.js +1 -3
  132. package/node_modules/@maka/runtime-host/dist/operator/retirement-cancellation.js +20 -0
  133. package/node_modules/@maka/runtime-host/dist/peer-mesh/limits.js +0 -1
  134. package/node_modules/@maka/runtime-host/dist/peer-mesh/model.js +1 -1
  135. package/node_modules/@maka/runtime-host/dist/peer-reachability/publisher.js +1 -3
  136. package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +21 -12
  137. package/node_modules/@maka/runtime-host/dist/protocol/index.js +29 -1
  138. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
  139. package/node_modules/@maka/runtime-host/dist/protocol/plan.js +1 -0
  140. package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +27 -5
  141. package/node_modules/@maka/runtime-host/dist/protocol/project-catalog.js +2 -2
  142. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +40 -20
  143. package/node_modules/@maka/runtime-host/dist/protocol/session-transcript.js +0 -5
  144. package/node_modules/@maka/runtime-host/dist/protocol/session-turns.js +29 -58
  145. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +11 -1
  146. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +31 -2
  147. package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +1 -0
  148. package/node_modules/@maka/runtime-host/dist/server/agent-graph-coordinator.js +0 -3
  149. package/node_modules/@maka/runtime-host/dist/server/canonical-turn-snapshot.js +19 -4
  150. package/node_modules/@maka/runtime-host/dist/server/connection-session.js +7 -6
  151. package/node_modules/@maka/runtime-host/dist/server/execution-artifacts.js +32 -32
  152. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +144 -33
  153. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +5 -5
  154. package/node_modules/@maka/runtime-host/dist/server/execution-model-composition.js +45 -16
  155. package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +40 -1
  156. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +96 -17
  157. package/node_modules/@maka/runtime-host/dist/server/host-residency-registry.js +13 -7
  158. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-admission-registry.js +3 -1
  159. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +124 -62
  160. package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +47 -22
  161. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +76 -6
  162. package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +6 -0
  163. package/node_modules/@maka/runtime-host/dist/server/plugin-platform.js +6 -0
  164. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +325 -73
  165. package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +37 -2
  166. package/node_modules/@maka/runtime-host/dist/server/serial-outbound-writer.js +45 -3
  167. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +50 -7
  168. package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +87 -25
  169. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +5 -2
  170. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +12 -55
  171. package/node_modules/@maka/runtime-host/dist/server/session-sidecar-purge.js +1 -16
  172. package/node_modules/@maka/runtime-host/dist/server/session-transcript-pager.js +0 -1
  173. package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +460 -70
  174. package/node_modules/@maka/runtime-host/dist/server/shared-session-transcript.js +1 -1
  175. package/node_modules/@maka/runtime-host/dist/server/storage-maintenance.js +109 -0
  176. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +20 -2
  177. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +13 -4
  178. package/node_modules/@maka/runtime-host/dist/server/workhub-message-attachments.js +46 -0
  179. package/node_modules/@maka/storage/dist/artifact-store.js +21 -10
  180. package/node_modules/@maka/storage/dist/artifact-stores.js +1 -1
  181. package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +0 -5
  182. package/node_modules/@maka/storage/dist/codex-session-adapter.js +284 -198
  183. package/node_modules/@maka/storage/dist/context-offload-snapshot.js +325 -0
  184. package/node_modules/@maka/storage/dist/execution-stores.js +9 -7
  185. package/node_modules/@maka/storage/dist/file-session-repository.js +1012 -0
  186. package/node_modules/@maka/storage/dist/git-exec.js +61 -0
  187. package/node_modules/@maka/storage/dist/git-worktree-child-executor.js +8 -35
  188. package/node_modules/@maka/storage/dist/goal-authority.js +2 -2
  189. package/node_modules/@maka/storage/dist/interaction-store.js +2 -2
  190. package/node_modules/@maka/storage/dist/opencode-session-adapter.js +0 -4
  191. package/node_modules/@maka/storage/dist/operational-state-backup.js +9 -5
  192. package/node_modules/@maka/storage/dist/project-catalog.js +2 -23
  193. package/node_modules/@maka/storage/dist/runtime-event-invariants.js +2 -2
  194. package/node_modules/@maka/storage/dist/runtime-event-persistence.js +1 -0
  195. package/node_modules/@maka/storage/dist/runtime-policy/connection-catalog-document.js +8 -3
  196. package/node_modules/@maka/storage/dist/runtime-transcript-query.js +308 -0
  197. package/node_modules/@maka/storage/dist/scheduled-task-store.js +249 -283
  198. package/node_modules/@maka/storage/dist/session-bundle-manifest.js +5 -17
  199. package/node_modules/@maka/storage/dist/session-bundle-policy.js +26 -5
  200. package/node_modules/@maka/storage/dist/session-copy-cleanup.js +2 -1
  201. package/node_modules/@maka/storage/dist/session-message-projection.js +15 -0
  202. package/node_modules/@maka/storage/dist/session-repository.js +767 -0
  203. package/node_modules/@maka/storage/dist/session-store.js +25 -99
  204. package/node_modules/@maka/storage/dist/sqlite-artifact-metadata.js +10 -3
  205. package/node_modules/@maka/storage/dist/sqlite-context-offload-store.js +24 -8
  206. package/node_modules/@maka/storage/dist/sqlite-core-execution-schema.js +7 -1
  207. package/node_modules/@maka/storage/dist/sqlite-runtime-schema.js +44 -1
  208. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +130 -27
  209. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +109 -892
  210. package/node_modules/@maka/storage/dist/storage-id.js +28 -0
  211. package/node_modules/@maka/storage/dist/tool-result-archive-evidence.js +138 -0
  212. package/node_modules/@maka/storage/dist/workspace-identity.js +2 -15
  213. package/node_modules/@maka/storage/dist/workspace-version-authority-internal.js +1 -8
  214. package/node_modules/@maka/storage/package.json +2 -0
  215. package/package.json +1 -1
@@ -17,8 +17,8 @@
17
17
  * under the License.
18
18
  */
19
19
  import { randomUUID } from 'node:crypto';
20
- import { basename } from 'node:path';
21
20
  import { Key, ProcessTerminal, SelectList, TuiMainScreen, isKeyRelease, isKeyRepeat, matchesKey, } from '@earendil-works/pi-tui';
21
+ import { CurrentTodoStore, TodoOverlay, renderTodoIndicator } from './pi-tui-todo.js';
22
22
  import { isThinkingLevel } from '@maka/core/model-thinking';
23
23
  import { deriveConnectionSlug, } from '@maka/core/llm-connections';
24
24
  import { projectRevisionLinkedSessionTree } from '@maka/core/session-revisions';
@@ -41,6 +41,7 @@ import { MakaAutocompleteAboveEditorComponent } from './tui-autocomplete-layout.
41
41
  import { TranscriptViewerOverlay } from './pi-tui-transcript-viewer.js';
42
42
  import { copyToClipboard } from './tui-clipboard.js';
43
43
  import { getTuiCopyCopy, lastAssistantText, serializeTranscriptText } from './tui-copy-command.js';
44
+ import { getTuiHostOwnerCopy } from './tui-host-owner-copy.js';
44
45
  import { McpManagementOverlay } from './pi-tui-mcp-status.js';
45
46
  import { createShellRunElapsedTicker } from './shell-run-elapsed-ticker.js';
46
47
  import { createShellRunHydrationController } from './shell-run-hydration.js';
@@ -48,7 +49,7 @@ import { CTX_REFRESH_DEBOUNCE_MS, createCtxRefresher, scheduleCtxRefreshTimeout,
48
49
  import { sessionStatusBadge } from './tui-session-status.js';
49
50
  import { AttentionController, DISABLE_FOCUS_REPORTING, ENABLE_FOCUS_REPORTING, FOCUS_IN_SEQUENCE, FOCUS_OUT_SEQUENCE, } from './tui-attention.js';
50
51
  import { MakaActivityStripComponent, MakaPendingQueueComponent, MakaPiLayoutComponent, MakaStatusLineComponent, MakaTranscriptComponent, } from './pi-tui-layout.js';
51
- import { MakaAutocompleteProvider, DirectoryPickerOverlay, ModelSearchOverlay, OnboardingWizard, PickerOverlay, UserQuestionOverlay, modelChoiceConnectionLabels, getTuiPickerCopy, modelPickerItems, onboardingFailureMessage, permissionModePickerItems, skillPickerItems, thinkingLevelPickerItems, } from './pi-tui-pickers.js';
52
+ import { MakaAutocompleteProvider, DirectoryPickerOverlay, ModelSearchOverlay, SessionSearchOverlay, OnboardingWizard, PickerOverlay, UserQuestionOverlay, modelChoiceConnectionLabels, getTuiPickerCopy, modelPickerItems, onboardingFailureMessage, permissionModePickerItems, skillPickerItems, thinkingLevelPickerItems, } from './pi-tui-pickers.js';
52
53
  import { formatMakaResumeCommand } from './cli-invocation.js';
53
54
  import { goalAttachedNoticeText, goalPausedNoticeText, goalStatusLabel, goalSummaryLines, isLiveGoalStatus, } from './pi-goal.js';
54
55
  import { getTuiPrimaryGuidance } from './tui-primary-guidance.js';
@@ -211,6 +212,20 @@ export async function runMakaPiTui(input) {
211
212
  // True while the /session picker is open mid-turn: Escape must close the
212
213
  // overlay, not arm the double-Escape interrupt for the running Turn (#3380).
213
214
  let sessionPickerOverlayOpen = false;
215
+ let transcriptOverlay;
216
+ let todoOverlay;
217
+ const closeTodoOverlay = () => {
218
+ todoOverlay?.hide();
219
+ todoOverlay = undefined;
220
+ };
221
+ let transcriptViewer;
222
+ let transcriptViewerSessionId;
223
+ const resetTranscriptViewer = () => {
224
+ transcriptOverlay?.hide();
225
+ transcriptOverlay = undefined;
226
+ transcriptViewer = undefined;
227
+ transcriptViewerSessionId = undefined;
228
+ };
214
229
  let lastTurnEscapeAt = 0;
215
230
  let lastIdleEscapeAt = 0;
216
231
  let lastIdleCtrlCAt = 0;
@@ -317,6 +332,34 @@ export async function runMakaPiTui(input) {
317
332
  const activityStrip = new MakaActivityStripComponent(metadata);
318
333
  const pendingQueue = new MakaPendingQueueComponent(state, locale);
319
334
  const statusLine = new MakaStatusLineComponent(metadata);
335
+ const currentTodo = new CurrentTodoStore({
336
+ read: async (sessionId) => {
337
+ if (!input.driver.queryTodo)
338
+ throw new Error('Todo query unavailable');
339
+ return input.driver.queryTodo(sessionId);
340
+ },
341
+ }, () => {
342
+ if (!closed)
343
+ tui.requestRender();
344
+ });
345
+ const syncTodoSession = () => {
346
+ currentTodo.setSession(input.driver.getSessionId() ?? undefined);
347
+ };
348
+ const unsubscribeTodoChanges = input.driver.subscribeTodoChanges?.((sessionId) => {
349
+ if (sessionId !== input.driver.getSessionId())
350
+ return;
351
+ syncTodoSession();
352
+ void currentTodo.refresh();
353
+ });
354
+ const todoIndicator = {
355
+ invalidate() { },
356
+ render(width) {
357
+ if (!input.driver.queryTodo)
358
+ return [];
359
+ const line = renderTodoIndicator(currentTodo.getState(), { locale, width });
360
+ return line === undefined ? [] : [line];
361
+ },
362
+ };
320
363
  // Use the vendor editor's full 20-item autocomplete capacity. Larger command
321
364
  // catalogs remain scrollable and keep an exact position/total counter.
322
365
  const editor = new MakaSkillHighlightEditor(tui, editorTheme(), {
@@ -325,7 +368,7 @@ export async function runMakaPiTui(input) {
325
368
  });
326
369
  let refreshEditorCwd;
327
370
  const editorSurface = new MakaAutocompleteAboveEditorComponent(editor);
328
- const layout = new MakaPiLayoutComponent(state, transcript, activityStrip, pendingQueue, editorSurface, statusLine, terminal);
371
+ const layout = new MakaPiLayoutComponent(state, transcript, activityStrip, pendingQueue, editorSurface, statusLine, terminal, todoIndicator);
329
372
  const attention = new AttentionController(terminal, {
330
373
  baseTitle: input.title,
331
374
  ...(input.attentionLongTurnThresholdMs !== undefined
@@ -607,6 +650,9 @@ export async function runMakaPiTui(input) {
607
650
  unsubscribeModelCatalogChanges?.();
608
651
  unsubscribeSessionTitleChanges();
609
652
  unsubscribeGoalChanges?.();
653
+ unsubscribeTodoChanges?.();
654
+ currentTodo.dispose();
655
+ closeTodoOverlay();
610
656
  void sideConversation?.stopParentObserver?.();
611
657
  unsubscribeStartedTurns();
612
658
  unsubscribeResolvedInteractions();
@@ -632,10 +678,8 @@ export async function runMakaPiTui(input) {
632
678
  rejectClosed(error);
633
679
  else
634
680
  resolveClosed();
635
- // Runtime stop is best-effort after the shell has its terminal back. A
636
- // double-Escape/Ctrl-C interrupt may already have one in flight; reuse it.
637
- if (!interruptRequested)
638
- void input.driver.stop().catch(() => { });
681
+ // Closing a viewport is not a Turn or Host stop. Explicit interrupt keys
682
+ // retain their own stop path; an attached Host continues independently.
639
683
  };
640
684
  const handleProcessExit = (exitCode, error) => {
641
685
  process.exitCode = exitCode;
@@ -1210,6 +1254,7 @@ export async function runMakaPiTui(input) {
1210
1254
  });
1211
1255
  }
1212
1256
  const adoptSessionMetadata = (summary, announceIdentity = true) => {
1257
+ syncTodoSession();
1213
1258
  cwd = summary.cwd ?? cwd;
1214
1259
  setSessionTitle(summary.name);
1215
1260
  model = summary.model;
@@ -1298,6 +1343,8 @@ export async function runMakaPiTui(input) {
1298
1343
  // `messages`. Shared by switchSession and rewindToTurn so both land the same
1299
1344
  // runner state (model/connection/thinking/transcript/scroll).
1300
1345
  const applySwitchResult = async ({ summary, messages, activeTurn, }) => {
1346
+ resetTranscriptViewer();
1347
+ closeTodoOverlay();
1301
1348
  adoptSessionMetadata(summary, false);
1302
1349
  replaceTranscript(messages);
1303
1350
  syncInteractionOverlays();
@@ -1857,6 +1904,11 @@ export async function runMakaPiTui(input) {
1857
1904
  formOverlay = showBottomPicker(formOverlayComponent);
1858
1905
  };
1859
1906
  const syncInteractionOverlays = () => {
1907
+ if (state.pendingInteraction) {
1908
+ closeTodoOverlay();
1909
+ transcriptOverlay?.hide();
1910
+ transcriptOverlay = undefined;
1911
+ }
1860
1912
  syncUserQuestionOverlay();
1861
1913
  syncFormOverlay();
1862
1914
  };
@@ -2306,45 +2358,58 @@ export async function runMakaPiTui(input) {
2306
2358
  foreignByValue.set(`foreign:${summary.source}:${summary.id}`, summary);
2307
2359
  }
2308
2360
  }
2361
+ let overlay;
2362
+ let sessionSearch;
2309
2363
  const renderScope = () => {
2310
2364
  const visibleSessions = sessionListScope === 'current'
2311
2365
  ? projectedSessions.filter(({ session }) => session.cwd === cwd)
2312
2366
  : projectedSessions;
2313
- const items = visibleSessions.map(({ session, depth }) => {
2367
+ const choices = visibleSessions.map(({ session, depth }) => {
2314
2368
  const state = availability.get(session.id);
2315
2369
  const statusBadge = sessionStatusBadge(session, locale);
2316
2370
  const statusDetail = statusBadge ? ` · ${statusBadge}` : '';
2317
- const location = sessionListScope === 'all' && session.cwd ? ` ${basename(session.cwd)}` : '';
2371
+ const location = sessionListScope === 'all' && session.cwd ? ` ${session.cwd}` : '';
2318
2372
  const childDetail = session.subagentRuntime
2319
2373
  ? ` subagent:${session.subagentRuntime.profile}`
2320
2374
  : '';
2321
- return {
2375
+ const item = {
2322
2376
  value: session.id,
2323
2377
  label: `${depth > 0 ? `${' '.repeat(depth - 1)}↳ ` : ''}${session.name || session.id}`,
2324
2378
  description: state?.available === false
2325
2379
  ? `${shortSessionId(session.id)}${statusDetail} ${state.reason}`
2326
2380
  : `${shortSessionId(session.id)}${statusDetail}${location}${childDetail} ${session.llmConnectionSlug} ${session.model}`,
2327
2381
  };
2382
+ return {
2383
+ item,
2384
+ searchText: [
2385
+ session.name,
2386
+ session.id,
2387
+ session.cwd,
2388
+ session.model,
2389
+ session.llmConnectionSlug,
2390
+ ]
2391
+ .filter(Boolean)
2392
+ .join(' ')
2393
+ .toLocaleLowerCase(),
2394
+ };
2328
2395
  });
2329
2396
  // Foreign sessions are cwd-scoped; show them in both scope views (they
2330
2397
  // belong to this project) so a Tab toggle never makes them vanish.
2331
2398
  for (const [value, summary] of foreignByValue) {
2332
- items.push({
2333
- value,
2334
- label: summary.title,
2335
- description: `↩ resume from ${foreignSourceLabel(summary.source)}`,
2399
+ choices.push({
2400
+ item: {
2401
+ value,
2402
+ label: summary.title,
2403
+ description: `↩ resume from ${foreignSourceLabel(summary.source)}`,
2404
+ },
2405
+ searchText: `${summary.title} ${summary.id} ${summary.cwd} ${summary.source}`.toLocaleLowerCase(),
2336
2406
  });
2337
2407
  }
2338
- const list = new SelectList(items, 10, selectListTheme(), {
2339
- minPrimaryColumnWidth: 20,
2340
- maxPrimaryColumnWidth: Math.max(20, terminal.columns - 30),
2341
- });
2342
- let overlay;
2343
2408
  const closeOverlay = () => {
2344
2409
  sessionPickerOverlayOpen = false;
2345
2410
  overlay?.hide();
2346
2411
  };
2347
- list.onSelect = (item) => {
2412
+ const onSelect = (item) => {
2348
2413
  const foreign = foreignByValue.get(item.value);
2349
2414
  if (foreign) {
2350
2415
  closeOverlay();
@@ -2356,21 +2421,27 @@ export async function runMakaPiTui(input) {
2356
2421
  closeOverlay();
2357
2422
  void goToSession(item.value);
2358
2423
  };
2359
- list.onCancel = () => closeOverlay();
2360
- sessionPickerOverlayOpen = true;
2361
- overlay = showBottomPicker(new PickerOverlay(list, {
2362
- title: 'Resume Session',
2363
- rightLabel: sessionListScope === 'current' ? 'Current' : 'All',
2364
- hint: 'Tab scope · ↑↓ move · Enter select · Esc close',
2365
- onInput: (data) => {
2366
- if (!matchesKey(data, Key.tab) || isKeyRelease(data) || isKeyRepeat(data))
2367
- return false;
2424
+ const scopeLabel = sessionListScope === 'current'
2425
+ ? pickerCopy.sessionScopeCurrent
2426
+ : pickerCopy.sessionScopeAll;
2427
+ if (sessionSearch) {
2428
+ sessionSearch.updateChoices(choices, scopeLabel);
2429
+ sessionSearch.invalidate();
2430
+ return;
2431
+ }
2432
+ sessionSearch = new SessionSearchOverlay(tui, {
2433
+ locale,
2434
+ choices,
2435
+ scopeLabel,
2436
+ onSelect,
2437
+ onCancel: closeOverlay,
2438
+ onToggleScope: () => {
2368
2439
  sessionListScope = sessionListScope === 'current' ? 'all' : 'current';
2369
- overlay?.hide();
2370
2440
  renderScope();
2371
- return true;
2372
2441
  },
2373
- }));
2442
+ });
2443
+ sessionPickerOverlayOpen = true;
2444
+ overlay = showBottomPicker(sessionSearch);
2374
2445
  };
2375
2446
  renderScope();
2376
2447
  };
@@ -2428,6 +2499,9 @@ export async function runMakaPiTui(input) {
2428
2499
  requestRender();
2429
2500
  return false;
2430
2501
  }
2502
+ resetTranscriptViewer();
2503
+ closeTodoOverlay();
2504
+ syncTodoSession();
2431
2505
  // A fresh session is not bound by the previous one's boundary. Falling back
2432
2506
  // to the *current* label would keep the previous Session's mode, including
2433
2507
  // Auto while a changed Host default creates with full access; the launch
@@ -2509,20 +2583,42 @@ export async function runMakaPiTui(input) {
2509
2583
  requestRender();
2510
2584
  };
2511
2585
  const showTranscriptViewer = () => {
2512
- let overlay;
2513
- const renderTranscript = transcript.createDocumentRenderer();
2514
- const viewer = new TranscriptViewerOverlay({
2515
- renderTranscript,
2516
- viewportRows: () => terminal.rows,
2517
- onClose: () => overlay?.hide(),
2518
- onChange: () => tui.requestRender(),
2519
- });
2520
- overlay = tui.showOverlay(viewer, {
2586
+ if (state.pendingInteraction)
2587
+ return;
2588
+ const sessionId = input.driver.getSessionId() ?? undefined;
2589
+ if (!transcriptViewer || transcriptViewerSessionId !== sessionId) {
2590
+ transcriptViewerSessionId = sessionId;
2591
+ transcriptViewer = new TranscriptViewerOverlay({
2592
+ renderTranscript: transcript.createDocumentRenderer(),
2593
+ locale,
2594
+ viewportRows: () => terminal.rows,
2595
+ onClose: () => {
2596
+ transcriptOverlay?.hide();
2597
+ transcriptOverlay = undefined;
2598
+ },
2599
+ onChange: () => tui.requestRender(),
2600
+ });
2601
+ }
2602
+ transcriptOverlay = tui.showOverlay(transcriptViewer, {
2521
2603
  anchor: 'top-left',
2522
2604
  width: '100%',
2523
2605
  maxHeight: '100%',
2524
2606
  });
2525
2607
  };
2608
+ const showTodo = () => {
2609
+ if (state.pendingInteraction)
2610
+ return;
2611
+ syncTodoSession();
2612
+ void currentTodo.refresh();
2613
+ closeTodoOverlay();
2614
+ todoOverlay = tui.showOverlay(new TodoOverlay({
2615
+ locale,
2616
+ getState: () => input.driver.queryTodo ? currentTodo.getState() : { status: 'error', items: [] },
2617
+ viewportRows: () => terminal.rows,
2618
+ onClose: closeTodoOverlay,
2619
+ onChange: () => tui.requestRender(),
2620
+ }), { anchor: 'top-left', width: '100%', maxHeight: '100%' });
2621
+ };
2526
2622
  const showMcpStatus = () => {
2527
2623
  let overlay;
2528
2624
  const viewer = new McpManagementOverlay({
@@ -2943,7 +3039,78 @@ export async function runMakaPiTui(input) {
2943
3039
  notice('Goal cleared.');
2944
3040
  }
2945
3041
  };
3042
+ const runHostCommand = async (parts) => {
3043
+ const copy = getTuiHostOwnerCopy(locale);
3044
+ if (!input.hostControl)
3045
+ throw new Error(copy.unavailable);
3046
+ const action = parts[1] ?? 'status';
3047
+ if (action === 'status' && parts.length <= 2) {
3048
+ const text = await input.hostControl.status();
3049
+ if (!closed) {
3050
+ state.entries.push({ kind: 'notice', level: 'info', text });
3051
+ requestRender();
3052
+ }
3053
+ return;
3054
+ }
3055
+ if (action !== 'stop' && action !== 'restart' && action !== 'update') {
3056
+ throw new Error(copy.usage);
3057
+ }
3058
+ const targets = parts.slice(2);
3059
+ if (targets.length > (action === 'update' ? 1 : 0)) {
3060
+ throw new Error(copy.usage);
3061
+ }
3062
+ const accepted = await input.hostControl.prepare({
3063
+ action,
3064
+ ...(targets[0] ? { target: targets[0] } : {}),
3065
+ }, (detail) => new Promise((resolve) => {
3066
+ if (closed)
3067
+ return resolve('cancel');
3068
+ showSelectPicker(copy.pickerTitle, 'cancel', [
3069
+ {
3070
+ value: 'cancel',
3071
+ label: copy.cancel,
3072
+ description: copy.cancelDescription,
3073
+ },
3074
+ {
3075
+ value: 'safe',
3076
+ label: copy.safe,
3077
+ description: copy.safeDescription,
3078
+ },
3079
+ ...(action === 'update'
3080
+ ? []
3081
+ : [
3082
+ {
3083
+ value: 'interrupt',
3084
+ label: copy.interrupt,
3085
+ description: copy.interruptDescription,
3086
+ },
3087
+ ]),
3088
+ ], (item) => resolve(item.value), {
3089
+ minPrimaryColumnWidth: 16,
3090
+ maxPrimaryColumnWidth: 26,
3091
+ selectedIndex: 0,
3092
+ notice: detail,
3093
+ onCancel: () => resolve('cancel'),
3094
+ });
3095
+ }));
3096
+ if (accepted)
3097
+ beginGracefulClose();
3098
+ };
2946
3099
  const slashCommandHandlers = {
3100
+ host: {
3101
+ description: primaryGuidance.commands.host,
3102
+ midTurn: 'local',
3103
+ run: (parts) => {
3104
+ void runHostCommand(parts).catch(reportError);
3105
+ },
3106
+ },
3107
+ update: {
3108
+ description: primaryGuidance.commands.update,
3109
+ midTurn: 'local',
3110
+ run: (parts) => {
3111
+ void runHostCommand(['/host', 'update', ...parts.slice(1)]).catch(reportError);
3112
+ },
3113
+ },
2947
3114
  context: {
2948
3115
  description: primaryGuidance.commands.context,
2949
3116
  // Read-only diagnostics, but runControl-gated: mid-turn it would
@@ -3241,6 +3408,22 @@ export async function runMakaPiTui(input) {
3241
3408
  showTranscriptViewer();
3242
3409
  },
3243
3410
  },
3411
+ todo: {
3412
+ description: primaryGuidance.commands.todo,
3413
+ midTurn: 'local',
3414
+ run: (parts) => {
3415
+ if (parts.length !== 1) {
3416
+ state.entries.push({
3417
+ kind: 'notice',
3418
+ level: 'error',
3419
+ text: TUI_COPY_RESOURCES.todo[locale].usage,
3420
+ });
3421
+ requestRender();
3422
+ return;
3423
+ }
3424
+ showTodo();
3425
+ },
3426
+ },
3244
3427
  permissions: {
3245
3428
  description: primaryGuidance.commands.permissions,
3246
3429
  midTurn: 'refuse',
@@ -3535,9 +3718,8 @@ export async function runMakaPiTui(input) {
3535
3718
  if (!matchesKey(data, Key.escape))
3536
3719
  lastIdleEscapeAt = 0;
3537
3720
  if (matchesKey(data, Key.ctrl('o')) && !isKeyRepeat(data)) {
3538
- if (handleExpansionToggleKey('tool')) {
3539
- return { consume: true };
3540
- }
3721
+ showTranscriptViewer();
3722
+ return { consume: true };
3541
3723
  }
3542
3724
  if (matchesKey(data, Key.ctrl('t')) && !isKeyRepeat(data)) {
3543
3725
  if (handleExpansionToggleKey('thinking')) {
@@ -3648,11 +3830,11 @@ export async function runMakaPiTui(input) {
3648
3830
  // re-wrapping) a full clear would wipe the scrollback the user scrolls through.
3649
3831
  // Differential rendering clears the vacated rows without the wipe.
3650
3832
  //
3651
- // The Ctrl+O / Ctrl+T toggles are viewport-anchored for the same reason: an
3833
+ // The live Ctrl+T thinking toggle is viewport-anchored for the same reason: an
3652
3834
  // entry above the live viewport lives in terminal scrollback, which cannot
3653
3835
  // be rewritten, so resizing it would push pi-tui's differential renderer
3654
3836
  // into a scrollback-clearing full redraw (its `firstChanged < viewportTop`
3655
- // path). The toggles therefore retarget only entries inside the viewport;
3837
+ // path). It therefore retargets only entries inside the viewport;
3656
3838
  // see entryInLiveViewport in pi-transcript.ts (#1097). A block whose own
3657
3839
  // expansion pushed its head above the viewport can consequently not be
3658
3840
  // collapsed by the next press (#1134): the toggle still flips the default
@@ -3661,6 +3843,7 @@ export async function runMakaPiTui(input) {
3661
3843
  // again within EXPANSION_COLLAPSE_CONFIRM_WINDOW_MS applies the collapsed
3662
3844
  // default to those blocks too and pays one scrollback-clearing full redraw
3663
3845
  // (requestRender(true)), re-anchoring the viewport at the tail.
3846
+ // Ctrl+O instead opens a detached reader whose details never resize live rows.
3664
3847
  tui.setClearOnShrink(false);
3665
3848
  tui.addChild(layout);
3666
3849
  tui.setFocus(editorSurface);
@@ -3702,6 +3885,7 @@ export async function runMakaPiTui(input) {
3702
3885
  }
3703
3886
  });
3704
3887
  }
3888
+ syncTodoSession();
3705
3889
  return closedPromise;
3706
3890
  }
3707
3891
  const BOTTOM_PICKER_MARGIN_ROWS = 4;
@@ -0,0 +1,221 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { Key, isKeyRelease, isKeyRepeat, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from '@earendil-works/pi-tui';
20
+ import { projectSessionTodoItemsForDisplay, } from '@maka/core/session-todo';
21
+ import { ansi } from './tui-ansi.js';
22
+ import { TUI_COPY_RESOURCES } from './tui-copy-catalog.js';
23
+ /** Owns only the current session; late queries cannot cross session changes or disposal. */
24
+ export class CurrentTodoStore {
25
+ reader;
26
+ onChange;
27
+ snapshot = { status: 'idle', items: [] };
28
+ generation = 0;
29
+ disposed = false;
30
+ pending;
31
+ refreshAgain = false;
32
+ constructor(reader, onChange = () => undefined) {
33
+ this.reader = reader;
34
+ this.onChange = onChange;
35
+ }
36
+ getState() {
37
+ return this.snapshot;
38
+ }
39
+ setSession(sessionId) {
40
+ if (this.disposed || this.snapshot.sessionId === sessionId)
41
+ return;
42
+ this.generation++;
43
+ this.pending = undefined;
44
+ this.refreshAgain = false;
45
+ this.publish({ status: 'idle', ...(sessionId ? { sessionId } : {}), items: [] });
46
+ if (sessionId)
47
+ void this.refresh();
48
+ }
49
+ refresh() {
50
+ const { sessionId } = this.snapshot;
51
+ if (this.disposed || !sessionId)
52
+ return Promise.resolve(false);
53
+ if (this.pending) {
54
+ this.refreshAgain = true;
55
+ return this.pending;
56
+ }
57
+ const pending = this.load(sessionId, this.generation).finally(() => {
58
+ if (this.pending !== pending)
59
+ return;
60
+ this.pending = undefined;
61
+ if (this.refreshAgain) {
62
+ this.refreshAgain = false;
63
+ void this.refresh();
64
+ }
65
+ });
66
+ this.pending = pending;
67
+ return pending;
68
+ }
69
+ dispose() {
70
+ this.disposed = true;
71
+ this.generation++;
72
+ this.pending = undefined;
73
+ this.refreshAgain = false;
74
+ this.snapshot = { status: 'idle', items: [] };
75
+ }
76
+ async load(sessionId, generation) {
77
+ // Keep current data visible during background refreshes, without a second cache.
78
+ if (this.snapshot.status !== 'ready' && this.snapshot.status !== 'empty') {
79
+ this.publish({ status: 'loading', sessionId, items: [] });
80
+ }
81
+ try {
82
+ const result = await this.reader.read(sessionId);
83
+ if (generation !== this.generation)
84
+ return false;
85
+ const items = projectSessionTodoItemsForDisplay(result.items);
86
+ this.publish({ status: items.length ? 'ready' : 'empty', sessionId, items });
87
+ }
88
+ catch {
89
+ if (generation !== this.generation)
90
+ return false;
91
+ this.publish({ status: 'error', sessionId, items: [] });
92
+ }
93
+ return true;
94
+ }
95
+ publish(snapshot) {
96
+ this.snapshot = snapshot;
97
+ this.onChange();
98
+ }
99
+ }
100
+ /** Render the one-line current Todo indicator. Empty and initial states stay hidden. */
101
+ export function renderTodoIndicator(snapshot, input) {
102
+ const copy = TUI_COPY_RESOURCES.todo[input.locale];
103
+ if (snapshot.status === 'idle' || snapshot.status === 'loading' || snapshot.status === 'empty') {
104
+ return undefined;
105
+ }
106
+ if (snapshot.status === 'error')
107
+ return fitLine(`${copy.unavailable} · ${copy.open}`, input.width);
108
+ const current = snapshot.items.find((item) => item.status === 'in_progress');
109
+ const completed = snapshot.items.filter((item) => item.status === 'completed').length;
110
+ const suffix = `${copy.progress} ${completed}/${snapshot.items.length} · ${copy.open}`;
111
+ const prefix = current ? `${ansi.accent('◐')} ` : '';
112
+ const available = input.width - visibleWidth(prefix) - visibleWidth(suffix) - (current ? 3 : 1);
113
+ if (!current)
114
+ return fitLine(suffix, input.width);
115
+ if (available < 1)
116
+ return fitLine(suffix, input.width);
117
+ const content = truncateToWidth(current.content, Math.max(1, available), '…');
118
+ return fitLine(`${prefix}${content} · ${suffix}`, input.width);
119
+ }
120
+ /** Read-only, scrollable view of the current Todo projection. */
121
+ export class TodoOverlay {
122
+ input;
123
+ top = 0;
124
+ bodyRows = 1;
125
+ width = 80;
126
+ constructor(input) {
127
+ this.input = input;
128
+ }
129
+ invalidate() { }
130
+ handleInput(data) {
131
+ if (isKeyRelease(data))
132
+ return;
133
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
134
+ if (!isKeyRepeat(data))
135
+ this.input.onClose();
136
+ return;
137
+ }
138
+ if (matchesKey(data, Key.up))
139
+ this.scrollBy(-1);
140
+ else if (matchesKey(data, Key.down))
141
+ this.scrollBy(1);
142
+ else if (matchesKey(data, Key.pageUp))
143
+ this.scrollBy(-this.bodyRows);
144
+ else if (matchesKey(data, Key.pageDown))
145
+ this.scrollBy(this.bodyRows);
146
+ else if (matchesKey(data, Key.home)) {
147
+ this.scrollTo(0);
148
+ this.input.onChange?.();
149
+ }
150
+ else if (matchesKey(data, Key.end)) {
151
+ this.scrollTo(Number.MAX_SAFE_INTEGER);
152
+ this.input.onChange?.();
153
+ }
154
+ }
155
+ render(width) {
156
+ const safeWidth = Math.max(1, width);
157
+ this.width = safeWidth;
158
+ const copy = TUI_COPY_RESOURCES.todo[this.input.locale];
159
+ const viewport = Math.max(1, Math.floor(this.input.viewportRows()));
160
+ const showFooter = viewport > 2;
161
+ this.bodyRows = Math.max(0, viewport - (showFooter ? 2 : 1));
162
+ const snapshot = this.input.getState();
163
+ const rows = this.rows(snapshot, safeWidth);
164
+ const maxTop = Math.max(0, rows.length - this.bodyRows);
165
+ this.top = Math.min(this.top, maxTop);
166
+ const visible = rows.slice(this.top, this.top + this.bodyRows);
167
+ const completed = snapshot.items.filter((item) => item.status === 'completed').length;
168
+ const summary = snapshot.status === 'ready'
169
+ ? ` · ${completed}/${snapshot.items.length} ${copy.markedComplete}`
170
+ : '';
171
+ const position = rows.length > this.bodyRows && this.bodyRows > 0
172
+ ? ` · ${this.top + 1}-${this.top + visible.length}/${rows.length}`
173
+ : '';
174
+ const header = padLine(`${ansi.bold(copy.title)}${summary}${ansi.dim(position)}`, safeWidth);
175
+ const body = [
176
+ ...visible,
177
+ ...Array.from({ length: this.bodyRows - visible.length }, () => ' '.repeat(safeWidth)),
178
+ ];
179
+ if (!showFooter)
180
+ return [header, ...body];
181
+ const footer = padLine(ansi.dim(copy.hint), safeWidth);
182
+ return [header, ...body, footer];
183
+ }
184
+ rows(snapshot, width) {
185
+ const copy = TUI_COPY_RESOURCES.todo[this.input.locale];
186
+ if (snapshot.status === 'loading')
187
+ return [padLine(copy.loading, width)];
188
+ if (snapshot.status === 'error')
189
+ return [padLine(copy.unavailable, width)];
190
+ if (snapshot.status === 'idle' || snapshot.status === 'empty')
191
+ return [padLine(copy.empty, width)];
192
+ return snapshot.items.flatMap((item) => {
193
+ const symbol = item.status === 'completed'
194
+ ? ansi.green('✓')
195
+ : item.status === 'in_progress'
196
+ ? ansi.accent('●')
197
+ : ansi.muted('○');
198
+ return wrapTodoItem(`${symbol} `, item.content, width);
199
+ });
200
+ }
201
+ scrollBy(delta) {
202
+ this.scrollTo(this.top + delta);
203
+ this.input.onChange?.();
204
+ }
205
+ scrollTo(top) {
206
+ const rows = this.rows(this.input.getState(), this.width);
207
+ this.top = Math.max(0, Math.min(top, Math.max(0, rows.length - this.bodyRows)));
208
+ }
209
+ }
210
+ function wrapTodoItem(prefix, content, width) {
211
+ const lines = wrapTextWithAnsi(content, Math.max(1, width - 2));
212
+ return (lines.length ? lines : ['']).map((line, index) => padLine(`${index === 0 ? prefix : ' '}${line}`, width));
213
+ }
214
+ function fitLine(text, width) {
215
+ return truncateToWidth(text, Math.max(1, width), '…');
216
+ }
217
+ function padLine(text, width) {
218
+ const safeWidth = Math.max(1, width);
219
+ const trimmed = fitLine(text, safeWidth);
220
+ return `${trimmed}${' '.repeat(Math.max(0, safeWidth - visibleWidth(trimmed)))}`;
221
+ }