koneck 2.69.0 → 2.71.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/dist/ink-chat.js CHANGED
@@ -1004,7 +1004,15 @@ export function defaultAltScreen(stdout = process.stdout, env = process.env) {
1004
1004
  return false;
1005
1005
  return true;
1006
1006
  }
1007
- function App({ config: initialConfig, clearFrame }) {
1007
+ /**
1008
+ * Exported for one reason: so a test can mount it.
1009
+ *
1010
+ * The CLI shipped unable to start — an expression in a useState initialiser reached for a helper
1011
+ * declared further down the component, and every release after that crashed on the first render.
1012
+ * Typechecking passes on that code and no test could reach it, because nothing in the suite mounted
1013
+ * an Ink app. Something does now.
1014
+ */
1015
+ export function App({ config: initialConfig, clearFrame }) {
1008
1016
  const { exit } = useApp();
1009
1017
  const [cfg, setCfg] = useState(initialConfig);
1010
1018
  // Tool approval callbacks belong to a session created on an earlier render. Keep the control
@@ -1330,9 +1338,22 @@ function App({ config: initialConfig, clearFrame }) {
1330
1338
  realTokRef.current = Math.max(0, s.completionTokens - tokBaseRef.current);
1331
1339
  },
1332
1340
  };
1333
- const activeSession = useState(() => ({
1334
- p: createAgentSession({ ...initialConfig, pace: paceFrom(initialConfig.pace ?? savedEffort(undefined), 'medium'), ...failoverHooks(), silent: true, ...sessionOpts }),
1335
- }))[0];
1341
+ /**
1342
+ * The live session, built on first use rather than while the component renders.
1343
+ *
1344
+ * It used to be created in this useState's initialiser, which runs during the render — and the
1345
+ * hooks it is built with close over things declared further down the component, so the very first
1346
+ * render threw "Cannot access 'failoverHooks' before initialization" and the CLI would not start
1347
+ * at all. The initialiser had been fine until those hooks existed; adding them moved a working
1348
+ * expression above its own dependencies, and nothing caught it because the test suite has no way
1349
+ * to mount an Ink app. There is one now.
1350
+ *
1351
+ * Building it on first use fixes the ordering by construction: every caller runs from an effect or
1352
+ * a callback, which is after the whole body has been evaluated. The mount effect that sets the
1353
+ * pace asks for it immediately, so it is still created when the app starts, not lazily on the
1354
+ * first message.
1355
+ */
1356
+ const activeSession = useState(() => ({ p: null }))[0];
1336
1357
  const [rows, setRows] = useState([{ role: 'header' }]);
1337
1358
  const [draft, setDraft] = useState('');
1338
1359
  // Recall is deliberately session-local. It is for quick retries and refinements, not another
@@ -1601,12 +1622,46 @@ function App({ config: initialConfig, clearFrame }) {
1601
1622
  // What this turn leaves behind, said where the rest of the session's own account of itself is.
1602
1623
  onHandoff: (said) => addSystem(said),
1603
1624
  });
1625
+ /**
1626
+ * The session, creating it if this is the first ask.
1627
+ *
1628
+ * Deliberately next to failoverHooks, because the two are why it cannot be built during the
1629
+ * render: these hooks reach for state declared below them, so the session can only be assembled
1630
+ * once the whole component body has run.
1631
+ */
1632
+ const sessionNow = () => {
1633
+ if (!activeSession.p) {
1634
+ activeSession.p = hold(createAgentSession({
1635
+ ...initialConfig,
1636
+ pace: paceFrom(initialConfig.pace ?? savedEffort(undefined), 'medium'),
1637
+ ...failoverHooks(), silent: true, ...sessionOpts,
1638
+ }));
1639
+ }
1640
+ return activeSession.p;
1641
+ };
1642
+ /**
1643
+ * Parks a session promise so a failure cannot take the process down before anyone asks.
1644
+ *
1645
+ * A session is created and stored, then read later by whoever needs it. If creating it fails — an
1646
+ * endpoint that is not listening is the ordinary case — the stored promise is rejected with nobody
1647
+ * yet attached, and on Node 24 an unhandled rejection ends the process. So a no-op handler is
1648
+ * attached the moment it is stored. The promise handed out is the original, so every caller still
1649
+ * sees the failure and reports it in its own way; this only stops the copy sitting in a variable
1650
+ * from being counted as ignored.
1651
+ *
1652
+ * Found by the test that mounts the app: it points at a stub that answers nothing useful, and six
1653
+ * unhandled rejections came out of a suite that had been green.
1654
+ */
1655
+ function hold(p) {
1656
+ p.catch(() => { });
1657
+ return p;
1658
+ }
1604
1659
  const effortRef = useRef('medium');
1605
1660
  useEffect(() => {
1606
1661
  effortRef.current = effort;
1607
1662
  void (async () => {
1608
1663
  try {
1609
- const live = await activeSession.p;
1664
+ const live = await sessionNow();
1610
1665
  live.setPace?.(paceFrom(effort, 'medium'));
1611
1666
  }
1612
1667
  catch { /* no session yet, and the next one is built with this pace */ }
@@ -1962,13 +2017,13 @@ function App({ config: initialConfig, clearFrame }) {
1962
2017
  submitDraft(task);
1963
2018
  }
1964
2019
  async function getSession() {
1965
- return activeSession.p;
2020
+ return sessionNow();
1966
2021
  }
1967
2022
  async function resetSession(newCfg) {
1968
2023
  const c = newCfg ?? cfg;
1969
2024
  cfgRef.current = c;
1970
2025
  setDetectedContext(null);
1971
- activeSession.p = createAgentSession({ ...c, pace: paceFrom(effortRef.current, 'medium'), ...failoverHooks(), silent: true, ...sessionOpts });
2026
+ activeSession.p = hold(createAgentSession({ ...c, pace: paceFrom(effortRef.current, 'medium'), ...failoverHooks(), silent: true, ...sessionOpts }));
1972
2027
  if (newCfg)
1973
2028
  setCfg(newCfg);
1974
2029
  }
@@ -2001,7 +2056,7 @@ function App({ config: initialConfig, clearFrame }) {
2001
2056
  setRepaint(n => n + 1);
2002
2057
  }
2003
2058
  async function doSave() {
2004
- const s = await activeSession.p.catch(() => null);
2059
+ const s = await sessionNow().catch(() => null);
2005
2060
  const firstUser = s?.messages.find((m) => m.role === 'user');
2006
2061
  // Masked before it is written. A key pasted into the composer became the first user message,
2007
2062
  // which is where this title comes from — so it was persisted to disk and then printed in the
@@ -2124,7 +2179,7 @@ function App({ config: initialConfig, clearFrame }) {
2124
2179
  ...(st.note ? { note: st.note } : {}),
2125
2180
  })), updatedAt: Date.now() }
2126
2181
  : undefined;
2127
- activeSession.p = createAgentSession({ ...restoredCfg, silent: true, ...sessionOpts }, messages, { turns: meta.turns, totalTokens: meta.totalTokens }, restoredPlan);
2182
+ activeSession.p = hold(createAgentSession({ ...restoredCfg, silent: true, ...sessionOpts }, messages, { turns: meta.turns, totalTokens: meta.totalTokens }, restoredPlan));
2128
2183
  if (restoredPlan)
2129
2184
  setPlan(restoredPlan);
2130
2185
  setSessionGoal(meta.goal ?? null);
@@ -2399,7 +2454,7 @@ function App({ config: initialConfig, clearFrame }) {
2399
2454
  return;
2400
2455
  }
2401
2456
  if (signalsRef.current === null) {
2402
- const live = await activeSession.p.catch(() => null);
2457
+ const live = await sessionNow().catch(() => null);
2403
2458
  signalsRef.current = await gatherSignals(cfg.cwd, live?.trajectory ?? [])
2404
2459
  .catch(() => ({}));
2405
2460
  }
@@ -2721,7 +2776,7 @@ function App({ config: initialConfig, clearFrame }) {
2721
2776
  }
2722
2777
  case '/mcp': {
2723
2778
  const configured = await loadMCPConfig(cfg.cwd);
2724
- const session = await activeSession.p.catch(() => null);
2779
+ const session = await sessionNow().catch(() => null);
2725
2780
  addSystem(mcpStatusText(Object.keys(configured?.servers ?? {}), session?.mcpServers ?? []));
2726
2781
  return;
2727
2782
  }
@@ -3130,7 +3185,7 @@ function App({ config: initialConfig, clearFrame }) {
3130
3185
  case '/trajectory': {
3131
3186
  // The timings the transcript cannot answer for: where the time went, which tool was slow,
3132
3187
  // how much of a long turn was spent waiting for a first token rather than generating.
3133
- const s = await activeSession.p.catch(() => null);
3188
+ const s = await sessionNow().catch(() => null);
3134
3189
  addSystem(renderTrajectory(s?.trajectory ?? []));
3135
3190
  return;
3136
3191
  }
@@ -3398,7 +3453,7 @@ function App({ config: initialConfig, clearFrame }) {
3398
3453
  return;
3399
3454
  }
3400
3455
  case '/rewind': {
3401
- const s = await activeSession.p.catch(() => null);
3456
+ const s = await sessionNow().catch(() => null);
3402
3457
  if (!s) {
3403
3458
  addSystem('Nothing to rewind.');
3404
3459
  return;