@hwip/cli 0.1.3 → 0.1.4
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 +33 -11
- package/dist/agents.d.ts +10 -0
- package/dist/agents.js +12 -1
- package/dist/codex-config.d.ts +8 -5
- package/dist/codex-config.js +121 -12
- package/dist/command-scan.d.ts +7 -3
- package/dist/command-scan.js +59 -15
- package/dist/installer.js +7 -1
- package/dist/listener.d.ts +9 -0
- package/dist/listener.js +46 -11
- package/dist/mcp-command.d.ts +13 -1
- package/dist/mcp-command.js +18 -5
- package/dist/pane-fit.d.ts +39 -11
- package/dist/pane-fit.js +162 -29
- package/dist/remote-agents.d.ts +64 -12
- package/dist/remote-agents.js +141 -18
- package/dist/session-registry.js +6 -2
- package/dist/session-title.d.ts +46 -0
- package/dist/session-title.js +103 -0
- package/dist/templates.d.ts +1 -0
- package/dist/templates.js +41 -1
- package/dist/verify.d.ts +3 -0
- package/dist/verify.js +33 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,17 @@ Codex / …). Safe to re-run: a saved login is reused untouched (no
|
|
|
39
39
|
prompts), so a re-run just refreshes the agent integrations. To switch
|
|
40
40
|
account, `hwip install --relogin` forces a fresh sign-in.
|
|
41
41
|
|
|
42
|
+
**Auto session names.** Agents without a title of their own — Codex,
|
|
43
|
+
Cursor, Windsurf, Gemini and pi — name their sessions after what they were
|
|
44
|
+
asked, so the phone shows that instead of a tmux name. The registrations
|
|
45
|
+
`hwip install` writes turn it on (`HWIP_AUTONAME=1` in the MCP entry, the
|
|
46
|
+
`hwip_session_title` tool in the pi extension), so **after updating
|
|
47
|
+
`@hwip/cli`, run `hwip install` once more** to get it on a machine
|
|
48
|
+
installed before; `hwip verify` says which MCP agents still lack it. Claude
|
|
49
|
+
Code and opencode title sessions themselves and are left as they are. To
|
|
50
|
+
hide the names agents choose, turn off *Name sessions automatically* in
|
|
51
|
+
the app's settings — names you set yourself always show.
|
|
52
|
+
|
|
42
53
|
**Why global, not `npx`?** `hwip cc` (drive a session from your phone)
|
|
43
54
|
needs `hwip` on your `PATH`, and the agent hooks this installs are
|
|
44
55
|
`$(command -v hwip || npx …)` — a global binary skips an npx cold-start
|
|
@@ -432,17 +443,28 @@ block here.
|
|
|
432
443
|
hwip sh --detach # create it, print its name, exit
|
|
433
444
|
```
|
|
434
445
|
|
|
435
|
-
**The mirror fits a
|
|
436
|
-
|
|
437
|
-
right-aligned clocks fold in two on a phone.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
phone's width and
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
a
|
|
446
|
+
**The mirror fits a session to the phone.** A pane is as wide as the
|
|
447
|
+
terminal that last drew it, so a desktop's prompt rules and
|
|
448
|
+
right-aligned clocks fold in two on a phone. Whoever used the session
|
|
449
|
+
last decides its width, the way tmux's `window-size latest` does: typing
|
|
450
|
+
from the phone makes the phone the latest user, and the listener resizes
|
|
451
|
+
the window to the phone's width, and coming back to the mirror later
|
|
452
|
+
(a locked screen, a resubscribe) takes it again unless the desktop was
|
|
453
|
+
used since. Opening the mirror without typing does the same once
|
|
454
|
+
the desktop terminal has been idle for a minute, so a phone reconnecting
|
|
455
|
+
on its own never shrinks a terminal you are typing in. Pressing a key in
|
|
456
|
+
(or attaching) a desktop terminal makes it the latest user, and within a
|
|
457
|
+
renew (5 s) the window is handed back to tmux, which sizes it to that
|
|
458
|
+
terminal. Moving the mouse over a desktop pane or clicking another
|
|
459
|
+
pane is not using it: on macOS the listener reads when a key was last
|
|
460
|
+
pressed (the HID system clock, no permission needed) and counts a
|
|
461
|
+
desktop terminal as used no later than that key or its attach. Elsewhere
|
|
462
|
+
any tmux client activity counts. While the phone holds a session, a desktop terminal still
|
|
463
|
+
attached to it shows it narrower. When the phone leaves, a session no
|
|
464
|
+
terminal is attached to gets its old width back and an attached one is
|
|
465
|
+
sized to its terminal. A window split into panes is never resized. A
|
|
466
|
+
listener that died while a window was fitted restores it the next time
|
|
467
|
+
it starts.
|
|
446
468
|
|
|
447
469
|
`hwip cursor` runs **`cursor-agent`**, Cursor's terminal agent — a
|
|
448
470
|
separate install from the Cursor IDE (the bare `cursor` on your PATH
|
package/dist/agents.d.ts
CHANGED
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
* pi 0.85.1): global is `~/.pi/agent/skills/` and `~/.agents/skills/`, while
|
|
11
11
|
* `.pi/skills/` is the PROJECT dir — scanning `~/.pi/skills` found only three
|
|
12
12
|
* dangling symlinks and reported pi as having no skills at all.
|
|
13
|
+
*
|
|
14
|
+
* codex reads `$CODEX_HOME/skills` and `~/.agents/skills` (codex-rs
|
|
15
|
+
* `ext/skills/src/host_roots.rs`), and keeps its bundled skills in the hidden
|
|
16
|
+
* `.codex/skills/.system`, which the scanner's dot-skip would otherwise pass
|
|
17
|
+
* over. opencode reads the claude and agents dirs as well as its own
|
|
18
|
+
* (opencode `docs/skills.mdx § Discovery`, 2.0.29). Measured against the agents'
|
|
19
|
+
* own reports on 2026-10-20: codex 59 of `codex app-server` `skills/list` 59;
|
|
20
|
+
* opencode 87 of `opencode serve` `/api/skill` 91 — the rest are two built-ins
|
|
21
|
+
* and two skills it reads out of `~/.claude/skills/.trash/`, which the scanner's
|
|
22
|
+
* dot-skip leaves out on purpose.
|
|
13
23
|
*/
|
|
14
24
|
export declare const AGENT_SKILL_DIRS: Record<string, string[]>;
|
|
15
25
|
export interface Agent {
|
package/dist/agents.js
CHANGED
|
@@ -20,10 +20,21 @@ const path_1 = require("path");
|
|
|
20
20
|
* pi 0.85.1): global is `~/.pi/agent/skills/` and `~/.agents/skills/`, while
|
|
21
21
|
* `.pi/skills/` is the PROJECT dir — scanning `~/.pi/skills` found only three
|
|
22
22
|
* dangling symlinks and reported pi as having no skills at all.
|
|
23
|
+
*
|
|
24
|
+
* codex reads `$CODEX_HOME/skills` and `~/.agents/skills` (codex-rs
|
|
25
|
+
* `ext/skills/src/host_roots.rs`), and keeps its bundled skills in the hidden
|
|
26
|
+
* `.codex/skills/.system`, which the scanner's dot-skip would otherwise pass
|
|
27
|
+
* over. opencode reads the claude and agents dirs as well as its own
|
|
28
|
+
* (opencode `docs/skills.mdx § Discovery`, 2.0.29). Measured against the agents'
|
|
29
|
+
* own reports on 2026-10-20: codex 59 of `codex app-server` `skills/list` 59;
|
|
30
|
+
* opencode 87 of `opencode serve` `/api/skill` 91 — the rest are two built-ins
|
|
31
|
+
* and two skills it reads out of `~/.claude/skills/.trash/`, which the scanner's
|
|
32
|
+
* dot-skip leaves out on purpose.
|
|
23
33
|
*/
|
|
24
34
|
exports.AGENT_SKILL_DIRS = {
|
|
25
35
|
claude: ['.claude/skills'],
|
|
26
|
-
codex: ['.codex/skills'],
|
|
36
|
+
codex: ['.codex/skills', '.codex/skills/.system', '.agents/skills'],
|
|
37
|
+
opencode: ['.claude/skills', '.agents/skills', '.config/opencode/skills'],
|
|
27
38
|
pi: ['.pi/agent/skills', '.agents/skills'],
|
|
28
39
|
};
|
|
29
40
|
const HOME = (0, os_1.homedir)();
|
package/dist/codex-config.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/** The table hwip writes into a config that has none.
|
|
2
|
-
* the Cursor/Windsurf JSON entry: codex is a terminal
|
|
3
|
-
* inherits the shell's `HWIP_API_KEY`, and an unexpanded
|
|
4
|
-
* literal would shadow the ~/.hwip/config.json fallback
|
|
5
|
-
* reasoning as opencode's). */
|
|
1
|
+
/** The table hwip writes into a config that has none. Its `env` holds only the
|
|
2
|
+
* auto-name flag, unlike the Cursor/Windsurf JSON entry: codex is a terminal
|
|
3
|
+
* app, so the server inherits the shell's `HWIP_API_KEY`, and an unexpanded
|
|
4
|
+
* `"${HWIP_API_KEY}"` literal would shadow the ~/.hwip/config.json fallback
|
|
5
|
+
* with junk (the same reasoning as opencode's). */
|
|
6
6
|
export declare const CODEX_MCP_TABLE: string;
|
|
7
7
|
/**
|
|
8
8
|
* The config with hwip's entry present and current, leaving every other byte
|
|
@@ -10,6 +10,9 @@ export declare const CODEX_MCP_TABLE: string;
|
|
|
10
10
|
* it, so nothing above can capture the keys.
|
|
11
11
|
*/
|
|
12
12
|
export declare const upsertHwipMcpTable: (config: string) => string;
|
|
13
|
+
/** Whether hwip's entry turns auto names on — what verify checks to tell an
|
|
14
|
+
* install from before the flag existed. */
|
|
15
|
+
export declare const hasHwipAutoName: (config: string) => boolean;
|
|
13
16
|
/** The config with hwip's entry and its sub-tables gone, or null when it was
|
|
14
17
|
* never there (so the caller can report "nothing to remove" rather than
|
|
15
18
|
* rewriting an untouched file). */
|
package/dist/codex-config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.readHwipMcpArgv = exports.removeHwipMcpTable = exports.upsertHwipMcpTable = exports.CODEX_MCP_TABLE = void 0;
|
|
3
|
+
exports.readHwipMcpArgv = exports.removeHwipMcpTable = exports.hasHwipAutoName = exports.upsertHwipMcpTable = exports.CODEX_MCP_TABLE = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* The hwip entry inside `~/.codex/config.toml`, which Codex CLI owns.
|
|
6
6
|
*
|
|
@@ -18,8 +18,10 @@ exports.readHwipMcpArgv = exports.removeHwipMcpTable = exports.upsertHwipMcpTabl
|
|
|
18
18
|
* `[mcp_servers.hwip.tools.hwip_ask]` when the user picks "always allow".
|
|
19
19
|
* Removal takes them along (a child of a deleted server is dead config);
|
|
20
20
|
* re-install keeps them, since they are the user's answer, not ours.
|
|
21
|
-
* - **Only `command` and `
|
|
22
|
-
* `cwd`, `startup_timeout_sec`, a comment
|
|
21
|
+
* - **Only `command`, `args` and the `HWIP_AUTONAME` env pair are ours.** Any
|
|
22
|
+
* other key in the table — `cwd`, `startup_timeout_sec`, a comment, the
|
|
23
|
+
* user's own env pairs — survives a re-install; the flag joins whichever
|
|
24
|
+
* env the user already keeps.
|
|
23
25
|
* - **Comments before the next table belong to that table**, not to ours, so
|
|
24
26
|
* the region we replace or delete stops short of them.
|
|
25
27
|
*
|
|
@@ -31,6 +33,8 @@ const [MCP_BIN, ...MCP_SUBCOMMAND] = mcp_command_js_1.MCP_LAUNCH_ARGV;
|
|
|
31
33
|
const HWIP_TABLE_HEADER = /^\[mcp_servers\.(?:hwip|"hwip")\]$/;
|
|
32
34
|
/** A sub-table of ours: `[mcp_servers.hwip.tools.hwip_ask]` and friends. */
|
|
33
35
|
const HWIP_SUBTABLE_HEADER = /^\[mcp_servers\.(?:hwip|"hwip")\..+\]$/;
|
|
36
|
+
/** The user's own env table for hwip — where the flag goes when they keep one. */
|
|
37
|
+
const HWIP_ENV_SUBTABLE_HEADER = /^\[mcp_servers\.(?:hwip|"hwip")\.(?:env|"env")\]$/;
|
|
34
38
|
/**
|
|
35
39
|
* The dotted/inline form — `mcp_servers.hwip = { command = "npx", … }` — which
|
|
36
40
|
* is valid TOML and which older hand-written configs use. It has to be found,
|
|
@@ -44,12 +48,87 @@ const isHwipDefinition = (line) => HWIP_TABLE_HEADER.test(line.trim()) || HWIP_I
|
|
|
44
48
|
const OWNED_KEY = /^(?:command|args)\s*=/;
|
|
45
49
|
const commandLine = `command = "${MCP_BIN}"`;
|
|
46
50
|
const argsLine = `args = [${MCP_SUBCOMMAND.map((a) => `"${a}"`).join(', ')}]`;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const flagPair = `${mcp_command_js_1.AUTONAME_ENV} = "1"`;
|
|
52
|
+
const envLine = `env = { ${flagPair} }`;
|
|
53
|
+
/** `env = { … }` in hwip's table — known by its key, so a comment after it cannot hide it. */
|
|
54
|
+
const INLINE_ENV = /^env\s*=\s*\{/;
|
|
55
|
+
/** `env.KEY = …`, the dotted spelling of the same table. */
|
|
56
|
+
const DOTTED_ENV = /^env\./;
|
|
57
|
+
/** The flag's key, bare or quoted: TOML reads all three as one key. */
|
|
58
|
+
const FLAG = `(?:${mcp_command_js_1.AUTONAME_ENV}|"${mcp_command_js_1.AUTONAME_ENV}"|'${mcp_command_js_1.AUTONAME_ENV}')`;
|
|
59
|
+
const FLAG_KEY = new RegExp(`^${FLAG}\\s*=`);
|
|
60
|
+
const DOTTED_FLAG_KEY = new RegExp(`^env\\.${FLAG}\\s*=`);
|
|
61
|
+
const FLAG_ON = new RegExp(`(?:^|[\\s{,.])${FLAG}\\s*=\\s*["']1["']`);
|
|
62
|
+
/** The table hwip writes into a config that has none. Its `env` holds only the
|
|
63
|
+
* auto-name flag, unlike the Cursor/Windsurf JSON entry: codex is a terminal
|
|
64
|
+
* app, so the server inherits the shell's `HWIP_API_KEY`, and an unexpanded
|
|
65
|
+
* `"${HWIP_API_KEY}"` literal would shadow the ~/.hwip/config.json fallback
|
|
66
|
+
* with junk (the same reasoning as opencode's). */
|
|
67
|
+
exports.CODEX_MCP_TABLE = ['[mcp_servers.hwip]', commandLine, argsLine, envLine].join('\n');
|
|
68
|
+
/**
|
|
69
|
+
* Walks a line of TOML outside its strings: `at(i, depth)` sees every character that is not
|
|
70
|
+
* inside one, with the brace depth before it, and stops the walk by returning true.
|
|
71
|
+
*/
|
|
72
|
+
const walkOutsideStrings = (text, from, at) => {
|
|
73
|
+
let depth = 0;
|
|
74
|
+
let quote = null;
|
|
75
|
+
for (let i = from; i < text.length; i += 1) {
|
|
76
|
+
const ch = text[i];
|
|
77
|
+
if (quote) {
|
|
78
|
+
if (ch === '\\' && quote === '"')
|
|
79
|
+
i += 1;
|
|
80
|
+
else if (ch === quote)
|
|
81
|
+
quote = null;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (ch === '"' || ch === "'") {
|
|
85
|
+
quote = ch;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (at(i, depth))
|
|
89
|
+
return;
|
|
90
|
+
if (ch === '{' || ch === '[')
|
|
91
|
+
depth += 1;
|
|
92
|
+
else if (ch === '}' || ch === ']')
|
|
93
|
+
depth -= 1;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/** The inline table's inside and what follows its closing brace (a comment), or null when it does
|
|
97
|
+
* not close on this line. */
|
|
98
|
+
const inlineParts = (line) => {
|
|
99
|
+
const open = line.indexOf('{');
|
|
100
|
+
let parts = null;
|
|
101
|
+
walkOutsideStrings(line, open, (i, depth) => {
|
|
102
|
+
if (line[i] !== '}' || depth !== 1)
|
|
103
|
+
return false;
|
|
104
|
+
parts = { inner: line.slice(open + 1, i), tail: line.slice(i + 1) };
|
|
105
|
+
return true;
|
|
106
|
+
});
|
|
107
|
+
return parts;
|
|
108
|
+
};
|
|
109
|
+
/** The pairs of an inline table, split at its own commas — never one inside a string or a nested value. */
|
|
110
|
+
const inlinePairs = (inner) => {
|
|
111
|
+
const pairs = [];
|
|
112
|
+
let start = 0;
|
|
113
|
+
walkOutsideStrings(inner, 0, (i, depth) => {
|
|
114
|
+
if (inner[i] === ',' && depth === 0) {
|
|
115
|
+
pairs.push(inner.slice(start, i));
|
|
116
|
+
start = i + 1;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
});
|
|
120
|
+
pairs.push(inner.slice(start));
|
|
121
|
+
return pairs.map((pair) => pair.trim()).filter(Boolean);
|
|
122
|
+
};
|
|
123
|
+
/** An inline env table with the flag on and first, the user's pairs and comment kept. One that
|
|
124
|
+
* cannot be read is left alone: writing a second `env` beside it would break the whole file. */
|
|
125
|
+
const withFlag = (line) => {
|
|
126
|
+
const parts = inlineParts(line);
|
|
127
|
+
if (!parts)
|
|
128
|
+
return line;
|
|
129
|
+
const rest = inlinePairs(parts.inner).filter((pair) => !FLAG_KEY.test(pair));
|
|
130
|
+
return `env = { ${[flagPair, ...rest].join(', ')} }${parts.tail}`;
|
|
131
|
+
};
|
|
53
132
|
/** CRLF in, CRLF out — a Windows config must not come back with mixed endings. */
|
|
54
133
|
const eolOf = (config) => (config.includes('\r\n') ? '\r\n' : '\n');
|
|
55
134
|
const splitLines = (config) => config.split(/\r?\n/);
|
|
@@ -109,18 +188,48 @@ const upsertHwipMcpTable = (config) => {
|
|
|
109
188
|
const table = exports.CODEX_MCP_TABLE.split('\n');
|
|
110
189
|
return rejoin([...lines.slice(0, start), ...table, ...lines.slice(start + 1)], eol);
|
|
111
190
|
}
|
|
191
|
+
// A key defined twice makes codex refuse the whole file, so the flag goes
|
|
192
|
+
// into whichever env the user already keeps, and gets one only if none.
|
|
193
|
+
const envTable = lines.slice(bodyEnd(lines, start), entryEnd(lines, start))
|
|
194
|
+
.findIndex((line) => HWIP_ENV_SUBTABLE_HEADER.test(line.trim()));
|
|
195
|
+
const after = [...lines];
|
|
196
|
+
if (envTable !== -1) {
|
|
197
|
+
const header = bodyEnd(lines, start) + envTable;
|
|
198
|
+
const tableEnd = bodyEnd(lines, header);
|
|
199
|
+
const rows = lines.slice(header + 1, tableEnd).filter((line) => !FLAG_KEY.test(line.trim()));
|
|
200
|
+
after.splice(header, tableEnd - header, lines[header], flagPair, ...rows);
|
|
201
|
+
}
|
|
112
202
|
const end = withoutTrailingPreamble(lines, start, bodyEnd(lines, start));
|
|
113
|
-
const kept = lines.slice(start + 1, end)
|
|
203
|
+
const kept = lines.slice(start + 1, end)
|
|
204
|
+
.filter((line) => !OWNED_KEY.test(line.trim()) && !DOTTED_FLAG_KEY.test(line.trim()));
|
|
205
|
+
const inline = kept.findIndex((line) => INLINE_ENV.test(line.trim()));
|
|
206
|
+
if (inline !== -1)
|
|
207
|
+
kept[inline] = withFlag(kept[inline].trim());
|
|
208
|
+
const ours = envTable !== -1 || inline !== -1 ? []
|
|
209
|
+
: kept.some((line) => DOTTED_ENV.test(line.trim())) ? [`env.${flagPair}`]
|
|
210
|
+
: [envLine];
|
|
114
211
|
return rejoin([
|
|
115
|
-
...
|
|
212
|
+
...after.slice(0, start),
|
|
116
213
|
'[mcp_servers.hwip]',
|
|
117
214
|
commandLine,
|
|
118
215
|
argsLine,
|
|
216
|
+
...ours,
|
|
119
217
|
...kept,
|
|
120
|
-
...
|
|
218
|
+
...after.slice(end),
|
|
121
219
|
], eol);
|
|
122
220
|
};
|
|
123
221
|
exports.upsertHwipMcpTable = upsertHwipMcpTable;
|
|
222
|
+
/** Whether hwip's entry turns auto names on — what verify checks to tell an
|
|
223
|
+
* install from before the flag existed. */
|
|
224
|
+
const hasHwipAutoName = (config) => {
|
|
225
|
+
const lines = splitLines(config);
|
|
226
|
+
const start = definitionIndex(lines);
|
|
227
|
+
if (start === -1)
|
|
228
|
+
return false;
|
|
229
|
+
const end = HWIP_INLINE_ENTRY.test(lines[start].trim()) ? start + 1 : entryEnd(lines, start);
|
|
230
|
+
return lines.slice(start, end).some((line) => !line.trim().startsWith('#') && FLAG_ON.test(line.trim()));
|
|
231
|
+
};
|
|
232
|
+
exports.hasHwipAutoName = hasHwipAutoName;
|
|
124
233
|
/** The config with hwip's entry and its sub-tables gone, or null when it was
|
|
125
234
|
* never there (so the caller can report "nothing to remove" rather than
|
|
126
235
|
* rewriting an untouched file). */
|
package/dist/command-scan.d.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Payload discipline (plan PLAN-13-16-18): paths never leave this machine, so
|
|
5
5
|
* the output carries skill names and nothing else.
|
|
6
6
|
*
|
|
7
|
-
* An entry is the
|
|
8
|
-
* registers the skill under (`AGENT_SKILL_PREFIX`, plugin names for Claude
|
|
7
|
+
* An entry is the invocation token itself, so it carries whatever prefix the
|
|
8
|
+
* agent registers the skill under (`AGENT_SKILL_PREFIX`, plugin names for Claude
|
|
9
|
+
* Code) — including the `$`/`@` mention sigil of agents that take no slash.
|
|
9
10
|
*
|
|
10
11
|
* Names come from the directory, not from the file — an ASSUMPTION, not a rule
|
|
11
12
|
* the agents enforce: pi resolves a skill's name as `frontmatter.name` and only
|
|
@@ -23,7 +24,10 @@
|
|
|
23
24
|
* existence until a divergent skill is actually measured.
|
|
24
25
|
*/
|
|
25
26
|
export interface AgentCommandEntry {
|
|
26
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* What the user types to run the skill: after a `/`, unless it starts with a
|
|
29
|
+
* mention sigil (`$`, `@`), which is typed as is — see `AGENT_SKILL_PREFIX`.
|
|
30
|
+
*/
|
|
27
31
|
name: string;
|
|
28
32
|
}
|
|
29
33
|
/** agentKind -> skill entries; agents with no skills are omitted. */
|
package/dist/command-scan.js
CHANGED
|
@@ -13,12 +13,24 @@ const agents_js_1 = require("./agents.js");
|
|
|
13
13
|
* pi session was observed to abort rather than run the skill (2026-09-13).
|
|
14
14
|
* Claude Code's user skills are `/<name>`; its PLUGIN skills are
|
|
15
15
|
* `/<plugin>:<name>`, so those get their prefix from the install entry instead.
|
|
16
|
+
*
|
|
17
|
+
* codex and opencode do not run skills as slash commands at all. codex takes a
|
|
18
|
+
* `$name` mention (codex-rs `skills/src/selection.rs`; its `/skills` only opens a
|
|
19
|
+
* picker), and opencode 2.0 an `@name` mention (`docs/skills.mdx § Loading`; its
|
|
20
|
+
* `/api/command` lists no skills). Their entries carry that sigil, and a client
|
|
21
|
+
* inserts a name starting with `$` or `@` as typed, without the `/`.
|
|
16
22
|
*/
|
|
17
23
|
const AGENT_SKILL_PREFIX = {
|
|
24
|
+
codex: '$',
|
|
25
|
+
opencode: '@',
|
|
18
26
|
pi: 'skill:',
|
|
19
27
|
};
|
|
20
|
-
/**
|
|
21
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Agents that register skills filed under grouping folders — see `scanSkillDir`.
|
|
30
|
+
* opencode walks them too: it lists `~/.claude/skills/synced/<id>/<name>`, which
|
|
31
|
+
* Claude Code itself does not register (`/api/skill`, 2026-10-20).
|
|
32
|
+
*/
|
|
33
|
+
const AGENT_SKILL_RECURSIVE = new Set(['opencode', 'pi']);
|
|
22
34
|
/** Serialized-catalog budget. Names-only measures ~2KB here; ws frames die at 128KB. */
|
|
23
35
|
exports.PAYLOAD_LIMIT_BYTES = 64 * 1024;
|
|
24
36
|
/**
|
|
@@ -88,28 +100,60 @@ const scanSkillDir = (dir, recursive = false, depth = 1) => {
|
|
|
88
100
|
}
|
|
89
101
|
return names;
|
|
90
102
|
};
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
* appear.
|
|
96
|
-
*/
|
|
97
|
-
const scanClaudePlugins = (home) => {
|
|
103
|
+
/** A plain JSON object, as opposed to an array, a scalar or null. */
|
|
104
|
+
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
105
|
+
/** The object under `key` in a JSON file, or an empty one when the file is missing, not JSON, or holds another shape there. */
|
|
106
|
+
const readJsonObject = (file, key) => {
|
|
98
107
|
let parsed;
|
|
99
108
|
try {
|
|
100
|
-
parsed = JSON.parse((0, fs_1.readFileSync)(
|
|
109
|
+
parsed = JSON.parse((0, fs_1.readFileSync)(file, 'utf-8'));
|
|
101
110
|
}
|
|
102
111
|
catch {
|
|
103
|
-
return
|
|
112
|
+
return {};
|
|
104
113
|
}
|
|
114
|
+
const value = isObject(parsed) ? parsed[key] : undefined;
|
|
115
|
+
return isObject(value) ? value : {};
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Plugin keys Claude Code loads: `enabledPlugins[key] === true` in the user's
|
|
119
|
+
* `~/.claude/settings.json`. Installed is not enabled — a key set to `false`, or
|
|
120
|
+
* absent, registers nothing (`claude -p` init `slash_commands`, 2026-10-20: 71
|
|
121
|
+
* skills of two disabled plugins and one unlisted plugin were missing). A
|
|
122
|
+
* project's own settings can enable more, but the catalog is device-level.
|
|
123
|
+
*/
|
|
124
|
+
const enabledClaudePlugins = (home) => {
|
|
125
|
+
const enabled = readJsonObject((0, path_1.join)(home, '.claude', 'settings.json'), 'enabledPlugins');
|
|
126
|
+
return new Set(Object.entries(enabled).filter(([, on]) => on === true).map(([key]) => key));
|
|
127
|
+
};
|
|
128
|
+
/** Command files of a plugin: `<installPath>/commands/<name>.md`. */
|
|
129
|
+
const scanCommandFiles = (dir) => entriesOf(dir)
|
|
130
|
+
.filter((entry) => entry.endsWith('.md') && !entry.startsWith('.'))
|
|
131
|
+
.map((entry) => entry.slice(0, -'.md'.length));
|
|
132
|
+
/**
|
|
133
|
+
* Plugin commands are `<installPath>/skills/<name>/SKILL.md` and
|
|
134
|
+
* `<installPath>/commands/<name>.md`, both registered as `/<plugin>:<name>`. Only
|
|
135
|
+
* install paths listed in `installed_plugins.json` are scanned — the plugin cache
|
|
136
|
+
* also holds uninstalled marketplace entries (514 of 710 on this machine) that
|
|
137
|
+
* must not appear — and of those only the enabled ones.
|
|
138
|
+
*/
|
|
139
|
+
const scanClaudePlugins = (home) => {
|
|
140
|
+
const installed = readJsonObject((0, path_1.join)(home, '.claude', 'plugins', 'installed_plugins.json'), 'plugins');
|
|
141
|
+
const enabled = enabledClaudePlugins(home);
|
|
105
142
|
const names = [];
|
|
106
|
-
for (const [key, installs] of Object.entries(
|
|
143
|
+
for (const [key, installs] of Object.entries(installed)) {
|
|
144
|
+
if (!enabled.has(key) || !Array.isArray(installs))
|
|
145
|
+
continue;
|
|
107
146
|
// Key is `<plugin>@<marketplace>`; the command prefix is the plugin half.
|
|
108
147
|
const plugin = key.split('@')[0];
|
|
109
|
-
for (const install of installs
|
|
110
|
-
|
|
148
|
+
for (const install of installs) {
|
|
149
|
+
const installPath = isObject(install) ? install.installPath : undefined;
|
|
150
|
+
if (typeof installPath !== 'string' || !installPath)
|
|
111
151
|
continue;
|
|
112
|
-
|
|
152
|
+
const commands = [
|
|
153
|
+
...scanSkillDir((0, path_1.join)(installPath, 'skills')),
|
|
154
|
+
...scanCommandFiles((0, path_1.join)(installPath, 'commands')),
|
|
155
|
+
];
|
|
156
|
+
names.push(...commands.map((name) => `${plugin}:${name}`));
|
|
113
157
|
}
|
|
114
158
|
}
|
|
115
159
|
return names;
|
package/dist/installer.js
CHANGED
|
@@ -334,7 +334,13 @@ const installGemini = () => {
|
|
|
334
334
|
ok('MCP server added (legacy gemini CLI)');
|
|
335
335
|
}
|
|
336
336
|
catch {
|
|
337
|
-
|
|
337
|
+
try {
|
|
338
|
+
(0, child_process_1.execSync)(mcp_command_js_1.GEMINI_MCP_ADD_BARE, { stdio: 'pipe' });
|
|
339
|
+
ok('MCP server added (legacy gemini CLI, without auto session names)');
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
fail(`MCP add failed. Manual: ${mcp_command_js_1.GEMINI_MCP_ADD}`);
|
|
343
|
+
}
|
|
338
344
|
}
|
|
339
345
|
}
|
|
340
346
|
try {
|
package/dist/listener.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { type HwipConfig } from './config.js';
|
|
|
24
24
|
import { type ShellAuditEntry } from './shell-audit.js';
|
|
25
25
|
import { type KnownSession } from './session-registry.js';
|
|
26
26
|
import { type AgentKind } from './remote-agents.js';
|
|
27
|
+
import type { ProviderNameSource } from './session-title.js';
|
|
27
28
|
import { type TranscriptSource } from './transcript-tail.js';
|
|
28
29
|
import { type AgentState } from './agent-state.js';
|
|
29
30
|
import { type SealedFileMeta, type SealedV2 } from './crypto.js';
|
|
@@ -121,6 +122,12 @@ interface AgentSession {
|
|
|
121
122
|
* word for both would put the ambiguity in the type.
|
|
122
123
|
*/
|
|
123
124
|
providerSessionName?: string | null;
|
|
125
|
+
/**
|
|
126
|
+
* Who chose `providerSessionName`: a person in the agent (`'user'`) or the
|
|
127
|
+
* agent itself (`'auto'`). Omitted when the agent's store cannot say. The
|
|
128
|
+
* account setting that hides agent-made names hides `'auto'` only.
|
|
129
|
+
*/
|
|
130
|
+
providerSessionNameSource?: ProviderNameSource;
|
|
124
131
|
createdAt?: string;
|
|
125
132
|
/**
|
|
126
133
|
* Which run of the name this is (hwip docs/session-runs.md): tmux
|
|
@@ -274,6 +281,8 @@ export declare const resolveKeys: (keys: string[]) => string[] | null;
|
|
|
274
281
|
export { invalidateTmuxSocketCache, silenceListenerLog, tmuxArgs };
|
|
275
282
|
export { liveSessionNames, sessionExists, sessionLiveness };
|
|
276
283
|
export { detectRemoteAgent, isSubagentSessionName, recordSubagentTranscripts, recordTargets, subagentTranscriptFor, tmuxTargetFor };
|
|
284
|
+
/** Test hook. */
|
|
285
|
+
export declare const resetPaneFitClaims: () => void;
|
|
277
286
|
/**
|
|
278
287
|
* Parse a `hwip-*` tmux session name into `{project, label}`. The wrapper
|
|
279
288
|
* emits `hwip-<project>` and, with `--label <x>`, `hwip-<project>-<x>`; the
|
package/dist/listener.js
CHANGED
|
@@ -25,9 +25,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
26
|
};
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
-
exports.
|
|
29
|
-
exports.
|
|
30
|
-
exports.handleListener = exports.liveScheduleRunnerDeps = exports.listenerSettings = exports.resolveWsUrl = exports.streamSession = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.displayFolder = exports.claimLanFile = exports.saveFileFromS3 = exports.downloadSealedEach = exports.answerFileBound = exports.downloadAttachments = exports.setAttachmentContext = exports.shellTextRefusal = exports.writeRemoteMarker = void 0;
|
|
28
|
+
exports.setSubscriberTrust = exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.withSettledPrompts = exports.PROMPT_SEAL_WAIT_MS = exports.withBlockedPrompts = exports.setPromptSealer = exports.deriveSessionState = exports.resetSessionStates = exports.continueRuns = exports.runIdOfEpoch = exports.__checkoutCacheSize = exports.parseSessionName = exports.resetPaneFitClaims = exports.tmuxTargetFor = exports.subagentTranscriptFor = exports.recordTargets = exports.recordSubagentTranscripts = exports.isSubagentSessionName = exports.detectRemoteAgent = exports.sessionLiveness = exports.sessionExists = exports.liveSessionNames = exports.tmuxArgs = exports.silenceListenerLog = exports.invalidateTmuxSocketCache = exports.resolveKeys = exports.probePane = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = exports.commandsReportDue = exports.commandsFingerprint = exports.COMMAND_SCAN_INTERVAL_MS = exports.sessionsReportDue = exports.sessionsFingerprint = exports.knownSessionsFingerprint = exports.knownSessionsToReport = exports.withRunSeqs = exports.KNOWN_SESSIONS_REPORTED = exports.staleConnectionAction = exports.askedToReconnect = exports.MAX_STALE_CONNECTION_DROPS = exports.createReportAckWatch = exports.REPORT_ACK_TIMEOUT_MS = exports.SESSION_REPORT_HEARTBEAT_MS = exports.SESSION_REPORT_INTERVAL_MS = exports.heartbeatVerdict = exports.pingDue = exports.PING_IDLE_MS = void 0;
|
|
29
|
+
exports.syncTmuxAgentNames = exports.recordInventory = exports.collectSessionsVerbose = exports.handleCommandInput = exports.pendingInputDecrypts = exports.MAX_PENDING_DECRYPTS = exports.MAX_INPUT_LANES = exports.validateInputMessage = exports.MAX_INPUT_BODY_CHARS = exports.MAX_INPUT_KEYS = exports.handleStreamControl = exports.buildStreamFrame = exports.isStreamCapReached = exports.fitSealedFrame = exports.SEALED_FRAME_MAX_BYTES = exports.stopAllStreams = exports.stopStream = exports.stopStreamOf = exports.handleTurnWatch = exports.sealTurnBatch = exports.resolveWatchTranscript = exports.MAX_CONCURRENT_STREAMS = exports.STREAM_RENEW_FRESH_MS = exports.STREAM_LEASE_MS = exports.claimFrameSend = exports.hasFrameBudget = exports.streamCadence = exports.MAX_FRAMES_PER_SEC = exports.BURST_WINDOW_MS = exports.BURST_INTERVAL_MS = exports.STREAM_INTERVAL_MS = exports.STREAM_CAPTURE_LINES = exports.scheduleInjectSnapshots = exports.historyLinesIn = exports.cursorLineFor = exports.parsePaneGeometry = exports.STREAM_GEOMETRY_FORMAT = exports.handleScreenHistoryRequest = exports.SCREEN_HISTORY_MAX_LINES = exports.handleSessionForgetRequest = exports.handleSessionExitRequest = exports.SESSION_EXIT_STEP_MS = exports.handleSessionResumeRequest = exports.handleScheduleListRequest = exports.handleFileBlobRequest = exports.handleFileTreeRequest = exports.handleDiffFileRequest = exports.handleDiffFilesRequest = exports.fileViewBytes = exports.handleScreenRequest = void 0;
|
|
30
|
+
exports.handleListener = exports.liveScheduleRunnerDeps = exports.listenerSettings = exports.resolveWsUrl = exports.streamSession = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.displayFolder = exports.claimLanFile = exports.saveFileFromS3 = exports.downloadSealedEach = exports.answerFileBound = exports.downloadAttachments = exports.setAttachmentContext = exports.shellTextRefusal = exports.writeRemoteMarker = exports.isInventoried = void 0;
|
|
31
31
|
const child_process_1 = require("child_process");
|
|
32
32
|
const crypto_1 = require("crypto");
|
|
33
33
|
const fs_1 = require("fs");
|
|
@@ -328,7 +328,7 @@ exports.knownSessionsFingerprint = knownSessionsFingerprint;
|
|
|
328
328
|
const sessionsFingerprint = (sessions) => sessions
|
|
329
329
|
.map((s) => JSON.stringify([
|
|
330
330
|
s.name, s.attached, s.agentKind, s.agentSessionId ?? null,
|
|
331
|
-
s.project, s.label ?? null, s.providerSessionName ?? null, s.createdAt ?? null, s.runId ?? null, s.firstSeq ?? null,
|
|
331
|
+
s.project, s.label ?? null, s.providerSessionName ?? null, s.providerSessionNameSource ?? null, s.createdAt ?? null, s.runId ?? null, s.firstSeq ?? null,
|
|
332
332
|
s.lastActivityAt ?? null, s.state ?? null,
|
|
333
333
|
s.stateChangedAt ?? null, s.stateRuleId ?? null,
|
|
334
334
|
s.blockedPrompt?.id ?? null, s.blockedPrompt?.at ?? null,
|
|
@@ -588,8 +588,13 @@ const worktreeCleanup = (0, listener_worktree_cleanup_js_1.createWorktreeCleanup
|
|
|
588
588
|
notify: desktop_notify_js_1.notifyDesktop,
|
|
589
589
|
tmux: (args, timeoutMs) => (0, worktree_js_1.tmuxRun)((0, tmux_socket_js_1.tmuxArgs)(args), timeoutMs),
|
|
590
590
|
});
|
|
591
|
-
/** Sizes a watched
|
|
592
|
-
const paneFitter = (0, pane_fit_js_1.createPaneFitter)(runTmux);
|
|
591
|
+
/** Sizes a watched window to its viewer while the phone is its latest user (pane-fit.ts). */
|
|
592
|
+
const paneFitter = (0, pane_fit_js_1.createPaneFitter)(runTmux, undefined, (0, pane_fit_js_1.macosLastKeyAt)());
|
|
593
|
+
/** Test hook. */
|
|
594
|
+
const resetPaneFitClaims = () => paneFitter.forgetClaims();
|
|
595
|
+
exports.resetPaneFitClaims = resetPaneFitClaims;
|
|
596
|
+
/** How often fitted windows are checked for a desktop that took them back; idle windows cost nothing. */
|
|
597
|
+
const PANE_FIT_RECHECK_MS = 1_000;
|
|
593
598
|
// ─── Session inventory ──────────────────────────────────────────────
|
|
594
599
|
/**
|
|
595
600
|
* Parse a `hwip-*` tmux session name into `{project, label}`. The wrapper
|
|
@@ -2076,9 +2081,9 @@ const leaseFor = (renewing) => (renewing ? exports.STREAM_LEASE_MS : STREAM_MAX_
|
|
|
2076
2081
|
* Live mirrors, one per (session, viewing device): every viewer keeps the whole of the machinery a
|
|
2077
2082
|
* single viewer had — its own capture loop state, lease, nonce, epoch and seq, sealed frames — so
|
|
2078
2083
|
* two devices watching one pane do not take each other's stream over. A viewer with no device id
|
|
2079
|
-
* (a web tab) shares the empty slot, one at a time, as before. The tmux pane is shared:
|
|
2080
|
-
* is whatever the last viewer to send input or `cols` asked for
|
|
2081
|
-
* receives the pane at that width.
|
|
2084
|
+
* (a web tab) shares the empty slot, one at a time, as before. The tmux pane is shared: while the
|
|
2085
|
+
* phone is its latest user its width is whatever the last viewer to send input or `cols` asked for,
|
|
2086
|
+
* and a desktop client used since takes it back (pane-fit.ts); every viewer receives the pane at that width.
|
|
2082
2087
|
*/
|
|
2083
2088
|
const activeStreams = new Map();
|
|
2084
2089
|
const streamKey = (sessionName, deviceId) => `${sessionName}\u0000${typeof deviceId === 'string' ? deviceId : ''}`;
|
|
@@ -2607,6 +2612,9 @@ const handleStreamControl = (req, send) => {
|
|
|
2607
2612
|
const renewTarget = (0, tmux_pane_js_1.tmuxTargetFor)(req.sessionName);
|
|
2608
2613
|
if (renewTarget !== null && (colsChanged || streamsOf(req.sessionName).length === 1))
|
|
2609
2614
|
paneFitter.fit(req.sessionName, renewTarget, req.cols);
|
|
2615
|
+
// Otherwise keep the width the pane has, but still notice a desktop client used since (pane-fit.ts).
|
|
2616
|
+
else
|
|
2617
|
+
paneFitter.recheck(req.sessionName);
|
|
2610
2618
|
return true;
|
|
2611
2619
|
}
|
|
2612
2620
|
// Addressed to us but we hold no such stream: it was reaped (lost
|
|
@@ -2648,6 +2656,8 @@ const handleStreamControl = (req, send) => {
|
|
|
2648
2656
|
return true;
|
|
2649
2657
|
}
|
|
2650
2658
|
const fitTarget = (0, tmux_pane_js_1.tmuxTargetFor)(sessionName);
|
|
2659
|
+
// Opening the mirror (or resubscribing) takes the width only from an idle desktop (pane-fit.ts).
|
|
2660
|
+
paneFitter.open(sessionName);
|
|
2651
2661
|
if (fitTarget !== null)
|
|
2652
2662
|
paneFitter.fit(sessionName, fitTarget, req.cols);
|
|
2653
2663
|
let lastContent = null;
|
|
@@ -3047,6 +3057,23 @@ const deliverInput = (input) => {
|
|
|
3047
3057
|
// Only a reason that does not depend on the text: this input may be sealed.
|
|
3048
3058
|
if (!injected)
|
|
3049
3059
|
input.send(streamErrorFrame(input.sessionName, 'input_rejected', input, reason && (0, command_refusal_js_1.isSessionStateRefusal)(reason) ? reason : undefined));
|
|
3060
|
+
else
|
|
3061
|
+
takeWidthBack(input.sessionName, input.deviceId);
|
|
3062
|
+
};
|
|
3063
|
+
/**
|
|
3064
|
+
* Typing from the phone makes it the session's latest user again (pane-fit.ts): a window a desktop
|
|
3065
|
+
* client took back is sized to the sender's width once more.
|
|
3066
|
+
*/
|
|
3067
|
+
const takeWidthBack = (sessionName, deviceId) => {
|
|
3068
|
+
paneFitter.claim(sessionName);
|
|
3069
|
+
const viewer = activeStreams.get(streamKey(sessionName, deviceId)) ?? streamsOf(sessionName)[0];
|
|
3070
|
+
// Already at the sender's width: the claim is enough, and a keystroke costs no tmux round trip (renew rechecks).
|
|
3071
|
+
if (!viewer || paneFitter.fitted(sessionName, viewer.cols))
|
|
3072
|
+
return;
|
|
3073
|
+
const target = (0, tmux_pane_js_1.tmuxTargetFor)(sessionName);
|
|
3074
|
+
if (target === null)
|
|
3075
|
+
return;
|
|
3076
|
+
paneFitter.fit(sessionName, target, viewer.cols);
|
|
3050
3077
|
};
|
|
3051
3078
|
/** Ceiling on concurrent sender lanes. The lane key includes a relay-stamped
|
|
3052
3079
|
* deviceId, but a relay older than the unconditional stamp lets a sender vary
|
|
@@ -3448,7 +3475,7 @@ const collectSessionsVerbose = () => {
|
|
|
3448
3475
|
const agentSessionId = info.currentPath
|
|
3449
3476
|
? (main.agent.resolveSessionId?.(info.currentPath, info.panePid ?? undefined) ?? null)
|
|
3450
3477
|
: null;
|
|
3451
|
-
const
|
|
3478
|
+
const providerName = info.currentPath
|
|
3452
3479
|
? (main.agent.resolveSessionName?.(info.currentPath, info.panePid ?? undefined) ?? null)
|
|
3453
3480
|
: null;
|
|
3454
3481
|
if (info.currentPath)
|
|
@@ -3466,7 +3493,8 @@ const collectSessionsVerbose = () => {
|
|
|
3466
3493
|
agentSessionId,
|
|
3467
3494
|
project: parsed.project,
|
|
3468
3495
|
label: parsed.label,
|
|
3469
|
-
providerSessionName,
|
|
3496
|
+
providerSessionName: providerName?.name ?? null,
|
|
3497
|
+
...(providerName?.source ? { providerSessionNameSource: providerName.source } : {}),
|
|
3470
3498
|
createdAt: epochToIso(group.created),
|
|
3471
3499
|
runId: (0, exports.runIdOfEpoch)(group.created),
|
|
3472
3500
|
lastActivityAt: epochToIso(group.activity),
|
|
@@ -4935,6 +4963,10 @@ const streamSession = (wsUrl, apiKey) => {
|
|
|
4935
4963
|
clearInterval(commandsTimer);
|
|
4936
4964
|
commandsTimer = null;
|
|
4937
4965
|
}
|
|
4966
|
+
if (paneFitTimer) {
|
|
4967
|
+
clearInterval(paneFitTimer);
|
|
4968
|
+
paneFitTimer = null;
|
|
4969
|
+
}
|
|
4938
4970
|
if (connectTimer) {
|
|
4939
4971
|
clearTimeout(connectTimer);
|
|
4940
4972
|
connectTimer = null;
|
|
@@ -4981,6 +5013,7 @@ const streamSession = (wsUrl, apiKey) => {
|
|
|
4981
5013
|
// rescan changes it. The rescan has its own slow timer — it reads the
|
|
4982
5014
|
// filesystem, so it must never ride the 5 s session loop.
|
|
4983
5015
|
let commandsTimer = null;
|
|
5016
|
+
let paneFitTimer = null;
|
|
4984
5017
|
// Per connection, like the inventory gate above: `sock.send` returning
|
|
4985
5018
|
// is not delivery, and a fingerprint that outlived the socket would let
|
|
4986
5019
|
// one lost frame leave the server's catalog stale until the user next
|
|
@@ -5157,6 +5190,8 @@ const streamSession = (wsUrl, apiKey) => {
|
|
|
5157
5190
|
sessionsTimer = setInterval(() => void reportSessions(), exports.SESSION_REPORT_INTERVAL_MS);
|
|
5158
5191
|
void reportCommands();
|
|
5159
5192
|
commandsTimer = setInterval(reportCommands, exports.COMMAND_SCAN_INTERVAL_MS);
|
|
5193
|
+
// A desktop key takes a fitted window back within a second, not at the phone's next renew (5 s).
|
|
5194
|
+
paneFitTimer = setInterval(() => paneFitter.recheckAll(), PANE_FIT_RECHECK_MS);
|
|
5160
5195
|
loopSampleAt = Date.now();
|
|
5161
5196
|
loopMeterTimer = setInterval(() => {
|
|
5162
5197
|
const now = Date.now();
|