ai-agentmanager 1.11.0 → 1.11.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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.11.1 (2026-11-01)
4
+
5
+ - **Copy and paste in terminals on phones:** **Copy** and **Paste** in the terminal's key bar (and in the Terminal's bar). A phone can't select the terminal's text or paste into it directly, so Copy opens its recent output as plain text you can select (or **Copy all**), and Paste reads the clipboard (the phone asks first) or offers a box to paste into, then inserts it as one paste without pressing Enter.
6
+
3
7
  ## 1.11.0 (2026-11-01)
4
8
 
5
9
  - **Inbox:** permission menus that newer Claude Code draws as one run of text ("Do you want to proceed? ❯ 1. Yes 2. … 3. No") get their choices again, so they can be answered from the inbox.
package/README.md CHANGED
@@ -85,13 +85,13 @@ Runs on macOS and Linux; on Windows, use WSL2. Requires Node 20+, `git`, and the
85
85
 
86
86
  ```sh
87
87
  npm install -g ai-agentmanager # the latest release
88
- npm install -g ai-agentmanager@1.11.0 # or a specific one
88
+ npm install -g ai-agentmanager@1.11.1 # or a specific one
89
89
  ```
90
90
 
91
91
  Or from the repository, at a release tag:
92
92
 
93
93
  ```sh
94
- git clone --branch v1.11.0 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
94
+ git clone --branch v1.11.1 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
95
95
  cd ai-agentmanager-core
96
96
  npm install
97
97
  npm link # optional: puts `agentmgr` on your PATH
@@ -111,7 +111,7 @@ npm link
111
111
  | Installed with | Update |
112
112
  | --- | --- |
113
113
  | npm | `npm install -g ai-agentmanager@latest` |
114
- | A clone at a release tag | `git fetch --tags && git checkout v1.11.0 && npm install` (the tag you want) |
114
+ | A clone at a release tag | `git fetch --tags && git checkout v1.11.1 && npm install` (the tag you want) |
115
115
  | A clone of `main` | `git pull && npm install` |
116
116
 
117
117
  **Check your setup.** `agentmgr doctor` checks Node.js, Claude Code and its sign-in, git, `gh`, the data folder, the daemon's port and Chrome, and says how to fix anything missing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-agentmanager",
3
- "version": "1.11.0",
3
+ "version": "1.11.1",
4
4
  "description": "Run background coding agents (Claude Code, Codex) per intent, and keep track of them from a web UI, your phone or the CLI",
5
5
  "keywords": [
6
6
  "claude",
package/public/app.js CHANGED
@@ -6,6 +6,7 @@ import { touchScroll } from '/touchscroll.js';
6
6
  import { openStream, post, REMOTE } from '/transport.js';
7
7
  import { attachBox } from '/attach.js';
8
8
  import { initSide } from '/side.js';
9
+ import { openCopy, pasteInto } from '/termclip.js';
9
10
  import { addTools, download, htmlFile, initExport } from '/export.js';
10
11
  import { badgeHtml as scoreBadge, scorecardHtml, scorecardSpeech } from '/scorecard.js';
11
12
  import { bindListen, listenHtml, play, speakable, now as speaking, stop as stopSpeaking } from '/speak.js';
@@ -3660,6 +3661,10 @@ const KEYS = {
3660
3661
  enter: '\r', ctrlc: '\x03', 1: '1', 2: '2', 3: '3', 4: '4',
3661
3662
  };
3662
3663
  $('#keybar').addEventListener('click', (e) => {
3664
+ // Copy / Paste: a phone can't select xterm's text or paste into it directly (termclip.js).
3665
+ const clip = e.target.closest('[data-clip-act]')?.dataset.clipAct;
3666
+ if (clip === 'copy') return openCopy(term.xterm, { esc, toast });
3667
+ if (clip === 'paste') return pasteInto(term.xterm, { toast });
3663
3668
  const key = e.target.closest('[data-key]')?.dataset.key;
3664
3669
  if (key) term.send({ t: 'i', d: KEYS[key] });
3665
3670
  });
package/public/index.html CHANGED
@@ -161,6 +161,8 @@
161
161
  </div>
162
162
  <div id="term-drop" class="term-drop" hidden>Drop files to attach</div>
163
163
  <div class="keybar" id="keybar" aria-label="Terminal keys">
164
+ <button data-clip-act="copy" title="Copy text from the terminal">Copy</button>
165
+ <button data-clip-act="paste" title="Paste into the terminal">Paste</button>
164
166
  <button data-key="esc">Esc</button>
165
167
  <button data-key="tab">Tab</button>
166
168
  <button data-key="stab" title="Shift+Tab: cycle permission mode">⇧Tab</button>
@@ -767,6 +769,8 @@
767
769
  <button class="term-tab term-add" id="machine-new" title="Open another terminal">+ Terminal</button>
768
770
  <span id="machine-title" class="hint"></span>
769
771
  <span class="spacer"></span>
772
+ <button id="machine-copy" class="ghost small" title="Copy text from the terminal">Copy</button>
773
+ <button id="machine-paste" class="ghost small" title="Paste into the terminal">Paste</button>
770
774
  <button id="machine-close" class="ghost small" title="Hide (the shells keep running)">Hide</button>
771
775
  </div>
772
776
  <div id="machine-term" class="terminal"></div>
package/public/machine.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // any signed-in device (over remote access it streams like an agent's shell). The shells keep
3
3
  // running when the window is hidden; × ends one.
4
4
  import { openStream } from '/transport.js';
5
+ import { openCopy, pasteInto } from '/termclip.js';
5
6
 
6
7
  const MACHINE = '__machine__';
7
8
 
@@ -86,6 +87,9 @@ export function initMachine({ api, esc, toast, $, Terminal, FitAddon }) {
86
87
  shells = [...new Set([...shells, n])];
87
88
  connect(n);
88
89
  };
90
+ // Copy / Paste buttons: on a phone the terminal's text can't be selected or pasted into directly.
91
+ $('#machine-copy').onclick = () => xterm && openCopy(xterm, { esc, toast });
92
+ $('#machine-paste').onclick = () => xterm && pasteInto(xterm, { toast });
89
93
  $('#machine-close').onclick = () => {
90
94
  conn?.close();
91
95
  conn = null;
package/public/style.css CHANGED
@@ -940,3 +940,12 @@ form.dropping { outline: 2px dashed var(--accent, #6b8cff); outline-offset: 4px;
940
940
  /* Settings → More: separators belong to the links, so hidden (local-only) ones leave none behind. */
941
941
  .settings-links button + button::before { content: '·'; display: inline-block; margin: 0 8px; color: var(--muted); }
942
942
  body.remote .settings-links .local-only { display: none; }
943
+
944
+ /* Copy / Paste sheets for terminals on touch screens. */
945
+ .clip-sheet { width: min(760px, calc(100vw - 16px)); max-height: calc(100vh - 32px); padding: 0; display: flex; flex-direction: column; }
946
+ .clip-sheet:not([open]) { display: none; }
947
+ .clip-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
948
+ .clip-text { margin: 0; padding: 10px 12px; overflow: auto; flex: 1; min-height: 200px; max-height: 70vh; font: 12px/1.45 var(--mono, ui-monospace, monospace); white-space: pre-wrap; word-break: break-word; -webkit-user-select: text; user-select: text; background: var(--term-bg); color: #e6edf3; }
949
+ .clip-box { margin: 10px 12px; width: calc(100% - 24px); font: 13px var(--mono, ui-monospace, monospace); }
950
+ .clip-foot { display: flex; align-items: center; gap: 8px; padding: 0 12px 12px; }
951
+ .clip-foot .hint { flex: 1; }
@@ -0,0 +1,92 @@
1
+ // Copy and paste for terminals on touch screens. xterm draws its text itself, so a phone can't
2
+ // long-press-select it, and its hidden input rarely gets the paste menu. Copy opens the terminal's
3
+ // recent output as plain text the phone can select (or Copy all); Paste reads the clipboard (the
4
+ // phone asks first) or, where it can't, offers a box to paste into, then sends it as one paste.
5
+
6
+ const MAX_LINES = 600;
7
+
8
+ /** The terminal's text: the last lines of its buffer, trailing blanks trimmed. */
9
+ export function terminalText(xterm, max = MAX_LINES) {
10
+ const buf = xterm.buffer.active;
11
+ const lines = [];
12
+ for (let i = Math.max(0, buf.length - max); i < buf.length; i++) {
13
+ const line = buf.getLine(i);
14
+ if (!line) continue;
15
+ // A wrapped line continues the one before it.
16
+ if (line.isWrapped && lines.length) lines[lines.length - 1] += line.translateToString(true);
17
+ else lines.push(line.translateToString(true));
18
+ }
19
+ while (lines.length && !lines.at(-1).trim()) lines.pop();
20
+ return lines.join('\n');
21
+ }
22
+
23
+ function sheet(html) {
24
+ const d = document.createElement('dialog');
25
+ d.className = 'clip-sheet';
26
+ d.innerHTML = html;
27
+ document.body.append(d);
28
+ d.addEventListener('close', () => d.remove());
29
+ d.showModal();
30
+ return d;
31
+ }
32
+
33
+ async function writeClipboard(text) {
34
+ try {
35
+ await navigator.clipboard.writeText(text);
36
+ return true;
37
+ } catch {
38
+ // Older browsers, or no permission: copy through a selected text area.
39
+ const t = Object.assign(document.createElement('textarea'), { value: text });
40
+ t.style.cssText = 'position:fixed;opacity:0';
41
+ document.body.append(t);
42
+ t.select();
43
+ const ok = document.execCommand?.('copy');
44
+ t.remove();
45
+ return Boolean(ok);
46
+ }
47
+ }
48
+
49
+ /** Copy: the terminal's text in a sheet, selectable with the phone's own handles, and Copy all. */
50
+ export function openCopy(xterm, { esc, toast }) {
51
+ const text = terminalText(xterm);
52
+ const d = sheet(`<div class="clip-head"><b>Copy from the terminal</b><span class="hint">Select with your finger, or copy it all</span><span class="spacer"></span><button type="button" class="small primary" data-clip="all">Copy all</button><button type="button" class="small ghost" data-clip="close">Close</button></div>
53
+ <pre class="clip-text">${esc(text) || '<span class="hint">Nothing in this terminal yet.</span>'}</pre>`);
54
+ // Open at the end, where the latest output is.
55
+ const pre = d.querySelector('.clip-text');
56
+ pre.scrollTop = pre.scrollHeight;
57
+ d.addEventListener('click', async (e) => {
58
+ const act = e.target.closest('[data-clip]')?.dataset.clip;
59
+ if (act === 'close') d.close();
60
+ if (act === 'all') {
61
+ toast((await writeClipboard(text)) ? 'Copied the terminal text' : 'Copy failed: select the text and use your phone\'s Copy', 3000);
62
+ d.close();
63
+ }
64
+ });
65
+ }
66
+
67
+ /** Paste: the clipboard into the terminal (one paste, not submitted), or a box to paste into. */
68
+ export async function pasteInto(xterm, { toast }) {
69
+ try {
70
+ const text = await navigator.clipboard.readText();
71
+ if (text) {
72
+ xterm.paste(text);
73
+ return;
74
+ }
75
+ } catch {
76
+ // Not allowed here (or refused): fall back to a box the phone can paste into.
77
+ }
78
+ const d = sheet(`<div class="clip-head"><b>Paste into the terminal</b><span class="spacer"></span><button type="button" class="small ghost" data-clip="close">Cancel</button></div>
79
+ <textarea class="clip-box" rows="6" placeholder="Long-press here and choose Paste"></textarea>
80
+ <div class="clip-foot"><span class="hint">It goes in as typed text; press Enter (⏎) in the terminal when you want it run.</span><button type="button" class="small primary" data-clip="send">Insert</button></div>`);
81
+ const box = d.querySelector('.clip-box');
82
+ box.focus();
83
+ d.addEventListener('click', (e) => {
84
+ const act = e.target.closest('[data-clip]')?.dataset.clip;
85
+ if (act === 'close') d.close();
86
+ if (act === 'send') {
87
+ if (box.value) xterm.paste(box.value);
88
+ else toast('Nothing to paste');
89
+ d.close();
90
+ }
91
+ });
92
+ }