@tangle-network/ui 11.16.0 → 11.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.16.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 74e6dfb: `CommandPreview` output regions are keyboard-focusable and labelled (stdout, stderr, error), so a long output that scrolls inside the block can be scrolled without a pointer (WCAG 2.1.1; axe `scrollable-region-focusable`).
8
+
9
+ ## 11.16.1
10
+
11
+ ### Patch Changes
12
+
13
+ - c59ed11: `CommandPreview` prints a failed command's text once when the producer persisted it as both the output and the error.
14
+
15
+ It also reads a JSON string as a `{ stdout, stderr, exitCode }` envelope only when every key is an envelope key, so a command whose own output is JSON with a `stdout` field is shown as printed, and an `error` status turns the exit badge red even when the retained exit code is 0.
16
+
3
17
  ## 11.16.0
4
18
 
5
19
  ### Minor Changes
@@ -67,6 +67,20 @@ function fromObject(obj) {
67
67
  exitCode: finiteNumber(obj.exitCode ?? obj.exit_code ?? obj.code)
68
68
  };
69
69
  }
70
+ /** The keys a serialized command result carries, and nothing else. */
71
+ const ENVELOPE_KEYS = /* @__PURE__ */ new Set([
72
+ "stdout",
73
+ "stderr",
74
+ "output",
75
+ "exitCode",
76
+ "exit_code",
77
+ "code",
78
+ "signal",
79
+ "durationMs",
80
+ "duration_ms",
81
+ "timedOut",
82
+ "timed_out"
83
+ ]);
70
84
  /** A string that is a serialized `{ stdout, stderr, exitCode }` record. */
71
85
  function parseStructured(text) {
72
86
  const trimmed = text.trimStart();
@@ -76,6 +90,7 @@ function parseStructured(text) {
76
90
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
77
91
  const obj = parsed;
78
92
  if (!("stdout" in obj) && !("stderr" in obj)) return void 0;
93
+ if (!Object.keys(obj).every((key) => ENVELOPE_KEYS.has(key))) return;
79
94
  return fromObject(obj);
80
95
  } catch {
81
96
  return;
@@ -91,7 +106,7 @@ function ExitBadge({ exitCode, status }) {
91
106
  "aria-hidden": "true"
92
107
  }), "running"]
93
108
  });
94
- const failed = exitCode !== void 0 ? exitCode !== 0 : status === "error";
109
+ const failed = status === "error" || exitCode !== void 0 && exitCode !== 0;
95
110
  const label = exitCode !== void 0 ? `exit ${exitCode}` : failed ? "error" : null;
96
111
  if (label === null) return null;
97
112
  return /* @__PURE__ */ jsx("span", {
@@ -113,7 +128,8 @@ const CommandPreview = memo(({ part, defaultExpanded = false, className }) => {
113
128
  const command = commandOf(part.state.input);
114
129
  const { status } = part.state;
115
130
  const output = extractCommandOutput(part.state.output);
116
- const errorText = part.state.error;
131
+ const normalize = (text) => text.replace(/\s+/g, " ").trim();
132
+ const errorText = part.state.error !== void 0 && output.stdout.length > 0 && normalize(part.state.error) === normalize(output.stdout) ? void 0 : part.state.error;
117
133
  const hasBody = output.stdout.length > 0 || output.stderr.length > 0 || errorText !== void 0 && errorText.length > 0;
118
134
  const promptLine = /* @__PURE__ */ jsxs(Fragment$1, { children: [
119
135
  /* @__PURE__ */ jsx("span", {
@@ -155,16 +171,21 @@ const CommandPreview = memo(({ part, defaultExpanded = false, className }) => {
155
171
  className: "border-t border-white/10",
156
172
  children: [
157
173
  output.stdout ? /* @__PURE__ */ jsx("pre", {
158
- className: "max-h-80 overflow-auto whitespace-pre-wrap break-all px-3 py-2 leading-relaxed",
174
+ tabIndex: 0,
175
+ "aria-label": "stdout",
176
+ className: cn("max-h-80 overflow-auto whitespace-pre-wrap break-all px-3 py-2 leading-relaxed", focusRing),
159
177
  children: output.stdout
160
178
  }) : null,
161
179
  output.stderr ? /* @__PURE__ */ jsx("pre", {
180
+ tabIndex: 0,
162
181
  "aria-label": "stderr",
163
- className: "max-h-80 overflow-auto whitespace-pre-wrap break-all border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]",
182
+ className: cn("max-h-80 overflow-auto whitespace-pre-wrap break-all border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]", focusRing),
164
183
  children: output.stderr
165
184
  }) : null,
166
185
  errorText ? /* @__PURE__ */ jsx("pre", {
167
- className: "max-h-80 overflow-auto whitespace-pre-wrap break-all border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]",
186
+ tabIndex: 0,
187
+ "aria-label": "error",
188
+ className: cn("max-h-80 overflow-auto whitespace-pre-wrap break-all border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]", focusRing),
168
189
  children: errorText
169
190
  }) : null
170
191
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.16.0",
3
+ "version": "11.16.2",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -79,6 +79,21 @@ function fromObject(obj: Record<string, unknown>): CommandOutput {
79
79
  };
80
80
  }
81
81
 
82
+ /** The keys a serialized command result carries, and nothing else. */
83
+ const ENVELOPE_KEYS = new Set([
84
+ "stdout",
85
+ "stderr",
86
+ "output",
87
+ "exitCode",
88
+ "exit_code",
89
+ "code",
90
+ "signal",
91
+ "durationMs",
92
+ "duration_ms",
93
+ "timedOut",
94
+ "timed_out",
95
+ ]);
96
+
82
97
  /** A string that is a serialized `{ stdout, stderr, exitCode }` record. */
83
98
  function parseStructured(text: string): CommandOutput | undefined {
84
99
  const trimmed = text.trimStart();
@@ -89,7 +104,13 @@ function parseStructured(text: string): CommandOutput | undefined {
89
104
  return undefined;
90
105
  }
91
106
  const obj = parsed as Record<string, unknown>;
107
+ // Only an unambiguous envelope: a stream key, and nothing but envelope keys.
108
+ // A command whose own stdout is JSON that happens to have a `stdout` field
109
+ // (`{"stdout":"v","result":"ok"}`) is shown as the text it printed.
92
110
  if (!("stdout" in obj) && !("stderr" in obj)) return undefined;
111
+ if (!Object.keys(obj).every((key) => ENVELOPE_KEYS.has(key))) {
112
+ return undefined;
113
+ }
93
114
  return fromObject(obj);
94
115
  } catch {
95
116
  return undefined;
@@ -113,7 +134,9 @@ function ExitBadge({
113
134
  </span>
114
135
  );
115
136
  }
116
- const failed = exitCode !== undefined ? exitCode !== 0 : status === "error";
137
+ // The tool's own error status wins: a runner can fail after the shell exited
138
+ // 0, and a green "exit 0" beside that error would contradict it.
139
+ const failed = status === "error" || (exitCode !== undefined && exitCode !== 0);
117
140
  const label =
118
141
  exitCode !== undefined ? `exit ${exitCode}` : failed ? "error" : null;
119
142
  if (label === null) return null;
@@ -146,7 +169,16 @@ export const CommandPreview = memo(
146
169
  const command = commandOf(part.state.input);
147
170
  const { status } = part.state;
148
171
  const output = extractCommandOutput(part.state.output);
149
- const errorText = part.state.error;
172
+ // A failed call often persists the same text twice — as its output and as
173
+ // its error. Print it once. Only an exact repeat (ignoring whitespace) is
174
+ // folded: an error that merely CONTAINS the output, or the reverse, is a
175
+ // distinct message and keeps its own region.
176
+ const normalize = (text: string) => text.replace(/\s+/g, " ").trim();
177
+ const errorRepeats =
178
+ part.state.error !== undefined &&
179
+ output.stdout.length > 0 &&
180
+ normalize(part.state.error) === normalize(output.stdout);
181
+ const errorText = errorRepeats ? undefined : part.state.error;
150
182
  const hasBody =
151
183
  output.stdout.length > 0 ||
152
184
  output.stderr.length > 0 ||
@@ -203,20 +235,38 @@ export const CommandPreview = memo(
203
235
  {expanded && hasBody ? (
204
236
  <div id={bodyId} className="border-t border-white/10">
205
237
  {output.stdout ? (
206
- <pre className="max-h-80 overflow-auto whitespace-pre-wrap break-all px-3 py-2 leading-relaxed">
238
+ <pre
239
+ tabIndex={0}
240
+ aria-label="stdout"
241
+ className={cn(
242
+ "max-h-80 overflow-auto whitespace-pre-wrap break-all px-3 py-2 leading-relaxed",
243
+ focusRing,
244
+ )}
245
+ >
207
246
  {output.stdout}
208
247
  </pre>
209
248
  ) : null}
210
249
  {output.stderr ? (
211
250
  <pre
251
+ tabIndex={0}
212
252
  aria-label="stderr"
213
- className="max-h-80 overflow-auto whitespace-pre-wrap break-all border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]"
253
+ className={cn(
254
+ "max-h-80 overflow-auto whitespace-pre-wrap break-all border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]",
255
+ focusRing,
256
+ )}
214
257
  >
215
258
  {output.stderr}
216
259
  </pre>
217
260
  ) : null}
218
261
  {errorText ? (
219
- <pre className="max-h-80 overflow-auto whitespace-pre-wrap break-all border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]">
262
+ <pre
263
+ tabIndex={0}
264
+ aria-label="error"
265
+ className={cn(
266
+ "max-h-80 overflow-auto whitespace-pre-wrap break-all border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]",
267
+ focusRing,
268
+ )}
269
+ >
220
270
  {errorText}
221
271
  </pre>
222
272
  ) : null}