@agfpd/iapeer 0.4.50 → 0.4.51

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.4.50",
3
+ "version": "0.4.51",
4
4
  "description": "Run Claude Code and Codex CLI as local background agents that message each other, wake on demand, and share one memory.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
@@ -125,3 +125,14 @@ export function ellipsize(s: string, width: number): string {
125
125
  const chars = [...s]
126
126
  return chars.length <= width ? s : chars.slice(0, Math.max(0, width - 1)).join('') + '…'
127
127
  }
128
+
129
+ // ─── attach-child failure surfacing ─────────────────────────────────────────
130
+
131
+ /** Why a spawned `iapeer attach <peer>` run needs the operator's attention, or null when it
132
+ * ended fine. PURE: a spawn error AND a non-zero/signal exit both surface — a failed child
133
+ * silently remounted over reads as "Enter does nothing" to the operator (live incident 03.07). */
134
+ export function attachFailureMessage(r: { error?: Error; status: number | null }): string | null {
135
+ if (r.error) return r.error.message
136
+ if ((r.status ?? 1) !== 0) return `iapeer attach exited with ${r.status ?? 'a signal'}`
137
+ return null
138
+ }
@@ -4,7 +4,11 @@
4
4
  // - alt-screen: the dashboard is a full-screen live surface; it renders on the
5
5
  // alternate screen so quitting restores the operator's scrollback untouched
6
6
  // (Apple Terminal keeps no E3, so repeated in-scrollback repaints would pile up —
7
- // the alt-screen sidesteps that class entirely).
7
+ // the alt-screen sidesteps that class entirely). The alt-screen is owned by INK
8
+ // (`alternateScreen: true`), NOT written manually: Ink's resize/erase heuristics
9
+ // (clear-on-width-shrink, fullscreen-frame clears) key on the buffer IT manages —
10
+ // a manual \x1b[?1049h behind Ink's back left it erasing by stale line-counts, so
11
+ // a window resize stacked old frames (live incident: the footer repeated ~5×).
8
12
  // - интерактив только в реальном TTY (контракт фазы): stdin+stdout must both be
9
13
  // TTYs, else the caller falls back to the scriptable table (never render + hang;
10
14
  // the same fail-closed belt as the onboard wizard).
@@ -12,7 +16,10 @@
12
16
  // dashboard fully unmounts + leaves the alt-screen + returns the terminal to
13
17
  // cooked mode, then spawns `iapeer attach <peer>` as a CHILD with inherited stdio
14
18
  // (the runtime TUI gets the REAL tty — the readline-class constraint), waits, and
15
- // remounts. One full-screen TUI owns the terminal at any moment.
19
+ // remounts. One full-screen TUI owns the terminal at any moment. A child that
20
+ // FAILS (spawn error OR exit≠0) is surfaced and acknowledged — never silently
21
+ // remounted over (live incident: Enter looked dead because the attach child died
22
+ // instantly and the remount swallowed its error).
16
23
  //
17
24
  // Self-invocation uses the same `$bunfs` discriminator as supervisor's
18
25
  // resolveDaemonSelfArgv: compiled standalone → re-invoke the binary by verb; running
@@ -23,13 +30,11 @@ import { render } from 'ink'
23
30
  import { spawnSync } from 'node:child_process'
24
31
  import { fileURLToPath } from 'node:url'
25
32
  import { DashboardApp, type DashAction } from './app.tsx'
33
+ import { attachFailureMessage } from './model.ts'
26
34
 
27
35
  /** Sentinel: no real interactive TTY — the caller should print the scriptable table. */
28
36
  export const DASHBOARD_NOT_INTERACTIVE = -1
29
37
 
30
- const ALT_SCREEN_ON = '\x1b[?1049h\x1b[H'
31
- const ALT_SCREEN_OFF = '\x1b[?1049l'
32
-
33
38
  /** argv prefix that re-invokes THIS package's CLI (compiled binary by verb / src via
34
39
  * bun). Same discriminator as supervisor/index.ts resolveDaemonSelfArgv: `$bunfs` in
35
40
  * the module path marks the compiled standalone (existsSync is NO discriminator —
@@ -49,17 +54,16 @@ export interface DashboardOptions {
49
54
  /** Mount the dashboard once; resolve with the action that ended it. */
50
55
  async function mountOnce(env: NodeJS.ProcessEnv): Promise<DashAction> {
51
56
  let action: DashAction = { type: 'quit' }
52
- process.stdout.write(ALT_SCREEN_ON)
53
- try {
54
- const app = render(<DashboardApp env={env} onAction={a => (action = a)} />, {
55
- // the dashboard repaints in place on the alt screen; Ctrl-C is handled by the
56
- // app itself (quit action) so a half-torn frame never leaks to the main screen
57
- exitOnCtrlC: false,
58
- })
59
- await app.waitUntilExit()
60
- } finally {
61
- process.stdout.write(ALT_SCREEN_OFF)
62
- }
57
+ const app = render(<DashboardApp env={env} onAction={a => (action = a)} />, {
58
+ // the dashboard repaints in place on the alt screen; Ctrl-C is handled by the
59
+ // app itself (quit action) so a half-torn frame never leaks to the main screen
60
+ exitOnCtrlC: false,
61
+ // Ink OWNS the alt-screen (enter on mount, exit+cursor-restore on unmount —
62
+ // BEFORE waitUntilExit resolves, so the attach child always gets the primary
63
+ // buffer). Manual 1049-writes behind Ink's back broke its resize erase model.
64
+ alternateScreen: true,
65
+ })
66
+ await app.waitUntilExit()
63
67
  return action
64
68
  }
65
69
 
@@ -87,8 +91,12 @@ export async function runDashboard(opts: DashboardOptions = {}): Promise<number>
87
91
  stdio: 'inherit',
88
92
  env,
89
93
  })
90
- if (r.error) {
91
- process.stdout.write(`attach failed: ${r.error.message}\npress Enter to continue…`)
94
+ // Surface BOTH failure shapes before remounting: a spawn error AND a child that ran but
95
+ // FAILED (exit≠0 / signal — its own stderr is already on the primary screen via inherit).
96
+ // Remounting silently over either reads as "Enter does nothing" (live incident).
97
+ const failure = attachFailureMessage(r)
98
+ if (failure) {
99
+ process.stdout.write(`attach ${action.personality} failed: ${failure}\npress Enter to continue…`)
92
100
  // swallow one line so the operator sees the error before the alt-screen returns
93
101
  spawnSync('sh', ['-c', 'read _'], { stdio: 'inherit' })
94
102
  }