Phase 6.5: first-run setup wizard and a tour that follows what was set up #12
Loading…
Reference in a new issue
No description provided.
Delete branch "refactor/phase-6-5-setup"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 6.5 of the DDD/CQRS refactor: a first-run setup wizard and a reworked guided tour. Based on
main.What changes
The steps come from the kinds of plugin. Each contribution kind in the plugin host can declare a setup step: runtimes, trackers and channels do today.
Setup.planputs those between the app's own steps (welcome, first project, done), and each step lists the plugins that offer that kind. The app has a screen for each step it knows. An unknown step, such as a future kind's, renders a generic screen: the plugins of that kind with their switch, permissions and Configure form. So a new kind of plugin gets a step without any wizard code./setuphas its own layout and no app shortcuts or tour. Its steps:Every step can be skipped, and Settings › General can run the setup again.
Who sees it. The new
setupsettings section records the version of the setup the user finished. A fresh install is redirected to/setupby a 302 at the first render, so nothing flickers. A later release that adds a step shows only the new step. Migration 0013 marks installs that already have a project or a finished tour as through version 1, so an upgrade never shows the wizard.The runtime is a setting. Sessions run on
agents.runtime, falling back to Claude Code when that plugin is off. Adding it surfaced a bug, now fixed: the server's settings cleanup and the General form both rebuilt the agents section field by field, so they would have reset the runtime on every save.The tour is data with conditions. A step that needs something to exist (a project, a tracker, a channel) is skipped when it doesn't, and the progress counts only the steps shown. The tour no longer starts by itself: the setup's last step and Settings start it. Every step was checked against today's screens.
The plugin card is a shared molecule.
PluginOptionRowis now used both in Settings and in the wizard.Checks
/setup. The walk-through covered switching the language, Claude Code detected, a Redmine connection tested against a fake Redmine, channels turned off, and a project added. The tour that followed left out the notifications step, because the channels were off./answers 200 instead of a redirect, and the setup is recorded as through version 1.Known and left
🤖 Generated with Claude Code
A fresh install is sent to /setup (middleware/setup.global.ts). It asks the server once per app load, during the server render when there is one, so the redirect is a 302 and nothing flashes first; it never decides from the defaults the shared settings hold before the server answers. The same rule sends an upgraded install to the steps a release added, and only those. The page is thin: SetupTemplate (a step rail with done/skipped marks, the step's title, Back / Skip / Next, Finish on the last step, Skip the rest) around one organism per step the app knows: welcome (language, applied at once as in Settings, and the theme picker), runtimes (each runtime plugin with what was found and how to install it, the runtime tasks run on, the planning and review models), trackers (tracker plugins, a first connection with its check), channels (channel plugins with their state and Test), project (a first project with its workflow, linking a tracker project), done (what was set up; the tour or the board). A step with an id the app does not know, which a future kind of plugin brings, gets SetupPluginsStep: its plugins with permissions, switch and Configure. The forms that read their own data mount on first use, so the steps stay storybook-able. The setup has its own layout (no header, tour or keyboard help) and turns the app-wide shortcuts off through `definePageMeta({ shortcuts: false })`. Settings › General can run it again with every step (`/setup?all=1`). Every string is in the catalogues, English and Spanish, including the step keys the server's plan names and the runtime problem keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>packages/{core,contracts,infra-sqlite,plugin-sdk} and plugins/* become workspaces (@kanbot/core, @kanbot/contracts, @kanbot/infra-sqlite, @kanbot/plugin-sdk, @kanbot/plugin-<id>), each with the dependencies it imports and nothing else: effect pinned at the repo's exact version, the workspace packages as "*" (npm links a workspace whose version satisfies the range; it has no workspace: protocol). Only the SDK is not private. The plugins take effect and the SDK as peers: the host supplies the one copy of each, which is also what a prebuilt plugin bundle leaves external. zod and the Claude Agent SDK leave the root for @kanbot/plugin-claude-code, the only code that imports them. A plugin's package version is its manifest's. Each package's exports point at its TypeScript sources: the app, Nitro, vitest and Storybook compile them as they did, through the workspace symlinks. Imports still use relative paths; the next commit switches them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Cookies are not scoped by port. The packaged app set its per-launch cookie (kanbot_local) on 127.0.0.1, and after the tray's "Open in browser" the system browser had it too: it then sent it to every server on 127.0.0.1, whatever the port, for instance a dev server an agent started from a cloned repository's code, which could log it (and drive Kanbot with it) or overwrite it with a Set-Cookie of its own. The shell now makes a name for this machine once per install, kanbot-<16 hex>.localhost (kept 0600 in the user-data folder), and the window, its cookie and the browser handoff all use it; the shell's own REST calls and boot poll stay on 127.0.0.1 with the Bearer header. The name goes to the server with the token over the message port (it is not secret, but must be known), and hostIsLoopback accepts exactly that name besides the loopback names; the server takes no name the shell could not have made (/^kanbot-[a-z0-9]{12,32}\.localhost$/). Nothing changes in how the server listens: it still binds 127.0.0.1. Browsers resolve *.localhost to this machine by themselves (Chromium, Firefox), and Safari asks macOS's resolver, which does too (getaddrinfo answers 127.0.0.1 and ::1 here), so the handoff needs no fallback to 127.0.0.1. Checked in headless Chrome against a server bound to 127.0.0.1: the name resolves, and a cookie set under it is not sent to 127.0.0.1 or localhost on another port. Remaining: the cookie still goes to the same name on another port, so a program that reads the name from the user-data folder and gets the browser to open it on its own port could still read the cookie; it is one more step than before, not a wall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>vue-i18n reads @ as a linked message, so plugins.problem.noMetadata and undecorated failed to compile and took the dev page down with them. They are reworded rather than escaped as {'@'}, which the server's translate() would print verbatim. A test compiles every message, the plugins' included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Any code from Settings › Devices could pair a node, so a code offered for someone's phone could become a node the hub sends prompts to. An offer is now for a node or, by default, for a browser or a script (Devices.offerPairing takes an optional { kind }; no payload still works), and redeeming it as the other kind fails like a wrong code (it counts towards burning it; every failure is the same PairingRefused / 401). Settings › Devices' pairing card has a "For a node" switch: it asks for a node's code and shows the kanbot-node pair command to run there instead of the address and QR code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>A code offered for a desktop (Devices.offerPairing({ kind: 'desktop' })) pairs two devices in one transaction: the window's browser device (its token is the kanbot_device cookie, Secure over HTTPS) and the node it runs (its token in the answer: { deviceId, nodeDeviceId, nodeToken }). devices.companion_id (migration 0022) points each at the other. Revoking either revokes both and cuts both off. A code for another kind fails like a wrong one. Settings › Devices offers the three kinds and shows a desktop as one entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Over the utility process's parent port ({ type: 'kanbot-shell', action: 'open-connect' }), only from the desktop app's own window (a Local caller) and only when the shell started the server to talk over that port; anything else is ShellUnavailable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>