claudish 14.11.0 → 14.12.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.
Files changed (2) hide show
  1. package/dist/index.js +246 -17
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -5491,7 +5491,7 @@ var init_onepassword_config = __esm(() => {
5491
5491
  });
5492
5492
 
5493
5493
  // src/version.ts
5494
- var VERSION = "14.11.0";
5494
+ var VERSION = "14.12.0";
5495
5495
 
5496
5496
  // src/redact.ts
5497
5497
  function isPlaceholder(value) {
@@ -73048,7 +73048,8 @@ var init_contract = __esm(() => {
73048
73048
  "capture_since_seq",
73049
73049
  "capture_spans",
73050
73050
  "resize",
73051
- "allowance"
73051
+ "allowance",
73052
+ "scroll"
73052
73053
  ];
73053
73054
  SLOT_STATES = [
73054
73055
  "STARTING",
@@ -93473,6 +93474,39 @@ var init_screen_classifier = __esm(() => {
93473
93474
  NON_ANSWER_ROW = /^\s+\u23BF|^\s{2}(Ran|Read|Searched|Wrote|Edited|Listed|Updated) \d+ |^\s*[\u00B7\u2722\u2733\u2736\u273B\u273D*\u25CF\u2217]\s|^\s*\(ctrl\+/;
93474
93475
  });
93475
93476
 
93477
+ // src/pane/scroll-input.ts
93478
+ function isLiveBottom(pos) {
93479
+ return pos.atBottom && pos.lastDirection === null;
93480
+ }
93481
+ function scrollRowsOf(by) {
93482
+ if (!Number.isFinite(by))
93483
+ return 0;
93484
+ return Math.min(Math.abs(Math.trunc(by)), MAX_SCROLL_ROWS);
93485
+ }
93486
+ function planScroll(pos, req) {
93487
+ if ("to" in req)
93488
+ return { bytes: CTRL_END, events: 1, next: LIVE_BOTTOM };
93489
+ const n = scrollRowsOf(req.by);
93490
+ if (n === 0)
93491
+ return { bytes: "", events: 0, next: pos };
93492
+ const dir = req.by < 0 ? "up" : "down";
93493
+ const extra = pos.atBottom || pos.lastDirection !== dir ? 1 : 0;
93494
+ const events = n + extra;
93495
+ return {
93496
+ bytes: (dir === "up" ? WHEEL_UP : WHEEL_DOWN).repeat(events),
93497
+ events,
93498
+ next: { atBottom: false, lastDirection: dir }
93499
+ };
93500
+ }
93501
+ var MAX_SCROLL_ROWS = 200, WHEEL_UP = "\x1B[<64;1;1M", WHEEL_DOWN = "\x1B[<65;1;1M", CTRL_END = "\x1B[1;5F", LIVE_BOTTOM, UNKNOWN_POSITION;
93502
+ var init_scroll_input = __esm(() => {
93503
+ LIVE_BOTTOM = Object.freeze({ atBottom: true, lastDirection: null });
93504
+ UNKNOWN_POSITION = Object.freeze({
93505
+ atBottom: false,
93506
+ lastDirection: null
93507
+ });
93508
+ });
93509
+
93476
93510
  // src/pane/session-registry.ts
93477
93511
  import { readFileSync as readFileSync40, readdirSync as readdirSync6 } from "fs";
93478
93512
  import { join as join48 } from "path";
@@ -94617,6 +94651,9 @@ class PaneSessionImpl {
94617
94651
  resizeQuietFrom = 0;
94618
94652
  resizeQuietUntil = 0;
94619
94653
  activityChangedAt = 0;
94654
+ scrollPos = LIVE_BOTTOM;
94655
+ scrollEndsInFlight = 0;
94656
+ scrollEndConfirmedAt = 0;
94620
94657
  gate = null;
94621
94658
  gateTrusted = false;
94622
94659
  gateInFlight = false;
@@ -95407,8 +95444,8 @@ ${gate}`
95407
95444
  await this.pause(adm, TICK_MS);
95408
95445
  continue;
95409
95446
  }
95410
- if (!this.client || !this.gateTrusted) {
95411
- await this.awaitTrustedClient(adm);
95447
+ if (!this.trustedConnection() || this.needsAdmissionLiveBottom(adm)) {
95448
+ await this.connectionStep(adm);
95412
95449
  continue;
95413
95450
  }
95414
95451
  if (await this.beforeSend(adm) !== "send")
@@ -95420,8 +95457,22 @@ ${gate}`
95420
95457
  adm.inFlight = false;
95421
95458
  }
95422
95459
  }
95460
+ needsAdmissionLiveBottom(adm) {
95461
+ if (adm.liveBottomClient === this.client)
95462
+ return false;
95463
+ adm.liveBottomClient = this.client;
95464
+ return !isLiveBottom(this.scrollPos);
95465
+ }
95466
+ trustedConnection() {
95467
+ return !!this.client && !this.client.isClosed && this.gateTrusted;
95468
+ }
95469
+ connectionStep(adm) {
95470
+ if (!this.trustedConnection())
95471
+ return this.awaitTrustedClient(adm);
95472
+ return this.returnToLiveBottom(Math.max(0, adm.deadline - Date.now()));
95473
+ }
95423
95474
  awaitTrustedClient(adm) {
95424
- return this.waitFor(() => !this.admitting(adm) || !!this.client && this.gateTrusted, Math.max(0, adm.deadline - Date.now()));
95475
+ return this.waitFor(() => !this.admitting(adm) || this.trustedConnection(), Math.max(0, adm.deadline - Date.now()));
95425
95476
  }
95426
95477
  async attemptOnce(adm) {
95427
95478
  const c = this.client;
@@ -95846,6 +95897,7 @@ ${screenTailOf(this.screen)}` });
95846
95897
  accept() {
95847
95898
  this.admission = null;
95848
95899
  this.initialDelivery = false;
95900
+ this.scrollPos = LIVE_BOTTOM;
95849
95901
  const now = Date.now();
95850
95902
  this.lastChatSeenAt = now;
95851
95903
  this.turnAcceptedAt = now;
@@ -95922,6 +95974,7 @@ ${screenTailOf(this.screen)}`
95922
95974
  const d = this.currentDelivery;
95923
95975
  this.turnSource = "screen";
95924
95976
  this.anomaly("degraded_mode");
95977
+ this.returnToLiveBottom(SCROLL_REPLY_MS);
95925
95978
  this.screenTurn = {
95926
95979
  echo: d && d.mode !== "refused" ? d.line : "",
95927
95980
  acceptedAt: at,
@@ -96399,11 +96452,21 @@ ${screenTailOf(this.screen)}`
96399
96452
  const g = this.gateNow();
96400
96453
  if (!g || !g.ready || g.dialog !== null || workingAboveBox(this.screen) === true)
96401
96454
  return false;
96402
- const quiet = this.aboveBoxQuietMs() ?? Date.now() - this.screen.changedAt;
96455
+ let quiet = this.aboveBoxQuietMs() ?? Date.now() - this.screen.changedAt;
96456
+ if (this.scrollEndConfirmedAt > 0)
96457
+ quiet = Math.min(quiet, Date.now() - this.scrollEndConfirmedAt);
96403
96458
  if (view.availability === "off" && quiet >= this.secondaryQuietMs)
96404
96459
  return true;
96405
96460
  return (!!answer || !!apiError) && quiet >= 2 * this.t("screenSettleQuietMs", SCREEN_SETTLE_QUIET_MS);
96406
96461
  }
96462
+ screenViewLive() {
96463
+ if (this.scrollEndsInFlight > 0)
96464
+ return false;
96465
+ if (isLiveBottom(this.scrollPos))
96466
+ return true;
96467
+ this.returnToLiveBottom(SCROLL_REPLY_MS);
96468
+ return false;
96469
+ }
96407
96470
  evalScreenTurn(view) {
96408
96471
  if (this.witnessed(view.current)) {
96409
96472
  this.turnSource = "transcript";
@@ -96418,7 +96481,7 @@ ${screenTailOf(this.screen)}`
96418
96481
  const answer = screenAnswer(this.screen, st.echo);
96419
96482
  const apiError = this.screenApiError(st, answer);
96420
96483
  this.activity = "thinking";
96421
- if (!this.screenSettleReady(view, answer, apiError))
96484
+ if (!this.screenViewLive() || !this.screenSettleReady(view, answer, apiError))
96422
96485
  return;
96423
96486
  this.follower.poll();
96424
96487
  if (this.witnessed(this.follower.view().current))
@@ -96749,6 +96812,69 @@ ${screenTailOf(this.screen)}`
96749
96812
  answer({ ...set, changed: true, seq: this.screen.seq });
96750
96813
  }
96751
96814
  }
96815
+ async scroll(req) {
96816
+ const refusal = this.scrollRefusal();
96817
+ if (refusal)
96818
+ throw new ContractErrorException("scroll_unavailable", refusal);
96819
+ const c = this.client;
96820
+ if (!c || c.isClosed)
96821
+ throw new ContractErrorException("scroll_unavailable", "the pane's socket is reconnecting");
96822
+ const plan = planScroll(this.scrollPos, req);
96823
+ if (plan.bytes === "")
96824
+ return { seq: this.screen.seq };
96825
+ this.scrollPos = plan.next;
96826
+ const seq0 = this.screen.seq;
96827
+ this.extendResizeQuiet();
96828
+ const r = await c.request({ type: "send", pane: 0, text: plan.bytes, enter: false, noGate: true }, SCROLL_REPLY_MS);
96829
+ if (!r.ok) {
96830
+ this.scrollPos = UNKNOWN_POSITION;
96831
+ if (r.code === "client_lost" || r.code === "client_timeout")
96832
+ this.extendResizeQuiet();
96833
+ throw new ContractErrorException("scroll_unavailable", `magmux did not confirm the scroll: ${r.code}${r.error ? `: ${r.error}` : ""}`);
96834
+ }
96835
+ this.extendResizeQuiet();
96836
+ await this.waitFor(() => isTerminalPhase(this.phase) || this.screen.seq !== seq0, this.t("scrollScreenMs", SCROLL_SCREEN_MS));
96837
+ return { seq: this.screen.seq };
96838
+ }
96839
+ screenTurnActive() {
96840
+ if (this.turnSource !== "screen")
96841
+ return false;
96842
+ return this.phase === "RUNNING" || this.phase === "QUESTION" || this.phase === "PERMISSION";
96843
+ }
96844
+ scrollRefusal() {
96845
+ if (isTerminalPhase(this.phase) || this.final)
96846
+ return "the session has finished";
96847
+ if (this.phase === "BOOTING" || this.screen.seq === 0)
96848
+ return "the session is still starting";
96849
+ if (this.phase === "ADMITTING")
96850
+ return "a prompt is being delivered to the session";
96851
+ if (this.screenTurnActive())
96852
+ return "the session's turn is being read from its screen (no transcript)";
96853
+ if (!this.client || this.client.isClosed || !this.gateTrusted)
96854
+ return "the pane's socket is reconnecting";
96855
+ return null;
96856
+ }
96857
+ async returnToLiveBottom(ms) {
96858
+ if (isLiveBottom(this.scrollPos))
96859
+ return;
96860
+ const c = this.client;
96861
+ if (!c || c.isClosed || !this.gateTrusted || ms <= 0)
96862
+ return;
96863
+ this.scrollPos = LIVE_BOTTOM;
96864
+ this.extendResizeQuiet();
96865
+ this.scrollEndsInFlight++;
96866
+ try {
96867
+ const r = await c.request({ type: "send", pane: 0, text: CTRL_END, enter: false, noGate: true }, Math.min(SCROLL_REPLY_MS, ms));
96868
+ if (r.ok) {
96869
+ this.scrollEndConfirmedAt = Date.now();
96870
+ } else {
96871
+ this.scrollPos = UNKNOWN_POSITION;
96872
+ this.anomaly(`scroll_end_unconfirmed:${r.code}`);
96873
+ }
96874
+ } finally {
96875
+ this.scrollEndsInFlight--;
96876
+ }
96877
+ }
96752
96878
  turnAnswer(index) {
96753
96879
  const done = this.answers.get(index);
96754
96880
  if (done !== undefined)
@@ -97001,7 +97127,8 @@ function newAdmission(a) {
97001
97127
  clearAfter: 0,
97002
97128
  lostType: null,
97003
97129
  last: null,
97004
- noted: new Set
97130
+ noted: new Set,
97131
+ liveBottomClient: null
97005
97132
  };
97006
97133
  }
97007
97134
  function markClearFirst(adm) {
@@ -97168,7 +97295,7 @@ async function startPaneSession(o) {
97168
97295
  await session.start({ magmuxBinary: magmux.binary, env: p.magmuxEnv, cwd: p.cwd });
97169
97296
  return session;
97170
97297
  }
97171
- var BOOT_TIMEOUT_MS = 90000, ADMIT_TIMEOUT_MS = 30000, DEGRADED_ENTRY_MS = 1e4, SCREEN_SETTLE_QUIET_MS = 3000, PANEL_SETTLE_MS = 500, SEND_CLIENT_SLACK_MS = 2000, MIN_READY_TIMEOUT_MS = 1000, BUSY_RETRY_MS = 250, MISMATCH_SETTLE_MS = 1000, CLEAR_WAIT_MS = 1000, BOOT_STATIC_MS = 5000, TICK_MS = 250, WATCH_FPS = 4, POLL_BACKSTOP_MS = 1000, TURN_END_MISSING_FACTOR = 3, RESIZE_REPLY_MS = 1e4, RESIZE_SCREEN_MS = 1000, GATE_POLL_MS = 250, GATE_REPLY_MS = 1000, GATE_STALE_MS2 = 1000, GATE_GRACE_MS, GATE_DEAD_BACKSTOP_MS = 1000, EXIT_CODE_QUERY_MS = 2000, EXIT_CODE_RETRY_MS = 50, GATE_KINDLESS_REASONS, RUNTIME_DIALOG_KINDS, REGISTRY_READ_MS = 250, REGISTRY_DISAGREE_MS = 2000, REGISTRY_GATE_BOOT_DISAGREE_MS = 5000, REGISTRY_MISSING_MS = 5000, MAX_ANOMALY_KEYS = 64, KEPT_ANSWERS = 4, DEFAULT_MAX_CONSECUTIVE = 3, EVENT_REASON, VERDICT_EVENT, PaneStartError;
97298
+ var BOOT_TIMEOUT_MS = 90000, ADMIT_TIMEOUT_MS = 30000, DEGRADED_ENTRY_MS = 1e4, SCREEN_SETTLE_QUIET_MS = 3000, PANEL_SETTLE_MS = 500, SEND_CLIENT_SLACK_MS = 2000, MIN_READY_TIMEOUT_MS = 1000, BUSY_RETRY_MS = 250, MISMATCH_SETTLE_MS = 1000, CLEAR_WAIT_MS = 1000, BOOT_STATIC_MS = 5000, TICK_MS = 250, WATCH_FPS = 4, POLL_BACKSTOP_MS = 1000, TURN_END_MISSING_FACTOR = 3, RESIZE_REPLY_MS = 1e4, RESIZE_SCREEN_MS = 1000, SCROLL_REPLY_MS = 5000, SCROLL_SCREEN_MS = 1000, GATE_POLL_MS = 250, GATE_REPLY_MS = 1000, GATE_STALE_MS2 = 1000, GATE_GRACE_MS, GATE_DEAD_BACKSTOP_MS = 1000, EXIT_CODE_QUERY_MS = 2000, EXIT_CODE_RETRY_MS = 50, GATE_KINDLESS_REASONS, RUNTIME_DIALOG_KINDS, REGISTRY_READ_MS = 250, REGISTRY_DISAGREE_MS = 2000, REGISTRY_GATE_BOOT_DISAGREE_MS = 5000, REGISTRY_MISSING_MS = 5000, MAX_ANOMALY_KEYS = 64, KEPT_ANSWERS = 4, DEFAULT_MAX_CONSECUTIVE = 3, EVENT_REASON, VERDICT_EVENT, PaneStartError;
97172
97299
  var init_pane_session = __esm(() => {
97173
97300
  init_queue();
97174
97301
  init_parent_session();
@@ -97185,6 +97312,7 @@ var init_pane_session = __esm(() => {
97185
97312
  init_prompt_delivery();
97186
97313
  init_screen_classifier();
97187
97314
  init_screen_model();
97315
+ init_scroll_input();
97188
97316
  init_session_registry();
97189
97317
  init_settle_rule();
97190
97318
  init_slot_state();
@@ -97239,6 +97367,7 @@ var init_pane = __esm(() => {
97239
97367
  init_pane_launch();
97240
97368
  init_pane_registry();
97241
97369
  init_pane_session();
97370
+ init_scroll_input();
97242
97371
  init_contract();
97243
97372
  });
97244
97373
 
@@ -97967,6 +98096,20 @@ async function resizeTeamSlot(path, slot, cols, rows, runId) {
97967
98096
  return { ...where, cols: DEFAULT_PANE_COLS, rows: DEFAULT_PANE_ROWS, changed: false, seq: 0 };
97968
98097
  return { ...where, ...await e.session.resize(cols, rows) };
97969
98098
  }
98099
+ async function scrollTeamSlot(path, slot, req, runId) {
98100
+ const run = addressRun(path, runId);
98101
+ if (!run)
98102
+ throw unknownRun(path, runId);
98103
+ const e = run.slots.get(slot);
98104
+ if (!e)
98105
+ throw new ContractErrorException("unknown_slot", `run ${run.runId} has no slot ${JSON.stringify(slot)}`);
98106
+ if (!e.session) {
98107
+ const state = run.status.models[e.id]?.state;
98108
+ throw new ContractErrorException("scroll_unavailable", state === undefined || isTerminalState(state) ? "the slot has finished" : "the slot is still starting");
98109
+ }
98110
+ const r = await e.session.scroll(req);
98111
+ return { run_id: run.runId, path: run.path, slot, seq: r.seq };
98112
+ }
97970
98113
  function teamLiveMaps(path, runId) {
97971
98114
  const run = addressRun(path, runId);
97972
98115
  if (!run)
@@ -100007,6 +100150,15 @@ class SessionManager {
100007
100150
  const r = await entry.session.resize(cols, rows);
100008
100151
  return { session_id: sessionId, ...r };
100009
100152
  }
100153
+ async scrollSession(sessionId, req) {
100154
+ const entry = this.liveEntry(sessionId);
100155
+ if (!entry)
100156
+ throw unknownSession(sessionId);
100157
+ if (!entry.session)
100158
+ throw new ContractErrorException("scroll_unavailable", isTerminalState(entry.info.state) ? "the session has finished" : "the session is still starting");
100159
+ const r = await entry.session.scroll(req);
100160
+ return { session_id: sessionId, seq: r.seq };
100161
+ }
100010
100162
  captureSession(sessionId, sinceSeq, spans) {
100011
100163
  const entry = this.liveEntry(sessionId);
100012
100164
  if (!entry)
@@ -100442,7 +100594,7 @@ var NESTED_MCP_TOOL_NAMES, NESTED_INSTRUCTIONS = `Claudish MCP server provides a
100442
100594
 
100443
100595
  ## Nested mode \u2014 no sessions
100444
100596
 
100445
- This server runs inside a session that a claudish MCP server started (CLAUDISH_PANE_CHILD=1). One level of nesting is the limit, so these tools are not registered: create_session, team, send_input, get_output, cancel_session, list_sessions, get_diagnostics, capture_session, preflight, report_error. No channel notifications are sent.
100597
+ This server runs inside a session that a claudish MCP server started (CLAUDISH_PANE_CHILD=1). One level of nesting is the limit, so these tools are not registered: create_session, team, send_input, get_output, cancel_session, list_sessions, get_diagnostics, capture_session, resize_session, scroll_session, preflight, report_error. No channel notifications are sent.
100446
100598
 
100447
100599
  - run_prompt or compare_models: a one-shot answer from another model.
100448
100600
  - list_models, search_models: find a model.
@@ -101539,6 +101691,22 @@ function requiredInteger(raw, name, mode) {
101539
101691
  throw new ContractErrorException("invalid_args", `'${name}' is required${mode ? ` for mode '${mode}'` : ""}`);
101540
101692
  return n;
101541
101693
  }
101694
+ function scrollArgs(args) {
101695
+ const by = optionalInteger(args.by, "by");
101696
+ const to = args.to;
101697
+ if (to !== undefined && to !== null && to !== "end")
101698
+ throw new ContractErrorException("invalid_args", `'to' must be "end"`);
101699
+ const hasTo = to === "end";
101700
+ if (by !== undefined && hasTo)
101701
+ throw new ContractErrorException("invalid_args", "give exactly one of 'by' and 'to', not both");
101702
+ if (hasTo)
101703
+ return { to: "end" };
101704
+ if (by === undefined)
101705
+ throw new ContractErrorException("invalid_args", "one of 'by' and 'to' is required");
101706
+ if (by === 0)
101707
+ throw new ContractErrorException("invalid_args", "'by' must not be 0");
101708
+ return { by };
101709
+ }
101542
101710
  function teamStatusAnswer(path, runId) {
101543
101711
  let row = null;
101544
101712
  if (runId) {
@@ -101583,6 +101751,7 @@ async function teamContractVerb(mode, args) {
101583
101751
  const spans = optionalBoolean(args.spans, "spans");
101584
101752
  const cols = mode === "resize" ? requiredInteger(args.cols, "cols", mode) : undefined;
101585
101753
  const rows = mode === "resize" ? requiredInteger(args.rows, "rows", mode) : undefined;
101754
+ const scroll = mode === "scroll" ? scrollArgs(args) : undefined;
101586
101755
  switch (mode) {
101587
101756
  case "status":
101588
101757
  return contractAnswer(teamStatusAnswer(path, runId));
@@ -101598,6 +101767,11 @@ async function teamContractVerb(mode, args) {
101598
101767
  throw new ContractErrorException("invalid_args", "'slot' is required for mode 'resize'");
101599
101768
  return contractAnswer(await resizeTeamSlot(path, slot, cols, rows, runId));
101600
101769
  }
101770
+ case "scroll": {
101771
+ if (slot === undefined)
101772
+ throw new ContractErrorException("invalid_args", "'slot' is required for mode 'scroll'");
101773
+ return contractAnswer(await scrollTeamSlot(path, slot, scroll, runId));
101774
+ }
101601
101775
  }
101602
101776
  } catch (e) {
101603
101777
  return contractErrorAnswer(e);
@@ -101945,18 +102119,28 @@ This searched OpenRouter's listing only. Subscription wire ids and catalog alias
101945
102119
  });
101946
102120
  tools.push({
101947
102121
  name: "team",
101948
- description: "Run AI models on a task with anonymized outputs and optional blind judging. Every slot is an interactive Claude Code session in its own headless magmux pane. " + "Modes: 'run' (START the models and return once each has taken its prompt \u2014 it does " + "NOT wait for answers), 'status' (per-slot state; read `run.slots`), 'list' (every run this server holds), 'capture' (one slot's screen), 'resize' (set one slot's terminal size), 'cancel' (stop one slot or the whole run), 'judge'(blind-vote on existing outputs), 'run-and-judge' (the blocking pipeline). Poll 'status' (read run.slots) or 'list'. NO SLOT IS EVER KILLED ON A TIMER once its prompt is accepted. A team slot is a full Claude Code session and may work for a long time; a slot inside a build or test suite emits nothing for minutes and is working, not stuck. Judge the silence against the task you set, and use 'cancel' if you decide a slot is wedged. A slot that stops on a question it cannot answer is FAILED 'blocked' (team has no way to answer it). JUDGING A WORKING SLOT: read live_output_bytes_by_slot, idle_seconds_by_slot and activity_by_slot (or each row's idle_seconds and " + "activity). Do NOT use 'outputSize' \u2014 it is the size of the final answer, written " + "only when a slot finishes, so it is 0 for every working slot no matter how much that slot has produced.",
102122
+ description: "Run AI models on a task with anonymized outputs and optional blind judging. Every slot is an interactive Claude Code session in its own headless magmux pane. " + "Modes: 'run' (START the models and return once each has taken its prompt \u2014 it does " + "NOT wait for answers), 'status' (per-slot state; read `run.slots`), 'list' (every run this server holds), 'capture' (one slot's screen), 'resize' (set one slot's terminal size), 'scroll' (scroll one slot's transcript view), 'cancel' (stop one slot or the whole run), 'judge'(blind-vote on existing outputs), 'run-and-judge' (the blocking pipeline). Poll 'status' (read run.slots) or 'list'. NO SLOT IS EVER KILLED ON A TIMER once its prompt is accepted. A team slot is a full Claude Code session and may work for a long time; a slot inside a build or test suite emits nothing for minutes and is working, not stuck. Judge the silence against the task you set, and use 'cancel' if you decide a slot is wedged. A slot that stops on a question it cannot answer is FAILED 'blocked' (team has no way to answer it). JUDGING A WORKING SLOT: read live_output_bytes_by_slot, idle_seconds_by_slot and activity_by_slot (or each row's idle_seconds and " + "activity). Do NOT use 'outputSize' \u2014 it is the size of the final answer, written " + "only when a slot finishes, so it is 0 for every working slot no matter how much that slot has produced.",
101949
102123
  inputSchema: {
101950
102124
  type: "object",
101951
102125
  properties: {
101952
102126
  mode: {
101953
102127
  type: "string",
101954
- enum: ["run", "judge", "run-and-judge", "status", "cancel", "list", "capture", "resize"],
101955
- description: "Operation mode. 'run' STARTS the models and returns once every slot has taken " + "its prompt (or failed to) \u2014 it does not wait for answers. Poll 'status' (read " + "`run.slots`) or 'list' for progress, then 'judge' once the slots have finished. 'capture' returns one slot's screen; 'resize' sets its terminal size. 'run-and-judge' is the blocking pipeline and holds the call open for the whole run. 'cancel' stops one slot or the whole run."
102128
+ enum: [
102129
+ "run",
102130
+ "judge",
102131
+ "run-and-judge",
102132
+ "status",
102133
+ "cancel",
102134
+ "list",
102135
+ "capture",
102136
+ "resize",
102137
+ "scroll"
102138
+ ],
102139
+ description: "Operation mode. 'run' STARTS the models and returns once every slot has taken " + "its prompt (or failed to) \u2014 it does not wait for answers. Poll 'status' (read " + "`run.slots`) or 'list' for progress, then 'judge' once the slots have finished. 'capture' returns one slot's screen; 'resize' sets its terminal size; 'scroll' scrolls its transcript view (`by` or `to`). 'run-and-judge' is the blocking pipeline and holds the call open for the whole run. 'cancel' stops one slot or the whole run."
101956
102140
  },
101957
102141
  slot: {
101958
102142
  type: "string",
101959
- description: "For 'cancel', 'capture' and 'resize': the anonymised slot id (e.g. '02') from the slot map 'run' returned. 'capture' and 'resize' require it; for 'cancel', omit it to stop every slot in the run."
102143
+ description: "For 'cancel', 'capture', 'resize' and 'scroll': the anonymised slot id (e.g. '02') from the slot map 'run' returned. 'capture', 'resize' and 'scroll' require it; for 'cancel', omit it to stop every slot in the run."
101960
102144
  },
101961
102145
  path: {
101962
102146
  type: "string",
@@ -101964,7 +102148,7 @@ This searched OpenRouter's listing only. Subscription wire ids and catalog alias
101964
102148
  },
101965
102149
  run_id: {
101966
102150
  type: "string",
101967
- description: "For 'status', 'cancel', 'capture' and 'resize': the run_id a 'run' answer returned. It addresses THAT run while this server retains it, even after a newer run reused the path. Omit it to address the newest run at 'path'."
102151
+ description: "For 'status', 'cancel', 'capture', 'resize' and 'scroll': the run_id a 'run' answer returned. It addresses THAT run while this server retains it, even after a newer run reused the path. Omit it to address the newest run at 'path'."
101968
102152
  },
101969
102153
  since_seq: {
101970
102154
  type: "number",
@@ -101982,6 +102166,15 @@ This searched OpenRouter's listing only. Subscription wire ids and catalog alias
101982
102166
  type: "integer",
101983
102167
  description: "For 'resize' (required): the terminal height in rows, clamped to 24\u20139999 and to " + "at most 4 000 000 cells (cols \xD7 rows)."
101984
102168
  },
102169
+ by: {
102170
+ type: "integer",
102171
+ description: "For 'scroll': rows to scroll the slot's transcript view; negative = toward older " + `content, positive = toward the live bottom. A target, \xB1 1 row; |by| is capped at ${MAX_SCROLL_ROWS}. Not 0. Give exactly one of 'by' and 'to'. The answer {run_id, path, slot, seq} carries the screen's seq after the scroll.`
102172
+ },
102173
+ to: {
102174
+ type: "string",
102175
+ enum: ["end"],
102176
+ description: `For 'scroll': "end" returns the slot's view to the live bottom. Give exactly one of 'by' and 'to'.`
102177
+ },
101985
102178
  models: {
101986
102179
  type: "array",
101987
102180
  items: { type: "string" },
@@ -102038,7 +102231,7 @@ This searched OpenRouter's listing only. Subscription wire ids and catalog alias
102038
102231
  heartbeat: true,
102039
102232
  handler: async (args, ctx) => {
102040
102233
  const mode = args.mode;
102041
- if (mode === "list" || mode === "status" || mode === "cancel" || mode === "capture" || mode === "resize") {
102234
+ if (mode === "list" || mode === "status" || mode === "cancel" || mode === "capture" || mode === "resize" || mode === "scroll") {
102042
102235
  return teamContractVerb(mode, args);
102043
102236
  }
102044
102237
  try {
@@ -102739,6 +102932,38 @@ Report manually at https://github.com/anthropics/claudish/issues${autoSendHint}`
102739
102932
  }
102740
102933
  }
102741
102934
  });
102935
+ tools.push({
102936
+ name: "scroll_session",
102937
+ description: "Scroll a session's transcript view, as a mouse wheel over the child's Claude Code would: the next capture_session shows the child scrolled. `by` is a signed row count (negative = toward older content, positive = toward the live bottom); it is a " + `target, \xB1 1 row, and |by| is capped at ${MAX_SCROLL_ROWS} per call. \`to:"end"\` returns to the live bottom. Give exactly one of \`by\` and \`to\`; \`by:0\` is refused. It never types into the prompt, submits or answers a dialog, and a prompt claudish delivers next returns the view to the bottom first. The answer {session_id, seq} carries the screen's seq after the scroll (the first changed screen within 1 s, else the current one). Errors are a JSON ContractError: \`scroll_unavailable\` while the session is still starting, finished, delivering a prompt, reading its turn from the screen or reconnecting, or when magmux did not confirm the send (the scroll may still land). Nothing is queued.`,
102938
+ inputSchema: {
102939
+ type: "object",
102940
+ properties: {
102941
+ session_id: { type: "string", description: "Session ID from create_session" },
102942
+ by: {
102943
+ type: "integer",
102944
+ description: `Rows to scroll; negative = older content. Not 0; |by| capped at ${MAX_SCROLL_ROWS}; a target, \xB1 1 row`
102945
+ },
102946
+ to: {
102947
+ type: "string",
102948
+ enum: ["end"],
102949
+ description: '"end" returns the view to the live bottom'
102950
+ }
102951
+ },
102952
+ required: ["session_id"]
102953
+ },
102954
+ group: "session",
102955
+ handler: async (args) => {
102956
+ try {
102957
+ const sessionId = optionalString(args.session_id, "session_id");
102958
+ if (sessionId === undefined)
102959
+ throw new ContractErrorException("invalid_args", "'session_id' is required");
102960
+ const req = scrollArgs(args);
102961
+ return contractAnswer(await sessionManager.scrollSession(sessionId, req));
102962
+ } catch (e) {
102963
+ return contractErrorAnswer(e);
102964
+ }
102965
+ }
102966
+ });
102742
102967
  return tools;
102743
102968
  }
102744
102969
  function resolveToolGroups(mode) {
@@ -102882,7 +103107,7 @@ Each session is an interactive Claude Code running the model in its own headless
102882
103107
  3. send_input may be called at any time; it is queued until the session is idle.
102883
103108
  4. COMPLETED: call get_output for the answer.
102884
103109
  5. FAILED or TIMEOUT \u2014 or COMPLETED with an empty or surprising answer \u2014 call get_diagnostics. It returns the child's final screen, the upstream error bodies, the recent state records, the resolved model chain and the paths to the full records. It needs no re-run and no debug flag.
102885
- 6. capture_session reads a session's current screen; resize_session sets its terminal size (any time, mid-turn included); cancel_session stops it. Nothing stops a session for being idle: a child inside a long build is silent and working.
103110
+ 6. capture_session reads a session's current screen; resize_session sets its terminal size (any time, mid-turn included); scroll_session scrolls the child's transcript view (by rows, or back to the live bottom); cancel_session stops it. Nothing stops a session for being idle: a child inside a long build is silent and working.
102886
103111
 
102887
103112
  The session_id from create_session is the key for every session tool.`, proxyInstance = null, proxyStarting = null, SCORE_TIE_EPSILON = 0.000001, NEXT_STEP, KEEP_EFFORT = "default", USER_REQUIRED_MODEL_DESCRIPTION, sanitize2, TOOL_MODES;
102888
103113
  var init_mcp_server = __esm(() => {
@@ -116084,6 +116309,7 @@ function buildClaudishSettingsOverlay(statusLine, proxyAuthMode, paneChild = isP
116084
116309
  if (paneChild) {
116085
116310
  settings.skipDangerousModePermissionPrompt = true;
116086
116311
  settings.editorMode = "normal";
116312
+ settings.wheelScrollAccelerationEnabled = false;
116087
116313
  }
116088
116314
  return settings;
116089
116315
  }
@@ -116108,6 +116334,9 @@ function mergeUserSettingsIfPresent(config, tempSettingsPath, statusLine, proxyA
116108
116334
  if (paneChild) {
116109
116335
  userSettings.editorMode = "normal";
116110
116336
  }
116337
+ if (paneChild) {
116338
+ userSettings.wheelScrollAccelerationEnabled = false;
116339
+ }
116111
116340
  chmodSync5(tempSettingsPath, 384);
116112
116341
  writeFileSync27(tempSettingsPath, JSON.stringify(userSettings, null, 2), {
116113
116342
  encoding: "utf-8",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudish",
3
- "version": "14.11.0",
3
+ "version": "14.12.0",
4
4
  "description": "Run Claude Code with any model - OpenRouter, Ollama, LM Studio & local models",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -71,10 +71,10 @@
71
71
  "ai"
72
72
  ],
73
73
  "optionalDependencies": {
74
- "@claudish/magmux-darwin-arm64": "14.11.0",
75
- "@claudish/magmux-darwin-x64": "14.11.0",
76
- "@claudish/magmux-linux-arm64": "14.11.0",
77
- "@claudish/magmux-linux-x64": "14.11.0"
74
+ "@claudish/magmux-darwin-arm64": "14.12.0",
75
+ "@claudish/magmux-darwin-x64": "14.12.0",
76
+ "@claudish/magmux-linux-arm64": "14.12.0",
77
+ "@claudish/magmux-linux-x64": "14.12.0"
78
78
  },
79
79
  "author": "Jack Rudenko <i@madappgang.com>",
80
80
  "license": "MIT",