claude-think 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/CHANGELOG.md +16 -0
- package/README.md +4 -3
- package/bun.lock +3 -0
- package/package.json +2 -1
- package/src/tui/App.tsx +103 -16
- package/src/tui/components/Help.tsx +58 -28
- package/src/tui/components/Preview.tsx +86 -0
- package/src/tui/components/QuickActions.tsx +155 -0
- package/src/tui/components/Search.tsx +129 -0
- package/src/tui/components/StatusBar.tsx +80 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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.3.0] - 2025-02-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Enhanced TUI with new features:
|
|
12
|
+
- **Status bar** - shows learnings count, pending count, last sync time
|
|
13
|
+
- **Quick actions menu** (press `a`) - sync, add learning, search
|
|
14
|
+
- **Preview panel** (press `p`) - view generated CLAUDE.md with scrolling
|
|
15
|
+
- **Search** (press `/`) - search across all config files
|
|
16
|
+
- **Better help screen** (press `?`) - organized shortcuts and CLI commands
|
|
17
|
+
- New keyboard shortcuts: `a` actions, `p` preview, `/` search, `Ctrl+S` sync
|
|
18
|
+
- Bordered content panels for cleaner visual design
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- TUI now uses modals for actions, preview, search, and help
|
|
22
|
+
- Improved navigation hints in footer
|
|
23
|
+
|
|
8
24
|
## [0.2.1] - 2025-02-05
|
|
9
25
|
|
|
10
26
|
### Added
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Personal context manager for Claude. Stop repeating yourself.
|
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
# With bun (recommended)
|
|
28
|
-
bun
|
|
28
|
+
bun add -g claude-think
|
|
29
29
|
|
|
30
30
|
# With npm
|
|
31
31
|
npm install -g claude-think
|
|
@@ -47,8 +47,8 @@ claude
|
|
|
47
47
|
## How it works
|
|
48
48
|
|
|
49
49
|
1. Your preferences live in `~/.think/` (markdown files)
|
|
50
|
-
2. `think sync` generates
|
|
51
|
-
3. Claude Code auto-loads
|
|
50
|
+
2. `think sync` generates `~/.claude/CLAUDE.md`
|
|
51
|
+
3. Claude Code auto-loads CLAUDE.md at session start
|
|
52
52
|
|
|
53
53
|
## Commands
|
|
54
54
|
|
|
@@ -65,6 +65,7 @@ claude
|
|
|
65
65
|
| `think edit <file>` | Edit any config file |
|
|
66
66
|
| `think allow "cmd"` | Pre-approve a command |
|
|
67
67
|
| `think tree` | Preview project file tree |
|
|
68
|
+
| `think project learn` | Generate CLAUDE.md for current project |
|
|
68
69
|
| `think help` | Show all commands |
|
|
69
70
|
|
|
70
71
|
## What you can configure
|
package/bun.lock
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"commander": "^14.0.3",
|
|
11
11
|
"gray-matter": "^4.0.3",
|
|
12
12
|
"ink": "^6.6.0",
|
|
13
|
+
"ink-text-input": "^6.0.0",
|
|
13
14
|
"react": "^19.2.4",
|
|
14
15
|
"string-similarity": "^4.0.4",
|
|
15
16
|
},
|
|
@@ -86,6 +87,8 @@
|
|
|
86
87
|
|
|
87
88
|
"ink": ["ink@6.6.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.2.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^8.1.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-QDt6FgJxgmSxAelcOvOHUvFxbIUjVpCH5bx+Slvc5m7IEcpGt3dYwbz/L+oRnqEGeRvwy1tineKK4ect3nW1vQ=="],
|
|
88
89
|
|
|
90
|
+
"ink-text-input": ["ink-text-input@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "type-fest": "^4.18.2" }, "peerDependencies": { "ink": ">=5", "react": ">=18" } }, "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw=="],
|
|
91
|
+
|
|
89
92
|
"is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
|
90
93
|
|
|
91
94
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-think",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Personal context manager for Claude - manage your preferences, patterns, and memory",
|
|
5
5
|
"author": "Amit Feldman",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"commander": "^14.0.3",
|
|
33
33
|
"gray-matter": "^4.0.3",
|
|
34
34
|
"ink": "^6.6.0",
|
|
35
|
+
"ink-text-input": "^6.0.0",
|
|
35
36
|
"react": "^19.2.4",
|
|
36
37
|
"string-similarity": "^4.0.4"
|
|
37
38
|
}
|
package/src/tui/App.tsx
CHANGED
|
@@ -9,9 +9,14 @@ import { Skills } from "./components/Skills";
|
|
|
9
9
|
import { Agents } from "./components/Agents";
|
|
10
10
|
import { Automation } from "./components/Automation";
|
|
11
11
|
import { Help } from "./components/Help";
|
|
12
|
+
import { StatusBar } from "./components/StatusBar";
|
|
13
|
+
import { QuickActions } from "./components/QuickActions";
|
|
14
|
+
import { Preview } from "./components/Preview";
|
|
15
|
+
import { Search } from "./components/Search";
|
|
12
16
|
import { existsSync } from "fs";
|
|
13
17
|
import { CONFIG } from "../core/config";
|
|
14
18
|
|
|
19
|
+
// Note: "help" is handled as a modal, not a navigation section
|
|
15
20
|
type Section =
|
|
16
21
|
| "profile"
|
|
17
22
|
| "preferences"
|
|
@@ -19,13 +24,15 @@ type Section =
|
|
|
19
24
|
| "permissions"
|
|
20
25
|
| "skills"
|
|
21
26
|
| "agents"
|
|
22
|
-
| "automation"
|
|
23
|
-
|
|
27
|
+
| "automation";
|
|
28
|
+
|
|
29
|
+
type Modal = "none" | "help" | "actions" | "preview" | "search";
|
|
24
30
|
|
|
25
31
|
export function App() {
|
|
26
32
|
const { exit } = useApp();
|
|
27
33
|
const [section, setSection] = useState<Section>("profile");
|
|
28
|
-
const [
|
|
34
|
+
const [modal, setModal] = useState<Modal>("none");
|
|
35
|
+
const [statusMessage, setStatusMessage] = useState<string | undefined>();
|
|
29
36
|
const [initialized, setInitialized] = useState(false);
|
|
30
37
|
|
|
31
38
|
useEffect(() => {
|
|
@@ -33,25 +40,88 @@ export function App() {
|
|
|
33
40
|
}, []);
|
|
34
41
|
|
|
35
42
|
useInput((input, key) => {
|
|
43
|
+
// Global shortcuts (when no modal open)
|
|
44
|
+
if (modal !== "none") return;
|
|
45
|
+
|
|
36
46
|
if (input === "q" || (key.ctrl && input === "c")) {
|
|
37
47
|
exit();
|
|
38
48
|
}
|
|
39
49
|
if (input === "?") {
|
|
40
|
-
|
|
50
|
+
setModal("help");
|
|
51
|
+
}
|
|
52
|
+
if (key.ctrl && input === "s") {
|
|
53
|
+
setModal("actions");
|
|
54
|
+
}
|
|
55
|
+
if (input === "a") {
|
|
56
|
+
setModal("actions");
|
|
57
|
+
}
|
|
58
|
+
if (input === "p") {
|
|
59
|
+
setModal("preview");
|
|
60
|
+
}
|
|
61
|
+
if (input === "/" || (key.ctrl && input === "f")) {
|
|
62
|
+
setModal("search");
|
|
41
63
|
}
|
|
42
64
|
});
|
|
43
65
|
|
|
66
|
+
function handleMessage(msg: string) {
|
|
67
|
+
setStatusMessage(msg);
|
|
68
|
+
setTimeout(() => setStatusMessage(undefined), 3000);
|
|
69
|
+
}
|
|
70
|
+
|
|
44
71
|
if (!initialized) {
|
|
45
72
|
return (
|
|
46
73
|
<Box flexDirection="column" padding={1}>
|
|
47
|
-
<
|
|
48
|
-
|
|
74
|
+
<Box
|
|
75
|
+
borderStyle="round"
|
|
76
|
+
borderColor="red"
|
|
77
|
+
paddingX={2}
|
|
78
|
+
paddingY={1}
|
|
79
|
+
flexDirection="column"
|
|
80
|
+
>
|
|
81
|
+
<Text color="red" bold>
|
|
82
|
+
~/.think not found
|
|
83
|
+
</Text>
|
|
84
|
+
<Box marginTop={1}>
|
|
85
|
+
<Text color="gray">Run `think init` first, then launch the TUI.</Text>
|
|
86
|
+
</Box>
|
|
87
|
+
</Box>
|
|
88
|
+
</Box>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Render modals
|
|
93
|
+
if (modal === "help") {
|
|
94
|
+
return <Help onClose={() => setModal("none")} />;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (modal === "actions") {
|
|
98
|
+
return (
|
|
99
|
+
<Box flexDirection="column" padding={1}>
|
|
100
|
+
<Header />
|
|
101
|
+
<QuickActions
|
|
102
|
+
onMessage={handleMessage}
|
|
103
|
+
onClose={() => setModal("none")}
|
|
104
|
+
/>
|
|
105
|
+
</Box>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (modal === "preview") {
|
|
110
|
+
return (
|
|
111
|
+
<Box flexDirection="column" padding={1}>
|
|
112
|
+
<Header />
|
|
113
|
+
<Preview onClose={() => setModal("none")} />
|
|
49
114
|
</Box>
|
|
50
115
|
);
|
|
51
116
|
}
|
|
52
117
|
|
|
53
|
-
if (
|
|
54
|
-
return
|
|
118
|
+
if (modal === "search") {
|
|
119
|
+
return (
|
|
120
|
+
<Box flexDirection="column" padding={1}>
|
|
121
|
+
<Header />
|
|
122
|
+
<Search onClose={() => setModal("none")} />
|
|
123
|
+
</Box>
|
|
124
|
+
);
|
|
55
125
|
}
|
|
56
126
|
|
|
57
127
|
const renderSection = () => {
|
|
@@ -77,24 +147,41 @@ export function App() {
|
|
|
77
147
|
|
|
78
148
|
return (
|
|
79
149
|
<Box flexDirection="column">
|
|
80
|
-
<
|
|
81
|
-
<Text color="green" bold>
|
|
82
|
-
▀█▀ █░█ █ █▄░█ █▄▀
|
|
83
|
-
</Text>
|
|
84
|
-
<Text color="gray"> Personal Context for Claude</Text>
|
|
85
|
-
</Box>
|
|
150
|
+
<Header />
|
|
86
151
|
|
|
87
152
|
<Navigation currentSection={section} onSectionChange={setSection} />
|
|
88
153
|
|
|
89
|
-
<Box
|
|
154
|
+
<Box
|
|
155
|
+
flexDirection="column"
|
|
156
|
+
marginTop={1}
|
|
157
|
+
borderStyle="single"
|
|
158
|
+
borderColor="gray"
|
|
159
|
+
padding={1}
|
|
160
|
+
minHeight={15}
|
|
161
|
+
>
|
|
90
162
|
{renderSection()}
|
|
91
163
|
</Box>
|
|
92
164
|
|
|
165
|
+
<Box marginTop={1}>
|
|
166
|
+
<StatusBar message={statusMessage} />
|
|
167
|
+
</Box>
|
|
168
|
+
|
|
93
169
|
<Box marginTop={1}>
|
|
94
170
|
<Text color="gray">
|
|
95
|
-
Tab:
|
|
171
|
+
Tab: sections | a: actions | p: preview | /: search | ?: help | q: quit
|
|
96
172
|
</Text>
|
|
97
173
|
</Box>
|
|
98
174
|
</Box>
|
|
99
175
|
);
|
|
100
176
|
}
|
|
177
|
+
|
|
178
|
+
function Header() {
|
|
179
|
+
return (
|
|
180
|
+
<Box marginBottom={1}>
|
|
181
|
+
<Text color="green" bold>
|
|
182
|
+
▀█▀ █░█ █ █▄░█ █▄▀
|
|
183
|
+
</Text>
|
|
184
|
+
<Text color="gray"> Personal Context for Claude</Text>
|
|
185
|
+
</Box>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -16,40 +16,70 @@ export function Help({ onClose }: HelpProps) {
|
|
|
16
16
|
<Box flexDirection="column" padding={1}>
|
|
17
17
|
<Box marginBottom={1}>
|
|
18
18
|
<Text bold color="green">
|
|
19
|
-
|
|
19
|
+
▀█▀ █░█ █ █▄░█ █▄▀
|
|
20
20
|
</Text>
|
|
21
|
+
<Text color="gray"> Help</Text>
|
|
21
22
|
</Box>
|
|
22
23
|
|
|
23
|
-
<Box
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
24
|
+
<Box
|
|
25
|
+
flexDirection="column"
|
|
26
|
+
borderStyle="single"
|
|
27
|
+
borderColor="gray"
|
|
28
|
+
padding={1}
|
|
29
|
+
>
|
|
30
|
+
<Box flexDirection="row">
|
|
31
|
+
<Box flexDirection="column" marginRight={4}>
|
|
32
|
+
<Text bold color="cyan">Navigation</Text>
|
|
33
|
+
<Text><Text color="green">Tab</Text> Switch sections</Text>
|
|
34
|
+
<Text><Text color="green">1-7</Text> Jump to section</Text>
|
|
35
|
+
<Text><Text color="green">←/→</Text> Sub-sections</Text>
|
|
36
|
+
<Text><Text color="green">↑/↓</Text> Select item</Text>
|
|
37
|
+
</Box>
|
|
38
|
+
|
|
39
|
+
<Box flexDirection="column" marginRight={4}>
|
|
40
|
+
<Text bold color="cyan">Quick Actions</Text>
|
|
41
|
+
<Text><Text color="green">a</Text> Actions menu</Text>
|
|
42
|
+
<Text><Text color="green">p</Text> Preview CLAUDE.md</Text>
|
|
43
|
+
<Text><Text color="green">/</Text> Search</Text>
|
|
44
|
+
<Text><Text color="green">Ctrl+S</Text> Sync</Text>
|
|
45
|
+
</Box>
|
|
46
|
+
|
|
47
|
+
<Box flexDirection="column">
|
|
48
|
+
<Text bold color="cyan">Editing</Text>
|
|
49
|
+
<Text><Text color="green">e</Text> Edit in $EDITOR</Text>
|
|
50
|
+
<Text><Text color="green">n</Text> Create new item</Text>
|
|
51
|
+
<Text><Text color="green">Enter</Text> Select/Open</Text>
|
|
52
|
+
<Text><Text color="green">Esc</Text> Close modal</Text>
|
|
53
|
+
</Box>
|
|
54
|
+
</Box>
|
|
55
|
+
|
|
56
|
+
<Box marginTop={1} flexDirection="column">
|
|
57
|
+
<Text bold color="cyan">CLI Commands</Text>
|
|
58
|
+
<Box flexDirection="row">
|
|
59
|
+
<Box flexDirection="column" marginRight={2}>
|
|
60
|
+
<Text color="gray">think init</Text>
|
|
61
|
+
<Text color="gray">think setup</Text>
|
|
62
|
+
<Text color="gray">think sync</Text>
|
|
63
|
+
<Text color="gray">think status</Text>
|
|
64
|
+
</Box>
|
|
65
|
+
<Box flexDirection="column" marginRight={2}>
|
|
66
|
+
<Text color="gray">think learn "..."</Text>
|
|
67
|
+
<Text color="gray">think review</Text>
|
|
68
|
+
<Text color="gray">think profile</Text>
|
|
69
|
+
<Text color="gray">think edit <file></Text>
|
|
70
|
+
</Box>
|
|
71
|
+
<Box flexDirection="column">
|
|
72
|
+
<Text color="gray">think allow "cmd"</Text>
|
|
73
|
+
<Text color="gray">think tree</Text>
|
|
74
|
+
<Text color="gray">think project learn</Text>
|
|
75
|
+
<Text color="gray">think help</Text>
|
|
76
|
+
</Box>
|
|
77
|
+
</Box>
|
|
78
|
+
</Box>
|
|
49
79
|
</Box>
|
|
50
80
|
|
|
51
81
|
<Box marginTop={1}>
|
|
52
|
-
<Text color="gray">Press ? or
|
|
82
|
+
<Text color="gray">Press ? or Esc to close</Text>
|
|
53
83
|
</Box>
|
|
54
84
|
</Box>
|
|
55
85
|
);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { CONFIG } from "../../core/config";
|
|
6
|
+
|
|
7
|
+
interface PreviewProps {
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Preview({ onClose }: PreviewProps) {
|
|
12
|
+
const [content, setContent] = useState<string[]>([]);
|
|
13
|
+
const [scroll, setScroll] = useState(0);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const maxLines = 20;
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
loadPreview();
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
async function loadPreview() {
|
|
22
|
+
if (existsSync(CONFIG.claudeMdPath)) {
|
|
23
|
+
const text = await readFile(CONFIG.claudeMdPath, "utf-8");
|
|
24
|
+
setContent(text.split("\n"));
|
|
25
|
+
}
|
|
26
|
+
setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
useInput((input, key) => {
|
|
30
|
+
if (key.escape || input === "q") {
|
|
31
|
+
onClose();
|
|
32
|
+
}
|
|
33
|
+
if (key.upArrow || input === "k") {
|
|
34
|
+
setScroll((s) => Math.max(0, s - 1));
|
|
35
|
+
}
|
|
36
|
+
if (key.downArrow || input === "j") {
|
|
37
|
+
setScroll((s) => Math.min(content.length - maxLines, s + 1));
|
|
38
|
+
}
|
|
39
|
+
if (key.pageUp) {
|
|
40
|
+
setScroll((s) => Math.max(0, s - maxLines));
|
|
41
|
+
}
|
|
42
|
+
if (key.pageDown) {
|
|
43
|
+
setScroll((s) => Math.min(content.length - maxLines, s + maxLines));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (loading) {
|
|
48
|
+
return <Text color="gray">Loading preview...</Text>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const visibleLines = content.slice(scroll, scroll + maxLines);
|
|
52
|
+
const scrollPercent = content.length > maxLines
|
|
53
|
+
? Math.round((scroll / (content.length - maxLines)) * 100)
|
|
54
|
+
: 100;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Box flexDirection="column" borderStyle="single" borderColor="cyan" padding={1}>
|
|
58
|
+
<Box marginBottom={1}>
|
|
59
|
+
<Text bold color="cyan">CLAUDE.md Preview</Text>
|
|
60
|
+
<Text color="gray"> ({content.length} lines)</Text>
|
|
61
|
+
<Box flexGrow={1} />
|
|
62
|
+
<Text color="gray">{scrollPercent}%</Text>
|
|
63
|
+
</Box>
|
|
64
|
+
|
|
65
|
+
<Box flexDirection="column" height={maxLines}>
|
|
66
|
+
{visibleLines.map((line, i) => {
|
|
67
|
+
let color: string | undefined;
|
|
68
|
+
if (line.startsWith("# ")) color = "green";
|
|
69
|
+
else if (line.startsWith("## ")) color = "yellow";
|
|
70
|
+
else if (line.startsWith("- ")) color = "white";
|
|
71
|
+
else if (line.startsWith("**")) color = "cyan";
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Text key={scroll + i} color={color} dimColor={!color}>
|
|
75
|
+
{line || " "}
|
|
76
|
+
</Text>
|
|
77
|
+
);
|
|
78
|
+
})}
|
|
79
|
+
</Box>
|
|
80
|
+
|
|
81
|
+
<Box marginTop={1}>
|
|
82
|
+
<Text color="gray">↑↓/jk: scroll | PgUp/PgDn: page | Esc: close</Text>
|
|
83
|
+
</Box>
|
|
84
|
+
</Box>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import { generatePlugin } from "../../core/generator";
|
|
5
|
+
import { addLearning } from "../../core/dedup";
|
|
6
|
+
import { readFile, writeFile } from "fs/promises";
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
import { thinkPath, CONFIG } from "../../core/config";
|
|
9
|
+
|
|
10
|
+
interface QuickActionsProps {
|
|
11
|
+
onMessage: (msg: string) => void;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type Action = "menu" | "sync" | "learn" | "search";
|
|
16
|
+
|
|
17
|
+
export function QuickActions({ onMessage, onClose }: QuickActionsProps) {
|
|
18
|
+
const [action, setAction] = useState<Action>("menu");
|
|
19
|
+
const [input, setInput] = useState("");
|
|
20
|
+
const [selected, setSelected] = useState(0);
|
|
21
|
+
|
|
22
|
+
const menuItems = [
|
|
23
|
+
{ key: "s", label: "Sync", desc: "Regenerate CLAUDE.md" },
|
|
24
|
+
{ key: "l", label: "Learn", desc: "Add a new learning" },
|
|
25
|
+
{ key: "f", label: "Search", desc: "Search all files" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
useInput((key, mod) => {
|
|
29
|
+
if (mod.escape || key === "q") {
|
|
30
|
+
if (action === "menu") {
|
|
31
|
+
onClose();
|
|
32
|
+
} else {
|
|
33
|
+
setAction("menu");
|
|
34
|
+
setInput("");
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (action === "menu") {
|
|
40
|
+
if (mod.upArrow || key === "k") {
|
|
41
|
+
setSelected((s) => (s - 1 + menuItems.length) % menuItems.length);
|
|
42
|
+
}
|
|
43
|
+
if (mod.downArrow || key === "j") {
|
|
44
|
+
setSelected((s) => (s + 1) % menuItems.length);
|
|
45
|
+
}
|
|
46
|
+
if (mod.return) {
|
|
47
|
+
const item = menuItems[selected]!;
|
|
48
|
+
if (item.key === "s") handleSync();
|
|
49
|
+
else if (item.key === "l") setAction("learn");
|
|
50
|
+
else if (item.key === "f") setAction("search");
|
|
51
|
+
}
|
|
52
|
+
// Quick keys
|
|
53
|
+
if (key === "s") handleSync();
|
|
54
|
+
if (key === "l") setAction("learn");
|
|
55
|
+
if (key === "f") setAction("search");
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
async function handleSync() {
|
|
60
|
+
onMessage("Syncing...");
|
|
61
|
+
try {
|
|
62
|
+
await generatePlugin();
|
|
63
|
+
onMessage("Synced successfully!");
|
|
64
|
+
} catch (e) {
|
|
65
|
+
onMessage("Sync failed!");
|
|
66
|
+
}
|
|
67
|
+
setTimeout(onClose, 1500);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function handleLearn() {
|
|
71
|
+
if (!input.trim()) return;
|
|
72
|
+
|
|
73
|
+
const learningsPath = thinkPath(CONFIG.files.learnings);
|
|
74
|
+
let content = "";
|
|
75
|
+
if (existsSync(learningsPath)) {
|
|
76
|
+
content = await readFile(learningsPath, "utf-8");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = addLearning(content, input.trim());
|
|
80
|
+
if (result.added) {
|
|
81
|
+
await writeFile(learningsPath, result.newContent);
|
|
82
|
+
onMessage(`Added: "${input.trim()}"`);
|
|
83
|
+
await generatePlugin();
|
|
84
|
+
} else {
|
|
85
|
+
onMessage(`Similar exists: "${result.similar}"`);
|
|
86
|
+
}
|
|
87
|
+
setInput("");
|
|
88
|
+
setTimeout(onClose, 1500);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (action === "menu") {
|
|
92
|
+
return (
|
|
93
|
+
<Box flexDirection="column" borderStyle="round" borderColor="green" padding={1}>
|
|
94
|
+
<Text bold color="green">Quick Actions</Text>
|
|
95
|
+
<Box flexDirection="column" marginTop={1}>
|
|
96
|
+
{menuItems.map((item, i) => (
|
|
97
|
+
<Box key={item.key}>
|
|
98
|
+
<Text color={i === selected ? "green" : "gray"}>
|
|
99
|
+
{i === selected ? "▸ " : " "}
|
|
100
|
+
</Text>
|
|
101
|
+
<Text color={i === selected ? "white" : "gray"} bold={i === selected}>
|
|
102
|
+
[{item.key}] {item.label}
|
|
103
|
+
</Text>
|
|
104
|
+
<Text color="gray"> - {item.desc}</Text>
|
|
105
|
+
</Box>
|
|
106
|
+
))}
|
|
107
|
+
</Box>
|
|
108
|
+
<Box marginTop={1}>
|
|
109
|
+
<Text color="gray">↑↓: navigate | Enter: select | Esc: close</Text>
|
|
110
|
+
</Box>
|
|
111
|
+
</Box>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (action === "learn") {
|
|
116
|
+
return (
|
|
117
|
+
<Box flexDirection="column" borderStyle="round" borderColor="yellow" padding={1}>
|
|
118
|
+
<Text bold color="yellow">Add Learning</Text>
|
|
119
|
+
<Box marginTop={1}>
|
|
120
|
+
<Text color="gray">▸ </Text>
|
|
121
|
+
<TextInput
|
|
122
|
+
value={input}
|
|
123
|
+
onChange={setInput}
|
|
124
|
+
onSubmit={handleLearn}
|
|
125
|
+
placeholder="Type your learning..."
|
|
126
|
+
/>
|
|
127
|
+
</Box>
|
|
128
|
+
<Box marginTop={1}>
|
|
129
|
+
<Text color="gray">Enter: add | Esc: cancel</Text>
|
|
130
|
+
</Box>
|
|
131
|
+
</Box>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (action === "search") {
|
|
136
|
+
return (
|
|
137
|
+
<Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1}>
|
|
138
|
+
<Text bold color="cyan">Search</Text>
|
|
139
|
+
<Box marginTop={1}>
|
|
140
|
+
<Text color="gray">▸ </Text>
|
|
141
|
+
<TextInput
|
|
142
|
+
value={input}
|
|
143
|
+
onChange={setInput}
|
|
144
|
+
placeholder="Search term..."
|
|
145
|
+
/>
|
|
146
|
+
</Box>
|
|
147
|
+
<Box marginTop={1}>
|
|
148
|
+
<Text color="gray">Enter: search | Esc: cancel</Text>
|
|
149
|
+
</Box>
|
|
150
|
+
</Box>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import { readFile } from "fs/promises";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import { thinkPath, CONFIG } from "../../core/config";
|
|
7
|
+
|
|
8
|
+
interface SearchProps {
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SearchResult {
|
|
13
|
+
file: string;
|
|
14
|
+
line: number;
|
|
15
|
+
content: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const searchFiles = [
|
|
19
|
+
{ name: "profile", path: CONFIG.files.profile },
|
|
20
|
+
{ name: "tools", path: CONFIG.files.tools },
|
|
21
|
+
{ name: "patterns", path: CONFIG.files.patterns },
|
|
22
|
+
{ name: "anti-patterns", path: CONFIG.files.antiPatterns },
|
|
23
|
+
{ name: "learnings", path: CONFIG.files.learnings },
|
|
24
|
+
{ name: "corrections", path: CONFIG.files.corrections },
|
|
25
|
+
{ name: "subagents", path: CONFIG.files.subagents },
|
|
26
|
+
{ name: "workflows", path: CONFIG.files.workflows },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function Search({ onClose }: SearchProps) {
|
|
30
|
+
const [query, setQuery] = useState("");
|
|
31
|
+
const [results, setResults] = useState<SearchResult[]>([]);
|
|
32
|
+
const [selected, setSelected] = useState(0);
|
|
33
|
+
const [searching, setSearching] = useState(false);
|
|
34
|
+
|
|
35
|
+
useInput((input, key) => {
|
|
36
|
+
if (key.escape) {
|
|
37
|
+
onClose();
|
|
38
|
+
}
|
|
39
|
+
if (key.upArrow || (key.ctrl && input === "p")) {
|
|
40
|
+
setSelected((s) => Math.max(0, s - 1));
|
|
41
|
+
}
|
|
42
|
+
if (key.downArrow || (key.ctrl && input === "n")) {
|
|
43
|
+
setSelected((s) => Math.min(results.length - 1, s + 1));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
async function handleSearch(q: string) {
|
|
48
|
+
setQuery(q);
|
|
49
|
+
if (q.length < 2) {
|
|
50
|
+
setResults([]);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setSearching(true);
|
|
55
|
+
const found: SearchResult[] = [];
|
|
56
|
+
const lowerQuery = q.toLowerCase();
|
|
57
|
+
|
|
58
|
+
for (const file of searchFiles) {
|
|
59
|
+
const fullPath = thinkPath(file.path);
|
|
60
|
+
if (!existsSync(fullPath)) continue;
|
|
61
|
+
|
|
62
|
+
const content = await readFile(fullPath, "utf-8");
|
|
63
|
+
const lines = content.split("\n");
|
|
64
|
+
|
|
65
|
+
lines.forEach((line, i) => {
|
|
66
|
+
if (line.toLowerCase().includes(lowerQuery)) {
|
|
67
|
+
found.push({
|
|
68
|
+
file: file.name,
|
|
69
|
+
line: i + 1,
|
|
70
|
+
content: line.trim(),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setResults(found.slice(0, 15));
|
|
77
|
+
setSelected(0);
|
|
78
|
+
setSearching(false);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1}>
|
|
83
|
+
<Box marginBottom={1}>
|
|
84
|
+
<Text bold color="cyan">Search</Text>
|
|
85
|
+
{results.length > 0 && (
|
|
86
|
+
<Text color="gray"> ({results.length} results)</Text>
|
|
87
|
+
)}
|
|
88
|
+
</Box>
|
|
89
|
+
|
|
90
|
+
<Box marginBottom={1}>
|
|
91
|
+
<Text color="cyan">▸ </Text>
|
|
92
|
+
<TextInput
|
|
93
|
+
value={query}
|
|
94
|
+
onChange={handleSearch}
|
|
95
|
+
placeholder="Type to search..."
|
|
96
|
+
/>
|
|
97
|
+
</Box>
|
|
98
|
+
|
|
99
|
+
{searching && <Text color="gray">Searching...</Text>}
|
|
100
|
+
|
|
101
|
+
{results.length > 0 && (
|
|
102
|
+
<Box flexDirection="column" marginTop={1}>
|
|
103
|
+
{results.map((r, i) => (
|
|
104
|
+
<Box key={`${r.file}-${r.line}`}>
|
|
105
|
+
<Text color={i === selected ? "cyan" : "gray"}>
|
|
106
|
+
{i === selected ? "▸ " : " "}
|
|
107
|
+
</Text>
|
|
108
|
+
<Text color="yellow">{r.file}</Text>
|
|
109
|
+
<Text color="gray">:{r.line} </Text>
|
|
110
|
+
<Text color={i === selected ? "white" : "gray"}>
|
|
111
|
+
{r.content.length > 50
|
|
112
|
+
? r.content.substring(0, 50) + "..."
|
|
113
|
+
: r.content}
|
|
114
|
+
</Text>
|
|
115
|
+
</Box>
|
|
116
|
+
))}
|
|
117
|
+
</Box>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{query.length >= 2 && results.length === 0 && !searching && (
|
|
121
|
+
<Text color="gray">No results found</Text>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
<Box marginTop={1}>
|
|
125
|
+
<Text color="gray">↑↓: navigate | Esc: close</Text>
|
|
126
|
+
</Box>
|
|
127
|
+
</Box>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { existsSync, statSync } from "fs";
|
|
4
|
+
import { readFile } from "fs/promises";
|
|
5
|
+
import { CONFIG, thinkPath } from "../../core/config";
|
|
6
|
+
import { extractLearnings } from "../../core/dedup";
|
|
7
|
+
|
|
8
|
+
interface StatusBarProps {
|
|
9
|
+
message?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function StatusBar({ message }: StatusBarProps) {
|
|
13
|
+
const [pendingCount, setPendingCount] = useState(0);
|
|
14
|
+
const [learningsCount, setLearningsCount] = useState(0);
|
|
15
|
+
const [lastSync, setLastSync] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
loadStatus();
|
|
19
|
+
const interval = setInterval(loadStatus, 5000);
|
|
20
|
+
return () => clearInterval(interval);
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
async function loadStatus() {
|
|
24
|
+
// Count pending
|
|
25
|
+
const pendingPath = thinkPath(CONFIG.files.pending);
|
|
26
|
+
if (existsSync(pendingPath)) {
|
|
27
|
+
const content = await readFile(pendingPath, "utf-8");
|
|
28
|
+
setPendingCount(extractLearnings(content).length);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Count learnings
|
|
32
|
+
const learningsPath = thinkPath(CONFIG.files.learnings);
|
|
33
|
+
if (existsSync(learningsPath)) {
|
|
34
|
+
const content = await readFile(learningsPath, "utf-8");
|
|
35
|
+
setLearningsCount(extractLearnings(content).length);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Last sync time
|
|
39
|
+
if (existsSync(CONFIG.claudeMdPath)) {
|
|
40
|
+
const stats = statSync(CONFIG.claudeMdPath);
|
|
41
|
+
const ago = formatTimeAgo(stats.mtime);
|
|
42
|
+
setLastSync(ago);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Box borderStyle="single" borderColor="gray" paddingX={1}>
|
|
48
|
+
<Box flexGrow={1}>
|
|
49
|
+
{message ? (
|
|
50
|
+
<Text color="yellow">{message}</Text>
|
|
51
|
+
) : (
|
|
52
|
+
<Text color="gray">Ready</Text>
|
|
53
|
+
)}
|
|
54
|
+
</Box>
|
|
55
|
+
<Box marginLeft={2}>
|
|
56
|
+
<Text color="green">{learningsCount}</Text>
|
|
57
|
+
<Text color="gray"> learnings</Text>
|
|
58
|
+
</Box>
|
|
59
|
+
{pendingCount > 0 && (
|
|
60
|
+
<Box marginLeft={2}>
|
|
61
|
+
<Text color="yellow">{pendingCount}</Text>
|
|
62
|
+
<Text color="gray"> pending</Text>
|
|
63
|
+
</Box>
|
|
64
|
+
)}
|
|
65
|
+
{lastSync && (
|
|
66
|
+
<Box marginLeft={2}>
|
|
67
|
+
<Text color="gray">synced {lastSync}</Text>
|
|
68
|
+
</Box>
|
|
69
|
+
)}
|
|
70
|
+
</Box>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatTimeAgo(date: Date): string {
|
|
75
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
76
|
+
if (seconds < 60) return "just now";
|
|
77
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
78
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
|
79
|
+
return `${Math.floor(seconds / 86400)}d ago`;
|
|
80
|
+
}
|