claude-mission-control 1.11.1 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,12 @@
1
- # Claude Dashboard
1
+ # Claude Mission Control
2
2
 
3
3
  One web page that shows you everything Claude Code is doing on your machine.
4
4
 
5
- Claude Code runs in a terminal. Once you have two or three sessions going, you lose track. Which one is waiting for an answer? What did that session change while you were away? How much has this week cost? Claude Dashboard answers those questions on one screen, updated live, from the files Claude Code already writes to `~/.claude`.
5
+ **[Try the live demo →](https://jonimmswordpressdev.github.io/claude-dashboard/)** Click around with invented projects and sessions before you install anything.
6
+
7
+ ![A session flips to NEEDS YOU on the board, a notification names the command waiting for permission, and one click opens the transcript at that request](docs/screenshots/demo.gif)
8
+
9
+ Claude Code runs in a terminal. Once you have two or three sessions going, you lose track. Which one is waiting for an answer? What did that session change while you were away? How much has this week cost? Claude Mission Control answers those questions on one screen, updated live, from the files Claude Code already writes to `~/.claude`.
6
10
 
7
11
  **What you get**
8
12
 
@@ -13,6 +17,7 @@ Claude Code runs in a terminal. Once you have two or three sessions going, you l
13
17
  - Cost per session, project, model, and week, estimated from the tokens in your transcripts, with a breakdown by model and cache use.
14
18
  - Per project, the plugins, agents, skills, and MCP servers Claude Code would actually use there, and the exact setting to switch each one on or off.
15
19
  - A macOS notification the moment a session needs you, and a reminder if you leave it waiting.
20
+ - When you will hit your 5-hour or weekly limit at your current pace, before you hit it.
16
21
 
17
22
  **Why you would want it**
18
23
 
@@ -26,10 +31,17 @@ Claude Code runs in a terminal. Once you have two or three sessions going, you l
26
31
 
27
32
  **Requirements:** macOS or Linux (Windows experimental, see below), Node.js ≥ 18, Claude Code.
28
33
 
29
- ![Claude Dashboard, dark mode: three live sessions with live cost, burn rate and context meters, two of them waiting with the question shown, plus the pinned strip, unpushed work, and today's digest](docs/screenshots/dashboard-dark.png)
34
+ ![Claude Mission Control, dark mode: three live sessions with live cost, burn rate and context meters, two of them waiting with the question shown, the limit forecast on the session meter, plus the pinned strip, unpushed work, and today's digest](docs/screenshots/dashboard-dark.png)
30
35
 
31
36
  *Every screenshot here is demo data (`CLAUDE_DASH_DEMO=1`). The projects are invented.*
32
37
 
38
+ **New in 1.12.0**
39
+
40
+ - A limit forecast on the session and week meters: an amber `→ 3:40 PM` when your current pace runs out before the reset, and one notification per window.
41
+ - A share card for your week: a PNG with totals, daily bars, and what your usage was worth against your plan's price. Totals only, no project names.
42
+ - A Claude Code plugin: `/plugin install mission-control --marketplace JonImmsWordpressDev/claude-dashboard`, then `/mission-control:open`.
43
+ - A statusline, `claude-dashboard statusline`: model, context, limits and cost, plus sessions waiting on you and the forecast when the dashboard is up.
44
+
33
45
  **New in 1.11.0**
34
46
 
35
47
  - Live cost and a `$/h` burn rate on every running session, and a cost breakdown by model, cache use, and subagents in each transcript.
@@ -65,6 +77,33 @@ brew install claude-dashboard
65
77
  brew services start claude-dashboard # always-on, starts at login
66
78
  ```
67
79
 
80
+ ## Claude Code plugin
81
+
82
+ Install it from inside Claude Code:
83
+
84
+ ```
85
+ /plugin install mission-control --marketplace JonImmsWordpressDev/claude-dashboard
86
+ ```
87
+
88
+ On Claude Code older than 2.1.275, add the marketplace first: `/plugin marketplace add JonImmsWordpressDev/claude-dashboard`, then `/plugin install mission-control@jonimms`.
89
+
90
+ - `/mission-control:open` starts the dashboard if it isn't running (your global install, or npx) and opens it.
91
+ - `/mission-control:statusline` sets up the statusline below. It checks your installed version first and shows the setting to add.
92
+
93
+ ## Statusline
94
+
95
+ `claude-dashboard statusline` prints one line for Claude Code's status bar:
96
+
97
+ ```
98
+ ❯ acme-storefront needs you · Opus 5.5 · ctx 41% · 5h 64% → 3:40 PM · $3.20
99
+ ```
100
+
101
+ Model, context use, your 5-hour limit, and this session's cost come from Claude Code itself. When the dashboard is running, the line also names other sessions waiting on you and shows the limit forecast in amber. If the dashboard is down, you get the rest of the line and no delay. It needs a global install (npm or Homebrew), not npx. Add it to `~/.claude/settings.json`:
102
+
103
+ ```json
104
+ { "statusLine": { "type": "command", "command": "claude-dashboard statusline", "padding": 0 } }
105
+ ```
106
+
68
107
  ## Install (always-on)
69
108
 
70
109
  ```bash
@@ -129,7 +168,10 @@ If something looks off, the log is at `~/Library/Logs/claude-dashboard.log`. To
129
168
  - **Search** — the header box searches every prompt you've ever sent plus all session titles (Enter to run, 2+ characters). Narrow with `project:name` or `since:7d` / `since:2026-08-01`. Clicking a prompt result opens the transcript scrolled to the matching turn. If the answer isn't in a prompt, **search inside full transcripts** runs the same query across the conversation bodies — slower, so it's a second click rather than the default.
130
169
  - **Command palette** — `⌘K` from anywhere: fuzzy-jump to any project or session, watch a live session, start a new one, open settings or stats. Arrow keys + Enter.
131
170
  - **Stats** — click the weekly bar chart in the header: a 26-week activity heatmap, your busiest hours, a weekly-rhythm grid (prompts by day of week and hour), estimated cost per day for the last 90 days (list-price value on a subscription), and an all-time per-model token/cost breakdown. The daily cost history is computed from your existing transcripts, so it's full from the first run.
132
- - **Your week with Claude** — a 7-day report (`⌘K`, or the button at the top of stats): sessions, prompts, estimated cost (list-price value on a subscription) against your budget, busiest day and hour, models used, how long you typically take to answer a question, and your cache hit rate. Exports as markdown.
171
+ - **Your week with Claude** — a 7-day report (`⌘K`, or the button at the top of stats): sessions, prompts, estimated cost (list-price value on a subscription) against your budget, busiest day and hour, models used, how long you typically take to answer a question, and your cache hit rate. Exports as markdown, or as a share card: a 1200×630 PNG with your week's totals, daily bars, and, on Pro or Max, how many times the plan's weekly price that usage was worth at list price. Download it or copy it straight to the clipboard. The card shows totals only, never project names or prompts.
172
+
173
+ ![Share card: a week's list-price value as a multiple of the Max 20x plan's weekly price, daily bars, sessions, prompts, cache hit rate and busiest hour](docs/screenshots/share-card.png)
174
+
133
175
  - **Day timeline** — `⌘K` → Day timeline: the last 24 hours as one lane per project, with amber ticks where a session needed you.
134
176
  - **Catch-up** — the `🔔` in the header keeps every notification the server has fired since it started, with an unread count. Useful after a morning away from the desk.
135
177
  - **Export** — any transcript downloads as clean markdown via the `export ⇩` button.
@@ -163,6 +205,7 @@ If something looks off, the log is at `~/Library/Logs/claude-dashboard.log`. To
163
205
  - **Session cost breakdown** — the `≈$ ▾` button in a transcript's header opens a table by model (input, output, cache read, cache write), the subagent share, and the session's cache hit rate.
164
206
  - **Context and tools** — each live session shows how full its context window was on the last turn (`412k / 1M`, amber from 70%, red from 90%), so you can see a compaction or a fresh session coming. Mission Control panes add the session's tool-call count with failures in amber, and the transcript's cost panel shows the same count in its footer. Read from the same incremental transcript scan as everything else; subagents are not included.
165
207
  - **Most expensive sessions** — in the stats view: the ten costliest sessions all time, click to open.
208
+ - **Limit forecast** — the session and week meters project your current pace forward. When it runs out before the window resets, the meter shows an amber `→ 3:40 PM` (or `→ Thu 2:15 PM` for the week), and the tooltip says how early. Otherwise the tooltip shows where you will land by the reset. Pace is your last 30 minutes (5-hour window) or 24 hours (week) once the dashboard has watched that long, and the average since the window opened before that. One notification per window, once you are past 50% and on pace to run out.
166
209
  - **Stuck flag** — a session that's "busy" but has written nothing to its transcript for 10+ minutes gets an amber `quiet Nm` cell; at 20 minutes you get one notification. It's a hint, not a verdict — a session waiting on slow background work can look the same.
167
210
  - **Idle reminder** — a session left on **needs you** for an hour (configurable in settings, `0` to turn off) gets one more notification and a `still waiting` entry in the catch-up bell, and its elapsed cell turns amber with the wait time. Nothing repeats until you answer it.
168
211
 
@@ -4,5 +4,11 @@
4
4
  // Starts the server and opens the dashboard in the default browser.
5
5
  // Pass --no-open to skip the browser (services use `node server.js` directly
6
6
  // and never auto-open).
7
- if (!process.argv.includes('--no-open')) process.env.CLAUDE_DASH_OPEN = '1';
8
- require('../server.js');
7
+ // `claude-dashboard statusline` prints one line for Claude Code's statusLine
8
+ // setting instead of starting anything.
9
+ if (process.argv[2] === 'statusline') {
10
+ require('../lib/statusline').runStatusline();
11
+ } else {
12
+ if (!process.argv.includes('--no-open')) process.env.CLAUDE_DASH_OPEN = '1';
13
+ require('../server.js');
14
+ }
@@ -1,5 +1,5 @@
1
1
  [Unit]
2
- Description=Claude Dashboard — local dashboard for Claude Code sessions
2
+ Description=Claude Mission Control — local dashboard for Claude Code sessions
3
3
  After=default.target
4
4
 
5
5
  [Service]
package/install.ps1 CHANGED
@@ -1,4 +1,4 @@
1
- # Install Claude Dashboard on Windows (EXPERIMENTAL — testers wanted).
1
+ # Install Claude Mission Control on Windows (EXPERIMENTAL — testers wanted).
2
2
  # Registers a logon Scheduled Task that runs the server hidden.
3
3
  # Run from the repo folder in PowerShell: .\install.ps1
4
4
  $ErrorActionPreference = 'Stop'
package/install.sh CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # Install the Claude Dashboard as an always-on service that starts at login:
2
+ # Install the Claude Mission Control as an always-on service that starts at login:
3
3
  # a LaunchAgent on macOS, a systemd user unit on Linux.
4
4
  set -euo pipefail
5
5
 
package/lib/collector.js CHANGED
@@ -14,9 +14,10 @@ const { isIgnored } = require('./ignore');
14
14
  const { worktreeRoot } = require('./paths');
15
15
  const { sendNotification, newlyWaiting, overdueWaits, isProjectMuted, trackWaits } = require('./notify');
16
16
  const { readConfig, readPricing } = require('./config');
17
- const { estimateCost, costReport, budgetLevel, typicalWait, burnRate, topByCost } = require('./pricing');
17
+ const { estimateCost, planValue, costReport, budgetLevel, typicalWait, burnRate, topByCost } = require('./pricing');
18
18
  const { readPlan } = require('./plan');
19
19
  const { readTeams, agentStatus, resolveTeam } = require('./teams');
20
+ const { WINDOWS, recordSample, forecastWindow, shouldAlert, clockLabel, durationLabel } = require('./forecast');
20
21
 
21
22
  // A busy session whose transcript hasn't grown for this long may be stalled.
22
23
  const QUIET_FLAG_MS = 10 * 60 * 1000;
@@ -42,6 +43,8 @@ class Collector {
42
43
  this.clientCount = 0;
43
44
  this.timers = [];
44
45
  this.fingerprint = '';
46
+ this.usageSamples = { session: [], week: [] };
47
+ this.forecastAlerted = { session: null, week: null }; // resetsAt last alerted on
45
48
  }
46
49
 
47
50
  onChange(fn) {
@@ -182,6 +185,40 @@ class Collector {
182
185
  // Live OAuth usage first (Keychain-authed, macOS); statusline cache as
183
186
  // the fallback for other platforms or when the toggle is off.
184
187
  this.raw.quota = (await fetchOauthUsage()) || (await readQuota());
188
+ const q = this.raw.quota;
189
+ if (q && !q.stale) {
190
+ const now = Date.now();
191
+ this.usageSamples.session = recordSample(this.usageSamples.session, q.utilization, q.resetsAt, now, WINDOWS.session.lookbackMs * 2);
192
+ this.usageSamples.week = recordSample(this.usageSamples.week, q.weeklyUtilization, q.weeklyResetsAt, now, WINDOWS.week.lookbackMs * 2);
193
+ }
194
+ }
195
+
196
+ // Forecast both usage windows, and notify once per window when the
197
+ // current pace runs out before the reset.
198
+ limitForecast() {
199
+ const q = this.raw.quota;
200
+ if (!q || q.stale) return null;
201
+ const now = Date.now();
202
+ const out = {};
203
+ const windows = [
204
+ ['session', q.utilization, q.resetsAt, '5-hour'],
205
+ ['week', q.weeklyUtilization, q.weeklyResetsAt, 'weekly'],
206
+ ];
207
+ for (const [key, utilization, resetsAt, label] of windows) {
208
+ const f = forecastWindow({ ...WINDOWS[key], utilization, resetsAt, samples: this.usageSamples[key], now });
209
+ out[key] = f;
210
+ if (!shouldAlert(f, this.forecastAlerted[key])) continue;
211
+ this.forecastAlerted[key] = f.resetsAt;
212
+ const when = clockLabel(f.hitAt, now);
213
+ const early = durationLabel(f.resetsAt - f.hitAt);
214
+ this.logEvent('forecast', `${label[0].toUpperCase()}${label.slice(1)} limit`, `At this pace you hit it ${when}, ${early} before it resets (${Math.round(f.utilization)}% now)`);
215
+ sendNotification({
216
+ title: `Claude ${label} limit`,
217
+ body: `At this pace you hit your ${label} limit ${when}, ${early} before it resets. ${Math.round(f.utilization)}% used.`,
218
+ sound: 'Basso',
219
+ });
220
+ }
221
+ return out;
185
222
  }
186
223
 
187
224
  projectPaths() {
@@ -442,7 +479,9 @@ class Collector {
442
479
  }
443
480
  const budget = budgetLimit ? { limit: budgetLimit, spent: weeklyCost[7], level } : null;
444
481
 
445
- const next = { quota: this.raw.quota, plan: readPlan(), theme: readConfig().theme || 'board', liveSessions, projects, weeklyCost, budget, pinned, events: this.events.slice(0, 50), errors: this.state.errors.slice(-5) };
482
+ const forecast = this.limitForecast();
483
+
484
+ const next = { quota: this.raw.quota, forecast, plan: readPlan(), theme: readConfig().theme || 'board', liveSessions, projects, weeklyCost, budget, pinned, events: this.events.slice(0, 50), errors: this.state.errors.slice(-5) };
446
485
  const fp = JSON.stringify(next);
447
486
  if (fp !== this.fingerprint) {
448
487
  this.fingerprint = fp;
@@ -622,6 +661,7 @@ class Collector {
622
661
  waits: weekWaits,
623
662
  typicalWait: typicalWait(weekWaits),
624
663
  cacheHitRatio: weekCacheAll ? Math.round((weekCacheRead / weekCacheAll) * 100) / 100 : null,
664
+ planValue: planValue(readPlan(), weekCost),
625
665
  },
626
666
  byModel,
627
667
  perProject: perProject.slice(0, 15),
package/lib/demo.js CHANGED
@@ -54,7 +54,11 @@ function demoState() {
54
54
  return {
55
55
  generatedAt: now,
56
56
  plan: { tier: 'Max 20x' },
57
- quota: { utilization: 34, weeklyUtilization: 52, resetsAt: '2pm', weeklyResetsAt: 'Wed 9am' },
57
+ quota: { utilization: 64, weeklyUtilization: 52, resetsAt: '2pm', weeklyResetsAt: 'Wed 9am' },
58
+ forecast: {
59
+ session: { hitAt: now + 70 * MIN, projected: 128, resetsAt: now + 3 * HOUR, utilization: 64, basis: 'recent' },
60
+ week: { hitAt: null, projected: 81, resetsAt: now + 3 * 24 * HOUR, utilization: 52, basis: 'average' },
61
+ },
58
62
  weeklyCost: [31, 48, 22, 75, 61, 90, 84, 143],
59
63
  budget: { limit: 200, spent: 143, level: 75 },
60
64
  pinned: [
@@ -219,6 +223,7 @@ function demoStats() {
219
223
  busiestDay: 'Tuesday', busiestHour: 11,
220
224
  waits: [22, 31, 18, 4], typicalWait: 'under 2m',
221
225
  cacheHitRatio: 0.91,
226
+ planValue: { label: 'Max 20x', weeklyPrice: 46.15, multiple: 3.1 },
222
227
  },
223
228
  byModel: {
224
229
  'claude-opus-5': { tokens: 310e6, cost: 412.5 },
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+ // Limit forecast: project the 5-hour and weekly usage meters forward and
3
+ // say when they will hit 100%, if that comes before the window resets.
4
+ //
5
+ // Pace comes from recent samples when we have enough history, otherwise
6
+ // from the average since the window opened (resetsAt - window length) —
7
+ // so a fresh server start still has a forecast.
8
+
9
+ const WINDOWS = {
10
+ session: { windowMs: 5 * 3600_000, lookbackMs: 30 * 60_000, minElapsedMs: 15 * 60_000 },
11
+ week: { windowMs: 7 * 24 * 3600_000, lookbackMs: 24 * 3600_000, minElapsedMs: 6 * 3600_000 },
12
+ };
13
+ const SAMPLE_GAP_MS = 5 * 60_000; // the usage API is polled at most every 5 min
14
+ const ROUND_MS = 5 * 60_000; // keep hitAt stable between polls
15
+ const ALERT_MIN_PCT = 50; // don't alarm on early, noisy extrapolation
16
+
17
+ function toMs(v) {
18
+ if (v == null || v === '') return null;
19
+ if (typeof v === 'number') return v > 1e12 ? v : v * 1000;
20
+ const n = Number(v);
21
+ if (Number.isFinite(n)) return n > 1e12 ? n : n * 1000;
22
+ const t = Date.parse(v);
23
+ return Number.isFinite(t) ? t : null;
24
+ }
25
+
26
+ // Returns a new samples array with this reading added. A changed resetsAt
27
+ // or a drop in utilization means a new window: start over.
28
+ function recordSample(samples, utilization, resetsAt, now, keepMs) {
29
+ if (utilization == null) return samples;
30
+ const reset = toMs(resetsAt);
31
+ let out = samples.filter((s) => s.resetsAt === reset && now - s.at <= keepMs);
32
+ const last = out[out.length - 1];
33
+ if (last && utilization < last.utilization) out = [];
34
+ else if (last && now - last.at < SAMPLE_GAP_MS) return out;
35
+ out.push({ at: now, utilization, resetsAt: reset });
36
+ return out;
37
+ }
38
+
39
+ // { hitAt, projected, resetsAt, utilization, basis } or null when there is
40
+ // nothing sensible to say. hitAt is null when the window resets first.
41
+ function forecastWindow({ utilization, resetsAt, windowMs, lookbackMs, minElapsedMs, samples = [], now = Date.now() }) {
42
+ const reset = toMs(resetsAt);
43
+ if (utilization == null || !reset || reset <= now) return null;
44
+
45
+ let rate = null; // percentage points per ms
46
+ let basis = 'recent';
47
+ const mine = samples.filter((s) => s.resetsAt === reset && s.at < now);
48
+ let anchor = null;
49
+ for (const s of mine) if (s.at <= now - lookbackMs) anchor = s;
50
+ if (!anchor && mine.length && now - mine[0].at >= lookbackMs / 2) anchor = mine[0];
51
+ if (anchor) rate = Math.max(0, (utilization - anchor.utilization) / (now - anchor.at));
52
+
53
+ if (rate == null) {
54
+ const elapsed = now - (reset - windowMs);
55
+ if (elapsed < minElapsedMs) return null;
56
+ rate = utilization / elapsed;
57
+ basis = 'average';
58
+ }
59
+
60
+ const projected = Math.min(999, Math.round(utilization + rate * (reset - now)));
61
+ let hitAt = null;
62
+ if (utilization >= 100) hitAt = Math.floor(now / ROUND_MS) * ROUND_MS;
63
+ else if (rate > 0) {
64
+ const t = now + (100 - utilization) / rate;
65
+ if (t < reset) hitAt = Math.floor(t / ROUND_MS) * ROUND_MS;
66
+ }
67
+ return { hitAt, projected, resetsAt: reset, utilization, basis };
68
+ }
69
+
70
+ // True when this forecast deserves a notification we haven't sent yet for
71
+ // this window. alertedFor is the resetsAt we last alerted on.
72
+ function shouldAlert(f, alertedFor) {
73
+ return !!(f && f.hitAt && f.utilization < 100 && f.utilization >= ALERT_MIN_PCT && f.resetsAt !== alertedFor);
74
+ }
75
+
76
+ // "around 3:40 PM" today, "Thu around 2:15 PM" on another day.
77
+ function clockLabel(ms, now = Date.now()) {
78
+ const d = new Date(ms);
79
+ const time = d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
80
+ if (d.toDateString() === new Date(now).toDateString()) return `around ${time}`;
81
+ return `${d.toLocaleDateString(undefined, { weekday: 'short' })} around ${time}`;
82
+ }
83
+
84
+ // "35m", "1h 10m", "2d 4h".
85
+ function durationLabel(ms) {
86
+ const m = Math.max(0, Math.round(ms / 60_000));
87
+ if (m < 60) return `${m}m`;
88
+ const h = Math.floor(m / 60);
89
+ if (h < 24) return m % 60 ? `${h}h ${m % 60}m` : `${h}h`;
90
+ return h % 24 ? `${Math.floor(h / 24)}d ${h % 24}h` : `${Math.floor(h / 24)}d`;
91
+ }
92
+
93
+ module.exports = { WINDOWS, toMs, recordSample, forecastWindow, shouldAlert, clockLabel, durationLabel };
package/lib/notify.js CHANGED
@@ -31,7 +31,7 @@ $xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([
31
31
  $t = $xml.GetElementsByTagName('text')
32
32
  $null = $t.Item(0).AppendChild($xml.CreateTextNode('${esc(title)}'))
33
33
  $null = $t.Item(1).AppendChild($xml.CreateTextNode('${esc(body)}'))
34
- [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Claude Dashboard').Show([Windows.UI.Notifications.ToastNotification]::new($xml))`;
34
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Claude Mission Control').Show([Windows.UI.Notifications.ToastNotification]::new($xml))`;
35
35
  execFile('powershell', ['-NoProfile', '-NonInteractive', '-Command', ps], { timeout: 8000 }, () => {});
36
36
  } else {
37
37
  execFile('notify-send', [title, body], { timeout: 5000 }, () => {});
package/lib/pricing.js CHANGED
@@ -208,4 +208,22 @@ function contextWindow(model) {
208
208
  return MILLION_WINDOW.some((p) => id.startsWith(p)) ? 1_000_000 : 200_000;
209
209
  }
210
210
 
211
- module.exports = { estimateCost, costReport, totalTokens, rateFor, normalizeModel, loadPricing, overrideFor, budgetLevel, typicalWait, burnRate, cacheHitRatio, topByCost, contextWindow };
211
+ // Subscription list prices (USD/month) for the share card's "worth N× your
212
+ // plan" line. Team and Enterprise vary by contract, so they get no multiple.
213
+ const PLAN_MONTHLY = { Pro: 20, 'Max 5x': 100, 'Max 20x': 200 };
214
+
215
+ // { label, weeklyPrice, multiple } on a known subscription, label only on an
216
+ // unknown one, null on API billing or no plan.
217
+ function planValue(plan, weekCost) {
218
+ if (!plan || !plan.label || plan.billing === 'api' || plan.label === 'API') return null;
219
+ const monthly = PLAN_MONTHLY[plan.label];
220
+ if (!monthly) return { label: plan.label, weeklyPrice: null, multiple: null };
221
+ const weekly = (monthly * 12) / 52;
222
+ return {
223
+ label: plan.label,
224
+ weeklyPrice: Math.round(weekly * 100) / 100,
225
+ multiple: weekCost > 0 ? Math.round((weekCost / weekly) * 10) / 10 : null,
226
+ };
227
+ }
228
+
229
+ module.exports = { estimateCost, planValue, costReport, totalTokens, rateFor, normalizeModel, loadPricing, overrideFor, budgetLevel, typicalWait, burnRate, cacheHitRatio, topByCost, contextWindow };
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+ // `claude-dashboard statusline`: one line for Claude Code's statusLine
3
+ // setting. Reads Claude Code's JSON on stdin and, when the dashboard is up,
4
+ // adds what only it knows: other sessions waiting on you and the limit
5
+ // forecast. Must stay fast, so the dashboard gets a short timeout and is
6
+ // optional.
7
+ const http = require('http');
8
+
9
+ const AMBER = '\x1b[33m';
10
+ const DIM = '\x1b[2m';
11
+ const RESET = '\x1b[0m';
12
+ const STATE_TIMEOUT_MS = 400;
13
+
14
+ function clock(ms) {
15
+ return new Date(ms).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
16
+ }
17
+
18
+ // input: Claude Code's statusline JSON. state: the dashboard's /api/state,
19
+ // or null when it isn't running.
20
+ function formatStatusline(input, state, { color = true } = {}) {
21
+ const amber = (s) => (color ? AMBER + s + RESET : s);
22
+ const dim = (s) => (color ? DIM + s + RESET : s);
23
+ const parts = [];
24
+
25
+ if (state) {
26
+ const waiting = (state.liveSessions || []).filter((s) => s.status === 'waiting' && s.sessionId !== input.session_id);
27
+ if (waiting.length) {
28
+ const first = waiting[0].projectName || 'a session';
29
+ parts.push(amber(`❯ ${waiting.length === 1 ? first : `${waiting.length} sessions`} need${waiting.length === 1 ? 's' : ''} you`));
30
+ }
31
+ }
32
+
33
+ if (input.model && input.model.display_name) parts.push(input.model.display_name);
34
+
35
+ const ctx = input.context_window && input.context_window.used_percentage;
36
+ if (typeof ctx === 'number') {
37
+ const s = `ctx ${Math.round(ctx)}%`;
38
+ parts.push(ctx >= 70 ? amber(s) : s);
39
+ }
40
+
41
+ const limits = input.rate_limits || {};
42
+ const forecast = (state && state.forecast) || {};
43
+ for (const [key, label, f] of [['five_hour', '5h', forecast.session], ['seven_day', 'week', forecast.week]]) {
44
+ const pct = limits[key] && limits[key].used_percentage;
45
+ if (typeof pct !== 'number') continue;
46
+ if (f && f.hitAt && pct < 100) parts.push(amber(`${label} ${Math.round(pct)}% → ${clock(f.hitAt)}`));
47
+ else if (key === 'five_hour' || pct >= 50) parts.push(`${label} ${Math.round(pct)}%`);
48
+ }
49
+
50
+ const cost = input.cost && input.cost.total_cost_usd;
51
+ if (typeof cost === 'number' && cost >= 0.01) parts.push(dim(`$${cost.toFixed(2)}`));
52
+
53
+ return parts.join(dim(' · '));
54
+ }
55
+
56
+ function fetchState(port) {
57
+ return new Promise((resolve) => {
58
+ const req = http.get({ host: '127.0.0.1', port, path: '/api/state', timeout: STATE_TIMEOUT_MS }, (res) => {
59
+ if (res.statusCode !== 200) { res.resume(); return resolve(null); }
60
+ let body = '';
61
+ res.setEncoding('utf8');
62
+ res.on('data', (d) => (body += d));
63
+ res.on('end', () => {
64
+ try { resolve(JSON.parse(body)); } catch { resolve(null); }
65
+ });
66
+ });
67
+ req.on('timeout', () => req.destroy());
68
+ req.on('error', () => resolve(null));
69
+ });
70
+ }
71
+
72
+ async function runStatusline() {
73
+ let raw = '';
74
+ process.stdin.setEncoding('utf8');
75
+ for await (const chunk of process.stdin) raw += chunk;
76
+ let input = {};
77
+ try { input = JSON.parse(raw); } catch { /* print what we can */ }
78
+ const state = await fetchState(Number(process.env.CLAUDE_DASH_PORT) || 4517);
79
+ process.stdout.write(formatStatusline(input, state) + '\n');
80
+ }
81
+
82
+ module.exports = { formatStatusline, runStatusline };
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
- # SwiftBar plugin for the Claude Dashboard. Refreshes every 15s (filename).
3
- # <swiftbar.title>Claude Dashboard</swiftbar.title>
2
+ # SwiftBar plugin for the Claude Mission Control. Refreshes every 15s (filename).
3
+ # <swiftbar.title>Claude Mission Control</swiftbar.title>
4
4
  # <swiftbar.hideAbout>true</swiftbar.hideAbout>
5
5
  # <swiftbar.hideRunInTerminal>true</swiftbar.hideRunInTerminal>
6
6
  # <swiftbar.hideDisablePlugin>true</swiftbar.hideDisablePlugin>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mission-control",
3
- "version": "1.11.1",
3
+ "version": "1.12.1",
4
4
  "description": "Local dashboard for Claude Code: live sessions, transcripts, costs, git status — all your projects in one place.",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -24,7 +24,8 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "start": "node server.js",
27
- "test": "node --test test/pure-logic.test.js"
27
+ "test": "node --test test/pure-logic.test.js",
28
+ "demo:build": "node scripts/build-demo.js"
28
29
  },
29
30
  "engines": {
30
31
  "node": ">=18"
package/public/index.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Claude Dashboard</title>
6
+ <title>Claude Mission Control</title>
7
7
  <meta name="mobile-web-app-capable" content="yes">
8
8
  <meta name="apple-mobile-web-app-capable" content="yes">
9
9
  <link rel="manifest" href="/manifest.webmanifest">
@@ -209,6 +209,7 @@
209
209
  /* ---------- header: the station sign ---------- */
210
210
  header {
211
211
  display: flex; align-items: center; gap: 16px;
212
+ flex-wrap: wrap; row-gap: 10px;
212
213
  padding: 12px 20px;
213
214
  border-bottom: 2px solid var(--line);
214
215
  background: var(--surface);
@@ -241,6 +242,7 @@
241
242
  .meter .fill { height: 100%; border-radius: 3px; background: var(--accent); }
242
243
  .meter .fill.hot { background: var(--warn); }
243
244
  .meter .fill.crit { background: var(--bad); }
245
+ .meter .eta { color: var(--warn); white-space: nowrap; }
244
246
 
245
247
  /* ---------- live rail: the departures rows ---------- */
246
248
  #live {
@@ -393,6 +395,14 @@
393
395
  }
394
396
  #search:focus { outline: 2px solid var(--accent); outline-offset: 0; }
395
397
  #search::placeholder { color: var(--faint); }
398
+ /* A full set of meters needs ~1600px; tighten before the header wraps. */
399
+ @media (max-width: 1600px) {
400
+ header { gap: 12px; }
401
+ #search { width: 120px; }
402
+ #open-pref-wrap .open-label { display: none; }
403
+ #quota { gap: 12px; }
404
+ .meter .bar { width: 44px; }
405
+ }
396
406
  #gear, #mc-btn, #bell-btn {
397
407
  border: none; background: none; color: var(--muted); cursor: pointer;
398
408
  font-size: 15px; padding: 2px 6px; border-radius: 2px; margin-left: 4px;
@@ -829,11 +839,11 @@
829
839
  <body>
830
840
  <header>
831
841
  <div id="conn" title="connection"></div>
832
- <h1>Claude<span class="tilde"> · </span>Dashboard</h1>
833
- <input id="search" type="search" placeholder="Search sessions… ⏎" autocomplete="off">
842
+ <h1>Claude<span class="tilde"> · </span>Mission Control</h1>
843
+ <input id="search" type="search" placeholder="Search… ⏎" autocomplete="off">
834
844
  <div class="spacer"></div>
835
- <label id="open-pref-wrap">Open in
836
- <select id="open-pref">
845
+ <label id="open-pref-wrap"><span class="open-label">Open in</span>
846
+ <select id="open-pref" aria-label="Open sessions in">
837
847
  <option value="terminal">Terminal</option>
838
848
  </select>
839
849
  </label>
@@ -986,15 +996,36 @@ function weeklyChart(weeks) {
986
996
  aria-label="${esc(costWording().noun)} per week, last 8 weeks">${bars}</svg>`;
987
997
  }
988
998
 
989
- function meter(label, pct, title) {
999
+ function meter(label, pct, title, extra = '') {
990
1000
  const cls = pct >= 90 ? 'crit' : pct >= 75 ? 'hot' : '';
991
1001
  return `<div class="meter" title="${esc(title || '')}">
992
1002
  <span>${esc(label)}</span>
993
1003
  <span class="bar"><span class="fill ${cls}" style="width:${Math.min(100, pct)}%"></span></span>
994
- <span>${Math.round(pct)}%</span>
1004
+ <span>${Math.round(pct)}%</span>${extra}
995
1005
  </div>`;
996
1006
  }
997
1007
 
1008
+ // Limit forecast for a usage meter: an amber "→ 3:40 PM" when the current
1009
+ // pace runs out before the reset, and the projection in the tooltip.
1010
+ function forecastMeter(label, pct, resetsRaw, f) {
1011
+ const sameDay = (ms) => new Date(ms).toDateString() === new Date().toDateString();
1012
+ const when = (ms) => (sameDay(ms) ? '' : new Date(ms).toLocaleDateString(undefined, { weekday: 'short' }) + ' ') + fmtWhen(ms);
1013
+ const parsed = Date.parse(resetsRaw);
1014
+ const resetAt = resetsRaw ? (Number.isFinite(parsed) ? when(parsed) : resetsRaw) : '';
1015
+ const resets = resetAt ? 'resets ' + resetAt : '';
1016
+ if (!f) return meter(label, pct, resets);
1017
+ const pace = f.basis === 'recent' ? 'at your recent pace' : 'at your average pace this window';
1018
+ if (!f.hitAt) {
1019
+ return meter(label, pct, `${resets} · ${pace}, about ${f.projected}% by then`);
1020
+ }
1021
+ const early = Math.max(0, Math.round((f.resetsAt - f.hitAt) / 60000));
1022
+ const earlyTxt = early >= 1440 ? `${Math.floor(early / 1440)}d ${Math.floor((early % 1440) / 60)}h` : early >= 60 ? `${Math.floor(early / 60)}h ${early % 60}m` : `${early}m`;
1023
+ const title = f.utilization >= 100
1024
+ ? `limit reached · ${resets}`
1025
+ : `${pace}, you hit 100% around ${when(f.hitAt)}, ${earlyTxt} before the reset${resetAt ? ' at ' + resetAt : ''}`;
1026
+ return meter(label, pct, title, `<span class="eta">→ ${esc(when(f.hitAt))}</span>`);
1027
+ }
1028
+
998
1029
  // One display line for what a waiting session needs: glyph + 80-char text,
999
1030
  // plus the untruncated version for tooltips.
1000
1031
  function reasonLine(r) {
@@ -1335,8 +1366,8 @@ function render(state) {
1335
1366
  if (q) {
1336
1367
  qEl.className = q.stale ? 'stale' : '';
1337
1368
  patch(qEl, spendChip +
1338
- (q.utilization != null ? meter('session', q.utilization, q.resetsAt ? 'resets ' + q.resetsAt : '') : '') +
1339
- (q.weeklyUtilization != null ? meter('week', q.weeklyUtilization, q.weeklyResetsAt ? 'resets ' + q.weeklyResetsAt : '') : '') +
1369
+ (q.utilization != null ? forecastMeter('session', q.utilization, q.resetsAt, state.forecast && state.forecast.session) : '') +
1370
+ (q.weeklyUtilization != null ? forecastMeter('week', q.weeklyUtilization, q.weeklyResetsAt, state.forecast && state.forecast.week) : '') +
1340
1371
  (q.costUsed != null && q.costLimit ? `<span title="extra usage">$${q.costUsed.toFixed(0)}/$${q.costLimit.toFixed(0)}</span>` : ''));
1341
1372
  } else {
1342
1373
  qEl.className = '';
@@ -1367,7 +1398,7 @@ function render(state) {
1367
1398
  patch($('#grid'), active.length || dormant.length
1368
1399
  ? active.map(projectCard).join('')
1369
1400
  : `<div id="welcome">
1370
- <h2><span class="tilde">❯</span> Welcome to Claude Dashboard</h2>
1401
+ <h2><span class="tilde">❯</span> Welcome to Claude Mission Control</h2>
1371
1402
  <p>No Claude Code projects found yet. Run <code>claude</code> in any folder and it appears here automatically — live status, transcripts, git state, costs, the lot.</p>
1372
1403
  <p>Tip: press <code>⌘K</code> to jump anywhere, <code>/</code> to search, and click ⚙ for settings.</p>
1373
1404
  </div>`);
@@ -1384,9 +1415,9 @@ function render(state) {
1384
1415
  : 'Settings';
1385
1416
 
1386
1417
  const waiting = live.filter((s) => s.status === 'waiting').length;
1387
- document.title = waiting ? `(${waiting} waiting) Claude Dashboard`
1388
- : live.length ? `(${live.length} running) Claude Dashboard`
1389
- : 'Claude Dashboard';
1418
+ document.title = waiting ? `(${waiting} waiting) Claude Mission Control`
1419
+ : live.length ? `(${live.length} running) Claude Mission Control`
1420
+ : 'Claude Mission Control';
1390
1421
  }
1391
1422
 
1392
1423
  function toast(msg) {
@@ -2903,7 +2934,7 @@ async function openWeekReport() {
2903
2934
  ['Cache hit rate', w.cacheHitRatio != null ? `${Math.round(w.cacheHitRatio * 100)}% of input tokens served from cache` : 'not enough data yet'],
2904
2935
  ['Your typical reply time', w.typicalWait ? `${w.typicalWait} (${waitTotal} prompts measured)` : 'not enough data yet'],
2905
2936
  ];
2906
- weekReport = { w, b, rows };
2937
+ weekReport = { w, b, rows, days: (s.costDays || []).slice(-7) };
2907
2938
  const table = `<table class="stat-table">${rows.map(([k, v]) =>
2908
2939
  `<tr><td>${esc(k)}</td><td style="text-align:right">${esc(v)}</td></tr>`).join('')}</table>`;
2909
2940
  const projRows = (w.projects || []).map((p) =>
@@ -2911,12 +2942,119 @@ async function openWeekReport() {
2911
2942
  $('#d-body').innerHTML =
2912
2943
  dSection('The week', table) +
2913
2944
  dSection('Where it went', projRows ? `<table class="stat-table">${projRows}</table>` : null, 'No spend recorded this week.') +
2945
+ dSection('Share card', '<img id="share-card" alt="Your week with Claude as an image" style="width:100%;border-radius:6px;border:1px solid var(--line)">' +
2946
+ '<div class="set-sub" style="margin-top:6px">Totals only. No project names or prompts.</div>' +
2947
+ '<div style="margin-top:10px;display:flex;gap:8px"><button class="copy" data-card-download>download PNG ⇩</button><button class="copy" data-card-copy>copy image</button></div>') +
2914
2948
  '<div style="margin-top:16px"><button class="copy" data-week-export>download as markdown ⇩</button></div>';
2949
+ renderShareCard().then((url) => { const img = $('#share-card'); if (img) img.src = url; }).catch(() => {});
2915
2950
  } catch {
2916
2951
  $('#d-body').innerHTML = '<div class="d-empty" style="padding:20px 0">Couldn\'t load the report.</div>';
2917
2952
  }
2918
2953
  }
2919
2954
 
2955
+ // ---------- share card: the week as a 1200×630 PNG ----------
2956
+ // Always drawn in the dark departures palette so shared cards look alike.
2957
+ const CARD = { w: 1200, h: 630, bg: '#0d1215', flap: '#151d22', flapHi: '#1c262c', split: 'rgba(0,0,0,0.45)', ink: '#e9e4d6', muted: '#93a09e', faint: '#5d6b6b', line: '#202b31', green: '#46c073', amber: '#ffb020' };
2958
+ let shareCanvas = null;
2959
+
2960
+ async function renderShareCard() {
2961
+ const { w, days } = weekReport;
2962
+ await Promise.all(['700 140px Oswald', '500 20px "JetBrains Mono"'].map((f) => document.fonts.load(f).catch(() => {})));
2963
+ const c = document.createElement('canvas');
2964
+ c.width = CARD.w * 2;
2965
+ c.height = CARD.h * 2;
2966
+ const g = c.getContext('2d');
2967
+ g.scale(2, 2);
2968
+ const oswald = (px, wt = 600) => `${wt} ${px}px Oswald, "Arial Narrow", sans-serif`;
2969
+ const mono = (px, wt = 500) => `${wt} ${px}px "JetBrains Mono", ui-monospace, monospace`;
2970
+ const text = (s, x, y, font, color, align = 'left', spacing = 0) => {
2971
+ g.font = font; g.fillStyle = color; g.textAlign = align;
2972
+ if ('letterSpacing' in g) g.letterSpacing = `${spacing}px`;
2973
+ g.fillText(s, x, y);
2974
+ if ('letterSpacing' in g) g.letterSpacing = '0px';
2975
+ };
2976
+ const flap = (x, y, fw, fh) => {
2977
+ g.fillStyle = CARD.flap; g.beginPath(); g.roundRect(x, y, fw, fh, 6); g.fill();
2978
+ g.fillStyle = CARD.flapHi; g.beginPath(); g.roundRect(x, y, fw, fh / 2, [6, 6, 0, 0]); g.fill();
2979
+ g.fillStyle = CARD.split; g.fillRect(x, y + fh / 2 - 0.5, fw, 1.5);
2980
+ };
2981
+
2982
+ g.fillStyle = CARD.bg; g.fillRect(0, 0, CARD.w, CARD.h);
2983
+
2984
+ // header
2985
+ const end = new Date();
2986
+ const start = new Date(end.getTime() - 6 * 86400000);
2987
+ const md = (d) => d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
2988
+ text('❯ MY WEEK WITH CLAUDE CODE', 64, 92, oswald(30), CARD.amber, 'left', 2);
2989
+ text(`${md(start)} – ${md(end)}, ${end.getFullYear()}`, CARD.w - 64, 90, mono(18), CARD.muted, 'right');
2990
+ g.fillStyle = CARD.line; g.fillRect(64, 116, CARD.w - 128, 2);
2991
+
2992
+ // the number
2993
+ const cost = w.cost || 0;
2994
+ const money = cost >= 10 ? Math.round(cost).toLocaleString() : cost.toFixed(2);
2995
+ const pv = w.planValue;
2996
+ text(pv ? 'LIST-PRICE VALUE' : 'ESTIMATED COST', 64, 172, mono(16), CARD.muted, 'left', 2);
2997
+ text(`≈$${money}`, 58, 312, oswald(150, 700), CARD.ink);
2998
+ if (pv && pv.multiple >= 1.1) text(`${pv.multiple}× the ${pv.label} plan's weekly price`, 64, 358, mono(22), CARD.amber);
2999
+ else if (pv) text(`on the ${pv.label} plan`, 64, 358, mono(22), CARD.muted);
3000
+
3001
+ // daily bars
3002
+ const bx = 700, by = 150, bw = 436, bh = 200;
3003
+ const max = Math.max(...days.map((d) => d.cost), 0.01);
3004
+ const slot = bw / 7;
3005
+ const busiest = days.reduce((i, d, j) => (d.cost > days[i].cost ? j : i), 0);
3006
+ days.forEach((d, i) => {
3007
+ const x = bx + i * slot + 8;
3008
+ const colW = slot - 16;
3009
+ flap(x, by, colW, bh);
3010
+ const fh = d.cost > 0 ? Math.max(4, (d.cost / max) * (bh - 8)) : 0;
3011
+ g.fillStyle = i === busiest && d.cost > 0 ? CARD.amber : CARD.green;
3012
+ g.beginPath(); g.roundRect(x + 4, by + bh - 4 - fh, colW - 8, fh, 3); g.fill();
3013
+ text(new Date(d.t).toLocaleDateString(undefined, { weekday: 'narrow' }), x + colW / 2, by + bh + 28, mono(16), CARD.muted, 'center');
3014
+ });
3015
+
3016
+ // stat flaps
3017
+ const stats = [
3018
+ ['SESSIONS', String(w.sessions ?? 0)],
3019
+ ['PROMPTS', String(w.prompts ?? 0)],
3020
+ ['CACHE HITS', w.cacheHitRatio != null ? `${Math.round(w.cacheHitRatio * 100)}%` : '—'],
3021
+ ['BUSIEST', w.busiestDay ? w.busiestDay.slice(0, 3) + (w.busiestHour != null ? ` ${w.busiestHour}:00` : '') : '—'],
3022
+ ];
3023
+ const sw = (CARD.w - 128 - 3 * 20) / 4;
3024
+ stats.forEach(([label, value], i) => {
3025
+ const x = 64 + i * (sw + 20);
3026
+ flap(x, 410, sw, 110);
3027
+ text(value, x + 22, 476, oswald(46, 600), CARD.ink);
3028
+ text(label, x + 22, 506, mono(13), CARD.muted, 'left', 2);
3029
+ });
3030
+
3031
+ // footer
3032
+ text((w.models || []).map(modelShort).join(' · '), 64, 578, mono(16), CARD.faint);
3033
+ text('npx claude-mission-control', CARD.w - 64, 578, mono(16), CARD.muted, 'right');
3034
+
3035
+ shareCanvas = c;
3036
+ return c.toDataURL('image/png');
3037
+ }
3038
+
3039
+ document.addEventListener('click', async (e) => {
3040
+ if (!shareCanvas) return;
3041
+ if (e.target.closest('[data-card-download]')) {
3042
+ const a = document.createElement('a');
3043
+ a.href = shareCanvas.toDataURL('image/png');
3044
+ a.download = `week-with-claude-${new Date().toISOString().slice(0, 10)}.png`;
3045
+ a.click();
3046
+ toast('Card downloaded');
3047
+ } else if (e.target.closest('[data-card-copy]')) {
3048
+ try {
3049
+ const blob = await new Promise((r) => shareCanvas.toBlob(r, 'image/png'));
3050
+ await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
3051
+ toast('Card copied, paste it anywhere');
3052
+ } catch {
3053
+ toast('Copy not supported here, use download');
3054
+ }
3055
+ }
3056
+ });
3057
+
2920
3058
  document.addEventListener('click', (e) => {
2921
3059
  if (e.target.closest('[data-week]')) { openWeekReport(); return; }
2922
3060
  if (!e.target.closest('[data-week-export]') || !weekReport) return;
@@ -2942,7 +3080,7 @@ document.addEventListener('click', async (e) => {
2942
3080
  try {
2943
3081
  const r = await fetch(`/api/session?id=${encodeURIComponent(exportableSession.id)}`);
2944
3082
  const t = await r.json();
2945
- const lines = [`# ${t.title}`, '', `_${t.projectName} — exported from Claude Dashboard, ${new Date().toLocaleString()}_`, ''];
3083
+ const lines = [`# ${t.title}`, '', `_${t.projectName} — exported from Claude Mission Control, ${new Date().toLocaleString()}_`, ''];
2946
3084
  for (const ev of t.events) {
2947
3085
  if (ev.kind === 'user') lines.push(`**You:** ${ev.text}`, '');
2948
3086
  else if (ev.kind === 'assistant') lines.push(ev.text, '');
@@ -1,6 +1,6 @@
1
1
  {
2
- "name": "Claude Dashboard",
3
- "short_name": "Claude Dash",
2
+ "name": "Claude Mission Control",
3
+ "short_name": "Mission Control",
4
4
  "description": "Local dashboard for Claude Code: live sessions, transcripts, costs, git status.",
5
5
  "start_url": "/",
6
6
  "display": "standalone",
package/uninstall.ps1 CHANGED
@@ -1,4 +1,4 @@
1
- # Remove the Claude Dashboard Scheduled Task (Windows).
1
+ # Remove the Claude Mission Control Scheduled Task (Windows).
2
2
  schtasks /End /TN 'ClaudeDashboard' 2>$null | Out-Null
3
3
  schtasks /Delete /TN 'ClaudeDashboard' /F 2>$null | Out-Null
4
4
  Get-Process -Name node -ErrorAction SilentlyContinue |
package/uninstall.sh CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # Remove the Claude Dashboard service (LaunchAgent on macOS, systemd user unit on Linux).
2
+ # Remove the Claude Mission Control service (LaunchAgent on macOS, systemd user unit on Linux).
3
3
  set -euo pipefail
4
4
 
5
5
  LABEL="com.claude-dashboard"