@datalabrotterdam/nova-ai-cli 1.2.0-alpha.4 → 1.2.0-alpha.5

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/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.5](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.4...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.5) (2026-10-23)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **cli:** keep the full-screen frame one row short of the terminal ([37355f4](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/37355f4efb8a237f204c061ad7008faf54a0dd9e))
7
+ * **cli:** pick the account's default model when none is set ([fb0f34c](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/fb0f34c7bac26f1b59c4b6b2245e6fb8365f99f1))
8
+
9
+
10
+ ### Features
11
+
12
+ * **cli:** full-screen TUI with a pinned prompt and mouse-wheel scrolling ([a68d693](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/a68d6930945bd6440de22c709fc14ae32a9c9f65))
13
+
1
14
  # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.4](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.3...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.4) (2026-10-05)
2
15
 
3
16
 
package/README.md CHANGED
@@ -71,9 +71,11 @@ Interactive options:
71
71
 
72
72
  ## Terminal UI
73
73
 
74
- The UI renders inline below your shell prompt. Finished messages go to the
75
- terminal's own scrollback (scroll, search and copy as usual); only the
76
- streaming answer, running tools, the prompt and the status rows are redrawn.
74
+ The UI runs full screen (the terminal's alternate screen, like `less` or
75
+ `vim`) and gives your shell its screen back on exit. The mouse wheel or
76
+ PgUp/PgDn scroll the transcript, Esc jumps back to the newest output, and
77
+ while you read an answer the prompt that asked for it stays pinned on the top
78
+ row. To select text, hold Shift (Option in some macOS terminals) while dragging.
77
79
 
78
80
  - **Modes** (Shift+Tab): `agent` does the work; `plan` reads the code, asks
79
81
  questions and builds a task list but changes nothing; `ask` only answers.
package/dist/index.js CHANGED
@@ -175,7 +175,7 @@ ${MAIN_HELP}
175
175
  );
176
176
  return 2;
177
177
  }
178
- const { default: runChat } = await import("./tui-OWU4YGTT.js");
178
+ const { default: runChat } = await import("./tui-4XTC6VJP.js");
179
179
  return runChat(options);
180
180
  }
181
181
  }
@@ -16,7 +16,12 @@ import {
16
16
 
17
17
  // src/tui/index.ts
18
18
  import { createInterface as createInterface2 } from "readline/promises";
19
- import { readCredentials } from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
19
+ import {
20
+ readCredentials,
21
+ saveDefaultModel as saveDefaultModel2
22
+ } from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
23
+ import { resolveDefaultModel as resolveDefaultModel2 } from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
24
+ import { createNovaClient as createNovaClient2 } from "@datalabrotterdam/nova-ai-agent/core/nova-client.js";
20
25
  import { listStoredSessions as listStoredSessions2, loadStoredSession as loadStoredSession2 } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
21
26
  import { setWorkspaceTrusted as setWorkspaceTrusted2 } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
22
27
 
@@ -115,8 +120,8 @@ The update did not install. Run it yourself: ${update.command}
115
120
  }
116
121
 
117
122
  // src/tui/ink/app.tsx
118
- import { Box as Box6, render, Static, Text as Text6, useBoxMetrics, useWindowSize } from "ink";
119
- import { useEffect, useRef, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
123
+ import { Box as Box6, render, Text as Text6, useBoxMetrics, useInput as useInput3, useWindowSize } from "ink";
124
+ import { memo, useEffect, useRef, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
120
125
  import { savedPermissionMode } from "@datalabrotterdam/nova-ai-agent/core/policy/settings.js";
121
126
 
122
127
  // src/tui/session/session-runner.ts
@@ -1153,6 +1158,17 @@ function kittyKeyboardOverride(env = process.env) {
1153
1158
  function isLateTerminalReply(input) {
1154
1159
  return /^\u001b?\[\?[\d;]*[uc]$/.test(input);
1155
1160
  }
1161
+ var MOUSE_REPORTING_ON = "\x1B[?1000h\x1B[?1006h";
1162
+ var MOUSE_REPORTING_OFF = "\x1B[?1000l\x1B[?1006l";
1163
+ var MOUSE_INPUT = /^\u001b?\[<(\d+);\d+;\d+[Mm]$/;
1164
+ function isMouseInput(input) {
1165
+ return MOUSE_INPUT.test(input);
1166
+ }
1167
+ function wheelRows(input) {
1168
+ const button = Number(MOUSE_INPUT.exec(input)?.[1] ?? -1);
1169
+ if (button < 0 || (button & 64) === 0) return 0;
1170
+ return button & 1 ? -3 : 3;
1171
+ }
1156
1172
 
1157
1173
  // src/tui/ink/components/prompt-input.tsx
1158
1174
  import { Box, Text, useInput, usePaste } from "ink";
@@ -1170,7 +1186,8 @@ var palette = {
1170
1186
  faint: "#64748b",
1171
1187
  code: "#fcd34d",
1172
1188
  diffAddBg: "#1b3a24",
1173
- diffRemoveBg: "#3d1f1f"
1189
+ diffRemoveBg: "#3d1f1f",
1190
+ stickyBackground: "#1e293b"
1174
1191
  };
1175
1192
  var colors = {
1176
1193
  primary: chalk.hex(palette.primary),
@@ -1197,7 +1214,7 @@ function PromptInput({
1197
1214
  }) {
1198
1215
  useSyncExternalStore(editor.subscribe, editor.getVersion);
1199
1216
  useInput((input, key) => {
1200
- if (onKey(input, key)) return;
1217
+ if (isMouseInput(input) || onKey(input, key)) return;
1201
1218
  editor.handleKey(input, key);
1202
1219
  });
1203
1220
  usePaste((text) => editor.paste(text));
@@ -1483,7 +1500,8 @@ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-run
1483
1500
  function StatusLine({
1484
1501
  ui,
1485
1502
  model,
1486
- mcp
1503
+ mcp,
1504
+ scroll = 0
1487
1505
  }) {
1488
1506
  const separator = /* @__PURE__ */ jsx2(Text2, { color: palette.faint, children: " | " });
1489
1507
  const usage = ui.contextUsage ? `ctx:${formatTokenCount(ui.contextUsage.totalTokens)}${ui.contextUsage.contextWindow ? `/${formatTokenCount(ui.contextUsage.contextWindow)}` : ""}` : null;
@@ -1511,6 +1529,10 @@ function StatusLine({
1511
1529
  `mcp:${mcp.connected}/${mcp.configured}`,
1512
1530
  mcp.failed ? /* @__PURE__ */ jsx2(Text2, { color: palette.danger, children: ` !${mcp.failed}` }) : null
1513
1531
  ] }) : null,
1532
+ scroll ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1533
+ separator,
1534
+ /* @__PURE__ */ jsx2(Text2, { color: palette.warning, children: `\u2191${scroll} lines \xB7 PgDn/Esc` })
1535
+ ] }) : null,
1514
1536
  mcp.skills ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1515
1537
  separator,
1516
1538
  `skills:${mcp.skills}`
@@ -3601,17 +3623,11 @@ var TranscriptCommitter = class {
3601
3623
  planSignature = null;
3602
3624
  snapshot = null;
3603
3625
  lastState = null;
3604
- redrawRequested = false;
3605
- /** The next sync starts a new epoch, e.g. after the terminal width changed. */
3606
- requestRedraw() {
3607
- this.redrawRequested = true;
3608
- this.lastState = null;
3609
- }
3610
3626
  sync(state, expanded) {
3611
3627
  if (this.snapshot && state === this.lastState && expanded === this.expanded)
3612
3628
  return this.snapshot;
3613
3629
  this.lastState = state;
3614
- if (this.redrawRequested || state.sessionId !== this.sessionId || expanded !== this.expanded || !this.prefixIntact(state.messages)) {
3630
+ if (state.sessionId !== this.sessionId || expanded !== this.expanded || !this.prefixIntact(state.messages)) {
3615
3631
  this.startEpoch(state, expanded);
3616
3632
  }
3617
3633
  const messages = state.messages;
@@ -3764,7 +3780,6 @@ var TranscriptCommitter = class {
3764
3780
  return true;
3765
3781
  }
3766
3782
  startEpoch(state, expanded) {
3767
- this.redrawRequested = false;
3768
3783
  this.epoch++;
3769
3784
  this.sessionId = state.sessionId;
3770
3785
  this.expanded = expanded;
@@ -3810,7 +3825,6 @@ function safeSplitOffset(text, from) {
3810
3825
  }
3811
3826
 
3812
3827
  // src/tui/ink/controller.ts
3813
- var CLEAR_SCREEN_AND_SCROLLBACK = "\x1B[2J\x1B[3J\x1B[H";
3814
3828
  var EXIT_CONFIRM_MS = 2e3;
3815
3829
  var TuiController = class {
3816
3830
  constructor(store, runner, io) {
@@ -3835,6 +3849,8 @@ var TuiController = class {
3835
3849
  exiting = false;
3836
3850
  view;
3837
3851
  lastEpoch = 0;
3852
+ scroll = 0;
3853
+ viewport = { rows: 10, contentRows: 0 };
3838
3854
  exitArmed = false;
3839
3855
  exitTimer;
3840
3856
  animationTimer;
@@ -3893,8 +3909,17 @@ var TuiController = class {
3893
3909
  this.runner.cancel();
3894
3910
  return true;
3895
3911
  }
3912
+ if (this.scroll > 0) {
3913
+ this.scrollTo(0);
3914
+ return true;
3915
+ }
3896
3916
  return false;
3897
3917
  }
3918
+ if (key.pageUp || key.pageDown) {
3919
+ const page = Math.max(1, this.viewport.rows - 2);
3920
+ this.scrollBy(key.pageUp ? page : -page);
3921
+ return true;
3922
+ }
3898
3923
  if (key.tab && key.shift) {
3899
3924
  this.cycleInteractionMode();
3900
3925
  return true;
@@ -4116,14 +4141,23 @@ ${skill.path}`
4116
4141
  }
4117
4142
  };
4118
4143
  /**
4119
- * Prints the whole transcript again. After the width changed, the terminal
4120
- * re-wrapped what is on screen on its own and the live area can't be
4121
- * erased exactly (Ink leaves stale prompt borders), and old scrollback keeps
4122
- * the old wrapping.
4144
+ * The view reports its transcript size after each layout. While scrolled
4145
+ * up, new output below grows the offset so the rows being read stay put.
4123
4146
  */
4124
- redrawAll() {
4125
- if (this.stopped || this.exiting) return;
4126
- this.committer.requestRedraw();
4147
+ setViewport(rows, contentRows) {
4148
+ const grown = contentRows - this.viewport.contentRows;
4149
+ this.viewport = { rows, contentRows };
4150
+ this.scrollTo(this.scroll > 0 && grown > 0 ? this.scroll + grown : this.scroll);
4151
+ }
4152
+ /** Positive scrolls toward older output. */
4153
+ scrollBy(rows) {
4154
+ this.scrollTo(this.scroll + rows);
4155
+ }
4156
+ scrollTo(offset) {
4157
+ const max = Math.max(0, this.viewport.contentRows - this.viewport.rows);
4158
+ const next = Math.max(0, Math.min(offset, max));
4159
+ if (next === this.scroll) return;
4160
+ this.scroll = next;
4127
4161
  this.refresh();
4128
4162
  }
4129
4163
  toggleToolDetails() {
@@ -4224,6 +4258,7 @@ ${skill.path}`
4224
4258
  submit = async (text, images = [], pastes = []) => {
4225
4259
  const value = text.trim();
4226
4260
  if (!value) return;
4261
+ this.scroll = 0;
4227
4262
  this.editor.addToHistory(value);
4228
4263
  this.store.setState((state) => ({
4229
4264
  inputHistory: [...state.inputHistory, value],
@@ -4325,7 +4360,10 @@ ${skill.path}`
4325
4360
  this.view = this.buildView();
4326
4361
  if (this.view.transcript.epoch !== this.lastEpoch) {
4327
4362
  this.lastEpoch = this.view.transcript.epoch;
4328
- this.io.write(CLEAR_SCREEN_AND_SCROLLBACK);
4363
+ if (this.scroll) {
4364
+ this.scroll = 0;
4365
+ this.view = this.buildView();
4366
+ }
4329
4367
  }
4330
4368
  for (const listener of this.listeners) listener();
4331
4369
  }
@@ -4339,7 +4377,8 @@ ${skill.path}`
4339
4377
  model: this.runner.model,
4340
4378
  mcp: mcpCounts(this.runner),
4341
4379
  expandedTools: this.expandedTools,
4342
- exiting: this.exiting
4380
+ exiting: this.exiting,
4381
+ scroll: this.scroll
4343
4382
  };
4344
4383
  }
4345
4384
  questionState(pending) {
@@ -4409,7 +4448,7 @@ function useDialogInput(handle, isActive) {
4409
4448
  const [, setTick] = useState(0);
4410
4449
  useInput2(
4411
4450
  (input, key) => {
4412
- if (handle(input, key) === "changed") setTick((tick) => tick + 1);
4451
+ if (!isMouseInput(input) && handle(input, key) === "changed") setTick((tick) => tick + 1);
4413
4452
  },
4414
4453
  { isActive }
4415
4454
  );
@@ -4551,7 +4590,7 @@ function QuestionDialog({
4551
4590
  const [, setTick] = useState(0);
4552
4591
  useInput2(
4553
4592
  (input, key) => {
4554
- if (state.handleKey(input, key) === "changed") setTick((tick) => tick + 1);
4593
+ if (!isMouseInput(input) && state.handleKey(input, key) === "changed") setTick((tick) => tick + 1);
4555
4594
  },
4556
4595
  { isActive }
4557
4596
  );
@@ -4633,7 +4672,7 @@ function PermissionDialog({
4633
4672
  }) {
4634
4673
  useInput2(
4635
4674
  (input, key) => {
4636
- if (permissionKey(request, input, key) === "closed") onClose();
4675
+ if (!isMouseInput(input) && permissionKey(request, input, key) === "closed") onClose();
4637
4676
  },
4638
4677
  { isActive }
4639
4678
  );
@@ -4684,9 +4723,8 @@ function wrapPlain(text, width) {
4684
4723
  }
4685
4724
 
4686
4725
  // src/tui/ink/app.tsx
4687
- import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
4726
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
4688
4727
  var FRAME_MS = 120;
4689
- var RESIZE_SETTLE_MS = 100;
4690
4728
  async function runInkTui(credentials, cwd, args, options = {}) {
4691
4729
  const store = createStore({
4692
4730
  messages: [],
@@ -4728,22 +4766,15 @@ async function runInkTui(credentials, cwd, args, options = {}) {
4728
4766
  // Ink otherwise turns non-interactive whenever CI is set in the
4729
4767
  // environment; the TUI only runs on a terminal anyway.
4730
4768
  interactive: Boolean(stdout.isTTY),
4769
+ // Full screen: the transcript scrolls inside the app (PgUp/PgDn) and the
4770
+ // terminal gets its own screen back on exit.
4771
+ alternateScreen: Boolean(stdout.isTTY),
4731
4772
  kittyKeyboard: { mode: kittyKeyboard ? "enabled" : "disabled" }
4732
4773
  });
4733
4774
  controller.start();
4775
+ if (stdout.isTTY) stdout.write(MOUSE_REPORTING_ON);
4734
4776
  const onSignal = () => controller.requestExit();
4735
4777
  process.once("SIGTERM", onSignal);
4736
- let printedColumns = stdout.columns;
4737
- let resizeTimer;
4738
- const onResize = () => {
4739
- if (resizeTimer) clearTimeout(resizeTimer);
4740
- resizeTimer = setTimeout(() => {
4741
- if (stdout.columns === printedColumns) return;
4742
- printedColumns = stdout.columns;
4743
- controller.redrawAll();
4744
- }, RESIZE_SETTLE_MS);
4745
- };
4746
- stdout.on("resize", onResize);
4747
4778
  void (options.checkForUpdate ?? checkForUpdate)().then((updateAvailable) => {
4748
4779
  if (updateAvailable) store.setState({ updateAvailable });
4749
4780
  });
@@ -4753,8 +4784,7 @@ async function runInkTui(credentials, cwd, args, options = {}) {
4753
4784
  await controller.stop();
4754
4785
  } finally {
4755
4786
  process.off("SIGTERM", onSignal);
4756
- stdout.off("resize", onResize);
4757
- if (resizeTimer) clearTimeout(resizeTimer);
4787
+ if (stdout.isTTY) stdout.write(MOUSE_REPORTING_OFF);
4758
4788
  instance.unmount();
4759
4789
  await instance.waitUntilExit().catch(() => {
4760
4790
  });
@@ -4768,31 +4798,85 @@ function App({ controller }) {
4768
4798
  const { rows, columns } = useWindowSize();
4769
4799
  const bottomRef = useRef(null);
4770
4800
  const bottom = useBoxMetrics(bottomRef);
4801
+ const contentRef = useRef(null);
4802
+ const content = useBoxMetrics(contentRef);
4771
4803
  const working = useWorking(view);
4804
+ const [userTops, setUserTops] = useState2(/* @__PURE__ */ new Map());
4805
+ useInput3((input) => {
4806
+ const rows2 = wheelRows(input);
4807
+ if (rows2) controller.scrollBy(rows2);
4808
+ });
4809
+ const height = Math.max(4, rows - 1);
4810
+ const viewportRows = Math.max(3, height - (bottom.hasMeasured ? bottom.height : 6));
4811
+ useEffect(() => {
4812
+ if (content.hasMeasured) controller.setViewport(viewportRows, content.height);
4813
+ }, [controller, viewportRows, content.hasMeasured, content.height]);
4814
+ if (view.exiting) return null;
4772
4815
  const { transcript } = view;
4773
- const scrollback = (
4774
- // <Static> is laid out as an absolute box; without a width its text
4775
- // wraps against its content and runs past the terminal edge.
4776
- /* @__PURE__ */ jsx6(Static, { items: [...transcript.committed], style: { width: columns }, children: (block) => /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block }) }, block.key) }, transcript.epoch)
4777
- );
4778
- if (view.exiting) return scrollback;
4779
- const liveRows = Math.max(3, rows - (bottom.hasMeasured ? bottom.height : 6) - 2);
4780
- const dialogRows = Math.max(6, rows - 4);
4781
- return /* @__PURE__ */ jsxs6(Fragment4, { children: [
4782
- scrollback,
4783
- /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", maxHeight: liveRows, overflowY: "hidden", justifyContent: "flex-end", children: /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", flexShrink: 0, children: [
4784
- transcript.live.map((block) => /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block, frame: working.frame }) }, block.key)),
4785
- working.line ? /* @__PURE__ */ jsx6(Box6, { marginTop: 1, paddingLeft: 1, children: /* @__PURE__ */ jsxs6(Text6, { children: [
4786
- /* @__PURE__ */ jsx6(Text6, { color: palette.accent, children: WORKING_FRAMES[working.frame] }),
4787
- /* @__PURE__ */ jsx6(Text6, { color: palette.muted, children: ` ${working.line}` })
4788
- ] }) }) : null
4789
- ] }) }),
4790
- /* @__PURE__ */ jsxs6(Box6, { ref: bottomRef, flexDirection: "column", marginTop: 1, children: [
4791
- /* @__PURE__ */ jsx6(Surface, { controller, view, rows: dialogRows, columns }),
4792
- /* @__PURE__ */ jsx6(StatusLine, { ui: view.ui, model: view.model, mcp: view.mcp })
4816
+ const blocks = [...transcript.committed, ...transcript.live];
4817
+ const liveKeys = new Set(transcript.live.map((block) => block.key));
4818
+ const onUserTop = (key, top) => setUserTops((tops) => tops.get(key) === top ? tops : new Map(tops).set(key, top));
4819
+ const firstVisibleRow = content.height - viewportRows - view.scroll;
4820
+ let sticky = null;
4821
+ for (const block of blocks) {
4822
+ if (block.kind !== "user") continue;
4823
+ const top = userTops.get(block.key);
4824
+ if (top !== void 0 && top < firstVisibleRow) sticky = block.text;
4825
+ }
4826
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height, children: [
4827
+ /* @__PURE__ */ jsxs6(
4828
+ Box6,
4829
+ {
4830
+ flexDirection: "column",
4831
+ height: viewportRows,
4832
+ flexShrink: 0,
4833
+ overflowY: "hidden",
4834
+ justifyContent: "flex-end",
4835
+ children: [
4836
+ /* @__PURE__ */ jsxs6(Box6, { ref: contentRef, flexDirection: "column", flexShrink: 0, marginBottom: -view.scroll, children: [
4837
+ blocks.map(
4838
+ (block) => liveKeys.has(block.key) ? /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block, frame: working.frame }) }, block.key) : /* @__PURE__ */ jsx6(CommittedBlock, { block, onUserTop }, block.key)
4839
+ ),
4840
+ working.line ? /* @__PURE__ */ jsx6(Box6, { marginTop: 1, paddingLeft: 1, children: /* @__PURE__ */ jsxs6(Text6, { children: [
4841
+ /* @__PURE__ */ jsx6(Text6, { color: palette.accent, children: WORKING_FRAMES[working.frame] }),
4842
+ /* @__PURE__ */ jsx6(Text6, { color: palette.muted, children: ` ${working.line}` })
4843
+ ] }) }) : null
4844
+ ] }),
4845
+ sticky !== null ? /* @__PURE__ */ jsx6(StickyPrompt, { text: sticky, columns }) : null
4846
+ ]
4847
+ }
4848
+ ),
4849
+ /* @__PURE__ */ jsxs6(Box6, { ref: bottomRef, flexDirection: "column", flexShrink: 0, children: [
4850
+ /* @__PURE__ */ jsx6(Surface, { controller, view, rows: Math.max(6, rows - 4), columns }),
4851
+ /* @__PURE__ */ jsx6(StatusLine, { ui: view.ui, model: view.model, mcp: view.mcp, scroll: view.scroll })
4793
4852
  ] })
4794
4853
  ] });
4795
4854
  }
4855
+ var CommittedBlock = memo(function CommittedBlock2({
4856
+ block,
4857
+ onUserTop
4858
+ }) {
4859
+ if (block.kind !== "user") {
4860
+ return /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block }) });
4861
+ }
4862
+ return /* @__PURE__ */ jsx6(UserAnchor, { block, onUserTop, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block }) });
4863
+ }, (before, after) => before.block === after.block);
4864
+ function UserAnchor({
4865
+ block,
4866
+ onUserTop,
4867
+ children
4868
+ }) {
4869
+ const ref = useRef(null);
4870
+ const { top, hasMeasured } = useBoxMetrics(ref);
4871
+ useEffect(() => {
4872
+ if (hasMeasured) onUserTop(block.key, top);
4873
+ }, [block.key, top, hasMeasured]);
4874
+ return /* @__PURE__ */ jsx6(Box6, { ref, marginTop: 1, children });
4875
+ }
4876
+ function StickyPrompt({ text, columns }) {
4877
+ const line = `> ${text.replace(/\s+/g, " ").trim()}`;
4878
+ return /* @__PURE__ */ jsx6(Box6, { position: "absolute", top: 0, left: 0, width: columns, children: /* @__PURE__ */ jsx6(Text6, { color: palette.primary, backgroundColor: palette.stickyBackground, wrap: "truncate-end", children: line.padEnd(columns) }) });
4879
+ }
4796
4880
  function Surface({
4797
4881
  controller,
4798
4882
  view,
@@ -4859,7 +4943,14 @@ async function runChat(options) {
4859
4943
  console.error("nova-ai isn't connected to a Nova AI account yet. Run `nova-ai login` first.");
4860
4944
  return 1;
4861
4945
  }
4862
- const credentials = options.model ? { ...stored, defaultModel: options.model } : stored;
4946
+ const model = options.model ?? stored.defaultModel ?? process.env.NOVA_MODEL ?? await pickDefaultModel(stored);
4947
+ if (!model) {
4948
+ console.error("nova-ai has no model to use yet. Start it with one, for example:");
4949
+ console.error(" nova-ai --model <model-id> (or set NOVA_MODEL)");
4950
+ console.error("Then Ctrl+P in nova-ai switches and remembers the model.");
4951
+ return 1;
4952
+ }
4953
+ const credentials = { ...stored, defaultModel: model };
4863
4954
  const updateCheck = checkForUpdate().then((update) => withInstallability(update));
4864
4955
  const updatedFrom = process.env[UPDATED_FROM_ENV];
4865
4956
  if (updatedFrom) {
@@ -4894,6 +4985,19 @@ async function runChat(options) {
4894
4985
  }
4895
4986
  return 0;
4896
4987
  }
4988
+ async function pickDefaultModel(credentials) {
4989
+ try {
4990
+ const { data } = await createNovaClient2(credentials.apiKey).models.list({ limit: 100 });
4991
+ const model = resolveDefaultModel2(data, void 0);
4992
+ if (model) {
4993
+ saveDefaultModel2(model);
4994
+ console.log(`Using model ${model} (Ctrl+P switches).`);
4995
+ }
4996
+ return model;
4997
+ } catch {
4998
+ return void 0;
4999
+ }
5000
+ }
4897
5001
  async function askToTrust(cwd) {
4898
5002
  const prompt = createInterface2({ input: process.stdin, output: process.stdout });
4899
5003
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalabrotterdam/nova-ai-cli",
3
- "version": "1.2.0-alpha.4",
3
+ "version": "1.2.0-alpha.5",
4
4
  "description": "Agentic coding with DataLab Rotterdam's Nova AI in your terminal: interactive TUI, headless mode, and an ACP agent for editors",
5
5
  "license": "Apache-2.0",
6
6
  "author": "DataLab Rotterdam",