claude-think 0.3.0 → 0.3.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.
@@ -30,7 +30,10 @@
30
30
  "WebSearch",
31
31
  "Bash(__NEW_LINE_db734bce153d7d1e__ echo \"\")",
32
32
  "Bash(git checkout:*)",
33
- "Bash(npm i:*)"
33
+ "Bash(npm i:*)",
34
+ "Bash(think learn \"Always test TUI changes locally before publishing releases\")",
35
+ "WebFetch(domain:github.com)",
36
+ "WebFetch(domain:www.npmjs.com)"
34
37
  ]
35
38
  }
36
39
  }
package/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ 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.2] - 2025-02-05
9
+
10
+ ### Added
11
+ - Fullscreen TUI using alternate screen buffer (preserves terminal history on exit)
12
+ - Scrollable content in all sections (↑↓ or j/k to scroll)
13
+ - Terminal resize support - UI adapts dynamically
14
+
15
+ ### Changed
16
+ - New double-line box header design
17
+ - Content area now fills available terminal height
18
+ - Improved responsive layout for different terminal sizes
19
+
20
+ ## [0.3.1] - 2025-02-05
21
+
22
+ ### Fixed
23
+ - Responsive TUI - adapts to terminal width:
24
+ - Narrow terminals (<80 cols): compact nav labels, shorter status
25
+ - Very narrow (<50 cols): minimal header, single nav indicator
26
+ - Footer shortcuts adjust to available space
27
+
8
28
  ## [0.3.0] - 2025-02-05
9
29
 
10
30
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-think",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Personal context manager for Claude - manage your preferences, patterns, and memory",
5
5
  "author": "Amit Feldman",
6
6
  "license": "MIT",
package/src/tui/App.tsx CHANGED
@@ -13,6 +13,7 @@ import { StatusBar } from "./components/StatusBar";
13
13
  import { QuickActions } from "./components/QuickActions";
14
14
  import { Preview } from "./components/Preview";
15
15
  import { Search } from "./components/Search";
16
+ import { FullScreen, useTerminalSize } from "./components/FullScreen";
16
17
  import { existsSync } from "fs";
17
18
  import { CONFIG } from "../core/config";
18
19
 
@@ -30,11 +31,21 @@ type Modal = "none" | "help" | "actions" | "preview" | "search";
30
31
 
31
32
  export function App() {
32
33
  const { exit } = useApp();
34
+ const { width, height } = useTerminalSize();
33
35
  const [section, setSection] = useState<Section>("profile");
34
36
  const [modal, setModal] = useState<Modal>("none");
35
37
  const [statusMessage, setStatusMessage] = useState<string | undefined>();
36
38
  const [initialized, setInitialized] = useState(false);
37
39
 
40
+ const isNarrow = width < 80;
41
+
42
+ // Calculate content height
43
+ // Banner: 5 lines (spacer + 3 + margin), compact: 2 lines
44
+ const hasBanner = width >= 44;
45
+ const headerHeight = hasBanner ? 5 : 2;
46
+ // total - header - nav(1) - borders(2) - status(3) - footer(1) - padding(2)
47
+ const contentHeight = Math.max(5, height - headerHeight - 9);
48
+
38
49
  useEffect(() => {
39
50
  setInitialized(existsSync(CONFIG.thinkDir));
40
51
  }, []);
@@ -70,118 +81,154 @@ export function App() {
70
81
 
71
82
  if (!initialized) {
72
83
  return (
73
- <Box flexDirection="column" padding={1}>
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>
84
+ <FullScreen>
85
+ <Box flexDirection="column" padding={1} justifyContent="center" alignItems="center" height="100%">
86
+ <Box
87
+ borderStyle="round"
88
+ borderColor="red"
89
+ paddingX={2}
90
+ paddingY={1}
91
+ flexDirection="column"
92
+ >
93
+ <Text color="red" bold>
94
+ ~/.think not found
95
+ </Text>
96
+ <Box marginTop={1}>
97
+ <Text color="gray">Run `think init` first, then launch the TUI.</Text>
98
+ </Box>
86
99
  </Box>
87
100
  </Box>
88
- </Box>
101
+ </FullScreen>
89
102
  );
90
103
  }
91
104
 
92
- // Render modals
105
+ // Render modals in fullscreen
93
106
  if (modal === "help") {
94
- return <Help onClose={() => setModal("none")} />;
107
+ return (
108
+ <FullScreen>
109
+ <Help onClose={() => setModal("none")} height={height} width={width} />
110
+ </FullScreen>
111
+ );
95
112
  }
96
113
 
97
114
  if (modal === "actions") {
98
115
  return (
99
- <Box flexDirection="column" padding={1}>
100
- <Header />
101
- <QuickActions
102
- onMessage={handleMessage}
103
- onClose={() => setModal("none")}
104
- />
105
- </Box>
116
+ <FullScreen>
117
+ <Box flexDirection="column" padding={1}>
118
+ <Header width={width} />
119
+ <QuickActions
120
+ onMessage={handleMessage}
121
+ onClose={() => setModal("none")}
122
+ />
123
+ </Box>
124
+ </FullScreen>
106
125
  );
107
126
  }
108
127
 
109
128
  if (modal === "preview") {
110
129
  return (
111
- <Box flexDirection="column" padding={1}>
112
- <Header />
113
- <Preview onClose={() => setModal("none")} />
114
- </Box>
130
+ <FullScreen>
131
+ <Box flexDirection="column" padding={1}>
132
+ <Header width={width} />
133
+ <Preview onClose={() => setModal("none")} height={height - 6} />
134
+ </Box>
135
+ </FullScreen>
115
136
  );
116
137
  }
117
138
 
118
139
  if (modal === "search") {
119
140
  return (
120
- <Box flexDirection="column" padding={1}>
121
- <Header />
122
- <Search onClose={() => setModal("none")} />
123
- </Box>
141
+ <FullScreen>
142
+ <Box flexDirection="column" padding={1}>
143
+ <Header width={width} />
144
+ <Search onClose={() => setModal("none")} height={height - 6} />
145
+ </Box>
146
+ </FullScreen>
124
147
  );
125
148
  }
126
149
 
127
150
  const renderSection = () => {
128
151
  switch (section) {
129
152
  case "profile":
130
- return <Profile />;
153
+ return <Profile height={contentHeight} />;
131
154
  case "preferences":
132
- return <Preferences />;
155
+ return <Preferences height={contentHeight} />;
133
156
  case "memory":
134
- return <Memory />;
157
+ return <Memory height={contentHeight} />;
135
158
  case "permissions":
136
- return <Permissions />;
159
+ return <Permissions height={contentHeight} />;
137
160
  case "skills":
138
- return <Skills />;
161
+ return <Skills height={contentHeight} />;
139
162
  case "agents":
140
- return <Agents />;
163
+ return <Agents height={contentHeight} />;
141
164
  case "automation":
142
- return <Automation />;
165
+ return <Automation height={contentHeight} />;
143
166
  default:
144
- return <Profile />;
167
+ return <Profile height={contentHeight} />;
145
168
  }
146
169
  };
147
170
 
148
171
  return (
149
- <Box flexDirection="column">
150
- <Header />
151
-
152
- <Navigation currentSection={section} onSectionChange={setSection} />
153
-
154
- <Box
155
- flexDirection="column"
156
- marginTop={1}
157
- borderStyle="single"
158
- borderColor="gray"
159
- padding={1}
160
- minHeight={15}
161
- >
162
- {renderSection()}
163
- </Box>
172
+ <FullScreen>
173
+ <Box flexDirection="column" padding={1} height="100%">
174
+ <Header width={width} />
164
175
 
165
- <Box marginTop={1}>
166
- <StatusBar message={statusMessage} />
167
- </Box>
176
+ <Navigation currentSection={section} onSectionChange={setSection} />
168
177
 
169
- <Box marginTop={1}>
170
- <Text color="gray">
171
- Tab: sections | a: actions | p: preview | /: search | ?: help | q: quit
172
- </Text>
178
+ <Box
179
+ flexDirection="column"
180
+ marginTop={1}
181
+ borderStyle="single"
182
+ borderColor="gray"
183
+ padding={1}
184
+ flexGrow={1}
185
+ height={contentHeight + 2}
186
+ >
187
+ {renderSection()}
188
+ </Box>
189
+
190
+ <Box marginTop={1}>
191
+ <StatusBar message={statusMessage} />
192
+ </Box>
193
+
194
+ <Box>
195
+ <Text color="gray">
196
+ {isNarrow
197
+ ? "Tab:nav ↑↓:scroll a:act p:prev /:search ?:help q:quit"
198
+ : "Tab: sections | ↑↓/jk: scroll | a: actions | p: preview | /: search | ?: help | q: quit"}
199
+ </Text>
200
+ </Box>
173
201
  </Box>
174
- </Box>
202
+ </FullScreen>
175
203
  );
176
204
  }
177
205
 
178
- function Header() {
206
+ function Header({ width, showBanner = true }: { width: number; showBanner?: boolean }) {
207
+ // Double-line box banner
208
+ if (width >= 44 && showBanner) {
209
+ return (
210
+ <Box flexDirection="column" marginBottom={1}>
211
+ <Text> </Text>
212
+ <Text color="green">╔═══════════════════════════════════════╗</Text>
213
+ <Text color="green">║ <Text bold>THINK</Text> · <Text color="gray">Personal Context for Claude</Text> ║</Text>
214
+ <Text color="green">╚═══════════════════════════════════════╝</Text>
215
+ </Box>
216
+ );
217
+ }
218
+
219
+ // Compact header for narrow terminals
220
+ if (width < 35) {
221
+ return (
222
+ <Box marginBottom={1}>
223
+ <Text color="green" bold>think</Text>
224
+ </Box>
225
+ );
226
+ }
227
+
179
228
  return (
180
229
  <Box marginBottom={1}>
181
- <Text color="green" bold>
182
- ▀█▀ █░█ █▄░█ █▄▀
183
- </Text>
184
- <Text color="gray"> Personal Context for Claude</Text>
230
+ <Text color="green" bold>think</Text>
231
+ <Text color="gray"> · Personal Context for Claude</Text>
185
232
  </Box>
186
233
  );
187
234
  }
@@ -13,7 +13,11 @@ interface AgentInfo {
13
13
  path: string;
14
14
  }
15
15
 
16
- export function Agents() {
16
+ interface AgentsProps {
17
+ height?: number;
18
+ }
19
+
20
+ export function Agents({ height = 15 }: AgentsProps) {
17
21
  const [agents, setAgents] = useState<AgentInfo[]>([]);
18
22
  const [selectedIndex, setSelectedIndex] = useState(0);
19
23
  const [loading, setLoading] = useState(true);
@@ -11,7 +11,11 @@ const files: { key: AutomationFile; label: string; path: string }[] = [
11
11
  { key: "workflows", label: "Workflows", path: CONFIG.files.workflows },
12
12
  ];
13
13
 
14
- export function Automation() {
14
+ interface AutomationProps {
15
+ height?: number;
16
+ }
17
+
18
+ export function Automation({ height = 15 }: AutomationProps) {
15
19
  const [selected, setSelected] = useState<AutomationFile>("subagents");
16
20
  const [content, setContent] = useState<string>("");
17
21
  const [loading, setLoading] = useState(true);
@@ -0,0 +1,72 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Box, useStdout } from "ink";
3
+
4
+ const enterAltScreenCommand = "\x1b[?1049h";
5
+ const leaveAltScreenCommand = "\x1b[?1049l";
6
+
7
+ interface FullScreenProps {
8
+ children: React.ReactNode;
9
+ }
10
+
11
+ export function FullScreen({ children }: FullScreenProps) {
12
+ const { stdout } = useStdout();
13
+ const [dimensions, setDimensions] = useState({
14
+ width: stdout?.columns ?? 80,
15
+ height: stdout?.rows ?? 24,
16
+ });
17
+
18
+ useEffect(() => {
19
+ // Enter alternate screen buffer (preserves terminal history)
20
+ process.stdout.write(enterAltScreenCommand);
21
+
22
+ // Handle resize
23
+ const handleResize = () => {
24
+ setDimensions({
25
+ width: process.stdout.columns,
26
+ height: process.stdout.rows,
27
+ });
28
+ };
29
+
30
+ process.stdout.on("resize", handleResize);
31
+
32
+ // Cleanup: leave alternate screen buffer
33
+ return () => {
34
+ process.stdout.off("resize", handleResize);
35
+ process.stdout.write(leaveAltScreenCommand);
36
+ };
37
+ }, []);
38
+
39
+ return (
40
+ <Box
41
+ width={dimensions.width}
42
+ height={dimensions.height}
43
+ flexDirection="column"
44
+ >
45
+ {children}
46
+ </Box>
47
+ );
48
+ }
49
+
50
+ export function useTerminalSize() {
51
+ const { stdout } = useStdout();
52
+ const [size, setSize] = useState({
53
+ width: stdout?.columns ?? 80,
54
+ height: stdout?.rows ?? 24,
55
+ });
56
+
57
+ useEffect(() => {
58
+ const handleResize = () => {
59
+ setSize({
60
+ width: process.stdout.columns,
61
+ height: process.stdout.rows,
62
+ });
63
+ };
64
+
65
+ process.stdout.on("resize", handleResize);
66
+ return () => {
67
+ process.stdout.off("resize", handleResize);
68
+ };
69
+ }, []);
70
+
71
+ return size;
72
+ }
@@ -3,22 +3,24 @@ import { Box, Text, useInput } from "ink";
3
3
 
4
4
  interface HelpProps {
5
5
  onClose: () => void;
6
+ height?: number;
7
+ width?: number;
6
8
  }
7
9
 
8
- export function Help({ onClose }: HelpProps) {
10
+ export function Help({ onClose, height = 24, width = 80 }: HelpProps) {
9
11
  useInput((input, key) => {
10
12
  if (input === "?" || key.escape || input === "q") {
11
13
  onClose();
12
14
  }
13
15
  });
14
16
 
17
+ const isNarrow = width < 70;
18
+
15
19
  return (
16
- <Box flexDirection="column" padding={1}>
20
+ <Box flexDirection="column" padding={1} height={height}>
17
21
  <Box marginBottom={1}>
18
- <Text bold color="green">
19
- ▀█▀ █░█ █▄░█ █▄▀
20
- </Text>
21
- <Text color="gray"> Help</Text>
22
+ <Text bold color="green">think</Text>
23
+ <Text color="gray"> · Help</Text>
22
24
  </Box>
23
25
 
24
26
  <Box
@@ -26,55 +28,55 @@ export function Help({ onClose }: HelpProps) {
26
28
  borderStyle="single"
27
29
  borderColor="gray"
28
30
  padding={1}
31
+ flexGrow={1}
29
32
  >
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
-
33
+ {isNarrow ? (
47
34
  <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>
35
+ <Text bold color="cyan">Navigation</Text>
36
+ <Text><Text color="green">Tab</Text> Switch sections</Text>
37
+ <Text><Text color="green">1-7</Text> Jump to section</Text>
38
+ <Text><Text color="green">←/→</Text> Sub-sections</Text>
39
+ <Text><Text color="green">↑↓/jk</Text> Scroll</Text>
40
+ <Text> </Text>
41
+ <Text bold color="cyan">Actions</Text>
42
+ <Text><Text color="green">a</Text> Actions menu</Text>
43
+ <Text><Text color="green">p</Text> Preview</Text>
44
+ <Text><Text color="green">/</Text> Search</Text>
45
+ <Text><Text color="green">e</Text> Edit in $EDITOR</Text>
46
+ <Text><Text color="green">?</Text> Help</Text>
47
+ <Text><Text color="green">q</Text> Quit</Text>
53
48
  </Box>
54
- </Box>
55
-
56
- <Box marginTop={1} flexDirection="column">
57
- <Text bold color="cyan">CLI Commands</Text>
49
+ ) : (
58
50
  <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>
51
+ <Box flexDirection="column" marginRight={4}>
52
+ <Text bold color="cyan">Navigation</Text>
53
+ <Text><Text color="green">Tab</Text> Switch sections</Text>
54
+ <Text><Text color="green">1-7</Text> Jump to section</Text>
55
+ <Text><Text color="green">←/→</Text> Sub-sections</Text>
56
+ <Text><Text color="green">↑↓/jk</Text> Scroll content</Text>
64
57
  </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>
58
+
59
+ <Box flexDirection="column" marginRight={4}>
60
+ <Text bold color="cyan">Quick Actions</Text>
61
+ <Text><Text color="green">a</Text> Actions menu</Text>
62
+ <Text><Text color="green">p</Text> Preview CLAUDE.md</Text>
63
+ <Text><Text color="green">/</Text> Search</Text>
64
+ <Text><Text color="green">Ctrl+S</Text> Sync</Text>
70
65
  </Box>
66
+
71
67
  <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>
68
+ <Text bold color="cyan">Editing</Text>
69
+ <Text><Text color="green">e</Text> Edit in $EDITOR</Text>
70
+ <Text><Text color="green">n</Text> Create new item</Text>
71
+ <Text><Text color="green">Esc</Text> Close modal</Text>
72
+ <Text><Text color="green">q</Text> Quit</Text>
76
73
  </Box>
77
74
  </Box>
75
+ )}
76
+
77
+ <Box marginTop={1} flexDirection="column">
78
+ <Text bold color="cyan">CLI Commands</Text>
79
+ <Text color="gray">think init | setup | sync | status | learn | review | profile | edit | tree | project learn</Text>
78
80
  </Box>
79
81
  </Box>
80
82
 
@@ -14,13 +14,19 @@ const sections: { key: MemorySection; label: string; path: string }[] = [
14
14
  { key: "pending", label: "Pending", path: CONFIG.files.pending },
15
15
  ];
16
16
 
17
- export function Memory() {
17
+ interface MemoryProps {
18
+ height?: number;
19
+ }
20
+
21
+ export function Memory({ height = 15 }: MemoryProps) {
18
22
  const [selected, setSelected] = useState<MemorySection>("learnings");
19
23
  const [items, setItems] = useState<string[]>([]);
20
24
  const [loading, setLoading] = useState(true);
25
+ const [scroll, setScroll] = useState(0);
21
26
 
22
27
  useEffect(() => {
23
28
  loadContent();
29
+ setScroll(0);
24
30
  }, [selected]);
25
31
 
26
32
  async function loadContent() {
@@ -38,14 +44,23 @@ export function Memory() {
38
44
  setLoading(false);
39
45
  }
40
46
 
47
+ const contentHeight = height - 3;
48
+ const maxScroll = Math.max(0, items.length - contentHeight);
49
+
41
50
  useInput((input, key) => {
42
51
  if (key.leftArrow || input === "h") {
43
52
  const idx = sections.findIndex((s) => s.key === selected);
44
- setSelected(sections[(idx - 1 + sections.length) % sections.length].key);
53
+ setSelected(sections[(idx - 1 + sections.length) % sections.length]!.key);
45
54
  }
46
55
  if (key.rightArrow || input === "l") {
47
56
  const idx = sections.findIndex((s) => s.key === selected);
48
- setSelected(sections[(idx + 1) % sections.length].key);
57
+ setSelected(sections[(idx + 1) % sections.length]!.key);
58
+ }
59
+ if (key.upArrow || input === "k") {
60
+ setScroll((s) => Math.max(0, s - 1));
61
+ }
62
+ if (key.downArrow || input === "j") {
63
+ setScroll((s) => Math.min(maxScroll, s + 1));
49
64
  }
50
65
  if (input === "e") {
51
66
  const section = sections.find((s) => s.key === selected);
@@ -60,8 +75,10 @@ export function Memory() {
60
75
  }
61
76
  });
62
77
 
78
+ const visibleItems = items.slice(scroll, scroll + contentHeight);
79
+
63
80
  return (
64
- <Box flexDirection="column">
81
+ <Box flexDirection="column" height={height}>
65
82
  <Box marginBottom={1}>
66
83
  {sections.map((section) => (
67
84
  <Box key={section.key} marginRight={2}>
@@ -74,16 +91,17 @@ export function Memory() {
74
91
  </Text>
75
92
  </Box>
76
93
  ))}
94
+ <Text color="gray">({items.length})</Text>
77
95
  </Box>
78
96
 
79
- <Box flexDirection="column" paddingLeft={1}>
97
+ <Box flexDirection="column" flexGrow={1}>
80
98
  {loading ? (
81
99
  <Text color="gray">Loading...</Text>
82
100
  ) : items.length === 0 ? (
83
101
  <Text color="gray">No items</Text>
84
102
  ) : (
85
- items.map((item, i) => (
86
- <Text key={i}>
103
+ visibleItems.map((item, i) => (
104
+ <Text key={scroll + i}>
87
105
  <Text color="green">• </Text>
88
106
  {item}
89
107
  </Text>
@@ -91,10 +109,8 @@ export function Memory() {
91
109
  )}
92
110
  </Box>
93
111
 
94
- <Box marginTop={1}>
95
- <Text color="gray">
96
- ←/→: switch | e: edit | {items.length} item(s)
97
- </Text>
112
+ <Box>
113
+ <Text color="gray">←/→: switch | e: edit{maxScroll > 0 ? " | ↑↓: scroll" : ""}</Text>
98
114
  </Box>
99
115
  </Box>
100
116
  );
@@ -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
@@ -11,7 +11,11 @@ const files: { key: PermissionFile; label: string; path: string }[] = [
11
11
  { key: "settings", label: "Settings", path: CONFIG.files.settings },
12
12
  ];
13
13
 
14
- export function Permissions() {
14
+ interface PermissionsProps {
15
+ height?: number;
16
+ }
17
+
18
+ export function Permissions({ height = 15 }: PermissionsProps) {
15
19
  const [selected, setSelected] = useState<PermissionFile>("commands");
16
20
  const [content, setContent] = useState<string>("");
17
21
  const [loading, setLoading] = useState(true);
@@ -12,13 +12,19 @@ const files: { key: PreferenceFile; label: string; path: string }[] = [
12
12
  { key: "antiPatterns", label: "Anti-Patterns", path: CONFIG.files.antiPatterns },
13
13
  ];
14
14
 
15
- export function Preferences() {
15
+ interface PreferencesProps {
16
+ height?: number;
17
+ }
18
+
19
+ export function Preferences({ height = 15 }: PreferencesProps) {
16
20
  const [selected, setSelected] = useState<PreferenceFile>("tools");
17
21
  const [content, setContent] = useState<string>("");
18
22
  const [loading, setLoading] = useState(true);
23
+ const [scroll, setScroll] = useState(0);
19
24
 
20
25
  useEffect(() => {
21
26
  loadContent();
27
+ setScroll(0);
22
28
  }, [selected]);
23
29
 
24
30
  async function loadContent() {
@@ -31,14 +37,24 @@ export function Preferences() {
31
37
  setLoading(false);
32
38
  }
33
39
 
40
+ const lines = content.split("\n");
41
+ const contentHeight = height - 3;
42
+ const maxScroll = Math.max(0, lines.length - contentHeight);
43
+
34
44
  useInput((input, key) => {
35
45
  if (key.leftArrow || input === "h") {
36
46
  const idx = files.findIndex((f) => f.key === selected);
37
- setSelected(files[(idx - 1 + files.length) % files.length].key);
47
+ setSelected(files[(idx - 1 + files.length) % files.length]!.key);
38
48
  }
39
49
  if (key.rightArrow || input === "l") {
40
50
  const idx = files.findIndex((f) => f.key === selected);
41
- setSelected(files[(idx + 1) % files.length].key);
51
+ setSelected(files[(idx + 1) % files.length]!.key);
52
+ }
53
+ if (key.upArrow || input === "k") {
54
+ setScroll((s) => Math.max(0, s - 1));
55
+ }
56
+ if (key.downArrow || input === "j") {
57
+ setScroll((s) => Math.min(maxScroll, s + 1));
42
58
  }
43
59
  if (input === "e") {
44
60
  const file = files.find((f) => f.key === selected);
@@ -53,8 +69,10 @@ export function Preferences() {
53
69
  }
54
70
  });
55
71
 
72
+ const visibleLines = lines.slice(scroll, scroll + contentHeight);
73
+
56
74
  return (
57
- <Box flexDirection="column">
75
+ <Box flexDirection="column" height={height}>
58
76
  <Box marginBottom={1}>
59
77
  {files.map((file) => (
60
78
  <Box key={file.key} marginRight={2}>
@@ -67,22 +85,25 @@ export function Preferences() {
67
85
  </Text>
68
86
  </Box>
69
87
  ))}
88
+ {maxScroll > 0 && (
89
+ <Text color="gray">[{scroll + 1}-{Math.min(scroll + contentHeight, lines.length)}/{lines.length}]</Text>
90
+ )}
70
91
  </Box>
71
92
 
72
- <Box flexDirection="column" paddingLeft={1}>
93
+ <Box flexDirection="column" flexGrow={1}>
73
94
  {loading ? (
74
95
  <Text color="gray">Loading...</Text>
75
96
  ) : (
76
- content.split("\n").map((line, i) => (
77
- <Text key={i} color={line.startsWith("#") ? "cyan" : undefined}>
78
- {line}
97
+ visibleLines.map((line, i) => (
98
+ <Text key={scroll + i} color={line.startsWith("#") ? "cyan" : undefined}>
99
+ {line || " "}
79
100
  </Text>
80
101
  ))
81
102
  )}
82
103
  </Box>
83
104
 
84
- <Box marginTop={1}>
85
- <Text color="gray">←/→: switch | e: edit in $EDITOR</Text>
105
+ <Box>
106
+ <Text color="gray">←/→: switch | e: edit{maxScroll > 0 ? " | ↑↓: scroll" : ""}</Text>
86
107
  </Box>
87
108
  </Box>
88
109
  );
@@ -6,13 +6,14 @@ import { CONFIG } from "../../core/config";
6
6
 
7
7
  interface PreviewProps {
8
8
  onClose: () => void;
9
+ height?: number;
9
10
  }
10
11
 
11
- export function Preview({ onClose }: PreviewProps) {
12
+ export function Preview({ onClose, height = 20 }: PreviewProps) {
12
13
  const [content, setContent] = useState<string[]>([]);
13
14
  const [scroll, setScroll] = useState(0);
14
15
  const [loading, setLoading] = useState(true);
15
- const maxLines = 20;
16
+ const maxLines = Math.max(5, height - 4);
16
17
 
17
18
  useEffect(() => {
18
19
  loadPreview();
@@ -54,7 +55,7 @@ export function Preview({ onClose }: PreviewProps) {
54
55
  : 100;
55
56
 
56
57
  return (
57
- <Box flexDirection="column" borderStyle="single" borderColor="cyan" padding={1}>
58
+ <Box flexDirection="column" borderStyle="single" borderColor="cyan" padding={1} height={height}>
58
59
  <Box marginBottom={1}>
59
60
  <Text bold color="cyan">CLAUDE.md Preview</Text>
60
61
  <Text color="gray"> ({content.length} lines)</Text>
@@ -4,10 +4,15 @@ import { parseMarkdown } from "../../core/parser";
4
4
  import { thinkPath, CONFIG } from "../../core/config";
5
5
  import { spawn } from "child_process";
6
6
 
7
- export function Profile() {
7
+ interface ProfileProps {
8
+ height?: number;
9
+ }
10
+
11
+ export function Profile({ height = 15 }: ProfileProps) {
8
12
  const [content, setContent] = useState<string>("");
9
13
  const [name, setName] = useState<string>("");
10
14
  const [loading, setLoading] = useState(true);
15
+ const [scroll, setScroll] = useState(0);
11
16
 
12
17
  useEffect(() => {
13
18
  loadProfile();
@@ -22,7 +27,11 @@ export function Profile() {
22
27
  setLoading(false);
23
28
  }
24
29
 
25
- useInput((input) => {
30
+ const lines = content.split("\n");
31
+ const contentHeight = height - 3; // header + footer
32
+ const maxScroll = Math.max(0, lines.length - contentHeight);
33
+
34
+ useInput((input, key) => {
26
35
  if (input === "e") {
27
36
  const editor = process.env.EDITOR || "vi";
28
37
  spawn(editor, [thinkPath(CONFIG.files.profile)], {
@@ -31,31 +40,40 @@ export function Profile() {
31
40
  loadProfile();
32
41
  });
33
42
  }
43
+ if (key.upArrow || input === "k") {
44
+ setScroll((s) => Math.max(0, s - 1));
45
+ }
46
+ if (key.downArrow || input === "j") {
47
+ setScroll((s) => Math.min(maxScroll, s + 1));
48
+ }
34
49
  });
35
50
 
36
51
  if (loading) {
37
52
  return <Text color="gray">Loading...</Text>;
38
53
  }
39
54
 
55
+ const visibleLines = lines.slice(scroll, scroll + contentHeight);
56
+
40
57
  return (
41
- <Box flexDirection="column">
58
+ <Box flexDirection="column" height={height}>
42
59
  <Box marginBottom={1}>
43
- <Text bold color="green">
44
- Profile
45
- </Text>
60
+ <Text bold color="green">Profile</Text>
46
61
  {name && <Text color="gray"> - {name}</Text>}
62
+ {maxScroll > 0 && (
63
+ <Text color="gray"> [{scroll + 1}-{Math.min(scroll + contentHeight, lines.length)}/{lines.length}]</Text>
64
+ )}
47
65
  </Box>
48
66
 
49
- <Box flexDirection="column" paddingLeft={1}>
50
- {content.split("\n").map((line, i) => (
51
- <Text key={i} color={line.startsWith("#") ? "cyan" : undefined}>
52
- {line}
67
+ <Box flexDirection="column" flexGrow={1}>
68
+ {visibleLines.map((line, i) => (
69
+ <Text key={scroll + i} color={line.startsWith("#") ? "cyan" : undefined}>
70
+ {line || " "}
53
71
  </Text>
54
72
  ))}
55
73
  </Box>
56
74
 
57
- <Box marginTop={1}>
58
- <Text color="gray">Press 'e' to edit in $EDITOR</Text>
75
+ <Box>
76
+ <Text color="gray">e: edit{maxScroll > 0 ? " | ↑↓/jk: scroll" : ""}</Text>
59
77
  </Box>
60
78
  </Box>
61
79
  );
@@ -0,0 +1,57 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+
4
+ interface ScrollableBoxProps {
5
+ children: React.ReactNode;
6
+ height: number;
7
+ showScrollbar?: boolean;
8
+ }
9
+
10
+ export function ScrollableBox({
11
+ children,
12
+ height,
13
+ showScrollbar = true,
14
+ }: ScrollableBoxProps) {
15
+ const [scrollOffset, setScrollOffset] = useState(0);
16
+ const [contentHeight, setContentHeight] = useState(0);
17
+
18
+ // Convert children to array of lines for scrolling
19
+ const lines = React.Children.toArray(children);
20
+ const maxScroll = Math.max(0, lines.length - height);
21
+
22
+ useInput((input, key) => {
23
+ if (key.upArrow || input === "k") {
24
+ setScrollOffset((s) => Math.max(0, s - 1));
25
+ }
26
+ if (key.downArrow || input === "j") {
27
+ setScrollOffset((s) => Math.min(maxScroll, s + 1));
28
+ }
29
+ if (key.pageUp) {
30
+ setScrollOffset((s) => Math.max(0, s - height));
31
+ }
32
+ if (key.pageDown) {
33
+ setScrollOffset((s) => Math.min(maxScroll, s + height));
34
+ }
35
+ });
36
+
37
+ const visibleLines = lines.slice(scrollOffset, scrollOffset + height);
38
+ const scrollPercent =
39
+ maxScroll > 0 ? Math.round((scrollOffset / maxScroll) * 100) : 100;
40
+
41
+ return (
42
+ <Box flexDirection="row" height={height}>
43
+ <Box flexDirection="column" flexGrow={1}>
44
+ {visibleLines}
45
+ {/* Pad remaining space */}
46
+ {Array.from({ length: height - visibleLines.length }).map((_, i) => (
47
+ <Text key={`pad-${i}`}> </Text>
48
+ ))}
49
+ </Box>
50
+ {showScrollbar && maxScroll > 0 && (
51
+ <Box flexDirection="column" marginLeft={1}>
52
+ <Text color="gray">{scrollPercent.toString().padStart(3)}%</Text>
53
+ </Box>
54
+ )}
55
+ </Box>
56
+ );
57
+ }
@@ -7,6 +7,7 @@ import { thinkPath, CONFIG } from "../../core/config";
7
7
 
8
8
  interface SearchProps {
9
9
  onClose: () => void;
10
+ height?: number;
10
11
  }
11
12
 
12
13
  interface SearchResult {
@@ -26,7 +27,7 @@ const searchFiles = [
26
27
  { name: "workflows", path: CONFIG.files.workflows },
27
28
  ];
28
29
 
29
- export function Search({ onClose }: SearchProps) {
30
+ export function Search({ onClose, height = 20 }: SearchProps) {
30
31
  const [query, setQuery] = useState("");
31
32
  const [results, setResults] = useState<SearchResult[]>([]);
32
33
  const [selected, setSelected] = useState(0);
@@ -13,7 +13,11 @@ interface SkillInfo {
13
13
  path: string;
14
14
  }
15
15
 
16
- export function Skills() {
16
+ interface SkillsProps {
17
+ height?: number;
18
+ }
19
+
20
+ export function Skills({ height = 15 }: SkillsProps) {
17
21
  const [skills, setSkills] = useState<SkillInfo[]>([]);
18
22
  const [selectedIndex, setSelectedIndex] = useState(0);
19
23
  const [loading, setLoading] = useState(true);
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect } from "react";
2
- import { Box, Text } from "ink";
2
+ import { Box, Text, useStdout } from "ink";
3
3
  import { existsSync, statSync } from "fs";
4
4
  import { readFile } from "fs/promises";
5
5
  import { CONFIG, thinkPath } from "../../core/config";
@@ -43,6 +43,10 @@ export function StatusBar({ message }: StatusBarProps) {
43
43
  }
44
44
  }
45
45
 
46
+ const { stdout } = useStdout();
47
+ const width = stdout?.columns ?? 80;
48
+ const isNarrow = width < 60;
49
+
46
50
  return (
47
51
  <Box borderStyle="single" borderColor="gray" paddingX={1}>
48
52
  <Box flexGrow={1}>
@@ -52,18 +56,18 @@ export function StatusBar({ message }: StatusBarProps) {
52
56
  <Text color="gray">Ready</Text>
53
57
  )}
54
58
  </Box>
55
- <Box marginLeft={2}>
59
+ <Box marginLeft={1}>
56
60
  <Text color="green">{learningsCount}</Text>
57
- <Text color="gray"> learnings</Text>
61
+ <Text color="gray">{isNarrow ? "L" : " learnings"}</Text>
58
62
  </Box>
59
63
  {pendingCount > 0 && (
60
- <Box marginLeft={2}>
64
+ <Box marginLeft={1}>
61
65
  <Text color="yellow">{pendingCount}</Text>
62
- <Text color="gray"> pending</Text>
66
+ <Text color="gray">{isNarrow ? "P" : " pending"}</Text>
63
67
  </Box>
64
68
  )}
65
- {lastSync && (
66
- <Box marginLeft={2}>
69
+ {lastSync && !isNarrow && (
70
+ <Box marginLeft={1}>
67
71
  <Text color="gray">synced {lastSync}</Text>
68
72
  </Box>
69
73
  )}