@deepseek-harness-tui/dsh-tui 0.5.2 → 0.6.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 (49) hide show
  1. package/README.md +7 -1
  2. package/lib/types/approvals.d.ts +80 -0
  3. package/lib/types/approvals.d.ts.map +1 -0
  4. package/lib/types/approvals.js +170 -0
  5. package/lib/types/channel.d.ts +5 -0
  6. package/lib/types/channel.d.ts.map +1 -1
  7. package/lib/types/channel.js +32 -25
  8. package/lib/types/commands.d.ts +14 -1
  9. package/lib/types/commands.d.ts.map +1 -1
  10. package/lib/types/commands.js +13 -0
  11. package/lib/types/compat/index.d.ts +1 -1
  12. package/lib/types/compat/index.d.ts.map +1 -1
  13. package/lib/types/compat/index.js +1 -1
  14. package/lib/types/compat/sessionLog.d.ts +21 -0
  15. package/lib/types/compat/sessionLog.d.ts.map +1 -1
  16. package/lib/types/compat/sessionLog.js +122 -37
  17. package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
  18. package/lib/types/components/CommandSuggestions.js +5 -3
  19. package/lib/types/components/HelpMenu.d.ts +1 -0
  20. package/lib/types/components/HelpMenu.d.ts.map +1 -1
  21. package/lib/types/components/HelpMenu.js +4 -1
  22. package/lib/types/components/PromptInput.d.ts.map +1 -1
  23. package/lib/types/components/PromptInput.js +16 -15
  24. package/lib/types/components/approvals/ApprovalPanel.d.ts +21 -0
  25. package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -0
  26. package/lib/types/components/approvals/ApprovalPanel.js +48 -0
  27. package/lib/types/components/questions/AskUserQuestionPanel.d.ts +6 -0
  28. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  29. package/lib/types/components/questions/AskUserQuestionPanel.js +9 -2
  30. package/lib/types/components/questions/PlanReviewPanel.d.ts +39 -0
  31. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -0
  32. package/lib/types/components/questions/PlanReviewPanel.js +168 -0
  33. package/lib/types/i18n.d.ts +189 -2
  34. package/lib/types/i18n.d.ts.map +1 -1
  35. package/lib/types/i18n.js +86 -1
  36. package/lib/types/ink/stringWidth.d.ts.map +1 -1
  37. package/lib/types/ink/stringWidth.js +61 -0
  38. package/lib/types/plugin.d.ts.map +1 -1
  39. package/lib/types/plugin.js +15 -0
  40. package/lib/types/questions.d.ts +3 -0
  41. package/lib/types/questions.d.ts.map +1 -1
  42. package/lib/types/questions.js +13 -1
  43. package/lib/types/screens/Chat.d.ts +8 -13
  44. package/lib/types/screens/Chat.d.ts.map +1 -1
  45. package/lib/types/screens/Chat.js +47 -18
  46. package/lib/types/utils/modifiers.d.ts +19 -0
  47. package/lib/types/utils/modifiers.d.ts.map +1 -0
  48. package/lib/types/utils/modifiers.js +17 -0
  49. package/package.json +2 -1
@@ -16,10 +16,13 @@
16
16
  * marked, and already-known events are never touched.
17
17
  *
18
18
  * Storage notes: the jsonl persistence flushes by APPENDING zstd frames, so
19
- * the file is a concatenation of frames; every frame is decoded, the log is
20
- * rewritten as one frame, and the file is swapped in atomically (tmp +
21
- * rename). Any decode/parse anomaly aborts the repair and resume proceeds
22
- * unpatched — the failure mode degrades to the pre-patch behavior.
19
+ * the file is a concatenation of frames. Frame layout is load-bearing: the
20
+ * backend asserts frame 0 holds EXACTLY the header line (listings read only
21
+ * that frame; `assertZstdHeaderFrame`), so the repair re-encodes each frame
22
+ * with its original line set — frame boundaries are preserved 1:1, and any
23
+ * frame whose lines were untouched is copied verbatim. Any decode/parse
24
+ * anomaly aborts the repair and resume proceeds unpatched — the failure
25
+ * mode degrades to the pre-patch behavior.
23
26
  * @module @deepseek-harness-tui/dsh-tui/compat/sessionLog
24
27
  */
25
28
  import { KNOWN_SESSION_EVENT_TYPES } from '@deepseek-ai/dsh-session';
@@ -61,11 +64,12 @@ function findSessionLogFile(sessionId) {
61
64
  return undefined;
62
65
  }
63
66
  /**
64
- * Decode a (possibly multi-frame) zstd jsonl log. Frames are split by magic
65
- * scan; any frame failing to decode or any line failing to parse throws, so
66
- * callers abort instead of rewriting a log they did not fully understand.
67
+ * Decode a (possibly multi-frame) zstd jsonl log, keeping frames separate.
68
+ * Frames are split by magic scan; any frame failing to decode or any line
69
+ * failing to parse throws, so callers abort instead of rewriting a log they
70
+ * did not fully understand.
67
71
  * @param buf - Raw file bytes.
68
- * @returns Parsed event envelopes in log order.
72
+ * @returns Per-frame byte spans and parsed event envelopes, in log order.
69
73
  */
70
74
  function decodeFrames(buf) {
71
75
  const offsets = [];
@@ -75,20 +79,21 @@ function decodeFrames(buf) {
75
79
  }
76
80
  if (offsets.length === 0)
77
81
  throw new Error('no zstd frame found');
78
- let text = '';
79
- for (let i = 0; i < offsets.length; i++) {
82
+ return offsets.map((start, i) => {
80
83
  const end = i + 1 < offsets.length ? offsets[i + 1] : buf.length;
81
- text += zstdDecompressSync(buf.subarray(offsets[i], end)).toString('utf8');
82
- }
83
- return text
84
- .split('\n')
85
- .filter((line) => line.length > 0)
86
- .map((line) => {
87
- const parsed = JSON.parse(line);
88
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
89
- throw new Error('session log line is not an event envelope');
90
- }
91
- return parsed;
84
+ const raw = buf.subarray(start, end);
85
+ const text = zstdDecompressSync(raw).toString('utf8');
86
+ const events = text
87
+ .split('\n')
88
+ .filter((line) => line.length > 0)
89
+ .map((line) => {
90
+ const parsed = JSON.parse(line);
91
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
92
+ throw new Error('session log line is not an event envelope');
93
+ }
94
+ return parsed;
95
+ });
96
+ return { raw, events };
92
97
  });
93
98
  }
94
99
  /**
@@ -103,25 +108,35 @@ export function repairSessionLogForResume(sessionId) {
103
108
  const file = findSessionLogFile(sessionId);
104
109
  if (file === undefined)
105
110
  return 'unavailable';
106
- const events = decodeFrames(readFileSync(file));
107
- let changed = false;
108
- for (const event of events) {
109
- const type = event['type'];
110
- // Only real log entries carry a numeric seq — the seq-less header row
111
- // is parsed by a separate path that must not see an extra field.
112
- if (typeof type === 'string' &&
113
- typeof event['seq'] === 'number' &&
114
- !KNOWN_SESSION_EVENT_TYPES.has(type) &&
115
- event['ignorable'] === undefined) {
116
- event['ignorable'] = true;
117
- changed = true;
111
+ const frames = decodeFrames(readFileSync(file));
112
+ // Mark unknown types in place, tracking which frames actually changed:
113
+ // untouched frames are copied back verbatim, so the header frame keeps
114
+ // its exact original bytes (and the one-header-line invariant with it).
115
+ const dirty = new Set();
116
+ frames.forEach((frame, index) => {
117
+ for (const event of frame.events) {
118
+ const type = event['type'];
119
+ // Only real log entries carry a numeric seq — the seq-less header row
120
+ // is parsed by a separate path that must not see an extra field.
121
+ if (typeof type === 'string' &&
122
+ typeof event['seq'] === 'number' &&
123
+ !KNOWN_SESSION_EVENT_TYPES.has(type) &&
124
+ event['ignorable'] === undefined) {
125
+ event['ignorable'] = true;
126
+ dirty.add(index);
127
+ }
118
128
  }
119
- }
120
- if (!changed)
129
+ });
130
+ if (dirty.size === 0)
121
131
  return 'clean';
122
- const payload = events.map((event) => JSON.stringify(event)).join('\n') + '\n';
132
+ const parts = frames.map((frame, index) => {
133
+ if (!dirty.has(index))
134
+ return frame.raw;
135
+ const payload = frame.events.map((event) => JSON.stringify(event)).join('\n') + '\n';
136
+ return zstdCompressSync(Buffer.from(payload, 'utf8'));
137
+ });
123
138
  const tmp = `${file}.compat-${randomUUID()}.tmp`;
124
- writeFileSync(tmp, zstdCompressSync(Buffer.from(payload, 'utf8')));
139
+ writeFileSync(tmp, Buffer.concat(parts));
125
140
  renameSync(tmp, file);
126
141
  return 'repaired';
127
142
  }
@@ -129,6 +144,76 @@ export function repairSessionLogForResume(sessionId) {
129
144
  return 'unavailable';
130
145
  }
131
146
  }
147
+ /**
148
+ * Read a session's display title from its persisted log, tolerantly.
149
+ *
150
+ * Why not `persistence.load()`: the backend validates every event against
151
+ * KNOWN_SESSION_EVENT_TYPES and throws the WHOLE load when a third-party
152
+ * plugin wrote an unmarked unknown type (e.g. activity/status before the
153
+ * resume repair touched it) — which is exactly why pickers fell back to the
154
+ * cwd basename for every working-activity session. A picker label is
155
+ * read-only UI state: decoding frames directly here keeps titles working
156
+ * for logs the strict path refuses, now and for future plugin event types.
157
+ *
158
+ * Title precedence: the LAST `session/title` event wins (a /rename append
159
+ * overrides the first-prompt auto title), falling back to the first user
160
+ * message text. `hasUserMessage` drives the picker's launch-artifact filter.
161
+ * @param sessionId - Session whose log should be read.
162
+ * @returns The title info, or undefined when the log is absent/undecodable.
163
+ */
164
+ export function readSessionTitleFromLog(sessionId) {
165
+ try {
166
+ const file = findSessionLogFile(sessionId);
167
+ if (file === undefined)
168
+ return undefined;
169
+ const frames = decodeFrames(readFileSync(file));
170
+ let titled;
171
+ let firstUser;
172
+ let hasUserMessage = false;
173
+ for (const frame of frames) {
174
+ for (const event of frame.events) {
175
+ if (event['type'] === 'session/title') {
176
+ const title = event['data']?.['title'];
177
+ if (typeof title === 'string' && title.trim().length > 0)
178
+ titled = title;
179
+ }
180
+ else if (event['type'] === 'user/message') {
181
+ hasUserMessage = true;
182
+ if (firstUser === undefined) {
183
+ firstUser = firstTextOfContent(event['data']?.['content']);
184
+ }
185
+ }
186
+ }
187
+ }
188
+ return { title: titled ?? firstUser, hasUserMessage };
189
+ }
190
+ catch {
191
+ return undefined;
192
+ }
193
+ }
194
+ /**
195
+ * Extract the first text block from a user/message `content` payload.
196
+ * Content is normally a block array; a bare string is accepted defensively.
197
+ * @param content - The event's content field.
198
+ * @returns The trimmed text, or undefined when no text block exists.
199
+ */
200
+ function firstTextOfContent(content) {
201
+ if (typeof content === 'string')
202
+ return content.trim() || undefined;
203
+ if (!Array.isArray(content))
204
+ return undefined;
205
+ for (const block of content) {
206
+ if (block !== null &&
207
+ typeof block === 'object' &&
208
+ block.type === 'text' &&
209
+ typeof block.text === 'string') {
210
+ const text = (block.text).trim();
211
+ if (text.length > 0)
212
+ return text;
213
+ }
214
+ }
215
+ return undefined;
216
+ }
132
217
  /**
133
218
  * Compat entry for the resume path: repair the target session's log, then
134
219
  * let resume proceed regardless of outcome. Never throws, never blocks on
@@ -1 +1 @@
1
- {"version":3,"file":"CommandSuggestions.d.ts","sourceRoot":"","sources":["../../../src/components/CommandSuggestions.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAElD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,aAAa,EACb,OAAO,GACR,EAAE;IACD,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAA;IACjC,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;CAChB,GAAG,KAAK,CAAC,SAAS,CAoDlB"}
1
+ {"version":3,"file":"CommandSuggestions.d.ts","sourceRoot":"","sources":["../../../src/components/CommandSuggestions.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAGlD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,aAAa,EACb,OAAO,GACR,EAAE;IACD,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAA;IACjC,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;CAChB,GAAG,KAAK,CAAC,SAAS,CAqDlB"}
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from '../ui.js';
3
3
  import { stringWidth } from '../ink/stringWidth.js';
4
4
  import { truncateToWidth } from '../ink/truncateToWidth.js';
5
+ import { localizedDescription } from '../commands.js';
5
6
  /**
6
7
  * The slash-command suggestion overlay, ported from the leak's
7
8
  * `PromptInputFooterSuggestions.tsx` (command layout only): a name column
@@ -25,9 +26,10 @@ export function CommandSuggestions({ commands, selectedIndex, columns, }) {
25
26
  const tagText = command.tag ? `[${command.tag}] ` : '';
26
27
  const tagWidth = stringWidth(tagText);
27
28
  const descriptionWidth = Math.max(0, columns - nameWidth - tagWidth - 4);
28
- const description = stringWidth(command.description) > descriptionWidth
29
- ? truncateToWidth(command.description, descriptionWidth - 1) + '…'
30
- : command.description;
29
+ const rawDescription = localizedDescription(command);
30
+ const description = stringWidth(rawDescription) > descriptionWidth
31
+ ? truncateToWidth(rawDescription, descriptionWidth - 1) + '…'
32
+ : rawDescription;
31
33
  return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: isSelected ? 'suggestion' : undefined, dimColor: !isSelected, children: padded }), tagText ? _jsx(Text, { dimColor: true, children: tagText }) : null, _jsx(Text, { color: isSelected ? 'suggestion' : undefined, dimColor: !isSelected, children: description })] }, command.name));
32
34
  }) }));
33
35
  }
@@ -5,6 +5,7 @@ import type { LocalCommand } from '../commands.js';
5
5
  * (three-column shortcut layout, trimmed to the keys dsh-tui actually binds).
6
6
  * The command column lists the merged slash-command surface: built-in
7
7
  * commands plus plugin-registered ones from the DSH registry (plan/goal/…).
8
+ * Modifier labels follow the platform convention: ⌘ on macOS, ctrl elsewhere.
8
9
  */
9
10
  export declare function HelpMenu({ commands, }: {
10
11
  commands: readonly LocalCommand[];
@@ -1 +1 @@
1
- {"version":3,"file":"HelpMenu.d.ts","sourceRoot":"","sources":["../../../src/components/HelpMenu.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAElD;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,EACvB,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAA;CAClC,GAAG,KAAK,CAAC,SAAS,CA6DlB"}
1
+ {"version":3,"file":"HelpMenu.d.ts","sourceRoot":"","sources":["../../../src/components/HelpMenu.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAIlD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,EACvB,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAA;CAClC,GAAG,KAAK,CAAC,SAAS,CA6DlB"}
@@ -1,12 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Box from '../ink/components/Box.js';
3
3
  import Text from '../ink/components/Text.js';
4
+ import { localizedDescription } from '../commands.js';
5
+ import { modLabel } from '../utils/modifiers.js';
4
6
  /**
5
7
  * The `?` help menu, ported from the leak's `PromptInputHelpMenu.tsx`
6
8
  * (three-column shortcut layout, trimmed to the keys dsh-tui actually binds).
7
9
  * The command column lists the merged slash-command surface: built-in
8
10
  * commands plus plugin-registered ones from the DSH registry (plan/goal/…).
11
+ * Modifier labels follow the platform convention: ⌘ on macOS, ctrl elsewhere.
9
12
  */
10
13
  export function HelpMenu({ commands, }) {
11
- return (_jsxs(Box, { paddingX: 2, flexDirection: "row", gap: 4, children: [_jsxs(Box, { flexDirection: "column", width: 26, flexShrink: 0, children: [_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "/ for commands" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "? for this help" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "ctrl+o for verbose output" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "ctrl+t to toggle context" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "ctrl+r to search history" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "ctrl+c to interrupt" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "ctrl+d to exit" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "ctrl+l to redraw" }) })] }), _jsxs(Box, { flexDirection: "column", width: 24, flexShrink: 0, children: [_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "esc to clear input" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 for history" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "\u2190/\u2192 to move cursor" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "ctrl+\u2190/\u2192 for word jumps" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "tab to complete command" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "shift+tab to cycle effort" }) })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 1, children: [_jsx(Text, { dimColor: true, children: "commands:" }), commands.map(command => (_jsx(Box, { children: _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["/", command.name, " \u2014 ", command.description] }) }, command.name)))] })] }));
14
+ return (_jsxs(Box, { paddingX: 2, flexDirection: "row", gap: 4, children: [_jsxs(Box, { flexDirection: "column", width: 26, flexShrink: 0, children: [_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "/ for commands" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "? for this help" }) }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [modLabel, "o for verbose output"] }) }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [modLabel, "t to toggle context"] }) }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [modLabel, "r to search history"] }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "ctrl+c to interrupt" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "ctrl+d to exit" }) }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [modLabel, "l to redraw"] }) })] }), _jsxs(Box, { flexDirection: "column", width: 24, flexShrink: 0, children: [_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "esc to clear input" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 for history" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "\u2190/\u2192 to move cursor" }) }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [modLabel, "\u2190/\u2192 for word jumps"] }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "tab to complete command" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "shift+tab to cycle effort" }) })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 1, children: [_jsx(Text, { dimColor: true, children: "commands:" }), commands.map(command => (_jsx(Box, { children: _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["/", command.name, " \u2014 ", localizedDescription(command)] }) }, command.name)))] })] }));
12
15
  }
@@ -1 +1 @@
1
- {"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../src/components/PromptInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAwC5C;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAA;IAClB,KAAK,IAAI,IAAI,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,IAAI,IAAI,CAAA;IACpB;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAA;IACrD,6EAA6E;IAC7E,eAAe,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,IAAI,IAAI,CAAA;IACvB,8EAA8E;IAC9E,eAAe,CAAC,IAAI,IAAI,CAAA;IACxB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;CACzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,cAAc,EACd,eAAe,EACf,aAAa,GACd,EAAE,gBAAgB,qBA+xBlB"}
1
+ {"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../src/components/PromptInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAyC5C;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAA;IAClB,KAAK,IAAI,IAAI,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,IAAI,IAAI,CAAA;IACpB;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAA;IACrD,6EAA6E;IAC7E,eAAe,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,IAAI,IAAI,CAAA;IACvB,8EAA8E;IAC9E,eAAe,CAAC,IAAI,IAAI,CAAA;IACxB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;CACzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,cAAc,EACd,eAAe,EACf,aAAa,GACd,EAAE,gBAAgB,qBA+xBlB"}
@@ -8,6 +8,7 @@ import { formatClipboardInsert, readClipboard } from '../utils/clipboard.js';
8
8
  import { filterCommands, parseCommandName } from '../commands.js';
9
9
  import { appendHistory } from '../history.js';
10
10
  import { mentionAtCaret } from '../utils/mentions.js';
11
+ import { isMod } from '../utils/modifiers.js';
11
12
  import { CommandSuggestions } from './CommandSuggestions.js';
12
13
  import { FileSuggestions } from './FileSuggestions.js';
13
14
  import { HelpMenu } from './HelpMenu.js';
@@ -347,10 +348,10 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
347
348
  insertAtCaret(input.replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
348
349
  return;
349
350
  }
350
- // Ctrl+V: raw mode hands the key to the app, so the clipboard is read
351
- // here — text, or file paths when Explorer copied files (pasted files
352
- // insert their paths).
353
- if (key.ctrl && input === 'v') {
351
+ // Ctrl+V / Cmd+V: raw mode hands the key to the app, so the clipboard is
352
+ // read here — text, or file paths when Explorer copied files (pasted
353
+ // files insert their paths).
354
+ if (isMod(key) && input === 'v') {
354
355
  if (clipboardBusyRef.current)
355
356
  return;
356
357
  clipboardBusyRef.current = true;
@@ -425,9 +426,9 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
425
426
  submitText(line);
426
427
  return;
427
428
  }
428
- if (key.return && key.ctrl) {
429
- // Ctrl+Enter: interrupt the running turn and process this message
430
- // immediately (Windows Terminal sends CSI 13;5u / 13;1;5u).
429
+ if (key.return && isMod(key)) {
430
+ // Ctrl+Enter / Cmd+Enter: interrupt the running turn and process this
431
+ // message immediately (Windows Terminal sends CSI 13;5u / 13;1;5u).
431
432
  interruptSend();
432
433
  return;
433
434
  }
@@ -537,12 +538,12 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
537
538
  setCursor(previous => Math.min(value.length, previous + 1));
538
539
  return;
539
540
  }
540
- if (key.ctrl && key.leftArrow) {
541
+ if (isMod(key) && key.leftArrow) {
541
542
  // Jump to the previous word boundary (readline alt+b).
542
543
  setCursor(previous => wordBoundaryLeft(value, previous));
543
544
  return;
544
545
  }
545
- if (key.ctrl && key.rightArrow) {
546
+ if (isMod(key) && key.rightArrow) {
546
547
  // Jump to the next word boundary (readline alt+f).
547
548
  setCursor(previous => wordBoundaryRight(value, previous));
548
549
  return;
@@ -572,31 +573,31 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
572
573
  setCursor(nextLine === -1 ? value.length : nextLine);
573
574
  return;
574
575
  }
575
- if (key.ctrl && input === 'a') {
576
+ if (isMod(key) && input === 'a') {
576
577
  const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
577
578
  setCursor(lineStart);
578
579
  return;
579
580
  }
580
- if (key.ctrl && input === 'e') {
581
+ if (isMod(key) && input === 'e') {
581
582
  const nextLine = value.indexOf('\n', cursor);
582
583
  setCursor(nextLine === -1 ? value.length : nextLine);
583
584
  return;
584
585
  }
585
- if (key.ctrl && input === 'u') {
586
+ if (isMod(key) && input === 'u') {
586
587
  // Delete to start of line.
587
588
  const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
588
589
  setValue(value.slice(0, lineStart) + value.slice(cursor));
589
590
  setCursor(lineStart);
590
591
  return;
591
592
  }
592
- if (key.ctrl && input === 'k') {
593
+ if (isMod(key) && input === 'k') {
593
594
  // Delete to end of line.
594
595
  const nextLine = value.indexOf('\n', cursor);
595
596
  const end = nextLine === -1 ? value.length : nextLine;
596
597
  setValue(value.slice(0, cursor) + value.slice(end));
597
598
  return;
598
599
  }
599
- if (key.ctrl && input === 'w') {
600
+ if (isMod(key) && input === 'w') {
600
601
  // Delete the word before the cursor (CC/readline behavior): skip
601
602
  // trailing whitespace, then the whitespace-delimited word.
602
603
  const before = value.slice(0, cursor);
@@ -678,7 +679,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
678
679
  onToggleHelp();
679
680
  return;
680
681
  }
681
- if (input && !key.ctrl && !key.meta && !key.tab && !key.escape) {
682
+ if (input && !key.ctrl && !key.meta && !key.super && !key.tab && !key.escape) {
682
683
  // Typing anything else dismisses the help menu (CC behavior).
683
684
  if (helpOpen)
684
685
  onToggleHelp();
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The approval panel — Claude Code style permission prompt for the DSH
3
+ * approval seam (`ctx.approval`). One ask per panel: a permission-colored
4
+ * divider header naming the tool, the gated command recovered from the
5
+ * paired tool call (CC verbose full-command semantics), the asker's reason,
6
+ * "Do you want to proceed?", and a numbered Yes/No list.
7
+ *
8
+ * The protocol's outcome set is closed (allowed-once / rejected /
9
+ * cancelled / unavailable) with no allow-always or feedback channel, so
10
+ * the panel deliberately offers exactly two rows; Esc and Ctrl+C reject
11
+ * (fail closed, CC's "Esc to cancel" semantics).
12
+ */
13
+ import React from 'react';
14
+ import type { ApprovalSnapshot } from '../../approvals.js';
15
+ export type ApprovalPanelProps = {
16
+ /** The approval to render (from the ApprovalStore snapshot). */
17
+ readonly approval: ApprovalSnapshot;
18
+ readonly onDecide: (outcome: 'allowed-once' | 'rejected') => void;
19
+ };
20
+ export declare function ApprovalPanel({ approval, onDecide }: ApprovalPanelProps): React.ReactNode;
21
+ //# sourceMappingURL=ApprovalPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApprovalPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/approvals/ApprovalPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAE1D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,cAAc,GAAG,UAAU,KAAK,IAAI,CAAA;CAClE,CAAA;AAID,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,kBAAkB,GAAG,KAAK,CAAC,SAAS,CAmEzF"}
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The approval panel — Claude Code style permission prompt for the DSH
4
+ * approval seam (`ctx.approval`). One ask per panel: a permission-colored
5
+ * divider header naming the tool, the gated command recovered from the
6
+ * paired tool call (CC verbose full-command semantics), the asker's reason,
7
+ * "Do you want to proceed?", and a numbered Yes/No list.
8
+ *
9
+ * The protocol's outcome set is closed (allowed-once / rejected /
10
+ * cancelled / unavailable) with no allow-always or feedback channel, so
11
+ * the panel deliberately offers exactly two rows; Esc and Ctrl+C reject
12
+ * (fail closed, CC's "Esc to cancel" semantics).
13
+ */
14
+ import React from 'react';
15
+ import { t } from '../../i18n.js';
16
+ import { Box, Text, useInput } from '../../ui.js';
17
+ import { Divider } from '../design-system/Divider.js';
18
+ import { POINTER } from '../../cc/figures.js';
19
+ const OUTCOMES = ['allowed-once', 'rejected'];
20
+ export function ApprovalPanel({ approval, onDecide }) {
21
+ const [focusIndex, setFocusIndex] = React.useState(0);
22
+ useInput((input, key) => {
23
+ if (key.escape || (key.ctrl && input === 'c')) {
24
+ onDecide('rejected');
25
+ return;
26
+ }
27
+ if (key.upArrow) {
28
+ setFocusIndex(index => (index + OUTCOMES.length - 1) % OUTCOMES.length);
29
+ return;
30
+ }
31
+ if (key.downArrow) {
32
+ setFocusIndex(index => (index + 1) % OUTCOMES.length);
33
+ return;
34
+ }
35
+ if (input === '1' || input === '2') {
36
+ onDecide(OUTCOMES[Number(input) - 1]);
37
+ return;
38
+ }
39
+ if (key.return) {
40
+ onDecide(OUTCOMES[focusIndex]);
41
+ }
42
+ }, { isActive: true });
43
+ const optionLabels = [t('approval-yes'), t('approval-no')];
44
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 2, paddingRight: 2, width: "100%", children: [_jsx(Divider, { color: "permission", title: t('approval-waiting', { tool: approval.toolName }), padding: 4 }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [approval.command !== undefined && (_jsx(Box, { flexDirection: "column", paddingX: 2, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: approval.command }) })), approval.reason !== undefined && (_jsx(Text, { dimColor: true, wrap: "wrap", children: approval.reason })), _jsx(Text, { dimColor: true, children: t('approval-proceed') })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: optionLabels.map((label, index) => {
45
+ const focused = index === focusIndex;
46
+ return (_jsxs(Box, { flexDirection: "row", marginTop: focused ? 1 : 0, children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: focused ? 'claude' : undefined, bold: focused, children: focused ? POINTER : ' ' }) }), _jsxs(Text, { bold: focused, color: focused ? 'claude' : undefined, wrap: "wrap", children: [index + 1, ". ", label] })] }, label));
47
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: t('approval-hint') }) })] }));
48
+ }
@@ -24,6 +24,12 @@ export type AskUserQuestionPanelProps = {
24
24
  readonly description?: string;
25
25
  }>;
26
26
  readonly multiSelect?: boolean;
27
+ /** Presentation intent tag (rc.6): 'plan-review' switches to the
28
+ * decision-card layout; an intent never changes the protocol. */
29
+ readonly intent?: {
30
+ readonly kind: 'plan-review';
31
+ readonly approve: string;
32
+ };
27
33
  };
28
34
  /** 1-based position within the batch (progress header). */
29
35
  readonly position: number;
@@ -1 +1 @@
1
- {"version":3,"file":"AskUserQuestionPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/AskUserQuestionPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAM3D,MAAM,MAAM,yBAAyB,GAAG;IACtC,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE;QACjB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;QACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;YAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QAC3F,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;KAC/B,CAAA;IACD,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACzD,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAC9B,CAAA;AAED,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,yBAAyB,GAAG,KAAK,CAAC,SAAS,CA8S7C"}
1
+ {"version":3,"file":"AskUserQuestionPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/AskUserQuestionPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAO3D,MAAM,MAAM,yBAAyB,GAAG;IACtC,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE;QACjB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;QACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;YAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QAC3F,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;QAC9B;0EACkE;QAClE,QAAQ,CAAC,MAAM,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;YAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAA;KAC7E,CAAA;IACD,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACzD,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAC9B,CAAA;AAED,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,yBAAyB,GAAG,KAAK,CAAC,SAAS,CAoT7C"}
@@ -17,10 +17,17 @@ import { t } from '../../i18n.js';
17
17
  import { Box, Text, useInput } from '../../ui.js';
18
18
  import { Divider } from '../design-system/Divider.js';
19
19
  import { POINTER } from '../../cc/figures.js';
20
+ import { PlanReviewPanel } from './PlanReviewPanel.js';
20
21
  const CHECKED = '◉';
21
22
  const UNCHECKED = '○';
22
23
  const PENCIL = '✎';
23
24
  export function AskUserQuestionPanel({ question, position, total, answered, onAnswer, onCancel, }) {
25
+ // Plan-mode's exit_plan_mode ask carries a presentation intent: render
26
+ // the CC-style decision card instead of the generic questionnaire. The
27
+ // branch precedes every hook so hook order stays stable per remount key.
28
+ if (question.intent?.kind === 'plan-review') {
29
+ return _jsx(PlanReviewPanel, { question: question, onAnswer: onAnswer, onCancel: onCancel });
30
+ }
24
31
  const options = question.options ?? [];
25
32
  const multiSelect = question.multiSelect === true;
26
33
  /** Rows: the real options plus the inline input row at the tail. */
@@ -147,7 +154,7 @@ export function AskUserQuestionPanel({ question, position, total, answered, onAn
147
154
  setCustomCursor(customText.length);
148
155
  return;
149
156
  }
150
- if (!key.ctrl && !key.meta && input) {
157
+ if (!key.ctrl && !key.meta && !key.super && input) {
151
158
  setCustomText(text => text.slice(0, customCursor) + input + text.slice(customCursor));
152
159
  setCustomCursor(cursor => cursor + input.length);
153
160
  setError(null);
@@ -191,7 +198,7 @@ export function AskUserQuestionPanel({ question, position, total, answered, onAn
191
198
  }
192
199
  // Typing on an option appends into the input row; single-select also
193
200
  // attaches this option's label so Enter carries label + text (#9).
194
- if (!key.ctrl && !key.meta && input) {
201
+ if (!key.ctrl && !key.meta && !key.super && input) {
195
202
  appendText(input);
196
203
  if (!multiSelect)
197
204
  setAttached(options[focusIndex]?.label ?? null);
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The plan-review panel — Claude Code style exit-plan-mode decision card
3
+ * for the DSH user-interaction seam. plan-mode's `exit_plan_mode` tool asks
4
+ * through `ctx.userQuestions` with `intent: { kind: 'plan-review',
5
+ * approve }`: the plan markdown arrives in `detail`, the approve/decline
6
+ * choices in `options` (labels verbatim — the protocol answers with the
7
+ * asker's own labels).
8
+ *
9
+ * Protocol-exact answer mapping (dsh-plan-mode):
10
+ * - Approve: `{ selected: [intent.approve] }` — custom MUST be absent, or
11
+ * plan-mode treats it as keep-planning-with-feedback.
12
+ * - Keep planning / feedback: `{ selected: [declineLabel], custom? }` where
13
+ * declineLabel is the first option that is not the approve label.
14
+ * - Esc / Ctrl+C: the store rejects with ASK_CANCELLED, which plan-mode
15
+ * reads as "the user dismissed the review to speak instead".
16
+ */
17
+ import React from 'react';
18
+ import type { QuestionSelection } from '../../questions.js';
19
+ export type PlanReviewPanelProps = {
20
+ /** The plan-review question (intent.kind === 'plan-review'). */
21
+ readonly question: {
22
+ readonly question: string;
23
+ readonly header?: string;
24
+ readonly detail?: string;
25
+ readonly options?: ReadonlyArray<{
26
+ readonly label: string;
27
+ readonly description?: string;
28
+ }>;
29
+ readonly intent?: {
30
+ readonly kind: 'plan-review';
31
+ readonly approve: string;
32
+ };
33
+ };
34
+ readonly onAnswer: (selection: QuestionSelection) => void;
35
+ /** Esc / Ctrl+C — dismissed to speak instead (ASK_CANCELLED). */
36
+ readonly onCancel: () => void;
37
+ };
38
+ export declare function PlanReviewPanel({ question, onAnswer, onCancel, }: PlanReviewPanelProps): React.ReactNode;
39
+ //# sourceMappingURL=PlanReviewPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PlanReviewPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/PlanReviewPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAI3D,MAAM,MAAM,oBAAoB,GAAG;IACjC,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE;QACjB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;QACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;YAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QAC3F,QAAQ,CAAC,MAAM,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;YAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAA;KAC7E,CAAA;IACD,QAAQ,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACzD,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAC9B,CAAA;AAED,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,oBAAoB,GAAG,KAAK,CAAC,SAAS,CAgOxC"}