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
2 changes: 2 additions & 0 deletions packages/app/src/components/session-context-usage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
return (
<Show when={params.id}>
<TooltipV2 value={tooltipValue()} placement={props.placement ?? "top"} shift={-8}>
<span class="flex shrink-0" data-tour-target="context-ring">
<Switch>
<Match when={variant() === "indicator"}>{circle()}</Match>
<Match when={buttonAppearance() === "v2"}>
Expand All @@ -155,6 +156,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
</Button>
</Match>
</Switch>
</span>
</TooltipV2>
</Show>
)
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/components/session/panel-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function PanelMenu(props: {
<DropdownMenu.Trigger
class="flex items-center justify-center w-7 h-7 rounded-md text-text-weak hover:text-text-base hover:bg-background-stronger transition-colors cursor-pointer"
aria-label="Open panel"
data-tour-target="panel-menu"
>
<Icon name="plus-small" />
</DropdownMenu.Trigger>
Expand Down
36 changes: 21 additions & 15 deletions packages/app/src/components/session/session-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -583,11 +583,15 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
<div class="flex items-center gap-2">

{/* amicode#274: Session Chats Dropdown — chat navigation from within a session */}
<SessionChatsDropdown currentSessionID={props.state.currentSessionID} />
<span class="flex shrink-0" data-tour-target="sessions">
<SessionChatsDropdown currentSessionID={props.state.currentSessionID} />
</span>
<Show when={!AMICODE_HIDE_STATUS_POPOVER}>
<TooltipV2 placement="bottom" value={props.state.statusLabel} class="shrink-0">
<StatusPopoverV2 healthDot={props.state.statusDotVisible} />
</TooltipV2>
<span class="flex shrink-0" data-tour-target="status">
<TooltipV2 placement="bottom" value={props.state.statusLabel} class="shrink-0">
<StatusPopoverV2 healthDot={props.state.statusDotVisible} />
</TooltipV2>
</span>
</Show>
<Show when={props.state.reviewVisible}>
<TooltipV2
Expand All @@ -602,17 +606,19 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
</>
}
>
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
class="!w-9 shrink-0"
state={props.state.reviewOpened ? "pressed" : undefined}
onClick={props.state.onReviewToggle}
aria-label={props.state.reviewLabel}
aria-controls="review-panel"
icon={<IconV2 name="sidebar-right" />}
/>
<span class="flex shrink-0" data-tour-target="side-panel">
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
class="!w-9 shrink-0"
state={props.state.reviewOpened ? "pressed" : undefined}
onClick={props.state.onReviewToggle}
aria-label={props.state.reviewLabel}
aria-controls="review-panel"
icon={<IconV2 name="sidebar-right" />}
/>
</span>
</TooltipV2>
</Show>
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/titlebar-tab-strip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ export function TitlebarTabStrip(props: {
})

return (
<div data-slot="titlebar-tabs" class="relative min-w-0">
<div data-slot="titlebar-tabs" data-tour-target="tabs" class="relative min-w-0">
<div
data-slot="titlebar-tabs-scroll"
class="flex min-w-0 flex-row items-center gap-1.5 overflow-x-auto no-scrollbar [app-region:no-drag]"
Expand Down
127 changes: 68 additions & 59 deletions packages/app/src/components/titlebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,18 +327,6 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl

tabs.newDraft({ server: fallback.server, directory: fallback.directory }, "")
}
const dialog = useDialog()
const [settingsOpen, setSettingsOpen] = createSignal(false)
const showSettings = () => {
const sessionID = params.id
void import("@/components/settings-v2").then((module) => {
setSettingsOpen(true)
void dialog.show(
() => <module.DialogSettings sessionID={sessionID} />,
() => setSettingsOpen(false),
)
})
}

command.register("titlebar-home", () => [])

Expand Down Expand Up @@ -390,34 +378,9 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
<Show when={windows() || linux()}>
<WindowsAppMenu command={command} platform={platform} variant="v2" />
</Show>
<TooltipV2
placement="bottom"
value={language.t("profile.title") || "Profile"}
class="shrink-0"
>
<ProfilePopoverTrigger />
</TooltipV2>
<TooltipV2
placement="bottom"
value={
<>
{language.t("command.settings.open")}
<KeybindV2 keys={command.keybindParts("settings.open")} variant="neutral" />
</>
}
class="shrink-0"
>
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
class="!w-9 shrink-0"
icon={<IconV2 name="settings-gear" />}
state={settingsOpen() ? "pressed" : undefined}
onClick={showSettings}
aria-label={language.t("command.settings.open")}
/>
</TooltipV2>
{/* Profile and Settings live at the trailing edge with the
other account/status controls (Sessions, Status, Side
Panel) — see TitlebarV2Right. */}
{/* Removed: sidebar-left toggle button (harmoniqs/amicode#265).
The button called layout.sidebar.toggle() but no component in
NewLayout observes that signal — WorkbenchPanel only mounts in
Expand All @@ -437,26 +400,28 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
onOverflowChange={setTabsAreOverflowing}
/>
<Show when={!(creating() && params.dir)}>
<TooltipV2
placement="bottom"
value={
<>
{language.t("command.session.new")}
<KeybindV2 keys={command.keybindParts("session.new")} variant="neutral" />
</>
}
class="shrink-0"
>
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
<span class="flex shrink-0" data-tour-target="new-chat">
<TooltipV2
placement="bottom"
value={
<>
{language.t("command.session.new")}
<KeybindV2 keys={command.keybindParts("session.new")} variant="neutral" />
</>
}
class="shrink-0"
icon={<IconV2 name="plus" />}
onClick={openNewTab}
aria-label={language.t("command.session.new")}
/>
</TooltipV2>
>
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
class="shrink-0"
icon={<IconV2 name="plus" />}
onClick={openNewTab}
aria-label={language.t("command.session.new")}
/>
</TooltipV2>
</span>
</Show>
<div class="flex-1" />
<TitlebarV2Right state={v2RightState()} />
Expand Down Expand Up @@ -614,12 +579,56 @@ type TitlebarV2RightState = {
}

function TitlebarV2Right(props: { state: TitlebarV2RightState }) {
const language = useLanguage()
const command = useCommand()
const dialog = useDialog()
const params = useParams()
const [settingsOpen, setSettingsOpen] = createSignal(false)
const showSettings = () => {
const sessionID = params.id
void import("@/components/settings-v2").then((module) => {
setSettingsOpen(true)
void dialog.show(
() => <module.DialogSettings sessionID={sessionID} />,
() => setSettingsOpen(false),
)
})
}
return (
<div class="relative z-20 flex shrink-0 items-center justify-end gap-0 overflow-visible">
<Show when={props.state.update.visible}>
<TitlebarUpdateIconButton state={props.state.update} />
</Show>
{/* Session-scoped controls (Sessions / Status / Side Panel) portal in here. */}
<div id="opencode-titlebar-right" class="flex shrink-0 items-center justify-end gap-0" />
<span class="flex shrink-0" data-tour-target="profile">
<TooltipV2 placement="bottom" value={language.t("profile.title") || "Profile"} class="shrink-0">
<ProfilePopoverTrigger />
</TooltipV2>
</span>
<span class="flex shrink-0" data-tour-target="settings">
<TooltipV2
placement="bottom"
value={
<>
{language.t("command.settings.open")}
<KeybindV2 keys={command.keybindParts("settings.open")} variant="neutral" />
</>
}
class="shrink-0"
>
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
class="!w-9 shrink-0"
icon={<IconV2 name="settings-gear" />}
state={settingsOpen() ? "pressed" : undefined}
onClick={showSettings}
aria-label={language.t("command.settings.open")}
/>
</TooltipV2>
</span>
</div>
)
}
Expand Down
82 changes: 82 additions & 0 deletions packages/app/src/design-polish.css
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,85 @@ span[data-component="tag"][data-variant="accent"] {
0%, 100% { opacity: 0.35; }
50% { opacity: 1; }
}

/* ── Onboarding walkthrough spotlight (session-tour.tsx) ───────────────────
A ring traced on the chrome element the current "Tour · X" stop names. The
ring is the whole marker — there is no label, because the stop's own text
already names the surface. Decorative only (aria-hidden, pointer-events
none); sits above the shell but below dialogs (10000+). */
[data-component="amc-tour-spotlight"] {
position: fixed;
inset: 0;
z-index: 9000;
pointer-events: none;
}
/* The scrim: everything except the stop's element and the walkthrough card is
softened, so the eye has one place to go. Blur is deliberately shallow (3px)
and paired with only a light dim — enough to push the rest of the window back
without hiding what the reader is being taught to recognise. The holes are
punched with an even-odd clip-path, recomputed as the ring moves. */
[data-component="amc-tour-spotlight"] .amc-tour-scrim {
position: absolute;
inset: 0;
backdrop-filter: blur(3px);
-webkit-backdrop-filter: blur(3px);
background: color-mix(in srgb, var(--v2-background-bg-base) 22%, transparent);
transition: opacity 0.2s ease;
}
[data-component="amc-tour-spotlight"] .amc-tour-ring {
position: absolute;
/* No glow — nothing else in this UI uses one, and the scrim already says
"look here". Just the accent edge, the way state is marked everywhere else.
No fill either: this sits ON TOP of a live control, and a wash would tint
the very thing the reader is being taught to recognise. */
border: 1px solid var(--accent);
border-radius: var(--radius-md);
/* Eases between elements rather than teleporting. */
transition: top 0.22s cubic-bezier(0.2, 0, 0, 1), left 0.22s cubic-bezier(0.2, 0, 0, 1),
width 0.22s cubic-bezier(0.2, 0, 0, 1), height 0.22s cubic-bezier(0.2, 0, 0, 1);
/* Fades in once as the highlight lands. Keyed on the stop, so it plays per
stop and not on the measure tick. */
animation: amc-tour-arrive 0.25s ease-out both;
}
@keyframes amc-tour-arrive {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
[data-component="amc-tour-spotlight"] .amc-tour-ring {
animation: none;
}
}

/* ── composer ⇄ dock card morph ──
The question / walkthrough card and the composer swap places in the same
dock. Without a transition the swap is an instant pop; these ease the card
in and out so the region reads as one surface changing shape rather than
two components replacing each other. Motion-reduced users get the swap with
no movement, which is the honest fallback for a positional animation. */
[data-component="session-prompt-dock"] [data-component="session-question-dock"] {
animation: amc-dock-morph-in 0.22s cubic-bezier(0.2, 0, 0, 1) both;
}
@keyframes amc-dock-morph-in {
from {
opacity: 0;
transform: translateY(6px) scale(0.995);
}
to {
opacity: 1;
transform: none;
}
}
[data-component="session-prompt-dock"] [data-component="prompt-input-v2"] {
animation: amc-dock-morph-in 0.22s cubic-bezier(0.2, 0, 0, 1) both;
}
@media (prefers-reduced-motion: reduce) {
[data-component="session-prompt-dock"] [data-component="session-question-dock"],
[data-component="session-prompt-dock"] [data-component="prompt-input-v2"] {
animation: none;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useSpring } from "@opencode-ai/ui/motion-spring"
import { type Accessor, createEffect, createMemo, createResource, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import type { PromptInputState } from "@/components/prompt-input"
import { isTourRequest } from "@/pages/session/composer/session-tour"
import { useSync } from "@/context/sync"
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
import type { SessionComposerController } from "./session-composer-state"
Expand Down Expand Up @@ -138,7 +139,11 @@ export function createSessionComposerRegionController(input: {
parentID,
child: () => !!parentID(),
archived,
showComposer: () => !input.state.blocked() || !!parentID() || archived(),
// A tour stop narrates rather than asks, so it leaves the composer up —
// both because nothing is actually being demanded of the reader, and
// because the Composer stop needs a composer on screen to point at.
showComposer: () =>
!input.state.blocked() || isTourRequest(input.state.questionRequest()) || !!parentID() || archived(),
handoffPrompt: () => getSessionHandoff(input.sessionKey())?.prompt,
promptReady: () => input.prompt.ready() || promptReady(),
dock: () => (store.ready && input.state.dock()) || value() > 0.001,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { bugReportEnabled } from "@/utils/amicode-bug-report"
import { SessionBugDock } from "@/pages/session/composer/session-bug-dock"
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
import { SessionTourSpotlight, isTourRequest } from "@/pages/session/composer/session-tour"
import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock"
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
Expand Down Expand Up @@ -33,6 +34,7 @@ export function SessionComposerRegion(props: {
}}
>
<div
data-tour-target="composer"
classList={{
"w-full px-3 pointer-events-auto": true,
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": controller.centered(),
Expand All @@ -45,6 +47,9 @@ export function SessionComposerRegion(props: {
</div>
)}
</Show>
{/* Overture Stage-7 tour: rings the chrome element the active
"Tour · X" question card names; renders nothing off-tour. */}
<SessionTourSpotlight request={controller.state.questionRequest()} />

<Show when={controller.state.permissionRequest()} keyed>
{(request) => (
Expand Down Expand Up @@ -154,7 +159,15 @@ export function SessionComposerRegion(props: {
fallback={
<Show
when={controller.archived()}
fallback={<Show when={!controller.state.blocked()}>{props.promptInput}</Show>}
fallback={
<Show
when={
!controller.state.blocked() || isTourRequest(controller.state.questionRequest())
}
>
{props.promptInput}
</Show>
}
>
<div
ref={controller.setPromptRef}
Expand Down
Loading
Loading