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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ dev-coach/
│ │ ├── share.ts share-fetch.ts # lesson sharing: payload, code/link/.devcoach.md codecs, parseSharedInput; URL fetch
│ │ ├── claude-history.ts # cross-project stack scan of ~/.claude (projects map, manifests, activity, memories)
│ │ ├── courses.ts # courses: directory + document validation over the db.ts rows (see Courses below)
│ │ ├── runtime.ts # where devcoach runs from: classifyPath (channel × Node manager × OS), describeRuntime, the stderr startup line
│ │ ├── runtime.ts # where devcoach runs from: classifyPath (channel × Node manager × OS), describeRuntime, the stderr startup line,
│ │ │ # doctorHint (channel → the diagnostic the dashboard's first-run / empty states show)
│ ├── mcp/server.ts # McpServer: 25 tools + 11 resources + devcoach_instructions prompt
│ ├── cli/commands.ts # Commander dispatcher (35 subcommands: 27 visible + 8 hidden hooks) + term.ts (colours, tables, OSC 8 link()) + open.ts (browser)
│ │ # install.ts (install/uninstall/doctor) + mcp-command.ts (resolveCommand, chooseServerCommand, checkServerCommand)
Expand Down
22 changes: 19 additions & 3 deletions docs/usage/web-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,17 @@ Changes save immediately as you edit.

**View mode**: topic names are clickable links that filter the lessons page to show only lessons for that topic.

**Stats bar** (top of page): `N lessons total · N / max today · N this week`, then whether a lesson is available now — or why not.
**Stats bar** (top of page, once there is a lesson): `N lessons total · N / max today · N this week`, then whether a lesson is available now — or why not.

**Before the first lesson** the page says what to do next instead of showing a blank map:

- **Not set up yet** (no topic at all): a *Set up devcoach* block — say *"set up devcoach"* to your
agent (with the Claude Code plugin, `/devcoach:setup`), then keep working. It also gives the one
command that checks the wiring for the way you installed devcoach: `devcoach doctor` for Homebrew
and global npm installs, `/devcoach:setup` under the plugin, `npx -y devcoach doctor` otherwise.
- **Set up, no lesson yet**: the map, with one line above it — the first lesson arrives after real
work in your agent, at most `max_per_day` a day, `min_gap_minutes` apart (both read from
Settings) — and a link to what a lesson looks like.

<ThemedShot
alt="Knowledge map"
Expand All @@ -100,8 +110,14 @@ Filterable, sortable table of all delivered lessons.
- Shared — All lessons / 👤 My own / 🤝 Shared with me / 🤝 from `<name>` (one entry per sender)
- Free-text search (also matches the sender of a shared lesson)

Active filters show as chips above the table; when nothing matches you get "No lessons match the
current filters."
Active filters show as chips above the table. When they hide every lesson, the page names them
(*No lessons match 🔍 "kafka" in 🕐 Last 7 days.*) and offers **Search all time** (drops the period
or date range, keeps the rest — shown only when one is set) and **Clear every filter**.

With no lesson at all and no filter, the page explains how lessons arrive instead: an example of a
lesson (greyed out, so you recognise the shape when the first one comes), the diagnostic command
for your install with a **Copy** button, and the pacing from Settings. A lesson someone shared with
you can be added right away with **+ Import** — it never counts against the daily limit.

**Sort:** click the Date, Topic, Title, Level or Feedback column header. Ascending or descending.

Expand Down
19 changes: 19 additions & 0 deletions src/core/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,25 @@ export function describeRuntime(inputs: RuntimeInputs = {}): RuntimeInfo {
};
}

/** How a user checks this install's wiring, and where they type it. */
export interface DoctorHint {
command: string;
/** A shell, or the Claude Code prompt (the plugin's own command). */
where: "terminal" | "claude-code";
}

/**
* The one diagnostic a page can point at for this install: the plugin has `/devcoach:setup`
* (runtime → server → profile), Homebrew and npm-global put `devcoach` on the PATH, and every
* other channel (npx, the Desktop extension, Gemini, a checkout) has no `devcoach` to call.
*/
export function doctorHint(channel: Channel): DoctorHint {
if (channel === "claude-plugin") return { command: "/devcoach:setup", where: "claude-code" };
if (channel === "homebrew" || channel === "npm-global")
return { command: "devcoach doctor", where: "terminal" };
return { command: "npx -y devcoach doctor", where: "terminal" };
}

/** `npm-global, bound to Node v26.5.0 (nvm)` — the install, and what it depends on. */
export function describeWhere(where: PathClass): string {
const parts: string[] = [where.channel];
Expand Down
20 changes: 17 additions & 3 deletions src/web/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as db from "../core/db";
import { detectGitUserName } from "../core/git";
import { hasCode, hasHighlighter, highlighterBlock } from "../core/highlighter";
import type { KnowledgeEntry, Lesson, UiHome } from "../core/models";
import { type Channel, describeRuntime, doctorHint } from "../core/runtime";
import {
buildSharePayload,
decodeShareCode,
Expand Down Expand Up @@ -157,10 +158,14 @@ function uiTheme(): string {
export interface AppOptions {
/** Invoked by POST /shutdown after the response is sent (startUi wires the graceful stop). */
onShutdown?: () => void;
/** How this devcoach was installed (default: the running process) — picks the diagnostic
* command the first-run pages show. */
channel?: Channel;
}

export function createApp(opts: AppOptions = {}): Hono {
const app = new Hono();
const doctor = doctorHint(opts.channel ?? describeRuntime().where.channel);

// A write refused because a newer devcoach upgraded the database: say so, with both versions,
// instead of a bare 500. Everything else keeps Hono's default behaviour.
Expand Down Expand Up @@ -248,7 +253,8 @@ export function createApp(opts: AppOptions = {}): Hono {
allGroups: profile.groups.map((g) => g.name),
stats,
rateLimit,
maxPerDay: settings.max_per_day,
pacing: { maxPerDay: settings.max_per_day, minGapMinutes: settings.min_gap_minutes },
doctor,
uiTheme: settings.ui_theme,
}),
);
Expand Down Expand Up @@ -410,7 +416,9 @@ export function createApp(opts: AppOptions = {}): Hono {
allBranches: db.getDistinctColumn(conn, "branch"),
allCommits: db.getDistinctColumn(conn, "commit_hash"),
allSharedBy: db.listSharedBy(conn),
theme: db.getSettings(conn).ui_theme,
settings: db.getSettings(conn),
// Only the no-lesson state asks; an empty knowledge table means onboarding never ran.
setUp: total > 0 || Object.keys(db.getAllKnowledge(conn)).length > 0,
};
});

Expand Down Expand Up @@ -450,7 +458,13 @@ export function createApp(opts: AppOptions = {}): Hono {
perPage: PER_PAGE,
total: data.total,
totalPages: data.totalPages,
uiTheme: data.theme,
uiTheme: data.settings.ui_theme,
setUp: data.setUp,
pacing: {
maxPerDay: data.settings.max_per_day,
minGapMinutes: data.settings.min_gap_minutes,
},
doctor,
}),
);
});
Expand Down
Loading
Loading