@tangle-network/ui 11.16.5 → 11.16.6

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,11 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.16.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 74fbea5: A closed terminal command is a quiet bordered row on the surface it sits on, and turns into the dark terminal only when opened, so a list of commands no longer reads as a stack of black bars. Red is kept for evidence the command failed: a nonzero exit, a timeout or a signal. A tool that reported an error with nothing recorded about the command shows a neutral "tool error" badge. A command wraps between its tokens, so `--short` is no longer split at its hyphen; a token wider than the line still breaks, and the copied text is unchanged. The status pill marks a failed state with a cross instead of a slashed disc, which read as "disabled".
8
+
3
9
  ## 11.16.5
4
10
 
5
11
  ### Patch Changes
@@ -10,8 +10,8 @@ import * as React from "react";
10
10
  * the tone's COLOUR, and the state's own LABEL as text.
11
11
  *
12
12
  * The glyphs are chosen to survive at 8px and to differ in outline rather than
13
- * in fill: a ring reads as "still open", a solid disc as "settled", a slashed
14
- * disc as "stopped". Two states never share one silhouette.
13
+ * in fill: a ring reads as "still open", a solid disc as "settled", a cross as
14
+ * "failed". Two states never share one silhouette.
15
15
  *
16
16
  * Each tone draws its fill, border and text from ONE matched token triple.
17
17
  * That pairing is the whole point: a status colour is solved against its own
@@ -45,22 +45,16 @@ function ToneGlyph({ tone }) {
45
45
  r: "3.25"
46
46
  })
47
47
  });
48
- case "danger": return /* @__PURE__ */ jsxs("svg", {
48
+ case "danger": return /* @__PURE__ */ jsx("svg", {
49
49
  ...common,
50
50
  "aria-hidden": "true",
51
51
  fill: "none",
52
52
  stroke: "currentColor",
53
- strokeWidth: "1.5",
54
- children: [/* @__PURE__ */ jsx("circle", {
55
- cx: "4",
56
- cy: "4",
57
- r: "3.1",
58
- fill: "currentColor",
59
- opacity: "0.35"
60
- }), /* @__PURE__ */ jsx("path", {
61
- d: "M1.9 6.1 6.1 1.9",
53
+ strokeWidth: "1.75",
54
+ children: /* @__PURE__ */ jsx("path", {
55
+ d: "M1.5 1.5 6.5 6.5M6.5 1.5 1.5 6.5",
62
56
  strokeLinecap: "round"
63
- })]
57
+ })
64
58
  });
65
59
  case "warning": return /* @__PURE__ */ jsx("svg", {
66
60
  ...common,
@@ -14,7 +14,9 @@ export interface CommandPreviewProps {
14
14
  * One shell command as a terminal block: a `$ command` prompt line with its
15
15
  * exit status, and stdout and stderr in separate regions below it.
16
16
  *
17
- * The block is always dark. It scopes the dark token set onto itself with
17
+ * Closed, a command is a quiet row on the surface it sits on: a list of
18
+ * commands should read as a list, not a stack of black bars. Opened, it is a
19
+ * dark terminal. It scopes the dark token set onto itself with
18
20
  * `data-theme="dark"`, so stderr, the badges and the muted prompt keep the
19
21
  * contrast they were designed with on a dark ground, in a light console too.
20
22
  */
@@ -61,10 +61,14 @@ function extractCommandOutput(output) {
61
61
  }
62
62
  function fromObject(obj) {
63
63
  const text = (value) => value === void 0 || value === null ? "" : String(value);
64
+ const timedOut = obj.timedOut ?? obj.timed_out;
65
+ const signal = obj.signal;
64
66
  return {
65
67
  stdout: text(obj.stdout ?? obj.output),
66
68
  stderr: text(obj.stderr),
67
- exitCode: finiteNumber(obj.exitCode ?? obj.exit_code ?? obj.code)
69
+ exitCode: finiteNumber(obj.exitCode ?? obj.exit_code ?? obj.code),
70
+ ...timedOut === true ? { timedOut: true } : {},
71
+ ...typeof signal === "string" && signal.length > 0 ? { signal } : {}
68
72
  };
69
73
  }
70
74
  /** The keys a serialized command result carries, and nothing else. */
@@ -96,9 +100,12 @@ function parseStructured(text) {
96
100
  return;
97
101
  }
98
102
  }
99
- /** Exit status as a badge: the code when one is known, else the tool's status.
100
- * Red only for a nonzero exit or an error; a clean run stays quiet green. */
101
- function ExitBadge({ exitCode, status }) {
103
+ /** Exit status as a badge. Red only where the command itself is known to have
104
+ * failed: a nonzero exit, a timeout, or a signal. A tool that reported an
105
+ * error with nothing recorded about the command (no exit code, or exit 0
106
+ * from a runner that failed afterwards) gets a neutral badge; its error text
107
+ * prints below. A clean run stays quiet green. */
108
+ function ExitBadge({ output, status }) {
102
109
  if (status === "running" || status === "pending") return /* @__PURE__ */ jsxs("span", {
103
110
  className: "inline-flex shrink-0 items-center gap-1 text-[11px] text-muted-foreground",
104
111
  children: [/* @__PURE__ */ jsx(Loader2, {
@@ -106,19 +113,53 @@ function ExitBadge({ exitCode, status }) {
106
113
  "aria-hidden": "true"
107
114
  }), "running"]
108
115
  });
109
- const failed = status === "error" || exitCode !== void 0 && exitCode !== 0;
110
- const label = exitCode !== void 0 ? `exit ${exitCode}` : failed ? "error" : null;
116
+ const { exitCode, timedOut, signal } = output;
117
+ let label;
118
+ let tone;
119
+ if (timedOut) {
120
+ label = "timed out";
121
+ tone = "danger";
122
+ } else if (signal) {
123
+ label = signal;
124
+ tone = "danger";
125
+ } else if (exitCode !== void 0 && exitCode !== 0) {
126
+ label = `exit ${exitCode}`;
127
+ tone = "danger";
128
+ } else if (status === "error") {
129
+ label = exitCode === 0 ? "exit 0 · tool error" : "tool error";
130
+ tone = "neutral";
131
+ } else {
132
+ label = exitCode === 0 ? "exit 0" : null;
133
+ tone = "success";
134
+ }
111
135
  if (label === null) return null;
112
136
  return /* @__PURE__ */ jsx("span", {
113
- className: cn("inline-flex shrink-0 items-center rounded-full px-1.5 py-px text-[11px] font-medium tabular-nums", failed ? "bg-[var(--surface-danger-bg)] text-[var(--surface-danger-text)]" : "bg-[var(--surface-success-bg)] text-[var(--surface-success-text)]"),
137
+ className: cn("inline-flex shrink-0 items-center rounded-full border px-1.5 py-px text-[11px] font-medium tabular-nums", tone === "danger" && "border-transparent bg-[var(--surface-danger-bg)] text-[var(--surface-danger-text)]", tone === "success" && "border-transparent bg-[var(--surface-success-bg)] text-[var(--surface-success-text)]", tone === "neutral" && "border-[var(--surface-neutral-border)] bg-[var(--surface-neutral-bg)] text-[var(--surface-neutral-text)]"),
114
138
  children: label
115
139
  });
116
140
  }
117
141
  /**
142
+ * A command split at its whitespace, each token in an inline box. Browsers
143
+ * break after a hyphen, which split `--short` into `--` and `short` on a
144
+ * phone. A box moves to the next line whole, and breaks inside only when it
145
+ * is wider than the line (a full SHA). The text is unchanged, so a copy still
146
+ * pastes the exact command.
147
+ */
148
+ function CommandText({ command, all }) {
149
+ const tokens = command.split(/(\s+)/);
150
+ const limit = all ? tokens.length : 128;
151
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [tokens.slice(0, limit).map((token, i) => token.length > 0 && !/\s/.test(token) ? /* @__PURE__ */ jsx("span", {
152
+ className: "inline-block max-w-full [overflow-wrap:anywhere]",
153
+ children: token
154
+ }, i) : token), limit < tokens.length ? tokens.slice(limit).join("") : null] });
155
+ }
156
+ /**
118
157
  * One shell command as a terminal block: a `$ command` prompt line with its
119
158
  * exit status, and stdout and stderr in separate regions below it.
120
159
  *
121
- * The block is always dark. It scopes the dark token set onto itself with
160
+ * Closed, a command is a quiet row on the surface it sits on: a list of
161
+ * commands should read as a list, not a stack of black bars. Opened, it is a
162
+ * dark terminal. It scopes the dark token set onto itself with
122
163
  * `data-theme="dark"`, so stderr, the badges and the muted prompt keep the
123
164
  * contrast they were designed with on a dark ground, in a light console too.
124
165
  */
@@ -139,23 +180,27 @@ const CommandPreview = memo(({ part, defaultExpanded = false, className }) => {
139
180
  }),
140
181
  /* @__PURE__ */ jsx("code", {
141
182
  className: cn("min-w-0 flex-1 whitespace-pre-wrap [overflow-wrap:anywhere]", expanded ? "" : "line-clamp-2"),
142
- children: command
183
+ children: /* @__PURE__ */ jsx(CommandText, {
184
+ command,
185
+ all: expanded
186
+ })
143
187
  }),
144
188
  /* @__PURE__ */ jsx(ExitBadge, {
145
- exitCode: output.exitCode,
189
+ output,
146
190
  status
147
191
  })
148
192
  ] });
149
193
  return /* @__PURE__ */ jsxs("div", {
150
- "data-theme": "dark",
194
+ "data-theme": expanded ? "dark" : void 0,
151
195
  "data-testid": "command-preview",
152
- className: cn("overflow-hidden rounded-[var(--radius-md)] bg-[var(--md3-surface-container-lowest)] font-mono text-xs text-foreground", className),
196
+ "data-expanded": expanded,
197
+ className: cn("overflow-hidden rounded-[var(--radius-md)] border font-mono text-xs text-foreground", expanded ? "border-[var(--md3-surface-container-lowest)] bg-[var(--md3-surface-container-lowest)]" : "border-border bg-transparent", className),
153
198
  children: [hasBody ? /* @__PURE__ */ jsxs("button", {
154
199
  type: "button",
155
200
  onClick: () => setExpanded((value) => !value),
156
201
  "aria-expanded": expanded,
157
202
  "aria-controls": bodyId,
158
- className: cn("flex w-full items-start gap-2 px-3 py-2 text-left transition-colors hover:bg-white/5", focusRing),
203
+ className: cn("flex w-full items-start gap-2 px-3 py-2 text-left transition-colors", expanded ? "hover:bg-white/5" : "hover:bg-muted", focusRing),
159
204
  children: [promptLine, expanded ? /* @__PURE__ */ jsx(ChevronDown, {
160
205
  className: "mt-px h-3.5 w-3.5 shrink-0 text-muted-foreground",
161
206
  "aria-hidden": "true"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.16.5",
3
+ "version": "11.16.6",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -142,7 +142,7 @@
142
142
  "peerDependencies": {
143
143
  "@hocuspocus/provider": ">=2.15.0 <5.0.0",
144
144
  "@nanostores/react": ">=0.8.4 <3.0.0",
145
- "@tangle-network/brand": "^1.10.1",
145
+ "@tangle-network/brand": "^1.10.2",
146
146
  "@tiptap/core": ">=3.20.4 <4.0.0",
147
147
  "@tiptap/extension-collaboration": ">=3.20.4 <4.0.0",
148
148
  "@tiptap/extension-collaboration-caret": ">=3.7.0 <4.0.0",
@@ -11,8 +11,8 @@ import { cn } from "../lib/utils";
11
11
  * the tone's COLOUR, and the state's own LABEL as text.
12
12
  *
13
13
  * The glyphs are chosen to survive at 8px and to differ in outline rather than
14
- * in fill: a ring reads as "still open", a solid disc as "settled", a slashed
15
- * disc as "stopped". Two states never share one silhouette.
14
+ * in fill: a ring reads as "still open", a solid disc as "settled", a cross as
15
+ * "failed". Two states never share one silhouette.
16
16
  *
17
17
  * Each tone draws its fill, border and text from ONE matched token triple.
18
18
  * That pairing is the whole point: a status colour is solved against its own
@@ -107,8 +107,8 @@ function ToneGlyph({ tone }: { tone: StatusTone }) {
107
107
  <circle cx="4" cy="4" r="3.25" />
108
108
  </svg>
109
109
  );
110
- // Settled and bad: a disc with a bar through it, so it differs from
111
- // success in OUTLINE and not only in hue.
110
+ // Settled and bad: a cross, so it differs from success in OUTLINE and not
111
+ // only in hue. A slashed disc read as "disabled" beside a failed run.
112
112
  case "danger":
113
113
  return (
114
114
  <svg
@@ -116,10 +116,9 @@ function ToneGlyph({ tone }: { tone: StatusTone }) {
116
116
  aria-hidden="true"
117
117
  fill="none"
118
118
  stroke="currentColor"
119
- strokeWidth="1.5"
119
+ strokeWidth="1.75"
120
120
  >
121
- <circle cx="4" cy="4" r="3.1" fill="currentColor" opacity="0.35" />
122
- <path d="M1.9 6.1 6.1 1.9" strokeLinecap="round" />
121
+ <path d="M1.5 1.5 6.5 6.5M6.5 1.5 1.5 6.5" strokeLinecap="round" />
123
122
  </svg>
124
123
  );
125
124
  // Needs a person: a triangle.
@@ -0,0 +1,73 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import type { ToolPart } from "../types/parts";
4
+ import { CommandPreview } from "./command-preview";
5
+
6
+ function part(state: Partial<ToolPart["state"]>): ToolPart {
7
+ return {
8
+ type: "tool",
9
+ id: "t1",
10
+ tool: "bash",
11
+ state: { status: "completed", input: { command: "git status --short" }, ...state },
12
+ } as ToolPart;
13
+ }
14
+
15
+ describe("CommandPreview", () => {
16
+ it("shows a neutral badge, not a red exit, when a tool errored without an exit code", () => {
17
+ render(<CommandPreview part={part({ status: "error", output: "warning: x" })} />);
18
+ const badge = screen.getByText("tool error");
19
+ expect(badge.className).toContain("surface-neutral");
20
+ expect(badge.className).not.toContain("surface-danger");
21
+ expect(screen.queryByText("error")).toBeNull();
22
+ });
23
+
24
+ it("keeps a recorded nonzero exit red", () => {
25
+ render(
26
+ <CommandPreview
27
+ part={part({ status: "error", output: { stdout: "", stderr: "no", exitCode: 2 } })}
28
+ />,
29
+ );
30
+ expect(screen.getByText("exit 2").className).toContain("surface-danger");
31
+ });
32
+
33
+ it("boxes each token so a phone wraps between flags", () => {
34
+ render(<CommandPreview part={part({ output: "M a" })} />);
35
+ const flag = screen.getByText("--short");
36
+ expect(flag.tagName).toBe("SPAN");
37
+ expect(flag.className).toContain("inline-block");
38
+ // A box wider than the line still breaks, so no token overflows.
39
+ expect(flag.className).toContain("[overflow-wrap:anywhere]");
40
+ // The text is unchanged, so a copy pastes the exact command.
41
+ expect(screen.getByTestId("command-preview").querySelector("code")?.textContent).toBe(
42
+ "git status --short",
43
+ );
44
+ });
45
+
46
+ it("treats a timeout or a signal as a failure even without an exit code", () => {
47
+ render(
48
+ <CommandPreview
49
+ part={part({ status: "error", output: { stdout: "", stderr: "", timedOut: true } })}
50
+ />,
51
+ );
52
+ expect(screen.getByText("timed out").className).toContain("surface-danger");
53
+ });
54
+
55
+ it("keeps a runner error after a clean exit neutral", () => {
56
+ render(
57
+ <CommandPreview
58
+ part={part({ status: "error", output: { stdout: "ok", stderr: "", exitCode: 0 } })}
59
+ />,
60
+ );
61
+ expect(screen.getByText("exit 0 · tool error").className).toContain("surface-neutral");
62
+ });
63
+
64
+ it("is a quiet row until opened, then a dark terminal", () => {
65
+ render(<CommandPreview part={part({ output: "M a" })} />);
66
+ const block = screen.getByTestId("command-preview");
67
+ expect(block).not.toHaveAttribute("data-theme");
68
+ expect(block.className).toContain("bg-transparent");
69
+ fireEvent.click(screen.getByRole("button"));
70
+ expect(block).toHaveAttribute("data-theme", "dark");
71
+ expect(screen.getByRole("region", { name: "stdout" })).toHaveTextContent("M a");
72
+ });
73
+ });
@@ -36,6 +36,10 @@ export interface CommandOutput {
36
36
  stderr: string;
37
37
  /** Undefined when the producer persisted plain text and never reported one. */
38
38
  exitCode: number | undefined;
39
+ /** The command ran past its time limit. */
40
+ timedOut?: boolean;
41
+ /** The signal that ended the command, such as `SIGKILL`. */
42
+ signal?: string;
39
43
  }
40
44
 
41
45
  function finiteNumber(value: unknown): number | undefined {
@@ -72,10 +76,14 @@ export function extractCommandOutput(output: unknown): CommandOutput {
72
76
  function fromObject(obj: Record<string, unknown>): CommandOutput {
73
77
  const text = (value: unknown) =>
74
78
  value === undefined || value === null ? "" : String(value);
79
+ const timedOut = obj.timedOut ?? obj.timed_out;
80
+ const signal = obj.signal;
75
81
  return {
76
82
  stdout: text(obj.stdout ?? obj.output),
77
83
  stderr: text(obj.stderr),
78
84
  exitCode: finiteNumber(obj.exitCode ?? obj.exit_code ?? obj.code),
85
+ ...(timedOut === true ? { timedOut: true } : {}),
86
+ ...(typeof signal === "string" && signal.length > 0 ? { signal } : {}),
79
87
  };
80
88
  }
81
89
 
@@ -117,13 +125,16 @@ function parseStructured(text: string): CommandOutput | undefined {
117
125
  }
118
126
  }
119
127
 
120
- /** Exit status as a badge: the code when one is known, else the tool's status.
121
- * Red only for a nonzero exit or an error; a clean run stays quiet green. */
128
+ /** Exit status as a badge. Red only where the command itself is known to have
129
+ * failed: a nonzero exit, a timeout, or a signal. A tool that reported an
130
+ * error with nothing recorded about the command (no exit code, or exit 0
131
+ * from a runner that failed afterwards) gets a neutral badge; its error text
132
+ * prints below. A clean run stays quiet green. */
122
133
  function ExitBadge({
123
- exitCode,
134
+ output,
124
135
  status,
125
136
  }: {
126
- exitCode: number | undefined;
137
+ output: CommandOutput;
127
138
  status: ToolPart["state"]["status"];
128
139
  }) {
129
140
  if (status === "running" || status === "pending") {
@@ -134,19 +145,37 @@ function ExitBadge({
134
145
  </span>
135
146
  );
136
147
  }
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);
140
- const label =
141
- exitCode !== undefined ? `exit ${exitCode}` : failed ? "error" : null;
148
+ const { exitCode, timedOut, signal } = output;
149
+ let label: string | null;
150
+ let tone: "danger" | "success" | "neutral";
151
+ if (timedOut) {
152
+ label = "timed out";
153
+ tone = "danger";
154
+ } else if (signal) {
155
+ label = signal;
156
+ tone = "danger";
157
+ } else if (exitCode !== undefined && exitCode !== 0) {
158
+ label = `exit ${exitCode}`;
159
+ tone = "danger";
160
+ } else if (status === "error") {
161
+ label = exitCode === 0 ? "exit 0 · tool error" : "tool error";
162
+ tone = "neutral";
163
+ } else {
164
+ label = exitCode === 0 ? "exit 0" : null;
165
+ tone = "success";
166
+ }
142
167
  if (label === null) return null;
143
168
  return (
144
169
  <span
145
170
  className={cn(
146
- "inline-flex shrink-0 items-center rounded-full px-1.5 py-px text-[11px] font-medium tabular-nums",
147
- failed
148
- ? "bg-[var(--surface-danger-bg)] text-[var(--surface-danger-text)]"
149
- : "bg-[var(--surface-success-bg)] text-[var(--surface-success-text)]",
171
+ // Every badge has a border, so mixed rows keep one height.
172
+ "inline-flex shrink-0 items-center rounded-full border px-1.5 py-px text-[11px] font-medium tabular-nums",
173
+ tone === "danger" &&
174
+ "border-transparent bg-[var(--surface-danger-bg)] text-[var(--surface-danger-text)]",
175
+ tone === "success" &&
176
+ "border-transparent bg-[var(--surface-success-bg)] text-[var(--surface-success-text)]",
177
+ tone === "neutral" &&
178
+ "border-[var(--surface-neutral-border)] bg-[var(--surface-neutral-bg)] text-[var(--surface-neutral-text)]",
150
179
  )}
151
180
  >
152
181
  {label}
@@ -154,11 +183,44 @@ function ExitBadge({
154
183
  );
155
184
  }
156
185
 
186
+ /** A closed row shows two lines, so only its first tokens need their own
187
+ * box; a long script renders the rest as plain text. */
188
+ const BOXED_TOKENS = 64;
189
+
190
+ /**
191
+ * A command split at its whitespace, each token in an inline box. Browsers
192
+ * break after a hyphen, which split `--short` into `--` and `short` on a
193
+ * phone. A box moves to the next line whole, and breaks inside only when it
194
+ * is wider than the line (a full SHA). The text is unchanged, so a copy still
195
+ * pastes the exact command.
196
+ */
197
+ function CommandText({ command, all }: { command: string; all: boolean }) {
198
+ const tokens = command.split(/(\s+)/);
199
+ const limit = all ? tokens.length : BOXED_TOKENS * 2;
200
+ return (
201
+ <>
202
+ {tokens.slice(0, limit).map((token, i) =>
203
+ token.length > 0 && !/\s/.test(token) ? (
204
+ // Tokens repeat (two spaces, two "&&"), so position is the key.
205
+ <span key={i} className="inline-block max-w-full [overflow-wrap:anywhere]">
206
+ {token}
207
+ </span>
208
+ ) : (
209
+ token
210
+ ),
211
+ )}
212
+ {limit < tokens.length ? tokens.slice(limit).join("") : null}
213
+ </>
214
+ );
215
+ }
216
+
157
217
  /**
158
218
  * One shell command as a terminal block: a `$ command` prompt line with its
159
219
  * exit status, and stdout and stderr in separate regions below it.
160
220
  *
161
- * The block is always dark. It scopes the dark token set onto itself with
221
+ * Closed, a command is a quiet row on the surface it sits on: a list of
222
+ * commands should read as a list, not a stack of black bars. Opened, it is a
223
+ * dark terminal. It scopes the dark token set onto itself with
162
224
  * `data-theme="dark"`, so stderr, the badges and the muted prompt keep the
163
225
  * contrast they were designed with on a dark ground, in a light console too.
164
226
  */
@@ -195,18 +257,24 @@ export const CommandPreview = memo(
195
257
  expanded ? "" : "line-clamp-2",
196
258
  )}
197
259
  >
198
- {command}
260
+ <CommandText command={command} all={expanded} />
199
261
  </code>
200
- <ExitBadge exitCode={output.exitCode} status={status} />
262
+ <ExitBadge output={output} status={status} />
201
263
  </>
202
264
  );
203
265
 
204
266
  return (
205
267
  <div
206
- data-theme="dark"
268
+ data-theme={expanded ? "dark" : undefined}
207
269
  data-testid="command-preview"
270
+ data-expanded={expanded}
208
271
  className={cn(
209
- "overflow-hidden rounded-[var(--radius-md)] bg-[var(--md3-surface-container-lowest)] font-mono text-xs text-foreground",
272
+ // One border in both states, so opening a row does not shift the
273
+ // list; the terminal's is its own fill colour.
274
+ "overflow-hidden rounded-[var(--radius-md)] border font-mono text-xs text-foreground",
275
+ expanded
276
+ ? "border-[var(--md3-surface-container-lowest)] bg-[var(--md3-surface-container-lowest)]"
277
+ : "border-border bg-transparent",
210
278
  className,
211
279
  )}
212
280
  >
@@ -217,7 +285,8 @@ export const CommandPreview = memo(
217
285
  aria-expanded={expanded}
218
286
  aria-controls={bodyId}
219
287
  className={cn(
220
- "flex w-full items-start gap-2 px-3 py-2 text-left transition-colors hover:bg-white/5",
288
+ "flex w-full items-start gap-2 px-3 py-2 text-left transition-colors",
289
+ expanded ? "hover:bg-white/5" : "hover:bg-muted",
221
290
  focusRing,
222
291
  )}
223
292
  >