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.
Files changed (59) hide show
  1. package/README.md +48 -47
  2. package/dist/application/native-session-projection.d.ts +4 -0
  3. package/dist/application/native-session-projection.js +135 -0
  4. package/dist/application/session-service.d.ts +0 -1
  5. package/dist/application/session-service.js +9 -170
  6. package/dist/application/top-level-agent-manager.d.ts +7 -0
  7. package/dist/application/top-level-agent-manager.js +21 -3
  8. package/dist/cli/controls.js +4 -0
  9. package/dist/cli/interactive.js +112 -156
  10. package/dist/cli/protocol.d.ts +5 -0
  11. package/dist/cli/protocol.js +45 -0
  12. package/dist/cli/provider-auth-command.d.ts +32 -0
  13. package/dist/cli/provider-auth-command.js +249 -0
  14. package/dist/cli/tui/ansi-frame-renderer.d.ts +2 -0
  15. package/dist/cli/tui/ansi-frame-renderer.js +109 -21
  16. package/dist/cli/tui/ansi-surface.d.ts +6 -22
  17. package/dist/cli/tui/ansi-surface.js +6 -126
  18. package/dist/cli/tui/ansi-theme.d.ts +3 -2
  19. package/dist/cli/tui/ansi-theme.js +22 -1
  20. package/dist/cli/tui/quiet-choice-rows.d.ts +26 -0
  21. package/dist/cli/tui/quiet-choice-rows.js +302 -0
  22. package/dist/cli/tui/quiet-frame-adapter.d.ts +18 -0
  23. package/dist/cli/tui/quiet-frame-adapter.js +90 -0
  24. package/dist/cli/tui/quiet-frame.d.ts +43 -0
  25. package/dist/cli/tui/quiet-frame.js +346 -0
  26. package/dist/cli/tui/quiet-operational-rows.d.ts +17 -0
  27. package/dist/cli/tui/quiet-operational-rows.js +413 -0
  28. package/dist/cli/tui/quiet-screen-projector.d.ts +25 -0
  29. package/dist/cli/tui/quiet-screen-projector.js +110 -0
  30. package/dist/cli/tui/quiet-settings-rows.d.ts +17 -0
  31. package/dist/cli/tui/quiet-settings-rows.js +490 -0
  32. package/dist/cli/tui/status-line.d.ts +15 -9
  33. package/dist/cli/tui/status-line.js +15 -6
  34. package/dist/cli/tui/theme.js +20 -20
  35. package/dist/cli/tui/transcript-viewport.d.ts +5 -0
  36. package/dist/cli/tui/transcript-viewport.js +213 -78
  37. package/dist/cli/tui/tui-row-ir.js +4 -4
  38. package/dist/cli/workspace-trust-prompt.js +2 -2
  39. package/dist/cli-runtime.d.ts +34 -1
  40. package/dist/cli-runtime.js +587 -184
  41. package/dist/maintenance/doctor.js +100 -22
  42. package/dist/persistence/in-memory-transcript-store.js +1 -7
  43. package/dist/persistence/native-transcript-store.d.ts +0 -9
  44. package/dist/persistence/native-transcript-store.js +4 -38
  45. package/dist/persistence/provider-credential-vault.d.ts +72 -0
  46. package/dist/persistence/provider-credential-vault.js +498 -0
  47. package/dist/providers/codex-oauth.d.ts +104 -0
  48. package/dist/providers/codex-oauth.js +770 -0
  49. package/dist/providers/codex-subscription.d.ts +33 -0
  50. package/dist/providers/codex-subscription.js +849 -0
  51. package/dist/providers/provider-auth.d.ts +71 -0
  52. package/dist/providers/provider-auth.js +149 -0
  53. package/dist/providers/provider-registry.d.ts +52 -0
  54. package/dist/providers/provider-registry.js +187 -0
  55. package/dist/providers/provider-settings.d.ts +38 -0
  56. package/dist/providers/provider-settings.js +334 -0
  57. package/dist/security/workspace-trust.d.ts +4 -3
  58. package/dist/security/workspace-trust.js +35 -1
  59. 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 + Array.from(segment.text).length, 0);
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
- const text = Array.from(segment.text).slice(0, remaining).join('');
37
- remaining -= Array.from(text).length;
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
- this.#writer.write(ALTERNATE_SCREEN_ENTER);
68
- this.#writer.write(HIDE_CURSOR);
69
- if (this.#synchronizedOutput)
70
- this.#writer.write(SYNCHRONIZED_BEGIN);
71
- this.#mounted = true;
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
- if (!changed)
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
- const commonLength = Math.max(lines.length, previousLines.length);
87
- for (let index = 0; index < commonLength; index += 1) {
88
- const line = lines[index];
89
- if (line !== undefined) {
90
- if (line === previousLines[index])
91
- continue;
92
- this.#writer.write(`\u001b[${index + 1};1H\u001b[2K${line}`);
93
- }
94
- else {
95
- this.#writer.write(`\u001b[${index + 1};1H\u001b[2K`);
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.#previousLines = lines;
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 { TuiScreenModel } from './tui-screen-model.js';
3
- export interface TuiAnsiSurfaceProps {
4
- screen: TuiScreenModel;
5
- width: number;
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
- /** ANSI mode is intentionally limited to the plain conversation surface. */
23
- export declare function supportsAnsiSurface(screen: TuiScreenModel): boolean;
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
- // Keep line breaks long enough for active stream projection to split them;
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: Math.max(1, Math.floor(props.width)),
126
- rows: limit,
127
- lines: [
128
- ...visibleHeaders,
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(projectAnsiSurfaceFrame(props));
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, TuiTextRole } from './theme.js';
2
- export declare function resolveAnsiTextStyles(theme: Pick<TuiSemanticTheme, 'text' | 'noColor' | 'screenReader'>): Partial<Record<TuiTextRole, string>>;
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
- for (const [role, style] of Object.entries(theme.text)) {
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