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.
- package/dist/acp/tool-event-mapper.js +14 -6
- package/dist/cli-core.js +1 -0
- package/dist/fullscreen-mode.js +165 -0
- package/dist/mcp-capability-provider.js +1 -0
- package/dist/pi-transcript.js +19 -7
- package/dist/pi-tui-layout.js +266 -0
- package/dist/pi-tui-runner.js +67 -4
- package/dist/runtime-host-tui-command.js +7 -2
- package/dist/skill-highlight-editor.js +9 -0
- package/dist/tui-copy-catalog.js +515 -488
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/connection-readiness.js +1 -1
- package/node_modules/@maka/core/dist/display-redaction.js +3 -2
- package/node_modules/@maka/core/dist/model-catalog.js +2 -1
- package/node_modules/@maka/core/dist/provider-auth.js +2 -1
- package/node_modules/@maka/core/dist/provider-registry.js +1 -11
- package/node_modules/@maka/core/dist/provider-retirement.js +45 -0
- package/node_modules/@maka/core/dist/redaction.js +4 -3
- package/node_modules/@maka/core/dist/relative-time-policy.js +3 -2
- package/node_modules/@maka/core/dist/runtime-event.js +5 -1
- package/node_modules/@maka/core/dist/session-trace.js +4 -1
- package/node_modules/@maka/core/dist/session.js +5 -0
- package/node_modules/@maka/core/dist/tool-quiet-preview.js +3 -2
- package/node_modules/@maka/core/dist/tool-result-status.js +12 -1
- package/node_modules/@maka/core/dist/ui-locale.js +8 -2
- package/node_modules/@maka/core/package.json +1 -0
- package/node_modules/@maka/runtime/dist/computer-use-tools.js +94 -22
- package/node_modules/@maka/runtime/dist/mcp-tools.js +1 -0
- package/node_modules/@maka/runtime/dist/read-page.js +28 -10
- package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +1 -0
- package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +1 -0
- package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +1 -0
- package/node_modules/@maka/runtime/dist/session-trace-projection.js +6 -1
- package/node_modules/@maka/runtime/dist/test-connection.js +3 -1
- package/node_modules/@maka/runtime/dist/tool-runtime.js +65 -24
- package/node_modules/@maka/runtime/dist/workers/filesystem-worker.mjs +24 -10
- package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +2 -1
- package/node_modules/@maka/runtime-host/dist/client/client-capability-channel.js +1 -0
- package/node_modules/@maka/runtime-host/dist/client/connection.js +19 -0
- package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +3 -2
- package/node_modules/@maka/runtime-host/dist/client/reconnecting-connection.js +8 -0
- package/node_modules/@maka/runtime-host/dist/protocol/artifact-change.js +46 -0
- package/node_modules/@maka/runtime-host/dist/protocol/client-capability.js +6 -1
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +10 -1
- package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +3 -1
- package/node_modules/@maka/runtime-host/dist/server/artifact-coordinator.js +4 -1
- package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +3 -0
- package/node_modules/@maka/runtime-host/dist/server/connection-session.js +8 -0
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +3 -1
- package/node_modules/@maka/runtime-host/dist/server/host-change-feed.js +8 -0
- package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +3 -0
- package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +49 -8
- package/node_modules/@maka/runtime-host/dist/server/session-sidecar-purge.js +5 -2
- package/node_modules/@maka/runtime-host/dist/server/shared-session-transcript.js +1 -0
- package/node_modules/@maka/storage/dist/config-transfer.js +2 -1
- package/node_modules/@maka/storage/dist/session-store-values.js +11 -1
- package/node_modules/node-pty/lib/windowsPtyAgent.js +7 -0
- package/node_modules/node-pty/lib/windowsTerminal.js +11 -1
- 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: {
|
|
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 {
|
|
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
|
+
}
|
package/dist/pi-transcript.js
CHANGED
|
@@ -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
|
|
718
|
-
|
|
719
|
-
|
|
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,
|
|
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
|
};
|
package/dist/pi-tui-layout.js
CHANGED
|
@@ -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
|
+
}
|