Skip to content

Commit d9af413

Browse files
authored
Improve prompt layout for narrow session panes (#437)
## Summary - Add measured `narrow` / `medium` / `wide` width steps to the session center column so chat UI can respond to pane width rather than viewport width. - Rework prompt composer controls: clear/expand move into the text field, narrow panes place auxiliary controls below the textarea, and stop/send align bottom-right. - Reduce branded empty-state logo/title size in narrow session panes and remove the prompt `@ files/agents` overlay hint. ## Validation - `npm run typecheck --workspace @codenomad/ui`
1 parent ca076f4 commit d9af413

6 files changed

Lines changed: 157 additions & 42 deletions

File tree

packages/ui/src/components/branded-empty-state.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ const BrandedEmptyState: Component<BrandedEmptyStateProps> = (props) => {
1717
<div class={`empty-state ${props.class ?? ""}`.trim()}>
1818
<div class="empty-state-content">
1919
<div class="flex flex-col items-center gap-3 mb-6">
20-
<img src={codeNomadLogo} alt={t("messageSection.empty.logoAlt")} class="h-48 w-auto" loading="lazy" />
21-
<h1 class="text-3xl font-semibold text-primary">{t("messageSection.empty.brandTitle")}</h1>
20+
<img src={codeNomadLogo} alt={t("messageSection.empty.logoAlt")} class="empty-state-logo h-48 w-auto" loading="lazy" />
21+
<h1 class="empty-state-brand-title text-3xl font-semibold text-primary">{t("messageSection.empty.brandTitle")}</h1>
2222
</div>
2323
{props.title ? <h3>{props.title}</h3> : null}
2424
<p>{props.description}</p>

packages/ui/src/components/instance/instance-shell2.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ import { isPermissionAutoAcceptEnabled } from "../../stores/permission-auto-acce
7070
const log = getLogger("session")
7171
const OPEN_SESSION_SEARCH_EVENT = "codenomad:open-session-search"
7272
const NO_SESSION_DRAFT_SESSION_ID = "__no_session_draft__"
73+
type SessionCenterWidthStep = "narrow" | "medium" | "wide"
74+
75+
function getSessionCenterWidthStep(width: number): SessionCenterWidthStep {
76+
if (width < 768) return "narrow"
77+
if (width < 1280) return "medium"
78+
return "wide"
79+
}
7380

7481
interface InstanceShellProps {
7582
instance: Instance
@@ -105,6 +112,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
105112
const [rightDrawerContentEl, setRightDrawerContentEl] = createSignal<HTMLElement | null>(null)
106113
const [leftToggleButtonEl, setLeftToggleButtonEl] = createSignal<HTMLElement | null>(null)
107114
const [rightToggleButtonEl, setRightToggleButtonEl] = createSignal<HTMLElement | null>(null)
115+
const [sessionCenterEl, setSessionCenterEl] = createSignal<HTMLElement | null>(null)
116+
const [sessionCenterWidthStep, setSessionCenterWidthStep] = createSignal<SessionCenterWidthStep>("wide")
108117

109118
const [selectedBackgroundProcess, setSelectedBackgroundProcess] = createSignal<BackgroundProcess | null>(null)
110119
const [showBackgroundOutput, setShowBackgroundOutput] = createSignal(false)
@@ -253,6 +262,25 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
253262
onCleanup(() => window.clearInterval(timer))
254263
})
255264

265+
createEffect(() => {
266+
const element = sessionCenterEl()
267+
if (!element || typeof ResizeObserver === "undefined") return
268+
269+
const updateWidthStep = (width: number) => {
270+
setSessionCenterWidthStep(getSessionCenterWidthStep(width))
271+
}
272+
273+
updateWidthStep(element.getBoundingClientRect().width)
274+
275+
const observer = new ResizeObserver((entries) => {
276+
const width = entries[0]?.contentRect.width ?? element.getBoundingClientRect().width
277+
updateWidthStep(width)
278+
})
279+
observer.observe(element)
280+
281+
onCleanup(() => observer.disconnect())
282+
})
283+
256284
const connectionStatus = () => sseManager.getStatus(props.instance.id)
257285
const connectionStatusClass = () => {
258286
const status = connectionStatus()
@@ -801,7 +829,12 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
801829
>
802830
{renderLeftPanel()}
803831

804-
<Box sx={{ display: "flex", flexDirection: "column", flex: 1, minWidth: 0, minHeight: 0, overflowX: "hidden" }}>
832+
<Box
833+
class="session-center-column"
834+
ref={setSessionCenterEl}
835+
data-session-center-width={sessionCenterWidthStep()}
836+
sx={{ display: "flex", flexDirection: "column", flex: 1, minWidth: 0, minHeight: 0, overflowX: "hidden" }}
837+
>
805838
<Show when={!mobileFullscreen()}>
806839
<AppBar position="sticky" color="default" elevation={0} class="border-b border-base">
807840
<Toolbar variant="dense" class="session-toolbar flex flex-wrap items-center gap-2 py-0 min-h-[40px]">

packages/ui/src/components/prompt-input.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ export default function PromptInput(props: PromptInputProps) {
630630
</Suspense>
631631
</Show>
632632

633-
<div class="flex flex-1 flex-col">
633+
<div class="prompt-input-main flex flex-1 flex-col">
634634
<div class={`prompt-input-field-container ${expandState() === "expanded" ? "is-expanded" : ""}`}>
635635

636636
<div class={`prompt-input-field ${expandState() === "expanded" ? "is-expanded" : ""}`}>
@@ -652,6 +652,22 @@ export default function PromptInput(props: PromptInputProps) {
652652
autoCapitalize="off"
653653
autocomplete="off"
654654
/>
655+
<button
656+
type="button"
657+
class="prompt-clear-button prompt-clear-button-inline"
658+
onClick={handleClearPrompt}
659+
disabled={!canClearPrompt()}
660+
aria-label={t("promptInput.clear.ariaLabel")}
661+
title={t("promptInput.clear.title")}
662+
>
663+
<X class="h-4 w-4" aria-hidden="true" />
664+
</button>
665+
<div class="prompt-expand-button-inline">
666+
<ExpandButton
667+
expandState={expandState}
668+
onToggleExpand={handleExpandToggle}
669+
/>
670+
</div>
655671
<Show when={shouldShowOverlay()}>
656672
<div class={`prompt-input-overlay keyboard-hints ${mode() === "shell" ? "shell-mode" : ""}`}>
657673
<Show
@@ -671,7 +687,7 @@ export default function PromptInput(props: PromptInputProps) {
671687
<Kbd>Enter</Kbd> {t("promptInput.overlay.send")}<Kbd shortcut="cmd+enter" /> {t("promptInput.overlay.newLine")}
672688
</>
673689
</Show>
674-
{" "}<Kbd>@</Kbd> {t("promptInput.overlay.filesAgents")}<Kbd>↑↓</Kbd> {t("promptInput.overlay.history")}
690+
{" "}<Kbd>↑↓</Kbd> {t("promptInput.overlay.history")}
675691
</span>
676692
<Show when={attachments().length > 0}>
677693
<span class="prompt-overlay-text prompt-overlay-muted">{t("promptInput.overlay.attachments", { count: attachments().length })}</span>
@@ -782,22 +798,8 @@ export default function PromptInput(props: PromptInputProps) {
782798
>
783799
<Paperclip class="h-4 w-4" aria-hidden="true" />
784800
</button>
785-
<button
786-
type="button"
787-
class="prompt-clear-button"
788-
onClick={handleClearPrompt}
789-
disabled={!canClearPrompt()}
790-
aria-label={t("promptInput.clear.ariaLabel")}
791-
title={t("promptInput.clear.title")}
792-
>
793-
<X class="h-4 w-4" aria-hidden="true" />
794-
</button>
795801
</div>
796802
<div class="prompt-nav-column prompt-nav-column-right">
797-
<ExpandButton
798-
expandState={expandState}
799-
onToggleExpand={handleExpandToggle}
800-
/>
801803
<Show when={hasHistory()}>
802804
<button
803805
type="button"

packages/ui/src/styles/messaging/prompt-input.css

Lines changed: 79 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55
}
66

77
.prompt-input-wrapper {
8+
--prompt-input-compact-height: 104px;
89
@apply grid items-stretch;
910
grid-template-columns: minmax(0, 1fr) 72px 64px;
1011
gap: 0;
1112
padding: 0;
1213
}
1314

15+
.prompt-input-main {
16+
min-width: 0;
17+
}
18+
1419
.prompt-input-actions {
1520
@apply flex flex-col items-center;
1621
align-self: stretch;
@@ -47,7 +52,7 @@
4752
.prompt-input {
4853
@apply w-full pt-2.5 border text-sm resize-none outline-none transition-colors;
4954
padding-inline-start: 0.75rem;
50-
padding-inline-end: 0.75rem;
55+
padding-inline-end: 4.5rem;
5156
font-family: inherit;
5257
background-color: var(--surface-base);
5358
color: var(--text-primary);
@@ -153,6 +158,20 @@
153158
cursor: not-allowed;
154159
}
155160

161+
.prompt-clear-button-inline {
162+
position: absolute;
163+
top: 2.5rem;
164+
inset-inline-end: 0.5rem;
165+
z-index: 2;
166+
}
167+
168+
.prompt-expand-button-inline {
169+
position: absolute;
170+
top: 0.5rem;
171+
inset-inline-end: 0.5rem;
172+
z-index: 2;
173+
}
174+
156175
.prompt-overlay-text {
157176
display: inline-flex;
158177
align-items: center;
@@ -403,40 +422,77 @@
403422
}
404423
}
405424

406-
@media (max-width: 1279px) {
407-
:root {
408-
--prompt-input-compact-height: 104px;
409-
}
425+
.session-center-column[data-session-center-width="medium"] .prompt-input-wrapper,
426+
.session-center-column[data-session-center-width="narrow"] .prompt-input-wrapper {
427+
min-height: var(--prompt-input-compact-height);
428+
}
410429

411-
.prompt-input-wrapper {
412-
min-height: var(--prompt-input-compact-height);
413-
}
430+
.session-center-column[data-session-center-width="medium"] .prompt-input-field-container,
431+
.session-center-column[data-session-center-width="narrow"] .prompt-input-field-container {
432+
min-height: var(--prompt-input-compact-height);
433+
height: var(--prompt-input-compact-height);
434+
}
414435

415-
.prompt-input-field-container {
416-
min-height: var(--prompt-input-compact-height);
417-
height: var(--prompt-input-compact-height);
418-
}
436+
.session-center-column[data-session-center-width="medium"] .prompt-input-field,
437+
.session-center-column[data-session-center-width="narrow"] .prompt-input-field {
438+
height: var(--prompt-input-compact-height);
439+
}
419440

420-
.prompt-input-field {
421-
height: var(--prompt-input-compact-height);
422-
}
441+
.session-center-column[data-session-center-width="medium"] .prompt-input-field-container.is-expanded,
442+
.session-center-column[data-session-center-width="medium"] .prompt-input-field.is-expanded,
443+
.session-center-column[data-session-center-width="narrow"] .prompt-input-field-container.is-expanded,
444+
.session-center-column[data-session-center-width="narrow"] .prompt-input-field.is-expanded {
445+
height: auto;
446+
}
423447

424-
.prompt-input-field-container.is-expanded,
425-
.prompt-input-field.is-expanded {
426-
height: auto;
427-
}
448+
.session-center-column[data-session-center-width="narrow"] .prompt-input-wrapper {
449+
grid-template-columns: minmax(0, 1fr) auto;
450+
grid-template-areas:
451+
"field field"
452+
"actions primary";
428453
}
429454

430-
@media (max-width: 720px) {
431-
.prompt-input-wrapper {
432-
grid-template-columns: minmax(0, 1fr) 64px 40px;
433-
}
455+
.session-center-column[data-session-center-width="narrow"] .prompt-input-main {
456+
grid-area: field;
457+
}
458+
459+
.session-center-column[data-session-center-width="narrow"] .prompt-input-actions {
460+
grid-area: actions;
461+
flex-direction: row;
462+
align-items: center;
463+
justify-content: flex-start;
464+
height: auto;
465+
padding: 0.25rem 0.5rem 0.5rem;
466+
}
467+
468+
.session-center-column[data-session-center-width="narrow"] .prompt-input-primary-actions {
469+
grid-area: primary;
470+
flex-direction: row;
471+
align-items: center;
472+
justify-content: flex-end;
473+
width: auto;
474+
height: auto;
475+
padding: 0.25rem 0.5rem 0.5rem 0;
476+
border-inline-start: none;
477+
}
478+
479+
.session-center-column[data-session-center-width="narrow"] .prompt-nav-buttons {
480+
justify-content: flex-start;
481+
gap: 0.25rem;
482+
}
483+
484+
.session-center-column[data-session-center-width="narrow"] .prompt-nav-column {
485+
flex-direction: row;
486+
align-items: center;
487+
min-width: 0;
488+
gap: 0.25rem;
434489
}
435490

436491
@media (max-width: 640px) {
437492
.prompt-input {
438493
min-height: 0;
439494
padding: 0.5rem 0.75rem;
495+
padding-inline-end: 4.5rem;
440496
padding-bottom: 0.75rem;
441497
}
442498

packages/ui/src/styles/panels/empty-loading.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@
1515
@apply text-center max-w-sm;
1616
}
1717

18+
.session-center-column[data-session-center-width="narrow"] .empty-state-logo {
19+
height: 9rem;
20+
}
21+
22+
.session-center-column[data-session-center-width="narrow"] .empty-state-brand-title {
23+
font-size: 1.5rem;
24+
line-height: 2rem;
25+
}
26+
1827
.empty-state-content h3 {
1928
font-size: var(--font-size-xl);
2029
margin-bottom: 12px;

packages/ui/src/styles/panels/session-layout.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,21 @@
66
overflow: hidden;
77
}
88

9+
.session-center-column {
10+
container-name: session-center;
11+
container-type: inline-size;
12+
--session-center-width-step: wide;
13+
}
14+
15+
/* Canonical session-center width steps: narrow < 768px, medium < 1280px, wide >= 1280px. */
16+
.session-center-column[data-session-center-width="narrow"] {
17+
--session-center-width-step: narrow;
18+
}
19+
20+
.session-center-column[data-session-center-width="medium"] {
21+
--session-center-width-step: medium;
22+
}
23+
924
.session-list-container {
1025
@apply flex flex-col flex-1 min-h-0 relative;
1126
background-color: var(--surface-secondary);

0 commit comments

Comments
 (0)