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.
- package/.claude/settings.local.json +4 -1
- package/CHANGELOG.md +20 -0
- package/package.json +1 -1
- package/src/tui/App.tsx +114 -67
- package/src/tui/components/Agents.tsx +5 -1
- package/src/tui/components/Automation.tsx +5 -1
- package/src/tui/components/FullScreen.tsx +72 -0
- package/src/tui/components/Help.tsx +48 -46
- package/src/tui/components/Memory.tsx +27 -11
- package/src/tui/components/Navigation.tsx +50 -12
- package/src/tui/components/Permissions.tsx +5 -1
- package/src/tui/components/Preferences.tsx +31 -10
- package/src/tui/components/Preview.tsx +4 -3
- package/src/tui/components/Profile.tsx +30 -12
- package/src/tui/components/ScrollableBox.tsx +57 -0
- package/src/tui/components/Search.tsx +2 -1
- package/src/tui/components/Skills.tsx +5 -1
- package/src/tui/components/StatusBar.tsx +11 -7
|
@@ -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
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
|
-
<
|
|
74
|
-
<Box
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<
|
|
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
|
-
</
|
|
101
|
+
</FullScreen>
|
|
89
102
|
);
|
|
90
103
|
}
|
|
91
104
|
|
|
92
|
-
// Render modals
|
|
105
|
+
// Render modals in fullscreen
|
|
93
106
|
if (modal === "help") {
|
|
94
|
-
return
|
|
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
|
-
<
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
<
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
<
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
<
|
|
150
|
-
<
|
|
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
|
-
|
|
166
|
-
<StatusBar message={statusMessage} />
|
|
167
|
-
</Box>
|
|
176
|
+
<Navigation currentSection={section} onSectionChange={setSection} />
|
|
168
177
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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">
|
|
49
|
-
<Text><Text color="green">
|
|
50
|
-
<Text><Text color="green">
|
|
51
|
-
<Text><Text color="green"
|
|
52
|
-
<Text><Text color="green"
|
|
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
|
-
|
|
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={
|
|
60
|
-
<Text color="
|
|
61
|
-
<Text color="
|
|
62
|
-
<Text color="
|
|
63
|
-
<Text color="
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
<Text color="
|
|
68
|
-
<Text color="
|
|
69
|
-
<Text color="
|
|
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="
|
|
73
|
-
<Text color="
|
|
74
|
-
<Text color="
|
|
75
|
-
<Text color="
|
|
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
|
-
|
|
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]
|
|
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]
|
|
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"
|
|
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
|
-
|
|
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
|
|
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]
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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]
|
|
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"
|
|
93
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
73
94
|
{loading ? (
|
|
74
95
|
<Text color="gray">Loading...</Text>
|
|
75
96
|
) : (
|
|
76
|
-
|
|
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
|
|
85
|
-
<Text color="gray">←/→: switch | e: edit
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
50
|
-
{
|
|
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
|
|
58
|
-
<Text color="gray">
|
|
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
|
-
|
|
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={
|
|
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={
|
|
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={
|
|
69
|
+
{lastSync && !isNarrow && (
|
|
70
|
+
<Box marginLeft={1}>
|
|
67
71
|
<Text color="gray">synced {lastSync}</Text>
|
|
68
72
|
</Box>
|
|
69
73
|
)}
|