claude-think 0.2.1 → 0.3.1

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.
@@ -29,7 +29,8 @@
29
29
  "Bash(xargs:*)",
30
30
  "WebSearch",
31
31
  "Bash(__NEW_LINE_db734bce153d7d1e__ echo \"\")",
32
- "Bash(git checkout:*)"
32
+ "Bash(git checkout:*)",
33
+ "Bash(npm i:*)"
33
34
  ]
34
35
  }
35
36
  }
package/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ 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.1] - 2025-02-05
9
+
10
+ ### Fixed
11
+ - Responsive TUI - adapts to terminal width:
12
+ - Narrow terminals (<80 cols): compact nav labels, shorter status
13
+ - Very narrow (<50 cols): minimal header, single nav indicator
14
+ - Footer shortcuts adjust to available space
15
+
16
+ ## [0.3.0] - 2025-02-05
17
+
18
+ ### Added
19
+ - Enhanced TUI with new features:
20
+ - **Status bar** - shows learnings count, pending count, last sync time
21
+ - **Quick actions menu** (press `a`) - sync, add learning, search
22
+ - **Preview panel** (press `p`) - view generated CLAUDE.md with scrolling
23
+ - **Search** (press `/`) - search across all config files
24
+ - **Better help screen** (press `?`) - organized shortcuts and CLI commands
25
+ - New keyboard shortcuts: `a` actions, `p` preview, `/` search, `Ctrl+S` sync
26
+ - Bordered content panels for cleaner visual design
27
+
28
+ ### Changed
29
+ - TUI now uses modals for actions, preview, search, and help
30
+ - Improved navigation hints in footer
31
+
8
32
  ## [0.2.1] - 2025-02-05
9
33
 
10
34
  ### 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 install -g claude-think
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 a Claude plugin at `~/.claude/plugins/think/`
51
- 3. Claude Code auto-loads the plugin, so your context is always there
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.2.1",
3
+ "version": "0.3.1",
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
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect } from "react";
2
- import { Box, Text, useInput, useApp } from "ink";
2
+ import { Box, Text, useInput, useApp, useStdout } from "ink";
3
3
  import { Navigation } from "./components/Navigation";
4
4
  import { Profile } from "./components/Profile";
5
5
  import { Preferences } from "./components/Preferences";
@@ -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,39 +24,110 @@ type Section =
19
24
  | "permissions"
20
25
  | "skills"
21
26
  | "agents"
22
- | "automation"
23
- | "help";
27
+ | "automation";
28
+
29
+ type Modal = "none" | "help" | "actions" | "preview" | "search";
24
30
 
25
31
  export function App() {
26
32
  const { exit } = useApp();
33
+ const { stdout } = useStdout();
27
34
  const [section, setSection] = useState<Section>("profile");
28
- const [showHelp, setShowHelp] = useState(false);
35
+ const [modal, setModal] = useState<Modal>("none");
36
+ const [statusMessage, setStatusMessage] = useState<string | undefined>();
29
37
  const [initialized, setInitialized] = useState(false);
30
38
 
39
+ // Get terminal dimensions
40
+ const width = stdout?.columns ?? 80;
41
+ const isNarrow = width < 80;
42
+ const isVeryNarrow = width < 50;
43
+
31
44
  useEffect(() => {
32
45
  setInitialized(existsSync(CONFIG.thinkDir));
33
46
  }, []);
34
47
 
35
48
  useInput((input, key) => {
49
+ // Global shortcuts (when no modal open)
50
+ if (modal !== "none") return;
51
+
36
52
  if (input === "q" || (key.ctrl && input === "c")) {
37
53
  exit();
38
54
  }
39
55
  if (input === "?") {
40
- setShowHelp(!showHelp);
56
+ setModal("help");
57
+ }
58
+ if (key.ctrl && input === "s") {
59
+ setModal("actions");
60
+ }
61
+ if (input === "a") {
62
+ setModal("actions");
63
+ }
64
+ if (input === "p") {
65
+ setModal("preview");
66
+ }
67
+ if (input === "/" || (key.ctrl && input === "f")) {
68
+ setModal("search");
41
69
  }
42
70
  });
43
71
 
72
+ function handleMessage(msg: string) {
73
+ setStatusMessage(msg);
74
+ setTimeout(() => setStatusMessage(undefined), 3000);
75
+ }
76
+
44
77
  if (!initialized) {
45
78
  return (
46
79
  <Box flexDirection="column" padding={1}>
47
- <Text color="red">~/.think not found.</Text>
48
- <Text>Run `think init` first, then launch the TUI.</Text>
80
+ <Box
81
+ borderStyle="round"
82
+ borderColor="red"
83
+ paddingX={2}
84
+ paddingY={1}
85
+ flexDirection="column"
86
+ >
87
+ <Text color="red" bold>
88
+ ~/.think not found
89
+ </Text>
90
+ <Box marginTop={1}>
91
+ <Text color="gray">Run `think init` first, then launch the TUI.</Text>
92
+ </Box>
93
+ </Box>
94
+ </Box>
95
+ );
96
+ }
97
+
98
+ // Render modals
99
+ if (modal === "help") {
100
+ return <Help onClose={() => setModal("none")} />;
101
+ }
102
+
103
+ if (modal === "actions") {
104
+ return (
105
+ <Box flexDirection="column" padding={1}>
106
+ <Header width={width} />
107
+ <QuickActions
108
+ onMessage={handleMessage}
109
+ onClose={() => setModal("none")}
110
+ />
49
111
  </Box>
50
112
  );
51
113
  }
52
114
 
53
- if (showHelp) {
54
- return <Help onClose={() => setShowHelp(false)} />;
115
+ if (modal === "preview") {
116
+ return (
117
+ <Box flexDirection="column" padding={1}>
118
+ <Header width={width} />
119
+ <Preview onClose={() => setModal("none")} />
120
+ </Box>
121
+ );
122
+ }
123
+
124
+ if (modal === "search") {
125
+ return (
126
+ <Box flexDirection="column" padding={1}>
127
+ <Header width={width} />
128
+ <Search onClose={() => setModal("none")} />
129
+ </Box>
130
+ );
55
131
  }
56
132
 
57
133
  const renderSection = () => {
@@ -77,24 +153,54 @@ export function App() {
77
153
 
78
154
  return (
79
155
  <Box flexDirection="column">
80
- <Box marginBottom={1}>
81
- <Text color="green" bold>
82
- ▀█▀ █░█ █ █▄░█ █▄▀
83
- </Text>
84
- <Text color="gray"> Personal Context for Claude</Text>
85
- </Box>
156
+ <Header width={width} />
86
157
 
87
158
  <Navigation currentSection={section} onSectionChange={setSection} />
88
159
 
89
- <Box flexDirection="column" marginTop={1}>
160
+ <Box
161
+ flexDirection="column"
162
+ marginTop={1}
163
+ borderStyle="single"
164
+ borderColor="gray"
165
+ padding={1}
166
+ minHeight={15}
167
+ >
90
168
  {renderSection()}
91
169
  </Box>
92
170
 
171
+ <Box marginTop={1}>
172
+ <StatusBar message={statusMessage} />
173
+ </Box>
174
+
93
175
  <Box marginTop={1}>
94
176
  <Text color="gray">
95
- Tab: switch sections | ?: help | q: quit
177
+ {isNarrow
178
+ ? "Tab:nav a:act p:prev /:search ?:help q:quit"
179
+ : "Tab: sections | a: actions | p: preview | /: search | ?: help | q: quit"}
96
180
  </Text>
97
181
  </Box>
98
182
  </Box>
99
183
  );
100
184
  }
185
+
186
+ function Header({ width }: { width: number }) {
187
+ // Full banner needs ~45 chars, compact needs ~25
188
+ if (width >= 50) {
189
+ return (
190
+ <Box marginBottom={1}>
191
+ <Text color="green" bold>
192
+ ▀█▀ █░█ █ █▄░█ █▄▀
193
+ </Text>
194
+ <Text color="gray"> Personal Context for Claude</Text>
195
+ </Box>
196
+ );
197
+ }
198
+
199
+ // Compact header for narrow terminals
200
+ return (
201
+ <Box marginBottom={1}>
202
+ <Text color="green" bold>think</Text>
203
+ <Text color="gray"> - Claude Context</Text>
204
+ </Box>
205
+ );
206
+ }
@@ -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
- Keyboard Shortcuts
19
+ ▀█▀ █░█ █ █▄░█ █▄▀
20
20
  </Text>
21
+ <Text color="gray"> Help</Text>
21
22
  </Box>
22
23
 
23
- <Box flexDirection="column" paddingLeft={1}>
24
- <Text key="nav-header" bold color="cyan">Navigation</Text>
25
- <Text key="nav-1"> Tab / Shift+Tab Switch sections</Text>
26
- <Text key="nav-2"> 1-7 Jump to section</Text>
27
- <Text key="nav-3"> ← / → Switch sub-sections</Text>
28
- <Text key="nav-4"> ↑ / ↓ Select item in list</Text>
29
- <Text key="nav-spacer"> </Text>
30
-
31
- <Text key="act-header" bold color="cyan">Actions</Text>
32
- <Text key="act-1"> e Edit in $EDITOR</Text>
33
- <Text key="act-2"> n Create new (skills/agents)</Text>
34
- <Text key="act-3"> Enter Select / Open</Text>
35
- <Text key="act-spacer"> </Text>
36
-
37
- <Text key="gen-header" bold color="cyan">General</Text>
38
- <Text key="gen-1"> ? Toggle help</Text>
39
- <Text key="gen-2"> q / Ctrl+C Quit</Text>
40
- <Text key="gen-spacer"> </Text>
41
-
42
- <Text key="cli-header" bold color="cyan">CLI Commands</Text>
43
- <Text key="cli-1"> think init Initialize ~/.think</Text>
44
- <Text key="cli-2"> think sync Sync to Claude plugin</Text>
45
- <Text key="cli-3"> think status Show status</Text>
46
- <Text key="cli-4"> think learn Add a learning</Text>
47
- <Text key="cli-5"> think review Review pending learnings</Text>
48
- <Text key="cli-6"> think allow Add allowed command</Text>
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 &lt;file&gt;</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 Escape to close</Text>
82
+ <Text color="gray">Press ? or Esc to close</Text>
53
83
  </Box>
54
84
  </Box>
55
85
  );
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { Box, Text, useInput } from "ink";
2
+ import { Box, Text, useInput, useStdout } from "ink";
3
3
 
4
4
  type Section =
5
5
  | "profile"
@@ -15,38 +15,76 @@ interface NavigationProps {
15
15
  onSectionChange: (section: Section) => void;
16
16
  }
17
17
 
18
- const sections: { key: Section; label: string }[] = [
19
- { key: "profile", label: "Profile" },
20
- { key: "preferences", label: "Preferences" },
21
- { key: "memory", label: "Memory" },
22
- { key: "permissions", label: "Permissions" },
23
- { key: "skills", label: "Skills" },
24
- { key: "agents", label: "Agents" },
25
- { key: "automation", label: "Automation" },
18
+ const sections: { key: Section; label: string; short: string }[] = [
19
+ { key: "profile", label: "Profile", short: "Prof" },
20
+ { key: "preferences", label: "Preferences", short: "Pref" },
21
+ { key: "memory", label: "Memory", short: "Mem" },
22
+ { key: "permissions", label: "Permissions", short: "Perm" },
23
+ { key: "skills", label: "Skills", short: "Skill" },
24
+ { key: "agents", label: "Agents", short: "Agent" },
25
+ { key: "automation", label: "Automation", short: "Auto" },
26
26
  ];
27
27
 
28
28
  export function Navigation({
29
29
  currentSection,
30
30
  onSectionChange,
31
31
  }: NavigationProps) {
32
+ const { stdout } = useStdout();
33
+ const width = stdout?.columns ?? 80;
34
+ const isNarrow = width < 80;
35
+ const isVeryNarrow = width < 50;
36
+
32
37
  useInput((input, key) => {
33
38
  if (key.tab) {
34
39
  const currentIndex = sections.findIndex((s) => s.key === currentSection);
35
40
  const nextIndex = key.shift
36
41
  ? (currentIndex - 1 + sections.length) % sections.length
37
42
  : (currentIndex + 1) % sections.length;
38
- onSectionChange(sections[nextIndex].key);
43
+ onSectionChange(sections[nextIndex]!.key);
39
44
  }
40
45
 
41
46
  // Number keys for quick navigation
42
47
  const num = parseInt(input);
43
48
  if (num >= 1 && num <= sections.length) {
44
- onSectionChange(sections[num - 1].key);
49
+ onSectionChange(sections[num - 1]!.key);
45
50
  }
46
51
  });
47
52
 
53
+ // Very narrow: show only current + numbers
54
+ if (isVeryNarrow) {
55
+ const currentIdx = sections.findIndex((s) => s.key === currentSection);
56
+ const current = sections[currentIdx]!;
57
+ return (
58
+ <Box>
59
+ <Text color="green" bold>
60
+ [{currentIdx + 1}/{sections.length}] {current.label}
61
+ </Text>
62
+ <Text color="gray"> (Tab/1-7)</Text>
63
+ </Box>
64
+ );
65
+ }
66
+
67
+ // Narrow: use short labels
68
+ if (isNarrow) {
69
+ return (
70
+ <Box flexWrap="wrap">
71
+ {sections.map((section, index) => (
72
+ <Box key={section.key} marginRight={1}>
73
+ <Text
74
+ color={currentSection === section.key ? "green" : "gray"}
75
+ bold={currentSection === section.key}
76
+ >
77
+ {index + 1}.{section.short}
78
+ </Text>
79
+ </Box>
80
+ ))}
81
+ </Box>
82
+ );
83
+ }
84
+
85
+ // Full width: show full labels with indicator
48
86
  return (
49
- <Box>
87
+ <Box flexWrap="wrap">
50
88
  {sections.map((section, index) => (
51
89
  <Box key={section.key} marginRight={1}>
52
90
  <Text
@@ -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,84 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Text, useStdout } 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
+ const { stdout } = useStdout();
47
+ const width = stdout?.columns ?? 80;
48
+ const isNarrow = width < 60;
49
+
50
+ return (
51
+ <Box borderStyle="single" borderColor="gray" paddingX={1}>
52
+ <Box flexGrow={1}>
53
+ {message ? (
54
+ <Text color="yellow">{message}</Text>
55
+ ) : (
56
+ <Text color="gray">Ready</Text>
57
+ )}
58
+ </Box>
59
+ <Box marginLeft={1}>
60
+ <Text color="green">{learningsCount}</Text>
61
+ <Text color="gray">{isNarrow ? "L" : " learnings"}</Text>
62
+ </Box>
63
+ {pendingCount > 0 && (
64
+ <Box marginLeft={1}>
65
+ <Text color="yellow">{pendingCount}</Text>
66
+ <Text color="gray">{isNarrow ? "P" : " pending"}</Text>
67
+ </Box>
68
+ )}
69
+ {lastSync && !isNarrow && (
70
+ <Box marginLeft={1}>
71
+ <Text color="gray">synced {lastSync}</Text>
72
+ </Box>
73
+ )}
74
+ </Box>
75
+ );
76
+ }
77
+
78
+ function formatTimeAgo(date: Date): string {
79
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
80
+ if (seconds < 60) return "just now";
81
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
82
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
83
+ return `${Math.floor(seconds / 86400)}d ago`;
84
+ }