maka-agent 0.2.1-dev.80.20261012 → 0.2.1-dev.83.20261014

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 (60) hide show
  1. package/dist/acp/tool-event-mapper.js +14 -6
  2. package/dist/cli-core.js +1 -0
  3. package/dist/fullscreen-mode.js +165 -0
  4. package/dist/mcp-capability-provider.js +1 -0
  5. package/dist/pi-transcript.js +19 -7
  6. package/dist/pi-tui-layout.js +266 -0
  7. package/dist/pi-tui-runner.js +67 -4
  8. package/dist/runtime-host-tui-command.js +7 -2
  9. package/dist/skill-highlight-editor.js +9 -0
  10. package/dist/tui-copy-catalog.js +515 -488
  11. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  12. package/node_modules/@maka/core/dist/connection-readiness.js +1 -1
  13. package/node_modules/@maka/core/dist/display-redaction.js +3 -2
  14. package/node_modules/@maka/core/dist/model-catalog.js +2 -1
  15. package/node_modules/@maka/core/dist/provider-auth.js +2 -1
  16. package/node_modules/@maka/core/dist/provider-registry.js +1 -11
  17. package/node_modules/@maka/core/dist/provider-retirement.js +45 -0
  18. package/node_modules/@maka/core/dist/redaction.js +4 -3
  19. package/node_modules/@maka/core/dist/relative-time-policy.js +3 -2
  20. package/node_modules/@maka/core/dist/runtime-event.js +5 -1
  21. package/node_modules/@maka/core/dist/session-trace.js +4 -1
  22. package/node_modules/@maka/core/dist/session.js +5 -0
  23. package/node_modules/@maka/core/dist/tool-quiet-preview.js +3 -2
  24. package/node_modules/@maka/core/dist/tool-result-status.js +12 -1
  25. package/node_modules/@maka/core/dist/ui-locale.js +8 -2
  26. package/node_modules/@maka/core/package.json +1 -0
  27. package/node_modules/@maka/runtime/dist/computer-use-tools.js +94 -22
  28. package/node_modules/@maka/runtime/dist/mcp-tools.js +1 -0
  29. package/node_modules/@maka/runtime/dist/read-page.js +28 -10
  30. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +1 -0
  31. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +1 -0
  32. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +1 -0
  33. package/node_modules/@maka/runtime/dist/session-trace-projection.js +6 -1
  34. package/node_modules/@maka/runtime/dist/test-connection.js +3 -1
  35. package/node_modules/@maka/runtime/dist/tool-runtime.js +65 -24
  36. package/node_modules/@maka/runtime/dist/workers/filesystem-worker.mjs +24 -10
  37. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +2 -1
  38. package/node_modules/@maka/runtime-host/dist/client/client-capability-channel.js +1 -0
  39. package/node_modules/@maka/runtime-host/dist/client/connection.js +19 -0
  40. package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +3 -2
  41. package/node_modules/@maka/runtime-host/dist/client/reconnecting-connection.js +8 -0
  42. package/node_modules/@maka/runtime-host/dist/protocol/artifact-change.js +46 -0
  43. package/node_modules/@maka/runtime-host/dist/protocol/client-capability.js +6 -1
  44. package/node_modules/@maka/runtime-host/dist/protocol/index.js +10 -1
  45. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +3 -1
  46. package/node_modules/@maka/runtime-host/dist/server/artifact-coordinator.js +4 -1
  47. package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +3 -0
  48. package/node_modules/@maka/runtime-host/dist/server/connection-session.js +8 -0
  49. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +3 -1
  50. package/node_modules/@maka/runtime-host/dist/server/host-change-feed.js +8 -0
  51. package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +1 -1
  52. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +3 -0
  53. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +49 -8
  54. package/node_modules/@maka/runtime-host/dist/server/session-sidecar-purge.js +5 -2
  55. package/node_modules/@maka/runtime-host/dist/server/shared-session-transcript.js +1 -0
  56. package/node_modules/@maka/storage/dist/config-transfer.js +2 -1
  57. package/node_modules/@maka/storage/dist/session-store-values.js +11 -1
  58. package/node_modules/node-pty/lib/windowsPtyAgent.js +7 -0
  59. package/node_modules/node-pty/lib/windowsTerminal.js +11 -1
  60. package/package.json +1 -1
@@ -86,7 +86,7 @@ export class AcpToolEventMapper {
86
86
  return;
87
87
  if (event.operationId)
88
88
  tool.meta.operationId = event.operationId;
89
- await this.#result(tool, event.isError, event.content, event.durationMs, event.contentOmitted === true);
89
+ await this.#result(tool, event.isError, event.content, event.durationMs, event.contentOmitted === true, event.outcome);
90
90
  return;
91
91
  }
92
92
  await this.#publish(tool);
@@ -97,7 +97,7 @@ export class AcpToolEventMapper {
97
97
  await this.#call(tool, message);
98
98
  }
99
99
  else if (message.type === 'tool_result') {
100
- await this.#result(this.#ensure(message.turnId, message.toolUseId), message.isError, message.content, message.durationMs, false);
100
+ await this.#result(this.#ensure(message.turnId, message.toolUseId), message.isError, message.content, message.durationMs, false, message.outcome);
101
101
  }
102
102
  }
103
103
  async pendingInteraction(pending) {
@@ -203,9 +203,10 @@ export class AcpToolEventMapper {
203
203
  }
204
204
  return tool;
205
205
  }
206
- async #result(tool, isError, result, durationMs, omitted) {
206
+ async #result(tool, isError, result, durationMs, omitted, outcome) {
207
207
  const resultDigest = digestValue({
208
208
  isError,
209
+ outcome,
209
210
  result: omitted ? null : result,
210
211
  durationMs,
211
212
  omitted,
@@ -215,7 +216,7 @@ export class AcpToolEventMapper {
215
216
  if (omitted)
216
217
  tool.resultAnnounced = true;
217
218
  tool.terminal = true;
218
- const hostStatus = toolResultActivityStatus(isError, omitted ? undefined : result);
219
+ const hostStatus = toolResultActivityStatus(isError, omitted ? undefined : result, outcome);
219
220
  tool.status = hostStatus === 'completed' ? 'completed' : 'failed';
220
221
  tool.meta.hostStatus = hostStatus;
221
222
  if (durationMs !== undefined)
@@ -315,7 +316,11 @@ export class AcpToolEventMapper {
315
316
  type: 'text',
316
317
  text: `[${output.stream}]${output.redacted ? ' [redacted]' : ''} ${output.chunk}`,
317
318
  _meta: {
318
- maka: { sequence: output.seq, stream: output.stream, redacted: output.redacted },
319
+ maka: {
320
+ sequence: output.seq,
321
+ stream: output.stream,
322
+ redacted: output.redacted,
323
+ },
319
324
  },
320
325
  },
321
326
  });
@@ -412,7 +417,10 @@ function bounded(text, max, suffix = '…') {
412
417
  const before = text.charCodeAt(length - 1);
413
418
  if (before >= 0xd800 && before <= 0xdbff)
414
419
  length -= 1;
415
- return { text: `${text.slice(0, length)}${suffix}`, dropped: text.length - length };
420
+ return {
421
+ text: `${text.slice(0, length)}${suffix}`,
422
+ dropped: text.length - length,
423
+ };
416
424
  }
417
425
  function textContent(text) {
418
426
  return [{ type: 'content', content: { type: 'text', text } }];
package/dist/cli-core.js CHANGED
@@ -668,6 +668,7 @@ export async function runMakaCli(argv = process.argv.slice(2), options = RELEASE
668
668
  locale: locale.locale,
669
669
  cwd: process.cwd(),
670
670
  onProcessExit: handleMakaCliProcessExit,
671
+ buildVersion: version,
671
672
  ...(command.resumeSessionId ? { resumeSessionId: command.resumeSessionId } : {}),
672
673
  ...(command.resumeCwd ? { resumeCwd: command.resumeCwd } : {}),
673
674
  ...(command.hostProfileId ? { hostProfileId: command.hostProfileId } : {}),
@@ -0,0 +1,165 @@
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 { spawn } from 'node:child_process';
20
+ import { defineUiMessageCatalog, formatUiMessage, resolveUiMessageCatalog, } from '@maka/core/ui-locale';
21
+ import { TUI_COPY_RESOURCES } from './tui-copy-catalog.js';
22
+ /**
23
+ * Nightly trial switch for the fullscreen (alternate-screen) TUI, issue #4136.
24
+ *
25
+ * The fullscreen path swaps `TuiMainScreen` for `TuiAltScreen`: the composer
26
+ * and status line stay anchored to the bottom of the screen while the
27
+ * transcript scrolls in an application-owned viewport. This is a scoped
28
+ * product experiment, not a stable mode: it defaults on only for nightly
29
+ * builds and must not grow a permanent user-facing toggle until the nightly
30
+ * evidence is in (issue non-goals).
31
+ *
32
+ * Precedence, highest first:
33
+ * 1. `setting` — an explicit caller decision (`MakaPiTuiInput.tuiFullscreen`),
34
+ * used by embeddings and tests.
35
+ * 2. `override` — the `MAKA_TUI_FULLSCREEN` environment variable. `1`/`true`
36
+ * opts a release build in; `0`/`false` opts a nightly build out.
37
+ * 3. `packageVersion` — nightly default: on for `-dev.` versions (the
38
+ * Product Nightly identity in scripts/product-nightly.mjs), off otherwise.
39
+ */
40
+ export const TUI_FULLSCREEN_ENV = 'MAKA_TUI_FULLSCREEN';
41
+ export function isNightlyPackageVersion(version) {
42
+ if (!version)
43
+ return false;
44
+ // Product Nightly versions look like `0.2.0-dev.<runNumber>.<YYYYMMDD>`;
45
+ // formal releases are always a stable product version.
46
+ return /-dev\.[1-9]\d*\.\d{8}$/u.test(version);
47
+ }
48
+ export function resolveTuiFullscreen(resolution = {}) {
49
+ if (typeof resolution.setting === 'boolean')
50
+ return resolution.setting;
51
+ const override = resolution.override?.trim().toLowerCase();
52
+ if (override === '1' || override === 'true')
53
+ return true;
54
+ if (override === '0' || override === 'false')
55
+ return false;
56
+ return isNightlyPackageVersion(resolution.packageVersion);
57
+ }
58
+ /**
59
+ * Counts transcript lines appended while the user is scrolled away from the
60
+ * bottom — the "unread / new output" signal for the anchored-composer trial.
61
+ *
62
+ * Updated once per frame with the current window snapshot: growth accumulates
63
+ * while the user is away, arriving at the bottom clears the count. Shrinks
64
+ * (collapsing tool output, re-wraps) never manufacture unread lines; the
65
+ * counter is approximate by design — it is an attention hint, not an exact
66
+ * diff.
67
+ */
68
+ export class UnreadOutputCounter {
69
+ lastDocumentLines;
70
+ unreadLines = 0;
71
+ update(window) {
72
+ if (!window.followingEnd &&
73
+ this.lastDocumentLines !== undefined &&
74
+ window.documentLines > this.lastDocumentLines) {
75
+ this.unreadLines += window.documentLines - this.lastDocumentLines;
76
+ }
77
+ if (window.followingEnd)
78
+ this.unreadLines = 0;
79
+ this.lastDocumentLines = window.documentLines;
80
+ return this.unreadLines;
81
+ }
82
+ }
83
+ const TUI_UNREAD_INDICATOR_COPY = resolveUiMessageCatalog(defineUiMessageCatalog()(TUI_COPY_RESOURCES['unread-indicator']));
84
+ /** The rendered unread line: accent-colored, one row, empty when nothing is new. */
85
+ export function renderUnreadIndicator(unreadLines, accent, locale) {
86
+ if (unreadLines <= 0)
87
+ return [];
88
+ return [
89
+ accent(formatUiMessage(TUI_UNREAD_INDICATOR_COPY[locale].hint, { count: unreadLines }, locale)),
90
+ ];
91
+ }
92
+ /**
93
+ * URL schemes a model-authored OSC 8 link may be opened with, mirroring the
94
+ * desktop's external-link guard (apps/desktop/src/main/external-link-guard.ts):
95
+ * web and mail only. Assistant Markdown is rendered with the raw href, so
96
+ * everything else — `file:`, `javascript:`, unknown handlers, UNC paths —
97
+ * must never reach an OS opener from a click.
98
+ */
99
+ const OPENABLE_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:']);
100
+ export function isOpenableExternalUrl(url) {
101
+ try {
102
+ return OPENABLE_URL_PROTOCOLS.has(new URL(url).protocol);
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ }
108
+ /**
109
+ * Opens an OSC 8 hyperlink activated by a primary-button click in the
110
+ * fullscreen viewport. Model-authored hrefs are untrusted input, so the
111
+ * opener is deliberately narrow:
112
+ *
113
+ * - Only `http:`, `https:`, and `mailto:` targets are handed off at all.
114
+ * - Windows never routes the URL through cmd.exe — `spawn`'s argument
115
+ * quoting does not escape shell metacharacters (`&` would start a second
116
+ * command under `cmd /c start`), so the opener is `rundll32
117
+ * url.dll,FileProtocolHandler`, which receives the URL as a single argv
118
+ * element and hands it to ShellExecute. The DLL/entrypoint half of the
119
+ * command line is a compile-time constant, so a hostile URL cannot
120
+ * redirect it.
121
+ * - macOS/Linux openers take the URL as a plain argv element (no shell).
122
+ *
123
+ * Failures are swallowed — a dead link must never take the TUI down.
124
+ */
125
+ /**
126
+ * Spawns a detached, fire-and-forget opener process. `spawn` reports a
127
+ * missing binary (and other spawn failures) asynchronously via the child's
128
+ * `error` event — with no listener attached, Node re-emits it as an
129
+ * uncaughtException, which the TUI's handler treats as fatal and begins
130
+ * session teardown. The child is therefore kept and its error swallowed: a
131
+ * dead link must degrade to "nothing opened", never end the session.
132
+ * Synchronous throws (invalid arguments) are swallowed here as well.
133
+ */
134
+ function spawnDetached(spawnProcess, command, args, windowsHide = false) {
135
+ try {
136
+ const child = spawnProcess(command, args, {
137
+ detached: true,
138
+ stdio: 'ignore',
139
+ ...(windowsHide ? { windowsHide: true } : {}),
140
+ });
141
+ child.on('error', () => { });
142
+ child.unref();
143
+ }
144
+ catch {
145
+ // Best-effort only; the terminal may also offer its own link handling.
146
+ }
147
+ }
148
+ export function openExternalUrl(url, platform = process.platform, spawnProcess = spawn) {
149
+ if (!isOpenableExternalUrl(url))
150
+ return;
151
+ if (platform === 'darwin') {
152
+ spawnDetached(spawnProcess, 'open', [url]);
153
+ return;
154
+ }
155
+ if (platform === 'win32') {
156
+ // Never cmd.exe: `spawn`'s argument quoting does not escape shell
157
+ // metacharacters, and `cmd /c start` would let a model-authored `&`
158
+ // start a second command. rundll32 receives the URL as a single argv
159
+ // element and hands it to ShellExecute; the DLL/entrypoint half is a
160
+ // compile-time constant, so a hostile URL cannot redirect what runs.
161
+ spawnDetached(spawnProcess, 'rundll32', ['url.dll,FileProtocolHandler', url], true);
162
+ return;
163
+ }
164
+ spawnDetached(spawnProcess, 'xdg-open', [url]);
165
+ }
@@ -103,6 +103,7 @@ function projectMcpTool(tool, wireServerId) {
103
103
  }
104
104
  function projectMcpResult(result) {
105
105
  return {
106
+ outcome: 'success',
106
107
  content: result.content.map((block) => structuredClone(block)),
107
108
  ...(result.structuredContent === undefined
108
109
  ? {}
@@ -544,7 +544,7 @@ export function applyMakaSessionEventToTranscript(state, event) {
544
544
  if (tool) {
545
545
  if (tool.suppressed)
546
546
  unsuppressToolAtTail(state, tool);
547
- tool.callStatus = toolResultActivityStatus(event.isError, event.content);
547
+ tool.callStatus = toolResultActivityStatus(event.isError, event.content, event.outcome);
548
548
  if (shellRun) {
549
549
  if (tool.toolName === 'Bash') {
550
550
  applyShellRunResult(tool, shellRun);
@@ -575,7 +575,7 @@ export function applyMakaSessionEventToTranscript(state, event) {
575
575
  ...(!event.contentOmitted ? { result: event.content } : {}),
576
576
  resultVersion: event.contentOmitted ? 0 : 1,
577
577
  durationMs: event.durationMs,
578
- callStatus: toolResultActivityStatus(event.isError, event.content),
578
+ callStatus: toolResultActivityStatus(event.isError, event.content, event.outcome),
579
579
  expanded: state.expandAllTools,
580
580
  });
581
581
  }
@@ -714,9 +714,21 @@ export function applyMakaSessionEventToTranscript(state, event) {
714
714
  }
715
715
  function storedMessagesToTranscriptEntries(messages) {
716
716
  const entries = [];
717
- const resultsByToolUseId = new Map(messages
718
- .filter((message) => message.type === 'tool_result')
719
- .map((message) => [message.toolUseId, message]));
717
+ const latestCallsByUseId = new Map();
718
+ const resultsByCall = new Map();
719
+ // Append order bounds an opaque ID's ownership: a result belongs to the latest
720
+ // preceding call, and a later call with that ID starts a new interval.
721
+ for (const message of messages) {
722
+ if (message.type === 'tool_call') {
723
+ latestCallsByUseId.set(message.id, message);
724
+ continue;
725
+ }
726
+ if (message.type !== 'tool_result')
727
+ continue;
728
+ const call = latestCallsByUseId.get(message.toolUseId);
729
+ if (call)
730
+ resultsByCall.set(call, message);
731
+ }
720
732
  const turnStatusById = new Map(deriveTurnRecords(messages).map((turn) => [turn.turnId, turn.status]));
721
733
  for (const message of messages) {
722
734
  switch (message.type) {
@@ -750,7 +762,7 @@ function storedMessagesToTranscriptEntries(messages) {
750
762
  break;
751
763
  }
752
764
  case 'tool_call':
753
- entries.push(storedToolToTranscriptEntry(message, resultsByToolUseId.get(message.id), turnStatusById.get(message.turnId)));
765
+ entries.push(storedToolToTranscriptEntry(message, resultsByCall.get(message), turnStatusById.get(message.turnId)));
754
766
  break;
755
767
  case 'system_note': {
756
768
  const entry = systemNoteToTranscriptEntry(message);
@@ -780,7 +792,7 @@ function storedToolToTranscriptEntry(call, result, turnStatus) {
780
792
  resultVersion: result ? 1 : 0,
781
793
  ...(result?.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
782
794
  callStatus: result
783
- ? toolResultActivityStatus(result.isError, result.content)
795
+ ? toolResultActivityStatus(result.isError, result.content, result.outcome)
784
796
  : unfinishedToolActivityStatus(turnStatus),
785
797
  expanded: false,
786
798
  };
@@ -21,7 +21,16 @@ import { Container } from '@earendil-works/pi-tui';
21
21
  // compare the same canonical lines pi-tui diffs, and pi-tui normalizes Thai/Lao
22
22
  // AM sequences before its diff. Pinned to pi-tui 0.80.3.
23
23
  import { normalizeTerminalOutput } from '@earendil-works/pi-tui/dist/utils.js';
24
+ // Separate statements, anchored below the deep import rather than appended to
25
+ // the Container import: upstream inserts its UiLocale import directly after
26
+ // the Container line and its TranscriptDocument import after the
27
+ // pi-transcript block, and imports in this gap keep this PR's changes out of
28
+ // both of those diff gaps so the three-way merge resolves cleanly.
29
+ import { ScrollView } from '@earendil-works/pi-tui';
30
+ import { renderUnreadIndicator, UnreadOutputCounter } from './fullscreen-mode.js';
24
31
  import { renderMakaPiActivityStrip, renderMakaPiPendingQueue, renderMakaPiStatusLine, renderMakaPiTranscript, } from './pi-transcript.js';
32
+ /** Rows the transcript keeps in the fullscreen layout even on tiny terminals. */
33
+ const FULLSCREEN_TRANSCRIPT_MIN_ROWS = 1;
25
34
  export function fitPendingQueueLines(lines, maxRows) {
26
35
  const rowBudget = Math.max(0, Math.floor(maxRows));
27
36
  if (lines.length <= rowBudget)
@@ -32,6 +41,28 @@ export function fitPendingQueueLines(lines, maxRows) {
32
41
  return [`… ${lines.length} more`];
33
42
  return [...lines.slice(0, rowBudget - 1), `… ${lines.length - rowBudget + 1} more`];
34
43
  }
44
+ /**
45
+ * The fullscreen chrome's editor/pending-queue row account, isolated in one
46
+ * function so the budget and its consumers cannot drift apart: from the rows
47
+ * available below the transcript, reserve the editor's minimum viewport (the
48
+ * queue trims first), then give the editor whatever remains, never less than
49
+ * that minimum while any budget is left to reserve it from. `rowsAvailable` is
50
+ * everything already carved out of the terminal rows — the chrome subtracts the
51
+ * status line, activity strip, unread indicator, and the reserved transcript row
52
+ * before calling. The main-screen layout keeps its own (upstream-owned)
53
+ * accounting; its contract is identical, so the two can be unified onto this
54
+ * function after merge.
55
+ */
56
+ function budgetEditorAndPendingRows(rowsAvailable, allPendingLines, editor) {
57
+ const budget = Math.max(0, Math.floor(rowsAvailable));
58
+ const minimumEditorRows = editor.minimumViewportRows();
59
+ const pendingRowsAvailable = Math.max(0, budget - minimumEditorRows);
60
+ const pendingLines = fitPendingQueueLines(allPendingLines, pendingRowsAvailable);
61
+ return {
62
+ pendingLines,
63
+ editorRows: budget === 0 ? 0 : Math.max(minimumEditorRows, budget - pendingLines.length),
64
+ };
65
+ }
35
66
  export class MakaTranscriptComponent {
36
67
  state;
37
68
  metadata;
@@ -311,3 +342,238 @@ export class MakaPiLayoutComponent extends Container {
311
342
  return Math.max(current, tailTop);
312
343
  }
313
344
  }
345
+ /**
346
+ * The ScrollView child of the fullscreen layout: renders the complete
347
+ * transcript document (the scroll view windows it) and exposes the rendered
348
+ * line count so the scroll view can count lines appended while the user is
349
+ * scrolled away. Lives in pi-tui-layout.ts alongside the other transcript
350
+ * adapters.
351
+ */
352
+ export class MakaTranscriptDocumentComponent {
353
+ transcript;
354
+ /** Rendered transcript document lines from the most recent frame. */
355
+ documentLines = 0;
356
+ constructor(transcript) {
357
+ this.transcript = transcript;
358
+ }
359
+ invalidate() {
360
+ this.transcript.invalidate();
361
+ }
362
+ render(width) {
363
+ const lines = this.transcript.render(width);
364
+ this.documentLines = lines.length;
365
+ return lines;
366
+ }
367
+ }
368
+ /**
369
+ * The fullscreen layout's transcript scroll view. `ScrollView.updateLayout`
370
+ * runs at the layout pass with this frame's content height and scroll state —
371
+ * the one point in the frame where the window is fresh — so this subclass
372
+ * computes the unread count there and compares it with what the anchored
373
+ * chrome actually rendered (`presentedUnread`, written back by the chrome via
374
+ * its `UnreadOutputFeed`). The chrome is measured before the scroll view is
375
+ * laid out, so its view lags one frame; when the rendered count falls behind,
376
+ * a catch-up render is requested and the indicator settles deterministically.
377
+ */
378
+ export class MakaTranscriptScrollView extends ScrollView {
379
+ document;
380
+ /** Unread count as of the most recent layout pass. */
381
+ computedUnread = 0;
382
+ /** Unread count the chrome last rendered. */
383
+ presentedUnread = 0;
384
+ counter = new UnreadOutputCounter();
385
+ constructor(document, options) {
386
+ super(document, options);
387
+ this.document = document;
388
+ }
389
+ updateLayout(contentHeight, viewportHeight, requestRender) {
390
+ super.updateLayout(contentHeight, viewportHeight, requestRender);
391
+ this.computedUnread = this.counter.update({
392
+ followingEnd: this.isFollowingEnd,
393
+ documentLines: this.document.documentLines,
394
+ });
395
+ if (this.computedUnread !== this.presentedUnread)
396
+ requestRender();
397
+ }
398
+ }
399
+ /**
400
+ * The anchored bottom chrome of the fullscreen layout (issue #4136): unread
401
+ * indicator, activity strip, pending queue, editor, and status line — stacked
402
+ * below the scrolling transcript and pinned to the screen bottom by the
403
+ * VStack. The transcript region above owns its own scrolling, so unlike
404
+ * `MakaPiLayoutComponent` this component emits only the chrome rows and never
405
+ * pads or windows the transcript.
406
+ *
407
+ * The unread count comes from the scroll view's `UnreadOutputFeed` (see
408
+ * `MakaTranscriptScrollView`): the layout engine measures this component
409
+ * before the scroll view is laid out, so the count it reads lags one frame and
410
+ * the scroll view requests a catch-up render whenever the rendered count falls
411
+ * behind.
412
+ *
413
+ * `renderGeometry.viewportTop` is pinned to 0: the app owns the whole screen,
414
+ * no rendered line sits in untouchable terminal scrollback, so the
415
+ * entry-freeze and viewport-restricted expansion toggles that main-screen mode
416
+ * needs (#1097, #1134, #4011) must not engage — every entry stays
417
+ * re-renderable and globally toggleable.
418
+ */
419
+ export class MakaFullscreenChromeComponent extends Container {
420
+ state;
421
+ activityStrip;
422
+ pendingQueue;
423
+ editor;
424
+ statusLine;
425
+ terminal;
426
+ unreadFeed;
427
+ accent;
428
+ locale;
429
+ todoIndicator;
430
+ /**
431
+ * Host-owned blocking interaction (the inline user-question prompt). The
432
+ * fullscreen layout root renders and routes only the mounted tree, so in
433
+ * fullscreen this chrome owns the role `MakaPiLayoutComponent` owns on the
434
+ * main screen: it draws the interaction with the same row budgeting (the
435
+ * editor yields to it, the transcript keeps its minimum row) and mounts it
436
+ * as a child so input routing reaches it (#4136 review P1).
437
+ */
438
+ blockingInteraction;
439
+ constructor(state, activityStrip, pendingQueue, editor, statusLine, terminal, unreadFeed, accent, locale, todoIndicator) {
440
+ super();
441
+ this.state = state;
442
+ this.activityStrip = activityStrip;
443
+ this.pendingQueue = pendingQueue;
444
+ this.editor = editor;
445
+ this.statusLine = statusLine;
446
+ this.terminal = terminal;
447
+ this.unreadFeed = unreadFeed;
448
+ this.accent = accent;
449
+ this.locale = locale;
450
+ this.todoIndicator = todoIndicator;
451
+ }
452
+ setBlockingInteraction(interaction) {
453
+ if (this.blockingInteraction === interaction)
454
+ return;
455
+ if (this.blockingInteraction)
456
+ this.removeChild(this.blockingInteraction);
457
+ this.blockingInteraction = interaction;
458
+ if (interaction)
459
+ this.addChild(interaction);
460
+ }
461
+ render(width) {
462
+ const unreadLines = this.unreadFeed.current();
463
+ const indicatorLines = renderUnreadIndicator(unreadLines, this.accent, this.locale);
464
+ this.unreadFeed.present(unreadLines);
465
+ // App-owned viewport: no terminal scrollback exists, so expansion toggles
466
+ // may retarget any entry and no entry is ever frozen off-screen.
467
+ this.state.renderGeometry.viewportTop = 0;
468
+ const allActivityLines = this.activityStrip.render(width);
469
+ // The activity strip renders one row even when idle (an empty string);
470
+ // an all-empty strip would burn a permanent chrome row between the
471
+ // transcript and the editor, so it collapses to nothing when idle.
472
+ const activityRows = allActivityLines.some((line) => line.length > 0) ? allActivityLines : [];
473
+ // #1064's separator, fullscreen edition: keep "Working... Ns" from
474
+ // touching the last visible transcript line when a turn is running. This
475
+ // row is part of the chrome's emitted height, so the budget must count it
476
+ // too — otherwise the VStack clips the chrome's bottom (the status line)
477
+ // on short terminals while a Host question is live (#4136 review P2).
478
+ const activitySeparator = activityRows.length > 0 ? [''] : [];
479
+ const allPendingLines = this.pendingQueue.render(width);
480
+ const statusLines = this.statusLine.render(width);
481
+ const blockingInteraction = this.blockingInteraction;
482
+ // Same input selection as MakaPiLayoutComponent: an active blocking
483
+ // question replaces the composer in the chrome stack.
484
+ const input = blockingInteraction ?? this.editor;
485
+ const minimumInputRows = input.minimumViewportRows();
486
+ // Same row account as MakaPiLayoutComponent via budgetEditorAndPendingRows,
487
+ // with the unread indicator, the live-activity separator, the Todo row, and
488
+ // the transcript's minimum row reserved up front so the chrome's intrinsic
489
+ // height can never push the transcript below one row or overflow the
490
+ // allocation the VStack gives it.
491
+ let effectiveMargin = blockingInteraction ? 1 : 0;
492
+ const fixedChrome = indicatorLines.length +
493
+ activitySeparator.length +
494
+ activityRows.length +
495
+ statusLines.length +
496
+ effectiveMargin +
497
+ FULLSCREEN_TRANSCRIPT_MIN_ROWS;
498
+ const todoLines = this.todoIndicator &&
499
+ this.terminal.rows > fixedChrome + allPendingLines.length + minimumInputRows
500
+ ? (this.todoIndicator.render(width) ?? []).slice(0, 1)
501
+ : [];
502
+ // With a blocking question live, the pending queue yields to the question
503
+ // (budgeted against the question's minimum rows, as on the main screen)
504
+ // and the question then takes the remaining rows; without one, the editor
505
+ // and pending queue split the budget through the shared helper.
506
+ const budgetChrome = (fixed) => blockingInteraction
507
+ ? (() => {
508
+ const questionPending = fitPendingQueueLines(allPendingLines, Math.max(0, this.terminal.rows - fixed - todoLines.length - minimumInputRows));
509
+ return {
510
+ pendingLines: questionPending,
511
+ inputRows: this.terminal.rows - fixed - todoLines.length - questionPending.length,
512
+ };
513
+ })()
514
+ : (() => {
515
+ const budgeted = budgetEditorAndPendingRows(this.terminal.rows - fixed - todoLines.length, allPendingLines, this.editor);
516
+ return { pendingLines: budgeted.pendingLines, inputRows: budgeted.editorRows };
517
+ })();
518
+ let effectiveFixedChrome = fixedChrome;
519
+ let effectiveSeparator = activitySeparator;
520
+ let effectiveActivityRows = activityRows;
521
+ let effectiveIndicator = indicatorLines;
522
+ let { pendingLines, inputRows } = budgetChrome(effectiveFixedChrome);
523
+ // Squeeze rule (#4136 review P2): if the remaining budget cannot even give
524
+ // the input its own minimum, the input is load-bearing and clamps to that
525
+ // minimum, so decorative chrome rows yield in priority order until the
526
+ // frame fits the chrome's VStack allocation: the live activity strip and
527
+ // its separator first, then the question's blank margin row (pure
528
+ // decoration), and only as a last resort the unread banner — information
529
+ // yields after decoration. A user scrolled away can catch up after
530
+ // answering; a clipped status line or an unreachable answer field cannot.
531
+ // The second round of this review caught the residual case at the 8-row
532
+ // boundary: an unread banner plus a three-option question still pushed the
533
+ // chrome two rows past its allocation because only the strip yielded.
534
+ const yielders = [
535
+ () => {
536
+ const drop = effectiveSeparator.length + effectiveActivityRows.length;
537
+ if (drop === 0)
538
+ return 0;
539
+ effectiveSeparator = [];
540
+ effectiveActivityRows = [];
541
+ return drop;
542
+ },
543
+ () => {
544
+ if (effectiveMargin === 0)
545
+ return 0;
546
+ effectiveMargin = 0;
547
+ return 1;
548
+ },
549
+ () => {
550
+ const drop = effectiveIndicator.length;
551
+ if (drop === 0)
552
+ return 0;
553
+ effectiveIndicator = [];
554
+ return drop;
555
+ },
556
+ ];
557
+ for (const yieldRows of yielders) {
558
+ if (inputRows >= minimumInputRows)
559
+ break;
560
+ const dropped = yieldRows();
561
+ if (dropped === 0)
562
+ continue;
563
+ effectiveFixedChrome -= dropped;
564
+ ({ pendingLines, inputRows } = budgetChrome(effectiveFixedChrome));
565
+ }
566
+ input.setViewportRows(inputRows);
567
+ const inputLines = input.render(width);
568
+ return [
569
+ ...effectiveIndicator,
570
+ ...effectiveSeparator,
571
+ ...effectiveActivityRows,
572
+ ...pendingLines,
573
+ ...todoLines,
574
+ ...inputLines,
575
+ ...(effectiveMargin > 0 ? [''] : []),
576
+ ...statusLines,
577
+ ];
578
+ }
579
+ }