wisp-agents 0.8.0 → 0.10.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.
package/server/mcp-ask.js CHANGED
@@ -38,7 +38,7 @@ const TEAM_TOOLS = [
38
38
  'Hand tasks to your subagents (separate coding agents the user picked, each on its own harness and model). ' +
39
39
  'Tasks in one call run in parallel in your working folder. By default the call waits until every task finishes and ' +
40
40
  'returns each report plus the files it changed; wait=false returns delegation ids at once. Delegating to a subagent ' +
41
- 'again continues its conversation.\n\nThe team:\n' + roster,
41
+ 'again continues its conversation. Use this instead of any built-in subagent or task tool.\n\nThe team:\n' + roster,
42
42
  inputSchema: {
43
43
  type: 'object',
44
44
  properties: {
@@ -1,5 +1,5 @@
1
1
  // `wisp mcp`: a stdio MCP server that lets any MCP-capable agent or editor (Claude Code, Codex,
2
- // opencode, Cursor, Antigravity, Claude Desktop, Zed, …) hand tasks to wisp and read the results back.
2
+ // opencode, Cursor, Antigravity, Grok, Claude Desktop, Zed, …) hand tasks to wisp and read the results back.
3
3
  // It talks to the running wisp over HTTP (starting a headless one if needed) via the client SDK.
4
4
  //
5
5
  // claude mcp add wisp -- npx -y wisp-agents mcp
@@ -9,7 +9,7 @@ import fs from 'node:fs';
9
9
  import { WispClient } from './client.js';
10
10
 
11
11
  const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
12
- const AGENTS = ['claude', 'codex', 'opencode', 'cursor', 'antigravity'];
12
+ const AGENTS = ['claude', 'codex', 'opencode', 'cursor', 'antigravity', 'grok'];
13
13
  const DEFAULT_WAIT_S = 240; // stay under typical MCP client tool-call timeouts; call wisp_wait again for more
14
14
 
15
15
  const TOOLS = [
package/server/openapi.js CHANGED
@@ -16,7 +16,7 @@ export function openapi({ version, port }) {
16
16
  title: 'wisp',
17
17
  version,
18
18
  description:
19
- 'Local API of wisp, a launcher and queue for coding agents (Claude Code, Codex, opencode, Cursor, Antigravity). ' +
19
+ 'Local API of wisp, a launcher and queue for coding agents (Claude Code, Codex, opencode, Cursor, Antigravity, Grok). ' +
20
20
  'Binds to 127.0.0.1 only. Requests must carry a Host of 127.0.0.1/localhost:<port>; non-GET requests must be ' +
21
21
  'application/json (uploads: application/octet-stream) and, if they send an Origin, a local one. ' +
22
22
  'The running server advertises { url, port, pid, version } in ~/.wisp/server.json.',
@@ -143,6 +143,22 @@ export function openapi({ version, port }) {
143
143
  responses: { 200: json({ type: 'object' }), 400: err[400] },
144
144
  },
145
145
  },
146
+ '/machines': {
147
+ get: { summary: 'Machines (other computers over SSH) with connection state, steps, hints and their wisp version and harnesses', responses: { 200: json({ type: 'array', items: { type: 'object' } }) } },
148
+ post: {
149
+ summary: 'Add a machine and connect (unless connect: false). wisp starts itself there over SSH and tunnels back',
150
+ requestBody: body({ type: 'object', required: ['target'], properties: { target: { type: 'string', description: 'user@host, a host, or a Host from ~/.ssh/config' }, name: { type: 'string' }, port: { type: 'integer', default: 4477, description: 'wisp port on the machine' }, command: { type: 'string', description: 'Start command; default npx -y wisp-agents@<this version>' }, autoConnect: { type: 'boolean', default: true }, connect: { type: 'boolean', default: true } } }),
151
+ responses: { 201: json({ type: 'object' }), 400: err[400] },
152
+ },
153
+ },
154
+ '/machines/discover': { get: { summary: 'Hosts this computer can reach: ~/.ssh/config, Tailscale peers, ~/.ssh/known_hosts', responses: { 200: json({ type: 'array', items: { type: 'object' } }) } } },
155
+ '/machines/ssh-hosts': { get: { summary: 'Hosts from ~/.ssh/config, for suggestions', responses: { 200: json({ type: 'array', items: { type: 'object' } }) } } },
156
+ '/machines/{mid}': { parameters: [{ name: 'mid', in: 'path', required: true, schema: { type: 'string' } }], patch: { summary: 'Edit a machine (host, port or command changes reconnect)', responses: { 200: json({ type: 'object' }) } }, delete: { summary: 'Remove a machine and its projects here (nothing on the machine is touched)', responses: { 200: json({ type: 'object' }) } } },
157
+ '/machines/{mid}/connect': { parameters: [{ name: 'mid', in: 'path', required: true, schema: { type: 'string' } }], post: { summary: 'Connect: SSH in, start wisp there if needed, open the tunnel', responses: { 200: json({ type: 'object' }) } } },
158
+ '/machines/{mid}/disconnect': { parameters: [{ name: 'mid', in: 'path', required: true, schema: { type: 'string' } }], post: { summary: 'Close the tunnel; wisp and its agents keep running there', responses: { 200: json({ type: 'object' }) } } },
159
+ '/machines/{mid}/stop': { parameters: [{ name: 'mid', in: 'path', required: true, schema: { type: 'string' } }], post: { summary: 'Stop wisp on the machine (its agents too); restart: true starts this version in its place', requestBody: body({ type: 'object', properties: { restart: { type: 'boolean' } } }), responses: { 200: json({ type: 'object' }) } } },
160
+ '/machines/{mid}/fs': { parameters: [{ name: 'mid', in: 'path', required: true, schema: { type: 'string' } }, { name: 'q', in: 'query', schema: { type: 'string' } }], get: { summary: 'Folder suggestions on the machine', responses: { 200: json({ type: 'array', items: { type: 'string' } }) } } },
161
+ '/machines/{mid}/discover': { parameters: [{ name: 'mid', in: 'path', required: true, schema: { type: 'string' } }], get: { summary: 'Git repos found on the machine that are not projects yet', responses: { 200: json({ type: 'array', items: { type: 'object' } }) } } },
146
162
  '/remote/test': { post: { summary: 'Send a test message to the enabled channels', responses: { 200: json({ type: 'object' }), 400: err[400] } } },
147
163
  '/usage': {
148
164
  get: {
@@ -231,7 +247,7 @@ export function openapi({ version, port }) {
231
247
  properties: {
232
248
  prompt: { type: 'string' },
233
249
  cwd: { type: 'string', description: 'Project folder (absolute; ~ allowed). Default: the server’s cwd' },
234
- agent: { enum: ['claude', 'codex', 'opencode', 'cursor', 'antigravity'], default: 'claude' },
250
+ agent: { enum: ['claude', 'codex', 'opencode', 'cursor', 'antigravity', 'grok'], default: 'claude' },
235
251
  mode: { enum: ['plan', 'edits', 'auto'], default: 'edits' },
236
252
  model: { type: 'string' },
237
253
  effort: { type: 'string', description: 'Thinking level: low, medium, high, xhigh, max (per harness)' },
package/server/remote.js CHANGED
@@ -20,6 +20,7 @@ export class Remote {
20
20
  constructor({ store, act, onChange = () => {} }) {
21
21
  Object.assign(this, { store, act, onChange });
22
22
  this.status = { ntfy: null, telegram: null };
23
+ this.tgElsewhere = new Map(); // Telegram message id -> session id, for sessions on a machine (server/machines.js)
23
24
  this.ac = null;
24
25
  this.ntfyPollMs = 4000;
25
26
  this.tgLongPoll = 25;
@@ -188,9 +189,12 @@ export class Remote {
188
189
  text: clip(`${msg.title}\n\n${msg.text}${s ? '\n\n↩ Reply to this message to answer or follow up.' : ''}`, 4000),
189
190
  ...(buttons.length && { reply_markup: { inline_keyboard: buttons } }),
190
191
  });
191
- if (s && sent?.message_id) {
192
+ if (s && sent?.message_id && this.store.get(s.id) === s) {
192
193
  s.remote = { ...s.remote, tg: [...(s.remote?.tg || []), sent.message_id].slice(-MAX_TG_IDS) };
193
194
  this.store.save(s);
195
+ } else if (s && sent?.message_id) {
196
+ this.tgElsewhere.set(sent.message_id, s.id);
197
+ if (this.tgElsewhere.size > 200) this.tgElsewhere.delete(this.tgElsewhere.keys().next().value);
194
198
  }
195
199
  }
196
200
 
@@ -238,9 +242,9 @@ export class Remote {
238
242
  }
239
243
  if (String(m.chat.id) !== String(t.chatId)) return;
240
244
  const replyTo = m.reply_to_message?.message_id;
241
- const s = replyTo && this.store.all().find((x) => x.remote?.tg?.includes(replyTo));
242
- if (!s) return say('Reply to one of wisp’s messages to answer it or send a follow-up.');
243
- await say(await this.act({ sessionId: s.id, kind: 'text', text: m.text }), { reply_to_message_id: m.message_id });
245
+ const sessionId = replyTo && (this.store.all().find((x) => x.remote?.tg?.includes(replyTo))?.id || this.tgElsewhere.get(replyTo));
246
+ if (!sessionId) return say('Reply to one of wisp’s messages to answer it or send a follow-up.');
247
+ await say(await this.act({ sessionId, kind: 'text', text: m.text }), { reply_to_message_id: m.message_id });
244
248
  }
245
249
 
246
250
  // For the settings view: config with secrets masked, plus channel health.
package/server/runner.js CHANGED
@@ -67,6 +67,7 @@ export function summarize(s, store) {
67
67
  asks: !!s.ask || !!last?.presentation?.blocks?.some((b) => b.type === 'question'),
68
68
  question: s.ask?.question || '',
69
69
  branch: s.worktree?.branch || '',
70
+ toShip: s.toShip || null, // { dirty, ahead, unpushed } of the worktree, null until checked
70
71
  pr: s.pr ? { number: s.pr.number, url: s.pr.url, state: s.pr.state, draft: s.pr.draft, review: s.pr.review, checks: s.pr.checks } : null,
71
72
  groupId: s.groupId || null,
72
73
  origin: s.origin || null, // { kind: 'recipe' | 'trigger', id, name }
@@ -351,6 +352,8 @@ export class Runner extends EventEmitter {
351
352
  model: session.model,
352
353
  effort: session.effort || '',
353
354
  systemPrompt: prompt0 + workspaceNote(session) + this.promptExtra(session),
355
+ // A lead with a team delegates through wisp, so the harness's own subagent tool is switched off.
356
+ delegating: team.length > 0 && !!this.bridge,
354
357
  mcpServers: this.bridge && adapter.askTool !== null && !session.parentId
355
358
  ? {
356
359
  wisp: {
package/server/store.js CHANGED
@@ -7,12 +7,13 @@ import { randomUUID } from 'node:crypto';
7
7
  import { parsePresentation } from './protocol.js';
8
8
 
9
9
  const DEFAULT_CONFIG = {
10
- projects: [], // [{ id, name, path, color, addedAt }]
10
+ projects: [], // [{ id, name, path, color, addedAt, machine? }]; machine: the server/machines.js id it lives on
11
11
  defaults: { agent: 'claude', mode: 'edits', model: '', effort: '' },
12
12
  recipes: [], // saved prompts with {params}: server/recipes.js
13
13
  triggers: [], // schedules and GitHub watchers: server/triggers.js
14
14
  teams: [], // saved subagent presets: server/subagents.js
15
15
  remote: {}, // phone channels (ntfy, Telegram): server/remote.js
16
+ machines: [], // other computers reached over SSH: server/machines.js
16
17
  };
17
18
 
18
19
  const IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg']);
@@ -55,6 +56,7 @@ export class Store {
55
56
  cfg.triggers ||= [];
56
57
  cfg.teams ||= [];
57
58
  cfg.remote ||= {};
59
+ cfg.machines ||= [];
58
60
  return cfg;
59
61
  } catch {
60
62
  return structuredClone(DEFAULT_CONFIG);
@@ -67,21 +69,24 @@ export class Store {
67
69
 
68
70
  // --- projects: plain folders, matched to sessions by path prefix ---
69
71
 
70
- projectFor(cwd = '') {
72
+ // machine: a project on another computer (paths there are POSIX); null = this one.
73
+ projectFor(cwd = '', machine = null) {
74
+ const sep = machine ? '/' : path.sep;
71
75
  let best = null;
72
76
  for (const p of this.config.projects)
73
- if ((cwd === p.path || cwd.startsWith(p.path + path.sep)) && (!best || p.path.length > best.path.length)) best = p;
77
+ if ((p.machine || null) === machine && (cwd === p.path || cwd.startsWith(p.path + sep)) && (!best || p.path.length > best.path.length)) best = p;
74
78
  return best;
75
79
  }
76
80
 
77
- addProject(dir, { name, color } = {}) {
78
- const existing = this.config.projects.find((p) => p.path === dir);
81
+ addProject(dir, { name, color, machine } = {}) {
82
+ const existing = this.config.projects.find((p) => p.path === dir && (p.machine || null) === (machine || null));
79
83
  if (existing) return existing;
80
84
  const used = new Set(this.config.projects.map((p) => p.color));
81
85
  const project = {
82
86
  id: Math.random().toString(36).slice(2, 10),
83
- name: name || path.basename(dir) || dir,
87
+ name: name || (machine ? path.posix.basename(dir) : path.basename(dir)) || dir,
84
88
  path: dir,
89
+ ...(machine && { machine }),
85
90
  color: color || PROJECT_COLORS.find((c) => !used.has(c)) || PROJECT_COLORS[this.config.projects.length % PROJECT_COLORS.length],
86
91
  addedAt: Date.now(),
87
92
  };
@@ -65,7 +65,8 @@ Hand them work with the \`delegate\` tool (${tool}): pass one or more tasks, \`{
65
65
  ${multi ? `- A subagent with ×N has N identical copies that run at once. Put several tasks for \`${multi.name}\` in one delegate call and each goes to a free copy; name a copy (\`${copies(multi)[1]}\`) to continue that copy's conversation.\n` : ''}- Subagents work in your folder and see each other's edits. Give parallel subagents separate files or concerns.
66
66
  - Brief them like a colleague: goal, files, constraints, how to verify. They cannot see your conversation or ask the user.
67
67
  - Follow the user's "use it for" notes when you pick a subagent: send each kind of work to the one meant for it.
68
- - Use them when the user's request calls for it or when the work splits cleanly; you stay responsible for the result. Review what they changed before you report.
68
+ - These are your subagents for this session. Whenever you would spawn a subagent, sub-task or helper agent (your harness's built-in Task/Agent tool included), use \`delegate\` instead: don't use built-in subagents.
69
+ - Delegate when the user's request calls for it or when the work splits cleanly; you stay responsible for the result. Review what they changed before you report.
69
70
  - Your presentation covers the whole task; wisp shows each subagent's report next to it, so don't repeat them in full.`;
70
71
  }
71
72
 
package/server/update.js CHANGED
@@ -88,12 +88,16 @@ export class Updater {
88
88
  this.set({ status: 'idle', error: err.message });
89
89
  throw err;
90
90
  }
91
- const args = process.versions.electron ? '' : ' --browser --no-open'; // desktop: reopen the app; browser: keep the tab
92
- const node = JSON.stringify(process.versions.electron ? 'node' : process.execPath);
93
- const relaunch = this.method === 'npx' ? `npx -y ${PKG.name}@latest${args}` : `${node} ${JSON.stringify(path.join(ROOT, 'bin', 'wisp.js'))}${args}`;
94
- handOff(this.port, relaunch);
91
+ handOff(this.port, this.relaunchCommand());
95
92
  this.set({ status: 'restarting' });
96
93
  }
94
+
95
+ // Same port as now (wisp may have been started with --port), so the open tab finds it again.
96
+ relaunchCommand(electron = !!process.versions.electron) {
97
+ const args = `${electron ? '' : ' --browser --no-open'} --port ${this.port}`; // desktop: reopen the app; browser: keep the tab
98
+ const node = JSON.stringify(electron ? 'node' : process.execPath);
99
+ return this.method === 'npx' ? `npx -y ${PKG.name}@latest${args}` : `${node} ${JSON.stringify(path.join(ROOT, 'bin', 'wisp.js'))}${args}`;
100
+ }
97
101
  }
98
102
 
99
103
  function run(cmd) {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: wisp
3
- description: Delegate coding tasks to background agents (Claude Code, Codex, opencode, Cursor, Antigravity) through wisp, then read back their presentation (summary, diffs, tests). Use when the user asks to "send this to wisp", run something in the background or in parallel, fan a task out to other agents, or check on / follow up a wisp session.
3
+ description: Delegate coding tasks to background agents (Claude Code, Codex, opencode, Cursor, Antigravity, Grok) through wisp, then read back their presentation (summary, diffs, tests). Use when the user asks to "send this to wisp", run something in the background or in parallel, fan a task out to other agents, or check on / follow up a wisp session.
4
4
  ---
5
5
 
6
6
  # wisp: hand tasks to background agents
@@ -22,7 +22,7 @@ wisp "audit dependencies for known CVEs" --mode plan --agent codex --json
22
22
  wisp "add pagination to /api/users" --cwd ~/code/api --wait # block until done, print the result
23
23
  ```
24
24
 
25
- Options: `--agent claude|codex|opencode|cursor|antigravity`, `--mode plan|edits|auto`, `--model ID`,
25
+ Options: `--agent claude|codex|opencode|cursor|antigravity|grok`, `--mode plan|edits|auto`, `--model ID`,
26
26
  `--effort low|medium|high`, `--cwd DIR` (default: current folder), `--wait`, `--json`.
27
27
  `--json` prints the session summary; its `id` is what every other command takes.
28
28
 
package/web/css/app.css CHANGED
@@ -140,12 +140,43 @@ kbd {
140
140
  .link { color: var(--accent-ink); font-weight: 500; }
141
141
  .link:hover { text-decoration: underline; text-underline-offset: 3px; }
142
142
  .link.on { color: var(--ink); }
143
- select {
144
- height: 30px; border-radius: 9px; border: 1px solid var(--line); background: var(--surface); padding: 0 26px 0 10px;
145
- appearance: none; font-size: 13px; color: var(--ink-2);
143
+ /* ---------- form kit ----------
144
+ One look for every standalone control. Use these classes, don't restyle controls per component:
145
+ <input class="input"> <textarea class="textarea"> <select> (styled by default)
146
+ .mono code-ish values: paths, cron, tokens, commands
147
+ .sm / .lg 30px rows in dense panels / the 38px add-project row
148
+ .field a box holding an icon / prefix + a bare <input>: <label class="field">icon<input></label>
149
+ .check a label wrapping a checkbox and its text
150
+ Components set only layout (width, flex) on these; a container already tinted surface-2 sets
151
+ --field-bg: var(--surface) so its fields don't melt into it. Prompt boxes (launcher, composer, rerun) and
152
+ inline renames (project name, memory edit) are their own, deliberately borderless pattern. */
153
+ :is(.input, .textarea, select, .field) {
154
+ width: 100%; min-width: 0; height: 32px; padding: 0 10px; border-radius: 9px; border: 1px solid var(--line-2);
155
+ background-color: var(--field-bg, var(--surface-2)); color: var(--ink); font: 13px/1.4 var(--sans); outline: 0;
156
+ transition: border-color 0.15s, box-shadow 0.15s, background-color 0.15s;
157
+ }
158
+ :is(.input, .textarea, select, .field):hover { border-color: color-mix(in srgb, var(--ink) 14%, var(--line-2)); }
159
+ :is(.input, .textarea, select):focus, .field:focus-within {
160
+ border-color: var(--accent); background-color: var(--surface); box-shadow: 0 0 0 3px var(--accent-soft);
161
+ }
162
+ :is(.input, .textarea, .field input)::placeholder { color: var(--faint); }
163
+ :is(.input, .textarea, select):disabled { opacity: 0.55; cursor: not-allowed; }
164
+ :is(.input, .textarea, select):is([aria-invalid='true'], :user-invalid) { border-color: var(--bad); }
165
+ :is(.input, .textarea, select):is([aria-invalid='true'], :user-invalid):focus { box-shadow: 0 0 0 3px var(--del-bg); }
166
+ .textarea { display: block; height: auto; min-height: 64px; padding: 7px 10px; line-height: 1.5; resize: vertical; }
167
+ :is(.input, .textarea, .field).mono { font-family: var(--mono); font-size: 12.5px; }
168
+ :is(.input, .field).sm { height: 30px; padding: 0 8px; }
169
+ :is(.input, .field).lg { height: 38px; padding: 0 12px; border-radius: 11px; }
170
+ select:not([multiple]) {
171
+ width: auto; padding-right: 28px; appearance: none; cursor: pointer; text-overflow: ellipsis;
146
172
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%);
147
- background-position: calc(100% - 13px) 13px, calc(100% - 9px) 13px; background-size: 4px 4px; background-repeat: no-repeat;
173
+ background-position: calc(100% - 14px) 50%, calc(100% - 10px) 50%; background-size: 4px 4px; background-repeat: no-repeat;
148
174
  }
175
+ .field { display: flex; align-items: center; gap: 8px; color: var(--muted); }
176
+ .field .i { width: 14px; height: 14px; flex: none; }
177
+ .field > input { flex: 1; min-width: 0; height: 100%; padding: 0; border: 0; outline: 0; background: none; font: inherit; color: var(--ink); }
178
+ input[type='checkbox'] { width: 15px; height: 15px; margin: 0; flex: none; accent-color: var(--accent); cursor: pointer; }
179
+ .check { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-2); cursor: pointer; }
149
180
 
150
181
  /* ---------- the mark ---------- */
151
182
  /* A plain dot. Brand: ink dot with an offset accent dot. Live: a thin spinner ring. */
@@ -379,10 +410,7 @@ body:has(.launcher.docked:is(:focus-within, :has(.open))) .to-top { display: non
379
410
  .pk-text small:empty { display: none; }
380
411
  .pk-dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
381
412
  .pk-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
382
- .pk-search { flex: none; display: flex; align-items: center; gap: 8px; margin: 8px 8px 2px; padding: 0 10px; height: 34px; border-radius: 9px; border: 1px solid var(--line); background: var(--bg-2); color: var(--faint); transition: border-color 0.15s, background 0.15s; }
383
- .pk-search:focus-within { border-color: var(--accent); background: var(--surface); }
384
- .pk-search .i { width: 14px; height: 14px; flex: none; }
385
- .pk-search input { flex: 1; min-width: 0; border: 0; outline: 0; background: none; font: inherit; font-size: 13px; color: var(--ink); }
413
+ .pk-search { flex: none; width: auto; margin: 8px 8px 2px; color: var(--faint); }
386
414
  .pk-list { flex: 1; overflow-y: auto; padding: 6px; }
387
415
  .pk-group + .pk-group { margin-top: 4px; }
388
416
  .pk-check { width: 15px; height: 15px; flex: none; color: var(--accent); visibility: hidden; }
@@ -600,9 +628,7 @@ body:has(.launcher.docked:is(:focus-within, :has(.open))) .to-top { display: non
600
628
  .opt .k { display: grid; place-items: center; width: 20px; height: 20px; border-radius: 6px; background: var(--bg-2); font: 500 11px var(--mono); color: var(--muted); flex: none; }
601
629
  .opt:hover:not(:disabled) { border-color: var(--accent); background: var(--accent-soft); }
602
630
  .opt.own { color: var(--muted); border-style: dashed; padding-left: 12px; }
603
- .q-own { display: flex; gap: 8px; margin-top: 12px; }
604
- .q-own input { flex: 1; height: 34px; border-radius: 10px; border: 1px solid var(--line); background: var(--surface-2); padding: 0 12px; outline: 0; }
605
- .q-own input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
631
+ .q-own { display: flex; align-items: center; gap: 8px; margin-top: 12px; }
606
632
  .ask-card.sent { opacity: 0.5; pointer-events: none; }
607
633
 
608
634
  /* ---------- presentation blocks ---------- */
@@ -1105,13 +1131,11 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
1105
1131
  .rr-file .i { width: 12px; height: 12px; color: var(--muted); }
1106
1132
  .overlay:not([hidden]) ~ :is(.picker, .menu) { z-index: 55; }
1107
1133
  .add-row { display: flex; gap: 8px; }
1108
- .path-input { position: relative; flex: 1; display: flex; align-items: center; gap: 8px; height: 38px; padding: 0 12px; border-radius: 11px; border: 1px solid var(--line-2); background: var(--surface-2); }
1109
- .path-input:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
1110
- .path-input .i { color: var(--muted); }
1134
+ .path-input { position: relative; flex: 1; }
1135
+ .path-input .i { width: 16px; height: 16px; }
1111
1136
  .path-input .browse { display: grid; place-items: center; margin-left: -4px; width: 26px; height: 26px; border-radius: 7px; }
1112
1137
  .path-input .browse:hover { background: var(--bg-2); }
1113
1138
  .path-input .browse:hover .i { color: var(--accent); }
1114
- .path-input input { flex: 1; border: 0; outline: 0; background: transparent; font: 13px var(--mono); }
1115
1139
  .suggest { position: absolute; left: 0; right: 0; top: calc(100% + 6px); z-index: 2; padding: 4px; border-radius: 12px; background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow-2); }
1116
1140
  .suggest button { display: block; width: 100%; text-align: left; padding: 6px 10px; border-radius: 8px; font: 12.5px var(--mono); color: var(--ink-2); }
1117
1141
  .suggest button:hover, .suggest button.on { background: var(--accent-soft); color: var(--ink); }
@@ -1141,7 +1165,7 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
1141
1165
  .switch[aria-checked='true'] { background: var(--accent); }
1142
1166
  .switch[aria-checked='true']::after { transform: translateX(14px); }
1143
1167
  .switch:disabled { opacity: 0.4; cursor: not-allowed; }
1144
- .settings-modal input { flex: 1; max-width: 340px; height: 30px; border-radius: 9px; border: 1px solid var(--line); background: var(--surface-2); padding: 0 10px; font: 12px var(--mono); outline: 0; }
1168
+ .settings-modal label > .input { flex: 1; max-width: 340px; }
1145
1169
 
1146
1170
  /* settings: sidebar of sections, one pane at a time */
1147
1171
  .modal.sx { display: grid; grid-template-columns: 196px 1fr; width: min(880px, 100%); height: min(620px, 86vh); padding: 0; overflow: hidden; }
@@ -1158,7 +1182,7 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
1158
1182
  .sx-main > header { align-items: flex-start; gap: 16px; margin-bottom: 18px; }
1159
1183
  .sx-title { margin: 2px 0 4px; font: 600 16px/1.3 var(--display); letter-spacing: -0.01em; }
1160
1184
  .sx-lede { margin: 0; font-size: 12.5px; color: var(--muted); }
1161
- .sx-group { border: 1px solid var(--line); border-radius: 12px; background: var(--surface-2); }
1185
+ .sx-group { --field-bg: var(--surface); border: 1px solid var(--line); border-radius: 12px; background: var(--surface-2); }
1162
1186
  .sx-group + .sx-group, .sx-pane > * + .sx-group { margin-top: 14px; }
1163
1187
  .sx-row { display: flex; align-items: center; gap: 16px; min-height: 52px; padding: 9px 14px; }
1164
1188
  .sx-row + .sx-row { border-top: 1px solid var(--line); }
@@ -1168,7 +1192,7 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
1168
1192
  .sx-text small { font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; }
1169
1193
  .sx-text code { font-size: 11px; }
1170
1194
  .sx-ctl { flex: none; display: flex; align-items: center; gap: 8px; }
1171
- .sx-ctl input { width: 300px; }
1195
+ .sx-ctl .input { width: 300px; }
1172
1196
  .sx-state { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
1173
1197
  .sx-state.bad { color: var(--bad); }
1174
1198
  .sx-usage .it-loading { grid-column: 1 / -1; }
@@ -1184,7 +1208,7 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
1184
1208
  .sx-nav h2 { display: none; }
1185
1209
  .sx-nav button { flex: none; }
1186
1210
  .sx-main { padding: 16px; }
1187
- .sx-ctl input { width: 180px; }
1211
+ .sx-ctl .input { width: 180px; }
1188
1212
  .sx-usage { grid-template-columns: repeat(2, 1fr); }
1189
1213
  }
1190
1214
 
@@ -1226,7 +1250,6 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
1226
1250
  .mem-item:hover .mem-acts, .mem-item:focus-within .mem-acts { opacity: 1; }
1227
1251
  .mem-edit { align-items: center; padding-top: 7px; padding-bottom: 7px; background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--accent); }
1228
1252
  .mem-edit input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; font: 13.5px var(--sans); color: var(--ink); }
1229
- .memory-modal .mem-edit input { max-width: none; height: auto; padding: 0; }
1230
1253
  .mem-add { display: inline-flex; align-items: center; gap: 6px; margin: 4px 0 0; padding: 4px 8px 4px 6px; border-radius: 7px; font-size: 12.5px; color: var(--muted); }
1231
1254
  .mem-add:hover { color: var(--accent-ink); background: var(--accent-soft); }
1232
1255
  .mem-add .i { width: 13px; height: 13px; }
@@ -1335,6 +1358,124 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
1335
1358
  @keyframes ring { from { transform: scale(0.6); opacity: 0.9; } to { transform: scale(1.7); opacity: 0; } }
1336
1359
  @keyframes shimmer { from { background-position: 100% 0; } to { background-position: -200% 0; } }
1337
1360
 
1361
+ /* ---------- machines: other computers over SSH ---------- */
1362
+ .machines-btn.trouble .i { color: var(--warn); }
1363
+ .q-machine {
1364
+ display: inline-flex; align-items: center; gap: 4px; max-width: 120px; padding: 0 6px; border-radius: 5px; border: 1px solid var(--line);
1365
+ font-size: 11px; font-weight: 500; line-height: 17px; color: var(--ink-2); white-space: nowrap;
1366
+ }
1367
+ .q-machine span { overflow: hidden; text-overflow: ellipsis; }
1368
+ .q-machine .i { flex: none; width: 11px; height: 11px; color: var(--accent); }
1369
+ .q-machine.off { color: var(--faint); border-style: dashed; }
1370
+ .q-machine.off .i { color: var(--faint); }
1371
+ /* the launcher: one group of project chips per place it runs on */
1372
+ .l-projects { row-gap: 6px; }
1373
+ .lp-group { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; }
1374
+ .lp-group + .lp-group { margin-left: 4px; padding-left: 10px; border-left: 1px solid var(--line); }
1375
+ .lp-gl { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 500; color: var(--ink-2); }
1376
+ .lp-gl .i { width: 13px; height: 13px; color: var(--accent); }
1377
+ .lp-gl.off { color: var(--faint); }
1378
+ .lp-gl.off .i { color: var(--faint); }
1379
+ .lp-gl .link { margin-left: 2px; }
1380
+ .where-btn .chev { width: 11px; height: 11px; opacity: 0.6; transform: rotate(90deg); }
1381
+ .where-btn.bare span, .where-btn.bare .chev { display: none; }
1382
+ .where-menu { min-width: 300px; }
1383
+ .wm-h { display: flex; align-items: baseline; gap: 8px; padding: 6px 10px 6px; font-size: 12px; font-weight: 600; color: var(--ink); }
1384
+ .wm-h small { font-weight: 400; color: var(--faint); }
1385
+ .where-menu .wm-row { gap: 10px; }
1386
+ .wm-check { display: grid; place-items: center; width: 16px; height: 16px; flex: none; border-radius: 5px; border: 1.5px solid var(--line-2); color: transparent; }
1387
+ .wm-check .i { width: 11px; height: 11px; }
1388
+ .wm-row.on .wm-check { background: var(--accent); border-color: var(--accent); color: #fff; }
1389
+ .wm-icon { position: relative; display: inline-grid; place-items: center; flex: none; color: var(--muted); }
1390
+ .wm-icon .i { width: 16px; height: 16px; }
1391
+ .wm-icon .dot { position: absolute; right: -3px; bottom: -2px; width: 7px; height: 7px; border: 1.5px solid var(--surface); box-sizing: content-box; }
1392
+ .wm-row.on .wm-icon { color: var(--accent); }
1393
+ .wm-name { flex: 1; min-width: 0; display: flex; flex-direction: column; text-align: left; }
1394
+ .wm-name b { font-weight: 500; color: var(--ink); }
1395
+ .wm-name small { font: 11px var(--mono); color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1396
+ .wm-agents { display: inline-flex; gap: 5px; }
1397
+ .wm-only { padding: 1px 6px; border-radius: 6px; font-size: 11px; color: var(--muted); opacity: 0; transition: opacity 0.12s; }
1398
+ .wm-row:hover .wm-only { opacity: 1; }
1399
+ .wm-only:hover { background: var(--bg-2); color: var(--ink); }
1400
+ .wm-sep { height: 1px; margin: 5px 4px; background: var(--line); }
1401
+ .p-chip .p-where { width: 11px; height: 11px; color: var(--muted); }
1402
+ .projects-modal .where { margin-bottom: 12px; }
1403
+ .projects-modal .where .i { width: 13px; height: 13px; color: var(--muted); }
1404
+ .projects-modal .where button.on .i { color: var(--accent); }
1405
+ .projects-modal .where .dot { width: 6px; height: 6px; }
1406
+ .machines-modal { width: min(640px, 100%); }
1407
+ .mc-list { display: flex; flex-direction: column; gap: 10px; }
1408
+ .mc { padding: 12px 14px; border-radius: 12px; border: 1px solid var(--line); background: var(--surface); }
1409
+ .mc-head { display: flex; align-items: center; gap: 10px; }
1410
+ .mc-icon { position: relative; flex: none; display: grid; place-items: center; width: 32px; height: 32px; border-radius: 9px; background: var(--bg-2); color: var(--muted); }
1411
+ .mc-icon .i { width: 17px; height: 17px; }
1412
+ .mc-dot { position: absolute; right: -2px; bottom: -2px; width: 10px; height: 10px; border-radius: 50%; background: var(--faint); border: 2px solid var(--surface); }
1413
+ .mc.st-online .mc-icon { color: var(--accent); }
1414
+ .mc.st-online .mc-dot { background: var(--good); }
1415
+ .mc.st-error .mc-dot { background: var(--bad); }
1416
+ .mc:is(.st-connecting, .st-reconnecting) .mc-dot { background: var(--warn); animation: blink 1.2s infinite; }
1417
+ .mc-title { flex: 1; min-width: 0; display: flex; flex-direction: column; }
1418
+ .mc-title b { font-size: 14px; font-weight: 600; }
1419
+ .mc-title span { font: 11.5px var(--mono); color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1420
+ .mc-state { font-size: 12px; color: var(--muted); white-space: nowrap; }
1421
+ .mc.st-online .mc-state { color: var(--good); }
1422
+ .mc.st-error .mc-state { color: var(--bad); }
1423
+ .mc-meta, .mc-steps, .mc-hint, .mc-projects, .mc-edit { margin-left: 42px; }
1424
+ .mc-meta { display: flex; align-items: center; gap: 12px; margin-top: 8px; font-size: 12px; color: var(--muted); }
1425
+ .mc-meta .bad { color: var(--bad); }
1426
+ .mc-agents { display: inline-flex; gap: 7px; }
1427
+ .mc-steps { list-style: none; display: flex; flex-direction: column; gap: 5px; margin-top: 10px; margin-bottom: 0; padding: 0; }
1428
+ .mc-steps li { display: grid; grid-template-columns: 16px auto 1fr; align-items: center; column-gap: 8px; font-size: 12.5px; color: var(--faint); }
1429
+ .mc-steps li b { font-weight: 500; }
1430
+ .mc-steps li small { font: 11.5px var(--mono); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1431
+ .mc-si { display: grid; place-items: center; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid var(--line-2); }
1432
+ .mc-si .i { width: 10px; height: 10px; }
1433
+ .mc-steps .s-done { color: var(--ink-2); }
1434
+ .mc-steps .s-done .mc-si { background: var(--good); border-color: var(--good); color: #fff; animation: check-in 0.25s var(--ease); }
1435
+ .mc-steps .s-run { color: var(--ink); }
1436
+ .mc-steps .s-run .mc-si { border-color: var(--accent); border-top-color: transparent; animation: spin 0.8s linear infinite; }
1437
+ .mc-steps .s-fail { color: var(--bad); }
1438
+ .mc-steps .s-fail .mc-si { background: var(--bad); border-color: var(--bad); color: #fff; }
1439
+ .mc-steps .s-fail small { color: var(--bad); white-space: normal; }
1440
+ .mc-steps .s-warn { color: var(--warn); }
1441
+ .mc-steps .s-warn .mc-si { border-color: var(--warn); color: var(--warn); }
1442
+ .mc-steps .s-warn small { white-space: normal; }
1443
+ .mc-hint { margin-top: 10px; padding: 10px 12px; border-radius: 10px; border: 1px solid color-mix(in srgb, var(--bad) 25%, var(--line)); background: var(--del-bg); font-size: 12.5px; color: var(--ink-2); }
1444
+ .mc-hint.soft { border-color: var(--line); background: var(--surface-2); }
1445
+ .mc-hint p { margin: 0 0 6px; }
1446
+ .mc-hint > :last-child { margin-bottom: 0; }
1447
+ .mc-cmd { display: flex; align-items: center; gap: 6px; margin: 4px 0 8px; padding: 3px 3px 3px 10px; border-radius: 8px; background: var(--term-bg); color: var(--term-ink); }
1448
+ .mc-cmd code { flex: 1; font: 12px var(--mono); }
1449
+ .mc-cmd .ghost { color: var(--term-ink); }
1450
+ .mc-cmd .ghost:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
1451
+ .mc-log summary { cursor: pointer; font-size: 12px; color: var(--muted); width: max-content; }
1452
+ .mc-log pre { max-height: 180px; overflow: auto; margin: 6px 0 0; padding: 8px 10px; border-radius: 8px; background: var(--surface); border: 1px solid var(--line); font: 11px/1.5 var(--mono); white-space: pre-wrap; }
1453
+ .mc-projects { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 10px; }
1454
+ .mc-edit { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }
1455
+ .mc-add { margin-top: 12px; }
1456
+ .mc-add h4 { margin-top: 18px; }
1457
+ .mc-new { height: 32px; }
1458
+ .mc-found { margin-bottom: 14px; }
1459
+ .mc-found h4 { margin: 0 0 8px; }
1460
+ .mc-found-list { display: flex; flex-wrap: wrap; gap: 6px; }
1461
+ .mc-found .lp-chip .i { width: 14px; height: 14px; color: var(--muted); }
1462
+ .mc-found .lp-chip.off { color: var(--muted); }
1463
+ .mc-fdot { width: 7px; height: 7px; border-radius: 50%; background: var(--good); }
1464
+ .lp-chip.off .mc-fdot { background: var(--faint); }
1465
+ .mc-form { display: flex; flex-direction: column; gap: 10px; }
1466
+ .mc-row { display: flex; gap: 10px; }
1467
+ .mc-field { display: flex; flex-direction: column; gap: 5px; width: 150px; min-width: 0; }
1468
+ .mc-field.grow { flex: 1; width: auto; }
1469
+ .mc-field span { font-size: 12px; font-weight: 500; color: var(--ink-2); }
1470
+ .mc-adv summary { cursor: pointer; width: max-content; font-size: 12.5px; color: var(--muted); }
1471
+ .mc-adv[open] summary { margin-bottom: 8px; }
1472
+ .mc-check { margin-top: 8px; }
1473
+ .mc-foot { display: flex; align-items: center; gap: 10px; }
1474
+ .mc-foot .hint { flex: 1; margin: 0 !important; font-size: 12px !important; }
1475
+ .mc-foot .primary { flex: none; }
1476
+ .mc-foot .primary .i { width: 14px; height: 14px; }
1477
+ .menu button.danger:hover { color: var(--bad); background: var(--del-bg); }
1478
+
1338
1479
  /* ---------- responsive ---------- */
1339
1480
  @media (max-width: 720px) {
1340
1481
  .topbar { padding: 0 12px; gap: 10px; }
@@ -1343,7 +1484,10 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
1343
1484
  .search-btn { padding: 0 8px; border-color: transparent; background: none; }
1344
1485
  .feed-wrap { padding: 14px 16px 80px; }
1345
1486
  .launcher.docked { width: calc(100% - 32px); }
1346
- .it-side .q-proj, .it-actions span, .c-done span { display: none; }
1487
+ .it-side .q-proj, .it-actions span, .c-done span, .q-machine span { display: none; }
1488
+ .mc-row { flex-direction: column; }
1489
+ .mc-field { width: auto; }
1490
+ .mc-meta, .mc-steps, .mc-hint, .mc-projects, .mc-edit { margin-left: 0; }
1347
1491
  .turns { padding: 6px 14px 8px; }
1348
1492
  .it-dock { padding: 4px 10px 10px; }
1349
1493
  .b-hero h2 { font-size: 22px; }
@@ -1441,12 +1585,8 @@ body.electron.mac .topbar { padding-left: 88px; }
1441
1585
  .ship-route .muted { margin-left: auto; }
1442
1586
  .ship-field { display: flex; flex-direction: column; gap: 6px; margin: 12px 0 0; }
1443
1587
  .ship-field > span { font-size: 12.5px; font-weight: 500; color: var(--ink-2); }
1444
- .ship-field input, .ship-field textarea, .ship-field select {
1445
- width: 100%; border-radius: 9px; border: 1px solid var(--line-2); background: var(--surface-2); padding: 7px 10px; font: 13px var(--sans); outline: 0; resize: vertical;
1446
- }
1447
- .ship-field textarea { font: 12.5px/1.5 var(--mono); }
1448
- .ship-field :is(input, textarea, select):focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
1449
- .ship-check { display: flex; align-items: center; gap: 8px; margin-top: 10px; font-size: 13px; }
1588
+ .ship-field select { width: 100%; }
1589
+ .ship-check { display: flex; margin-top: 10px; }
1450
1590
  .ship-foot { display: flex; align-items: center; gap: 12px; margin-top: 16px; }
1451
1591
  .ship-foot > .muted { margin-right: auto; }
1452
1592
  .ship-foot code { font-size: 11px; }
@@ -1490,9 +1630,7 @@ body.electron.mac .topbar { padding-left: 88px; }
1490
1630
  .au-form label > span, .au-row > span { width: 92px; flex: none; font-size: 12.5px; font-weight: 500; color: var(--ink-2); }
1491
1631
  .au-form label.stack { flex-direction: column; align-items: stretch; gap: 6px; }
1492
1632
  .au-form label.stack > span { width: auto; }
1493
- .au-form input, .au-form select, .au-form textarea { flex: 1; min-width: 0; border-radius: 8px; border: 1px solid var(--line); background: var(--surface-2); padding: 6px 9px; font: 13px var(--sans); outline: 0; }
1494
- .au-form textarea { font: 12.5px/1.5 var(--mono); resize: vertical; }
1495
- .au-form :is(input, select, textarea):focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
1633
+ .au-form :is(.input, .textarea, select) { flex: 1; }
1496
1634
  .au-form [hidden] { display: none !important; }
1497
1635
  .au-params { margin: 6px 0 2px; }
1498
1636
  .au-params code, .k-vars code { font-size: 11px; }
@@ -1531,7 +1669,7 @@ body.electron.mac .topbar { padding-left: 88px; }
1531
1669
  .phone-modal .ph-sec h4 { display: flex; align-items: center; gap: 8px; }
1532
1670
  .phone-modal .ph-sec h4 .switch { margin-left: auto; }
1533
1671
  .phone-modal .settings-modal { width: auto; padding: 0; border: 0; box-shadow: none; background: none; }
1534
- .ph-check { display: inline-flex !important; align-items: center; gap: 6px; padding: 0 !important; border: 0 !important; font-size: 13px; }
1672
+ .settings-modal label.check { padding: 0; border: 0; gap: 6px; }
1535
1673
  .ph-ok, .ph-bad { display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px; font-weight: 500; }
1536
1674
  .ph-ok { color: var(--good); }
1537
1675
  .ph-bad { color: var(--bad); max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -1594,14 +1732,12 @@ body.electron.mac .topbar { padding-left: 88px; }
1594
1732
  .tp-warn[hidden] { display: none; }
1595
1733
  .tp-warn .i { flex: none; width: 14px; height: 14px; margin-top: 1px; }
1596
1734
  .tp-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; padding: 0 14px 12px; }
1597
- .tp-row { display: grid; gap: 6px; padding: 8px; border-radius: 11px; border: 1px solid var(--line); background: var(--surface-2); animation: rise 0.25s var(--ease) both; }
1735
+ .tp-row { --field-bg: var(--surface); display: grid; gap: 6px; padding: 8px; border-radius: 11px; border: 1px solid var(--line); background: var(--surface-2); animation: rise 0.25s var(--ease) both; }
1598
1736
  .tp-top { display: flex; align-items: center; gap: 6px; min-width: 0; }
1599
1737
  .tp-runner { flex: 0 1 auto; min-width: 0; overflow: hidden; }
1600
1738
  .tp-runner .rb-m { max-width: 140px; }
1601
- .tp-name { flex: 1 1 90px; min-width: 70px; display: flex; align-items: center; height: 30px; padding: 0 8px; border-radius: 9px; border: 1px solid var(--line); background: var(--surface); color: var(--faint); font: 12.5px var(--mono); }
1602
- .tp-name:focus-within, .tp-use:focus-within { border-color: var(--accent); }
1603
- .tp-name input, .tp-use input { flex: 1; min-width: 0; border: 0; outline: 0; background: none; font: inherit; color: var(--ink); }
1604
- .tp-copies { flex: none; display: inline-flex; align-items: center; height: 30px; padding: 0 2px; border-radius: 9px; border: 1px solid var(--line); background: var(--surface); }
1739
+ .tp-name { flex: 1 1 90px; min-width: 70px; width: auto; gap: 0; color: var(--faint); }
1740
+ .tp-copies { flex: none; display: inline-flex; align-items: center; height: 30px; padding: 0 2px; border-radius: 9px; border: 1px solid var(--line-2); background: var(--surface); }
1605
1741
  .tp-copies b { min-width: 22px; text-align: center; font: 500 12px var(--mono); color: var(--muted); font-variant-numeric: tabular-nums; }
1606
1742
  .tp-copies.on { border-color: color-mix(in srgb, var(--accent) 35%, var(--line)); }
1607
1743
  .tp-copies.on b { color: var(--accent-ink); }
@@ -1612,16 +1748,14 @@ body.electron.mac .topbar { padding-left: 88px; }
1612
1748
  .tp-copies .i { width: 11px; height: 11px; }
1613
1749
  .tp-del { flex: none; width: 28px; height: 28px; }
1614
1750
  .tp-del .i { width: 13px; height: 13px; }
1615
- .tp-use { display: flex; align-items: center; gap: 8px; height: 30px; padding: 0 9px; border-radius: 9px; border: 1px solid var(--line); background: var(--surface); font-size: 13px; }
1616
1751
  .tp-use > span { flex: none; font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--faint); }
1617
- .tp-use input::placeholder, .tp-name input::placeholder { color: var(--faint); }
1618
1752
  .tp-empty { display: flex; align-items: center; gap: 12px; padding: 16px; border-radius: 11px; border: 1px dashed var(--line-2); color: var(--muted); font-size: 13px; }
1619
1753
  .tp-empty .i { flex: none; width: 22px; height: 22px; color: var(--faint); }
1620
1754
  .tp-empty p { margin: 0; line-height: 1.4; }
1621
1755
  .tp-foot { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--line); background: var(--surface-2); }
1622
1756
  .tp-count { font-size: 12px; color: var(--faint); font-variant-numeric: tabular-nums; }
1623
1757
  .tp-saveform { display: flex; align-items: center; gap: 8px; flex: 1; }
1624
- .tp-saveform input { flex: 1; min-width: 0; height: 30px; padding: 0 10px; border-radius: 9px; border: 1px solid var(--accent); background: var(--surface); font: inherit; font-size: 13px; color: var(--ink); outline: 0; }
1758
+ .tp-saveform .input { flex: 1; }
1625
1759
  .menu.tp-menu { width: 300px; }
1626
1760
  .menu.tp-menu button { align-items: center; text-align: left; }
1627
1761
  .menu.tp-menu button.on b { color: var(--accent-ink); }
package/web/index.html CHANGED
@@ -53,6 +53,8 @@
53
53
  <symbol id="i-menu" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></symbol>
54
54
  <symbol id="i-bolt" viewBox="0 0 24 24"><path d="M13.5 3L5 13.5h6L10.5 21 19 10.5h-6L13.5 3z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></symbol>
55
55
  <symbol id="i-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M12 7.5V12l3 2" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></symbol>
56
+ <symbol id="i-laptop" viewBox="0 0 24 24"><rect x="4.5" y="5" width="15" height="10.5" rx="1.8" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M2.5 19h19" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></symbol>
57
+ <symbol id="i-server" viewBox="0 0 24 24"><rect x="3.5" y="4" width="17" height="7" rx="2" fill="none" stroke="currentColor" stroke-width="1.7"/><rect x="3.5" y="13" width="17" height="7" rx="2" fill="none" stroke="currentColor" stroke-width="1.7"/><circle cx="7.3" cy="7.5" r="1.1" fill="currentColor"/><circle cx="7.3" cy="16.5" r="1.1" fill="currentColor"/><path d="M11 7.5h5.5M11 16.5h5.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol>
56
58
  <symbol id="i-play" viewBox="0 0 24 24"><path d="M7 5.5v13l11-6.5-11-6.5z" fill="currentColor"/></symbol>
57
59
  </defs>
58
60
  </svg>
@@ -67,6 +69,9 @@
67
69
  <button class="update-btn" id="updateBtn" hidden>
68
70
  <svg class="i"><use href="#i-update"/></svg><span>Update</span>
69
71
  </button>
72
+ <button class="auto-btn machines-btn" id="machinesBtn" title="Machines: run agents on another computer over SSH">
73
+ <svg class="i"><use href="#i-server"/></svg><span>Machines</span><b class="ab-n" hidden></b>
74
+ </button>
70
75
  <button class="auto-btn" id="autoBtn" title="Automations: recipes and triggers">
71
76
  <svg class="i"><use href="#i-bolt"/></svg><span>Automations</span><b class="ab-n" hidden></b>
72
77
  </button>