wisp-agents 0.15.0 → 0.17.0

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.
Files changed (122) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.md +29 -13
  3. package/bin/wisp.js +3 -2
  4. package/package.json +3 -3
  5. package/server/adapters/antigravity.js +7 -3
  6. package/server/adapters/claude.js +79 -55
  7. package/server/adapters/codex.js +5 -2
  8. package/server/adapters/common.js +14 -34
  9. package/server/adapters/cursor.js +4 -2
  10. package/server/adapters/grok.js +17 -2
  11. package/server/adapters/index.js +25 -2
  12. package/server/adapters/opencode.js +78 -12
  13. package/server/checkpoint.js +12 -68
  14. package/server/client.js +6 -4
  15. package/server/git.js +9 -4
  16. package/server/handoff.js +71 -0
  17. package/server/http/respond.js +59 -0
  18. package/server/http/router.js +64 -0
  19. package/server/http/static.js +36 -0
  20. package/server/index.js +138 -1216
  21. package/server/machines/bootstrap.js +56 -0
  22. package/server/machines/hosts.js +127 -0
  23. package/server/machines.js +7 -179
  24. package/server/mcp-ask.js +113 -8
  25. package/server/mcp-wisp.js +17 -5
  26. package/server/mentions.js +39 -0
  27. package/server/openapi.js +36 -4
  28. package/server/paths.js +32 -0
  29. package/server/preferences.js +2 -6
  30. package/server/proc.js +54 -0
  31. package/server/protocol.js +8 -1
  32. package/server/recipes.js +2 -4
  33. package/server/remote.js +14 -14
  34. package/server/render.js +3 -1
  35. package/server/router.js +7 -3
  36. package/server/routes/auto.js +29 -0
  37. package/server/routes/automations.js +92 -0
  38. package/server/routes/internal.js +176 -0
  39. package/server/routes/machines.js +43 -0
  40. package/server/routes/meta.js +76 -0
  41. package/server/routes/proxy.js +47 -0
  42. package/server/routes/sessions.js +351 -0
  43. package/server/routes/settings.js +144 -0
  44. package/server/routes/ship.js +61 -0
  45. package/server/routes/uploads.js +19 -0
  46. package/server/runner.js +216 -49
  47. package/server/services/auto.js +67 -0
  48. package/server/services/feedback.js +53 -0
  49. package/server/services/install.js +69 -0
  50. package/server/services/launch.js +127 -0
  51. package/server/services/phone.js +65 -0
  52. package/server/services/ship.js +82 -0
  53. package/server/services/telemetry.js +34 -0
  54. package/server/services/uploads.js +59 -0
  55. package/server/services/webhooks.js +28 -0
  56. package/server/store.js +13 -5
  57. package/server/subagents.js +5 -4
  58. package/server/telemetry.js +1 -1
  59. package/server/terminal.js +2 -1
  60. package/server/triggers.js +5 -7
  61. package/server/update.js +4 -3
  62. package/server/usage.js +94 -0
  63. package/server/util.js +42 -0
  64. package/server/welcome.js +3 -3
  65. package/web/css/auto.css +29 -0
  66. package/web/css/automations.css +71 -0
  67. package/web/css/base.css +83 -0
  68. package/web/css/blocks.css +461 -0
  69. package/web/css/composer.css +84 -0
  70. package/web/css/feed.css +235 -0
  71. package/web/css/launcher.css +204 -0
  72. package/web/css/layout.css +61 -0
  73. package/web/css/machines.css +125 -0
  74. package/web/css/modals.css +176 -0
  75. package/web/css/palette.css +79 -0
  76. package/web/css/responsive.css +35 -0
  77. package/web/css/ship.css +95 -0
  78. package/web/css/subagents.css +130 -0
  79. package/web/css/tokens.css +115 -0
  80. package/web/index.html +17 -1
  81. package/web/js/app.js +28 -5237
  82. package/web/js/blocks.js +46 -0
  83. package/web/js/core/api.js +25 -0
  84. package/web/js/core/bus.js +28 -0
  85. package/web/js/core/dom.js +7 -0
  86. package/web/js/core/format.js +27 -0
  87. package/web/js/core/state.js +43 -0
  88. package/web/js/features/actions.js +344 -0
  89. package/web/js/features/attachments.js +115 -0
  90. package/web/js/features/auto.js +54 -0
  91. package/web/js/features/automations.js +305 -0
  92. package/web/js/features/commands.js +123 -0
  93. package/web/js/features/compare.js +105 -0
  94. package/web/js/features/composer.js +315 -0
  95. package/web/js/features/feed.js +400 -0
  96. package/web/js/features/feedback.js +127 -0
  97. package/web/js/features/keys.js +83 -0
  98. package/web/js/features/launcher.js +561 -0
  99. package/web/js/features/live.js +91 -0
  100. package/web/js/features/machines.js +313 -0
  101. package/web/js/features/memory.js +154 -0
  102. package/web/js/features/mentions.js +125 -0
  103. package/web/js/features/notify.js +36 -0
  104. package/web/js/features/phone.js +124 -0
  105. package/web/js/features/places.js +38 -0
  106. package/web/js/features/projects.js +284 -0
  107. package/web/js/features/runner-picker.js +233 -0
  108. package/web/js/features/runners.js +163 -0
  109. package/web/js/features/scroll.js +75 -0
  110. package/web/js/features/settings.js +390 -0
  111. package/web/js/features/setup.js +94 -0
  112. package/web/js/features/ship.js +122 -0
  113. package/web/js/features/team.js +386 -0
  114. package/web/js/features/turns.js +432 -0
  115. package/web/js/features/update.js +48 -0
  116. package/web/js/features/usage.js +75 -0
  117. package/web/js/mention-text.js +59 -0
  118. package/web/js/ui/kbd.js +46 -0
  119. package/web/js/ui/overlay.js +38 -0
  120. package/web/js/ui/popover.js +52 -0
  121. package/web/js/ui/toast.js +23 -0
  122. package/web/css/app.css +0 -1910
package/server/update.js CHANGED
@@ -9,6 +9,7 @@ import { spawn } from 'node:child_process';
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
+ import { bad } from './util.js';
12
13
 
13
14
  const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
14
15
  const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
@@ -79,8 +80,8 @@ export class Updater {
79
80
  // Installs (global) then hands off to a detached helper that waits for this server's port to
80
81
  // free up and starts the new version. The caller shuts this process down.
81
82
  async install() {
82
- if (this.method === 'git') throw Object.assign(new Error('This is a git checkout: run `git pull` to update.'), { status: 400 });
83
- if (this.state.status === 'installing') throw Object.assign(new Error('Already updating.'), { status: 409 });
83
+ if (this.method === 'git') throw bad('This is a git checkout: run `git pull` to update.');
84
+ if (this.state.status === 'installing') throw bad('Already updating.', 409);
84
85
  this.set({ status: 'installing', error: null });
85
86
  try {
86
87
  if (this.method === 'global') await run(`npm install -g ${PKG.name}@latest`);
@@ -109,7 +110,7 @@ function run(cmd) {
109
110
  child.on('exit', (code) => {
110
111
  if (code === 0) return resolve();
111
112
  const hint = /EACCES|permission denied/i.test(err) ? ' (permission denied — run it yourself, maybe with sudo)' : '';
112
- reject(Object.assign(new Error(`\`${cmd}\` failed${hint}: ${err.trim().split('\n').slice(-3).join(' ')}`), { status: 400 }));
113
+ reject(bad(`\`${cmd}\` failed${hint}: ${err.trim().split('\n').slice(-3).join(' ')}`));
113
114
  });
114
115
  });
115
116
  }
package/server/usage.js CHANGED
@@ -77,3 +77,97 @@ export function usage(sessions, { days = 30, now = Date.now(), project = () => n
77
77
  runners: sorted(byRunner),
78
78
  };
79
79
  }
80
+
81
+ // Tool calls across sessions, for the agent-facing `wisp_stats` tool: how often each kind of
82
+ // action ran (run, read, edit, search, web, …), per harness, project and day. Kinds are wisp's
83
+ // normalized activity kinds, not the harness's own tool names. Spend and success come from usage().
84
+ const NOT_TOOLS = new Set(['text', 'error', 'answer']);
85
+ const tally = () => ({ calls: 0, kinds: {} });
86
+ const count = (b, kind) => {
87
+ b.calls++;
88
+ b.kinds[kind] = (b.kinds[kind] || 0) + 1;
89
+ };
90
+ const ranked = (m) => [...m.values()].sort((a, b) => b.calls - a.calls);
91
+
92
+ // filter: { agent, project } — agent is a harness id, project a project id or folder name.
93
+ export function toolStats(sessions, { days = 30, now = Date.now(), project = () => null, filter = {} } = {}) {
94
+ const keep = sessions
95
+ .filter((s) => {
96
+ if (!filter.project) return true;
97
+ const p = project(s);
98
+ return p?.id === filter.project || (p?.name || String(s.cwd || '').split(/[\\/]/).pop()) === filter.project;
99
+ })
100
+ .map((s) => (filter.agent ? { ...s, turns: (s.turns || []).filter((t) => (t.agent || s.agent) === filter.agent) } : s));
101
+ const u = usage(keep, { days, now, project });
102
+ const since = u.since;
103
+
104
+ const total = tally();
105
+ const byAgent = new Map();
106
+ const byProject = new Map();
107
+ const byDay = new Map(u.daily.map((d) => [d.date, { date: d.date, ...tally() }]));
108
+ for (const s of keep) {
109
+ if (s.agent === 'wisp') continue;
110
+ const p = project(s);
111
+ for (const t of s.turns || []) {
112
+ if (!t.startedAt || t.startedAt < since || t.status === 'running') continue;
113
+ const agent = t.agent || s.agent;
114
+ const pk = p?.id || s.cwd;
115
+ if (!byAgent.has(agent)) byAgent.set(agent, { agent, ...tally() });
116
+ if (!byProject.has(pk)) byProject.set(pk, { id: p?.id || null, name: p?.name || String(s.cwd || '').split(/[\\/]/).pop(), ...tally() });
117
+ for (const a of t.activity || []) {
118
+ if (!a?.kind || NOT_TOOLS.has(a.kind)) continue;
119
+ count(total, a.kind);
120
+ count(byAgent.get(agent), a.kind);
121
+ count(byProject.get(pk), a.kind);
122
+ const d = byDay.get(dayKey(a.t || t.startedAt));
123
+ if (d) count(d, a.kind);
124
+ }
125
+ }
126
+ }
127
+ const outcome = (b) => ({ turns: b.turns, cost: b.cost, priced: b.priced, successRate: b.successRate });
128
+ const runs = new Map();
129
+ for (const r of u.runners) {
130
+ const b = runs.get(r.agent) || { turns: 0, done: 0, cost: 0, priced: 0 };
131
+ runs.set(r.agent, { turns: b.turns + r.turns, done: b.done + r.done, cost: b.cost + r.cost, priced: b.priced + r.priced });
132
+ }
133
+ const withRuns = (a) => {
134
+ const r = runs.get(a.agent) || { turns: 0, done: 0, cost: 0, priced: 0 };
135
+ return { ...a, turns: r.turns, cost: Math.round(r.cost * 10000) / 10000, priced: r.priced, successRate: r.turns ? r.done / r.turns : null };
136
+ };
137
+ const projectRuns = new Map(u.projects.map((p) => [p.id || p.name, p]));
138
+ return {
139
+ days: u.days,
140
+ since,
141
+ filter: { agent: filter.agent || null, project: filter.project || null },
142
+ total: { ...total, sessions: u.total.sessions, ...outcome(u.total), inputTokens: u.total.inputTokens, outputTokens: u.total.outputTokens, questions: u.total.questions },
143
+ kinds: Object.entries(total.kinds).map(([kind, calls]) => ({ kind, calls })).sort((a, b) => b.calls - a.calls),
144
+ agents: ranked(byAgent).map(withRuns),
145
+ projects: ranked(byProject).map((p) => ({ ...p, ...outcome(projectRuns.get(p.id || p.name) || { turns: 0, cost: 0, priced: 0, successRate: null }) })),
146
+ daily: [...byDay.values()],
147
+ };
148
+ }
149
+
150
+ // toolStats() as markdown for an agent to read.
151
+ export function toolStatsText(st) {
152
+ const pct = (r) => (r === null || r === undefined ? '—' : `${Math.round(r * 100)}%`);
153
+ const usd = (b) => (b.priced ? `$${b.cost.toFixed(2)}${b.priced < b.turns ? ` (${b.priced}/${b.turns} turns priced)` : ''}` : '—');
154
+ const kinds = (k) => Object.entries(k).sort((a, b) => b[1] - a[1]).map(([n, c]) => `${n} ${c}`).join(', ') || '—';
155
+ const scope = [st.filter.agent && `harness ${st.filter.agent}`, st.filter.project && `project ${st.filter.project}`].filter(Boolean).join(', ');
156
+ const t = st.total;
157
+ const out = [
158
+ `## wisp tool stats — last ${st.days} day${st.days === 1 ? '' : 's'}${scope ? ` (${scope})` : ''}`,
159
+ `${t.calls} tool calls in ${t.turns} turns across ${t.sessions} sessions. Success rate ${pct(t.successRate)}, cost ${usd(t)}, ${t.questions} questions to the user, ${t.inputTokens} input / ${t.outputTokens} output tokens.`,
160
+ '',
161
+ 'Kinds are wisp’s categories, not harness tool names: run = shell commands, read / edit / search = files, web = fetch and search, agent = subagents, tool = other (MCP) tools, ask = questions to the user, wait = waiting on the user or a background task.',
162
+ ];
163
+ if (!t.turns) return [...out, '', 'No finished turns in this range.'].join('\n');
164
+ out.push('', '### By kind', '| kind | calls | share |', '|---|---:|---:|');
165
+ for (const k of st.kinds) out.push(`| ${k.kind} | ${k.calls} | ${t.calls ? Math.round((k.calls / t.calls) * 100) : 0}% |`);
166
+ out.push('', '### By harness', '| harness | calls | turns | success | cost | top kinds |', '|---|---:|---:|---:|---:|---|');
167
+ for (const a of st.agents) out.push(`| ${a.agent} | ${a.calls} | ${a.turns} | ${pct(a.successRate)} | ${usd(a)} | ${kinds(a.kinds)} |`);
168
+ out.push('', '### By project', '| project | calls | turns | success | cost | top kinds |', '|---|---:|---:|---:|---:|---|');
169
+ for (const p of st.projects) out.push(`| ${p.name} | ${p.calls} | ${p.turns} | ${pct(p.successRate)} | ${usd(p)} | ${kinds(p.kinds)} |`);
170
+ out.push('', '### By day (days with activity)', '| day | calls | kinds |', '|---|---:|---|');
171
+ for (const d of st.daily.filter((d) => d.calls)) out.push(`| ${d.date} | ${d.calls} | ${kinds(d.kinds)} |`);
172
+ return out.join('\n');
173
+ }
package/server/util.js ADDED
@@ -0,0 +1,42 @@
1
+ // Small helpers shared across the server. Nothing here imports another wisp module, so any
2
+ // file can use it without dragging in the runner, the store or an adapter.
3
+
4
+ import { randomUUID } from 'node:crypto';
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+
9
+ export const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
10
+ export const DEFAULT_PORT = 4477;
11
+ // The modes a session can run in: read-only planning, edits with approval rules, or full auto.
12
+ export const MODES = new Set(['plan', 'edits', 'auto']);
13
+
14
+ export const newId = () => randomUUID().replace(/-/g, '').slice(0, 12);
15
+
16
+ // An error the HTTP layer answers with `status` (400 unless said otherwise) and its message.
17
+ export const bad = (msg, status = 400) => Object.assign(new Error(msg), { status });
18
+
19
+ // ~ and ~/x → the home folder. `expandHome` keeps the string as typed otherwise; `expand` also resolves it.
20
+ export const expandHome = (p) => String(p).replace(/^~(?=$|[\\/])/, os.homedir());
21
+ export const expand = (p) => path.resolve(expandHome(p));
22
+
23
+ // wisp's own copies of harness CLIs, installed from the app when there's none (~/.wisp/tools).
24
+ export const toolsDir = () => path.join(process.env.WISP_HOME || path.join(os.homedir(), '.wisp'), 'tools');
25
+
26
+ // Single-quote for a POSIX shell.
27
+ export const shq = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
28
+
29
+ // One line, at most n characters (whitespace collapsed).
30
+ export function clip(s, n = 80) {
31
+ s = String(s ?? '').replace(/\s+/g, ' ').trim();
32
+ return s.length > n ? `${s.slice(0, n - 1)}…` : s;
33
+ }
34
+
35
+ // Write JSON so a crash mid-write never leaves a half file behind: tmp file, then rename.
36
+ export function writeJsonAtomic(file, data, space) {
37
+ fs.writeFileSync(`${file}.tmp`, JSON.stringify(data, null, space));
38
+ fs.renameSync(`${file}.tmp`, file);
39
+ }
40
+
41
+ // At most n characters, line breaks kept (messages, not one-liners).
42
+ export const truncate = (s, n) => (String(s || '').length > n ? `${String(s).slice(0, n - 1)}…` : String(s || ''));
package/server/welcome.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // using the same blocks an agent would. Doubles as a living block gallery.
3
3
 
4
4
  import os from 'node:os';
5
- import { newId } from './runner.js';
5
+ import { newId } from './util.js';
6
6
 
7
7
  export function welcomeSession() {
8
8
  const now = Date.now();
@@ -129,8 +129,8 @@ export function welcomeSession() {
129
129
  title: 'Permission modes (pick per launch)',
130
130
  items: [
131
131
  { text: 'Plan — read-only, the agent proposes', done: true },
132
- { text: 'Edits — the agent may change files, no shell prompts', done: true },
133
- { text: 'Full auto — no permission prompts at all; use on trusted repos', done: false },
132
+ { text: 'Edits — the agent may change files; anything else it needs, you approve', done: true },
133
+ { text: 'Full auto — no permission prompts at all; the default, each task in its own worktree', done: true },
134
134
  ],
135
135
  },
136
136
  {
@@ -0,0 +1,29 @@
1
+ /* Auto: wisp's router as a harness (picker, card badge, Settings → Auto) */
2
+ .auto-mark { display: inline-flex; align-items: center; justify-content: center; color: var(--accent); }
3
+ .auto-mark .i { width: 100%; height: 100%; }
4
+ .model-logo.routed::before { content: ''; position: absolute; right: -3px; bottom: -3px; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 1.5px var(--surface); }
5
+ .pk-note { margin: 8px 10px 4px; font-size: 11.5px; line-height: 1.45; color: var(--muted); }
6
+ .pk-note a { color: var(--accent-ink); }
7
+ .ro-goal { flex: none; }
8
+ .ro-allowed { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px; max-width: 360px; }
9
+ .chip-tog { display: inline-flex; align-items: center; gap: 5px; padding: 3px 8px; border-radius: 7px; border: 1px solid var(--line); font-size: 12px; color: var(--muted); }
10
+ .chip-tog:hover { color: var(--ink); }
11
+ .chip-tog.on { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-ink); }
12
+ .router-modal { width: min(720px, 100%); position: relative; padding-bottom: 20px; }
13
+ .router-modal .hint { margin: 0 0 12px; }
14
+ .router-modal h4 { margin: 16px 0 6px; }
15
+ .ro-table td small.ro-types { display: block; margin: 2px 0 0 19px; font-size: 11px; color: var(--faint); }
16
+ .ro-table .sx-state { justify-content: flex-end; }
17
+ .sx-state.ok { color: var(--good); }
18
+ .sx-state.warn { color: var(--warn); }
19
+ .dot.warn { background: var(--warn); }
20
+ .ro-recent { list-style: none; margin: 0; padding: 0; }
21
+ .ro-recent li { display: flex; align-items: center; gap: 8px; padding: 6px 2px; border-bottom: 1px solid var(--line); font-size: 12.5px; }
22
+ .ro-recent .ro-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); }
23
+ .ro-recent small { flex: none; color: var(--faint); }
24
+ .ro-recent .ok { color: var(--good); }
25
+ .ro-recent .warn { color: var(--warn); }
26
+ .ro-recent .bad { color: var(--bad); }
27
+ .ro-recent .dim { color: var(--faint); }
28
+ .ro-foot { display: flex; align-items: center; gap: 10px; margin-top: 16px; }
29
+ .ro-foot .hint { margin: 0; }
@@ -0,0 +1,71 @@
1
+ /* ---------- automations, usage, phone ---------- */
2
+ .auto-modal, .usage-modal, .phone-modal { width: min(720px, 100%); position: relative; }
3
+ .usage-modal { padding-bottom: 24px; }
4
+ .mem-lede code { font-size: 11.5px; padding: 1px 5px; border-radius: 5px; background: var(--bg-2); }
5
+ .auto-body { padding-top: 10px; }
6
+ .au-list { display: flex; flex-direction: column; gap: 2px; }
7
+ .au-item { display: flex; align-items: flex-start; gap: 10px; padding: 10px 8px; border-radius: 10px; }
8
+ .au-item:hover { background: var(--surface-2); }
9
+ .au-item.off .au-main { opacity: 0.55; }
10
+ .au-main { flex: 1; min-width: 0; }
11
+ .au-main b { font-size: 13.5px; }
12
+ .au-main p { margin: 2px 0 0; font-size: 12.5px; color: var(--muted); }
13
+ .au-main code { font-size: 11.5px; padding: 0 4px; border-radius: 4px; background: var(--bg-2); }
14
+ .au-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
15
+ .au-tags i { font-style: normal; font-size: 11px; padding: 1px 6px; border-radius: 5px; background: var(--bg-2); color: var(--ink-2); }
16
+ .au-tags a { color: var(--accent-ink); }
17
+ .au-err { display: flex; align-items: center; gap: 5px; color: var(--bad) !important; }
18
+ .au-err .i { width: 12px; height: 12px; }
19
+ .au-acts { display: flex; align-items: center; gap: 4px; flex: none; }
20
+ .au-form { display: flex; flex-direction: column; gap: 0; }
21
+ .au-form > label, .au-form > div > label, .au-row { display: flex; align-items: center; gap: 14px; padding: 8px 0; border-bottom: 1px solid var(--line); }
22
+ .au-form label > span, .au-row > span { width: 92px; flex: none; font-size: 12.5px; font-weight: 500; color: var(--ink-2); }
23
+ .au-form label.stack { flex-direction: column; align-items: stretch; gap: 6px; }
24
+ .au-form label.stack > span { width: auto; }
25
+ .au-form :is(.input, .textarea, select) { flex: 1; }
26
+ .au-form [hidden] { display: none !important; }
27
+ .au-params { margin: 6px 0 2px; }
28
+ .au-params code, .k-vars code { font-size: 11px; }
29
+ .au-runner { display: flex; align-items: center; gap: 10px; }
30
+ .au-presets { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 6px 0 4px 106px; }
31
+ .au-kinds { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-bottom: 6px; }
32
+ .au-kinds button { display: grid; grid-template-columns: 16px 1fr; column-gap: 8px; text-align: left; padding: 8px 10px; border-radius: 10px; border: 1px solid var(--line); }
33
+ .au-kinds button .i { grid-row: span 2; margin-top: 2px; color: var(--muted); }
34
+ .au-kinds button b { font-size: 13px; }
35
+ .au-kinds button small { font-size: 11px; color: var(--muted); }
36
+ .au-kinds button.on { border-color: var(--accent); background: var(--accent-soft); }
37
+ .au-kinds button.on .i { color: var(--accent); }
38
+ .au-warn { display: flex; gap: 8px; margin: 10px 0 0; padding: 8px 10px; border-radius: 9px; font-size: 12.5px; color: var(--warn); background: color-mix(in srgb, var(--warn) 8%, transparent); }
39
+ .au-warn .i { flex: none; margin-top: 1px; }
40
+ .au-foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
41
+
42
+ .usage-modal header { gap: 12px; }
43
+ .usage-modal header h2 { margin-right: auto; }
44
+ .us-tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
45
+ .us-tiles div { display: flex; flex-direction: column; padding: 10px 12px; border-radius: 11px; border: 1px solid var(--line); background: var(--surface-2); }
46
+ .us-tiles span { font-size: 11.5px; color: var(--muted); }
47
+ .us-tiles b { font: 600 22px/1.3 var(--display); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
48
+ .us-tiles small { font-size: 11px; color: var(--faint); }
49
+ .us-chart { display: flex; align-items: flex-end; gap: 2px; height: 120px; padding-bottom: 1px; border-bottom: 1px solid var(--line-2); }
50
+ .us-bar { flex: 1; height: 100%; display: flex; align-items: flex-end; cursor: default; }
51
+ .us-bar i { display: block; width: 100%; border-radius: 4px 4px 0 0; background: var(--accent); opacity: 0.85; }
52
+ .us-bar:hover i { opacity: 1; }
53
+ .us-axis { display: flex; justify-content: space-between; margin-top: 4px; font-size: 11px; color: var(--faint); }
54
+ .us-tip { position: absolute; z-index: 5; transform: translate(-50%, -100%); padding: 3px 7px; border-radius: 6px; background: var(--ink); color: var(--bg); font-size: 11px; white-space: nowrap; pointer-events: none; }
55
+ .us-table { width: 100%; border-collapse: collapse; font-size: 12.5px; font-variant-numeric: tabular-nums; }
56
+ .us-table th { text-align: right; font-weight: 500; font-size: 11px; color: var(--faint); padding: 4px 6px; border-bottom: 1px solid var(--line); }
57
+ .us-table td { text-align: right; padding: 6px; border-bottom: 1px solid var(--line); color: var(--ink-2); }
58
+ .us-table :is(th, td):first-child { text-align: left; color: var(--ink); }
59
+ .us-proj, .us-runner { display: inline-flex; align-items: center; gap: 6px; }
60
+
61
+ .phone-modal .ph-sec h4 { display: flex; align-items: center; gap: 8px; }
62
+ .phone-modal .ph-sec h4 .switch { margin-left: auto; }
63
+ .phone-modal .settings-modal { width: auto; padding: 0; border: 0; box-shadow: none; background: none; }
64
+ .settings-modal label.check { padding: 0; border: 0; gap: 6px; }
65
+ .ph-ok, .ph-bad { display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px; font-weight: 500; }
66
+ .ph-ok { color: var(--good); }
67
+ .ph-bad { color: var(--bad); max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
68
+ .ph-ok .i, .ph-bad .i { width: 12px; height: 12px; }
69
+ .ph-start { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); }
70
+ .ph-row { display: flex; align-items: center; gap: 8px; font-size: 13px; margin-bottom: 6px; }
71
+
@@ -0,0 +1,83 @@
1
+ /* ---------- buttons ---------- */
2
+ .primary {
3
+ display: inline-flex; align-items: center; gap: 8px; height: 36px; padding: 0 14px 0 16px;
4
+ border-radius: 8px; background: var(--ink); color: var(--bg); font-weight: 500;
5
+ transition: opacity 0.15s;
6
+ }
7
+ .primary:hover { opacity: 0.86; }
8
+ .primary:disabled { opacity: 0.5; }
9
+ .primary kbd { background: transparent; color: inherit; border-color: color-mix(in srgb, var(--bg) 35%, transparent); opacity: 0.7; }
10
+ .primary.sm { height: 30px; padding: 0 12px; font-size: 13px; border-radius: 9px; }
11
+ .ghost {
12
+ display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 10px; border-radius: 9px;
13
+ color: var(--ink-2); transition: background 0.15s, color 0.15s;
14
+ }
15
+ .ghost:hover { background: var(--bg-2); color: var(--ink); }
16
+ .ghost.on { color: var(--good); }
17
+ .ghost.sm { height: 28px; font-size: 13px; }
18
+ .ghost.xs { height: 24px; padding: 0 6px; border-radius: 7px; }
19
+ .ghost.danger:hover { color: var(--bad); background: var(--del-bg); }
20
+ .icon-btn {
21
+ display: inline-grid; place-items: center; width: 32px; height: 32px; border-radius: 9px; color: var(--muted);
22
+ transition: background 0.15s, color 0.15s;
23
+ }
24
+ .icon-btn:hover { background: var(--bg-2); color: var(--ink); }
25
+ .link { color: var(--accent-ink); font-weight: 500; }
26
+ .link:hover { text-decoration: underline; text-underline-offset: 3px; }
27
+ .link.on { color: var(--ink); }
28
+ /* ---------- form kit ----------
29
+ One look for every standalone control. Use these classes, don't restyle controls per component:
30
+ <input class="input"> <textarea class="textarea"> <select> (styled by default)
31
+ .mono code-ish values: paths, cron, tokens, commands
32
+ .sm / .lg 30px rows in dense panels / the 38px add-project row
33
+ .field a box holding an icon / prefix + a bare <input>: <label class="field">icon<input></label>
34
+ .check a label wrapping a checkbox and its text
35
+ Components set only layout (width, flex) on these; a container already tinted surface-2 sets
36
+ --field-bg: var(--surface) so its fields don't melt into it. Prompt boxes (launcher, composer, rerun) and
37
+ inline renames (project name, memory edit) are their own, deliberately borderless pattern. */
38
+ :is(.input, .textarea, select, .field) {
39
+ width: 100%; min-width: 0; height: 32px; padding: 0 10px; border-radius: 9px; border: 1px solid var(--line-2);
40
+ background-color: var(--field-bg, var(--surface-2)); color: var(--ink); font: 13px/1.4 var(--sans); outline: 0;
41
+ transition: border-color 0.15s, box-shadow 0.15s, background-color 0.15s;
42
+ }
43
+ :is(.input, .textarea, select, .field):hover { border-color: color-mix(in srgb, var(--ink) 14%, var(--line-2)); }
44
+ :is(.input, .textarea, select):focus, .field:focus-within {
45
+ border-color: var(--accent); background-color: var(--surface); box-shadow: 0 0 0 3px var(--accent-soft);
46
+ }
47
+ :is(.input, .textarea, .field input)::placeholder { color: var(--faint); }
48
+ :is(.input, .textarea, select):disabled { opacity: 0.55; cursor: not-allowed; }
49
+ :is(.input, .textarea, select):is([aria-invalid='true'], :user-invalid) { border-color: var(--bad); }
50
+ :is(.input, .textarea, select):is([aria-invalid='true'], :user-invalid):focus { box-shadow: 0 0 0 3px var(--del-bg); }
51
+ .textarea { display: block; height: auto; min-height: 64px; padding: 7px 10px; line-height: 1.5; resize: vertical; }
52
+ :is(.input, .textarea, .field).mono { font-family: var(--mono); font-size: 12.5px; }
53
+ :is(.input, .field).sm { height: 30px; padding: 0 8px; }
54
+ :is(.input, .field).lg { height: 38px; padding: 0 12px; border-radius: 11px; }
55
+ select:not([multiple]) {
56
+ width: auto; padding-right: 28px; appearance: none; cursor: pointer; text-overflow: ellipsis;
57
+ background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%);
58
+ background-position: calc(100% - 14px) 50%, calc(100% - 10px) 50%; background-size: 4px 4px; background-repeat: no-repeat;
59
+ }
60
+ .field { display: flex; align-items: center; gap: 8px; color: var(--muted); }
61
+ .field .i { width: 14px; height: 14px; flex: none; }
62
+ .field > input { flex: 1; min-width: 0; height: 100%; padding: 0; border: 0; outline: 0; background: none; font: inherit; color: var(--ink); }
63
+ input[type='checkbox'] { width: 15px; height: 15px; margin: 0; flex: none; accent-color: var(--accent); cursor: pointer; }
64
+ .check { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-2); cursor: pointer; }
65
+
66
+ /* ---------- the mark ---------- */
67
+ /* A plain dot. Brand: ink dot with an offset accent dot. Live: a thin spinner ring. */
68
+ .orb { --s: 14px; position: relative; display: inline-block; width: var(--s); height: var(--s); border-radius: 50%; background: var(--ink); flex: none; }
69
+ .orb i { display: none; }
70
+ .orb.tiny { --s: 12px; }
71
+ .orb.tiny::after {
72
+ content: ''; position: absolute; width: 6px; height: 6px; right: -4px; bottom: -2px; border-radius: 50%;
73
+ background: var(--accent); box-shadow: 0 0 0 2px var(--bg);
74
+ }
75
+ .orb.big { display: none; }
76
+ .orb.live {
77
+ --s: 18px; background: none; border: 2px solid var(--line-2); border-top-color: var(--accent);
78
+ animation: orb-spin 0.9s linear infinite;
79
+ }
80
+ .waiting .orb.live { border: 0; background: var(--warn); --s: 10px; margin: 4px; animation: pulse 1.6s ease-in-out infinite; }
81
+
82
+ @keyframes orb-spin { to { rotate: 360deg; } }
83
+