@manix-cli/manix 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -29,6 +29,8 @@ or `export OPENROUTER_API_KEY=...`.
29
29
  `--yolo` to live dangerously.
30
30
  - **Any model** — `/model` opens a live picker with context windows and $/M pricing.
31
31
  - **Sessions** — every conversation is saved; `manix --continue` or `/resume`.
32
+ - **Rewind** — `/rewind` jumps back to any earlier message: reverts the file edits it made,
33
+ trims the chat, and drops the message back in the composer to redo.
32
34
  - **MANIX.md** — project memory auto-loaded each session; generate with `/init`.
33
35
  - **Skills** — drop a `SKILL.md` playbook in `~/.manix/skills/<name>/` or `.manix/skills/<name>/`;
34
36
  the agent loads it when relevant, or invoke directly with `/<name>`.
@@ -45,11 +47,12 @@ or `export OPENROUTER_API_KEY=...`.
45
47
  | `/help` | commands & shortcuts | `manix "task"` | start with a prompt |
46
48
  | `/model [id]` | switch model | `-p, --print` | non-interactive mode |
47
49
  | `/resume` | resume a session | `-m, --model <id>` | model for this run |
48
- | `/compact` | summarize history | `-c, --continue` | resume last session |
49
- | `/cost` | usage + credits | `-r, --resume [id]` | pick a session |
50
- | `/mcp` `/skills` | status / list | `--yolo` | auto-approve tools |
51
- | `/init` | generate MANIX.md | `-v` `-h` | version / help |
52
- | `/clear` `/yolo` `/exit` | | `echo task \| manix` | pipe a prompt |
50
+ | `/rewind` | revert to a past message | `-c, --continue` | resume last session |
51
+ | `/compact` | summarize history | `-r, --resume [id]` | pick a session |
52
+ | `/cost` | usage + credits | `--yolo` | auto-approve tools |
53
+ | `/mcp` `/skills` | status / list | `-v` `-h` | version / help |
54
+ | `/init` | generate MANIX.md | `echo task \| manix` | pipe a prompt |
55
+ | `/clear` `/yolo` `/exit` | | | |
53
56
 
54
57
  Shortcuts: **Esc** interrupt · **↑/↓** history · **Tab** complete · **Ctrl+C** twice to quit.
55
58
 
package/dist/agent.js CHANGED
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import { streamChat, fetchModels, cachedModels } from "./openrouter.js";
2
4
  import { systemPrompt, COMPACT_PROMPT } from "./prompts.js";
3
5
  import { builtinTools, toOpenAI } from "./tools/index.js";
@@ -16,6 +18,8 @@ class Agent {
16
18
  this.messages = [];
17
19
  this.session = null;
18
20
  this.usage = { prompt: 0, completion: 0, cost: 0, requests: 0 };
21
+ this.checkpoints = [];
22
+ this._currentCheckpoint = null;
19
23
  this.contextLength = 128e3;
20
24
  this.ac = null;
21
25
  fetchModels(this.apiKey).then(() => this.refreshModelInfo()).catch(() => {
@@ -45,6 +49,52 @@ class Agent {
45
49
  reset() {
46
50
  this.messages = [];
47
51
  this.session = null;
52
+ this.checkpoints = [];
53
+ }
54
+ /** Turn list for the /rewind picker (one entry per user message). */
55
+ turns() {
56
+ return this.checkpoints.map((cp, i) => ({
57
+ index: i,
58
+ label: cp.label,
59
+ snapshots: cp.snapshots.length,
60
+ bashCount: cp.bashCount,
61
+ itemCount: cp.itemCount
62
+ }));
63
+ }
64
+ /**
65
+ * Rewind to just before checkpoint `index`. Restores every file touched from
66
+ * `index` onward to its pre-range content, trims history + checkpoints, and
67
+ * returns { files, bashCount } so the UI can report what happened.
68
+ */
69
+ rewind(index) {
70
+ const seen = /* @__PURE__ */ new Set();
71
+ const restores = [];
72
+ let bashCount = 0;
73
+ for (let i = index; i < this.checkpoints.length; i++) {
74
+ bashCount += this.checkpoints[i].bashCount || 0;
75
+ for (const snap of this.checkpoints[i].snapshots) {
76
+ if (seen.has(snap.path)) continue;
77
+ seen.add(snap.path);
78
+ restores.push(snap);
79
+ }
80
+ }
81
+ for (const snap of restores) {
82
+ if (!snap.existed) {
83
+ try {
84
+ fs.unlinkSync(snap.path);
85
+ } catch {
86
+ }
87
+ } else {
88
+ try {
89
+ fs.writeFileSync(snap.path, snap.before);
90
+ } catch {
91
+ }
92
+ }
93
+ }
94
+ this.messages = this.messages.slice(0, this.checkpoints[index].messageCount);
95
+ this.checkpoints = this.checkpoints.slice(0, index);
96
+ this.h.onStats?.(this.stats());
97
+ return { files: restores.length, bashCount };
48
98
  }
49
99
  buildTools() {
50
100
  return [...builtinTools(), skillTool(this.skills), ...this.mcp?.getTools() || []];
@@ -73,9 +123,27 @@ class Agent {
73
123
  abort() {
74
124
  this.ac?.abort();
75
125
  }
76
- async send(text) {
126
+ /**
127
+ * Send a user message. `itemCount` is the UI log watermark (key of the item
128
+ * for this message) and `label` is what to show / refill on rewind — both are
129
+ * captured by the caller *before* the message is drawn. Opens one checkpoint.
130
+ */
131
+ async send(text, itemCount = 0, label = null) {
132
+ this._currentCheckpoint = {
133
+ label: (label ?? String(text)).split("\n")[0].slice(0, 72),
134
+ messageCount: this.messages.length,
135
+ itemCount,
136
+ bashCount: 0,
137
+ snapshots: []
138
+ };
139
+ this.checkpoints.push(this._currentCheckpoint);
77
140
  this.pushMessage({ role: "user", content: text });
78
- await this.run();
141
+ this.h.onStats?.(this.stats());
142
+ try {
143
+ await this.run();
144
+ } finally {
145
+ this._currentCheckpoint = null;
146
+ }
79
147
  }
80
148
  async run() {
81
149
  this.ac = new AbortController();
@@ -152,6 +220,19 @@ class Agent {
152
220
  return "The user denied permission for this action. Do not retry it; ask or take another approach.";
153
221
  }
154
222
  this.h.onToolStart?.({ display });
223
+ if (this._currentCheckpoint) {
224
+ if ((name === "write_file" || name === "edit_file") && args.path) {
225
+ const absPath = path.resolve(this.cwd, args.path);
226
+ const existed = fs.existsSync(absPath);
227
+ this._currentCheckpoint.snapshots.push({
228
+ path: absPath,
229
+ existed,
230
+ before: existed ? fs.readFileSync(absPath, "utf8") : null
231
+ });
232
+ } else if (name === "bash") {
233
+ this._currentCheckpoint.bashCount++;
234
+ }
235
+ }
155
236
  try {
156
237
  const out = String(await tool.run(args, { cwd: this.cwd }));
157
238
  this.h.onToolEnd?.({ display, ok: true, summary: summarize(out) });
package/dist/cli.js CHANGED
@@ -94,6 +94,16 @@ if (!process.stdout.isTTY) {
94
94
  mcp.start(mcpConfigs);
95
95
  const { render } = await import("ink");
96
96
  const { default: App } = await import("./ui/App.js");
97
+ const screen = {
98
+ reset() {
99
+ try {
100
+ instance.clear();
101
+ instance.fullStaticOutput = "";
102
+ } catch {
103
+ }
104
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
105
+ }
106
+ };
97
107
  const instance = render(
98
108
  /* @__PURE__ */ jsx(
99
109
  App,
@@ -106,7 +116,8 @@ const instance = render(
106
116
  contextFiles,
107
117
  initialPrompt: prompt || null,
108
118
  resumeTarget: args.resume || null,
109
- yolo: !!args.yolo
119
+ yolo: !!args.yolo,
120
+ screen
110
121
  }
111
122
  ),
112
123
  { exitOnCtrlC: false }
package/dist/markdown.js CHANGED
@@ -1,21 +1,35 @@
1
- import { t } from "./theme.js";
1
+ import chalk from "chalk";
2
+ import { t, color } from "./theme.js";
3
+ const codeBg = chalk.bgHex("#1e2030");
4
+ const codeText = chalk.hex(color.code).bgHex("#1e2030");
5
+ const gutterCol = chalk.hex(color.border).bgHex("#1e2030");
6
+ const labelCol = chalk.hex(color.faint);
7
+ const termWidth = () => Math.min(process.stdout.columns || 80, 120);
2
8
  function renderMarkdown(md) {
3
9
  const out = [];
4
10
  let inCode = false;
11
+ let lang = "";
5
12
  for (const line of String(md).split("\n")) {
6
13
  const fence = line.match(/^\s*```(\S*)/);
7
14
  if (fence) {
8
15
  inCode = !inCode;
9
- out.push(inCode ? t.faint("\u250C\u2574" + (fence[1] || "code")) : t.faint("\u2514\u2574"));
16
+ lang = fence[1] || "";
17
+ if (inCode) {
18
+ const label = lang ? ` ${lang} ` : " code ";
19
+ out.push(labelCol(" \u256D\u2500") + labelCol(label) + labelCol("\u2500".repeat(Math.max(2, termWidth() - label.length - 6))));
20
+ } else {
21
+ out.push(labelCol(" \u2570" + "\u2500".repeat(termWidth() - 3)));
22
+ }
10
23
  continue;
11
24
  }
12
25
  if (inCode) {
13
- out.push(t.faint("\u2502 ") + t.code(line));
26
+ const padded = " " + line + " ".repeat(Math.max(0, termWidth() - line.length - 3));
27
+ out.push(gutterCol(" \u2502") + codeText(padded));
14
28
  continue;
15
29
  }
16
30
  out.push(renderLine(line));
17
31
  }
18
- if (inCode) out.push(t.faint("\u2514\u2574"));
32
+ if (inCode) out.push(labelCol(" \u2570" + "\u2500".repeat(termWidth() - 3)));
19
33
  return out.join("\n");
20
34
  }
21
35
  function renderLine(line) {
package/dist/slash.js CHANGED
@@ -3,6 +3,7 @@ function slashCommands(skills = []) {
3
3
  { name: "help", desc: "Show commands and shortcuts" },
4
4
  { name: "model", desc: "Switch model \u2014 picker, or /model <id>" },
5
5
  { name: "resume", desc: "Resume a previous session" },
6
+ { name: "rewind", desc: "Pick a past turn and revert all file changes since then" },
6
7
  { name: "clear", desc: "Start a fresh conversation" },
7
8
  { name: "compact", desc: "Summarize history to free context" },
8
9
  { name: "cost", desc: "Session usage + OpenRouter credits" },
package/dist/ui/App.js CHANGED
@@ -26,7 +26,8 @@ function App({
26
26
  contextFiles,
27
27
  initialPrompt,
28
28
  resumeTarget,
29
- yolo
29
+ yolo,
30
+ screen
30
31
  }) {
31
32
  const { exit } = useApp();
32
33
  const [ready, setReady] = useState(!!config.apiKey);
@@ -47,6 +48,8 @@ function App({
47
48
  const [model, setModelState] = useState(config.model);
48
49
  const [yoloOn, setYoloOn] = useState(!!yolo);
49
50
  const [exitHint, setExitHint] = useState(false);
51
+ const [prefill, setPrefill] = useState({ text: "", seq: 0 });
52
+ const [staticKey, setStaticKey] = useState(0);
50
53
  const keyRef = useRef(0);
51
54
  const agentRef = useRef(null);
52
55
  const permsRef = useRef(null);
@@ -70,9 +73,10 @@ function App({
70
73
  setStats(agentRef.current.stats());
71
74
  }
72
75
  function runPrompt(text, displayText = text) {
76
+ const watermark = keyRef.current;
73
77
  push({ kind: "user", text: displayText });
74
78
  setBusy(true);
75
- agentRef.current.send(text).finally(() => {
79
+ agentRef.current.send(text, watermark, displayText).finally(() => {
76
80
  setBusy(false);
77
81
  setActivity(null);
78
82
  });
@@ -168,6 +172,11 @@ OpenRouter credits: $${(credits.total_credits - credits.total_usage).toFixed(2)}
168
172
  return setOverlay({ type: "models" });
169
173
  case "resume":
170
174
  return setOverlay({ type: "sessions" });
175
+ case "rewind": {
176
+ const turns = agentRef.current.turns();
177
+ if (!turns.length) return push({ kind: "info", text: "Nothing to rewind \u2014 no turns in this session yet." });
178
+ return setOverlay({ type: "rewind", turns });
179
+ }
171
180
  default:
172
181
  return;
173
182
  }
@@ -294,9 +303,47 @@ OpenRouter credits: $${(credits.total_credits - credits.total_usage).toFixed(2)}
294
303
  },
295
304
  onCancel: () => setOverlay(null)
296
305
  }
306
+ ) : overlay?.type === "rewind" ? /* @__PURE__ */ jsx(
307
+ Picker,
308
+ {
309
+ title: "Rewind to a message \u2014 its file edits (and everything after) are reverted",
310
+ load: () => overlay.turns.map((t) => {
311
+ const parts = [];
312
+ if (t.snapshots) parts.push(`${t.snapshots} file edit${t.snapshots !== 1 ? "s" : ""}`);
313
+ if (t.bashCount) parts.push(`${t.bashCount} bash`);
314
+ return {
315
+ id: String(t.index),
316
+ label: t.label || "(empty)",
317
+ extra: parts.length ? parts.join(" \xB7 ") : "no file changes"
318
+ };
319
+ }),
320
+ onSelect: (it) => {
321
+ setOverlay(null);
322
+ const index = Number(it.id);
323
+ const turn = overlay.turns.find((t) => t.index === index);
324
+ const watermark = turn?.itemCount ?? 0;
325
+ const { files, bashCount } = agentRef.current.rewind(index);
326
+ const notes = [];
327
+ if (files > 0) notes.push(`reverted ${files} file change${files !== 1 ? "s" : ""}`);
328
+ if (bashCount > 0)
329
+ notes.push(
330
+ `${bashCount} bash command${bashCount !== 1 ? "s" : ""} ran after this point \u2014 any files they changed were NOT reverted`
331
+ );
332
+ const notice = notes.length ? { kind: "info", text: `Rewound \u2014 ${notes.join("; ")}.`, key: keyRef.current++ } : null;
333
+ screen?.reset?.();
334
+ setItems((prev) => {
335
+ const kept = prev.filter((x) => x.key < watermark);
336
+ return notice ? [...kept, notice] : kept;
337
+ });
338
+ setStaticKey((k) => k + 1);
339
+ setPrefill((p) => ({ text: turn?.label || "", seq: p.seq + 1 }));
340
+ setStats(agentRef.current.stats());
341
+ },
342
+ onCancel: () => setOverlay(null)
343
+ }
297
344
  ) : null;
298
345
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
299
- /* @__PURE__ */ jsx(Static, { items, children: (item) => /* @__PURE__ */ jsx(LogItem, { item }, item.key) }),
346
+ /* @__PURE__ */ jsx(Static, { items, children: (item) => /* @__PURE__ */ jsx(LogItem, { item }, item.key) }, staticKey),
300
347
  streamText ? /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
301
348
  /* @__PURE__ */ jsxs(Text, { color: color.accent, children: [
302
349
  GLYPH.assistant,
@@ -322,7 +369,8 @@ OpenRouter credits: $${(credits.total_credits - credits.total_usage).toFixed(2)}
322
369
  onSubmit: handleSubmit,
323
370
  commands,
324
371
  active: !busy,
325
- history: historyRef.current
372
+ history: historyRef.current,
373
+ prefill
326
374
  }
327
375
  ),
328
376
  /* @__PURE__ */ jsx(Footer, { model, stats, yolo: yoloOn, cwd, exitHint })
package/dist/ui/Footer.js CHANGED
@@ -8,14 +8,16 @@ function Footer({ model, stats, yolo, cwd, exitHint }) {
8
8
  if (exitHint) {
9
9
  return /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: color.warn, children: "press ctrl+c again to exit" }) });
10
10
  }
11
- const pct = stats.contextLength ? Math.min(99, Math.round(stats.tokens / stats.contextLength * 100)) : 0;
11
+ const raw = stats.contextLength ? stats.tokens / stats.contextLength * 100 : 0;
12
+ const pctDisplay = raw < 1 ? raw.toFixed(1) : Math.min(99, Math.round(raw));
13
+ const pct = Math.min(99, raw);
12
14
  const pctColor = pct < 50 ? color.dim : pct < 80 ? color.warn : color.err;
13
15
  return /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
14
16
  /* @__PURE__ */ jsx(Text, { color: color.faint, children: model }),
15
17
  /* @__PURE__ */ jsx(Text, { color: color.faint, children: SEP }),
16
18
  /* @__PURE__ */ jsxs(Text, { color: pctColor, children: [
17
19
  "ctx ",
18
- pct,
20
+ pctDisplay,
19
21
  "%"
20
22
  ] }),
21
23
  /* @__PURE__ */ jsx(Text, { color: color.faint, children: SEP }),
@@ -1,13 +1,19 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
- import React, { useState } from "react";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useState } from "react";
3
3
  import { Box, Text, useInput } from "ink";
4
4
  import chalk from "chalk";
5
5
  import { color, hr } from "../theme.js";
6
- function InputBox({ onSubmit, commands, active, history }) {
6
+ function InputBox({ onSubmit, commands, active, history, prefill }) {
7
7
  const [value, setValue] = useState("");
8
8
  const [pos, setPos] = useState(0);
9
9
  const [histIdx, setHistIdx] = useState(-1);
10
10
  const [menuIdx, setMenuIdx] = useState(0);
11
+ useEffect(() => {
12
+ if (!prefill?.text) return;
13
+ setValue(prefill.text);
14
+ setPos(prefill.text.length);
15
+ setHistIdx(-1);
16
+ }, [prefill?.seq]);
11
17
  const menuOpen = active && value.startsWith("/") && !value.includes(" ");
12
18
  const matches = menuOpen ? commands.filter((c) => ("/" + c.name).startsWith(value.toLowerCase())) : [];
13
19
  const sel = matches.length ? Math.min(menuIdx, matches.length - 1) : 0;
@@ -39,7 +45,7 @@ function InputBox({ onSubmit, commands, active, history }) {
39
45
  return;
40
46
  }
41
47
  if (key.upArrow) {
42
- if (menuOpen && matches.length) return setMenuIdx((i) => (i + matches.length - 1) % matches.length);
48
+ if (menuOpen && matches.length) return setMenuIdx((i) => (i - 1 + matches.length) % matches.length);
43
49
  if (history.length) {
44
50
  const next = histIdx === -1 ? history.length - 1 : Math.max(0, histIdx - 1);
45
51
  setHistIdx(next);
@@ -97,14 +103,34 @@ function InputBox({ onSubmit, commands, active, history }) {
97
103
  { isActive: active }
98
104
  );
99
105
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
100
- menuOpen && matches.length > 0 && /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: matches.slice(0, 6).map((c, i) => /* @__PURE__ */ jsxs(Text, { children: [
101
- /* @__PURE__ */ jsxs(Text, { color: i === sel ? color.accent : color.dim, children: [
102
- i === sel ? "\u276F " : " ",
103
- "/",
104
- c.name.padEnd(14)
105
- ] }),
106
- /* @__PURE__ */ jsx(Text, { color: color.faint, children: c.desc })
107
- ] }, c.name)) }),
106
+ menuOpen && matches.length > 0 && /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: (() => {
107
+ const PAGE = 6;
108
+ const start = Math.min(Math.max(0, sel - PAGE + 1), Math.max(0, matches.length - PAGE));
109
+ const visible = matches.slice(start, start + PAGE);
110
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
111
+ start > 0 && /* @__PURE__ */ jsxs(Text, { color: color.faint, children: [
112
+ " \u2191 ",
113
+ start,
114
+ " more"
115
+ ] }),
116
+ visible.map((c, j) => {
117
+ const abs = start + j;
118
+ return /* @__PURE__ */ jsxs(Text, { children: [
119
+ /* @__PURE__ */ jsxs(Text, { color: abs === sel ? color.accent : color.dim, children: [
120
+ abs === sel ? "\u276F " : " ",
121
+ "/",
122
+ c.name.padEnd(14)
123
+ ] }),
124
+ /* @__PURE__ */ jsx(Text, { color: color.faint, children: c.desc })
125
+ ] }, c.name);
126
+ }),
127
+ start + PAGE < matches.length && /* @__PURE__ */ jsxs(Text, { color: color.faint, children: [
128
+ " \u2193 ",
129
+ matches.length - start - PAGE,
130
+ " more"
131
+ ] })
132
+ ] });
133
+ })() }),
108
134
  /* @__PURE__ */ jsx(Text, { color: color.faint, children: hr(2) }),
109
135
  /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
110
136
  /* @__PURE__ */ jsx(Text, { color: color.accent, children: "\u276F " }),
@@ -7,10 +7,11 @@ import Mascot from "./Mascot.js";
7
7
  function LogItem({ item }) {
8
8
  switch (item.kind) {
9
9
  case "banner":
10
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
10
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 2, children: [
11
11
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
12
12
  /* @__PURE__ */ jsx(Mascot, {}),
13
13
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
14
+ /* @__PURE__ */ jsx(Text, { children: " " }),
14
15
  /* @__PURE__ */ jsxs(Text, { children: [
15
16
  /* @__PURE__ */ jsx(Text, { bold: true, color: color.user, children: "Manix" }),
16
17
  /* @__PURE__ */ jsxs(Text, { color: color.faint, children: [
@@ -22,7 +23,7 @@ function LogItem({ item }) {
22
23
  /* @__PURE__ */ jsx(Text, { color: color.faint, children: item.cwd })
23
24
  ] })
24
25
  ] }),
25
- /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
26
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 1, children: [
26
27
  !item.hasManixmd && /* @__PURE__ */ jsxs(Text, { color: color.faint, children: [
27
28
  GLYPH.info,
28
29
  " MANIX.md not found \u2014 /init to create"
@@ -35,7 +36,7 @@ function LogItem({ item }) {
35
36
  ] })
36
37
  ] });
37
38
  case "user":
38
- return /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
39
+ return /* @__PURE__ */ jsxs(Box, { marginTop: 2, children: [
39
40
  /* @__PURE__ */ jsxs(Text, { color: color.accent, children: [
40
41
  GLYPH.user,
41
42
  " "
package/dist/ui/Mascot.js CHANGED
@@ -1,11 +1,20 @@
1
- import { jsx } from "react/jsx-runtime";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { Box, Text } from "ink";
4
- import { MASCOT, color, lerpColor } from "../theme.js";
4
+ import { color, lerpColor } from "../theme.js";
5
+ const ROWS = 5;
6
+ const lines = Array.from({ length: ROWS }, (_, i) => {
7
+ const glyphs = 2 * i + 1;
8
+ const pad = ROWS - 1 - i;
9
+ return { glyphs, pad };
10
+ });
5
11
  function Mascot() {
6
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginRight: 2, children: MASCOT.map((row, i) => {
7
- const c = lerpColor(color.accent, color.amber, MASCOT.length > 1 ? i / (MASCOT.length - 1) : 0);
8
- return /* @__PURE__ */ jsx(Text, { children: [...row].map((ch, j) => ch === "#" ? c("\u2588\u2588") : " ").join("") }, i);
12
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginRight: 2, children: lines.map(({ glyphs, pad }, i) => {
13
+ const c = lerpColor(color.accent, color.amber, i / (ROWS - 1));
14
+ return /* @__PURE__ */ jsxs(Text, { children: [
15
+ " ".repeat(pad),
16
+ c("\u25B2".repeat(glyphs))
17
+ ] }, i);
9
18
  }) });
10
19
  }
11
20
  export {
package/dist/ui/Picker.js CHANGED
@@ -17,16 +17,18 @@ function Picker({ title, load, onSelect, onCancel }) {
17
17
  const filtered = (items || []).filter(
18
18
  (it) => (it.label + " " + (it.extra || "")).toLowerCase().includes(q.toLowerCase())
19
19
  );
20
- const visible = filtered.slice(0, 10);
21
- const sel = visible.length ? Math.min(idx, visible.length - 1) : 0;
20
+ const PAGE = 10;
21
+ const sel = filtered.length ? (idx % filtered.length + filtered.length) % filtered.length : 0;
22
+ const start = Math.min(Math.max(0, sel - PAGE + 1), Math.max(0, filtered.length - PAGE));
23
+ const visible = filtered.slice(start, start + PAGE);
22
24
  useInput((input, key) => {
23
25
  if (key.escape) return onCancel();
24
26
  if (key.return) {
25
- if (visible.length) onSelect(visible[sel]);
27
+ if (filtered.length) onSelect(filtered[sel]);
26
28
  return;
27
29
  }
28
- if (key.upArrow) return setIdx(() => Math.max(0, sel - 1));
29
- if (key.downArrow) return setIdx(() => Math.min(visible.length - 1, sel + 1));
30
+ if (key.upArrow) return setIdx((i) => (i - 1 + filtered.length) % filtered.length);
31
+ if (key.downArrow) return setIdx((i) => (i + 1) % filtered.length);
30
32
  if (key.backspace || key.delete) {
31
33
  setQ((s) => s.slice(0, -1));
32
34
  setIdx(0);
@@ -46,17 +48,30 @@ function Picker({ title, load, onSelect, onCancel }) {
46
48
  ] }),
47
49
  error && /* @__PURE__ */ jsx(Text, { color: color.err, children: error }),
48
50
  !items && !error && /* @__PURE__ */ jsx(Text, { color: color.dim, children: "loading\u2026" }),
49
- visible.map((it, i) => /* @__PURE__ */ jsxs(Text, { children: [
50
- /* @__PURE__ */ jsxs(Text, { color: i === sel ? color.accent : color.user, children: [
51
- i === sel ? "\u276F " : " ",
52
- it.label
53
- ] }),
54
- it.extra ? /* @__PURE__ */ jsxs(Text, { color: color.dim, children: [
55
- " ",
56
- it.extra
57
- ] }) : null
58
- ] }, it.id ?? it.label)),
59
- items && !visible.length && /* @__PURE__ */ jsx(Text, { color: color.dim, children: "no matches" }),
51
+ start > 0 && /* @__PURE__ */ jsxs(Text, { color: color.faint, children: [
52
+ " \u2191 ",
53
+ start,
54
+ " more"
55
+ ] }),
56
+ visible.map((it) => {
57
+ const abs = filtered.indexOf(it);
58
+ return /* @__PURE__ */ jsxs(Text, { children: [
59
+ /* @__PURE__ */ jsxs(Text, { color: abs === sel ? color.accent : color.user, children: [
60
+ abs === sel ? "\u276F " : " ",
61
+ it.label
62
+ ] }),
63
+ it.extra ? /* @__PURE__ */ jsxs(Text, { color: color.dim, children: [
64
+ " ",
65
+ it.extra
66
+ ] }) : null
67
+ ] }, it.id ?? it.label);
68
+ }),
69
+ start + PAGE < filtered.length && /* @__PURE__ */ jsxs(Text, { color: color.faint, children: [
70
+ " \u2193 ",
71
+ filtered.length - start - PAGE,
72
+ " more"
73
+ ] }),
74
+ items && !filtered.length && /* @__PURE__ */ jsx(Text, { color: color.dim, children: "no matches" }),
60
75
  /* @__PURE__ */ jsxs(Text, { color: color.faint, children: [
61
76
  "\u2191\u2193 select \xB7 enter confirm \xB7 esc cancel \xB7 ",
62
77
  filtered.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manix-cli/manix",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Manix — a fast, beautiful terminal coding agent powered by OpenRouter. Any model, one key.",
5
5
  "type": "module",
6
6
  "bin": {