@agfpd/iapeer 0.4.66 → 0.4.67
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/docs/16-tray.md +16 -8
- package/docs/ru/16-tray.md +14 -6
- package/package.json +1 -1
- package/src/cli/index.ts +15 -0
- package/src/tray/index.ts +9 -3
- package/src/tray/install.ts +30 -0
- package/src/tray/render.ts +47 -37
package/docs/16-tray.md
CHANGED
|
@@ -26,17 +26,25 @@ activation that also installs SwiftBar.app when it is absent.
|
|
|
26
26
|
`●` live · `○` asleep · `✕` stopped — green when any runtime is live. Badges: `👤`
|
|
27
27
|
a human is attached, `🔒` launchd-managed (H4 read-only), `⏳N` an ephemeral peer's
|
|
28
28
|
queue depth.
|
|
29
|
-
- **Click
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
- **Click an agent peer → Terminal.app running `iapeer attach <peer>`.** The row itself is
|
|
30
|
+
the attach action (no submenu, so a click attaches rather than merely expanding). The
|
|
31
|
+
handoff goes through `iapeer tray attach-term`, which writes a per-peer `.command`
|
|
32
|
+
launcher and `open`s it — `open <file>.command` hands the file to Terminal.app with
|
|
33
|
+
**no Automation/Accessibility permission prompt**. This deliberately avoids SwiftBar's
|
|
34
|
+
own `terminal=true`, whose Terminal launch drives a `Cmd-T` System-Events keystroke that
|
|
35
|
+
silently no-ops unless SwiftBar holds Accessibility permission. **Launchd-managed infra
|
|
36
|
+
peers (telegram / notifier, `🔒`) are not pty-attachable** — their rows are plain status
|
|
37
|
+
lines. Lifecycle actions (Wake / Stop / Start / New / Interrupt / Refresh / Compact) live
|
|
38
|
+
in the **Manage** submenu, each a `POST /fleet/v1/peers/<peer>/<cmd>` via `iapeer tray cmd`.
|
|
34
39
|
|
|
35
40
|
The list updates itself: the plugin is **streamable** — it subscribes to the event stream
|
|
36
41
|
and re-fetches the snapshot on every change (a peer waking, an operator stop/start, a
|
|
37
|
-
death), coalescing bursts. A
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
death), coalescing bursts. A heartbeat keeps ages fresh and self-heals a missed event; the
|
|
43
|
+
stream reconnects across daemon restarts. Every emitted menu block is prefixed with the
|
|
44
|
+
`~~~` stream separator (SwiftBar's default leading-separator mode = replace), including the
|
|
45
|
+
first — otherwise a manual Refresh, which `terminate()`s and re-`invoke()`s the plugin
|
|
46
|
+
without clearing SwiftBar's held content, would append the restarted block and render the
|
|
47
|
+
whole dashboard twice.
|
|
40
48
|
|
|
41
49
|
## Verbs
|
|
42
50
|
|
package/docs/ru/16-tray.md
CHANGED
|
@@ -25,15 +25,23 @@ SwiftBar.app, если его нет.
|
|
|
25
25
|
`●` живой · `○` спит · `✕` остановлен — зелёный, если хоть один рантайм живой. Бейджи:
|
|
26
26
|
`👤` подключён человек, `🔒` под launchd (H4 read-only), `⏳N` глубина очереди эфемерного
|
|
27
27
|
пира.
|
|
28
|
-
- **Клик по
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
- **Клик по агент-пиру → Terminal.app с `iapeer attach <peer>`.** Строка сама и есть действие
|
|
29
|
+
attach (без подменю — клик аттачит, а не раскрывает). Передача идёт через `iapeer tray
|
|
30
|
+
attach-term`: пишет per-peer `.command`-лаунчер и `open`'ит его — `open <файл>.command`
|
|
31
|
+
отдаёт файл Terminal.app **без запроса Automation/Accessibility**. Это намеренно обходит
|
|
32
|
+
родной `terminal=true` SwiftBar, чей запуск Terminal бьёт `Cmd-T` кейстроком через System
|
|
33
|
+
Events и молча не срабатывает без права Accessibility у SwiftBar. **Launchd-инфра-пиры
|
|
34
|
+
(telegram / notifier, `🔒`) не pty-аттачатся** — их строки просто статус. Lifecycle-действия
|
|
35
|
+
(Wake / Stop / Start / New / Interrupt / Refresh / Compact) — в подменю **Manage**, каждое —
|
|
36
|
+
`POST /fleet/v1/peers/<peer>/<cmd>` через `iapeer tray cmd`.
|
|
32
37
|
|
|
33
38
|
Список обновляется сам: плагин **streamable** — подписан на поток событий и перечитывает
|
|
34
39
|
снапшот на каждое изменение (пробуждение пира, стоп/старт оператором, смерть), склеивая
|
|
35
|
-
всплески. Хартбит
|
|
36
|
-
переподключается через рестарты демона.
|
|
40
|
+
всплески. Хартбит держит возрасты свежими и самозалечивает пропущенное событие; поток
|
|
41
|
+
переподключается через рестарты демона. Каждый выдаваемый блок меню предваряется сепаратором
|
|
42
|
+
`~~~` (дефолтный leading-режим SwiftBar = замена), включая первый — иначе ручной Refresh,
|
|
43
|
+
который `terminate()`'ит и заново `invoke()`'ит плагин НЕ очищая удерживаемый SwiftBar контент,
|
|
44
|
+
пришил бы перезапущенный блок и отрисовал бы весь дашборд дважды.
|
|
37
45
|
|
|
38
46
|
## Verb'ы
|
|
39
47
|
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -2505,6 +2505,21 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2505
2505
|
out(await tray.renderTrayOnce(env))
|
|
2506
2506
|
return 0
|
|
2507
2507
|
}
|
|
2508
|
+
case 'attach-term': {
|
|
2509
|
+
// Terminal handoff for a tray click: open a system Terminal running
|
|
2510
|
+
// `iapeer attach <peer>` via an `open`ed `.command` (no Accessibility/
|
|
2511
|
+
// Automation TCC — unlike SwiftBar's own terminal=true).
|
|
2512
|
+
const peer = positionals[1]
|
|
2513
|
+
if (!peer) return argErr(errOut, 'usage: iapeer tray attach-term <peer> [runtime]')
|
|
2514
|
+
try {
|
|
2515
|
+
const r = tray.trayAttachTerm({ env, personality: peer, runtime: positionals[2] })
|
|
2516
|
+
out(`opening Terminal → iapeer attach ${peer} (${r.cmdFile})\n`)
|
|
2517
|
+
} catch (e) {
|
|
2518
|
+
errOut(`tray attach-term: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
2519
|
+
return 1
|
|
2520
|
+
}
|
|
2521
|
+
return 0
|
|
2522
|
+
}
|
|
2508
2523
|
case 'cmd': {
|
|
2509
2524
|
const command = positionals[1]
|
|
2510
2525
|
const peer = positionals[2]
|
package/src/tray/index.ts
CHANGED
|
@@ -72,14 +72,20 @@ export async function streamTray(
|
|
|
72
72
|
out: (s: string) => void = s => process.stdout.write(s),
|
|
73
73
|
signal?: AbortSignal,
|
|
74
74
|
): Promise<void> {
|
|
75
|
-
let first = true
|
|
76
75
|
// Serialize all emits through a promise chain so a `~~~`+block is never interleaved.
|
|
77
76
|
let chain: Promise<void> = Promise.resolve()
|
|
78
77
|
const emit = (): Promise<void> => {
|
|
79
78
|
chain = chain.then(async () => {
|
|
80
79
|
const block = await renderTrayOnce(env)
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// EVERY block — including the first — is prefixed with the `~~~` stream separator.
|
|
81
|
+
// SwiftBar's default (leading-separator) handler sets content = <text after the
|
|
82
|
+
// last ~~~>, i.e. REPLACE; a block WITHOUT a leading ~~~ is APPENDED to the
|
|
83
|
+
// current content. On a refresh (SwiftBar terminate()s the process WITHOUT
|
|
84
|
+
// clearing content, then re-invoke()s), the restarted process's first block would
|
|
85
|
+
// otherwise append to the stale pre-refresh menu → the whole dashboard renders
|
|
86
|
+
// twice. Always leading with ~~~ makes every emit a clean replace. (root: SwiftBar
|
|
87
|
+
// v2.0.1 StreamablePlugin.onOutputUpdate.)
|
|
88
|
+
out(`~~~\n${block}`)
|
|
83
89
|
})
|
|
84
90
|
return chain
|
|
85
91
|
}
|
package/src/tray/install.ts
CHANGED
|
@@ -259,6 +259,36 @@ export function uninstallTray(
|
|
|
259
259
|
return { removed, refreshed }
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Open a system Terminal attached to a peer WITHOUT SwiftBar's `terminal=true`. SwiftBar
|
|
264
|
+
* v2's Terminal.app launch drives a `Cmd-T` keystroke via System Events, which silently
|
|
265
|
+
* no-ops unless SwiftBar holds Accessibility permission — so tray attach clicks appeared
|
|
266
|
+
* dead even after the Automation→Terminal grant. Instead: write a per-peer `.command`
|
|
267
|
+
* launcher and `open` it. `open <file>.command` hands the file to Terminal.app (its
|
|
268
|
+
* default handler) and runs it — a document-open, needing NO Automation/Accessibility
|
|
269
|
+
* TCC. This is the tray's terminal handoff (docs/15: attach is client-side). The peer
|
|
270
|
+
* name is validated (it is interpolated into a shell script). Returns the launcher path.
|
|
271
|
+
*/
|
|
272
|
+
export function trayAttachTerm(opts: {
|
|
273
|
+
env?: NodeJS.ProcessEnv
|
|
274
|
+
personality: string
|
|
275
|
+
runtime?: string
|
|
276
|
+
run?: Runner
|
|
277
|
+
}): { cmdFile: string } {
|
|
278
|
+
const env = opts.env ?? process.env
|
|
279
|
+
const run = opts.run ?? defaultRun
|
|
280
|
+
const p = opts.personality
|
|
281
|
+
if (!/^[a-z][a-z0-9-]*$/.test(p)) throw new Error(`invalid peer name "${p}"`)
|
|
282
|
+
const rt = opts.runtime && /^[a-z][a-z0-9-]*$/.test(opts.runtime) ? opts.runtime : undefined
|
|
283
|
+
const binPath = iapeerBinPath(env)
|
|
284
|
+
const dir = join(resolveGlobalRoot(env), 'tray')
|
|
285
|
+
mkdirSync(dir, { recursive: true })
|
|
286
|
+
const cmdFile = join(dir, `attach-${p}.command`)
|
|
287
|
+
writeFileSync(cmdFile, `#!/bin/bash\nexec "${binPath}" attach ${p}${rt ? ` ${rt}` : ''}\n`, { mode: 0o755 })
|
|
288
|
+
run('open', [cmdFile])
|
|
289
|
+
return { cmdFile }
|
|
290
|
+
}
|
|
291
|
+
|
|
262
292
|
export interface TrayStatus {
|
|
263
293
|
daemon: { fleet: boolean; version?: string; sock?: string; tcp?: string }
|
|
264
294
|
swiftbar: { installed: boolean; pluginDir?: string }
|
package/src/tray/render.ts
CHANGED
|
@@ -79,11 +79,6 @@ function line(depth: number, text: string, p: Record<string, string | number | b
|
|
|
79
79
|
return `${'--'.repeat(depth)}${text}${params(p)}`
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/** A menu separator at submenu `depth`: the `--`×depth prefix + `---` (SwiftBar: a
|
|
83
|
-
* top-level separator is `---`, a depth-1 one is `-----`, etc.). */
|
|
84
|
-
function separator(depth: number): string {
|
|
85
|
-
return `${'--'.repeat(depth)}---`
|
|
86
|
-
}
|
|
87
82
|
|
|
88
83
|
// ── time helpers ────────────────────────────────────────────────────────────────
|
|
89
84
|
|
|
@@ -154,14 +149,19 @@ export function renderSwiftBar(snapshot: TraySnapshot, opts: RenderOptions): str
|
|
|
154
149
|
if (providers) lines.push(line(0, providers, { color: COLOR.meta }))
|
|
155
150
|
lines.push('---')
|
|
156
151
|
|
|
157
|
-
// ── peers ──
|
|
152
|
+
// ── peers (each row is a DIRECT attach action — click → Terminal) ──
|
|
158
153
|
if (peers.length === 0) {
|
|
159
154
|
lines.push(line(0, 'no peers registered', { color: COLOR.meta }))
|
|
160
155
|
}
|
|
161
156
|
for (const p of peers) {
|
|
162
|
-
lines.push(
|
|
157
|
+
lines.push(renderPeerRow(p, bin, now))
|
|
163
158
|
}
|
|
164
159
|
|
|
160
|
+
// ── Manage submenu (lifecycle commands per peer; kept off the rows so a row click
|
|
161
|
+
// attaches instead of just expanding a submenu) ──
|
|
162
|
+
lines.push('---')
|
|
163
|
+
lines.push(...renderManage(peers, bin))
|
|
164
|
+
|
|
165
165
|
// ── footer ──
|
|
166
166
|
lines.push('---')
|
|
167
167
|
lines.push(line(0, 'Refresh', { refresh: true, sfimage: 'arrow.clockwise' }))
|
|
@@ -184,7 +184,23 @@ function renderProviders(host: TraySnapshot['host']): string {
|
|
|
184
184
|
return seg.join(' · ')
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
/** A peer is attachable when it is NOT launchd-managed: launchd-owned infra peers
|
|
188
|
+
* (telegram / notifier) have no attachable pty session, so their rows carry no attach
|
|
189
|
+
* action (a plain status line). Agent peers (claude / codex) are attachable. */
|
|
190
|
+
function attachable(p: TrayPeer): boolean {
|
|
191
|
+
return !p.launchd_managed
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* One peer row. For an ATTACHABLE peer the row IS a DIRECT attach action (no submenu,
|
|
196
|
+
* so a click attaches instead of merely expanding): clicking runs `iapeer tray
|
|
197
|
+
* attach-term <peer>` in the BACKGROUND (terminal=false), which opens a `.command` via
|
|
198
|
+
* `open` — a system-Terminal handoff that needs NO Accessibility/Automation TCC. This
|
|
199
|
+
* deliberately AVOIDS SwiftBar's own `terminal=true`, whose Terminal.app launch drives a
|
|
200
|
+
* `Cmd-T` System-Events keystroke that silently no-ops without Accessibility permission.
|
|
201
|
+
* Infra (launchd) rows are plain status lines. Lifecycle lives in the Manage submenu.
|
|
202
|
+
*/
|
|
203
|
+
function renderPeerRow(p: TrayPeer, bin: string, now: number): string {
|
|
188
204
|
const agg = aggregate(p)
|
|
189
205
|
const detail = p.runtimes.map(r => `${r.runtime}${GLYPH[r.status]}`).join(' ')
|
|
190
206
|
const badges =
|
|
@@ -194,36 +210,30 @@ function renderPeerRow(p: TrayPeer, bin: string, now: number): string[] {
|
|
|
194
210
|
const ageStr = p.last_active_ms ? ` ${fmtAge(p.last_active_ms, now)}` : ''
|
|
195
211
|
const label = `${p.personality} ${detail}${badges}${ageStr}`
|
|
196
212
|
const rowColor = agg === 'live' ? COLOR.live : COLOR.meta
|
|
213
|
+
if (!attachable(p)) return line(0, label, { color: rowColor })
|
|
214
|
+
return line(0, label, { color: rowColor, bash: bin, param1: 'tray', param2: 'attach-term', param3: p.personality, terminal: false })
|
|
215
|
+
}
|
|
197
216
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
out.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
bash: bin,
|
|
219
|
-
param1: 'tray',
|
|
220
|
-
param2: 'cmd',
|
|
221
|
-
param3: c.cmd,
|
|
222
|
-
param4: p.personality,
|
|
223
|
-
terminal: false,
|
|
224
|
-
refresh: true,
|
|
225
|
-
}),
|
|
226
|
-
)
|
|
217
|
+
/** The Manage submenu: one sub-submenu per peer with its lifecycle commands, each a
|
|
218
|
+
* `iapeer tray cmd <c> <peer>` → POST /fleet/v1/peers/<peer>/<c> (terminal=false,
|
|
219
|
+
* refresh to reflect the outcome). Kept OFF the peer rows so a row click attaches. */
|
|
220
|
+
function renderManage(peers: TrayPeer[], bin: string): string[] {
|
|
221
|
+
const out: string[] = [line(0, 'Manage', { sfimage: 'slider.horizontal.3' })]
|
|
222
|
+
for (const p of peers) {
|
|
223
|
+
out.push(line(1, p.personality))
|
|
224
|
+
for (const c of MANAGE_COMMANDS) {
|
|
225
|
+
out.push(
|
|
226
|
+
line(2, c.label, {
|
|
227
|
+
bash: bin,
|
|
228
|
+
param1: 'tray',
|
|
229
|
+
param2: 'cmd',
|
|
230
|
+
param3: c.cmd,
|
|
231
|
+
param4: p.personality,
|
|
232
|
+
terminal: false,
|
|
233
|
+
refresh: true,
|
|
234
|
+
}),
|
|
235
|
+
)
|
|
236
|
+
}
|
|
227
237
|
}
|
|
228
238
|
return out
|
|
229
239
|
}
|