Skip to content

Commit a7dc17d

Browse files
committed
feat(app): show a background shell's output from the pill
1 parent bd097cd commit a7dc17d

11 files changed

Lines changed: 802 additions & 67 deletions

File tree

‎packages/app/component-tests/activity-chips.spec.ts‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { expect, story } from "../../stories/playwright/story"
22

33
story("shows how long each background item has been running, ticking while open", async ({ mount, page }) => {
4-
await page.clock.install({ time: new Date("2026-10-19T12:00:00Z") })
4+
// Paused, so the story's items start at exactly this time.
5+
await page.clock.install({ time: new Date("2026-10-19T11:59:59Z") })
6+
await page.clock.pauseAt(new Date("2026-10-19T12:00:00Z"))
57
await mount("session-activity-chips--subagents-and-shells")
68

79
await page.locator('[data-slot="session-activity-chip"]').click()
@@ -15,3 +17,29 @@ story("shows how long each background item has been running, ticking while open"
1517
await expect(elapsed).toHaveText(["13s", "1m 06s", "1h 02m", "5m 42s"])
1618
expect(await page.locator('[data-slot="session-activity-panel"]').evaluate((node) => node.clientWidth)).toBe(width)
1719
})
20+
21+
story("opens a background shell's output in the menu and returns focus on the way out", async ({ mount, page }) => {
22+
await mount("session-activity-chips--subagents-and-shells")
23+
24+
const chip = page.locator('[data-slot="session-activity-chip"]')
25+
const panel = page.locator('[data-slot="session-activity-panel"]')
26+
await chip.click()
27+
await panel.getByRole("menuitem", { name: /bun test --watch/ }).click()
28+
29+
// The story's shells print `$ <id>` and `Ready.`.
30+
await expect(panel.locator('[data-slot="session-activity-shell-output"]')).toHaveText(/\$ shl_test\s+Ready\./)
31+
await expect(panel.locator('[data-slot="session-activity-back"]')).toBeFocused()
32+
33+
await page.keyboard.press("Enter")
34+
await expect(panel.getByRole("menuitem", { name: /bun test --watch/ })).toBeFocused()
35+
36+
await panel.getByRole("menuitem", { name: /bun test --watch/ }).click()
37+
await expect(panel.locator('[data-slot="session-activity-back"]')).toBeFocused()
38+
await page.keyboard.press("Escape")
39+
await expect(panel).toBeHidden()
40+
await expect(chip).toBeFocused()
41+
42+
// Closing forgets the shell, so the menu opens on the list again.
43+
await chip.click()
44+
await expect(panel).toHaveAttribute("data-view", "list")
45+
})

‎packages/app/src/runtime/i18n/en.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,14 @@ export const dict = {
622622
"session.activity.subagents.other": "{{count}} subagents",
623623
"session.activity.shells.one": "{{count}} shell",
624624
"session.activity.shells.other": "{{count}} shells",
625+
"session.activity.shell.back": "Back to background work",
626+
"session.activity.shell.stop": "Stop",
627+
"session.activity.shell.stopping": "Stopping…",
628+
"session.activity.shell.exited": "Exited",
629+
"session.activity.shell.exitCode": "Exited with code {{code}}",
630+
"session.activity.shell.timeout": "Timed out",
631+
"session.activity.shell.killed": "Stopped",
632+
"session.activity.shell.empty": "No output yet",
625633
"session.activity.summary": "{{subagents}} · {{shells}}",
626634
"session.activity.section.subagents": "Subagents",
627635
"session.activity.section.shells": "Shells",

‎packages/app/src/session/composer/activity-chips.css‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,111 @@
142142
}
143143
}
144144

145+
/* One shell: a wider panel with its header row over the tail of its output. Switching views changes size at once. */
146+
&[data-view="shell"] {
147+
display: flex;
148+
flex-direction: column;
149+
width: min(560px, calc(100vw - 56px));
150+
max-width: none;
151+
max-height: min(320px, var(--kb-popper-content-available-height, 320px));
152+
overflow: hidden;
153+
}
154+
155+
[data-slot="session-activity-shell-header"] {
156+
display: flex;
157+
flex: none;
158+
align-items: center;
159+
gap: 8px;
160+
min-height: 28px;
161+
padding-inline-end: 2px;
162+
163+
[data-component="menu-v2-item"],
164+
[data-slot="menu-v2-item-content"] {
165+
display: flex;
166+
flex: none;
167+
}
168+
169+
[data-component="menu-v2-item"] {
170+
padding-inline: 6px;
171+
}
172+
173+
/* Matches the list's glyph column, so the arrow sits where the row icons were. */
174+
[data-slot="session-activity-back"] {
175+
width: 28px;
176+
justify-content: center;
177+
--menu-v2-fg: var(--v2-icon-icon-muted);
178+
179+
[data-slot="menu-v2-item-content"] {
180+
flex: none;
181+
}
182+
183+
svg:dir(rtl) {
184+
transform: scaleX(-1);
185+
}
186+
}
187+
188+
[data-slot="session-activity-shell-stop"][aria-disabled="true"] {
189+
--menu-v2-fg: var(--v2-text-text-muted);
190+
}
191+
}
192+
193+
[data-slot="session-activity-shell-command"] {
194+
font-family: var(--v2-font-family-code);
195+
font-size: 13px;
196+
font-weight: 440;
197+
line-height: var(--line-height-compact);
198+
letter-spacing: -0.04px;
199+
color: var(--v2-text-text-base);
200+
text-align: left;
201+
}
202+
203+
[data-slot="session-activity-shell-status"] {
204+
flex: none;
205+
padding-inline: 6px;
206+
font-size: 13px;
207+
line-height: var(--line-height-compact);
208+
white-space: nowrap;
209+
color: var(--v2-text-text-muted);
210+
211+
&[data-failed] {
212+
color: var(--v2-state-fg-danger);
213+
}
214+
}
215+
216+
[data-slot="session-activity-shell-output"] {
217+
min-height: 0;
218+
flex: 1 1 auto;
219+
overflow-x: hidden;
220+
overflow-y: auto;
221+
margin-top: 4px;
222+
padding: 8px 10px;
223+
border-radius: 6px;
224+
background: var(--v2-background-bg-base);
225+
226+
pre {
227+
margin: 0;
228+
text-align: left;
229+
}
230+
231+
code {
232+
display: block;
233+
font-family: var(--v2-font-family-code);
234+
font-size: 13px;
235+
font-weight: 440;
236+
line-height: var(--line-height-compact);
237+
letter-spacing: -0.04px;
238+
color: var(--v2-text-text-base);
239+
white-space: pre-wrap;
240+
overflow-wrap: anywhere;
241+
}
242+
}
243+
244+
[data-slot="session-activity-shell-empty"] {
245+
font-size: 13px;
246+
line-height: var(--line-height-compact);
247+
color: var(--v2-text-text-muted);
248+
}
249+
145250
:root[data-motion="reduce"] & {
146251
animation: none;
147252
}

‎packages/app/src/session/composer/activity-chips.stories.tsx‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ type PreviewArgs = { stage: number; subagents: number; shells: number }
6464
// Driven by args, so moving between these stories updates the same capsule and its transitions play.
6565
function Preview(props: PreviewArgs) {
6666
const value = createMemo(() => liveDocument(stages[props.stage] ?? stages[0]))
67+
// Built once per count, so each item keeps the start time it had when it appeared.
68+
const activity = createMemo(() => ({
69+
subagents: Array.from({ length: props.subagents }, (_, index) => subagentItem(index)),
70+
shells: Array.from({ length: props.shells }, (_, index) => shellItem(index)),
71+
}))
6772

6873
onMount(() => pin("auto"))
6974
createEffect(on(value, () => pin("smooth"), { defer: true }))
@@ -74,10 +79,7 @@ function Preview(props: PreviewArgs) {
7479
description="opencode · activity chips"
7580
document={value()}
7681
timelineDetail={timelineDetail}
77-
activity={{
78-
subagents: Array.from({ length: props.subagents }, (_, index) => subagentItem(index)),
79-
shells: Array.from({ length: props.shells }, (_, index) => shellItem(index)),
80-
}}
82+
activity={activity()}
8183
/>
8284
)
8385
}

0 commit comments

Comments
 (0)