diff --git a/apps/ade-cli/src/cli.test.ts b/apps/ade-cli/src/cli.test.ts index 3a674e4cc..94836839f 100644 --- a/apps/ade-cli/src/cli.test.ts +++ b/apps/ade-cli/src/cli.test.ts @@ -338,9 +338,9 @@ describe("ADE CLI", () => { }); expect(graph).toContain("ADE lanes"); - expect(graph).toContain("\\- main [main]"); - expect(graph).toContain("|- child [feature]"); - expect(graph).toContain("\\- sibling [feature-2]"); + expect(graph).toContain("\\- main (id: main) [main]"); + expect(graph).toContain("|- child (id: child) [feature]"); + expect(graph).toContain("\\- sibling (id: sibling) [feature-2]"); }); it("accepts --option=value syntax equivalently to --option value", () => { @@ -782,7 +782,7 @@ describe("ADE CLI", () => { expect(summarized).toMatchObject({ lanes: expect.any(Array), }); - expect((summarized as any).visual).toContain("\\- main [main]"); - expect((summarized as any).visual).toContain("\\- child [feature]"); + expect((summarized as any).visual).toContain("\\- main (id: main) [main]"); + expect((summarized as any).visual).toContain("\\- child (id: child) [feature]"); }); }); diff --git a/apps/ade-cli/src/cli.ts b/apps/ade-cli/src/cli.ts index e6a230075..f528c5afe 100644 --- a/apps/ade-cli/src/cli.ts +++ b/apps/ade-cli/src/cli.ts @@ -2646,8 +2646,9 @@ function renderLaneGraph(result: unknown): string { const branch = asString(lane.branchRef) ?? ""; const status = asString(lane.status) ?? ""; const archived = asString(lane.archivedAt) ? " archived" : ""; - lines.push(`${prefix}${isLast ? "\\- " : "|- "}${name}${branch ? ` [${branch}]` : ""}${status ? ` ${status}` : ""}${archived}`); const id = asString(lane.id); + const idSuffix = id ? ` (id: ${id})` : ""; + lines.push(`${prefix}${isLast ? "\\- " : "|- "}${name}${idSuffix}${branch ? ` [${branch}]` : ""}${status ? ` ${status}` : ""}${archived}`); const children = id ? byParent.get(id) ?? [] : []; children.forEach((child, index) => visit(child, `${prefix}${isLast ? " " : "| "}`, index === children.length - 1)); }; diff --git a/apps/desktop/src/main/services/cli/adeCliService.test.ts b/apps/desktop/src/main/services/cli/adeCliService.test.ts index de26ff570..2c552c5d2 100644 --- a/apps/desktop/src/main/services/cli/adeCliService.test.ts +++ b/apps/desktop/src/main/services/cli/adeCliService.test.ts @@ -179,6 +179,213 @@ describe("createAdeCliService", () => { } }); + it("adds the user install dir to the shell profile when installing Terminal access", async () => { + const root = makeTempRoot(); + const home = path.join(root, "home"); + const resourcesPath = path.join(root, "Resources"); + const packagedBinDir = path.join(resourcesPath, "ade-cli", "bin"); + const packagedCommandPath = path.join(packagedBinDir, "ade"); + const installerPath = path.join(resourcesPath, "ade-cli", "install-path.sh"); + writeExecutable(packagedCommandPath); + writeExecutable(installerPath); + fs.writeFileSync(path.join(resourcesPath, "ade-cli", "cli.cjs"), "console.log('ade')\n"); + + const service = createAdeCliService({ + isPackaged: true, + resourcesPath, + userDataPath: path.join(root, "user-data"), + appExecutablePath: path.join(root, "ADE.app", "Contents", "MacOS", "ADE"), + env: { HOME: home, SHELL: "/bin/zsh", PATH: "/usr/bin:/bin" }, + logger: logger() as any, + }); + + const result = await service.installForUser(); + const profilePath = path.join(home, ".zshrc"); + const profile = fs.readFileSync(profilePath, "utf8"); + + expect(result.ok).toBe(true); + expect(result.message).toContain(`added ${path.join(home, ".local", "bin")} to ${profilePath}`); + expect(profile).toContain("# ADE CLI"); + expect(profile).toContain('export PATH="$HOME/.local/bin:$PATH"'); + }); + + it("writes to ~/.bashrc when SHELL is bash", async () => { + const root = makeTempRoot(); + const home = path.join(root, "home"); + const resourcesPath = path.join(root, "Resources"); + const packagedBinDir = path.join(resourcesPath, "ade-cli", "bin"); + writeExecutable(path.join(packagedBinDir, "ade")); + writeExecutable(path.join(resourcesPath, "ade-cli", "install-path.sh")); + fs.writeFileSync(path.join(resourcesPath, "ade-cli", "cli.cjs"), "console.log('ade')\n"); + + const service = createAdeCliService({ + isPackaged: true, + resourcesPath, + userDataPath: path.join(root, "user-data"), + appExecutablePath: path.join(root, "ADE.app", "Contents", "MacOS", "ADE"), + env: { HOME: home, SHELL: "/usr/local/bin/bash", PATH: "/usr/bin:/bin" }, + logger: logger() as any, + }); + + const result = await service.installForUser(); + const profilePath = path.join(home, ".bashrc"); + + expect(result.ok).toBe(true); + expect(result.message).toContain(profilePath); + expect(fs.readFileSync(profilePath, "utf8")).toContain('export PATH="$HOME/.local/bin:$PATH"'); + }); + + it("falls back to ~/.profile when SHELL is unrecognized", async () => { + const root = makeTempRoot(); + const home = path.join(root, "home"); + const resourcesPath = path.join(root, "Resources"); + const packagedBinDir = path.join(resourcesPath, "ade-cli", "bin"); + writeExecutable(path.join(packagedBinDir, "ade")); + writeExecutable(path.join(resourcesPath, "ade-cli", "install-path.sh")); + fs.writeFileSync(path.join(resourcesPath, "ade-cli", "cli.cjs"), "console.log('ade')\n"); + + const service = createAdeCliService({ + isPackaged: true, + resourcesPath, + userDataPath: path.join(root, "user-data"), + appExecutablePath: path.join(root, "ADE.app", "Contents", "MacOS", "ADE"), + env: { HOME: home, SHELL: "/usr/bin/nu", PATH: "/usr/bin:/bin" }, + logger: logger() as any, + }); + + const result = await service.installForUser(); + const profilePath = path.join(home, ".profile"); + + expect(result.ok).toBe(true); + expect(result.message).toContain(profilePath); + expect(fs.readFileSync(profilePath, "utf8")).toContain('export PATH="$HOME/.local/bin:$PATH"'); + }); + + it("writes fish-syntax PATH update to ~/.config/fish/config.fish for fish shell", async () => { + const root = makeTempRoot(); + const home = path.join(root, "home"); + const resourcesPath = path.join(root, "Resources"); + const packagedBinDir = path.join(resourcesPath, "ade-cli", "bin"); + writeExecutable(path.join(packagedBinDir, "ade")); + writeExecutable(path.join(resourcesPath, "ade-cli", "install-path.sh")); + fs.writeFileSync(path.join(resourcesPath, "ade-cli", "cli.cjs"), "console.log('ade')\n"); + + const service = createAdeCliService({ + isPackaged: true, + resourcesPath, + userDataPath: path.join(root, "user-data"), + appExecutablePath: path.join(root, "ADE.app", "Contents", "MacOS", "ADE"), + env: { HOME: home, SHELL: "/usr/bin/fish", PATH: "/usr/bin:/bin" }, + logger: logger() as any, + }); + + const result = await service.installForUser(); + const profilePath = path.join(home, ".config", "fish", "config.fish"); + + expect(result.ok).toBe(true); + expect(result.message).toContain(profilePath); + const profile = fs.readFileSync(profilePath, "utf8"); + expect(profile).toContain("# ADE CLI"); + expect(profile).toContain("fish_add_path -gP $HOME/.local/bin"); + expect(profile).not.toContain("export PATH="); + }); + + it("skips the shell-profile write when the install dir is already on PATH", async () => { + const root = makeTempRoot(); + const home = path.join(root, "home"); + const resourcesPath = path.join(root, "Resources"); + const packagedBinDir = path.join(resourcesPath, "ade-cli", "bin"); + writeExecutable(path.join(packagedBinDir, "ade")); + writeExecutable(path.join(resourcesPath, "ade-cli", "install-path.sh")); + fs.writeFileSync(path.join(resourcesPath, "ade-cli", "cli.cjs"), "console.log('ade')\n"); + const targetDir = path.join(home, ".local", "bin"); + // Simulate an ade binary already at the install location so getStatus + // reports it as installed once PATH contains targetDir. + writeExecutable(path.join(targetDir, "ade")); + + const service = createAdeCliService({ + isPackaged: true, + resourcesPath, + userDataPath: path.join(root, "user-data"), + appExecutablePath: path.join(root, "ADE.app", "Contents", "MacOS", "ADE"), + env: { HOME: home, SHELL: "/bin/zsh", PATH: `${targetDir}:/usr/bin:/bin` }, + logger: logger() as any, + }); + + const result = await service.installForUser(); + const profilePath = path.join(home, ".zshrc"); + + expect(result.ok).toBe(true); + expect(result.message).toBe("Installed ade for Terminal access."); + expect(fs.existsSync(profilePath)).toBe(false); + }); + + it("does not append the PATH line twice when the marker is already present", async () => { + const root = makeTempRoot(); + const home = path.join(root, "home"); + const resourcesPath = path.join(root, "Resources"); + const packagedBinDir = path.join(resourcesPath, "ade-cli", "bin"); + writeExecutable(path.join(packagedBinDir, "ade")); + writeExecutable(path.join(resourcesPath, "ade-cli", "install-path.sh")); + fs.writeFileSync(path.join(resourcesPath, "ade-cli", "cli.cjs"), "console.log('ade')\n"); + + const profilePath = path.join(home, ".zshrc"); + const seeded = "# previous user content\n\n# ADE CLI\nexport PATH=\"$HOME/.local/bin:$PATH\"\n"; + fs.mkdirSync(home, { recursive: true }); + fs.writeFileSync(profilePath, seeded); + + const service = createAdeCliService({ + isPackaged: true, + resourcesPath, + userDataPath: path.join(root, "user-data"), + appExecutablePath: path.join(root, "ADE.app", "Contents", "MacOS", "ADE"), + env: { HOME: home, SHELL: "/bin/zsh", PATH: "/usr/bin:/bin" }, + logger: logger() as any, + }); + + const result = await service.installForUser(); + + expect(result.ok).toBe(true); + expect(result.message).toContain(profilePath); + expect(result.message).toContain("PATH entry already present"); + expect(result.message).not.toMatch(/and added .* to /); + // Profile contents are unchanged — exactly one ADE CLI marker, exactly one PATH line. + const profile = fs.readFileSync(profilePath, "utf8"); + expect(profile).toBe(seeded); + expect(profile.match(/# ADE CLI/g)?.length).toBe(1); + }); + + it("inserts a leading newline when the existing profile has no trailing newline", async () => { + const root = makeTempRoot(); + const home = path.join(root, "home"); + const resourcesPath = path.join(root, "Resources"); + const packagedBinDir = path.join(resourcesPath, "ade-cli", "bin"); + writeExecutable(path.join(packagedBinDir, "ade")); + writeExecutable(path.join(resourcesPath, "ade-cli", "install-path.sh")); + fs.writeFileSync(path.join(resourcesPath, "ade-cli", "cli.cjs"), "console.log('ade')\n"); + + const profilePath = path.join(home, ".zshrc"); + fs.mkdirSync(home, { recursive: true }); + fs.writeFileSync(profilePath, "alias foo=bar"); // no trailing newline + + const service = createAdeCliService({ + isPackaged: true, + resourcesPath, + userDataPath: path.join(root, "user-data"), + appExecutablePath: path.join(root, "ADE.app", "Contents", "MacOS", "ADE"), + env: { HOME: home, SHELL: "/bin/zsh", PATH: "/usr/bin:/bin" }, + logger: logger() as any, + }); + + const result = await service.installForUser(); + expect(result.ok).toBe(true); + + const profile = fs.readFileSync(profilePath, "utf8"); + expect(profile.startsWith("alias foo=bar\n")).toBe(true); + expect(profile).toContain("\n# ADE CLI\n"); + expect(profile).toContain('export PATH="$HOME/.local/bin:$PATH"\n'); + }); + it("creates a dev shim under userData without changing global PATH", () => { const root = makeTempRoot(); const repoRoot = path.join(root, "repo"); diff --git a/apps/desktop/src/main/services/cli/adeCliService.ts b/apps/desktop/src/main/services/cli/adeCliService.ts index a90fe0531..ea15e56a3 100644 --- a/apps/desktop/src/main/services/cli/adeCliService.ts +++ b/apps/desktop/src/main/services/cli/adeCliService.ts @@ -397,12 +397,58 @@ function resolveCliPaths(args: CreateAdeCliServiceArgs): ResolvedCliPaths { }; } -function installTargetPath(): string { +function homeDir(env: NodeJS.ProcessEnv = process.env): string { + return env.HOME?.trim() || os.homedir(); +} + +function installTargetPath(env: NodeJS.ProcessEnv = process.env): string { if (process.platform === "win32") { - const localAppData = process.env.LOCALAPPDATA?.trim() || path.join(os.homedir(), "AppData", "Local"); + const localAppData = env.LOCALAPPDATA?.trim() || path.join(homeDir(env), "AppData", "Local"); return path.join(localAppData, "ADE", "bin", "ade.cmd"); } - return path.join(os.homedir(), ".local", "bin", "ade"); + return path.join(homeDir(env), ".local", "bin", "ade"); +} + +type ShellProfile = { path: string; flavor: "posix" | "fish" }; + +function shellProfilePath(env: NodeJS.ProcessEnv = process.env): ShellProfile { + const shell = env.SHELL?.trim() ?? ""; + const home = homeDir(env); + if (shell.endsWith("zsh")) return { path: path.join(home, ".zshrc"), flavor: "posix" }; + if (shell.endsWith("bash")) return { path: path.join(home, ".bashrc"), flavor: "posix" }; + if (shell.endsWith("fish")) return { path: path.join(home, ".config", "fish", "config.fish"), flavor: "fish" }; + return { path: path.join(home, ".profile"), flavor: "posix" }; +} + +function shellPathEntry(targetDir: string, env: NodeJS.ProcessEnv = process.env): string { + const home = homeDir(env); + const relativeToHome = path.relative(home, targetDir); + if (relativeToHome && !relativeToHome.startsWith("..") && !path.isAbsolute(relativeToHome)) { + return `$HOME/${relativeToHome.split(path.sep).join("/")}`; + } + return targetDir; +} + +type ShellPathResult = { profilePath: string; modified: boolean }; + +function ensureUserBinOnShellPath( + targetDir: string, + env: NodeJS.ProcessEnv = process.env, +): ShellPathResult | null { + if (process.platform === "win32" || pathContainsDir(getPathEnvValue(env), targetDir)) return null; + const profile = shellProfilePath(env); + const entry = shellPathEntry(targetDir, env); + const marker = "# ADE CLI"; + const line = + profile.flavor === "fish" ? `fish_add_path -gP ${entry}` : `export PATH="${entry}:$PATH"`; + const existing = fs.existsSync(profile.path) ? fs.readFileSync(profile.path, "utf8") : ""; + if (existing.includes(marker) || existing.includes(line) || existing.includes(targetDir)) { + return { profilePath: profile.path, modified: false }; + } + const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""; + fs.mkdirSync(path.dirname(profile.path), { recursive: true }); + fs.appendFileSync(profile.path, `${prefix}\n${marker}\n${line}\n`); + return { profilePath: profile.path, modified: true }; } function statusMessage(args: { @@ -466,7 +512,7 @@ export function createAdeCliService(args: CreateAdeCliServiceArgs) { const getStatus = async (): Promise => { const terminalCommandPath = resolveCommandOnPath("ade", hostPathSnapshot, envSnapshot); - const targetPath = installTargetPath(); + const targetPath = installTargetPath(envSnapshot); const targetDir = path.dirname(targetPath); const terminalInstalled = Boolean(terminalCommandPath); const bundledAvailable = Boolean(resolved.commandPath && isExecutable(resolved.commandPath)); @@ -518,10 +564,16 @@ export function createAdeCliService(args: CreateAdeCliServiceArgs) { if (result.status !== 0) { throw new Error(result.stderr.trim() || result.stdout.trim() || "ADE CLI installer failed."); } + const targetDir = path.dirname(installTargetPath(envSnapshot)); + const profileResult = ensureUserBinOnShellPath(targetDir, envSnapshot); const status = await getStatus(); return { ok: true, - message: status.installTargetDirOnPath + message: profileResult + ? profileResult.modified + ? `Installed ade for Terminal access and added ${targetDir} to ${profileResult.profilePath}. Open a new terminal or source that file.` + : `Installed ade for Terminal access. PATH entry already present in ${profileResult.profilePath}; open a new terminal or source that file.` + : status.installTargetDirOnPath ? "Installed ade for Terminal access." : `Installed ade at ${status.installTargetPath}. Add ${path.dirname(status.installTargetPath)} to PATH if your shell cannot find it.`, status, diff --git a/apps/desktop/src/renderer/components/app/AppShell.tsx b/apps/desktop/src/renderer/components/app/AppShell.tsx index d4e8fe28b..b23db088e 100644 --- a/apps/desktop/src/renderer/components/app/AppShell.tsx +++ b/apps/desktop/src/renderer/components/app/AppShell.tsx @@ -51,6 +51,7 @@ import { listContextDocsByHealth, } from "../context/contextShared"; import { disposeTerminalRuntimesForProjectChange } from "../terminals/TerminalView"; +import { buildPrsRouteSearch, type PrDetailRouteTab } from "../prs/prsRouteState"; type PrToast = { id: string; @@ -1234,9 +1235,23 @@ export function AppShell({ children }: { children: React.ReactNode }) { type="button" className="inline-flex h-8 items-center gap-1.5 rounded-md border border-border/60 bg-transparent px-3 text-[11px] font-medium text-fg/85 transition-colors hover:border-fg/20 hover:bg-fg/[0.04] hover:text-fg" onClick={() => { - selectLane(toast.event.laneId); - setLaneInspectorTab(toast.event.laneId, "merge"); - window.location.hash = `#/lanes?laneId=${encodeURIComponent(toast.event.laneId)}&focus=single&inspectorTab=merge`; + let detailTab: PrDetailRouteTab | null = null; + if (toast.event.kind === "checks_failing") { + detailTab = "checks"; + } else if ( + toast.event.kind === "changes_requested" || + toast.event.kind === "review_requested" + ) { + detailTab = "activity"; + } + const search = buildPrsRouteSearch({ + activeTab: "normal", + selectedPrId: toast.event.prId, + selectedQueueGroupId: null, + selectedRebaseItemId: null, + detailTab, + }); + navigate(`/prs${search}`); dismissPrToast(toast.id); }} > diff --git a/apps/desktop/src/renderer/components/settings/GeneralSection.tsx b/apps/desktop/src/renderer/components/settings/GeneralSection.tsx index 444170382..a01ef5080 100644 --- a/apps/desktop/src/renderer/components/settings/GeneralSection.tsx +++ b/apps/desktop/src/renderer/components/settings/GeneralSection.tsx @@ -11,6 +11,7 @@ import { LABEL_STYLE, primaryButton, } from "../lanes/laneDesignTokens"; +import { AdeCliSection } from "./AdeCliSection"; const sectionLabelStyle: React.CSSProperties = { ...LABEL_STYLE, @@ -103,6 +104,11 @@ export function GeneralSection() { +
+
ADE CLI
+ +
+
AI MODE
diff --git a/apps/desktop/src/renderer/onboarding/stepBuilders/createLaneDialog.ts b/apps/desktop/src/renderer/onboarding/stepBuilders/createLaneDialog.ts index 61495f343..9fdef0e30 100644 --- a/apps/desktop/src/renderer/onboarding/stepBuilders/createLaneDialog.ts +++ b/apps/desktop/src/renderer/onboarding/stepBuilders/createLaneDialog.ts @@ -41,8 +41,8 @@ export function buildCreateLaneDialogWalkthrough(): TourStep[] { { id: "createLane.openMenu", target: '[data-tour="lanes.newLane"]', - title: "Create a lane", - body: "Click **New Lane**. A lane is a safe copy of the project for one task, like `fix-login-copy` or `try-new-checkout-flow`. The tutorial makes one disposable lane so you can see the shape.", + title: "Make your first lane", + body: "Click **New Lane**. We'll make a throwaway sandbox just for this tutorial — call it whatever you like, then delete it at the end. Real lanes get useful names like `fix-login-bug` or `try-dark-mode`.", placement: "bottom", docUrl: docs.lanesOverview, waitForSelector: '[data-tour="lanes.newLane"]', @@ -56,8 +56,8 @@ export function buildCreateLaneDialogWalkthrough(): TourStep[] { { id: "createLane.chooseCreate", target: '[data-tour="lanes.createNewLane"]', - title: "Create new lane", - body: "Choose **Create new lane**. ADE will make a fresh Git worktree: a real folder on disk with its own branch, separate from primary.", + title: "Choose \"Create new lane\"", + body: "Pick this option. Behind the scenes, ADE creates a new folder on your computer that's a separate copy of your project — that's what makes it a sandbox. (Git people: it's a worktree on a fresh branch.)", placement: "right", docUrl: docs.lanesCreating, waitForSelector: '[data-tour="lanes.createNewLane"]', @@ -72,8 +72,8 @@ export function buildCreateLaneDialogWalkthrough(): TourStep[] { { id: "createLane.nameField", target: '[data-tour="lanes.createDialog.name"]', - title: "Name it", - body: "Use a short task name, not a sentence. Good examples: `fix-login-copy`, `test-new-sidebar`, `tour-sample`. ADE uses this name for the lane and its branch/worktree identity.", + title: "Give it a name", + body: "Short and task-shaped works best — like `fix-login-copy` or `try-dark-mode`, not full sentences. ADE uses this name for the folder and the Git branch.", placement: "right", requires: CREATE_LANE_DIALOG_REQUIRES, beforeEnter: (ctx) => { @@ -89,8 +89,8 @@ export function buildCreateLaneDialogWalkthrough(): TourStep[] { { id: "createLane.sourceChoices", target: '[data-tour="lanes.createDialog.tabs"]', - title: "Three ways to start", - body: "**Primary** starts from the clean main project. **Branch** is for work you already started on a Git branch. **Child** makes a stacked lane that depends on another lane. Leave **Primary** selected here.", + title: "Where to start from", + body: "Three options: **Primary** (start from your clean main project — the usual choice), **Branch** (use work you already started on a Git branch), or **Child** (build on top of another lane). Leave **Primary** selected for the tutorial.", placement: "right", requires: CREATE_LANE_DIALOG_REQUIRES, preventTargetInteraction: true, @@ -101,8 +101,8 @@ export function buildCreateLaneDialogWalkthrough(): TourStep[] { { id: "createLane.branchBase", target: '[data-tour="lanes.createDialog.branchBase"]', - title: "Pick a branch base", - body: "The base is the starting line. If the base is `main`, ADE compares your lane to `main`; if new commits land on `main`, ADE can tell you the lane may need a rebase.", + title: "What to copy from", + body: "Pick which branch this sandbox copies from — usually `main`. ADE compares your lane's changes against this so it can tell you what's different and warn you when the original has moved on.", placement: "right", requires: CREATE_LANE_DIALOG_REQUIRES, focusTarget: true, @@ -113,8 +113,8 @@ export function buildCreateLaneDialogWalkthrough(): TourStep[] { { id: "createLane.branchTab", target: '[data-tour="lanes.createDialog.branchTab"]', - title: "Branch is for existing work", - body: "Use **Branch** when work already exists, like `feature/search` on your machine or GitHub, and you want ADE to manage it as a lane. Do not choose it for this tutorial lane.", + title: "Already started somewhere else?", + body: "If you already have a Git branch with work on it (like `feature/search` from a teammate or your earlier work), use **Branch** to bring it in as a lane instead of starting fresh. Skip this for the tutorial.", placement: "right", requires: CREATE_LANE_DIALOG_REQUIRES, ghostCursor: { @@ -129,8 +129,8 @@ export function buildCreateLaneDialogWalkthrough(): TourStep[] { { id: "createLane.childTab", target: '[data-tour="lanes.createDialog.childTab"]', - title: "Child is for stacked lanes", - body: "Use **Child** when task B depends on task A. Example: parent lane `build-checkout-page`, child lane `polish-checkout-errors`. The child ships after the parent.", + title: "Building on another lane?", + body: "Use **Child** when one task depends on another — like *\"build the checkout page\"* (parent) and *\"polish the checkout error messages\"* (child). The child ships after the parent. Skip for the tutorial.", placement: "right", requires: CREATE_LANE_DIALOG_REQUIRES, ghostCursor: { @@ -145,8 +145,8 @@ export function buildCreateLaneDialogWalkthrough(): TourStep[] { { id: "createLane.create", target: '[data-tour="lanes.createDialog.create"]', - title: "Create the lane", - body: "Click **Create**. ADE makes the branch and worktree folder, selects the new lane, and keeps primary untouched.", + title: "Make it", + body: "Click **Create**. ADE makes the new sandbox folder, switches you to it, and your real project stays untouched.", placement: "left", requires: ["laneCountIncreased"], awaitingActionLabel: "Waiting for the new test lane", diff --git a/apps/desktop/src/renderer/onboarding/stepBuilders/gitActionsPane.ts b/apps/desktop/src/renderer/onboarding/stepBuilders/gitActionsPane.ts index d524a1c99..3002b6934 100644 --- a/apps/desktop/src/renderer/onboarding/stepBuilders/gitActionsPane.ts +++ b/apps/desktop/src/renderer/onboarding/stepBuilders/gitActionsPane.ts @@ -21,8 +21,8 @@ export function buildGitActionsPaneWalkthrough(): TourStep[] { { id: "gitActions.stage", target: '[data-tour="lanes.gitActionsPane"]', - title: "Stage files", - body: "When unstaged files exist, they appear in the Unstaged section. Use per-file controls or Stage all to choose what goes into the next commit.", + title: "1. Pick what to keep", + body: "When you change files, they show up here as **unstaged**. \"Staging\" just means *\"include this in my next save.\"* You can stage one file at a time, or hit **Stage all** to include everything.", placement: "left", requires: ["laneExists"], waitForSelector: '[data-tour="lanes.gitActionsPane"]', @@ -31,12 +31,12 @@ export function buildGitActionsPaneWalkthrough(): TourStep[] { { id: "gitActions.commit", target: '[data-tour="lanes.gitActionsPane"]', - title: "Commit controls", + title: "2. Save a snapshot", bodyTemplate: (ctx) => { const lane = ctx.get("laneName") ?? "this lane"; - return `Commits require staged changes in ${lane} unless Amend is enabled. ${modifierKey}+Enter runs the commit action when the button is enabled.`; + return `A **commit** is a saved snapshot of your work — a checkpoint you can come back to. Write a short message saying what changed in **${lane}**, then click commit (or hit ${modifierKey}+Enter). You can only commit if you've staged something first.`; }, - body: `Commits require staged changes unless Amend is enabled. ${modifierKey}+Enter runs the commit action when the button is enabled.`, + body: `A **commit** is a saved snapshot of your work — a checkpoint you can come back to. Write a short message saying what changed, then click commit (or hit ${modifierKey}+Enter). You can only commit if you've staged something first.`, placement: "left", requires: ["laneExists"], waitForSelector: '[data-tour="lanes.gitActionsPane"]', @@ -45,8 +45,8 @@ export function buildGitActionsPaneWalkthrough(): TourStep[] { { id: "gitActions.push", target: '[data-tour="lanes.gitActionsPane"]', - title: "Publish or push", - body: "The remote button changes with the lane state: Publish for a new remote branch, Push for local commits, or Force Push when history was rewritten.", + title: "3. Share it", + body: "**Pushing** uploads your saved snapshots somewhere shareable (like GitHub) so others can see them. The button label changes based on what you need: **Publish** the first time, **Push** for new snapshots, or **Force Push** for unusual cases.", placement: "left", requires: ["laneExists"], waitForSelector: '[data-tour="lanes.gitActionsPane"]', diff --git a/apps/desktop/src/renderer/onboarding/stepBuilders/manageLaneDialog.ts b/apps/desktop/src/renderer/onboarding/stepBuilders/manageLaneDialog.ts index 8075c14a7..630c226cc 100644 --- a/apps/desktop/src/renderer/onboarding/stepBuilders/manageLaneDialog.ts +++ b/apps/desktop/src/renderer/onboarding/stepBuilders/manageLaneDialog.ts @@ -19,12 +19,12 @@ export function buildManageLaneDialogWalkthrough(): TourStep[] { { id: "manageLane.openMenu", target: '[data-tour="lanes.laneTab"]', - title: "Open lane actions", + title: "Open the lane menu", bodyTemplate: (ctx) => { const lane = ctx.get("laneName") ?? "this lane"; - return `Right-click ${lane}'s lane tab to open its actions menu.`; + return `Right-click **${lane}**'s tab to open its menu of actions (rename, archive, delete, etc.).`; }, - body: "Right-click a lane tab to open its actions menu.", + body: "Right-click a lane's tab to open its menu of actions (rename, archive, delete, etc.).", placement: "bottom", docUrl: docs.lanesOverview, waitForSelector: '[data-tour="lanes.laneTab"]', @@ -36,8 +36,8 @@ export function buildManageLaneDialogWalkthrough(): TourStep[] { { id: "manageLane.openDialog", target: '[data-tour="lanes.manageLane"]', - title: "Manage lane", - body: "Choose Manage Lane. The dialog opens without touching the lane yet.", + title: "Manage Lane", + body: "Pick **Manage Lane**. This just opens a dialog where you can choose what to do — nothing happens to your lane yet.", placement: "right", docUrl: docs.lanesOverview, waitForSelector: '[data-tour="lanes.manageLane"]', @@ -52,8 +52,8 @@ export function buildManageLaneDialogWalkthrough(): TourStep[] { { id: "manageLane.laneInfo", target: '[data-tour="lanes.manageDialog.laneInfo"]', - title: "Lane at a glance", - body: "Name, branch, type, and worktree path live here. Management actions below affect this selected lane.", + title: "What lane this is", + body: "Quick check: this is the lane you're about to manage. Name, branch, where its folder lives. Everything below affects *this* lane only.", placement: "bottom", requires: MANAGE_LANE_DIALOG_REQUIRES, waitForSelector: '[data-tour="lanes.manageDialog.laneInfo"]', @@ -64,8 +64,8 @@ export function buildManageLaneDialogWalkthrough(): TourStep[] { { id: "manageLane.archive", target: '[data-tour="lanes.manageDialog.archive"]', - title: "Archive, don't delete", - body: "Archive hides a lane from ADE without touching the worktree or branch. Good for parking a lane you might come back to.", + title: "Park it instead of deleting", + body: "**Archive** hides the lane from your list without actually deleting anything. Good for *\"I might come back to this someday\"* situations — the files stay on your computer, ADE just stops showing it.", placement: "left", requires: MANAGE_LANE_DIALOG_REQUIRES, waitForSelector: '[data-tour="lanes.manageDialog.archive"]', @@ -76,8 +76,8 @@ export function buildManageLaneDialogWalkthrough(): TourStep[] { { id: "manageLane.deleteTabs", target: '[data-tour="lanes.manageDialog.tabs"]', - title: "Choose what to remove", - body: "Choose how far deletion goes: remove only the worktree, also delete the local branch, or also delete the remote branch.", + title: "How thorough to delete", + body: "Three levels: remove just the lane folder, also delete the branch on your computer, or also delete the branch on GitHub. Pick how far you want it gone.", placement: "bottom", requires: MANAGE_LANE_DIALOG_REQUIRES, waitForSelector: '[data-tour="lanes.manageDialog.tabs"]', @@ -88,8 +88,8 @@ export function buildManageLaneDialogWalkthrough(): TourStep[] { { id: "manageLane.deleteConfirm", target: '[data-tour="lanes.manageDialog.confirm"]', - title: "Confirm the lane", - body: "Type the exact phrase shown above the field. The delete button enables only after it matches.", + title: "Type to confirm", + body: "Deletion is permanent, so ADE asks you to type the lane's name to make sure you really mean it. The delete button stays disabled until what you type matches.", placement: "right", requires: MANAGE_LANE_DIALOG_REQUIRES, waitForSelector: '[data-tour="lanes.manageDialog.confirm"]', @@ -100,8 +100,8 @@ export function buildManageLaneDialogWalkthrough(): TourStep[] { { id: "manageLane.deleteButton", target: '[data-tour="lanes.manageDialog.delete"]', - title: "Delete only when you mean it", - body: "This is the destructive action. Primary lanes are protected and never reach this state.", + title: "The point of no return", + body: "Click this and the lane is gone for good. Your real project is always protected — you can never accidentally delete it from this dialog.", placement: "left", requires: MANAGE_LANE_DIALOG_REQUIRES, waitForSelector: '[data-tour="lanes.manageDialog.delete"]', diff --git a/apps/desktop/src/renderer/onboarding/stepBuilders/prCreateModal.ts b/apps/desktop/src/renderer/onboarding/stepBuilders/prCreateModal.ts index 22ba5eee3..165444ce2 100644 --- a/apps/desktop/src/renderer/onboarding/stepBuilders/prCreateModal.ts +++ b/apps/desktop/src/renderer/onboarding/stepBuilders/prCreateModal.ts @@ -19,8 +19,8 @@ export function buildPrCreateModalWalkthrough(): TourStep[] { { id: "prCreate.open", target: '[data-tour="prs.createBtn"]', - title: "Create a PR", - body: "Click Create PR to open the PR dialog. It starts with source and target branches, then moves to title and description.", + title: "Open the PR dialog", + body: "Click **Create PR**. A dialog will open with two short steps: pick what you're shipping (and where it should go), then write a title and description.", placement: "bottom", docUrl: docs.lanesOverview, waitForSelector: '[data-tour="prs.createBtn"]', @@ -31,8 +31,8 @@ export function buildPrCreateModalWalkthrough(): TourStep[] { { id: "prCreate.source", target: '[data-tour="prs.createModal.source"]', - title: "Source lane", - body: "Choose the lane you want to ship. Only non-primary lanes can be PR sources.", + title: "Which lane to ship", + body: "Pick the lane whose changes you want to merge into your main project. (You can only pick a lane, not your real project — that's the thing you're merging *into*.)", placement: "right", requires: PR_CREATE_DIALOG_REQUIRES, fallbackAfterMs: PR_CREATE_FALLBACK_MS, @@ -46,8 +46,8 @@ export function buildPrCreateModalWalkthrough(): TourStep[] { { id: "prCreate.base", target: '[data-tour="prs.createModal.base"]', - title: "Target branch", - body: "This field chooses where the PR will merge. ADE defaults it from the primary lane.", + title: "Where it should go", + body: "Pick which branch the changes should land on. Almost always this is your project's main branch (`main`) — ADE picks that for you by default.", placement: "top", requires: PR_CREATE_DIALOG_REQUIRES, fallbackAfterMs: PR_CREATE_FALLBACK_MS, @@ -61,8 +61,8 @@ export function buildPrCreateModalWalkthrough(): TourStep[] { { id: "prCreate.next", target: '[data-tour="prs.createModal.next"]', - title: "Move to details", - body: "Click Next step after the source and target are set. The title and description fields appear on the next screen.", + title: "Onto the details", + body: "With the lane and target picked, click **Next step**. The title and description fields appear next.", placement: "left", requires: PR_CREATE_DIALOG_REQUIRES, fallbackAfterMs: PR_CREATE_FALLBACK_MS, @@ -78,8 +78,8 @@ export function buildPrCreateModalWalkthrough(): TourStep[] { { id: "prCreate.title", target: '[data-tour="prs.createModal.title"]', - title: "PR title", - body: "Write a concise change title. ADE falls back to the lane name if you leave it empty.", + title: "Title", + body: "A short summary of the change — like *\"Add dark mode toggle\"*. This is what reviewers see first. If you leave it empty, ADE uses the lane name.", placement: "bottom", requires: PR_CREATE_DIALOG_REQUIRES, fallbackAfterMs: PR_CREATE_FALLBACK_MS, @@ -94,7 +94,7 @@ export function buildPrCreateModalWalkthrough(): TourStep[] { id: "prCreate.body", target: '[data-tour="prs.createModal.body"]', title: "Description", - body: "Use the body for what changed, why, and how to test. The draft button can fill it from commits when a source lane is selected.", + body: "Explain the change — *what changed, why, how to try it*. Reviewers will read this. The **Draft** button writes a starter for you based on what you saved in the lane.", placement: "bottom", requires: PR_CREATE_DIALOG_REQUIRES, fallbackAfterMs: PR_CREATE_FALLBACK_MS, @@ -108,8 +108,8 @@ export function buildPrCreateModalWalkthrough(): TourStep[] { { id: "prCreate.submit", target: '[data-tour="prs.createModal.submit"]', - title: "Create button", - body: "Create pushes the lane and opens a PR only after the dialog is ready. If the button is disabled, finish the required fields first.", + title: "Ship it", + body: "Click **Create**. ADE uploads your lane to GitHub and opens the PR for you. If the button is grayed out, it means a required field is empty — fill it in and try again.", placement: "top", requires: ["prCreated"], fallbackAfterMs: PR_CREATE_FALLBACK_MS, diff --git a/apps/desktop/src/renderer/onboarding/tours/automationsTour.ts b/apps/desktop/src/renderer/onboarding/tours/automationsTour.ts index b1eef311e..8bc9d6bdb 100644 --- a/apps/desktop/src/renderer/onboarding/tours/automationsTour.ts +++ b/apps/desktop/src/renderer/onboarding/tours/automationsTour.ts @@ -8,22 +8,8 @@ export const automationsTour: Tour = { steps: [ { target: '[data-tour="automations.createTrigger"]', - title: "Triggers", - body: "Triggers decide what starts an automation: schedule, webhook, git event, or file watch.", - docUrl: docs.automationsOverview, - placement: "right", - }, - { - target: '[data-tour="automations.createTrigger"]', - title: "Actions", - body: "Actions run after a trigger fires: launch a command, dispatch a mission, or notify a worker.", - docUrl: docs.automationsOverview, - placement: "right", - }, - { - target: '[data-tour="automations.createTrigger"]', - title: "Guardrails", - body: "Guardrails cover rate limits, concurrency caps, quiet hours, and approval boundaries.", + title: "If this happens, do that", + body: "An automation is a little \"if/then\" rule that runs in the background. Three parts: a **trigger** (what kicks it off — a schedule, a button push, a Git event, a file change), an **action** (what it does — run a command, ask AI to do something, send a ping), and **guardrails** (limits to keep it safe — like \"don't run more than 3 of these at once\"). Click here to set one up.", docUrl: docs.automationsOverview, placement: "right", }, diff --git a/apps/desktop/src/renderer/onboarding/tours/ctoTour.ts b/apps/desktop/src/renderer/onboarding/tours/ctoTour.ts index 7368f8815..de6e5a33f 100644 --- a/apps/desktop/src/renderer/onboarding/tours/ctoTour.ts +++ b/apps/desktop/src/renderer/onboarding/tours/ctoTour.ts @@ -8,22 +8,22 @@ export const ctoTour: Tour = { steps: [ { target: '[data-tour="cto.sidebar"]', - title: "Agents", - body: "The sidebar lists the agents the CTO manages. Identities persist between sessions.", + title: "Your AI team", + body: "Everyone on this list is an AI helper the CTO manages — like team members. Each one has a name, a role, and remembers things between sessions, so you don't have to re-explain context every time.", docUrl: docs.ctoOverview, placement: "right", }, { target: '[data-tour="cto.teamPanel"]', - title: "Team panel", - body: "Inspect, edit, or archive agents. Budget caps and heartbeat intervals live here too.", + title: "Manage them like a real team", + body: "Look at what each AI helper is doing, change their role, or set them aside. You can also cap how much they're allowed to spend per month here — useful while you're learning what they're good for.", docUrl: docs.ctoOverview, placement: "left", }, { target: '[data-tour="cto.linearPanel"]', - title: "Linear sync", - body: "Connect Linear to let the CTO dispatch missions from tickets and report results back.", + title: "Hook up your task list", + body: "Use Linear (a popular project management tool) to track work? Connect it here and the CTO will turn tickets into AI tasks automatically, then post results back to the ticket. Skip if you don't use Linear.", docUrl: docs.ctoOverview, placement: "left", }, diff --git a/apps/desktop/src/renderer/onboarding/tours/filesTour.ts b/apps/desktop/src/renderer/onboarding/tours/filesTour.ts index 51da49632..24043665f 100644 --- a/apps/desktop/src/renderer/onboarding/tours/filesTour.ts +++ b/apps/desktop/src/renderer/onboarding/tours/filesTour.ts @@ -1,71 +1,39 @@ import { registerTour, type Tour } from "../registry"; import { docs } from "../docsLinks"; +// Curated to selectors that render before any file is opened. Mode toggle and +// breadcrumb actions only mount once a file is active in the editor — they're +// intentionally not in this tour to avoid a hang when a fresh user lands here. export const filesTour: Tour = { id: "files", title: "Files tab walkthrough", route: "/files", steps: [ - { - target: '[data-tour="files.header"]', - title: "Files header", - body: "The Files tab is your full-project editor. Switch between workspaces, manage git actions, and open files in external tools from this bar.", - docUrl: docs.filesEditor, - placement: "bottom", - }, { target: '[data-tour="files.workspaceSelector"]', - title: "Workspace selector", - body: "Each entry is a workspace — either your primary folder or a lane worktree. Switch here to browse a different lane's files without leaving the tab.", - docUrl: docs.lanesOverview, - placement: "bottom", - }, - { - target: '[data-tour="files.explorerPane"]', - title: "Explorer pane", - body: "The file tree lives here. Click a folder to expand it, click a file to open it in the editor. Right-click any item for create, rename, delete, and git actions.", - docUrl: docs.filesEditor, - placement: "right", - }, - { - target: '[data-tour="files.searchBar"]', - title: "Full-text search", - body: "Type to search across every file in the workspace. Results appear inline; click one to jump straight to that line.", + title: "Pick which copy to look at", + body: "Each lane has its own copy of the files (we call that a **worktree** — basically *\"this lane's folder\"*). Use this dropdown to switch between your main project and any lane.", docUrl: docs.filesEditor, placement: "bottom", }, { - target: '[data-tour="files.fileTree"]', - title: "File tree", - body: "Files with uncommitted changes show a colored badge — M for modified, A for added, D for deleted. Colour-coded icons identify file types at a glance.", + target: '[data-tour="files.fileTree"], [data-tour="files.explorerPane"]', + title: "Browse and spot changes", + body: "Click a folder to expand it, click a file to open it. Files with changes show a colored letter: **M** = edited, **A** = new, **D** = deleted — so you can see what this lane changed without opening anything.", docUrl: docs.filesEditor, placement: "right", }, { - target: '[data-tour="files.editorPane"]', - title: "Editor pane", - body: "Open files appear as tabs here. Edit directly in Code mode, review your uncommitted changes in Changes mode, or resolve merge markers in Merge mode.", - docUrl: docs.filesEditor, - placement: "left", - }, - { - target: '[data-tour="files.modeToggle"]', - title: "Code / Changes / Merge", - body: "CODE edits the raw file. CHANGES shows a side-by-side diff against the last commit. MERGE lets you pick which side of a conflict to keep.", - docUrl: docs.filesEditor, - placement: "bottom", - }, - { - target: '[data-tour="files.breadcrumb"]', - title: "Breadcrumb & git actions", - body: "The file path sits on the left. When a lane workspace is active, Stage, Unstage, and Discard buttons appear on the right to manage individual files.", + target: '[data-tour="files.searchBar"]', + title: "Search every file", + body: "Type anything — a function name, a word you remember, anything — and ADE searches every file in this lane. Click a result to jump to that line.", docUrl: docs.filesEditor, placement: "bottom", }, { target: '[data-tour="files.openIn"]', - title: "Open in external editor", - body: "Send the active file straight to VS Code, Cursor, Zed, or the system file browser without leaving ADE.", + title: "Open in your favorite editor", + body: "Already use VS Code, Cursor, or another code editor? This button hands the file (or the whole lane folder) over in one click. Keep ADE as your home base while editing wherever you like.", docUrl: docs.filesEditor, placement: "bottom", }, diff --git a/apps/desktop/src/renderer/onboarding/tours/firstJourneyTour.test.ts b/apps/desktop/src/renderer/onboarding/tours/firstJourneyTour.test.ts new file mode 100644 index 000000000..c7f7742c9 --- /dev/null +++ b/apps/desktop/src/renderer/onboarding/tours/firstJourneyTour.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; +import { firstJourneyTour } from "./firstJourneyTour"; + +const SECTION_PREFIXES = [ + "act1.laneWorkPane.", + "act6.history.", + "act8.run.", + "act9.automations.", + "act10.cto.", + "act11.settings.", +] as const; + +const FALLBACK_MS = 30_000; +const FALLBACK_LABEL = "Skip"; +const FALLBACK_NOTICE_DEFAULT = + "This step is waiting on a state that hasn't appeared — you can skip it without affecting the tutorial."; + +describe("firstJourneyTour tutorialSection wrapping", () => { + const sectionSteps = firstJourneyTour.steps.filter((step) => + SECTION_PREFIXES.some((prefix) => step.id?.startsWith(prefix)), + ); + + it("includes wrapped steps from every tutorialSection (smoke)", () => { + expect(sectionSteps.length).toBeGreaterThan(0); + for (const prefix of SECTION_PREFIXES) { + expect( + sectionSteps.some((step) => step.id?.startsWith(prefix)), + `expected at least one step with id prefix ${prefix}`, + ).toBe(true); + } + }); + + it("namespaces every wrapped step's id with its section prefix and index", () => { + for (const step of sectionSteps) { + const matchedPrefix = SECTION_PREFIXES.find((prefix) => + step.id?.startsWith(prefix), + ); + expect(matchedPrefix, `expected step id ${step.id} to match a section prefix`).toBeTruthy(); + // Format is `${sectionId}.${index}` — the suffix after the section prefix + // must be a non-empty index (numeric) when the source step had no id. + const suffix = step.id!.slice(matchedPrefix!.length); + expect(suffix.length).toBeGreaterThan(0); + } + }); + + it("attaches a non-empty requires gate to every wrapped step", () => { + for (const step of sectionSteps) { + expect(step.requires, `step ${step.id} should have a requires gate`).toBeTruthy(); + expect((step.requires ?? []).length).toBeGreaterThan(0); + } + }); + + it("derives waitForSelector from target when the source step did not set one", () => { + for (const step of sectionSteps) { + if (step.target) { + // When target is set and the source didn't override, waitForSelector === target. + // At minimum it must be a non-empty string for any step with a target. + expect(typeof step.waitForSelector).toBe("string"); + expect((step.waitForSelector ?? "").length).toBeGreaterThan(0); + } + } + }); + + it("injects a fallbackAfterMs/Skip/notice on every requires-gated wrapped step", () => { + // None of the sub-tours fed into tutorialSection currently set + // fallbackAfterMs themselves, so every wrapped step here must take the + // injected default. If a sub-tour starts overriding fallback fields, those + // steps would fail this expectation — and that's fine: drop them from this + // assertion explicitly rather than silently passing. + for (const step of sectionSteps) { + expect(step.fallbackAfterMs, `step ${step.id} fallbackAfterMs`).toBe(FALLBACK_MS); + expect(step.fallbackNextLabel, `step ${step.id} fallbackNextLabel`).toBe(FALLBACK_LABEL); + expect(step.fallbackNotice, `step ${step.id} fallbackNotice`).toBe(FALLBACK_NOTICE_DEFAULT); + } + }); + + it("does not add fallback fields to non-section act steps that have no requires gate", () => { + // Hero / actIntro steps like act0.welcome have target: "" and no requires, + // so they must remain free of fallback noise. + const welcome = firstJourneyTour.steps.find((step) => step.id === "act0.welcome"); + expect(welcome).toBeTruthy(); + expect(welcome!.fallbackAfterMs).toBeUndefined(); + expect(welcome!.fallbackNextLabel).toBeUndefined(); + expect(welcome!.fallbackNotice).toBeUndefined(); + }); +}); diff --git a/apps/desktop/src/renderer/onboarding/tours/firstJourneyTour.ts b/apps/desktop/src/renderer/onboarding/tours/firstJourneyTour.ts index 9030d2593..281abd3b4 100644 --- a/apps/desktop/src/renderer/onboarding/tours/firstJourneyTour.ts +++ b/apps/desktop/src/renderer/onboarding/tours/firstJourneyTour.ts @@ -8,13 +8,8 @@ import { buildManageLaneDialogWalkthrough, buildPrCreateModalWalkthrough, } from "../stepBuilders"; -import { graphTour } from "./graphTour"; -import { filesTour } from "./filesTour"; -import { workTour } from "./workTour"; import { runTour } from "./runTour"; -import { lanesTour } from "./lanesTour"; import { laneWorkPaneTour } from "./laneWorkPaneTour"; -import { prsTour } from "./prsTour"; import { historyTour } from "./historyTour"; import { automationsTour } from "./automationsTour"; import { ctoTour } from "./ctoTour"; @@ -37,17 +32,42 @@ const OPTIONAL_ACTION_FALLBACK_MS = 30_000; type Ctx = TourCtx; +// Wraps a per-tab walkthrough's steps for inclusion in the tutorial. Injects: +// - A stable id per step (sectionId.index) so progress tracking works. +// - A `requires` gate (default: project open) — overridable per-call. +// - A `waitForSelector` derived from `target` so the engine waits for the +// anchor before rendering — the underlying timeout is 10s in +// `waitForSelector.ts`, so a missing anchor never permanently hangs. +// - A `fallbackAfterMs` + skip label whenever the step has a `requires` gate +// and the source walkthrough didn't already provide a fallback. This means +// a user can never get stuck on a tutorial step waiting for state that +// never arrives — they always have a "Continue" / "Skip" path within +// `OPTIONAL_ACTION_FALLBACK_MS`. function tutorialSection( sectionId: string, steps: readonly TourStep[], requires: readonly string[] = PROJECT_OPEN_REQUIRES, ): TourStep[] { - return steps.map((step, index) => ({ - ...step, - id: step.id ?? `${sectionId}.${index}`, - requires: step.requires ?? requires, - waitForSelector: step.waitForSelector ?? (step.target ? step.target : undefined), - })); + return steps.map((step, index) => { + const effectiveRequires = step.requires ?? requires; + const needsFallback = + effectiveRequires.length > 0 && typeof step.fallbackAfterMs !== "number"; + return { + ...step, + id: step.id ?? `${sectionId}.${index}`, + requires: effectiveRequires, + waitForSelector: step.waitForSelector ?? (step.target ? step.target : undefined), + ...(needsFallback + ? { + fallbackAfterMs: OPTIONAL_ACTION_FALLBACK_MS, + fallbackNextLabel: step.fallbackNextLabel ?? "Skip", + fallbackNotice: + step.fallbackNotice ?? + "This step is waiting on a state that hasn't appeared — you can skip it without affecting the tutorial.", + } + : {}), + }; + }); } function laneName(ctx: Ctx): string { @@ -116,31 +136,13 @@ function waitForProjectBrowserClosed(): Promise { }); } -function buildTabHandoffStep( - id: string, - currentTab: string, - nextTab: string, - docUrl: string, -): TourStep { - return { - id, - target: '[data-tour="app.helpMenu"]', - title: "Replay this section later", - body: `You just walked the main **${currentTab}** controls. The ? menu can replay this same section by itself later, without the action steps. Next: **${nextTab}**.`, - placement: "left", - requires: LANE_EXISTS_REQUIRES, - waitForSelector: '[data-tour="app.helpMenu"]', - docUrl, - }; -} - // --- Act 0: Welcome + project picker --------------------------------------- const act0Welcome: TourStep = { id: "act0.welcome", target: "", title: "Welcome to ADE", - body: "ADE helps you work on one project in several safe copies at the same time. This tutorial creates one test lane, shows where its files/chats/Git state live, then cleans it up.", - actIntro: { title: "Welcome to ADE", subtitle: "Safe copies of one project, each with its own task.", variant: "drift" }, + body: "Imagine being able to try several different ideas on your project **at the same time** without any of them messing each other up. That's what ADE does. Each idea gets its own safe sandbox copy of your code — we call those **lanes**. This tutorial will create one test lane, show you around, then clean it up.", + actIntro: { title: "Welcome to ADE", subtitle: "Try several ideas in parallel — each in its own safe copy.", variant: "drift" }, docUrl: docs.welcome, branches: (_ctx: TourCtx) => { if (isWelcomeProjectScreenVisible()) return "act0.openProject"; @@ -152,7 +154,7 @@ const act0ProjectChoice: TourStep = { id: "act0.projectChoice", target: '[data-tour="project.activeTab"]', title: "Use this project", - body: "ADE already has a project open. Click **Use this project** to continue with this repo. If you want a different repo, use the project switcher after the tutorial.", + body: "A **project** is just a folder of code on your computer (technically a Git repo). You already have one open. Click **Use this project** to keep going with it. You can swap to a different folder later from the project switcher.", placement: "bottom", waitForSelector: '[data-tour="project.activeTab"]', advanceWhenSelector: '[data-tour="project.browser"]', @@ -178,7 +180,7 @@ const act0OpenProject: TourStep = { id: "act0.openProject", target: '[data-tour="project.welcomeOpenButton"]', title: "Open a project", - body: "Open a recent project, or choose **Open Project** to browse for another Git repo.", + body: "Pick a folder of code to work on. ADE works with any Git repository — that just means a folder you've put under version control. Open a recent one, or click **Open Project** to browse to a different folder.", placement: "right", waitForSelector: '[data-tour="project.welcomeOpenButton"]', advanceWhenSelector: '[data-tour="project.browser"]', @@ -201,8 +203,8 @@ const act0OpenProject: TourStep = { const act0ProjectBrowser: TourStep = { id: "act0.projectBrowser", target: '[data-tour="project.browser"]', - title: "Pick your repo", - body: "Select a Git repo in this picker, then click Open. Close the picker to return to recent projects.", + title: "Pick your folder", + body: "Browse to the project folder you want to work on, then click **Open**. Close this picker if you'd rather pick from your recent projects instead.", placement: "top", waitForSelector: '[data-tour="project.browser"]', awaitingActionLabel: "Waiting for project to open", @@ -221,7 +223,7 @@ const act1Intro: TourStep = { id: "act1.intro", target: "", title: "Make a lane", - body: "Think of a lane as one safe workspace for one task. It has its own branch, folder, file changes, and worker chats, while your primary project stays clean.", + body: "A **lane** is a safe sandbox copy of your project for one task — like *\"try a new login screen\"* or *\"fix the broken search\"*. It has its own copy of the files, its own conversations with AI helpers, and its own changes. Your real project (we call it **primary**) stays untouched until you decide the work is good enough to keep.", actIntro: { title: "Make a lane", variant: "orbit" }, requires: PROJECT_OPEN_REQUIRES, beforeEnter: async () => [{ type: "navigate", to: "/lanes" }], @@ -232,20 +234,45 @@ const act1SidebarSweep: TourStep = { id: "act1.sidebarSweep", target: '[data-tour="app.sidebar"]', title: "Your tabs", - body: "The left rail is ADE's map. Lanes is where work starts, Graph shows how lanes relate, Files shows the lane's code, Work shows chats and terminals, and PRs/History help you ship and audit.", + body: "These icons on the left are how you move around ADE — like tabs in a browser. You're on **Lanes** right now (where you manage your sandbox copies). We'll visit each of the others as we go.", placement: "right", requires: PROJECT_OPEN_REQUIRES, waitForSelector: '[data-tour="app.sidebar"]', docUrl: docs.welcome, }; +// Lanes basics steps used inline by act1 (instead of spreading the full +// lanesTour, which would re-introduce New Lane and lane tabs the user just +// learned about via the interactive create-lane builder + lane spotlight). +const act1BranchSelector: TourStep = { + id: "act1.branchSelector", + target: '[data-tour="lanes.branchSelector"]', + title: "The clean starting point", + body: "Every sandbox copy needs a starting point. ADE uses your project's main branch (usually called `main`) as that. Each new lane copies from here, and ADE always compares the lane's changes back to this so you can see exactly what's different.", + placement: "bottom", + requires: LANE_EXISTS_REQUIRES, + waitForSelector: '[data-tour="lanes.branchSelector"]', + docUrl: docs.lanesOverview, +}; + +const act1StatusChips: TourStep = { + id: "act1.statusChips", + target: '[data-tour="lanes.statusChips"]', + title: "What's going on with each lane", + body: "These little badges tell you the status of every lane at a glance. **Running** = something is actively working in it. **Waiting** = it needs you (or an AI) to make a call. **Ended** = the work there is done or put away.", + placement: "bottom", + requires: LANE_EXISTS_REQUIRES, + waitForSelector: '[data-tour="lanes.statusChips"]', + docUrl: docs.lanesOverview, +}; + const act1LaneTabSpotlight: TourStep = { id: "act1.laneTabSpotlight", target: '[data-tour="lanes.laneTab"]', title: "Your new lane", - body: "That tab is your new lane. Select it any time you want this task's files, chats, and Git actions.", + body: "There it is — that's your sandbox copy. Click it any time to see this task's files, conversations, and changes. Your real project stays untouched.", bodyTemplate: (ctx) => - `${laneName(ctx)} is live. It has its own branch, folder, file changes, and worker chats. Primary stays separate.`, + `**${laneName(ctx)}** is live — that's your sandbox copy. Anything that happens in it (file changes, AI chats, etc.) stays inside it. Your real project is untouched.`, placement: "bottom", requires: LANE_EXISTS_REQUIRES, disableBack: true, @@ -256,24 +283,15 @@ const act1LaneTabSpotlight: TourStep = { docUrl: docs.lanesOverview, }; -const act1PerTabTours = buildTabHandoffStep("act1.perTabTours", "Lanes", "Graph", docs.lanesOverview); -const act2PerTabTours = buildTabHandoffStep("act2.perTabTours", "Graph", "Files", docs.workspaceGraph); -const act3PerTabTours = buildTabHandoffStep("act3.perTabTours", "Files", "Work", docs.filesEditor); -const act4PerTabTours = buildTabHandoffStep("act4.perTabTours", "Work", "Git actions", docs.chatOverview); -const act5PerTabTours = buildTabHandoffStep("act5.perTabTours", "Git actions", "PRs", docs.lanesOverview); -const act7PerTabTours = buildTabHandoffStep("act7.perTabTours", "PRs", "History", docs.prsOverview); -const act6PerTabTours = buildTabHandoffStep("act6.perTabTours", "History", "Run", docs.historyOverview); -const act8PerTabTours = buildTabHandoffStep("act8.perTabTours", "Run", "Automations", docs.projectHome); -const act9PerTabTours = buildTabHandoffStep("act9.perTabTours", "Automations", "CTO", docs.automationsOverview); -const act10PerTabTours = buildTabHandoffStep("act10.perTabTours", "CTO", "Settings", docs.ctoOverview); -const act11PerTabTours = buildTabHandoffStep("act11.perTabTours", "Settings", "cleanup", docs.settingsGeneral); +// Per-act handoff steps were collapsed into the single act12 finale — no need +// to remind the user 11 times that the ? menu replays sections. // --- Act 2: Graph ----------------------------------------------------------- const act2Intro: TourStep = { id: "act2.intro", target: "", - title: "See the shape", - body: "Graph draws every lane as a node, every relationship as an edge.", + title: "See how everything connects", + body: "Once you have a few lanes going, it can be hard to keep track of how they relate. **Graph** is a visual map — each lane is a circle, each connection between them is a line.", actIntro: { title: "See the shape", variant: "orbit" }, requires: LANE_EXISTS_REQUIRES, beforeEnter: async () => { @@ -291,10 +309,10 @@ const act2Intro: TourStep = { const act2LaneNode: TourStep = { id: "act2.laneNode", target: '[data-tour="graph.node"]', - title: "Your lane, as a node", - body: "That node is your lane hanging off primary. Edges show stacking — where one lane branches off another.", + title: "Your lane on the map", + body: "That circle is your new lane, drawn off the main project. The line between them shows it branched off from there. If you build a lane *on top of* another lane (called **stacking**), you'd see another line continuing out from it.", bodyTemplate: (ctx) => - `That node is your new lane (${laneName(ctx)}) hanging off primary. Edges show stacking — where one lane branches off another.`, + `That circle is **${laneName(ctx)}**, your new sandbox copy. The line shows it branched off from your main project. Lines between lanes show **stacking** — when one lane builds on top of another.`, placement: "bottom", requires: LANE_EXISTS_REQUIRES, waitForSelector: '[data-tour="graph.node"]', @@ -304,8 +322,8 @@ const act2LaneNode: TourStep = { const act2Zoom: TourStep = { id: "act2.zoom", target: '[data-tour="graph.zoom"]', - title: "Zoom and pan", - body: "Scroll to zoom, drag to pan. The graph redraws live as you create, rebase, or archive lanes.", + title: "Move around the map", + body: "Scroll to zoom in and out, drag to pan around. The map updates itself live as you make new lanes or change existing ones — no refresh needed.", placement: "left", requires: LANE_EXISTS_REQUIRES, waitForSelector: '[data-tour="graph.zoom"]', @@ -319,8 +337,8 @@ const act2Zoom: TourStep = { const act2Legend: TourStep = { id: "act2.legend", target: '[data-tour="graph.legend"]', - title: "Read the legend", - body: "The legend explains node colors and edge types. Glance at it when something looks unfamiliar — the shape of a node usually tells you its state.", + title: "What the colors mean", + body: "Lanes change color and shape based on their status — like \"has changes you haven't saved\" or \"ready to ship\". This little key explains them. Peek at it whenever something looks unfamiliar.", placement: "left", requires: LANE_EXISTS_REQUIRES, waitForSelector: '[data-tour="graph.legend"]', @@ -331,9 +349,9 @@ const act2Legend: TourStep = { const act3Intro: TourStep = { id: "act3.intro", target: "", - title: "Each lane, its own files", - body: "Files can browse the primary project or any lane worktree. Pick the workspace first, then inspect files and changes.", - actIntro: { title: "Each lane, its own files", variant: "drift" }, + title: "Browse the code", + body: "**Files** is your code browser — like Finder or Explorer, but for any of your lanes. Each lane has its own copy of the project's files (we call that a **worktree**, just a fancy word for \"this lane's folder\"). Pick which lane to look at, then explore.", + actIntro: { title: "Browse the code", variant: "drift" }, requires: LANE_EXISTS_REQUIRES, beforeEnter: async () => [{ type: "navigate", to: "/files" }], docUrl: docs.filesEditor, @@ -342,10 +360,10 @@ const act3Intro: TourStep = { const act3Workspace: TourStep = { id: "act3.workspace", target: '[data-tour="files.workspaceSelector"]', - title: "Pick a workspace", - body: "Use this selector to choose the primary project or a lane worktree before browsing files.", + title: "Pick which copy to look at", + body: "Each lane has its own files, so you have to tell ADE which one you want to see. Use this dropdown to switch between your main project and any lane.", bodyTemplate: (ctx) => - `Choose your new lane (${laneName(ctx)}) here to scope the tree and editor to that lane's worktree.`, + `Each lane has its own copy of the files. Pick **${laneName(ctx)}** here to see *that* lane's version — anything you do in there only affects that sandbox.`, placement: "bottom", requires: LANE_EXISTS_REQUIRES, waitForSelector: '[data-tour="files.workspaceSelector"]', @@ -355,8 +373,8 @@ const act3Workspace: TourStep = { const act3Tree: TourStep = { id: "act3.tree", target: '[data-tour="files.fileTree"]', - title: "Change badges", - body: "Modified, added, and deleted files get colored badges. Scan the tree to spot what's changed without diving into diffs.", + title: "Spot what's changed", + body: "When a file's been touched in this lane, it gets a colored letter next to it: **M** = you edited it, **A** = you made it new, **D** = you deleted it. So you can glance and see exactly what this sandbox has changed.", placement: "right", requires: LANE_EXISTS_REQUIRES, waitForSelector: '[data-tour="files.fileTree"]', @@ -366,30 +384,19 @@ const act3Tree: TourStep = { const act3Search: TourStep = { id: "act3.search", target: '[data-tour="files.searchBar"]', - title: "Full-text search", - body: "Search across every file in the lane's worktree. Results open in the editor with the match highlighted.", + title: "Search every file", + body: "Type anything — a function name, a piece of text, a typo you remember — and ADE searches every file in this lane. Click a result to jump straight to that line.", placement: "bottom", requires: LANE_EXISTS_REQUIRES, waitForSelector: '[data-tour="files.searchBar"]', docUrl: docs.filesEditor, }; -const act3Mode: TourStep = { - id: "act3.mode", - target: '[data-tour="files.modeToggle"]', - title: "Code, changes, or merge", - body: "Toggle between the editor, a per-file diff, and a three-way merge view. Same file, three lenses.", - placement: "bottom", - requires: LANE_EXISTS_REQUIRES, - waitForSelector: '[data-tour="files.modeToggle"]', - docUrl: docs.filesEditor, -}; - const act3OpenIn: TourStep = { id: "act3.openIn", target: '[data-tour="files.openIn"]', - title: "Jump to your editor", - body: "Open the current file — or the whole worktree — in VS Code, Cursor, or your system default. ADE stays home base.", + title: "Open in your favorite editor", + body: "Already use VS Code, Cursor, or another code editor? This button hands the file (or the whole lane folder) over to it in one click. Keep using ADE as your home base while editing wherever you like.", placement: "bottom", requires: LANE_EXISTS_REQUIRES, waitForSelector: '[data-tour="files.openIn"]', @@ -400,9 +407,9 @@ const act3OpenIn: TourStep = { const act4Intro: TourStep = { id: "act4.intro", target: "", - title: "Talk to a worker", - body: "Workers read files, run commands, and edit code. Work shows every session across every lane.", - actIntro: { title: "Talk to a worker", variant: "particles" }, + title: "Get help from AI", + body: "ADE can ask AI to read your files, run commands, and write code for you — we call those AI helpers **workers**. The **Work** tab shows every conversation you have with them, plus any terminal windows you've opened, all in one place.", + actIntro: { title: "Get help from AI", variant: "particles" }, requires: LANE_EXISTS_REQUIRES, beforeEnter: async () => [{ type: "navigate", to: "/work" }], docUrl: docs.chatOverview, @@ -411,8 +418,8 @@ const act4Intro: TourStep = { const act4Sessions: TourStep = { id: "act4.sessions", target: '[data-tour="work.sessionsPane"]', - title: "Every session, one place", - body: "Unlike the embedded Work view inside a lane, this one shows every worker across every lane at once.", + title: "Every conversation in one list", + body: "All your AI chats and terminal windows show up here, no matter which lane they're in. Each one is called a **session** — just one open conversation or one open terminal.", placement: "right", requires: LANE_EXISTS_REQUIRES, waitForSelector: '[data-tour="work.sessionsPane"]', @@ -422,10 +429,10 @@ const act4Sessions: TourStep = { const act4LaneFilter: TourStep = { id: "act4.laneFilter", target: '[data-tour="work.laneFilter"]', - title: "Filter to your lane", - body: "Narrow sessions to a single lane. Handy when workers are running across half a dozen worktrees at once.", + title: "Narrow the list", + body: "Got a lot going on? Filter the list down to just one lane's conversations. Useful once you have AI working in several lanes at once.", bodyTemplate: (ctx) => - `Filter to ${laneName(ctx)} here. Handy when workers are running across half a dozen worktrees at once.`, + `Click here to see only **${laneName(ctx)}**'s conversations. Useful once you have AI working in several lanes at once.`, placement: "bottom", requires: LANE_EXISTS_REQUIRES, waitForSelector: '[data-tour="work.laneFilter"]', @@ -439,24 +446,28 @@ const act4LaneFilter: TourStep = { const act4NewSession: TourStep = { id: "act4.newSession", target: '[data-tour="work.newSession"]', - title: "Start a session", - body: "Use this button when you want a chat, CLI agent, or shell. The session picker lets you choose the lane before anything starts.", + title: "Start a chat with AI", + body: "Click **New Chat** to open a conversation. You can ask the AI to do things like *\"add a dark mode toggle\"* or *\"figure out why this test is failing\"* — it'll read your files and make changes for you. The tutorial continues the moment your chat shows up in the list.", placement: "bottom", requires: LANE_EXISTS_REQUIRES, waitForSelector: '[data-tour="work.newSession"]', + awaitingActionLabel: "Waiting for a chat to start", + advanceWhenSelector: '[data-tour="work.sessionItem"]', + fallbackAfterMs: OPTIONAL_ACTION_FALLBACK_MS, + fallbackNextLabel: "Skip starting a chat", + fallbackNotice: "Don't want to start a chat right now? You can come back to this button any time.", + exitOnOutsideInteraction: true, + allowedInteractionSelectors: ['[data-tour="work.newSession"]'], docUrl: docs.chatOverview, }; const act4ViewArea: TourStep = { id: "act4.viewArea", target: '[data-tour="work.viewArea"]', - title: "Where the conversation lives", - body: "Open sessions appear here. If you have not started one yet, this area stays empty and the session list remains the source of truth.", + title: "Where the chat shows up", + body: "Your open chat lives here. Open more than one and they appear as tabs you can drag around. Close a tab to clean up. The list on the left always shows everything — even chats you've closed but not deleted.", placement: "left", - requires: ["projectOpen", "laneExists", "chatStarted"], - fallbackAfterMs: OPTIONAL_ACTION_FALLBACK_MS, - fallbackNextLabel: "Continue without a session", - fallbackNotice: "No worker session is required for the rest of the walkthrough.", + requires: LANE_EXISTS_REQUIRES, waitForSelector: '[data-tour="work.viewArea"]', docUrl: docs.chatOverview, }; @@ -465,9 +476,9 @@ const act4ViewArea: TourStep = { const act5Intro: TourStep = { id: "act5.intro", target: "", - title: "Git actions", - body: "This pane shows dirty files, the commit box, pull and push controls, and advanced git actions. Buttons enable only when the lane has the required state.", - actIntro: { title: "Git actions", variant: "drift" }, + title: "Save your work", + body: "When you've made changes you want to keep, you **commit** them — that's a saved snapshot of your work. Then you **push** to upload that snapshot somewhere shareable (like GitHub). This panel handles all of that without making you remember any commands. Buttons light up only when they make sense.", + actIntro: { title: "Save and share your work", variant: "drift" }, requires: LANE_EXISTS_REQUIRES, beforeEnter: async () => [{ type: "navigate", to: "/lanes" }], docUrl: docs.lanesOverview, @@ -478,7 +489,7 @@ const act6Intro: TourStep = { id: "act6.intro", target: "", title: "Nothing gets lost", - body: "Every lane, commit, push, and rebase lands in History. Scrub back whenever you need to know what happened.", + body: "**History** is your project's logbook. Every time you create a lane, save your work, share it, or anything else important happens — it goes here in order. Scroll back any time you want to remember what you (or your AI helpers) did.", actIntro: { title: "Nothing gets lost", variant: "orbit" }, requires: PROJECT_OPEN_REQUIRES, beforeEnter: async () => [{ type: "navigate", to: "/history" }], @@ -488,8 +499,8 @@ const act6Intro: TourStep = { const act6Entries: TourStep = { id: "act6.entries", target: '[data-tour="history.entries"]', - title: "Your trail of breadcrumbs", - body: "Recent events sit at the top. Lane creation appears here once the lane exists; commits and pushes appear only after you perform those actions.", + title: "What just happened", + body: "The newest events sit at the top — making a lane, saving work, sharing changes. The list grows as you do things. If you haven't done it yet, it won't show up here.", placement: "right", requires: PROJECT_OPEN_REQUIRES, waitForSelector: '[data-tour="history.entries"]', @@ -499,8 +510,8 @@ const act6Entries: TourStep = { const act6Filter: TourStep = { id: "act6.filter", target: '[data-tour="history.filter"]', - title: "Filter by importance or kind", - body: "Importance tiers tag the big moments — created, merged, deleted. Use filters to narrow the timeline when it gets noisy.", + title: "Find specific moments", + body: "When the list gets long, filter to just the big stuff — \"lane created\", \"shipped\", \"deleted\" — or just one type of event. Saves scrolling.", placement: "bottom", requires: PROJECT_OPEN_REQUIRES, waitForSelector: '[data-tour="history.filter"]', @@ -514,8 +525,8 @@ const act6Filter: TourStep = { const act6ColumnSettings: TourStep = { id: "act6.columnSettings", target: '[data-tour="history.export"]', - title: "Tune the timeline", - body: "Use column settings to choose the timeline details that matter for review or handoff.", + title: "Show what matters to you", + body: "Choose which details show up next to each event — like timestamps, who did it, or which lane it was in. Hide the noise, keep what's useful.", placement: "bottom", requires: PROJECT_OPEN_REQUIRES, waitForSelector: '[data-tour="history.export"]', @@ -526,90 +537,93 @@ const act6ColumnSettings: TourStep = { const act7Intro: TourStep = { id: "act7.intro", target: "", - title: "Ship a PR", - body: "PRs starts with lane selection and a target branch, then moves into title, description, checks, and merge readiness.", - actIntro: { title: "Ship a PR", variant: "orbit" }, + title: "Ship your work", + body: "When you're happy with what's in a lane and want it to become part of the real project, you open a **PR** (short for **Pull Request** — basically: *\"please pull this lane's changes into the main project\"*). It's how teams review and combine work on GitHub. ADE handles the whole thing for you here.", + actIntro: { title: "Ship your work", variant: "orbit" }, requires: LANE_EXISTS_REQUIRES, beforeEnter: async () => [{ type: "navigate", to: "/prs" }], docUrl: docs.lanesOverview, }; -const act7List: TourStep = { - id: "act7.list", - target: '[data-tour="prs.list"]', - title: "Every PR, at a glance", - body: "The list shows GitHub PRs and ADE-linked lanes. Select a row before inspecting checks, convergence, or close actions.", - placement: "right", - requires: ["projectOpen", "laneExists", "prCreated"], +const act7DetailDrawer: TourStep = { + id: "act7.detailDrawer", + target: '[data-tour="prs.detailDrawer"]', + title: "The PR you just shipped", + body: "Click any PR in the list and this panel opens up to show its details. There are five tabs at the top: **Overview** (the basics), **Path to Merge** (anything stopping it from shipping), **Files** (what changed), **CI / Checks** (automated tests), and **Activity** (review comments and discussion).", + placement: "left", + requires: PROJECT_OPEN_REQUIRES, fallbackAfterMs: OPTIONAL_ACTION_FALLBACK_MS, - fallbackNextLabel: "Continue without a PR", - fallbackNotice: "No PR is required for the remaining product surfaces.", - waitForSelector: '[data-tour="prs.list"]', + fallbackNextLabel: "Skip PR detail", + fallbackNotice: "The detail drawer appears once you select a PR row.", + waitForSelector: '[data-tour="prs.detailDrawer"]', docUrl: docs.lanesOverview, }; -const act7Checks: TourStep = { - id: "act7.checks", - target: '[data-tour="prs.checksPanel"]', - title: "Checks tab", - body: "Checks are shown inside a selected PR. Open a PR row and switch to Checks before using this panel.", +const act7Conflict: TourStep = { + id: "act7.conflict", + target: '[data-tour="prs.conflictSim"]', + title: "What's blocking me?", + body: "This is the most useful tab. It collects everything stopping your PR from shipping — automated tests that failed, comments asking for changes, code conflicts with the main project — into one ordered to-do list. Work top to bottom, and when the list is empty, you can ship.", placement: "left", requires: ["projectOpen", "prCreated"], beforeEnter: async () => [{ type: "ipc", call: async () => { - window.dispatchEvent(new CustomEvent("ade:tour-pr-detail-tab", { detail: "checks" })); + window.dispatchEvent(new CustomEvent("ade:tour-pr-detail-tab", { detail: "convergence" })); }, }], fallbackAfterMs: OPTIONAL_ACTION_FALLBACK_MS, - fallbackNextLabel: "Skip PR checks", - fallbackNotice: "Checks appear after a PR is selected; the walkthrough can continue without one.", - waitForSelector: '[data-tour="prs.checksPanel"]', + fallbackNextLabel: "Skip Path to Merge", + fallbackNotice: "Path to Merge appears once a PR is selected.", + waitForSelector: '[data-tour="prs.conflictSim"]', docUrl: docs.lanesOverview, }; -const act7Conflict: TourStep = { - id: "act7.conflict", - target: '[data-tour="prs.conflictSim"]', - title: "Path to merge", - body: "The convergence tab tracks checks, review comments, conflicts, and resolver runs for the selected PR.", +const act7Checks: TourStep = { + id: "act7.checks", + target: '[data-tour="prs.checksPanel"]', + title: "Automated tests", + body: "Most projects automatically run tests every time you push code (this is called **CI**, short for **Continuous Integration**). This tab shows the results live — passing, failing, still running. Click any row to read the full output without bouncing over to GitHub.", placement: "left", requires: ["projectOpen", "prCreated"], beforeEnter: async () => [{ type: "ipc", call: async () => { - window.dispatchEvent(new CustomEvent("ade:tour-pr-detail-tab", { detail: "convergence" })); + window.dispatchEvent(new CustomEvent("ade:tour-pr-detail-tab", { detail: "checks" })); }, }], fallbackAfterMs: OPTIONAL_ACTION_FALLBACK_MS, - fallbackNextLabel: "Skip merge path", - fallbackNotice: "Path to merge appears after a PR is selected.", - waitForSelector: '[data-tour="prs.conflictSim"]', + fallbackNextLabel: "Skip CI tab", + fallbackNotice: "Checks appear once a PR is selected.", + waitForSelector: '[data-tour="prs.checksPanel"]', docUrl: docs.lanesOverview, }; const act7Stacking: TourStep = { id: "act7.stacking", - target: '[data-tour="prs.stackingIndicator"]', - title: "Queue context", - body: "When a PR belongs to a queue, this button links back to that queue. PRs without queue context skip this control.", + target: '[data-tour="prs.stackingIndicator"], [data-tour="prs.detailDrawer"]', + title: "Stacking PRs", + body: "Sometimes you want to break a big change into smaller PRs that build on each other — like *\"add login API\"* → *\"add login UI\"* → *\"polish the login\"*. Each builds on the last. We call that **stacking**, and this badge shows up so you know where this PR sits in the stack. Standalone PRs don't show this.", placement: "left", - requires: ["projectOpen", "prCreated"], + requires: PROJECT_OPEN_REQUIRES, fallbackAfterMs: OPTIONAL_ACTION_FALLBACK_MS, - fallbackNextLabel: "Skip queue context", - fallbackNotice: "Queue context only appears for queued PRs.", - waitForSelector: '[data-tour="prs.stackingIndicator"]', + fallbackNextLabel: "Skip stacking", + fallbackNotice: "Stacking only shows up for PRs that belong to a stack.", + waitForSelector: '[data-tour="prs.stackingIndicator"], [data-tour="prs.detailDrawer"]', docUrl: docs.lanesStacks, }; const act7Close: TourStep = { id: "act7.close", - target: '[data-tour="prs.detailDrawer"], [data-tour="prs.list"]', - title: "Close actions", - body: "Close appears only on an open selected PR. The walkthrough points out the action; you decide whether a real PR should close.", + target: '[data-tour="prs.closeBtn"], [data-tour="prs.detailDrawer"]', + title: "Closing the PR", + body: "When the work is shipped (or you decide to drop it), close the PR with this button. The lane stays around in case you want to keep building on top of it later.", placement: "top", requires: PROJECT_OPEN_REQUIRES, - waitForSelector: '[data-tour="prs.detailDrawer"], [data-tour="prs.list"]', + fallbackAfterMs: OPTIONAL_ACTION_FALLBACK_MS, + fallbackNextLabel: "Skip close action", + fallbackNotice: "Close is only available for open, non-merged PRs.", + waitForSelector: '[data-tour="prs.closeBtn"], [data-tour="prs.detailDrawer"]', docUrl: docs.lanesOverview, }; @@ -617,9 +631,9 @@ const act7Close: TourStep = { const act8Intro: TourStep = { id: "act8.intro", target: "", - title: "Scripts and services", - body: "Run is where dev servers, tests, and scripts live. Nothing auto-runs — you pick what starts.", - actIntro: { title: "Scripts and services", variant: "drift" }, + title: "Run your project", + body: "Most projects need to *run* something — a dev server while you code, a test suite, a script. Normally you'd type these into a terminal. **Run** lets you save them as buttons you can click. Nothing starts automatically — you decide what runs and when.", + actIntro: { title: "Run your project", variant: "drift" }, requires: PROJECT_OPEN_REQUIRES, beforeEnter: async () => [{ type: "navigate", to: "/project" }], docUrl: docs.projectHome, @@ -666,9 +680,9 @@ const act8ProcessMonitor: TourStep = { const act9Intro: TourStep = { id: "act9.intro", target: "", - title: "Fire things on events", - body: "Automations fire when an event happens — a PR lands, a commit pushes, a test fails — and run an action in response.", - actIntro: { title: "Fire things on events", variant: "particles" }, + title: "Make things happen automatically", + body: "**Automations** are little \"if this happens, do that\" rules. *\"When a PR ships, ping me on Slack.\"* *\"When a test fails, ask AI to look at it.\"* Set them up once and they run themselves in the background.", + actIntro: { title: "Make things happen automatically", variant: "particles" }, requires: PROJECT_OPEN_REQUIRES, beforeEnter: async () => [{ type: "navigate", to: "/automations" }], docUrl: docs.automationsOverview, @@ -711,9 +725,9 @@ const act9Guardrails: TourStep = { const act10Intro: TourStep = { id: "act10.intro", target: "", - title: "Meet your CTO", - body: "A persistent agent that runs an org of workers. It pulls tickets from Linear, dispatches missions, and reports results back.", - actIntro: { title: "Meet your CTO", variant: "orbit" }, + title: "Your AI lead", + body: "**CTO** is an AI that acts like a tech lead on your team. You can give it a list of things to do (or hook up a project manager tool like Linear) and it'll assign work to other AI helpers, check in on their progress, and report back to you. Think of it as a manager for your AI workers.", + actIntro: { title: "Your AI lead", variant: "orbit" }, requires: PROJECT_OPEN_REQUIRES, beforeEnter: async () => [{ type: "navigate", to: "/cto" }], docUrl: docs.ctoOverview, @@ -763,7 +777,7 @@ const act11Intro: TourStep = { id: "act11.intro", target: "", title: "Make it yours", - body: "Settings is sectioned by concern — appearance, AI providers, mobile push, memory, and more. Only General is required to get going.", + body: "Tweak how ADE looks and behaves — themes, which AI services to use (Claude, OpenAI, etc.), notifications, and more. The defaults are fine to start; come back here when you want to customize.", actIntro: { title: "Make it yours", variant: "drift" }, requires: PROJECT_OPEN_REQUIRES, beforeEnter: async () => [{ type: "navigate", to: "/settings" }], @@ -818,8 +832,8 @@ const act11Templates: TourStep = { const act12Nav: TourStep = { id: "act12.nav", target: "", - title: "Clean up", - body: "Back to Lanes to tidy up the sample lane. The tutorial will show where cleanup happens before it ends.", + title: "Clean up the test lane", + body: "Time to delete the sandbox we made for the tutorial. Heading back to **Lanes** so you can see where lane cleanup lives.", requires: PROJECT_OPEN_REQUIRES, beforeEnter: async () => [{ type: "navigate", to: "/lanes" }], docUrl: docs.lanesOverview, @@ -832,8 +846,8 @@ const act12Nav: TourStep = { const act12Help: TourStep = { id: "act12.help", target: '[data-tour="app.helpMenu"]', - title: "Replay or get help", - body: "Open Help to replay tours, jump to docs, or review the glossary.", + title: "Where to get help", + body: "Click the **?** any time to replay any part of this tour, jump into the docs, or look up a word you don't recognize.", placement: "left", requires: PROJECT_OPEN_REQUIRES, waitForSelector: '[data-tour="app.helpMenu"]', @@ -843,9 +857,9 @@ const act12Help: TourStep = { const act12Finale: TourStep = { id: "act12.finale", target: "", - title: "That's every surface of ADE", - body: "You've seen Lanes, Graph, Files, Work, PRs, History, Run, Automations, CTO, and Settings. Replay any walkthrough from the ? menu in the top-right.", - actIntro: { title: "Done", variant: "drift" }, + title: "You've seen the whole app", + body: "Lanes for sandboxes, Graph for the map, Files for the code, Work for AI chats, Git Actions for saving, PRs for shipping, History for the logbook, plus Run, Automations, CTO, and Settings. If you ever forget what something does, hit the **?** in the top-right to replay the bit you need.", + actIntro: { title: "You're set", variant: "drift" }, requires: PROJECT_OPEN_REQUIRES, docUrl: docs.welcome, }; @@ -860,7 +874,9 @@ const act12Finale: TourStep = { const act7CloseWithBranch: TourStep = { ...act7Close, branches: (ctx: TourCtx) => { - if (ctx.get("noRemote")) return "act7.perTabTours"; + // Dry-run / noRemote setup: pushing isn't possible, so we never opened a + // sample PR — skip the close beat and jump straight to History. + if (ctx.get("noRemote")) return "act6.intro"; return null; }, }; @@ -889,66 +905,77 @@ const firstJourneyTour: Tour = { act0ProjectBrowser, // Act 1 — Lanes basics + // We don't spread the full lanesTour here: the user has already created + // a lane interactively (so New Lane and lane tabs are already covered). + // We only need the two pieces the spotlight didn't cover — base branch + // and the status filter chips — plus the Lane Work Pane intro. act1Intro, act1SidebarSweep, ...buildCreateLaneDialogWalkthrough(), act1LaneTabSpotlight, - ...tutorialSection("act1.lanes", lanesTour.steps, LANE_EXISTS_REQUIRES), + act1BranchSelector, + act1StatusChips, ...tutorialSection("act1.laneWorkPane", laneWorkPaneTour.steps, LANE_EXISTS_REQUIRES), - act1PerTabTours, // Act 2 — Graph + // act2LaneNode / Zoom / Legend have ctx-aware copy that names the user's + // lane — better than the generic graphTour spread used elsewhere. act2Intro, - ...tutorialSection("act2.graph", graphTour.steps, LANE_EXISTS_REQUIRES), - act2PerTabTours, + act2LaneNode, + act2Zoom, + act2Legend, // Act 3 — Files act3Intro, - ...tutorialSection("act3.files", filesTour.steps, LANE_EXISTS_REQUIRES), - act3PerTabTours, + act3Workspace, + act3Tree, + act3Search, + act3OpenIn, // Act 4 — Work act4Intro, - ...tutorialSection("act4.work", workTour.steps, LANE_EXISTS_REQUIRES), + act4Sessions, + act4LaneFilter, + act4NewSession, act4ViewArea, - act4PerTabTours, // Act 5 — Git act5IntroWithBranch, ...buildGitActionsPaneWalkthrough(), - act5PerTabTours, - // Act 6 — PRs + // Act 7 — PRs + // The interactive builder ships a real PR. After it lands, we walk the + // user through the detail drawer's tabs by dispatching the + // `ade:tour-pr-detail-tab` event before each step (handler in + // PrDetailPane.tsx). Order: drawer overview → Path to Merge (the most + // load-bearing tab) → CI / Checks → stacking → close. act7Intro, ...buildPrCreateModalWalkthrough(), - ...tutorialSection("act7.prs", prsTour.steps, LANE_EXISTS_REQUIRES), + act7DetailDrawer, + act7Conflict, + act7Checks, + act7Stacking, act7CloseWithBranch, - act7PerTabTours, - // Act 7 — History + // Act 6 — History act6Intro, ...tutorialSection("act6.history", historyTour.steps), - act6PerTabTours, // Act 8 — Run (bonus) act8Intro, ...tutorialSection("act8.run", runTour.steps), - act8PerTabTours, // Act 9 — Automations (bonus) act9Intro, ...tutorialSection("act9.automations", automationsTour.steps), - act9PerTabTours, // Act 10 — CTO (bonus) act10Intro, ...tutorialSection("act10.cto", ctoTour.steps), - act10PerTabTours, // Act 11 — Settings (bonus) act11Intro, ...tutorialSection("act11.settings", settingsTour.steps), - act11PerTabTours, // Act 12 — Cleanup (mandatory) act12Nav, diff --git a/apps/desktop/src/renderer/onboarding/tours/graphTour.ts b/apps/desktop/src/renderer/onboarding/tours/graphTour.ts index da6263c53..c71185785 100644 --- a/apps/desktop/src/renderer/onboarding/tours/graphTour.ts +++ b/apps/desktop/src/renderer/onboarding/tours/graphTour.ts @@ -8,22 +8,22 @@ export const graphTour: Tour = { steps: [ { target: '[data-tour="graph.focusedLane"], [data-tour="graph.canvas"]', - title: "Lane nodes", - body: "The graph maps lanes as nodes and branches as edges. When a lane is focused, this summary gives you the lane state and the next actions.", + title: "How everything connects", + body: "Every lane is a circle, every connection between lanes is a line. When you click a lane, this side panel shows what state it's in and what to do next with it.", docUrl: docs.workspaceGraph, placement: "bottom", }, { target: '[data-tour="graph.zoom"]', - title: "Zoom and pan", - body: "Scroll to zoom, drag to pan. The graph redraws as you create, rebase, or archive lanes.", + title: "Move around the map", + body: "Scroll to zoom in and out, drag to pan around. The map updates itself live — make a new lane, change one, and you'll see the change immediately.", docUrl: docs.workspaceGraph, placement: "left", }, { target: '[data-tour="graph.legend"]', - title: "Legend", - body: "The legend explains node colors and edge types. Use it when a lane state looks unfamiliar.", + title: "What the colors mean", + body: "Lanes change color and shape based on their state — \"has unsaved changes\", \"ready to ship\", and so on. This little key explains them.", docUrl: docs.workspaceGraph, placement: "left", }, diff --git a/apps/desktop/src/renderer/onboarding/tours/historyTour.ts b/apps/desktop/src/renderer/onboarding/tours/historyTour.ts index 153ccb7cc..326e6e421 100644 --- a/apps/desktop/src/renderer/onboarding/tours/historyTour.ts +++ b/apps/desktop/src/renderer/onboarding/tours/historyTour.ts @@ -8,22 +8,22 @@ export const historyTour: Tour = { steps: [ { target: '[data-tour="history.entries"]', - title: "Timeline", - body: "Recent events sit at the top. Lane creation, commits, pushes, PR activity, and missions all land here.", + title: "What just happened", + body: "The newest events sit at the top: lanes you made, work you saved, things you shipped, AI tasks that finished. Scroll to see further back.", docUrl: docs.historyOverview, placement: "right", }, { target: '[data-tour="history.filter"]', - title: "Filters", - body: "Filter by importance or kind when the project gets noisy.", + title: "Find specific moments", + body: "When the list gets long, filter to just the big stuff or just one type of event so you don't have to scroll forever.", docUrl: docs.historyOverview, placement: "bottom", }, { target: '[data-tour="history.export"]', - title: "Column settings", - body: "Tune which timeline details matter for review, handoff, or debugging.", + title: "Show what matters to you", + body: "Choose which details show up next to each event — timestamps, who did it, which lane. Hide the noise, keep what's useful.", docUrl: docs.historyOverview, placement: "bottom", }, diff --git a/apps/desktop/src/renderer/onboarding/tours/laneWorkPaneTour.ts b/apps/desktop/src/renderer/onboarding/tours/laneWorkPaneTour.ts index d642f59f1..8a4984967 100644 --- a/apps/desktop/src/renderer/onboarding/tours/laneWorkPaneTour.ts +++ b/apps/desktop/src/renderer/onboarding/tours/laneWorkPaneTour.ts @@ -8,57 +8,57 @@ export const laneWorkPaneTour: Tour = { steps: [ { target: '[data-tour="work.toolbar"]', - title: "Lane Work Pane", - body: "This pane is the command center for the selected lane. Anything you start here uses this lane's branch and folder, so experiments stay separate from primary.", + title: "The lane's command center", + body: "This is where you start work *inside* a specific lane. Anything you launch from here — AI chats, scripts, terminals — runs in this lane's copy of the project, so it can't mess up your real one or any other lane.", docUrl: docs.chatOverview, placement: "bottom", }, { target: '[data-tour="work.entryOptions"]', - title: "Start a new session", - body: "Pick the kind of help you need: New Chat for an agent conversation, CLI Tool for command-oriented agent work, or New Shell when you want a normal terminal in the lane folder.", + title: "Three ways to get help", + body: "Three buttons start three kinds of helpers — an AI chat, a command-line AI tool, or a plain terminal. Pick the one that fits the kind of help you need.", docUrl: docs.chatOverview, placement: "bottom", }, { target: '[data-tour="lanes.workNewChat"]', title: "AI chat", - body: "Use New Chat when the task is conversational. Example: \"Find why the login form flashes, fix it in this lane, and show me the changed files.\"", + body: "Best for *\"please figure this out and do it\"* tasks. Example: *\"Why does the login screen flash on Safari? Find it and fix it in this lane.\"* The AI reads your files, makes changes, and shows you what it did.", docUrl: docs.chatOverview, placement: "bottom", }, { target: '[data-tour="lanes.workCliTool"]', - title: "CLI tool", - body: "Use CLI Tool when the task is command-shaped. Example: \"Run the focused tests for the files changed in this lane and summarize failures.\"", + title: "Command-line AI tool", + body: "Best when the work is command-shaped — running scripts, processing files. Example: *\"Run the tests for the files I changed and summarize what failed.\"*", docUrl: docs.terminals, placement: "bottom", }, { target: '[data-tour="lanes.workNewShell"]', - title: "Shell terminal", - body: "Use New Shell when you want direct control. It opens a terminal already pointed at this lane's worktree, so commands affect the lane you are viewing.", + title: "Plain terminal", + body: "Just a regular terminal — no AI involved. Already pointed at this lane's folder, so commands you type only affect this lane's copy.", docUrl: docs.terminals, placement: "bottom", }, { target: '[data-tour="work.laneName"]', - title: "Lane context", - body: "This label is the safety check. If it says `checkout-page`, the chats and shells here see the checkout-page branch and files. Switch lanes before starting work for another task.", + title: "A safety check", + body: "This label tells you which lane you're inside. **Always glance here before starting a chat or running a command** — whatever you do affects this lane only. Switch lanes if you wanted a different one.", docUrl: docs.lanesOverview, placement: "bottom", }, { target: '[data-tour="work.sessionCount"]', - title: "Open sessions", - body: "This count tells you how many chats or terminals are open for this lane. It is normal to have zero before you ask an agent or open a shell.", + title: "How busy is this lane?", + body: "How many chats and terminals are open inside this lane right now. Zero is normal until you start something.", docUrl: docs.chatOverview, placement: "bottom", }, { target: '[data-tour="work.viewArea"]', - title: "Session view area", - body: "Open chats and terminals appear here. If it is empty, nothing is broken; it simply means this lane has no active work session yet.", + title: "Where it all shows up", + body: "Once you start a chat or terminal, it appears here. If it's empty, that's fine — this lane just doesn't have anything running yet.", docUrl: docs.chatOverview, placement: "top", }, diff --git a/apps/desktop/src/renderer/onboarding/tours/lanesTour.ts b/apps/desktop/src/renderer/onboarding/tours/lanesTour.ts index a6e7d5a7d..73dcebb73 100644 --- a/apps/desktop/src/renderer/onboarding/tours/lanesTour.ts +++ b/apps/desktop/src/renderer/onboarding/tours/lanesTour.ts @@ -1,6 +1,10 @@ import { registerTour, type Tour } from "../registry"; import { docs } from "../docsLinks"; +// Curated to selectors that always render on /lanes regardless of which sub-pane +// (Stack / Diff / Git Actions / Work) is currently active. Sub-pane content is +// covered by dedicated tours (laneWorkPaneTour, the gitActionsPane builder, the +// Files tab) so a user can drill into any of them independently. export const lanesTour: Tour = { id: "lanes", title: "Lanes walkthrough", @@ -8,74 +12,32 @@ export const lanesTour: Tour = { steps: [ { target: '[data-tour="lanes.branchSelector"]', - title: "Your main branch", - body: "This is the branch ADE treats as the clean starting point, usually `main`. New lanes start from the current primary branch, and ADE compares each lane back to its base so you can see what changed.", + title: "The clean starting point", + body: "Every sandbox copy needs a starting point. ADE uses your project's main branch (usually `main`) for that. Each new lane copies from here, and ADE always compares the lane's changes back to it.", docUrl: docs.lanesOverview, placement: "bottom", }, { target: '[data-tour="lanes.statusChips"]', - title: "Filter by status", - body: "Status chips answer, \"What needs attention right now?\" Running means work is active, waiting means a person or worker needs to decide, and ended means the lane is done or archived.", + title: "What's going on with each lane", + body: "These badges show the status of every lane at a glance. **Running** = active work. **Waiting** = needs you (or an AI) to make a call. **Ended** = done or put away.", docUrl: docs.lanesOverview, placement: "bottom", }, { target: '[data-tour="lanes.newLane"]', - title: "Make a new lane", - body: "Use this when you want a safe place for one task. Example: create `try-new-auth-flow`, let an agent edit there, and keep primary clean until you decide the work is worth shipping.", + title: "Make or adopt a lane", + body: "Make a brand-new sandbox copy from your main project, or adopt one you already have lying around (like a Git branch you started outside ADE). Either way, your real project stays untouched.", docUrl: docs.lanesCreating, placement: "bottom", }, { - target: '[data-tour="lanes.laneTab"]', - title: "Lane tabs", - body: "Each tab is one lane, like one open workspace. Click tabs to switch tasks. Badges call out state such as uncommitted files, pinned lanes, or lanes that may need a rebase.", + target: '[data-tour="lanes.laneTab"], [data-tour="lanes.statusChips"]', + title: "Switch between lanes", + body: "Each tab is one open sandbox copy. Click to switch between tasks. Little badges call out things like \"has unsaved changes\" or \"pinned\", so you can see what each lane needs at a glance.", docUrl: docs.lanesOverview, placement: "bottom", }, - { - target: '[data-tour="lanes.newLane"]', - title: "Already have worktrees?", - body: "The New Lane menu can also bring in work you already have. If a branch/worktree already exists, ADE can adopt it as a lane instead of making you recreate the work.", - docUrl: docs.lanesCreating, - placement: "bottom", - }, - { - target: '[data-tour="lanes.stackPane"]', - title: "Stack pane", - body: "Stacks explain dependency order. Example: `primary -> checkout-page -> checkout-errors` means the checkout-errors lane depends on checkout-page, so review and shipping should happen in that order.", - docUrl: docs.lanesStacks, - placement: "right", - }, - { - target: '[data-tour="lanes.gitActionsPane"]', - title: "Git, in plain words", - body: "This pane translates Git state into actions. Dirty files need a commit. A committed lane can push. A lane with new base commits may need a rebase. The buttons enable only when that action makes sense.", - docUrl: docs.lanesOverview, - placement: "right", - }, - { - target: '[data-tour="lanes.diffPane"]', - title: "See what changed", - body: "The Diff pane is the receipt for this lane. Red lines were removed, green lines were added, and each file shows exactly what this lane changed compared with its base.", - docUrl: docs.lanesOverview, - placement: "left", - }, - { - target: '[data-tour="lanes.workPane"]', - title: "The Work pane", - body: "This is where you ask for work inside this lane. A worker chat, CLI tool, or shell launched here edits and runs commands in this lane's folder, not in primary.", - docUrl: docs.chatOverview, - placement: "left", - }, - { - target: '[data-tour="app.helpMenu"]', - title: "Help lives here", - body: "The ? button can replay this Lanes walkthrough by itself later. Standalone walkthroughs explain the screen without asking you to create or delete anything.", - docUrl: docs.welcome, - placement: "bottom", - }, ], }; diff --git a/apps/desktop/src/renderer/onboarding/tours/prsTour.test.ts b/apps/desktop/src/renderer/onboarding/tours/prsTour.test.ts new file mode 100644 index 000000000..939d313ba --- /dev/null +++ b/apps/desktop/src/renderer/onboarding/tours/prsTour.test.ts @@ -0,0 +1,126 @@ +/* @vitest-environment jsdom */ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { prsTour } from "./prsTour"; + +const FALLBACK_MS = 12_000; + +function findStep(title: string) { + const step = prsTour.steps.find((entry) => entry.title === title); + expect(step, `expected a step titled ${title}`).toBeTruthy(); + return step!; +} + +describe("prsTour", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("registers under /prs", () => { + expect(prsTour.id).toBe("prs"); + expect(prsTour.route).toBe("/prs"); + }); + + it("attaches a fallback skip path to every step that depends on a PR being selected", () => { + // Steps that target conditional content under the detail drawer must offer + // a fallback so the user can move on when no PR is selected. Steps + // anchored to the always-visible list / create button do NOT need one. + const detailDrawerStepTitles = [ + "Inside a PR", + "What's blocking me?", + "Automated tests", + "Stacked PRs", + "Closing the PR", + ]; + for (const title of detailDrawerStepTitles) { + const step = findStep(title); + expect(step.fallbackAfterMs, `${title} fallbackAfterMs`).toBe(FALLBACK_MS); + expect(step.fallbackNextLabel, `${title} fallbackNextLabel`).toBeTruthy(); + expect(step.fallbackNotice, `${title} fallbackNotice`).toBeTruthy(); + } + }); + + it("does not attach fallback fields to always-visible steps", () => { + // The list and create-PR button are always rendered on /prs, so they must + // not silently auto-skip — those are required steps. + for (const title of ["Your PR list", "Open a new PR"]) { + const step = findStep(title); + expect(step.fallbackAfterMs, `${title} fallbackAfterMs`).toBeUndefined(); + expect(step.fallbackNextLabel, `${title} fallbackNextLabel`).toBeUndefined(); + expect(step.fallbackNotice, `${title} fallbackNotice`).toBeUndefined(); + } + }); + + it("uses comma-fallback selectors so detail-drawer steps spotlight the drawer when no PR is selected", () => { + // Each step targeting a conditional element inside the drawer must list + // [data-tour="prs.detailDrawer"] as a secondary fallback, so the spotlight + // lands on the drawer container instead of failing to anchor. + const stepsWithDrawerFallback = [ + "What's blocking me?", + "Automated tests", + "Stacked PRs", + "Closing the PR", + ]; + for (const title of stepsWithDrawerFallback) { + const step = findStep(title); + expect(step.target, `${title} target`).toContain('[data-tour="prs.detailDrawer"]'); + // Comma-separated list — the drawer must come after a primary anchor. + const parts = step.target.split(",").map((part) => part.trim()); + expect(parts.length, `${title} should have at least 2 fallback selectors`).toBeGreaterThanOrEqual(2); + } + }); + + it("dispatches ade:tour-pr-detail-tab with `convergence` when entering What's blocking me?", async () => { + const step = findStep("What's blocking me?"); + expect(step.beforeEnter).toBeTruthy(); + + const events: Array<{ type: string; detail: unknown }> = []; + const handler = (event: Event) => { + events.push({ type: event.type, detail: (event as CustomEvent).detail }); + }; + window.addEventListener("ade:tour-pr-detail-tab", handler); + try { + const actions = await step.beforeEnter!(); + expect(Array.isArray(actions)).toBe(true); + const ipcAction = (actions as any[]).find((entry) => entry.type === "ipc"); + expect(ipcAction, "expected an ipc action").toBeTruthy(); + await ipcAction.call(); + expect(events).toEqual([ + { type: "ade:tour-pr-detail-tab", detail: "convergence" }, + ]); + } finally { + window.removeEventListener("ade:tour-pr-detail-tab", handler); + } + }); + + it("dispatches ade:tour-pr-detail-tab with `checks` when entering Automated tests", async () => { + const step = findStep("Automated tests"); + expect(step.beforeEnter).toBeTruthy(); + + const events: Array<{ type: string; detail: unknown }> = []; + const handler = (event: Event) => { + events.push({ type: event.type, detail: (event as CustomEvent).detail }); + }; + window.addEventListener("ade:tour-pr-detail-tab", handler); + try { + const actions = await step.beforeEnter!(); + const ipcAction = (actions as any[]).find((entry) => entry.type === "ipc"); + expect(ipcAction).toBeTruthy(); + await ipcAction.call(); + expect(events).toEqual([ + { type: "ade:tour-pr-detail-tab", detail: "checks" }, + ]); + } finally { + window.removeEventListener("ade:tour-pr-detail-tab", handler); + } + }); + + it("does not switch the detail tab from steps that do not own a tab", async () => { + // Stacked PRs and Closing are conditional content elsewhere in the drawer + // — switching the active tab from these steps would be wrong, so they + // must not have a beforeEnter that fires the tab-switch event. + for (const title of ["Stacked PRs", "Closing the PR", "Inside a PR"]) { + const step = findStep(title); + expect(step.beforeEnter, `${title} should not dispatch a tab switch`).toBeUndefined(); + } + }); +}); diff --git a/apps/desktop/src/renderer/onboarding/tours/prsTour.ts b/apps/desktop/src/renderer/onboarding/tours/prsTour.ts index 6ac913fbf..0cc71ea73 100644 --- a/apps/desktop/src/renderer/onboarding/tours/prsTour.ts +++ b/apps/desktop/src/renderer/onboarding/tours/prsTour.ts @@ -1,6 +1,12 @@ import { registerTour, type Tour } from "../registry"; import { docs } from "../docsLinks"; +// Standalone PRs walkthrough. Replayable from the ? menu. +// Switches the detail drawer's tabs in-flight via `ade:tour-pr-detail-tab` +// (handler in PrDetailPane.tsx). Each detail-tab step has a comma-fallback to +// `prs.detailDrawer` so it spotlights a sensible area when no PR is selected. +const FALLBACK_MS = 12_000; + export const prsTour: Tour = { id: "prs", title: "PRs walkthrough", @@ -8,31 +14,79 @@ export const prsTour: Tour = { steps: [ { target: '[data-tour="prs.list"]', - title: "PR list", - body: "The list shows GitHub PRs and ADE-linked lanes. Select a row before inspecting checks, convergence, or close actions.", + title: "Your PR list", + body: "A **PR** (Pull Request) is how you say *\"please pull this lane's changes into the main project\"* on GitHub. This list shows all the PRs for your project. Click any one to see its details on the right.", docUrl: docs.prsOverview, placement: "right", }, + { + target: '[data-tour="prs.createBtn"]', + title: "Open a new PR", + body: "When you've got changes in a lane you want to ship, click here. A two-step dialog asks you which lane to ship and what to call the change — ADE handles the rest.", + docUrl: docs.prsOverview, + placement: "bottom", + }, { target: '[data-tour="prs.detailDrawer"], [data-tour="prs.list"]', - title: "Checks", - body: "Checks appear inside a selected PR. They show CI state and where to look when a review is blocked.", + title: "Inside a PR", + body: "When you click a PR, this panel opens with five tabs: **Overview** (the basics), **Path to Merge** (anything blocking it), **Files** (what changed), **CI / Checks** (automated tests), **Activity** (review comments). Pick a PR row to follow along.", docUrl: docs.prsOverview, placement: "left", + fallbackAfterMs: FALLBACK_MS, + fallbackNextLabel: "Skip drawer", + fallbackNotice: "Nothing's broken — this panel just stays empty until you click a PR.", }, { - target: '[data-tour="prs.detailDrawer"], [data-tour="prs.list"]', - title: "Path to merge", - body: "The convergence tab tracks checks, review comments, conflicts, and resolver runs for the selected PR.", + target: '[data-tour="prs.conflictSim"], [data-tour="prs.detailDrawer"]', + title: "What's blocking me?", + body: "The most useful tab for an in-flight PR. It collects everything stopping it from shipping — failed tests, comments asking for changes, code conflicts — into one ordered to-do list. Work top to bottom.", docUrl: docs.prsOverview, placement: "left", + beforeEnter: async () => [{ + type: "ipc", + call: async () => { + window.dispatchEvent(new CustomEvent("ade:tour-pr-detail-tab", { detail: "convergence" })); + }, + }], + fallbackAfterMs: FALLBACK_MS, + fallbackNextLabel: "Skip Path to Merge", + fallbackNotice: "Pick a PR row to see this tab fill in.", }, { - target: '[data-tour="prs.detailDrawer"], [data-tour="prs.list"]', - title: "Queue context", - body: "When a PR belongs to a queue, this control links back to that queue.", + target: '[data-tour="prs.checksPanel"], [data-tour="prs.detailDrawer"]', + title: "Automated tests", + body: "Live results from automated tests that run every time you push code (this is called **CI**). Click any row to read the full output without bouncing over to GitHub.", + docUrl: docs.prsOverview, + placement: "left", + beforeEnter: async () => [{ + type: "ipc", + call: async () => { + window.dispatchEvent(new CustomEvent("ade:tour-pr-detail-tab", { detail: "checks" })); + }, + }], + fallbackAfterMs: FALLBACK_MS, + fallbackNextLabel: "Skip Checks", + fallbackNotice: "Tests show up once a PR is selected.", + }, + { + target: '[data-tour="prs.stackingIndicator"], [data-tour="prs.detailDrawer"]', + title: "Stacked PRs", + body: "When you split a big change into smaller PRs that build on each other, this badge shows where this one sits in the stack. Standalone PRs don't show this.", docUrl: docs.lanesStacks, placement: "left", + fallbackAfterMs: FALLBACK_MS, + fallbackNextLabel: "Skip stacking", + fallbackNotice: "Stacking only shows for PRs that build on top of another PR.", + }, + { + target: '[data-tour="prs.closeBtn"], [data-tour="prs.detailDrawer"]', + title: "Closing the PR", + body: "When the work is shipped (or you've decided to drop it), close it from here. The lane stays around in case you want to keep working on it.", + docUrl: docs.prsOverview, + placement: "top", + fallbackAfterMs: FALLBACK_MS, + fallbackNextLabel: "Skip close", + fallbackNotice: "Close is only available for open, non-merged PRs.", }, ], }; diff --git a/apps/desktop/src/renderer/onboarding/tours/runTour.ts b/apps/desktop/src/renderer/onboarding/tours/runTour.ts index 1b700068b..535d3e302 100644 --- a/apps/desktop/src/renderer/onboarding/tours/runTour.ts +++ b/apps/desktop/src/renderer/onboarding/tours/runTour.ts @@ -1,60 +1,37 @@ import { registerTour, type Tour } from "../registry"; import { docs } from "../docsLinks"; +// commandCards / runtimeBar / processMonitor host conditionally-rendered +// content (cards / live runtimes / running processes). Their wrapper divs +// always exist, so the addCommand step here points at the always-visible +// "Add command" button — the conditional surfaces become useful once the +// user has actually defined or started something. export const runTour: Tour = { id: "run", title: "Run tab walkthrough", route: "/project", steps: [ - { - target: '[data-tour="run.header"]', - title: "Run tab", - body: "Run is your process manager. Define commands once, then start, stop, and monitor them from here without ever touching a terminal.", - docUrl: docs.lanesStacks, - placement: "bottom", - }, { target: '[data-tour="run.laneSelector"]', - title: "Default lane", - body: "Processes run inside a lane's worktree. Pick which lane gets new process runs here — you can override it per-command on the card.", + title: "Where it runs", + body: "Each lane has its own copy of the project, so when you start something it runs inside *one* lane's copy. Pick the default lane here — you can override it on each command if you want.", docUrl: docs.lanesStacks, placement: "bottom", }, { target: '[data-tour="run.stackTabs"]', - title: "Stacks", - body: "A Stack is a named group of commands you always run together — like \"dev\", \"test\", or \"deploy\". Click a tab to filter to its commands, then hit Run stack.", + title: "Group commands together", + body: "A **Stack** is a name you give to a group of commands you usually run together — like \"dev\" (your dev server + watcher), \"test\" (lint + tests), or \"deploy\". Click a tab to filter, then **Run stack** to start them all at once.", docUrl: docs.lanesStacks, placement: "bottom", }, { target: '[data-tour="run.addCommand"]', title: "Add a command", - body: "Define a new process — give it a name, a shell command, environment variables, a restart policy, and a readiness check. It shows up as a card immediately.", - docUrl: docs.lanesStacks, - placement: "bottom", - }, - { - target: '[data-tour="run.commandCards"]', - title: "Command cards", - body: "Each card is one process definition. The Play button starts a fresh run; the status badge and elapsed timer reflect the latest run. Click the card to edit.", - docUrl: docs.lanesStacks, - placement: "top", - }, - { - target: '[data-tour="run.runtimeBar"]', - title: "Runtime bar", - body: "Live health checks, preview URLs, and port leases for the active lane appear here so you can open your app in one click.", + body: "Save any shell command as a clickable button: a dev server, a test runner, a build script — anything. Give it a name and the command itself, and it shows up as a card you can launch any time.", docUrl: docs.lanesStacks, placement: "bottom", }, - { - target: '[data-tour="run.processMonitor"]', - title: "Process monitor", - body: "The monitor at the bottom streams live output from every running process and open shell. Click a tab to focus it, or hit Kill to terminate the run.", - docUrl: docs.lanesStacks, - placement: "top", - }, ], }; diff --git a/apps/desktop/src/renderer/onboarding/tours/settingsTour.ts b/apps/desktop/src/renderer/onboarding/tours/settingsTour.ts index 54fa00261..fd1d6cec0 100644 --- a/apps/desktop/src/renderer/onboarding/tours/settingsTour.ts +++ b/apps/desktop/src/renderer/onboarding/tours/settingsTour.ts @@ -8,29 +8,29 @@ export const settingsTour: Tour = { steps: [ { target: '[data-tour="settings.appearance"]', - title: "Appearance", - body: "Theme, density, accent color, and contrast settings live here.", + title: "How ADE looks", + body: "Pick a theme (light, dark, high-contrast), tweak how dense the layout is, change the accent color. Cosmetic only — nothing here breaks anything.", docUrl: docs.settingsGeneral, placement: "right", }, { target: '[data-tour="settings.ai"]', - title: "AI providers", - body: "Connect Claude, OpenAI, local models, or custom endpoints. Workers use these providers per session.", + title: "Which AI to use", + body: "ADE works with several AI services — Claude, OpenAI's GPT, local models you run yourself, or your own custom endpoint. Plug in your account here, and your AI helpers (workers) will use them.", docUrl: docs.settingsGeneral, placement: "right", }, { target: '[data-tour="settings.memory"]', - title: "Memory", - body: "Inspect and prune what ADE agents remember. Pin facts, consolidate episodes, and set retention caps.", + title: "What AI remembers", + body: "Your AI helpers remember things between conversations — preferences, project notes, decisions you've made. This is where you see what they remember, pin things you want kept, or forget things you don't.", docUrl: docs.settingsGeneral, placement: "right", }, { target: '[data-tour="settings.laneTemplates"]', - title: "Lane templates", - body: "Save reusable lane recipes with commands, runtimes, and setup defaults.", + title: "Reusable lane recipes", + body: "Save a lane setup as a **template** — its tools, scripts, runtime — so the next time you make a lane, you can apply the recipe in one click instead of setting it up again.", docUrl: docs.settingsGeneral, placement: "right", }, diff --git a/apps/desktop/src/renderer/onboarding/tours/workTour.ts b/apps/desktop/src/renderer/onboarding/tours/workTour.ts index 436ecb5dc..f53f12a97 100644 --- a/apps/desktop/src/renderer/onboarding/tours/workTour.ts +++ b/apps/desktop/src/renderer/onboarding/tours/workTour.ts @@ -1,6 +1,9 @@ import { registerTour, type Tour } from "../registry"; import { docs } from "../docsLinks"; +// The work.sessionItem anchor only mounts once a session card is rendered, so +// it's covered by the tutorial's interactive `act4.newSession` step rather than +// this passive walkthrough. export const workTour: Tour = { id: "work", title: "Work tab walkthrough", @@ -8,46 +11,32 @@ export const workTour: Tour = { steps: [ { target: '[data-tour="work.sessionsPane"]', - title: "All sessions, one place", - body: "Every session across every lane lives here. Unlike the Lane Work Pane, this view isn't scoped to one lane.", + title: "Every conversation in one list", + body: "All your AI chats and terminal windows show up here, no matter which lane they're in. Each one is called a **session** — just one open conversation or one open terminal.", docUrl: docs.chatOverview, placement: "right", }, { target: '[data-tour="work.laneFilter"]', - title: "Filter by lane", - body: "Filter sessions by lane or show them all. Handy when you have workers running across several worktrees.", + title: "Narrow the list", + body: "Got a lot going on? Filter the list down to just one lane's conversations. Useful once you have AI working in several lanes at once.", docUrl: docs.lanesOverview, placement: "bottom", }, { target: '[data-tour="work.newSession"]', - title: "Start a new session", - body: "Start a new chat, CLI tool, or shell from here. The session attaches to whichever lane you pick.", + title: "Start a new conversation", + body: "Open a new AI chat, command-line tool, or terminal window. The new session attaches to whichever lane you pick — and only sees that lane's files.", docUrl: docs.chatOverview, placement: "bottom", }, - { - target: '[data-tour="work.sessionItem"]', - title: "Open a session", - body: "Click a session to open its view. Right-click for rename, resume, delete, and other actions.", - docUrl: docs.chatOverview, - placement: "right", - }, { target: '[data-tour="work.viewArea"]', - title: "Session view area", - body: "Open chats and terminals live here. Drag tabs to rearrange, close a tab to reclaim space.", + title: "Where it all shows up", + body: "Whatever you've opened — chats, terminals — appears here. Drag tabs to rearrange them, close one to clean up. The list on the left always shows everything, even closed ones.", docUrl: docs.terminals, placement: "left", }, - { - target: '[data-tour="work.crossLaneSwitch"]', - title: "Cross-lane viewing", - body: "Switching sessions here doesn't change your active lane — just what you're looking at. Jump to the lane via right-click → Go to lane.", - docUrl: docs.lanesOverview, - placement: "right", - }, ], }; diff --git a/apps/desktop/src/shared/adeCliGuidance.ts b/apps/desktop/src/shared/adeCliGuidance.ts index 121cc15be..1c63c2dc6 100644 --- a/apps/desktop/src/shared/adeCliGuidance.ts +++ b/apps/desktop/src/shared/adeCliGuidance.ts @@ -1,8 +1,9 @@ export const ADE_CLI_AGENT_GUIDANCE = [ "## ADE CLI", - "`ade` is available in this ADE-managed session for internal ADE work: lanes, missions, PRs, chats/sessions, memory, proof, config, and process state.", + "`ade` should be available in this ADE-managed session for internal ADE work: lanes, missions, PRs, chats/sessions, memory, proof, config, and process state.", + "If `command -v ade` fails, try `${ADE_CLI_PATH:-}` when set, then `${ADE_CLI_BIN_DIR:-}/ade`, and in an ADE source checkout fall back to `node apps/ade-cli/dist/cli.cjs ...` after confirming the file exists.", "Before saying an ADE task is blocked or unsupported, try `ade` first: run `ade doctor` if needed, use typed commands like `ade lanes list --text` / `ade prs checks --text`, or discover with `ade actions list --text` and `ade actions run ...`.", ].join("\n"); export const ADE_CLI_INLINE_GUIDANCE = - "`ade` is available for ADE tasks. Before reporting an ADE lane, mission, PR, session, memory, proof, config, or process-state task as blocked, try `ade doctor`, typed `ade ... --text` commands, or `ade actions list --text` / `ade actions run ...`."; + "`ade` should be available for ADE tasks. If `command -v ade` fails, try `${ADE_CLI_PATH:-}`, then `${ADE_CLI_BIN_DIR:-}/ade`, and in an ADE source checkout `node apps/ade-cli/dist/cli.cjs ...` after confirming it exists. Before reporting an ADE lane, mission, PR, session, memory, proof, config, or process-state task as blocked, try `ade doctor`, typed `ade ... --text` commands, or `ade actions list --text` / `ade actions run ...`."; diff --git a/apps/ios/ADE/App/DeepLinkRouter.swift b/apps/ios/ADE/App/DeepLinkRouter.swift index 074e5b849..8a27c5f87 100644 --- a/apps/ios/ADE/App/DeepLinkRouter.swift +++ b/apps/ios/ADE/App/DeepLinkRouter.swift @@ -42,6 +42,10 @@ final class DeepLinkRouter { post(kind: "session", identifier: sessionId) return } + if let prId = userInfo["prId"] as? String, !prId.isEmpty { + post(kind: "pr", identifier: prId) + return + } if let pr = userInfo["prNumber"] { let identifier = "\(pr)" guard !identifier.isEmpty else { return } @@ -55,6 +59,32 @@ final class DeepLinkRouter { object: nil, userInfo: ["kind": kind, "identifier": identifier] ) + if kind == "pr", let prId = resolvePrId(from: identifier) { + SyncService.shared?.requestedPrNavigation = PrNavigationRequest(prId: prId) + } + } + + /// PR deep links carry either a numeric PR number (from `ade://pr/` + /// widget/live-activity URLs) or a stable `prId` (from notification payloads + /// that include both). Resolve the number to the matching `prId` via the + /// App Group workspace snapshot so navigation always uses the same + /// identifier as `PrsRootScreen`. + private func resolvePrId(from identifier: String) -> String? { + let trimmed = identifier.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + if let number = Int(trimmed) { + // Pure-number deep links (e.g. `ade://pr/123`) must look up the canonical + // prId from the workspace snapshot. If we can't, returning the numeric + // string would be stored as a `prId` in `PrNavigationRequest` and silently + // fail to match any PR in `PrsRootScreen`. Fall back to nil so callers can + // degrade gracefully (e.g., the number-based notification path). + guard let snapshot = ADESharedContainer.readWorkspaceSnapshot(), + let match = snapshot.prs.first(where: { $0.number == number }) else { + return nil + } + return match.id + } + return trimmed } } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cdd7d21fd..b92eed703 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -95,6 +95,17 @@ bridge. plus an `ade-cli-install-path.cmd` helper alongside the bundled Node runtime so that `ade` works from a normal Windows shell without a global Node install. See §14.4 for the packaging flow. +- **Install + PATH wiring (`adeCliService`)** — on macOS / Linux the + desktop installer drops the launcher at `$HOME/.local/bin/ade`; on + Windows it lands at `%LOCALAPPDATA%\ADE\bin\ade.cmd`. After a + successful install on POSIX, `ensureUserBinOnShellPath` appends a + marked `export PATH="$HOME/.local/bin:$PATH"` block to the user's + shell rc (`.zshrc` for zsh, `.bashrc` for bash, `.profile` otherwise) + iff (a) the install dir isn't already on the inherited `PATH` and + (b) the file doesn't already contain the marker / line / target dir. + The install IPC reply tells the renderer which profile was edited + so the Settings/Onboarding UI can prompt the user to open a new + terminal or `source` it. - **Session identity** — the CLI resolves caller role from ADE context environment variables and command flags. Role vocabulary: `cto`, `orchestrator`, `agent`, `external`, `evaluator`. diff --git a/docs/features/agents/README.md b/docs/features/agents/README.md index 6c50fa973..11f6d9084 100644 --- a/docs/features/agents/README.md +++ b/docs/features/agents/README.md @@ -23,6 +23,8 @@ registry / ADE CLI integration that all three share. | `apps/desktop/src/main/services/agentTools/agentToolsService.ts` | Detects external CLI tools (Claude Code, Codex, Cursor, Aider, Continue) on PATH. | | `apps/ade-cli/src/cli.ts` | Agent-focused `ade` command surface and text/JSON output formatters. | | `apps/ade-cli/src/adeRpcServer.ts` | Private ADE action RPC: registers actions, handles JSON-RPC, applies session-identity-based filtering. | +| `apps/desktop/src/main/services/cli/adeCliService.ts` | Desktop-side install / status / uninstall surface for the `ade` launcher. Owns the install-target path resolution and the optional shell-rc PATH append. | +| `apps/desktop/src/shared/adeCliGuidance.ts` | Canonical agent-prompt guidance for finding and using `ade` (env var fallback chain + "try `ade doctor` before declaring blocked"). | | `apps/desktop/src/shared/ctoPersonalityPresets.ts` | CTO personality overlays (`strategic`, `professional`, `hands_on`, `casual`, `minimal`, `custom`). | | `apps/desktop/src/shared/types/agents.ts` | `AgentIdentity`, `AgentCoreMemory`, `AgentRole`, `AdapterType`, adapter configs. | | `apps/desktop/src/shared/types/cto.ts` | `CtoIdentity`, `CtoCoreMemory`, `CtoCapabilityMode`, `CtoPersonalityPreset`. | diff --git a/docs/features/agents/tool-registration.md b/docs/features/agents/tool-registration.md index 90a092afb..8623dbbb8 100644 --- a/docs/features/agents/tool-registration.md +++ b/docs/features/agents/tool-registration.md @@ -18,6 +18,8 @@ filtering before exposing the final list. | `apps/desktop/src/main/services/ai/tools/` | In-process tool implementations (universal, workflow, CTO operator, Linear). | | `apps/desktop/src/main/services/orchestrator/coordinatorTools.ts` | Coordinator tool set for the mission orchestrator. | | `apps/desktop/src/main/services/agentTools/agentToolsService.ts` | External CLI detection (Claude Code, Codex, Cursor, Aider, Continue). | +| `apps/desktop/src/main/services/cli/adeCliService.ts` | Desktop-side CLI install / status / uninstall. Resolves the launcher target (`$HOME/.local/bin/ade` on POSIX, `%LOCALAPPDATA%\ADE\bin\ade.cmd` on Windows) and, on POSIX install, appends a marked `export PATH=...` block to the user's shell rc when the install dir isn't already on `$PATH`. | +| `apps/desktop/src/shared/adeCliGuidance.ts` | `ADE_CLI_AGENT_GUIDANCE` + `ADE_CLI_INLINE_GUIDANCE` strings injected into agent system prompts. Tells the agent how to find `ade` (PATH → `$ADE_CLI_PATH` → `$ADE_CLI_BIN_DIR/ade` → `node apps/ade-cli/dist/cli.cjs ...`) and to try `ade doctor` / typed commands / `ade actions list` before reporting an ADE task as blocked. | ## Two-path tool dispatch @@ -224,6 +226,27 @@ Both modes expose the same action protocol and output formatters. Agent prompts should prefer documented commands such as `ade lanes list`, `ade prs path-to-merge`, or the generic `ade actions run `. +### Agent-prompt fallbacks for missing `ade` on PATH + +`apps/desktop/src/shared/adeCliGuidance.ts` is the canonical text the +chat / agent system prompt embeds whenever a session has CLI access. +It tells the agent that `ade` *should* be available, and gives it an +ordered fallback chain when `command -v ade` fails: + +1. try `${ADE_CLI_PATH:-}` (set by managed shells when the launcher + path is known up front), +2. then `${ADE_CLI_BIN_DIR:-}/ade` (set when only the install dir is + known), +3. and as a last resort, in an ADE source checkout, `node + apps/ade-cli/dist/cli.cjs ...` after confirming the file exists. + +The wording explicitly tells agents to try `ade doctor`, typed +`ade ... --text` commands, and `ade actions list --text` / +`ade actions run ...` *before* claiming an ADE task is blocked. The +two exports (`ADE_CLI_AGENT_GUIDANCE` for full system-prompt builds, +`ADE_CLI_INLINE_GUIDANCE` for inline mentions) keep this guidance +consistent across surfaces. + ## Fragile and tricky wiring - **Identity must come from env or trusted CLI flags.** A rogue client diff --git a/docs/features/onboarding-and-settings/README.md b/docs/features/onboarding-and-settings/README.md index ecf03f42a..0b2f8710d 100644 --- a/docs/features/onboarding-and-settings/README.md +++ b/docs/features/onboarding-and-settings/README.md @@ -114,10 +114,23 @@ Renderer — onboarding: `prCreateModal`); kept separate from the per-surface tour files so dialog-scoped steps can be composed from multiple tours. - `apps/desktop/src/renderer/onboarding/tours/*.ts` — per-surface tours: - `lanesTour`, `laneWorkPaneTour` (new), `workTour`, `filesTour`, + `lanesTour`, `laneWorkPaneTour`, `workTour`, `filesTour`, `runTour`, `missionsTour`, `prsTour`, `graphTour`, `historyTour`, `automationsTour`, `ctoTour`, `settingsTour`, plus the first-session - `firstJourney` tour. + `firstJourneyTour`. The first-session tour reuses individual steps + from the per-surface tours via a small `tutorialSection(sectionId, + steps, requires)` wrapper that namespaces step ids + (`.`), forces a `requires` gate, derives + `waitForSelector` from `target`, and — for any step that has a + `requires` gate without its own `fallbackAfterMs` — injects a + default 30 s `Skip` fallback so the tutorial can never get + permanently stuck waiting on state that doesn't appear. The acts + themselves are intentionally streamlined: act 1 only borrows the + base-branch / status-chip / lane-work-pane bits (since the user has + just created a lane interactively); acts 2 + 3 inline ctx-aware + graph/files steps directly rather than spreading the full sub-tour; + the per-act "tab handoff" reminder steps were collapsed into the + single act 12 finale. - `apps/desktop/src/renderer/components/cto/...` — CTO first-run is a separate lightweight wizard covering identity, project context, and optional Linear (see `apps/desktop/src/renderer/components/cto/`). @@ -128,7 +141,16 @@ Renderer — settings: container with eight top-level sections. - `apps/desktop/src/renderer/components/settings/GeneralSection.tsx` — theme, AI mode, task routing, terminal preferences, keybindings - link. + link, and the embedded `AdeCliSection` (compact form) so the most + common terminal-CLI install/repair affordance lives next to the + other day-one settings without forcing a tab switch into + Integrations. +- `apps/desktop/src/renderer/components/settings/AdeCliSection.tsx` + — surfaces `ade.cli.getStatus` / `ade.cli.install` / `ade.cli.uninstall`. + In compact form (used by `GeneralSection` and the onboarding + `DevToolsSection`) it shows the current install path, an + Install / Repair button, and a "Add to PATH" hint when the install + target isn't on the user's `$PATH`. - `apps/desktop/src/renderer/components/settings/WorkspaceSettingsSection.tsx` + `ProjectSection.tsx` — project identity, base ref, paths. - `apps/desktop/src/renderer/components/settings/ContextSection.tsx` @@ -213,7 +235,7 @@ is changing rather than which service backs it: | Tab | Section file | What lives here | |---|---|---| -| General | `GeneralSection.tsx` | Theme, AI mode, task routing, terminal preferences (font size, line height, scrollback), keybindings link | +| General | `GeneralSection.tsx` (embeds `AdeCliSection` in compact form) | Theme, AI mode, task routing, terminal preferences (font size, line height, scrollback), keybindings link, and the `ade` CLI install / status surface | | Workspace | `WorkspaceSettingsSection.tsx`, `ProjectSection.tsx`, `ContextSection.tsx` | Project identity, context docs, skill files | | AI | `AiSettingsSection.tsx`, `AiFeaturesSection.tsx`, `ProvidersSection.tsx` | Provider CLIs, models, AI feature flags | | Sync | `SyncDevicesSection.tsx` | Multi-device sync, host-role transfer, peer status, pairing PIN, Tailscale tailnet discovery | diff --git a/docs/features/pull-requests/README.md b/docs/features/pull-requests/README.md index 9f3186b69..3a2551568 100644 --- a/docs/features/pull-requests/README.md +++ b/docs/features/pull-requests/README.md @@ -183,6 +183,17 @@ well as system notifications. The event payload includes `prTitle`, `repoOwner`, `repoName`, `baseBranch`, `headBranch` so consumers can format context-aware messages themselves. +In-app, the App Shell renders these events as PR toasts. Their +"View PR" action now navigates straight into the PR detail drawer +on `/prs` via `buildPrsRouteSearch`, with `selectedPrId` set to the +event's PR id and `detailTab` chosen from the event kind: +`checks_failing` → `checks`, `changes_requested` / +`review_requested` → `activity`, everything else → drawer overview. +This replaces the older "select lane + open lane inspector merge +tab" route, which depended on the lane being currently focused and +forced the user to leave the PRs surface to follow up on a PR +event. + ## PR context loading The PR page no longer assumes every tab loads every workflow query: diff --git a/docs/features/sync-and-multi-device/ios-companion.md b/docs/features/sync-and-multi-device/ios-companion.md index 6625b4407..e130e6fa7 100644 --- a/docs/features/sync-and-multi-device/ios-companion.md +++ b/docs/features/sync-and-multi-device/ios-companion.md @@ -21,6 +21,9 @@ apps/ios/ │ │ │ # setup, response/action routing, deep-link dispatch │ │ ├── ContentView.swift # slim 6-tab TabView │ │ ├── DeepLinkRouter.swift # ade://session/ + ade://pr/ URL handler +│ │ │ # plus notification userInfo dispatch +│ │ │ # (sessionId / prId / prNumber → prId via +│ │ │ # WorkspaceSnapshot lookup) │ │ └── NotificationCategories.swift # UNNotificationCategory / UNNotificationAction set │ ├── Models/ │ │ ├── RemoteModels.swift # Codable structs mirroring shared types @@ -379,10 +382,19 @@ over Apple Push Notification service. The full stack is implemented: `sessionId` or `prNumber` so the OS groups banners per session/PR, and raises `interruptionLevel` / `relevanceScore` based on category. -- `DeepLinkRouter.swift` — `ade://` URL handler. Parses - `ade://session/` and `ade://pr/` and posts +- `DeepLinkRouter.swift` — `ade://` URL handler and notification- + payload dispatcher. Parses `ade://session/` and + `ade://pr/` URLs, plus notification `userInfo` bags carrying + `sessionId`, `prId`, or `prNumber`, and posts `.adeDeepLinkRequested` to `NotificationCenter` so individual tab - views can flip their selection. + views can flip their selection. PR deep links specifically resolve + to a stable `prId`: when the inbound identifier is the GitHub PR + number (from a widget / Live Activity URL or a legacy `prNumber` + payload), `resolvePrId` looks it up against the App Group + `WorkspaceSnapshot` and stashes the matching id on + `SyncService.requestedPrNavigation` (a `PrNavigationRequest`) so + `PrsRootScreen` opens the same row the desktop would. When the + payload already carries a `prId`, that is used verbatim. **Notification preferences** (`apps/ios/ADE/Models/NotificationPreferences.swift`):