praxis-agent 0.43.0 → 0.43.1

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 (34) hide show
  1. package/README.md +24 -37
  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/cli/interactive.js +112 -156
  7. package/dist/cli/tui/ansi-frame-renderer.d.ts +2 -0
  8. package/dist/cli/tui/ansi-frame-renderer.js +109 -21
  9. package/dist/cli/tui/ansi-surface.d.ts +6 -22
  10. package/dist/cli/tui/ansi-surface.js +6 -126
  11. package/dist/cli/tui/ansi-theme.d.ts +3 -2
  12. package/dist/cli/tui/ansi-theme.js +22 -1
  13. package/dist/cli/tui/quiet-choice-rows.d.ts +26 -0
  14. package/dist/cli/tui/quiet-choice-rows.js +302 -0
  15. package/dist/cli/tui/quiet-frame-adapter.d.ts +18 -0
  16. package/dist/cli/tui/quiet-frame-adapter.js +90 -0
  17. package/dist/cli/tui/quiet-frame.d.ts +43 -0
  18. package/dist/cli/tui/quiet-frame.js +346 -0
  19. package/dist/cli/tui/quiet-operational-rows.d.ts +17 -0
  20. package/dist/cli/tui/quiet-operational-rows.js +413 -0
  21. package/dist/cli/tui/quiet-screen-projector.d.ts +25 -0
  22. package/dist/cli/tui/quiet-screen-projector.js +110 -0
  23. package/dist/cli/tui/quiet-settings-rows.d.ts +17 -0
  24. package/dist/cli/tui/quiet-settings-rows.js +490 -0
  25. package/dist/cli/tui/status-line.d.ts +15 -9
  26. package/dist/cli/tui/status-line.js +15 -6
  27. package/dist/cli/tui/theme.js +20 -20
  28. package/dist/cli/tui/transcript-viewport.d.ts +5 -0
  29. package/dist/cli/tui/transcript-viewport.js +213 -78
  30. package/dist/cli/tui/tui-row-ir.js +4 -4
  31. package/dist/persistence/in-memory-transcript-store.js +1 -7
  32. package/dist/persistence/native-transcript-store.d.ts +0 -9
  33. package/dist/persistence/native-transcript-store.js +4 -38
  34. package/package.json +3 -2
@@ -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
@@ -0,0 +1,26 @@
1
+ import { type QuietFrameDensity, type QuietFrameRow } from './quiet-frame.js';
2
+ import type { TuiCommandPaletteModel } from './command-palette-model.js';
3
+ import type { TuiDecisionSurfaceModel } from './decision-surface-model.js';
4
+ import type { TuiElicitationSurfaceModel } from './mcp-elicitation-surface-model.js';
5
+ import type { TuiMentionPickerModel } from './mention-picker-model.js';
6
+ import type { TuiPermissionSurfaceModel } from './permission-surface-model.js';
7
+ import type { TuiSessionPickerModel } from './session-picker-model.js';
8
+ export type QuietPrioritySurface = {
9
+ readonly kind: 'editor-wait';
10
+ } | {
11
+ readonly kind: 'permission';
12
+ readonly surface: TuiPermissionSurfaceModel;
13
+ } | TuiDecisionSurfaceModel | {
14
+ readonly kind: 'elicitation';
15
+ readonly surface: TuiElicitationSurfaceModel;
16
+ };
17
+ export type QuietOverlaySurface = TuiCommandPaletteModel | TuiMentionPickerModel | {
18
+ readonly kind: 'exit-confirmation';
19
+ };
20
+ export type QuietChoiceSurface = QuietPrioritySurface | QuietOverlaySurface | TuiSessionPickerModel | TuiPermissionSurfaceModel;
21
+ export interface QuietChoiceProjectionOptions {
22
+ readonly density: QuietFrameDensity;
23
+ readonly screenReader?: boolean;
24
+ }
25
+ export declare function projectQuietChoiceRows(surface: QuietChoiceSurface, options: QuietChoiceProjectionOptions): readonly QuietFrameRow[];
26
+ //# sourceMappingURL=quiet-choice-rows.d.ts.map
@@ -0,0 +1,302 @@
1
+ import { createQuietFrameRow, } from './quiet-frame.js';
2
+ const excerpt = (value, max = 160) => typeof value === 'string'
3
+ ? value.replace(/\s+/gu, ' ').trim().slice(0, max)
4
+ : '';
5
+ const detailAllowed = (density) => density === 'full' || density === 'standard';
6
+ const row = (key, text, role = 'body', sr) => createQuietFrameRow(key, text, 'focus', role, sr);
7
+ const footer = (screenReader, text = '↑/↓ select Enter confirm Esc cancel') => row('quiet:choice:footer', text, 'muted', screenReader
8
+ ? 'Use up and down arrows to select. Press Enter to confirm. Press Escape to cancel.'
9
+ : undefined);
10
+ const explicitFooter = (key, visual, accessible) => row(key, visual, 'muted', accessible);
11
+ const marker = (selected, label, screenReader) => screenReader
12
+ ? selected
13
+ ? `Selected: ${label}`
14
+ : `Option: ${label}`
15
+ : `${selected ? '❯ ' : ' '}${label}`;
16
+ const excerptOption = (label, description, density) => detailAllowed(density) && description
17
+ ? `${label} — ${excerpt(description)}`
18
+ : label;
19
+ const unsupported = (value) => {
20
+ throw new Error(`Unsupported quiet choice surface: ${String(value)}`);
21
+ };
22
+ function optionsRows(options, prefix, density, sr) {
23
+ return options.map((item, index) => row(`${prefix}:${item.id ?? item.index ?? index}`, marker(item.selected, excerptOption(item.label, item.description, density), sr), item.selected ? 'selection' : 'body', sr
24
+ ? item.selected
25
+ ? `Selected: ${item.label}`
26
+ : `Option: ${item.label}`
27
+ : undefined));
28
+ }
29
+ function permission(surface, density, sr) {
30
+ const out = [];
31
+ switch (surface.kind) {
32
+ case 'tool-request': {
33
+ out.push(row('quiet:permission:heading', excerpt(surface.tool.question || surface.heading), 'heading'));
34
+ if (detailAllowed(density)) {
35
+ for (const [key, value] of [
36
+ ['subtitle', surface.tool.subtitle],
37
+ ['explanation', surface.tool.explanation],
38
+ ])
39
+ if (value)
40
+ out.push(row(`quiet:permission:${key}`, excerpt(value), 'muted'));
41
+ surface.tool.detail
42
+ .slice(0, 20)
43
+ .forEach((item, i) => out.push(row(`quiet:permission:detail:${i}`, `${item.prefix ?? ' '} ${excerpt(item.text)}`, item.prefix === '-'
44
+ ? 'diffRemoved'
45
+ : item.prefix === '+'
46
+ ? 'diffAdded'
47
+ : 'body')));
48
+ }
49
+ out.push(...optionsRows(surface.options.map((item, index) => ({
50
+ ...item,
51
+ label: permissionLabel({
52
+ ...item,
53
+ index: index + 1,
54
+ selected: index === surface.selectedIndex,
55
+ }),
56
+ selected: index === surface.selectedIndex,
57
+ })), 'quiet:permission:option', density, sr));
58
+ if (surface.feedbackMode)
59
+ out.push(row('quiet:permission:feedback', `> ${excerpt(surface.feedback) || 'Enter feedback…'}`, 'input'));
60
+ return [...out, footer(sr)];
61
+ }
62
+ case 'recovery-request':
63
+ out.push(row('quiet:recovery:heading', surface.heading, 'heading'), row('quiet:recovery:question', surface.question), row('quiet:recovery:display', excerpt(surface.display), 'muted'));
64
+ if (surface.feedbackMode)
65
+ out.push(row('quiet:recovery:feedback', `> ${excerpt(surface.feedback) || surface.feedbackPlaceholder}`, 'input'));
66
+ return [
67
+ ...out,
68
+ ...optionsRows(surface.options, 'quiet:recovery:option', density, sr),
69
+ footer(sr),
70
+ ];
71
+ case 'permission-dashboard':
72
+ out.push(row('quiet:dashboard:heading', surface.heading, 'heading'), row('quiet:dashboard:tab', `Tab: ${surface.tabs.find((x) => x.current)?.label ?? 'Permissions'}`));
73
+ if (detailAllowed(density))
74
+ out.push(row('quiet:dashboard:description', excerpt(surface.description)));
75
+ if (surface.query)
76
+ out.push(row('quiet:dashboard:search', `Search: ${excerpt(surface.query)}`, 'input'));
77
+ if (surface.originalWorkspace)
78
+ out.push(row('quiet:dashboard:workspace:original', `Original workspace: ${surface.originalWorkspace.label}`, 'muted'));
79
+ out.push(...surface.rows
80
+ .slice(0, 20)
81
+ .map((item, i) => row(`quiet:dashboard:${item.id ?? item.index ?? i}`, marker(item.selected, `${item.label}${item.status === 'retrying' ? ' (retrying)' : item.status === 'denied' ? ' (denied)' : ''}`, sr), item.selected
82
+ ? 'selection'
83
+ : item.status === 'denied'
84
+ ? 'error'
85
+ : item.status === 'retrying'
86
+ ? 'warning'
87
+ : 'body')));
88
+ if (surface.rows.length === 0 && surface.emptyState)
89
+ out.push(row('quiet:dashboard:empty', surface.emptyState, 'muted'));
90
+ return [
91
+ ...out,
92
+ explicitFooter('quiet:dashboard:footer', surface.rows.length > 0
93
+ ? '↑/↓ select Enter open Esc close'
94
+ : 'Esc close', surface.rows.length > 0
95
+ ? 'Use left and right arrows to switch tabs. Use up and down arrows to select. Press Enter to open. Press Escape to close.'
96
+ : 'Use left and right arrows to switch tabs. Press Escape to close.'),
97
+ ];
98
+ case 'permission-rule-input':
99
+ case 'workspace-directory-input':
100
+ out.push(row(`quiet:${surface.kind}:heading`, surface.heading, 'heading'), row(`quiet:${surface.kind}:description`, excerpt(surface.description)), row(`quiet:${surface.kind}:value`, `> ${excerpt(surface.value) || surface.placeholder}`, 'input'));
101
+ return [
102
+ ...out,
103
+ explicitFooter(`quiet:${surface.kind}:footer`, 'Enter confirm Esc cancel', 'Press Enter to confirm. Press Escape to cancel.'),
104
+ ];
105
+ case 'permission-scope':
106
+ out.push(row('quiet:scope:heading', surface.heading, 'heading'), row('quiet:scope:description', excerpt(surface.description)));
107
+ return [
108
+ ...out,
109
+ ...optionsRows(surface.options, 'quiet:scope:option', density, sr),
110
+ footer(sr),
111
+ ];
112
+ case 'permission-delete':
113
+ out.push(row('quiet:delete:heading', surface.heading, 'heading'), row('quiet:delete:rule', `${surface.rule} · ${surface.scope}`), row('quiet:delete:question', surface.question, 'warning'));
114
+ if (surface.description)
115
+ out.push(row('quiet:delete:description', excerpt(surface.description), 'warning'));
116
+ return [
117
+ ...out,
118
+ ...optionsRows(surface.options, 'quiet:delete:option', density, sr).map((x, i) => i === 0
119
+ ? {
120
+ ...x,
121
+ segments: x.segments.map((s) => ({
122
+ ...s,
123
+ role: 'error',
124
+ })),
125
+ }
126
+ : x),
127
+ footer(sr),
128
+ ];
129
+ case 'workspace-directory-delete':
130
+ out.push(row('quiet:workspace-delete:heading', surface.heading, 'heading'), row('quiet:workspace-delete:path', surface.path), row('quiet:workspace-delete:description', excerpt(surface.description), 'warning'));
131
+ return [
132
+ ...out,
133
+ ...optionsRows(surface.options, 'quiet:workspace-delete:option', density, sr),
134
+ footer(sr),
135
+ ];
136
+ default:
137
+ return unsupported(surface);
138
+ }
139
+ }
140
+ function permissionLabel(item) {
141
+ if (item.action === 'allow-once')
142
+ return 'Allow once';
143
+ if (item.action === 'deny')
144
+ return 'Deny';
145
+ const label = item.label.trim();
146
+ if (/^yes,\s*/iu.test(label)) {
147
+ const consequence = label.replace(/^yes,\s*/iu, '');
148
+ return /^allow\b/iu.test(consequence)
149
+ ? `Allow${consequence.slice('allow'.length)}`
150
+ : `Allow ${consequence}`;
151
+ }
152
+ if (/^no,\s*/iu.test(label))
153
+ return `Deny ${label.replace(/^no,\s*/iu, '')}`;
154
+ if (/^yes$/iu.test(label))
155
+ return 'Allow for this session';
156
+ if (/^no$/iu.test(label))
157
+ return 'Deny';
158
+ return label;
159
+ }
160
+ function decision(surface, density, sr) {
161
+ const out = [
162
+ row(`quiet:decision:${surface.kind}:heading`, surface.heading, 'heading'),
163
+ ];
164
+ if (surface.kind === 'plan-approval') {
165
+ if (detailAllowed(density)) {
166
+ out.push(row('quiet:plan:explanation', surface.explanation));
167
+ if (surface.plan)
168
+ out.push(row('quiet:plan:content', excerpt(surface.plan, 160), 'muted'));
169
+ out.push(row('quiet:plan:path', `Plan: ${excerpt(surface.planPath)}`, 'muted'));
170
+ }
171
+ out.push(...optionsRows(surface.options, 'quiet:plan:option', density, sr));
172
+ if (surface.feedbackMode)
173
+ out.push(row('quiet:plan:feedback', `> ${excerpt(surface.feedback) || surface.feedbackPlaceholder}`, 'input'));
174
+ return [
175
+ ...out,
176
+ explicitFooter('quiet:plan:footer', '↑/↓ select Enter confirm Esc cancel', 'Use up and down arrows to select. Press Enter to confirm. Press Escape to cancel.'),
177
+ ];
178
+ }
179
+ if (surface.progress)
180
+ out.push(row('quiet:question:progress', surface.progress, 'muted'));
181
+ if (surface.question)
182
+ out.push(row('quiet:question:text', surface.question));
183
+ out.push(...optionsRows(surface.options, 'quiet:question:option', density, sr));
184
+ out.push(row('quiet:question:answer', `Current answer: ${excerpt(surface.answer) || '(empty)'}`, 'input'));
185
+ if (surface.guidance)
186
+ out.push(row('quiet:question:guidance', surface.guidance, 'muted'));
187
+ if (surface.emptyState)
188
+ out.push(row('quiet:question:empty', surface.emptyState, 'muted'));
189
+ out.push(explicitFooter('quiet:question:footer', 'Enter answer Esc cancel', 'Press Enter to submit the answer. Press Escape to cancel.'));
190
+ return out;
191
+ }
192
+ function picker(surface, density, sr) {
193
+ const name = surface.kind === 'session-picker'
194
+ ? 'Sessions'
195
+ : surface.kind === 'command-palette'
196
+ ? 'Commands'
197
+ : 'Mentions';
198
+ const out = [
199
+ row(`quiet:${surface.kind}:heading`, `${name}${surface.query ? ` · ${excerpt(surface.query)}` : ''}`, 'heading'),
200
+ ];
201
+ const { start, end } = surface.visibleRange;
202
+ surface.rows.slice(start, end).forEach((item) => {
203
+ const label = 'path' in item
204
+ ? item.path
205
+ : 'invocation' in item
206
+ ? detailAllowed(density)
207
+ ? `${item.invocation} — ${item.description}`
208
+ : item.invocation
209
+ : 'name' in item
210
+ ? detailAllowed(density)
211
+ ? `${item.name} — ${item.description}`
212
+ : item.name
213
+ : detailAllowed(density) && item.detail
214
+ ? `${item.label} — ${item.detail}`
215
+ : item.label;
216
+ out.push(row(`quiet:${surface.kind}:${item.id}`, marker(item.selected, excerptOption(label, undefined, density), sr), item.selected ? 'selection' : 'body'));
217
+ });
218
+ if (surface.rows.length === 0)
219
+ out.push(row(`quiet:${surface.kind}:empty`, 'No matching choices.', 'muted'));
220
+ return [
221
+ ...out,
222
+ explicitFooter(`quiet:${surface.kind}:footer`, '↑/↓ select Enter confirm Esc cancel', 'Use up and down arrows to select. Press Enter to confirm. Press Escape to cancel.'),
223
+ ];
224
+ }
225
+ function elicitation(surface, density, sr) {
226
+ const out = [
227
+ row(`quiet:elicitation:${surface.kind}:server`, `Server: ${excerpt(surface.serverName)}`, 'heading'),
228
+ row(`quiet:elicitation:${surface.kind}:message`, excerpt(surface.message)),
229
+ ];
230
+ if (surface.kind === 'elicitation-url') {
231
+ out.push(row('quiet:elicitation:url', `URL: ${excerpt(surface.url)}`), row('quiet:elicitation:waiting', surface.waiting
232
+ ? 'Waiting for confirmation…'
233
+ : marker(surface.selection === 0, surface.actionLabel, sr), surface.waiting
234
+ ? 'warning'
235
+ : surface.selection === 0
236
+ ? 'selection'
237
+ : 'body'), row('quiet:elicitation:cancel', marker(surface.selection === 1, 'Cancel', sr), surface.selection === 1 ? 'selection' : 'body'));
238
+ return [...out, footer(sr)];
239
+ }
240
+ const start = Math.max(0, Math.min(surface.state.focusIndex - 2, surface.state.fields.length - Math.max(1, surface.maxVisibleFields)));
241
+ surface.state.fields
242
+ .slice(start, start + Math.max(1, surface.maxVisibleFields))
243
+ .forEach((field) => {
244
+ const stored = surface.state.values[field.name];
245
+ const value = surface.state.fields.indexOf(field) === surface.state.focusIndex &&
246
+ ['text', 'number', 'integer'].includes(field.kind) &&
247
+ surface.state.expandedField !== field.name
248
+ ? surface.input
249
+ : stored;
250
+ const text = `${field.title}${field.required ? ' (required)' : ''}: ${value === undefined || value === '' ? '(empty)' : String(value)}${detailAllowed(density) && field.description ? ` — ${excerpt(field.description)}` : ''}${surface.state.errors[field.name] ? ` — ${excerpt(surface.state.errors[field.name])}` : ''}`;
251
+ out.push(row(`quiet:elicitation:field:${field.name}`, marker(surface.state.fields.indexOf(field) === surface.state.focusIndex, text, sr), surface.state.fields.indexOf(field) === surface.state.focusIndex
252
+ ? 'selection'
253
+ : surface.state.errors[field.name]
254
+ ? 'error'
255
+ : 'body'));
256
+ if (surface.state.expandedField === field.name) {
257
+ const optionStart = Math.max(0, Math.min(surface.state.optionIndex - 10, field.options.length - 20));
258
+ field.options
259
+ .slice(optionStart, optionStart + 20)
260
+ .forEach((option, offset) => {
261
+ const optionIndex = optionStart + offset;
262
+ const focused = surface.state.optionIndex === optionIndex;
263
+ const checked = field.kind === 'multi-enum' &&
264
+ Array.isArray(stored) &&
265
+ stored.includes(option.value);
266
+ const label = field.kind === 'multi-enum'
267
+ ? `${checked ? '[x]' : '[ ]'} ${option.label}`
268
+ : option.label;
269
+ out.push(row(`quiet:elicitation:field:${field.name}:option:${option.value}`, marker(focused, label, sr), focused ? 'selection' : checked ? 'success' : 'body'));
270
+ });
271
+ }
272
+ });
273
+ return [
274
+ ...out,
275
+ explicitFooter('quiet:elicitation:footer', '↑/↓ field Enter edit/select Esc cancel', 'Use up and down arrows to change fields. Press Enter to edit or select. Press Escape to cancel.'),
276
+ ];
277
+ }
278
+ export function projectQuietChoiceRows(surface, options) {
279
+ const sr = options.screenReader === true;
280
+ if (surface.kind === 'permission')
281
+ return permission(surface.surface, options.density, sr);
282
+ if (surface.kind === 'elicitation')
283
+ return elicitation(surface.surface, options.density, sr);
284
+ if (surface.kind === 'editor-wait')
285
+ return [
286
+ row('quiet:editor-wait', 'Editor is open. Waiting for changes…', 'warning'),
287
+ explicitFooter('quiet:editor-wait:footer', 'Esc cancel', 'Press Escape to cancel.'),
288
+ ];
289
+ if (surface.kind === 'exit-confirmation')
290
+ return [
291
+ row('quiet:exit:heading', 'Exit Praxis?', 'heading'),
292
+ explicitFooter('quiet:exit:footer', 'Enter confirm Esc cancel', 'Press Enter to confirm. Press Escape to cancel.'),
293
+ ];
294
+ if (surface.kind === 'plan-approval' || surface.kind === 'question')
295
+ return decision(surface, options.density, sr);
296
+ if (surface.kind === 'session-picker' ||
297
+ surface.kind === 'command-palette' ||
298
+ surface.kind === 'mention-picker')
299
+ return picker(surface, options.density, sr);
300
+ return permission(surface, options.density, sr);
301
+ }
302
+ //# sourceMappingURL=quiet-choice-rows.js.map
@@ -0,0 +1,18 @@
1
+ import type { QuietFrame } from './quiet-frame.js';
2
+ import { type TuiTextRole } from './theme.js';
3
+ export interface QuietInkLineSegment {
4
+ readonly text: string;
5
+ readonly role: TuiTextRole;
6
+ readonly cursor?: boolean;
7
+ }
8
+ export interface QuietInkFrameLine {
9
+ readonly key: string;
10
+ readonly segments: readonly QuietInkLineSegment[];
11
+ }
12
+ export declare function projectQuietInkFrameLines(frame: QuietFrame, screenReader?: boolean): readonly QuietInkFrameLine[];
13
+ export interface QuietInkFrameProps {
14
+ readonly frame: QuietFrame;
15
+ readonly screenReader?: boolean;
16
+ }
17
+ export declare function QuietInkFrame({ frame, screenReader, }: QuietInkFrameProps): import("react").JSX.Element;
18
+ //# sourceMappingURL=quiet-frame-adapter.d.ts.map
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { useTuiTheme } from './theme.js';
4
+ import { terminalGraphemeWidth, terminalGraphemes, terminalTextWidth, } from './transcript-viewport.js';
5
+ const ROLE_MAP = {
6
+ body: 'body',
7
+ heading: 'heading',
8
+ muted: 'muted',
9
+ accent: 'focusMarker',
10
+ success: 'success',
11
+ warning: 'warning',
12
+ error: 'error',
13
+ tool: 'info',
14
+ selection: 'focusMarker',
15
+ input: 'inputMarker',
16
+ diffAdded: 'diffAdded',
17
+ diffRemoved: 'diffRemoved',
18
+ };
19
+ function plainText(row) {
20
+ return row.segments.map((segment) => segment.text).join('') || ' ';
21
+ }
22
+ export function projectQuietInkFrameLines(frame, screenReader = false) {
23
+ return frame.lines.map((row) => {
24
+ if (screenReader)
25
+ return {
26
+ key: row.key,
27
+ segments: [
28
+ {
29
+ text: row.accessibleText ?? plainText(row),
30
+ role: 'body',
31
+ },
32
+ ],
33
+ };
34
+ const cursor = frame.cursor?.rowKey === row.key ? frame.cursor.column : undefined;
35
+ if (cursor === undefined)
36
+ return {
37
+ key: row.key,
38
+ segments: row.segments.map((segment) => ({
39
+ ...segment,
40
+ role: ROLE_MAP[segment.role],
41
+ })),
42
+ };
43
+ const output = [];
44
+ let position = 0;
45
+ let inserted = false;
46
+ for (const segment of row.segments) {
47
+ const chars = terminalGraphemes(segment.text);
48
+ const mappedRole = ROLE_MAP[segment.role];
49
+ const segmentWidth = terminalTextWidth(segment.text);
50
+ if (!inserted && cursor >= position && cursor < position + segmentWidth) {
51
+ let offset = 0;
52
+ let cell = position;
53
+ for (const [index, grapheme] of chars.entries()) {
54
+ const width = terminalGraphemeWidth(grapheme);
55
+ if (cursor < cell + width) {
56
+ offset = index;
57
+ break;
58
+ }
59
+ cell += width;
60
+ }
61
+ const before = chars.slice(0, offset).join('');
62
+ const at = chars[offset] ?? ' ';
63
+ const after = chars
64
+ .slice(offset + (offset < chars.length ? 1 : 0))
65
+ .join('');
66
+ if (before)
67
+ output.push({ text: before, role: mappedRole });
68
+ output.push({ text: at, role: mappedRole, cursor: true });
69
+ if (after)
70
+ output.push({ text: after, role: mappedRole });
71
+ inserted = true;
72
+ }
73
+ else
74
+ output.push({ text: segment.text, role: mappedRole });
75
+ position += segmentWidth;
76
+ }
77
+ if (!inserted)
78
+ output.push({ text: ' ', role: 'body', cursor: true });
79
+ return { key: row.key, segments: output };
80
+ });
81
+ }
82
+ export function QuietInkFrame({ frame, screenReader = false, }) {
83
+ const theme = useTuiTheme();
84
+ const lines = projectQuietInkFrameLines(frame, screenReader);
85
+ return (_jsx(_Fragment, { children: lines.map((line) => (_jsx(Text, { children: line.segments.map((segment, index) => {
86
+ const style = theme.text[segment.role];
87
+ return (_jsx(Text, { ...style, ...(segment.cursor || style.inverse ? { inverse: true } : {}), children: segment.text }, `${line.key}:${index}`));
88
+ }) }, line.key))) }));
89
+ }
90
+ //# sourceMappingURL=quiet-frame-adapter.js.map