Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,15 @@ already in use` and no way forward short of finding the file in a temporary

### Desktop / Web UI

- **Dialogs no longer close when you click beside them, and every one has a
close button** — a stray click anywhere in the dimmed area around a dialog
dismissed it instantly, discarding whatever it held: a half-filled sandbox
form, a provider being connected, a folder being picked. Clicking outside now
does nothing. In exchange every dialog carries an × in its top-right corner —
the several that had been hiding theirs included — so there is always a
visible way out; Escape still works everywhere it did. The one exception is
the provider setup screen the app shows when no provider is configured, which
has never been dismissable.
- **The Work tabs stay out of the way until you use sandboxes** — every desktop
install showed a **Code** / **Work** switch above the projects list, even
though Work was empty for anyone who had never made a sandbox: switching to it
Expand Down
3 changes: 3 additions & 0 deletions DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,9 @@ from the squash message first, or use a plain merge.
| Legacy prompt input removed: deleted `packages/app/src/components/prompt-input.tsx` and its now-orphaned submodules `packages/app/src/components/prompt-input/{attachments,files,paste,transient-state}.ts`, `{context-items,drag-overlay,image-attachments,slash-popover}.tsx`, `image-attachments.css`, `attachments.test.ts`, `packages/app/test-browser/{prompt-attachments,prompt-transient-state,prompt-at-options}.test.ts` (plus the fork-only `at-options{,.test}.ts`); edits in `packages/app/src/pages/session.tsx`, `packages/app/src/pages/session/composer/{session-composer-region-controller.ts,todo-panel-motion.stories.tsx}` | The fork hardcodes `newLayoutDesigns` to `true` (see the settings.tsx row), so the legacy `PromptInput` — the `fallback` of session.tsx's `<Show when={newSessionDesign()}>` — could never render. session.tsx now creates the V2 controller unconditionally and always mounts `PromptInputV2Composer` (no `Show`); `session-composer-region-controller.ts` imports `PromptInputState` from `prompt-input/contracts` (where it is defined) instead of the deleted barrel; the todo-panel-motion story fills its composer-region `promptInput` slot with a static styled placeholder instead of the legacy component (its unused `controls` mock removed). Kept: `prompt-input/{contracts,history,history-store,placeholder,submit,editor-dom,build-request-parts,submission-state}.ts` and their tests — the V2 composer path still uses them. | Dead code: retired with the legacy layout, kept compiling for months, and PR #56-style fixes had to be applied to it for nothing. | Keep deleted — `git rm` the upstream side of modify/delete conflicts on these files, and delete any new module upstream adds under `prompt-input/` that only the legacy `PromptInput` imports. Take upstream changes to the kept shared modules. If upstream restructures session.tsx's composer block, re-apply "always render the V2 composer" and keep the legacy fallback out; if upstream rewires the story's input slot, keep our placeholder. If the fork ever wants the legacy layout back, restore from upstream, not from history. |

| Work side is opt-in: `packages/app/src/context/settings.tsx`, `packages/app/src/components/settings-v2/general.tsx`, `packages/app/src/i18n/{en,es}.ts` | The Work side and the Code/Work tab strip render only when `workSideAvailable()` (fork-only `packages/app/src/workspace/side.ts`) says so: sandbox-capable platform AND (a sandbox exists OR the new `general.showWork` preference is on). settings.tsx gains `showWork` (default `false`) with its accessor/setter beside `showCustomAgents`; settings-v2's Advanced section gains a "Show Work" switch, wrapped in `<Show when={platform.sandboxes}>` so non-sandbox builds never offer it; i18n gains `settings.general.row.showWork.{title,description}`. The fork-only wiring is in `workspace/tabs.tsx`: a `useWorkAvailable()` hook feeds both `useWorkspaceSide()`'s `effectiveSide` call and `WorkspaceTabs`'s `<Show>` (which previously gated on `platform.sandboxes`), so the side and its strip can never disagree — a hidden strip with Work selected would strand the user on sandbox servers with no way back. An undefined sandbox list means "not loaded", not "none", and keeps Work (the same race the Code/Work split row calls out); the main process answers `sandbox-get-state` synchronously, so that window is sub-frame. Tests: `packages/app/test-browser/workspace-side.test.ts` covers the four `workSideAvailable` branches. | Sandboxes are a heavyweight opt-in feature (a VM per sandbox), but every desktop profile paid for them in the sidebar: a Code/Work strip with a Work side that had nothing in it and offered to build one. Hiding both until a sandbox exists makes the strip appear exactly when there are two sides worth picking between; the preference is the discoverable way in for someone who wants Work before creating anything, and Settings → Servers → + → Add sandbox is the other. Owning a sandbox shows Work whatever the preference says. | Keep the `workSideAvailable` gate on both `useWorkspaceSide` and `WorkspaceTabs` — gating only one reintroduces the stranded-side bug. Take upstream's surrounding settings/i18n changes. If upstream adds its own sandbox-visibility preference, prefer theirs and drop `showWork`. Never collapse on an unloaded sandbox list. |
| `packages/ui/src/context/dialog.tsx` (again) | The `Kobalte.Overlay` lost its `onClick`, so a backdrop click no longer closes the top dialog. Escape (the window `keydown` listener) and `onOpenChange` (the close button) are untouched, as is the `persistent` guard on both. | A stray click in the dimmed area around a dialog dismissed it and discarded whatever it held — a half-filled sandbox form, a provider mid-connect. Dismissal is now always deliberate: the corner close button or Escape. | Keep the overlay handler-less. If upstream adds behaviour to the overlay, take it minus the close. Pairs with the `onInteractOutside` guards on the three dialog contents below — removing the handler alone is not enough, Kobalte's own dismissable layer closes on outside pointerdown too. |
| Dialogs close on a button, never on an outside click: `packages/ui/src/components/dialog.tsx`, `packages/ui/src/components/dialog.css`, `packages/ui/src/v2/components/dialog-v2.tsx`, `packages/ui/src/v2/components/dialog-v2.css`, `packages/ui/src/components/image-preview.tsx`, plus the `*.stories.tsx` beside the two dialogs | All three `Kobalte.Content`s take `onInteractOutside={(e) => e.preventDefault()}`, which is what actually blocks Kobalte's dismissable layer (the context row above only removes the explicit overlay click). Both `Dialog`s gained `closeLabel?`/`hideClose?` and now render the `Kobalte.CloseButton` themselves, as the last child of their content, absolutely positioned at the content's top-right — v1 at 16px/20px (12px/12px when `data-no-header`), v2 at 14px/14px, both at `z-index: 30` so they clear the settings tab header's sticky `z-index: 10`. The button was removed from v2's `DialogHeader` (which loses its `hideClose`/`closeLabel` props and its `data-hide-close` attribute) and from v1's header `Switch`, whose `action` branch used to *replace* it; `action` now simply renders. Headers reserve the corner in `padding-right` (v1 52px, v2 42px) and v1's title takes `flex: 1`; both containers take a `min-height` (v1 52px, v2 48px) so a short `fit` dialog cannot clip the button. `aria-label` defaults to `ui.common.close` through `useI18n` — the first v2 component to use the UI i18n context, which has an English fallback. | Every dialog needed a visible way out once the backdrop stopped being one, including the dozen whose call sites passed `hideClose`. Pinning the button to the content corner rather than laying it out in the header is what lets header-less dialogs (settings, release notes, the command palettes) have one at all; keeping it inside `Kobalte.Content` keeps it inside the focus trap and the modal's pointer-events layer. | Keep the corner button and the `onInteractOutside` guards. Upstream owns the header/body/footer layout — take its changes and re-apply the `padding-right` reserve and the close button on top. If upstream adds its own close button to `DialogHeader`, drop ours from `Dialog` rather than shipping two. |
| Dialog close buttons at the call sites: `packages/app/src/sandbox/{dialog-add,dialog-add-default,dialog-delete,dialog-mounts}.tsx`, `packages/app/src/wsl/{dialog-add-server,dialog-delete}.tsx`, `packages/app/src/components/{dialog-manage-models,dialog-connect-provider,dialog-select-model-unpaid-v2,session-actions-menu,settings-v2/dialog-server-v2}.tsx`, `packages/app/src/components/gateway/dialog-configure.tsx`, `packages/app/src/components/dialog-select-file.tsx`, `packages/app/src/components/dialog-command-palette-v2.css` | Dropped every `hideClose`/`closeLabel` prop from `DialogHeader` (the button moved to `Dialog`, and hiding it is no longer wanted anywhere it was hidden). `dialog-configure` passes `hideClose` on its `Dialog` instead — it is the configure gate, which the dialog stack also marks `persistent`, so a close button there would be inert. The two search-first dialogs reserve the corner the button now occupies: `dialog-select-file` via `[&_[data-slot=list-search-wrapper]]:pr-10`, the v2 command palette via `padding-right` on `.command-palette-v2-search`. | Fork-only sandbox/WSL/gateway dialogs aside, these are upstream files whose only change is a removed prop. | Take upstream's version of these call sites and re-drop `hideClose`. The `hideClose` on `dialog-configure` is fork-only and must stay. |

## Known rebrand gaps

Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/components/dialog-command-palette-v2.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@

.command-palette-v2-search {
flex-shrink: 0;
padding: 6px;
/* Right padding keeps the search field clear of the dialog's corner close
button, which floats over the content at 14px from the top and right. */
padding: 6px 40px 6px 6px;
}

.command-palette-v2-search [data-component="text-input-v2"] {
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/dialog-connect-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export const DialogConnectProvider: Component<{
containerClass="!h-[min(calc(100vh_-_16px),512px)] !w-[min(calc(100vw_-_16px),640px)]"
class="[font-family:var(--v2-font-family-sans)] [&_[data-slot=dialog-header]]:!px-5 [&_[data-slot=dialog-header-title]]:!text-[15px] [&_[data-slot=dialog-header-title]]:!tracking-[-0.13px]"
>
<DialogHeader closeLabel={language.t("common.close")}>
<DialogHeader>
<Show
when={controller.selected()}
fallback={<DialogTitle>{language.t("command.provider.connect")}</DialogTitle>}
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/dialog-manage-models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export const DialogManageModelsV2: Component = () => {

return (
<DialogV2 size="large" variant="settings" class="settings-v2-manage-models-dialog">
<DialogHeader hideClose={true} closeLabel={language.t("common.close")}>
<DialogHeader>
<DialogTitleGroup
title={language.t("dialog.model.manage")}
description={language.t("dialog.model.manage.description")}
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/dialog-select-file.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function DialogSelectFileLegacy(props: { filesOnly: () => boolean; onOpenFile?:
}

return (
<Dialog class="pt-3 pb-0 !max-h-[480px]" transition>
<Dialog class="pt-3 pb-0 !max-h-[480px] [&_[data-slot=list-search-wrapper]]:pr-10" transition>
<List
class="px-3"
search={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
containerClass="!h-auto max-h-[calc(100vh_-_16px)] !w-[min(calc(100vw_-_16px),640px)]"
class="[font-family:var(--v2-font-family-sans)] [&_[data-slot=dialog-header]]:!px-5 [&_[data-slot=dialog-header-title]]:!text-[15px] [&_[data-slot=dialog-header-title]]:!tracking-[-0.13px]"
>
<DialogHeader closeLabel={language.t("common.close")}>
<DialogHeader>
<DialogTitle>{language.t("dialog.model.select.title")}</DialogTitle>
</DialogHeader>
<DialogBody class="max-h-[calc(100vh_-_68px)] min-h-0 flex-none gap-0 overflow-y-auto px-2 pb-2">
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/gateway/dialog-configure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export function DialogConfigure() {
}

return (
<Dialog class="h-full" transition>
<Dialog class="h-full" transition hideClose>
<div class="flex flex-col gap-6 px-5 py-4 w-[440px] max-w-full">
<Switch>
<Match when={step().kind === "menu"}>
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/session-actions-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export function DialogDeleteSession(props: { title: string; onConfirm: () => unk
if (settings.general.newLayoutDesigns())
return (
<DialogV2 fit>
<DialogHeader hideClose>
<DialogHeader>
<DialogTitleGroup
title={language.t("session.delete.title")}
description={language.t("session.delete.confirm", { name: props.title })}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const DialogServerV2: Component<{

return (
<Dialog fit class="settings-v2-server-dialog">
<DialogHeader hideClose={true}>
<DialogHeader>
<DialogTitle>{title()}</DialogTitle>
</DialogHeader>
<DividerV2 />
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/sandbox/dialog-add-default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function DialogAddDefaultSandbox(props: { onCreated?: (config: SandboxCon

return (
<Dialog fit>
<DialogHeader hideClose={true}>
<DialogHeader>
<DialogTitleGroup
title={language.t("sandbox.addDefault.title")}
description={language.t("sandbox.addDefault.intro")}
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/sandbox/dialog-add.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export function DialogAddSandbox(props: {

return (
<Dialog fit>
<DialogHeader hideClose={true}>
<DialogHeader>
{/* As a description under the title rather than a line in the body:
it explains the whole dialog, not the field it happens to sit
above, and DialogTitleGroup ties it to the dialog for screen
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/sandbox/dialog-delete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function DialogDeleteSandbox(props: { name: string; onConfirm: () => void

return (
<Dialog fit>
<DialogHeader hideClose={true}>
<DialogHeader>
<DialogTitle>{language.t("sandbox.delete.title")}</DialogTitle>
</DialogHeader>
<DividerV2 />
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/sandbox/dialog-mounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function DialogSandboxMounts(props: { id: string }) {
when={item()}
fallback={
<Dialog fit>
<DialogHeader hideClose={true}>
<DialogHeader>
<DialogTitle>{language.t("sandbox.mounts.title")}</DialogTitle>
</DialogHeader>
<DividerV2 />
Expand Down Expand Up @@ -137,7 +137,7 @@ function SandboxMountsEditor(props: { id: string }) {

return (
<Dialog fit>
<DialogHeader hideClose={true}>
<DialogHeader>
<DialogTitle>{language.t("sandbox.mounts.title")}</DialogTitle>
</DialogHeader>
<DividerV2 />
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/wsl/dialog-add-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
}
>
<Dialog fit class="settings-v2-wsl-dialog">
<DialogHeader hideClose={true}>
<DialogHeader>
<DialogTitle>
{controller.view() === "main" ? language.t("wsl.server.add") : language.t("wsl.onboarding.installDistro")}
</DialogTitle>
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/wsl/dialog-delete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function DialogDeleteWslServer(props: { distro: string; onConfirm: () =>

return (
<Dialog fit>
<DialogHeader hideClose={true}>
<DialogHeader>
<DialogTitle>{language.t("wsl.delete.title")}</DialogTitle>
</DialogHeader>
<DividerV2 />
Expand Down
29 changes: 27 additions & 2 deletions packages/ui/src/components/dialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@
z-index: 50;
width: min(calc(100vw - 16px), 640px);
height: min(calc(100vh - 16px), 512px);
/* Keeps a fit dialog whose content is shorter than the corner close button
from cutting the button off. */
min-height: 52px;
display: flex;
flex-direction: column;
align-items: center;
justify-items: start;
overflow: visible;

[data-slot="dialog-content"] {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
Expand All @@ -54,13 +58,18 @@

[data-slot="dialog-header"] {
display: flex;
padding: 16px 20px;
/* Right padding clears the corner close button (20px inset + 24px
button + 8px gap) so a long title or an action never runs under it. */
padding: 16px 52px 16px 20px;
gap: 8px;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
align-self: stretch;

[data-slot="dialog-title"] {
flex: 1;
min-width: 0;
color: var(--text-strong);

/* text-16-medium */
Expand All @@ -71,7 +80,23 @@
line-height: var(--line-height-x-large); /* 150% */
letter-spacing: var(--letter-spacing-tight);
}
/* [data-slot="dialog-close-button"] {} */
}

/* Same coordinates the header used to lay this button out at, so headed
dialogs look unchanged while header-less ones gain the button. Above
the settings tab header, which is sticky at z-index 10. */
> [data-slot="dialog-close-button"] {
position: absolute;
top: 16px;
right: 20px;
z-index: 30;
}

/* Nothing above the body to reserve room, so the button floats over the
content and sits a little tighter into the corner. */
&[data-no-header] > [data-slot="dialog-close-button"] {
top: 12px;
right: 12px;
}

[data-slot="dialog-description"] {
Expand Down
25 changes: 25 additions & 0 deletions packages/ui/src/components/dialog.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ Provide concise title/description and keep body focused.
- Optional: \`title\`, \`description\`, \`action\`.
- \`size\`: normal | large | x-large.
- \`fit\` and \`transition\` control layout and animation.
- \`closeLabel\` and \`hideClose\` control the corner close button.

### Variants and states
- Sizes and optional header/action controls.

### Behavior
- Intended to be rendered via \`useDialog().show\`.
- Dismissal is the corner close button or Escape; a click outside never closes it.

### Accessibility
- TODO: confirm focus trapping and aria attributes from Kobalte Dialog.
Expand Down Expand Up @@ -62,6 +64,29 @@ export const Basic = {
},
}

export const NoHeader = {
render: () => {
const dialog = useDialog()
const open = () =>
dialog.show(() => (
<mod.Dialog>
<div style={{ padding: "40px" }}>
A dialog with no header still gets the corner close button. Header-less dialogs lay their own content out,
so they leave the corner clear the way this one does.
</div>
</mod.Dialog>
))

onMount(open)

return (
<Button variant="secondary" onClick={open}>
Open dialog
</Button>
)
},
}

export const Sizes = {
render: () => {
const dialog = useDialog()
Expand Down
Loading
Loading