@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 +8 -5
- package/dist/agent.js +83 -2
- package/dist/cli.js +12 -1
- package/dist/markdown.js +18 -4
- package/dist/slash.js +1 -0
- package/dist/ui/App.js +52 -4
- package/dist/ui/Footer.js +4 -2
- package/dist/ui/InputBox.js +38 -12
- package/dist/ui/LogItem.js +4 -3
- package/dist/ui/Mascot.js +14 -5
- package/dist/ui/Picker.js +31 -16
- package/package.json +1 -1
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
|
-
| `/
|
|
49
|
-
| `/
|
|
50
|
-
| `/
|
|
51
|
-
| `/
|
|
52
|
-
| `/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
20
|
+
pctDisplay,
|
|
19
21
|
"%"
|
|
20
22
|
] }),
|
|
21
23
|
/* @__PURE__ */ jsx(Text, { color: color.faint, children: SEP }),
|
package/dist/ui/InputBox.js
CHANGED
|
@@ -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
|
|
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:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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 " }),
|
package/dist/ui/LogItem.js
CHANGED
|
@@ -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",
|
|
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,
|
|
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:
|
|
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 {
|
|
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:
|
|
7
|
-
const c = lerpColor(color.accent, color.amber,
|
|
8
|
-
return /* @__PURE__ */
|
|
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
|
|
21
|
-
const sel =
|
|
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 (
|
|
27
|
+
if (filtered.length) onSelect(filtered[sel]);
|
|
26
28
|
return;
|
|
27
29
|
}
|
|
28
|
-
if (key.upArrow) return setIdx(() =>
|
|
29
|
-
if (key.downArrow) return setIdx(() =>
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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,
|