Skip to content

Commit e6164fc

Browse files
committed
fix(webapp,agent): contextual cmd+J, bare progress lines, error links land
- cmd+J is contextual: closed opens the panel, open starts a new chat; closing is Esc or the header's x — the New chat tooltip now shows cmd+J (displayed once, registered once) - in-flight tool work renders as a bare spinner line, not a bordered pill — chips are for artifacts that stay, progress is transient - error evidence and navigate targets normalize the API's friendly id to the raw fingerprint, so View similar failures opens the error page instead of 'Error not found'
1 parent e60385d commit e6164fc

6 files changed

Lines changed: 60 additions & 28 deletions

File tree

apps/webapp/app/components/dashboard-agent/DashboardAgent.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ export function DashboardAgent({
5151
}, []);
5252
// A request from `openWith`, handed to the panel. `seq` makes repeat requests
5353
// with the same text distinct, so the panel can tell them apart.
54+
// Bumped by contextual ⌘J while the panel is open; the panel starts a new
55+
// chat when it changes.
56+
const [newChatSeq, setNewChatSeq] = useState(0);
5457
const [requestedMessage, setRequestedMessage] = useState<
5558
{ text: string; seq: number } | undefined
5659
>(undefined);
@@ -71,12 +74,18 @@ export function DashboardAgent({
7174
setRequestedMessage((current) => ({ text: trimmed, seq: (current?.seq ?? 0) + 1 }));
7275
}, []);
7376

74-
// ⌘J toggles the panel. Opening mounts the composer, which focuses itself, so
75-
// the shortcut lands you in the text field. Enabled inside inputs too, so the
76-
// same keystroke closes the panel while you're typing in it.
77+
// ⌘J is contextual: closed → open the panel (the composer focuses itself, so
78+
// the keystroke lands you in the text field); open → start a new chat.
79+
// Closing is Esc or the header's ×, never ⌘J.
7780
useShortcutKeys({
7881
shortcut: TOGGLE_PANEL_SHORTCUT,
79-
action: () => setPanelOpen(!open),
82+
action: () => {
83+
if (!open) {
84+
setPanelOpen(true);
85+
} else {
86+
setNewChatSeq((seq) => seq + 1);
87+
}
88+
},
8089
disabled: !hasAccess,
8190
enabledOnInputElements: true,
8291
});
@@ -121,6 +130,7 @@ export function DashboardAgent({
121130
<DashboardAgentPanel
122131
onClose={() => setPanelOpen(false)}
123132
requestedMessage={requestedMessage}
133+
newChatSeq={newChatSeq}
124134
promotedPrompt={promotedPrompt}
125135
isFullscreen={fullscreen}
126136
onToggleFullscreen={toggleFullscreen}

apps/webapp/app/components/dashboard-agent/DashboardAgentHeader.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import { ShortcutKey } from "~/components/primitives/ShortcutKey";
88
import type { Shortcut } from "~/hooks/useShortcutKeys";
99
import { DashboardAgentHistoryMenu, type DashboardAgentChat } from "./DashboardAgentHistory";
1010

11-
/** New chat. Sits next to the panel's own ⌘J so the pair is easy to remember. */
11+
/**
12+
* New chat — the same ⌘J that opens the panel: contextual, registered ONCE in
13+
* `DashboardAgent` (closed → open, open → new chat). This constant is display
14+
* only; nothing else may register it or the keystroke would fire twice.
15+
*/
1216
export const NEW_CHAT_SHORTCUT: Shortcut = {
13-
modifiers: ["mod", "shift"],
17+
modifiers: ["mod"],
1418
key: "j",
15-
// The composer holds focus while the panel is open, so a shortcut that only
16-
// fires outside inputs would never fire at all.
1719
enabledOnInputElements: true,
1820
};
1921

@@ -104,8 +106,12 @@ export function DashboardAgentHeader({
104106
variant="minimal/small"
105107
className="aspect-square h-6 p-1"
106108
aria-label="New chat"
107-
tooltip="New chat"
108-
shortcut={NEW_CHAT_SHORTCUT}
109+
tooltip={
110+
<span className="flex items-center">
111+
New chat
112+
<ShortcutKey shortcut={NEW_CHAT_SHORTCUT} variant="medium" />
113+
</span>
114+
}
109115
onClick={onNewChat}
110116
LeadingIcon={<PlusIcon className="size-4 text-text-dimmed" />}
111117
/>

apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ type ActiveChat = {
8181
export function DashboardAgentPanel({
8282
onClose,
8383
requestedMessage,
84+
newChatSeq,
8485
promotedPrompt,
8586
isFullscreen = false,
8687
onToggleFullscreen,
@@ -93,6 +94,9 @@ export function DashboardAgentPanel({
9394
// Text handed to the panel from outside (`openWith`). `seq` distinguishes
9495
// repeat requests with the same text.
9596
requestedMessage?: { text: string; seq: number };
97+
// Bumped by the contextual ⌘J while the panel is open — each change starts a
98+
// new chat.
99+
newChatSeq?: number;
96100
// The product-controlled promoted prompt chip, from the feature flag.
97101
promotedPrompt?: SuggestedPrompt;
98102
}) {
@@ -355,6 +359,15 @@ export function DashboardAgentPanel({
355359
[openChat]
356360
);
357361

362+
// Contextual ⌘J: each bump while the panel is open starts a new chat. A ref
363+
// skips the mount-time value so opening the panel never resets a restored chat.
364+
const seenNewChatSeq = useRef(newChatSeq ?? 0);
365+
useEffect(() => {
366+
if (newChatSeq === undefined || newChatSeq === seenNewChatSeq.current) return;
367+
seenNewChatSeq.current = newChatSeq;
368+
newChat();
369+
}, [newChatSeq, newChat]);
370+
358371
const deleteChat = useCallback(
359372
async (id: string) => {
360373
const body = new FormData();

apps/webapp/app/components/dashboard-agent/chat-layout.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* card (diagnosis, investigation, report, chart), a
2323
* callout, a chip row
2424
* - `ChatProgress` — a spinner and one line of progress
25-
* - `ChatPendingTool` — a tool call still in flight, as a compact pill
25+
* - `ChatPendingTool` — a tool call still in flight: a bare spinner line
2626
* - `ChatToolRow` — a tool-call row, optionally with progress under it
2727
* - `ChatNote` — an inline system / interceptor note
2828
* - `ChatStatusLine` — an icon and one line of status
@@ -221,7 +221,7 @@ export function ChatProgress({ children }: { children: React.ReactNode }) {
221221
}
222222

223223
/**
224-
* A tool call still in flight, as a compact pill: a spinner and one short phrase
224+
* A tool call still in flight: a spinner and one short phrase
225225
* saying what the agent is doing.
226226
*
227227
* It replaces the tool row for the whole in-flight phase, so the transcript never
@@ -232,17 +232,12 @@ export function ChatProgress({ children }: { children: React.ReactNode }) {
232232
*/
233233
export function ChatPendingTool({ label }: { label: string }) {
234234
const insetClass = useInsetClass();
235+
// A bare progress line, not a pill: bordered chips read as artifacts that
236+
// stay, while in-flight work is transient — the same register as ChatProgress.
235237
return (
236-
<div className={cn(insetClass, "flex min-w-0")}>
237-
<span
238-
className={cn(
239-
"inline-flex h-6 min-w-0 items-center rounded-full border border-border-bright bg-background-bright px-2.5 text-xs text-text-dimmed",
240-
CHIP_GAP
241-
)}
242-
>
243-
<Spinner className="size-3 shrink-0" />
244-
<span className="truncate">{label}</span>
245-
</span>
238+
<div className={cn(insetClass, "flex min-w-0 items-center text-xs text-text-dimmed", CHIP_GAP)}>
239+
<Spinner className="size-3 shrink-0" />
240+
<span className="truncate">{label}</span>
246241
</div>
247242
);
248243
}

internal-packages/dashboard-agent/src/dashboard-agent.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ describe("buildDashboardAgentTools", () => {
628628
await expect(
629629
callTool("navigate_to", { destination: { kind: "error", fingerprint: "error_1" } })
630630
).resolves.toEqual({
631-
intent: { kind: "navigate", target: "trigger://proj_abc/env_abc/error/error_1" },
631+
intent: { kind: "navigate", target: "trigger://proj_abc/env_abc/error/1" },
632632
});
633633

634634
// A queue name's `/` is percent-encoded, so a task queue round-trips.
@@ -809,9 +809,9 @@ describe("buildDashboardAgentTools", () => {
809809
expect(output.error).toBeUndefined();
810810
const investigation = output.blocks[0].investigation;
811811
expect(investigation.hypotheses[0].evidence.map((e: { uri: string }) => e.uri)).toEqual([
812-
"trigger://proj_abc/env_abc/error/error_c4b4a797397a9c43",
812+
"trigger://proj_abc/env_abc/error/c4b4a797397a9c43",
813813
"trigger://proj_abc/env_abc/deployment/20260726.4",
814-
"trigger://proj_abc/env_abc/error/error_c4b4a797397a9c43",
814+
"trigger://proj_abc/env_abc/error/c4b4a797397a9c43",
815815
]);
816816
expect(investigation.evidence.map((e: { uri: string }) => e.uri)).toEqual([
817817
"trigger://proj_abc/env_abc/run/run_abc123",
@@ -987,7 +987,7 @@ describe("buildDashboardAgentTools", () => {
987987
// The follow-up that navigates points at the canonical error URI.
988988
expect(actions[1].intent).toEqual({
989989
kind: "navigate",
990-
target: "trigger://proj_abc/env_abc/error/error_c4b4a797397a9c43",
990+
target: "trigger://proj_abc/env_abc/error/c4b4a797397a9c43",
991991
});
992992
});
993993

internal-packages/dashboard-agent/src/tools.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -783,7 +783,10 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe
783783
parsed = { ...base, kind: "run", runId: ref };
784784
break;
785785
case "error":
786-
parsed = { ...base, kind: "error", fingerprint: ref };
786+
// The errors API returns friendly ids ("error_<fingerprint>") but the
787+
// canonical URI — and the dashboard error page it resolves to — key on
788+
// the raw fingerprint. Same normalization the watch checks apply.
789+
parsed = { ...base, kind: "error", fingerprint: ref.replace(/^error_/, "") };
787790
break;
788791
case "queue":
789792
parsed = { ...base, kind: "queue", name: ref };
@@ -1440,7 +1443,12 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe
14401443
parsed = { kind: "run", ...scope, runId: destination.runId };
14411444
break;
14421445
case "error":
1443-
parsed = { kind: "error", ...scope, fingerprint: destination.fingerprint };
1446+
// Accept the API's friendly id ("error_<fp>") — the page keys on the raw one.
1447+
parsed = {
1448+
kind: "error",
1449+
...scope,
1450+
fingerprint: destination.fingerprint.replace(/^error_/, ""),
1451+
};
14441452
break;
14451453
case "queue":
14461454
parsed = { kind: "queue", ...scope, name: destination.name };

0 commit comments

Comments
 (0)