claude-think 0.4.1 → 0.4.2
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 +26 -0
- package/README.md +5 -4
- package/package.json +1 -1
- package/src/core/names.ts +52 -0
- package/src/core/parser.ts +17 -7
- package/src/tui/components/Agents.tsx +136 -11
- package/src/tui/components/Skills.tsx +135 -11
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.2] - 2025-02-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Smart project detection** - `think project` now detects:
|
|
12
|
+
- Monorepo tools (Turborepo, Nx, Lerna, pnpm/yarn/bun workspaces)
|
|
13
|
+
- Workspace structure with types (apps, packages, services)
|
|
14
|
+
- Frameworks from all workspaces (React, Tauri, Hono, Claude SDK, etc.)
|
|
15
|
+
- Tooling (Biome, Docker, TypeScript, Vite, Prisma, etc.)
|
|
16
|
+
- README tagline/description
|
|
17
|
+
- **Agent/Skill management in TUI**:
|
|
18
|
+
- `n` - create new with auto-generated name (e.g., "swift-falcon")
|
|
19
|
+
- `r` - rename selected item
|
|
20
|
+
- `d` - delete with confirmation
|
|
21
|
+
- Creative name generator for new agents/skills
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- YAML parsing errors no longer crash the TUI (graceful fallback)
|
|
25
|
+
- Long descriptions truncated in agent/skill lists
|
|
26
|
+
|
|
27
|
+
## [0.4.1] - 2025-02-06
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- Improved project detection for monorepos
|
|
31
|
+
- Detect runtime (Bun vs Node vs Deno)
|
|
32
|
+
- Detect frameworks from workspace dependencies
|
|
33
|
+
|
|
8
34
|
## [0.4.0] - 2025-02-05
|
|
9
35
|
|
|
10
36
|
### Added
|
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ Press `P` in the TUI to switch profiles interactively.
|
|
|
83
83
|
| `think edit <file>` | Edit any config file |
|
|
84
84
|
| `think allow "cmd"` | Pre-approve a command |
|
|
85
85
|
| `think tree` | Preview project file tree |
|
|
86
|
-
| `think project
|
|
86
|
+
| `think project` | Generate CLAUDE.md for current project |
|
|
87
87
|
| `think help` | Show all commands |
|
|
88
88
|
|
|
89
89
|
## TUI
|
|
@@ -94,13 +94,14 @@ Run `think` to launch the fullscreen TUI:
|
|
|
94
94
|
|-----|--------|
|
|
95
95
|
| `Tab` | Switch sections |
|
|
96
96
|
| `↑↓` / `jk` | Navigate / scroll |
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
97
|
+
| `e` | Edit selected item |
|
|
98
|
+
| `n` | New agent/skill |
|
|
99
|
+
| `r` | Rename agent/skill |
|
|
100
|
+
| `d` | Delete item |
|
|
99
101
|
| `a` | Quick actions (sync, learn, search) |
|
|
100
102
|
| `p` | Preview CLAUDE.md |
|
|
101
103
|
| `P` | Switch profile |
|
|
102
104
|
| `/` | Search all files |
|
|
103
|
-
| `e` | Open in $EDITOR |
|
|
104
105
|
| `?` | Help |
|
|
105
106
|
| `q` | Quit |
|
|
106
107
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate creative names like Claude Code does
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const adjectives = [
|
|
6
|
+
"swift", "bold", "calm", "bright", "quick", "sharp", "keen", "wise",
|
|
7
|
+
"agile", "clever", "steady", "nimble", "silent", "vivid", "subtle",
|
|
8
|
+
"curious", "daring", "eager", "gentle", "mighty", "patient", "precise",
|
|
9
|
+
"radiant", "serene", "vigilant", "witty", "zesty", "cosmic", "lunar",
|
|
10
|
+
"stellar", "amber", "azure", "coral", "crimson", "golden", "jade",
|
|
11
|
+
"ruby", "silver", "violet", "rustic", "urban", "ancient", "modern",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const nouns = [
|
|
15
|
+
"falcon", "phoenix", "tiger", "wolf", "hawk", "raven", "fox", "owl",
|
|
16
|
+
"dragon", "lion", "eagle", "bear", "panther", "viper", "cobra", "shark",
|
|
17
|
+
"storm", "thunder", "spark", "flame", "frost", "wave", "wind", "shadow",
|
|
18
|
+
"nova", "comet", "nebula", "quasar", "pulsar", "orbit", "vertex", "prism",
|
|
19
|
+
"cipher", "beacon", "sentinel", "guardian", "pilot", "scout", "ranger",
|
|
20
|
+
"forge", "anvil", "blade", "arrow", "shield", "helm", "crown", "torch",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function randomFrom<T>(arr: T[]): T {
|
|
24
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate a unique name like "swift-falcon" or "bold-phoenix"
|
|
29
|
+
*/
|
|
30
|
+
export function generateName(): string {
|
|
31
|
+
return `${randomFrom(adjectives)}-${randomFrom(nouns)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate a unique filename, checking for conflicts
|
|
36
|
+
*/
|
|
37
|
+
export function generateUniqueFilename(existingNames: string[], extension = ".md"): string {
|
|
38
|
+
let name: string;
|
|
39
|
+
let attempts = 0;
|
|
40
|
+
|
|
41
|
+
do {
|
|
42
|
+
name = generateName();
|
|
43
|
+
attempts++;
|
|
44
|
+
} while (existingNames.includes(name) && attempts < 100);
|
|
45
|
+
|
|
46
|
+
// If we somehow hit 100 collisions, add a number
|
|
47
|
+
if (existingNames.includes(name)) {
|
|
48
|
+
name = `${name}-${Date.now() % 1000}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return `${name}${extension}`;
|
|
52
|
+
}
|
package/src/core/parser.ts
CHANGED
|
@@ -16,14 +16,24 @@ export async function parseMarkdown(filePath: string): Promise<ParsedFile | null
|
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
try {
|
|
20
|
+
const raw = await readFile(filePath, "utf-8");
|
|
21
|
+
const { data, content } = matter(raw);
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
return {
|
|
24
|
+
frontmatter: data,
|
|
25
|
+
content: content.trim(),
|
|
26
|
+
raw,
|
|
27
|
+
};
|
|
28
|
+
} catch {
|
|
29
|
+
// If YAML parsing fails, return file as content-only
|
|
30
|
+
const raw = await readFile(filePath, "utf-8");
|
|
31
|
+
return {
|
|
32
|
+
frontmatter: {},
|
|
33
|
+
content: raw.trim(),
|
|
34
|
+
raw,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
/**
|
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
|
-
import
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import { readdir, rename, unlink } from "fs/promises";
|
|
4
5
|
import { existsSync } from "fs";
|
|
5
6
|
import { thinkPath, CONFIG } from "../../core/config";
|
|
6
7
|
import { parseMarkdown } from "../../core/parser";
|
|
8
|
+
import { generateUniqueFilename } from "../../core/names";
|
|
7
9
|
import { spawn } from "child_process";
|
|
8
|
-
import { join } from "path";
|
|
10
|
+
import { join, basename } from "path";
|
|
9
11
|
|
|
10
12
|
interface AgentInfo {
|
|
11
13
|
name: string;
|
|
12
14
|
description: string;
|
|
13
15
|
path: string;
|
|
16
|
+
filename: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
interface AgentsProps {
|
|
17
20
|
height?: number;
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
type Mode = "list" | "rename" | "confirmDelete";
|
|
24
|
+
|
|
20
25
|
export function Agents({ height = 15 }: AgentsProps) {
|
|
21
26
|
const [agents, setAgents] = useState<AgentInfo[]>([]);
|
|
22
27
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
23
28
|
const [loading, setLoading] = useState(true);
|
|
29
|
+
const [mode, setMode] = useState<Mode>("list");
|
|
30
|
+
const [renameValue, setRenameValue] = useState("");
|
|
24
31
|
|
|
25
32
|
useEffect(() => {
|
|
26
33
|
loadAgents();
|
|
@@ -42,19 +49,84 @@ export function Agents({ height = 15 }: AgentsProps) {
|
|
|
42
49
|
const agentInfos: AgentInfo[] = [];
|
|
43
50
|
for (const file of agentFiles) {
|
|
44
51
|
const path = join(agentsDir, file);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
try {
|
|
53
|
+
const parsed = await parseMarkdown(path);
|
|
54
|
+
agentInfos.push({
|
|
55
|
+
name: (parsed?.frontmatter?.name as string) || file.replace(".md", ""),
|
|
56
|
+
description: (parsed?.frontmatter?.description as string) || "",
|
|
57
|
+
path,
|
|
58
|
+
filename: file,
|
|
59
|
+
});
|
|
60
|
+
} catch {
|
|
61
|
+
agentInfos.push({
|
|
62
|
+
name: file.replace(".md", ""),
|
|
63
|
+
description: "",
|
|
64
|
+
path,
|
|
65
|
+
filename: file,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
51
68
|
}
|
|
52
69
|
|
|
53
70
|
setAgents(agentInfos);
|
|
54
71
|
setLoading(false);
|
|
55
72
|
}
|
|
56
73
|
|
|
74
|
+
async function handleRename() {
|
|
75
|
+
const agent = agents[selectedIndex];
|
|
76
|
+
if (!agent || !renameValue.trim()) return;
|
|
77
|
+
|
|
78
|
+
const newFilename = renameValue.trim().endsWith(".md")
|
|
79
|
+
? renameValue.trim()
|
|
80
|
+
: `${renameValue.trim()}.md`;
|
|
81
|
+
const newPath = join(thinkPath(CONFIG.dirs.agents), newFilename);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await rename(agent.path, newPath);
|
|
85
|
+
setMode("list");
|
|
86
|
+
setRenameValue("");
|
|
87
|
+
await loadAgents();
|
|
88
|
+
} catch {
|
|
89
|
+
// Failed to rename
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function handleDelete() {
|
|
94
|
+
const agent = agents[selectedIndex];
|
|
95
|
+
if (!agent) return;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await unlink(agent.path);
|
|
99
|
+
setMode("list");
|
|
100
|
+
await loadAgents();
|
|
101
|
+
if (selectedIndex >= agents.length - 1) {
|
|
102
|
+
setSelectedIndex(Math.max(0, agents.length - 2));
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Failed to delete
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
57
109
|
useInput((input, key) => {
|
|
110
|
+
// Rename mode
|
|
111
|
+
if (mode === "rename") {
|
|
112
|
+
if (key.escape) {
|
|
113
|
+
setMode("list");
|
|
114
|
+
setRenameValue("");
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Confirm delete mode
|
|
120
|
+
if (mode === "confirmDelete") {
|
|
121
|
+
if (input === "y" || input === "Y") {
|
|
122
|
+
handleDelete();
|
|
123
|
+
} else {
|
|
124
|
+
setMode("list");
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// List mode
|
|
58
130
|
if (key.upArrow || input === "k") {
|
|
59
131
|
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
60
132
|
}
|
|
@@ -71,19 +143,72 @@ export function Agents({ height = 15 }: AgentsProps) {
|
|
|
71
143
|
}
|
|
72
144
|
if (input === "n") {
|
|
73
145
|
const editor = process.env.EDITOR || "vi";
|
|
74
|
-
const
|
|
146
|
+
const existingNames = agents.map((a) => a.filename.replace(".md", ""));
|
|
147
|
+
const newFilename = generateUniqueFilename(existingNames);
|
|
148
|
+
const newPath = thinkPath(CONFIG.dirs.agents, newFilename);
|
|
75
149
|
spawn(editor, [newPath], {
|
|
76
150
|
stdio: "inherit",
|
|
77
151
|
}).on("exit", () => {
|
|
78
152
|
loadAgents();
|
|
79
153
|
});
|
|
80
154
|
}
|
|
155
|
+
if (input === "r" && agents[selectedIndex]) {
|
|
156
|
+
setRenameValue(agents[selectedIndex].filename.replace(".md", ""));
|
|
157
|
+
setMode("rename");
|
|
158
|
+
}
|
|
159
|
+
if (input === "d" && agents[selectedIndex]) {
|
|
160
|
+
setMode("confirmDelete");
|
|
161
|
+
}
|
|
81
162
|
});
|
|
82
163
|
|
|
83
164
|
if (loading) {
|
|
84
165
|
return <Text color="gray">Loading...</Text>;
|
|
85
166
|
}
|
|
86
167
|
|
|
168
|
+
// Rename mode
|
|
169
|
+
if (mode === "rename") {
|
|
170
|
+
return (
|
|
171
|
+
<Box flexDirection="column">
|
|
172
|
+
<Box marginBottom={1}>
|
|
173
|
+
<Text bold color="green">Rename Agent</Text>
|
|
174
|
+
</Box>
|
|
175
|
+
<Box>
|
|
176
|
+
<Text color="cyan">New name: </Text>
|
|
177
|
+
<TextInput
|
|
178
|
+
value={renameValue}
|
|
179
|
+
onChange={setRenameValue}
|
|
180
|
+
onSubmit={handleRename}
|
|
181
|
+
placeholder="agent-name"
|
|
182
|
+
/>
|
|
183
|
+
</Box>
|
|
184
|
+
<Box marginTop={1}>
|
|
185
|
+
<Text color="gray">Enter: save | Esc: cancel</Text>
|
|
186
|
+
</Box>
|
|
187
|
+
</Box>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Confirm delete mode
|
|
192
|
+
if (mode === "confirmDelete") {
|
|
193
|
+
const agent = agents[selectedIndex];
|
|
194
|
+
return (
|
|
195
|
+
<Box flexDirection="column">
|
|
196
|
+
<Box marginBottom={1}>
|
|
197
|
+
<Text bold color="red">Delete Agent</Text>
|
|
198
|
+
</Box>
|
|
199
|
+
<Box>
|
|
200
|
+
<Text>Delete </Text>
|
|
201
|
+
<Text color="cyan" bold>"{agent?.name}"</Text>
|
|
202
|
+
<Text>?</Text>
|
|
203
|
+
</Box>
|
|
204
|
+
<Box marginTop={1}>
|
|
205
|
+
<Text color="red" bold>y</Text>
|
|
206
|
+
<Text color="gray">: delete | any key: cancel</Text>
|
|
207
|
+
</Box>
|
|
208
|
+
</Box>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
87
212
|
return (
|
|
88
213
|
<Box flexDirection="column">
|
|
89
214
|
<Box marginBottom={1}>
|
|
@@ -106,7 +231,7 @@ export function Agents({ height = 15 }: AgentsProps) {
|
|
|
106
231
|
{agent.name}
|
|
107
232
|
</Text>
|
|
108
233
|
{agent.description && (
|
|
109
|
-
<Text color="gray"> - {agent.description}</Text>
|
|
234
|
+
<Text color="gray"> - {agent.description.slice(0, 50)}{agent.description.length > 50 ? "..." : ""}</Text>
|
|
110
235
|
)}
|
|
111
236
|
</Box>
|
|
112
237
|
))}
|
|
@@ -114,7 +239,7 @@ export function Agents({ height = 15 }: AgentsProps) {
|
|
|
114
239
|
)}
|
|
115
240
|
|
|
116
241
|
<Box marginTop={1}>
|
|
117
|
-
<Text color="gray"
|
|
242
|
+
<Text color="gray">↑↓: select | e: edit | n: new | r: rename | d: delete</Text>
|
|
118
243
|
</Box>
|
|
119
244
|
</Box>
|
|
120
245
|
);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
|
-
import
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import { readdir, rename, unlink } from "fs/promises";
|
|
4
5
|
import { existsSync } from "fs";
|
|
5
6
|
import { thinkPath, CONFIG } from "../../core/config";
|
|
6
7
|
import { parseMarkdown } from "../../core/parser";
|
|
8
|
+
import { generateUniqueFilename } from "../../core/names";
|
|
7
9
|
import { spawn } from "child_process";
|
|
8
10
|
import { join } from "path";
|
|
9
11
|
|
|
@@ -11,16 +13,21 @@ interface SkillInfo {
|
|
|
11
13
|
name: string;
|
|
12
14
|
description: string;
|
|
13
15
|
path: string;
|
|
16
|
+
filename: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
interface SkillsProps {
|
|
17
20
|
height?: number;
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
type Mode = "list" | "rename" | "confirmDelete";
|
|
24
|
+
|
|
20
25
|
export function Skills({ height = 15 }: SkillsProps) {
|
|
21
26
|
const [skills, setSkills] = useState<SkillInfo[]>([]);
|
|
22
27
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
23
28
|
const [loading, setLoading] = useState(true);
|
|
29
|
+
const [mode, setMode] = useState<Mode>("list");
|
|
30
|
+
const [renameValue, setRenameValue] = useState("");
|
|
24
31
|
|
|
25
32
|
useEffect(() => {
|
|
26
33
|
loadSkills();
|
|
@@ -42,19 +49,84 @@ export function Skills({ height = 15 }: SkillsProps) {
|
|
|
42
49
|
const skillInfos: SkillInfo[] = [];
|
|
43
50
|
for (const file of skillFiles) {
|
|
44
51
|
const path = join(skillsDir, file);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
try {
|
|
53
|
+
const parsed = await parseMarkdown(path);
|
|
54
|
+
skillInfos.push({
|
|
55
|
+
name: (parsed?.frontmatter?.name as string) || file.replace(".md", ""),
|
|
56
|
+
description: (parsed?.frontmatter?.description as string) || "",
|
|
57
|
+
path,
|
|
58
|
+
filename: file,
|
|
59
|
+
});
|
|
60
|
+
} catch {
|
|
61
|
+
skillInfos.push({
|
|
62
|
+
name: file.replace(".md", ""),
|
|
63
|
+
description: "",
|
|
64
|
+
path,
|
|
65
|
+
filename: file,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
51
68
|
}
|
|
52
69
|
|
|
53
70
|
setSkills(skillInfos);
|
|
54
71
|
setLoading(false);
|
|
55
72
|
}
|
|
56
73
|
|
|
74
|
+
async function handleRename() {
|
|
75
|
+
const skill = skills[selectedIndex];
|
|
76
|
+
if (!skill || !renameValue.trim()) return;
|
|
77
|
+
|
|
78
|
+
const newFilename = renameValue.trim().endsWith(".md")
|
|
79
|
+
? renameValue.trim()
|
|
80
|
+
: `${renameValue.trim()}.md`;
|
|
81
|
+
const newPath = join(thinkPath(CONFIG.dirs.skills), newFilename);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await rename(skill.path, newPath);
|
|
85
|
+
setMode("list");
|
|
86
|
+
setRenameValue("");
|
|
87
|
+
await loadSkills();
|
|
88
|
+
} catch {
|
|
89
|
+
// Failed to rename
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function handleDelete() {
|
|
94
|
+
const skill = skills[selectedIndex];
|
|
95
|
+
if (!skill) return;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await unlink(skill.path);
|
|
99
|
+
setMode("list");
|
|
100
|
+
await loadSkills();
|
|
101
|
+
if (selectedIndex >= skills.length - 1) {
|
|
102
|
+
setSelectedIndex(Math.max(0, skills.length - 2));
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Failed to delete
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
57
109
|
useInput((input, key) => {
|
|
110
|
+
// Rename mode
|
|
111
|
+
if (mode === "rename") {
|
|
112
|
+
if (key.escape) {
|
|
113
|
+
setMode("list");
|
|
114
|
+
setRenameValue("");
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Confirm delete mode
|
|
120
|
+
if (mode === "confirmDelete") {
|
|
121
|
+
if (input === "y" || input === "Y") {
|
|
122
|
+
handleDelete();
|
|
123
|
+
} else {
|
|
124
|
+
setMode("list");
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// List mode
|
|
58
130
|
if (key.upArrow || input === "k") {
|
|
59
131
|
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
60
132
|
}
|
|
@@ -70,21 +142,73 @@ export function Skills({ height = 15 }: SkillsProps) {
|
|
|
70
142
|
});
|
|
71
143
|
}
|
|
72
144
|
if (input === "n") {
|
|
73
|
-
// Create new skill
|
|
74
145
|
const editor = process.env.EDITOR || "vi";
|
|
75
|
-
const
|
|
146
|
+
const existingNames = skills.map((s) => s.filename.replace(".md", ""));
|
|
147
|
+
const newFilename = generateUniqueFilename(existingNames);
|
|
148
|
+
const newPath = thinkPath(CONFIG.dirs.skills, newFilename);
|
|
76
149
|
spawn(editor, [newPath], {
|
|
77
150
|
stdio: "inherit",
|
|
78
151
|
}).on("exit", () => {
|
|
79
152
|
loadSkills();
|
|
80
153
|
});
|
|
81
154
|
}
|
|
155
|
+
if (input === "r" && skills[selectedIndex]) {
|
|
156
|
+
setRenameValue(skills[selectedIndex].filename.replace(".md", ""));
|
|
157
|
+
setMode("rename");
|
|
158
|
+
}
|
|
159
|
+
if (input === "d" && skills[selectedIndex]) {
|
|
160
|
+
setMode("confirmDelete");
|
|
161
|
+
}
|
|
82
162
|
});
|
|
83
163
|
|
|
84
164
|
if (loading) {
|
|
85
165
|
return <Text color="gray">Loading...</Text>;
|
|
86
166
|
}
|
|
87
167
|
|
|
168
|
+
// Rename mode
|
|
169
|
+
if (mode === "rename") {
|
|
170
|
+
return (
|
|
171
|
+
<Box flexDirection="column">
|
|
172
|
+
<Box marginBottom={1}>
|
|
173
|
+
<Text bold color="green">Rename Skill</Text>
|
|
174
|
+
</Box>
|
|
175
|
+
<Box>
|
|
176
|
+
<Text color="cyan">New name: </Text>
|
|
177
|
+
<TextInput
|
|
178
|
+
value={renameValue}
|
|
179
|
+
onChange={setRenameValue}
|
|
180
|
+
onSubmit={handleRename}
|
|
181
|
+
placeholder="skill-name"
|
|
182
|
+
/>
|
|
183
|
+
</Box>
|
|
184
|
+
<Box marginTop={1}>
|
|
185
|
+
<Text color="gray">Enter: save | Esc: cancel</Text>
|
|
186
|
+
</Box>
|
|
187
|
+
</Box>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Confirm delete mode
|
|
192
|
+
if (mode === "confirmDelete") {
|
|
193
|
+
const skill = skills[selectedIndex];
|
|
194
|
+
return (
|
|
195
|
+
<Box flexDirection="column">
|
|
196
|
+
<Box marginBottom={1}>
|
|
197
|
+
<Text bold color="red">Delete Skill</Text>
|
|
198
|
+
</Box>
|
|
199
|
+
<Box>
|
|
200
|
+
<Text>Delete </Text>
|
|
201
|
+
<Text color="cyan" bold>"{skill?.name}"</Text>
|
|
202
|
+
<Text>?</Text>
|
|
203
|
+
</Box>
|
|
204
|
+
<Box marginTop={1}>
|
|
205
|
+
<Text color="red" bold>y</Text>
|
|
206
|
+
<Text color="gray">: delete | any key: cancel</Text>
|
|
207
|
+
</Box>
|
|
208
|
+
</Box>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
88
212
|
return (
|
|
89
213
|
<Box flexDirection="column">
|
|
90
214
|
<Box marginBottom={1}>
|
|
@@ -107,7 +231,7 @@ export function Skills({ height = 15 }: SkillsProps) {
|
|
|
107
231
|
{skill.name}
|
|
108
232
|
</Text>
|
|
109
233
|
{skill.description && (
|
|
110
|
-
<Text color="gray"> - {skill.description}</Text>
|
|
234
|
+
<Text color="gray"> - {skill.description.slice(0, 50)}{skill.description.length > 50 ? "..." : ""}</Text>
|
|
111
235
|
)}
|
|
112
236
|
</Box>
|
|
113
237
|
))}
|
|
@@ -115,7 +239,7 @@ export function Skills({ height = 15 }: SkillsProps) {
|
|
|
115
239
|
)}
|
|
116
240
|
|
|
117
241
|
<Box marginTop={1}>
|
|
118
|
-
<Text color="gray"
|
|
242
|
+
<Text color="gray">↑↓: select | e: edit | n: new | r: rename | d: delete</Text>
|
|
119
243
|
</Box>
|
|
120
244
|
</Box>
|
|
121
245
|
);
|