praxis-agent 0.43.0 → 0.44.0
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/README.md +48 -47
- package/dist/application/native-session-projection.d.ts +4 -0
- package/dist/application/native-session-projection.js +135 -0
- package/dist/application/session-service.d.ts +0 -1
- package/dist/application/session-service.js +9 -170
- package/dist/application/top-level-agent-manager.d.ts +7 -0
- package/dist/application/top-level-agent-manager.js +21 -3
- package/dist/cli/controls.js +4 -0
- package/dist/cli/interactive.js +112 -156
- package/dist/cli/protocol.d.ts +5 -0
- package/dist/cli/protocol.js +45 -0
- package/dist/cli/provider-auth-command.d.ts +32 -0
- package/dist/cli/provider-auth-command.js +249 -0
- package/dist/cli/tui/ansi-frame-renderer.d.ts +2 -0
- package/dist/cli/tui/ansi-frame-renderer.js +109 -21
- package/dist/cli/tui/ansi-surface.d.ts +6 -22
- package/dist/cli/tui/ansi-surface.js +6 -126
- package/dist/cli/tui/ansi-theme.d.ts +3 -2
- package/dist/cli/tui/ansi-theme.js +22 -1
- package/dist/cli/tui/quiet-choice-rows.d.ts +26 -0
- package/dist/cli/tui/quiet-choice-rows.js +302 -0
- package/dist/cli/tui/quiet-frame-adapter.d.ts +18 -0
- package/dist/cli/tui/quiet-frame-adapter.js +90 -0
- package/dist/cli/tui/quiet-frame.d.ts +43 -0
- package/dist/cli/tui/quiet-frame.js +346 -0
- package/dist/cli/tui/quiet-operational-rows.d.ts +17 -0
- package/dist/cli/tui/quiet-operational-rows.js +413 -0
- package/dist/cli/tui/quiet-screen-projector.d.ts +25 -0
- package/dist/cli/tui/quiet-screen-projector.js +110 -0
- package/dist/cli/tui/quiet-settings-rows.d.ts +17 -0
- package/dist/cli/tui/quiet-settings-rows.js +490 -0
- package/dist/cli/tui/status-line.d.ts +15 -9
- package/dist/cli/tui/status-line.js +15 -6
- package/dist/cli/tui/theme.js +20 -20
- package/dist/cli/tui/transcript-viewport.d.ts +5 -0
- package/dist/cli/tui/transcript-viewport.js +213 -78
- package/dist/cli/tui/tui-row-ir.js +4 -4
- package/dist/cli/workspace-trust-prompt.js +2 -2
- package/dist/cli-runtime.d.ts +34 -1
- package/dist/cli-runtime.js +587 -184
- package/dist/maintenance/doctor.js +100 -22
- package/dist/persistence/in-memory-transcript-store.js +1 -7
- package/dist/persistence/native-transcript-store.d.ts +0 -9
- package/dist/persistence/native-transcript-store.js +4 -38
- package/dist/persistence/provider-credential-vault.d.ts +72 -0
- package/dist/persistence/provider-credential-vault.js +498 -0
- package/dist/providers/codex-oauth.d.ts +104 -0
- package/dist/providers/codex-oauth.js +770 -0
- package/dist/providers/codex-subscription.d.ts +33 -0
- package/dist/providers/codex-subscription.js +849 -0
- package/dist/providers/provider-auth.d.ts +71 -0
- package/dist/providers/provider-auth.js +149 -0
- package/dist/providers/provider-registry.d.ts +52 -0
- package/dist/providers/provider-registry.js +187 -0
- package/dist/providers/provider-settings.d.ts +38 -0
- package/dist/providers/provider-settings.js +334 -0
- package/dist/security/workspace-trust.d.ts +4 -3
- package/dist/security/workspace-trust.js +35 -1
- package/package.json +8 -4
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { ProviderCredentialVault, } from '../persistence/provider-credential-vault.js';
|
|
2
|
+
import { deviceLoginWithCodexOAuth, loginWithCodexOAuth, } from '../providers/codex-oauth.js';
|
|
3
|
+
import { resolveProviderTarget } from '../providers/provider-settings.js';
|
|
4
|
+
import { resolveDataPlaneRoot } from '../persistence/data-plane.js';
|
|
5
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
6
|
+
const MAX_SECRET_BYTES = 64 * 1024;
|
|
7
|
+
function fail(message) {
|
|
8
|
+
throw new Error(message);
|
|
9
|
+
}
|
|
10
|
+
function identifier(value, label) {
|
|
11
|
+
if (!IDENTIFIER.test(value))
|
|
12
|
+
fail(`${label} must match [A-Za-z0-9][A-Za-z0-9._-]{0,127}`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function parse(args, options) {
|
|
16
|
+
if (args[0] === 'auth')
|
|
17
|
+
args = args.slice(1);
|
|
18
|
+
const action = args[0];
|
|
19
|
+
if (action !== 'status' &&
|
|
20
|
+
action !== 'set-key' &&
|
|
21
|
+
action !== 'login' &&
|
|
22
|
+
action !== 'logout') {
|
|
23
|
+
fail('auth requires status, set-key, login, or logout');
|
|
24
|
+
}
|
|
25
|
+
let provider;
|
|
26
|
+
let profile;
|
|
27
|
+
let json = options.json ?? false;
|
|
28
|
+
let noBrowser = options.noBrowser ?? false;
|
|
29
|
+
let device = options.device ?? false;
|
|
30
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
31
|
+
const value = args[index];
|
|
32
|
+
if (value === '--json') {
|
|
33
|
+
if (json)
|
|
34
|
+
fail('--json may only be specified once');
|
|
35
|
+
json = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (value === '--device') {
|
|
39
|
+
if (device)
|
|
40
|
+
fail('--device may only be specified once');
|
|
41
|
+
device = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (value === '--no-browser') {
|
|
45
|
+
if (noBrowser)
|
|
46
|
+
fail('--no-browser may only be specified once');
|
|
47
|
+
noBrowser = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (value === '--profile' || value?.startsWith('--profile=')) {
|
|
51
|
+
if (profile !== undefined)
|
|
52
|
+
fail('--profile may only be specified once');
|
|
53
|
+
const selected = value === '--profile' ? args[++index] : value.slice('--profile='.length);
|
|
54
|
+
if (typeof selected !== 'string' || selected.length === 0)
|
|
55
|
+
fail('--profile requires a value');
|
|
56
|
+
profile = identifier(selected, 'profile');
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (value?.startsWith('-'))
|
|
60
|
+
fail(`Unknown option: ${value}`);
|
|
61
|
+
if (typeof value !== 'string')
|
|
62
|
+
fail(`Unexpected operand for auth ${action}`);
|
|
63
|
+
if (provider !== undefined)
|
|
64
|
+
fail(`Unexpected operand for auth ${action}: ${value}`);
|
|
65
|
+
provider = identifier(value, 'provider');
|
|
66
|
+
}
|
|
67
|
+
if (options.profile !== undefined &&
|
|
68
|
+
profile !== undefined &&
|
|
69
|
+
options.profile !== profile)
|
|
70
|
+
fail('auth --profile conflicts with the selected profile');
|
|
71
|
+
if (options.providerProfile !== undefined &&
|
|
72
|
+
profile !== undefined &&
|
|
73
|
+
options.providerProfile !== profile)
|
|
74
|
+
fail('auth --profile conflicts with --provider-profile');
|
|
75
|
+
if (options.profile !== undefined &&
|
|
76
|
+
options.providerProfile !== undefined &&
|
|
77
|
+
options.profile !== options.providerProfile)
|
|
78
|
+
fail('auth --profile conflicts with --provider-profile');
|
|
79
|
+
const selectedProfile = profile ?? options.profile ?? options.providerProfile;
|
|
80
|
+
profile =
|
|
81
|
+
action === 'status' && selectedProfile === undefined
|
|
82
|
+
? undefined
|
|
83
|
+
: identifier(selectedProfile ?? 'default', 'profile');
|
|
84
|
+
if (action === 'status' && provider === undefined) {
|
|
85
|
+
// status accepts zero or one provider operand.
|
|
86
|
+
}
|
|
87
|
+
else if (action !== 'status' && provider === undefined) {
|
|
88
|
+
fail(`auth ${action} requires a provider`);
|
|
89
|
+
}
|
|
90
|
+
if (action === 'set-key' && provider === 'openai-codex')
|
|
91
|
+
fail('auth set-key does not support openai-codex; use auth login');
|
|
92
|
+
if (action === 'login' && provider !== 'openai-codex')
|
|
93
|
+
fail('auth login only supports openai-codex');
|
|
94
|
+
if (action !== 'login' && (device || noBrowser))
|
|
95
|
+
fail('--device and --no-browser are only valid with auth login');
|
|
96
|
+
return {
|
|
97
|
+
action,
|
|
98
|
+
...(provider === undefined ? {} : { provider }),
|
|
99
|
+
...(profile === undefined ? {} : { profile }),
|
|
100
|
+
json,
|
|
101
|
+
device,
|
|
102
|
+
noBrowser,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function defaultVault(options) {
|
|
106
|
+
const environment = options.environment ?? process.env;
|
|
107
|
+
const vaultOptions = {
|
|
108
|
+
configRoot: options.configRoot ?? resolveDataPlaneRoot({ environment }),
|
|
109
|
+
environment,
|
|
110
|
+
};
|
|
111
|
+
return new ProviderCredentialVault(vaultOptions);
|
|
112
|
+
}
|
|
113
|
+
async function assertCodexLoginEnabled(options, profileId) {
|
|
114
|
+
const environment = options.environment ?? process.env;
|
|
115
|
+
await resolveProviderTarget({
|
|
116
|
+
configRoot: options.configRoot ?? resolveDataPlaneRoot({ environment }),
|
|
117
|
+
cwd: options.cwd ?? process.cwd(),
|
|
118
|
+
environment,
|
|
119
|
+
provider: 'openai-codex',
|
|
120
|
+
profile: profileId,
|
|
121
|
+
model: 'codex-authentication',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function json(io, value) {
|
|
125
|
+
io.stdout(`${JSON.stringify(value)}\n`);
|
|
126
|
+
}
|
|
127
|
+
function text(io, message) {
|
|
128
|
+
io.stdout(`${message}\n`);
|
|
129
|
+
}
|
|
130
|
+
function metadata(value) {
|
|
131
|
+
return {
|
|
132
|
+
provider: value.key.providerId,
|
|
133
|
+
profile: value.key.profileId,
|
|
134
|
+
type: value.type,
|
|
135
|
+
updatedAt: value.updatedAt,
|
|
136
|
+
...(value.expiresAt === undefined
|
|
137
|
+
? {}
|
|
138
|
+
: { expiry: value.expiresAt > Date.now() ? 'valid' : 'expired' }),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function sortedMetadata(records, provider, profile) {
|
|
142
|
+
return records
|
|
143
|
+
.filter((record) => (provider === undefined || record.key.providerId === provider) &&
|
|
144
|
+
(profile === undefined || record.key.profileId === profile))
|
|
145
|
+
.sort((left, right) => {
|
|
146
|
+
const providerOrder = left.key.providerId.localeCompare(right.key.providerId);
|
|
147
|
+
return (providerOrder || left.key.profileId.localeCompare(right.key.profileId));
|
|
148
|
+
})
|
|
149
|
+
.map(metadata);
|
|
150
|
+
}
|
|
151
|
+
export function secretLine(value) {
|
|
152
|
+
if (Buffer.byteLength(value, 'utf8') > MAX_SECRET_BYTES)
|
|
153
|
+
fail('Credential exceeds the 64 KiB limit');
|
|
154
|
+
const stripped = value.endsWith('\r\n')
|
|
155
|
+
? value.slice(0, -2)
|
|
156
|
+
: value.endsWith('\r') || value.endsWith('\n')
|
|
157
|
+
? value.slice(0, -1)
|
|
158
|
+
: value;
|
|
159
|
+
if (/[\r\n]/u.test(stripped) || stripped.trim().length === 0)
|
|
160
|
+
fail('Credential must be exactly one non-blank line');
|
|
161
|
+
return stripped;
|
|
162
|
+
}
|
|
163
|
+
export async function executeProviderAuthCommand(args, options) {
|
|
164
|
+
const parsed = parse(args, options);
|
|
165
|
+
const vault = options.vault ?? defaultVault(options);
|
|
166
|
+
if (parsed.action === 'status') {
|
|
167
|
+
const credentials = sortedMetadata(await vault.list(), parsed.provider, parsed.profile);
|
|
168
|
+
if (parsed.json) {
|
|
169
|
+
json(options.io, { type: 'provider-auth-status', credentials });
|
|
170
|
+
}
|
|
171
|
+
else if (credentials.length === 0) {
|
|
172
|
+
text(options.io, 'No provider credentials configured.');
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
text(options.io, credentials
|
|
176
|
+
.map((credential) => `${credential.provider}/${credential.profile}: ${credential.type}${credential.expiry === undefined ? '' : ` (${credential.expiry})`}`)
|
|
177
|
+
.join('\n'));
|
|
178
|
+
}
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
const provider = parsed.provider;
|
|
182
|
+
if (provider === undefined)
|
|
183
|
+
fail(`auth ${parsed.action} requires a provider`);
|
|
184
|
+
const key = {
|
|
185
|
+
providerId: provider,
|
|
186
|
+
profileId: parsed.profile ?? 'default',
|
|
187
|
+
};
|
|
188
|
+
if (parsed.action === 'set-key') {
|
|
189
|
+
if (!options.io.readSecret)
|
|
190
|
+
fail('auth set-key requires an interactive secret reader');
|
|
191
|
+
const secret = secretLine(await options.io.readSecret('API key: ', options.signal));
|
|
192
|
+
await vault.modify(key, () => ({ type: 'api-key', secret }));
|
|
193
|
+
const result = {
|
|
194
|
+
provider: key.providerId,
|
|
195
|
+
profile: key.profileId,
|
|
196
|
+
type: 'api-key',
|
|
197
|
+
};
|
|
198
|
+
if (parsed.json)
|
|
199
|
+
json(options.io, result);
|
|
200
|
+
else
|
|
201
|
+
text(options.io, `Stored api-key credential for ${key.providerId}/${key.profileId}.`);
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
if (parsed.action === 'logout') {
|
|
205
|
+
const existing = await vault.read(key);
|
|
206
|
+
if (existing !== undefined)
|
|
207
|
+
await vault.delete(key);
|
|
208
|
+
const result = {
|
|
209
|
+
provider: key.providerId,
|
|
210
|
+
profile: key.profileId,
|
|
211
|
+
deleted: true,
|
|
212
|
+
};
|
|
213
|
+
if (parsed.json)
|
|
214
|
+
json(options.io, result);
|
|
215
|
+
else
|
|
216
|
+
text(options.io, `Logged out ${key.providerId}/${key.profileId}.`);
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
await (options.assertCodexLoginEnabled ??
|
|
220
|
+
((profileId) => assertCodexLoginEnabled(options, profileId)))(key.profileId);
|
|
221
|
+
const loginOptions = {
|
|
222
|
+
profileId: key.profileId,
|
|
223
|
+
vault: createCodexOAuthVault(vault),
|
|
224
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
225
|
+
...(parsed.noBrowser ? { noBrowser: true } : {}),
|
|
226
|
+
write: (message) => options.io.stderr?.(message),
|
|
227
|
+
};
|
|
228
|
+
const login = parsed.device
|
|
229
|
+
? (options.deviceLoginWithCodexOAuth ?? deviceLoginWithCodexOAuth)
|
|
230
|
+
: (options.loginWithCodexOAuth ?? loginWithCodexOAuth);
|
|
231
|
+
await login(loginOptions);
|
|
232
|
+
const result = {
|
|
233
|
+
provider: key.providerId,
|
|
234
|
+
profile: key.profileId,
|
|
235
|
+
type: 'oauth',
|
|
236
|
+
};
|
|
237
|
+
if (parsed.json)
|
|
238
|
+
json(options.io, result);
|
|
239
|
+
else
|
|
240
|
+
text(options.io, `Logged in ${key.providerId}/${key.profileId}.`);
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
function createCodexOAuthVault(vault) {
|
|
244
|
+
return {
|
|
245
|
+
read: (key) => vault.read(key),
|
|
246
|
+
modify: (key, callback) => vault.modify(key, async (current) => callback(current)),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=provider-auth-command.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { QuietFrameCursor } from './quiet-frame.js';
|
|
1
2
|
import type { TuiRow, TuiTextRole } from './tui-row-ir.js';
|
|
2
3
|
export interface AnsiFrameWriter {
|
|
3
4
|
write(chunk: string): void;
|
|
@@ -6,6 +7,7 @@ export interface AnsiFrame {
|
|
|
6
7
|
readonly columns: number;
|
|
7
8
|
readonly rows: number;
|
|
8
9
|
readonly lines: readonly TuiRow[];
|
|
10
|
+
readonly cursor?: QuietFrameCursor;
|
|
9
11
|
}
|
|
10
12
|
export interface AnsiFullscreenRendererOptions {
|
|
11
13
|
readonly writer: AnsiFrameWriter;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { stripVTControlCharacters } from 'node:util';
|
|
2
|
+
import { terminalGraphemes, terminalGraphemeWidth, terminalTextWidth, } from './transcript-viewport.js';
|
|
2
3
|
const ALTERNATE_SCREEN_ENTER = '\u001b[?1049h';
|
|
3
4
|
const ALTERNATE_SCREEN_LEAVE = '\u001b[?1049l';
|
|
4
5
|
const HIDE_CURSOR = '\u001b[?25l';
|
|
@@ -19,22 +20,44 @@ function validateFrame(frame) {
|
|
|
19
20
|
if (!Array.isArray(frame.lines)) {
|
|
20
21
|
throw new TypeError('ANSI frame lines must be an array');
|
|
21
22
|
}
|
|
23
|
+
if (frame.cursor !== undefined) {
|
|
24
|
+
if (frame.cursor === null ||
|
|
25
|
+
typeof frame.cursor !== 'object' ||
|
|
26
|
+
typeof frame.cursor.rowKey !== 'string' ||
|
|
27
|
+
frame.cursor.rowKey.length === 0 ||
|
|
28
|
+
!Number.isFinite(frame.cursor.column) ||
|
|
29
|
+
!Number.isInteger(frame.cursor.column) ||
|
|
30
|
+
frame.cursor.column < 0) {
|
|
31
|
+
throw new TypeError('ANSI frame cursor must have a non-empty row key and non-negative integer column');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
22
34
|
}
|
|
23
35
|
function renderLine(row, styles, columns) {
|
|
24
36
|
const segments = row.segments.map((segment) => ({
|
|
25
37
|
text: stripVTControlCharacters(segment.text).replace(/[\r\n]/gu, ''),
|
|
26
38
|
role: segment.role,
|
|
27
39
|
}));
|
|
28
|
-
const visibleLength = segments.reduce((length, segment) => length +
|
|
40
|
+
const visibleLength = segments.reduce((length, segment) => length + terminalTextWidth(segment.text), 0);
|
|
29
41
|
const wasTruncated = visibleLength > columns;
|
|
30
42
|
const targetLength = wasTruncated ? Math.max(0, columns - 1) : columns;
|
|
31
43
|
let remaining = targetLength;
|
|
32
44
|
let line = '';
|
|
45
|
+
let stopped = false;
|
|
33
46
|
for (const segment of segments) {
|
|
34
|
-
if (remaining === 0)
|
|
47
|
+
if (remaining === 0 || stopped)
|
|
35
48
|
break;
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
let text = '';
|
|
50
|
+
for (const grapheme of terminalGraphemes(segment.text)) {
|
|
51
|
+
const width = terminalGraphemeWidth(grapheme);
|
|
52
|
+
if (remaining < width) {
|
|
53
|
+
stopped = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
text += grapheme;
|
|
57
|
+
remaining -= width;
|
|
58
|
+
}
|
|
59
|
+
if (text.length === 0)
|
|
60
|
+
continue;
|
|
38
61
|
const style = styles?.[segment.role];
|
|
39
62
|
line += style === undefined ? text : `${style}${text}${RESET}`;
|
|
40
63
|
}
|
|
@@ -50,6 +73,7 @@ export class AnsiFullscreenRenderer {
|
|
|
50
73
|
#styles;
|
|
51
74
|
#mounted = false;
|
|
52
75
|
#previousLines = [];
|
|
76
|
+
#previousCursor;
|
|
53
77
|
constructor(options) {
|
|
54
78
|
this.#writer = options.writer;
|
|
55
79
|
this.#synchronizedOutput = options.synchronizedOutput === true;
|
|
@@ -64,11 +88,50 @@ export class AnsiFullscreenRenderer {
|
|
|
64
88
|
mount() {
|
|
65
89
|
if (this.#mounted)
|
|
66
90
|
return;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
91
|
+
let alternateScreenEntered = false;
|
|
92
|
+
let cursorHidden = false;
|
|
93
|
+
let synchronizedOutputBegun = false;
|
|
94
|
+
try {
|
|
95
|
+
alternateScreenEntered = true;
|
|
96
|
+
this.#writer.write(ALTERNATE_SCREEN_ENTER);
|
|
97
|
+
cursorHidden = true;
|
|
98
|
+
this.#writer.write(HIDE_CURSOR);
|
|
99
|
+
if (this.#synchronizedOutput) {
|
|
100
|
+
synchronizedOutputBegun = true;
|
|
101
|
+
this.#writer.write(SYNCHRONIZED_BEGIN);
|
|
102
|
+
}
|
|
103
|
+
this.#mounted = true;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
this.#mounted = false;
|
|
107
|
+
this.#previousLines = [];
|
|
108
|
+
this.#previousCursor = undefined;
|
|
109
|
+
if (synchronizedOutputBegun) {
|
|
110
|
+
try {
|
|
111
|
+
this.#writer.write(SYNCHRONIZED_END);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Rollback must continue if one restoration write fails.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (cursorHidden) {
|
|
118
|
+
try {
|
|
119
|
+
this.#writer.write(SHOW_CURSOR);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Rollback must continue if one restoration write fails.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (alternateScreenEntered) {
|
|
126
|
+
try {
|
|
127
|
+
this.#writer.write(ALTERNATE_SCREEN_LEAVE);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Rollback must continue if one restoration write fails.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
72
135
|
}
|
|
73
136
|
draw(frame) {
|
|
74
137
|
if (!this.#mounted)
|
|
@@ -78,24 +141,48 @@ export class AnsiFullscreenRenderer {
|
|
|
78
141
|
const previousLines = this.#previousLines;
|
|
79
142
|
const changed = lines.length !== previousLines.length ||
|
|
80
143
|
lines.some((line, index) => line !== previousLines[index]);
|
|
81
|
-
|
|
144
|
+
const cursorRow = frame.cursor
|
|
145
|
+
? frame.lines.findIndex((row) => row.key === frame.cursor?.rowKey)
|
|
146
|
+
: -1;
|
|
147
|
+
const nextCursor = cursorRow >= 0 && frame.cursor !== undefined
|
|
148
|
+
? {
|
|
149
|
+
rowKey: frame.cursor.rowKey,
|
|
150
|
+
column: Math.min(frame.cursor.column, frame.columns - 1),
|
|
151
|
+
row: cursorRow,
|
|
152
|
+
}
|
|
153
|
+
: undefined;
|
|
154
|
+
const cursorChanged = nextCursor?.rowKey !== this.#previousCursor?.rowKey ||
|
|
155
|
+
nextCursor?.column !== this.#previousCursor?.column ||
|
|
156
|
+
nextCursor?.row !== this.#previousCursor?.row;
|
|
157
|
+
if (!changed && !cursorChanged)
|
|
82
158
|
return;
|
|
83
159
|
if (this.#synchronizedOutput)
|
|
84
160
|
this.#writer.write(SYNCHRONIZED_BEGIN);
|
|
85
161
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
162
|
+
if (changed) {
|
|
163
|
+
if (this.#previousCursor !== undefined || nextCursor !== undefined)
|
|
164
|
+
this.#writer.write(HIDE_CURSOR);
|
|
165
|
+
const commonLength = Math.max(lines.length, previousLines.length);
|
|
166
|
+
for (let index = 0; index < commonLength; index += 1) {
|
|
167
|
+
const line = lines[index];
|
|
168
|
+
if (line !== undefined) {
|
|
169
|
+
if (line === previousLines[index])
|
|
170
|
+
continue;
|
|
171
|
+
this.#writer.write(`\u001b[${index + 1};1H\u001b[2K${line}`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
this.#writer.write(`\u001b[${index + 1};1H\u001b[2K`);
|
|
175
|
+
}
|
|
96
176
|
}
|
|
177
|
+
this.#previousLines = lines;
|
|
178
|
+
}
|
|
179
|
+
if (nextCursor !== undefined) {
|
|
180
|
+
this.#writer.write(`\u001b[${cursorRow + 1};${nextCursor.column + 1}H`);
|
|
181
|
+
this.#writer.write(SHOW_CURSOR);
|
|
97
182
|
}
|
|
98
|
-
this.#
|
|
183
|
+
else if (this.#previousCursor !== undefined)
|
|
184
|
+
this.#writer.write(HIDE_CURSOR);
|
|
185
|
+
this.#previousCursor = nextCursor;
|
|
99
186
|
}
|
|
100
187
|
finally {
|
|
101
188
|
if (this.#synchronizedOutput)
|
|
@@ -114,6 +201,7 @@ export class AnsiFullscreenRenderer {
|
|
|
114
201
|
finally {
|
|
115
202
|
this.#mounted = false;
|
|
116
203
|
this.#previousLines = [];
|
|
204
|
+
this.#previousCursor = undefined;
|
|
117
205
|
}
|
|
118
206
|
}
|
|
119
207
|
}
|
|
@@ -1,26 +1,10 @@
|
|
|
1
1
|
import { type AnsiFrame } from './ansi-frame-renderer.js';
|
|
2
|
-
import type {
|
|
3
|
-
export interface
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
rows: number | undefined;
|
|
7
|
-
input: string;
|
|
8
|
-
busy: boolean;
|
|
9
|
-
status: string;
|
|
10
|
-
screenReader?: boolean;
|
|
11
|
-
display?: {
|
|
12
|
-
cwd: string;
|
|
13
|
-
model?: string;
|
|
14
|
-
effort?: string;
|
|
15
|
-
permissionMode?: string;
|
|
16
|
-
version?: string;
|
|
17
|
-
};
|
|
18
|
-
sessionLabel?: string;
|
|
19
|
-
cursor?: number;
|
|
20
|
-
onError: (error: unknown) => void;
|
|
2
|
+
import type { QuietFrame } from './quiet-frame.js';
|
|
3
|
+
export interface TuiAnsiSurfaceFrameProps {
|
|
4
|
+
readonly frame: QuietFrame;
|
|
5
|
+
readonly onError: (error: unknown) => void;
|
|
21
6
|
}
|
|
22
|
-
|
|
23
|
-
export declare function
|
|
24
|
-
export declare function projectAnsiSurfaceFrame(props: TuiAnsiSurfaceProps): AnsiFrame;
|
|
7
|
+
export type TuiAnsiSurfaceProps = TuiAnsiSurfaceFrameProps;
|
|
8
|
+
export declare function projectAnsiQuietFrame(frame: QuietFrame): AnsiFrame;
|
|
25
9
|
export declare function TuiAnsiSurface(props: TuiAnsiSurfaceProps): null;
|
|
26
10
|
//# sourceMappingURL=ansi-surface.d.ts.map
|
|
@@ -3,132 +3,12 @@ import { useStdout } from 'ink';
|
|
|
3
3
|
import { AnsiFullscreenRenderer, } from './ansi-frame-renderer.js';
|
|
4
4
|
import { resolveAnsiTextStyles } from './ansi-theme.js';
|
|
5
5
|
import { useTuiTheme } from './theme.js';
|
|
6
|
-
|
|
7
|
-
// every emitted row is still guaranteed to contain no line breaks.
|
|
8
|
-
function isUnsafeControl(character) {
|
|
9
|
-
const codePoint = character.codePointAt(0) ?? 0;
|
|
10
|
-
return ((codePoint >= 0 && codePoint <= 9) ||
|
|
11
|
-
codePoint === 11 ||
|
|
12
|
-
codePoint === 12 ||
|
|
13
|
-
(codePoint >= 14 && codePoint <= 31) ||
|
|
14
|
-
codePoint === 127);
|
|
15
|
-
}
|
|
16
|
-
function safeText(value) {
|
|
17
|
-
return typeof value === 'string'
|
|
18
|
-
? Array.from(value)
|
|
19
|
-
.filter((character) => !isUnsafeControl(character))
|
|
20
|
-
.join('')
|
|
21
|
-
: '';
|
|
22
|
-
}
|
|
23
|
-
function row(key, text, role = 'body') {
|
|
24
|
-
return {
|
|
25
|
-
key,
|
|
26
|
-
segments: [{ text: safeText(text) || ' ', role }],
|
|
27
|
-
height: 1,
|
|
28
|
-
source: key,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
function activeRows(key, text, role = 'body') {
|
|
32
|
-
return safeText(text)
|
|
33
|
-
.replace(/\r\n?/gu, '\n')
|
|
34
|
-
.split('\n')
|
|
35
|
-
.map((line, index) => row(`${key}:${index}`, line, role));
|
|
36
|
-
}
|
|
37
|
-
function headerRows(props) {
|
|
38
|
-
const display = props.display;
|
|
39
|
-
const identity = `Praxis${display?.version ? ` ${safeText(display.version)}` : ''}${props.sessionLabel ? ` · session ${safeText(props.sessionLabel)}` : ''}`;
|
|
40
|
-
const metadata = [
|
|
41
|
-
display?.cwd ? `cwd ${safeText(display.cwd).slice(0, 80)}` : '',
|
|
42
|
-
display?.model ? `model ${safeText(display.model).slice(0, 40)}` : '',
|
|
43
|
-
display?.effort ? `effort ${safeText(display.effort).slice(0, 20)}` : '',
|
|
44
|
-
display?.permissionMode
|
|
45
|
-
? `permissions ${safeText(display.permissionMode).slice(0, 30)}`
|
|
46
|
-
: '',
|
|
47
|
-
]
|
|
48
|
-
.filter(Boolean)
|
|
49
|
-
.join(' · ');
|
|
50
|
-
return [
|
|
51
|
-
row('ansi:header:identity', identity, 'heading'),
|
|
52
|
-
row('ansi:header:context', metadata || 'Ready', 'muted'),
|
|
53
|
-
];
|
|
54
|
-
}
|
|
55
|
-
function summarize(value, depth = 0) {
|
|
56
|
-
if (depth > 1 || value === null || typeof value !== 'object')
|
|
57
|
-
return [];
|
|
58
|
-
const result = [];
|
|
59
|
-
for (const [key, item] of Object.entries(value)) {
|
|
60
|
-
if (typeof item === 'string' && item.length > 0) {
|
|
61
|
-
result.push(`${key}: ${safeText(item).slice(0, 120)}`);
|
|
62
|
-
}
|
|
63
|
-
else if (Array.isArray(item)) {
|
|
64
|
-
const strings = item.filter((entry) => typeof entry === 'string');
|
|
65
|
-
if (strings.length > 0)
|
|
66
|
-
result.push(`${key}: ${strings.slice(0, 4).map(safeText).join(', ')}`);
|
|
67
|
-
}
|
|
68
|
-
else if (typeof item === 'object' && item !== null) {
|
|
69
|
-
result.push(...summarize(item, depth + 1).slice(0, 4));
|
|
70
|
-
}
|
|
71
|
-
if (result.length >= 8)
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
return result.slice(0, 8);
|
|
75
|
-
}
|
|
76
|
-
function surfaceRows(screen) {
|
|
77
|
-
const body = screen.body;
|
|
78
|
-
if (body.kind === 'session-picker') {
|
|
79
|
-
return [
|
|
80
|
-
row('ansi:session-picker', 'Sessions', 'heading'),
|
|
81
|
-
...summarize(body.surface).map((text, index) => row(`ansi:session:${index}`, text, 'muted')),
|
|
82
|
-
];
|
|
83
|
-
}
|
|
84
|
-
if (body.foreground.kind !== 'compose') {
|
|
85
|
-
const surface = body.foreground.surface;
|
|
86
|
-
return [
|
|
87
|
-
row(`ansi:foreground:${body.foreground.kind}`, body.foreground.kind, 'heading'),
|
|
88
|
-
...summarize(surface).map((text, index) => row(`ansi:foreground:${index}`, text, 'muted')),
|
|
89
|
-
];
|
|
90
|
-
}
|
|
91
|
-
return [];
|
|
92
|
-
}
|
|
93
|
-
/** ANSI mode is intentionally limited to the plain conversation surface. */
|
|
94
|
-
export function supportsAnsiSurface(screen) {
|
|
95
|
-
if (screen.body.kind !== 'conversation')
|
|
96
|
-
return false;
|
|
97
|
-
return (screen.body.intro === 'none' &&
|
|
98
|
-
screen.body.foreground.kind === 'compose' &&
|
|
99
|
-
screen.body.foreground.overlays.length === 0);
|
|
100
|
-
}
|
|
101
|
-
export function projectAnsiSurfaceFrame(props) {
|
|
102
|
-
const body = props.screen.body;
|
|
103
|
-
const lines = [...surfaceRows(props.screen)];
|
|
104
|
-
const headers = body.kind === 'conversation' ? headerRows(props) : [];
|
|
105
|
-
if (body.kind === 'conversation') {
|
|
106
|
-
lines.push(...body.transcript.rows);
|
|
107
|
-
if (body.transcript.active.visible) {
|
|
108
|
-
if (safeText(body.transcript.active.text))
|
|
109
|
-
lines.push(...activeRows('ansi:active:text', body.transcript.active.text));
|
|
110
|
-
if (safeText(body.transcript.active.thinking))
|
|
111
|
-
lines.push(...activeRows('ansi:active:thinking', body.transcript.active.thinking, 'muted'));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
const cursor = Math.min(Math.max(0, props.cursor ?? props.input.length), props.input.length);
|
|
115
|
-
lines.push(row('composer', `❯ ${props.input.slice(0, cursor)}▌${props.input.slice(cursor)}`, 'input'));
|
|
116
|
-
lines.push(row('status', `● ${props.status}${props.busy ? ' · busy' : ''}`, 'muted'));
|
|
117
|
-
const limit = props.rows === undefined ? lines.length : Math.max(2, props.rows);
|
|
118
|
-
const chrome = lines.slice(-2);
|
|
119
|
-
const content = [...headers, ...lines.slice(0, -2)];
|
|
120
|
-
const headerCount = Math.min(headers.length, Math.max(0, limit - 2));
|
|
121
|
-
const visibleHeaders = headers.slice(0, headerCount);
|
|
122
|
-
const bodyRows = content.slice(headers.length);
|
|
123
|
-
const contentBudget = Math.max(0, limit - headerCount - 2);
|
|
6
|
+
export function projectAnsiQuietFrame(frame) {
|
|
124
7
|
return {
|
|
125
|
-
columns:
|
|
126
|
-
rows:
|
|
127
|
-
lines:
|
|
128
|
-
|
|
129
|
-
...bodyRows.slice(-contentBudget),
|
|
130
|
-
...chrome,
|
|
131
|
-
].slice(-limit),
|
|
8
|
+
columns: frame.columns,
|
|
9
|
+
rows: frame.rows,
|
|
10
|
+
lines: frame.lines,
|
|
11
|
+
...(frame.cursor === undefined ? {} : { cursor: frame.cursor }),
|
|
132
12
|
};
|
|
133
13
|
}
|
|
134
14
|
export function TuiAnsiSurface(props) {
|
|
@@ -175,7 +55,7 @@ export function TuiAnsiSurface(props) {
|
|
|
175
55
|
if (renderer === null)
|
|
176
56
|
return;
|
|
177
57
|
try {
|
|
178
|
-
renderer.draw(
|
|
58
|
+
renderer.draw(projectAnsiQuietFrame(props.frame));
|
|
179
59
|
}
|
|
180
60
|
catch (error) {
|
|
181
61
|
failedRef.current = true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { TuiSemanticTheme
|
|
2
|
-
|
|
1
|
+
import type { TuiSemanticTheme } from './theme.js';
|
|
2
|
+
import type { TuiTextRole as RowIrTextRole } from './tui-row-ir.js';
|
|
3
|
+
export declare function resolveAnsiTextStyles(theme: Pick<TuiSemanticTheme, 'text' | 'noColor' | 'screenReader'>): Partial<Record<RowIrTextRole | 'link', string>>;
|
|
3
4
|
//# sourceMappingURL=ansi-theme.d.ts.map
|
|
@@ -73,11 +73,32 @@ export function resolveAnsiTextStyles(theme) {
|
|
|
73
73
|
if (theme.noColor || theme.screenReader)
|
|
74
74
|
return {};
|
|
75
75
|
const styles = {};
|
|
76
|
-
|
|
76
|
+
const roleMap = {
|
|
77
|
+
body: 'body',
|
|
78
|
+
heading: 'heading',
|
|
79
|
+
muted: 'muted',
|
|
80
|
+
accent: 'focusMarker',
|
|
81
|
+
success: 'success',
|
|
82
|
+
warning: 'warning',
|
|
83
|
+
error: 'error',
|
|
84
|
+
tool: 'info',
|
|
85
|
+
selection: 'focusMarker',
|
|
86
|
+
input: 'inputMarker',
|
|
87
|
+
diffAdded: 'diffAdded',
|
|
88
|
+
diffRemoved: 'diffRemoved',
|
|
89
|
+
};
|
|
90
|
+
for (const [role, semanticRole] of Object.entries(roleMap)) {
|
|
91
|
+
const style = theme.text[semanticRole] ??
|
|
92
|
+
theme.text[role];
|
|
93
|
+
if (style === undefined)
|
|
94
|
+
continue;
|
|
77
95
|
const sequence = styleSequence(style);
|
|
78
96
|
if (sequence !== undefined)
|
|
79
97
|
styles[role] = sequence;
|
|
80
98
|
}
|
|
99
|
+
const link = theme.text.link === undefined ? undefined : styleSequence(theme.text.link);
|
|
100
|
+
if (link !== undefined)
|
|
101
|
+
styles.link = link;
|
|
81
102
|
return styles;
|
|
82
103
|
}
|
|
83
104
|
//# sourceMappingURL=ansi-theme.js.map
|